Skip to content

Commit ee0c2b6

Browse files
jackleeioclaudejackwener
authored
feat(youtube): add search filters — --type shorts/video/channel, --upload, --sort (#616)
* 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * fix(youtube): prevent duplicate sp= params and remove redundant Shorts URL rewrite - 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. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: jackwener <jakevingoo@gmail.com>
1 parent 0105752 commit ee0c2b6

1 file changed

Lines changed: 57 additions & 17 deletions

File tree

src/clis/youtube/search.ts

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,41 +12,81 @@ cli({
1212
args: [
1313
{ name: 'query', required: true, positional: true, help: 'Search query' },
1414
{ name: 'limit', type: 'int', default: 20, help: 'Max results (max 50)' },
15+
{ name: 'type', default: '', help: 'Filter type: shorts, video, channel, playlist' },
16+
{ name: 'upload', default: '', help: 'Upload date: hour, today, week, month, year' },
17+
{ name: 'sort', default: '', help: 'Sort by: relevance, date, views, rating' },
1518
],
16-
columns: ['rank', 'title', 'channel', 'views', 'duration', 'url'],
19+
columns: ['rank', 'title', 'channel', 'views', 'duration', 'published', 'url'],
1720
func: async (page, kwargs) => {
1821
const limit = Math.min(kwargs.limit || 20, 50);
19-
await page.goto('https://www.youtube.com');
20-
await page.wait(2);
22+
const query = encodeURIComponent(kwargs.query);
23+
24+
// Build search URL with filter params
25+
// YouTube uses sp= parameter for filters — we use the URL approach for reliability
26+
const spMap: Record<string, string> = {
27+
// type filters
28+
'shorts': 'EgIQCQ%3D%3D', // Shorts (type=9)
29+
'video': 'EgIQAQ%3D%3D',
30+
'channel': 'EgIQAg%3D%3D',
31+
'playlist': 'EgIQAw%3D%3D',
32+
// upload date filters (can be combined with type via URL)
33+
'hour': 'EgIIAQ%3D%3D',
34+
'today': 'EgIIAg%3D%3D',
35+
'week': 'EgIIAw%3D%3D',
36+
'month': 'EgIIBA%3D%3D',
37+
'year': 'EgIIBQ%3D%3D',
38+
};
39+
const sortMap: Record<string, string> = {
40+
'date': 'CAI%3D',
41+
'views': 'CAM%3D',
42+
'rating': 'CAE%3D',
43+
};
44+
45+
// YouTube only supports a single sp= parameter — pick the most specific filter.
46+
// Priority: type > upload > sort (type is the most common use case)
47+
let sp = '';
48+
if (kwargs.type && spMap[kwargs.type]) sp = spMap[kwargs.type];
49+
else if (kwargs.upload && spMap[kwargs.upload]) sp = spMap[kwargs.upload];
50+
else if (kwargs.sort && sortMap[kwargs.sort]) sp = sortMap[kwargs.sort];
51+
52+
let url = `https://www.youtube.com/results?search_query=${query}`;
53+
if (sp) url += `&sp=${sp}`;
54+
55+
await page.goto(url);
56+
await page.wait(3);
2157
const data = await page.evaluate(`
2258
(async () => {
23-
const cfg = window.ytcfg?.data_ || {};
24-
const apiKey = cfg.INNERTUBE_API_KEY;
25-
const context = cfg.INNERTUBE_CONTEXT;
26-
if (!apiKey || !context) return {error: 'YouTube config not found'};
59+
const data = window.ytInitialData;
60+
if (!data) return {error: 'YouTube data not found'};
2761
28-
const resp = await fetch('/youtubei/v1/search?key=' + apiKey + '&prettyPrint=false', {
29-
method: 'POST', credentials: 'include',
30-
headers: {'Content-Type': 'application/json'},
31-
body: JSON.stringify({context, query: '${kwargs.query.replace(/'/g, "\\'")}'})
32-
});
33-
if (!resp.ok) return {error: 'HTTP ' + resp.status};
34-
35-
const data = await resp.json();
3662
const contents = data.contents?.twoColumnSearchResultsRenderer?.primaryContents?.sectionListRenderer?.contents || [];
3763
const videos = [];
3864
for (const section of contents) {
39-
for (const item of (section.itemSectionRenderer?.contents || [])) {
40-
if (item.videoRenderer && videos.length < ${limit}) {
65+
const items = section.itemSectionRenderer?.contents || section.reelShelfRenderer?.items || [];
66+
for (const item of items) {
67+
if (videos.length >= ${limit}) break;
68+
if (item.videoRenderer) {
4169
const v = item.videoRenderer;
4270
videos.push({
4371
rank: videos.length + 1,
4472
title: v.title?.runs?.[0]?.text || '',
4573
channel: v.ownerText?.runs?.[0]?.text || '',
4674
views: v.viewCountText?.simpleText || v.shortViewCountText?.simpleText || '',
4775
duration: v.lengthText?.simpleText || 'LIVE',
76+
published: v.publishedTimeText?.simpleText || '',
4877
url: 'https://www.youtube.com/watch?v=' + v.videoId
4978
});
79+
} else if (item.reelItemRenderer) {
80+
const r = item.reelItemRenderer;
81+
videos.push({
82+
rank: videos.length + 1,
83+
title: r.headline?.simpleText || '',
84+
channel: r.navigationEndpoint?.reelWatchEndpoint?.overlay?.reelPlayerOverlayRenderer?.reelPlayerHeaderSupportedRenderers?.reelPlayerHeaderRenderer?.channelTitleText?.runs?.[0]?.text || '',
85+
views: r.viewCountText?.simpleText || '',
86+
duration: 'SHORT',
87+
published: r.publishedTimeText?.simpleText || '',
88+
url: 'https://www.youtube.com/shorts/' + r.videoId
89+
});
5090
}
5191
}
5292
}

0 commit comments

Comments
 (0)