mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
fix merge issues
This commit is contained in:
parent
33fa8a0826
commit
7bd4779745
1 changed files with 41 additions and 96 deletions
|
|
@ -67,35 +67,14 @@ export interface YouTubeExtractionResult {
|
||||||
// Constants — matching the Kotlin extractor exactly
|
// Constants — matching the Kotlin extractor exactly
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// Innertube client configs — we use Android (no cipher, direct URLs)
|
// Used for all GET requests (watch page, HLS manifest fetch)
|
||||||
// and web as fallback (may need cipher decode)
|
const DEFAULT_USER_AGENT =
|
||||||
// Note: ?key= param was deprecated by YouTube in mid-2023 and is no longer sent.
|
'Mozilla/5.0 (Linux; Android 12; Android TV) AppleWebKit/537.36 ' +
|
||||||
const INNERTUBE_URL = 'https://www.youtube.com/youtubei/v1/player';
|
'(KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36';
|
||||||
|
|
||||||
// Android client gives direct URLs without cipher obfuscation
|
const DEFAULT_HEADERS: Record<string, string> = {
|
||||||
const ANDROID_CLIENT_CONTEXT = {
|
'accept-language': 'en-US,en;q=0.9',
|
||||||
client: {
|
'user-agent': DEFAULT_USER_AGENT,
|
||||||
clientName: 'ANDROID',
|
|
||||||
clientVersion: '19.44.41',
|
|
||||||
androidSdkVersion: 30,
|
|
||||||
userAgent:
|
|
||||||
'com.google.android.youtube/19.44.41 (Linux; U; Android 11) gzip',
|
|
||||||
hl: 'en',
|
|
||||||
gl: 'US',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// iOS client as secondary fallback
|
|
||||||
const IOS_CLIENT_CONTEXT = {
|
|
||||||
client: {
|
|
||||||
clientName: 'IOS',
|
|
||||||
clientVersion: '19.45.4',
|
|
||||||
deviceModel: 'iPhone16,2',
|
|
||||||
userAgent:
|
|
||||||
'com.google.ios.youtube/19.45.4 (iPhone16,2; U; CPU iOS 17_5_1 like Mac OS X)',
|
|
||||||
hl: 'en',
|
|
||||||
gl: 'US',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const PREFERRED_ADAPTIVE_CLIENT = 'android_vr';
|
const PREFERRED_ADAPTIVE_CLIENT = 'android_vr';
|
||||||
|
|
@ -349,36 +328,37 @@ async function fetchWatchConfig(videoId: string): Promise<WatchConfig> {
|
||||||
|
|
||||||
async function fetchPlayerResponse(
|
async function fetchPlayerResponse(
|
||||||
videoId: string,
|
videoId: string,
|
||||||
context: object,
|
client: ClientDef,
|
||||||
userAgent: string,
|
apiKey: string | null,
|
||||||
clientNameId: string = '3'
|
visitorData: string | null,
|
||||||
): Promise<InnertubePlayerResponse | null> {
|
): Promise<PlayerResponse | null> {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
||||||
|
|
||||||
try {
|
const endpoint = apiKey
|
||||||
const body = {
|
? `https://www.youtube.com/youtubei/v1/player?key=${encodeURIComponent(apiKey)}&prettyPrint=false`
|
||||||
videoId,
|
: `https://www.youtube.com/youtubei/v1/player?prettyPrint=false`;
|
||||||
context,
|
|
||||||
contentCheckOk: true,
|
|
||||||
racyCheckOk: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch(
|
const headers: Record<string, string> = {
|
||||||
`${INNERTUBE_URL}?prettyPrint=false`,
|
...DEFAULT_HEADERS,
|
||||||
{
|
'content-type': 'application/json',
|
||||||
method: 'POST',
|
'origin': 'https://www.youtube.com',
|
||||||
headers: {
|
'referer': `https://www.youtube.com/watch?v=${videoId}`,
|
||||||
'Content-Type': 'application/json',
|
'x-youtube-client-name': client.id,
|
||||||
'User-Agent': userAgent,
|
'x-youtube-client-version': client.version,
|
||||||
'X-YouTube-Client-Name': clientNameId,
|
'user-agent': client.userAgent,
|
||||||
'Origin': 'https://www.youtube.com',
|
};
|
||||||
'Referer': `https://www.youtube.com/watch?v=${videoId}`,
|
if (visitorData) headers['x-goog-visitor-id'] = visitorData;
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
const body = JSON.stringify({
|
||||||
signal: controller.signal,
|
videoId,
|
||||||
}
|
contentCheckOk: true,
|
||||||
);
|
racyCheckOk: true,
|
||||||
|
context: { client: client.context },
|
||||||
|
playbackContext: {
|
||||||
|
contentPlaybackContext: { html5Preference: 'HTML5_PREF_WANTS' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(endpoint, {
|
const res = await fetch(endpoint, {
|
||||||
|
|
@ -617,48 +597,13 @@ export class YouTubeExtractor {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('YouTubeExtractor', `Extracting for videoId=${videoId} platform=${platform ?? 'unknown'}`);
|
const effectivePlatform = platform ?? (Platform.OS === 'android' ? 'android' : 'ios');
|
||||||
|
|
||||||
const clients: Array<{ context: object; userAgent: string; name: string; clientNameId: string }> = [
|
for (let attempt = 1; attempt <= MAX_RETRIES + 1; attempt++) {
|
||||||
{
|
if (attempt > 1) {
|
||||||
name: 'ANDROID',
|
const delay = attempt * 300;
|
||||||
clientNameId: '3',
|
logger.info('YouTubeExtractor', `Retry attempt ${attempt}/${MAX_RETRIES + 1} after ${delay}ms`);
|
||||||
context: ANDROID_CLIENT_CONTEXT,
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
userAgent: 'com.google.android.youtube/19.44.41 (Linux; U; Android 11) gzip',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'IOS',
|
|
||||||
clientNameId: '5',
|
|
||||||
context: IOS_CLIENT_CONTEXT,
|
|
||||||
userAgent: 'com.google.ios.youtube/19.45.4 (iPhone16,2; U; CPU iOS 17_5_1 like Mac OS X)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'TVHTML5_EMBEDDED',
|
|
||||||
clientNameId: '85',
|
|
||||||
context: TVHTML5_EMBEDDED_CONTEXT,
|
|
||||||
userAgent: 'Mozilla/5.0 (SMART-TV; Linux; Tizen 6.0)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'WEB_EMBEDDED',
|
|
||||||
clientNameId: '56',
|
|
||||||
context: WEB_EMBEDDED_CONTEXT,
|
|
||||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let muxedFormats: InnertubeFormat[] = [];
|
|
||||||
let adaptiveFormats: InnertubeFormat[] = [];
|
|
||||||
let playerResponse: InnertubePlayerResponse | null = null;
|
|
||||||
|
|
||||||
for (const client of clients) {
|
|
||||||
logger.info('YouTubeExtractor', `Trying ${client.name} client...`);
|
|
||||||
const resp = await fetchPlayerResponse(videoId, client.context, client.userAgent, client.clientNameId);
|
|
||||||
if (!resp) continue;
|
|
||||||
|
|
||||||
const status = resp.playabilityStatus?.status;
|
|
||||||
if (status === 'UNPLAYABLE' || status === 'LOGIN_REQUIRED') {
|
|
||||||
logger.warn('YouTubeExtractor', `${client.name}: playabilityStatus=${status}`);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
const result = await this.extractOnce(videoId, effectivePlatform);
|
const result = await this.extractOnce(videoId, effectivePlatform);
|
||||||
if (result) return result;
|
if (result) return result;
|
||||||
|
|
@ -723,7 +668,7 @@ export class YouTubeExtractor {
|
||||||
// Priority: HLS > progressive muxed
|
// Priority: HLS > progressive muxed
|
||||||
// HLS manifests don't need validation — they're not CDN segment URLs
|
// HLS manifests don't need validation — they're not CDN segment URLs
|
||||||
if (bestHls) {
|
if (bestHls) {
|
||||||
logger.info('YouTubeExtractor', `Using HLS: ${summarizeUrl(bestHls.manifestUrl)}`);
|
logger.info('YouTubeExtractor', `Using HLS manifest: ${summarizeUrl(bestHls.manifestUrl)} ${bestHls.height}p`);
|
||||||
return {
|
return {
|
||||||
videoUrl: bestHls.manifestUrl,
|
videoUrl: bestHls.manifestUrl,
|
||||||
audioUrl: null,
|
audioUrl: null,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue