diff --git a/play-dl/YouTube/utils/parser.ts b/play-dl/YouTube/utils/parser.ts index 7135a7c..03cd40c 100644 --- a/play-dl/YouTube/utils/parser.ts +++ b/play-dl/YouTube/utils/parser.ts @@ -2,11 +2,23 @@ import { YouTubeVideo } from '../classes/Video'; import { YouTubePlayList } from '../classes/Playlist'; import { YouTubeChannel } from '../classes/Channel'; import { YouTube } from '..'; +import { YouTubeThumbnail } from '../classes/Thumbnail'; +import { writeFileSync } from 'fs'; + +const BLURRED_THUMBNAILS = [ + '-oaymwEpCOADEI4CSFryq4qpAxsIARUAAAAAGAElAADIQj0AgKJDeAHtAZmZGUI=', + '-oaymwEiCOADEI4CSFXyq4qpAxQIARUAAIhCGAFwAcABBu0BmZkZQg==', + '-oaymwEiCOgCEMoBSFXyq4qpAxQIARUAAIhCGAFwAcABBu0BZmbmQQ==', + '-oaymwEiCNAFEJQDSFXyq4qpAxQIARUAAIhCGAFwAcABBu0BZmZmQg==', + '-oaymwEdCNAFEJQDSFryq4qpAw8IARUAAIhCGAHtAWZmZkI=', + '-oaymwEdCNACELwBSFryq4qpAw8IARUAAIhCGAHtAT0K10E=' +]; export interface ParseSearchInterface { type?: 'video' | 'playlist' | 'channel'; limit?: number; language?: string; + unblurNSFWThumbnails?: boolean; } export interface thumbnail { @@ -25,6 +37,7 @@ export function ParseSearchResult(html: string, options?: ParseSearchInterface): if (!options) options = { type: 'video', limit: 0 }; else if (!options.type) options.type = 'video'; const hasLimit = typeof options.limit === 'number' && options.limit > 0; + options.unblurNSFWThumbnails ??= false; const data = html .split('var ytInitialData = ')?.[1] @@ -35,13 +48,17 @@ export function ParseSearchResult(html: string, options?: ParseSearchInterface): const details = json_data.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents[0] .itemSectionRenderer.contents; + writeFileSync('results.json', JSON.stringify(details)); for (const detail of details) { if (hasLimit && results.length === options.limit) break; if (!detail.videoRenderer && !detail.channelRenderer && !detail.playlistRenderer) continue; switch (options.type) { case 'video': { const parsed = parseVideo(detail); - if (parsed) results.push(parsed); + if (parsed) { + if (options.unblurNSFWThumbnails) parsed.thumbnails.forEach(unblurThumbnail); + results.push(parsed); + } break; } case 'channel': { @@ -51,7 +68,10 @@ export function ParseSearchResult(html: string, options?: ParseSearchInterface): } case 'playlist': { const parsed = parsePlaylist(detail); - if (parsed) results.push(parsed); + if (parsed) { + if (options.unblurNSFWThumbnails && parsed.thumbnail) unblurThumbnail(parsed.thumbnail); + results.push(parsed); + } break; } default: @@ -189,3 +209,36 @@ export function parsePlaylist(data?: any): YouTubePlayList { return res; } + +function unblurThumbnail(thumbnail: YouTubeThumbnail) { + if (BLURRED_THUMBNAILS.find((sqp) => thumbnail.url.includes(sqp))) { + thumbnail.url = thumbnail.url.split('?')[0]; + + // we need to update the size parameters as the sqp parameter also included a cropped size + switch (thumbnail.url.split('/').at(-1)!.split('.')[0]) { + case 'hq2': + case 'hqdefault': + thumbnail.width = 480; + thumbnail.height = 360; + break; + case 'hq720': + thumbnail.width = 1280; + thumbnail.height = 720; + break; + case 'sddefault': + thumbnail.width = 640; + thumbnail.height = 480; + break; + case 'mqdefault': + thumbnail.width = 320; + thumbnail.height = 180; + break; + case 'default': + thumbnail.width = 120; + thumbnail.height = 90; + break; + default: + thumbnail.width = thumbnail.height = NaN; + } + } +} diff --git a/play-dl/index.ts b/play-dl/index.ts index f92770d..82e4e8e 100644 --- a/play-dl/index.ts +++ b/play-dl/index.ts @@ -67,6 +67,11 @@ interface SearchOptions { }; fuzzy?: boolean; language?: string; + /** + * !!! Before enabling this for public servers, please consider using Discord features like NSFW channels as not everyone in your server wants to see NSFW images. !!! + * Unblurred images will likely have different dimensions than specified in the {@link YouTubeThumbnail} objects. + */ + unblurNSFWThumbnails?: boolean; } import { createInterface } from 'node:readline'; @@ -184,6 +189,9 @@ async function search(query: string, options?: SearchOptions): Promise