Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 57 additions & 17 deletions src/clis/youtube/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,41 +12,81 @@ 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'],
columns: ['rank', 'title', 'channel', 'views', 'duration', 'published', '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<string, string> = {
// 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<string, string> = {
'date': 'CAI%3D',
'views': 'CAM%3D',
'rating': 'CAE%3D',
};

// 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];

let url = `https://www.youtube.com/results?search_query=${query}`;
if (sp) url += `&sp=${sp}`;

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 data = window.ytInitialData;
if (!data) return {error: 'YouTube data 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 = 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,
title: v.title?.runs?.[0]?.text || '',
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) {
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',
published: r.publishedTimeText?.simpleText || '',
url: 'https://www.youtube.com/shorts/' + r.videoId
});
}
}
}
Expand Down