Files
wehub-resource-sync 9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

101 lines
4.3 KiB
JavaScript

/**
* YouTube search — innertube API via browser session.
*/
import { cli, Strategy } from '@jackwener/opencli/registry';
cli({
site: 'youtube',
name: 'search',
access: 'read',
description: 'Search YouTube videos',
domain: 'www.youtube.com',
strategy: Strategy.COOKIE,
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', 'published', 'url'],
func: async (page, kwargs) => {
const limit = Math.min(kwargs.limit || 20, 50);
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 = {
// 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 = {
'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 data = window.ytInitialData;
if (!data) return {error: 'YouTube data not found'};
const contents = data.contents?.twoColumnSearchResultsRenderer?.primaryContents?.sectionListRenderer?.contents || [];
const videos = [];
for (const section of contents) {
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
});
}
}
}
return videos;
})()
`);
if (!Array.isArray(data))
return [];
return data;
},
});