From 1d0e11b264f9ff513d4a17a0649103bd76a62b70 Mon Sep 17 00:00:00 2001 From: Jack Lee <280147597@qq.com> Date: Tue, 31 Mar 2026 00:03:37 +0800 Subject: [PATCH 1/3] feat(youtube): add --type shorts/video/channel, --upload, --sort filters Uses YouTube's native sp= filter params. Shorts = type 9 (sp=EgIQCQ). Also parses reelItemRenderer for Shorts results. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/clis/youtube/search.ts | 75 ++++++++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 16 deletions(-) diff --git a/src/clis/youtube/search.ts b/src/clis/youtube/search.ts index 9fc9d94c..4f71554c 100644 --- a/src/clis/youtube/search.ts +++ b/src/clis/youtube/search.ts @@ -12,32 +12,57 @@ cli({ args: [ { name: 'query', required: true, positional: true, help: 'Search query' }, { name: 'limit', type: 'int', default: 20, help: 'Max results (max 50)' }, + { name: 'type', default: '', help: 'Filter type: shorts, video, channel, playlist' }, + { name: 'upload', default: '', help: 'Upload date: hour, today, week, month, year' }, + { name: 'sort', default: '', help: 'Sort by: relevance, date, views, rating' }, ], columns: ['rank', 'title', 'channel', 'views', 'duration', 'url'], func: async (page, kwargs) => { const limit = Math.min(kwargs.limit || 20, 50); - await page.goto('https://www.youtube.com'); - await page.wait(2); + const query = encodeURIComponent(kwargs.query); + + // Build search URL with filter params + // YouTube uses sp= parameter for filters — we use the URL approach for reliability + const spMap: Record = { + // type filters + 'shorts': 'EgIQCQ%3D%3D', // Shorts (type=9) + 'video': 'EgIQAQ%3D%3D', + 'channel': 'EgIQAg%3D%3D', + 'playlist': 'EgIQAw%3D%3D', + // upload date filters (can be combined with type via URL) + 'hour': 'EgIIAQ%3D%3D', + 'today': 'EgIIAg%3D%3D', + 'week': 'EgIIAw%3D%3D', + 'month': 'EgIIBA%3D%3D', + 'year': 'EgIIBQ%3D%3D', + }; + const sortMap: Record = { + 'date': 'CAI%3D', + 'views': 'CAM%3D', + 'rating': 'CAE%3D', + }; + + let url = `https://www.youtube.com/results?search_query=${query}`; + if (kwargs.type && spMap[kwargs.type]) url += `&sp=${spMap[kwargs.type]}`; + else if (kwargs.upload && spMap[kwargs.upload]) url += `&sp=${spMap[kwargs.upload]}`; + if (kwargs.sort && sortMap[kwargs.sort]) url += `&sp=${sortMap[kwargs.sort]}`; + + const isShorts = kwargs.type === 'shorts'; + + await page.goto(url); + await page.wait(3); const data = await page.evaluate(` (async () => { - const cfg = window.ytcfg?.data_ || {}; - const apiKey = cfg.INNERTUBE_API_KEY; - const context = cfg.INNERTUBE_CONTEXT; - if (!apiKey || !context) return {error: 'YouTube config not found'}; - - const resp = await fetch('/youtubei/v1/search?key=' + apiKey + '&prettyPrint=false', { - method: 'POST', credentials: 'include', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({context, query: '${kwargs.query.replace(/'/g, "\\'")}'}) - }); - if (!resp.ok) return {error: 'HTTP ' + resp.status}; + const data = window.ytInitialData; + if (!data) return {error: 'YouTube data not found'}; - const data = await resp.json(); const contents = data.contents?.twoColumnSearchResultsRenderer?.primaryContents?.sectionListRenderer?.contents || []; const videos = []; for (const section of contents) { - for (const item of (section.itemSectionRenderer?.contents || [])) { - if (item.videoRenderer && videos.length < ${limit}) { + const items = section.itemSectionRenderer?.contents || section.reelShelfRenderer?.items || []; + for (const item of items) { + if (videos.length >= ${limit}) break; + if (item.videoRenderer) { const v = item.videoRenderer; videos.push({ rank: videos.length + 1, @@ -47,6 +72,16 @@ cli({ duration: v.lengthText?.simpleText || 'LIVE', url: 'https://www.youtube.com/watch?v=' + v.videoId }); + } else if (item.reelItemRenderer) { + const r = item.reelItemRenderer; + videos.push({ + rank: videos.length + 1, + title: r.headline?.simpleText || '', + channel: r.navigationEndpoint?.reelWatchEndpoint?.overlay?.reelPlayerOverlayRenderer?.reelPlayerHeaderSupportedRenderers?.reelPlayerHeaderRenderer?.channelTitleText?.runs?.[0]?.text || '', + views: r.viewCountText?.simpleText || '', + duration: 'SHORT', + url: 'https://www.youtube.com/shorts/' + r.videoId + }); } } } @@ -54,6 +89,14 @@ cli({ })() `); if (!Array.isArray(data)) return []; + + // For Shorts: convert URL to /shorts/ format + if (isShorts) { + return data.map((v: any) => ({ + ...v, + url: v.url.replace('youtube.com/watch?v=', 'youtube.com/shorts/'), + })); + } return data; }, }); From ba68e5b2af2e4938c82af25945da94e9d0eaa591 Mon Sep 17 00:00:00 2001 From: Jack Lee <280147597@qq.com> Date: Tue, 31 Mar 2026 09:24:05 +0800 Subject: [PATCH 2/3] feat(youtube): add published time to search results Shows when video was uploaded (e.g. "8h ago", "4d ago", "3mo ago"). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/clis/youtube/search.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/clis/youtube/search.ts b/src/clis/youtube/search.ts index 4f71554c..2553daa2 100644 --- a/src/clis/youtube/search.ts +++ b/src/clis/youtube/search.ts @@ -16,7 +16,7 @@ cli({ { name: 'upload', default: '', help: 'Upload date: hour, today, week, month, year' }, { name: 'sort', default: '', help: 'Sort by: relevance, date, views, rating' }, ], - columns: ['rank', 'title', 'channel', 'views', 'duration', 'url'], + columns: ['rank', 'title', 'channel', 'views', 'duration', 'published', 'url'], func: async (page, kwargs) => { const limit = Math.min(kwargs.limit || 20, 50); const query = encodeURIComponent(kwargs.query); @@ -70,6 +70,7 @@ cli({ channel: v.ownerText?.runs?.[0]?.text || '', views: v.viewCountText?.simpleText || v.shortViewCountText?.simpleText || '', duration: v.lengthText?.simpleText || 'LIVE', + published: v.publishedTimeText?.simpleText || '', url: 'https://www.youtube.com/watch?v=' + v.videoId }); } else if (item.reelItemRenderer) { @@ -80,6 +81,7 @@ cli({ channel: r.navigationEndpoint?.reelWatchEndpoint?.overlay?.reelPlayerOverlayRenderer?.reelPlayerHeaderSupportedRenderers?.reelPlayerHeaderRenderer?.channelTitleText?.runs?.[0]?.text || '', views: r.viewCountText?.simpleText || '', duration: 'SHORT', + published: r.publishedTimeText?.simpleText || '', url: 'https://www.youtube.com/shorts/' + r.videoId }); } From a819c98c6de5a0a9965ee6cb3775fc8c0970a1ca Mon Sep 17 00:00:00 2001 From: jackwener Date: Wed, 1 Apr 2026 01:29:15 +0800 Subject: [PATCH 3/3] fix(youtube): prevent duplicate sp= params and remove redundant Shorts URL rewrite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - YouTube only supports one sp= parameter; using multiple causes unpredictable behavior. Pick the most specific filter with priority: type > upload > sort. - Remove the post-processing Shorts URL rewrite — the reelItemRenderer branch already generates /shorts/ URLs directly. --- src/clis/youtube/search.ts | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/clis/youtube/search.ts b/src/clis/youtube/search.ts index 2553daa2..e953f065 100644 --- a/src/clis/youtube/search.ts +++ b/src/clis/youtube/search.ts @@ -42,12 +42,15 @@ cli({ 'rating': 'CAE%3D', }; - let url = `https://www.youtube.com/results?search_query=${query}`; - if (kwargs.type && spMap[kwargs.type]) url += `&sp=${spMap[kwargs.type]}`; - else if (kwargs.upload && spMap[kwargs.upload]) url += `&sp=${spMap[kwargs.upload]}`; - if (kwargs.sort && sortMap[kwargs.sort]) url += `&sp=${sortMap[kwargs.sort]}`; + // YouTube only supports a single sp= parameter — pick the most specific filter. + // Priority: type > upload > sort (type is the most common use case) + let sp = ''; + if (kwargs.type && spMap[kwargs.type]) sp = spMap[kwargs.type]; + else if (kwargs.upload && spMap[kwargs.upload]) sp = spMap[kwargs.upload]; + else if (kwargs.sort && sortMap[kwargs.sort]) sp = sortMap[kwargs.sort]; - const isShorts = kwargs.type === 'shorts'; + let url = `https://www.youtube.com/results?search_query=${query}`; + if (sp) url += `&sp=${sp}`; await page.goto(url); await page.wait(3); @@ -91,14 +94,6 @@ cli({ })() `); if (!Array.isArray(data)) return []; - - // For Shorts: convert URL to /shorts/ format - if (isShorts) { - return data.map((v: any) => ({ - ...v, - url: v.url.replace('youtube.com/watch?v=', 'youtube.com/shorts/'), - })); - } return data; }, });