11require ( 'dotenv' ) . config ( ) ;
2- const KEY = process . env . KEY
3- const path = require ( 'path ' ) ;
4- const fs = require ( 'fs ' ) ;
2+ const KEY = process . env . KEY ;
3+ const { readFileSync , writeFileSync } = require ( 'fs ' ) ;
4+ const { join } = require ( 'path ' ) ;
55const https = require ( 'https' ) ;
6- const htmlContent = fs . readFileSync ( path . join ( __dirname , '..' , 'index.html' ) , 'utf8' ) ;
7- const searchDivRegex = / i d = " s h u f f l e " > ( [ \s \S ] * ?) < \/ d i v > / ;
8- const searchDiv = htmlContent . match ( searchDivRegex ) ?. [ 1 ] || '' ;
9- const playlistIdRegex = / (?: p = " ( [ ^ " ] + ) " | v = " ( [ ^ " ] + ) " ) / g;
10- const totalVideosRegex = / " t o t a l - v i d e o s " > ( [ ^ < ] * ) < \/ s p a n > / ;
11- function readExcludedIds ( ) {
12- try {
13- const data = fs . readFileSync ( path . join ( __dirname , '..' , 'yt' , 'exclude.txt' ) , 'utf8' ) ;
14- return data . split ( ',' ) . map ( id => id . trim ( ) ) . filter ( id => id !== '' ) ;
15- } catch ( err ) {
16- console . error ( 'Error reading exclude.txt:' , err ) ;
17- return [ ] ;
18- }
19- }
20- const excludedIds = readExcludedIds ( ) ;
21- if ( ! KEY ) {
22- throw new Error ( 'API key is missing or empty. Please provide a valid API key. https://developers.google.com/youtube/v3/getting-started#before-you-start' ) ;
23- }
24- const extractIds = ( regex , source ) => {
25- const ids = [ ] ;
26- let match ;
27- while ( ( match = regex . exec ( source ) ) !== null ) {
28- if ( match [ 1 ] ) {
29- ids . push ( ...match [ 1 ] . split ( ',' ) . map ( id => id . trim ( ) ) ) ;
30- } else if ( match [ 2 ] ) {
31- ids . push ( ...match [ 2 ] . split ( ',' ) . map ( id => id . trim ( ) ) ) ;
32- }
6+ if ( ! KEY ) throw new Error ( 'API key missing' ) ;
7+ const excludedIds = new Set (
8+ readFileSync ( join ( __dirname , '..' , 'yt' , 'exclude.txt' ) , 'utf8' )
9+ . split ( ',' ) . map ( id => id . trim ( ) ) . filter ( Boolean )
10+ ) ;
11+ const htmlContent = readFileSync ( join ( __dirname , '..' , 'index.html' ) , 'utf8' ) ;
12+ const searchDiv = htmlContent . match ( / i d = " s h u f f l e " > ( [ \s \S ] * ?) < \/ d i v > / ) ?. [ 1 ] || '' ;
13+ const cleanId = id => id . split ( '?' ) [ 0 ] ;
14+ const isVideoId = id => cleanId ( id ) . length === 11 ;
15+ const isPlaylistId = id => cleanId ( id ) . length > 11 ;
16+ const fetchUrl = url => new Promise ( ( resolve , reject ) => {
17+ https . get ( url . toString ( ) , res => {
18+ let data = '' ;
19+ res . on ( 'data' , c => data += c ) ;
20+ res . on ( 'end' , ( ) => resolve ( JSON . parse ( data ) ) ) ;
21+ } ) . on ( 'error' , reject ) ;
22+ } ) ;
23+ const buildUrl = ( base , params ) => {
24+ const u = new URL ( base ) ;
25+ u . search = new URLSearchParams ( params ) ;
26+ return u ;
27+ } ;
28+ const extractIds = ( regex , src ) => {
29+ const out = [ ] ;
30+ let m ;
31+ while ( ( m = regex . exec ( src ) ) ) {
32+ if ( m [ 1 ] ) out . push ( ...m [ 1 ] . split ( ',' ) . map ( s => s . trim ( ) ) ) ;
33+ if ( m [ 2 ] ) out . push ( ...m [ 2 ] . split ( ',' ) . map ( s => s . trim ( ) ) ) ;
3334 }
34- return ids ;
35+ return out ;
3536} ;
36- const allIds = extractIds ( playlistIdRegex , searchDiv ) ;
37- const playlistIds = allIds . filter ( id => id . split ( '?' ) [ 0 ] . length > 11 ) ;
38- const videoIds = allIds . filter ( id => id . split ( '?' ) [ 0 ] . length === 11 ) ;
39- const getPlaylistItems = async ( playlistIDs ) => {
40- let availableVideoIds = { } ;
41- let errorCount = 0 ;
42- for ( const playlistID of playlistIDs ) {
43- try {
44- console . log ( `Fetching playlist items for playlist ID: ${ playlistID } ` ) ;
45- let pageToken = null ;
46- let totalItems = 0 ;
47- let videoIds = [ ] ;
48- do {
37+ const allIds = extractIds ( / (?: p = " ( [ ^ " ] + ) " | v = " ( [ ^ " ] + ) " ) / g, searchDiv ) ;
38+ const playlistIds = allIds . filter ( isPlaylistId ) ;
39+ const videoIds = allIds . filter ( isVideoId ) ;
40+ const writeOutput = async ( ) => {
41+ const playlistVideos = { } ;
42+ let playlistErrors = 0 ;
43+ for ( const raw of playlistIds ) {
44+ const pid = cleanId ( raw ) ;
45+ let pageToken = null ;
46+ const ids = [ ] ;
47+ do {
48+ try {
4949 const params = {
50- part : 'id,snippet,status' ,
50+ part : 'snippet,status' ,
51+ playlistId : pid ,
5152 maxResults : 50 ,
52- playlistId : playlistID . trim ( ) ,
53- key : KEY ,
54- pageToken : pageToken ,
53+ key : KEY
5554 } ;
56- const url = new URL ( 'https://www.googleapis.com/youtube/v3/playlistItems' ) ;
57- url . search = new URLSearchParams ( params ) . toString ( ) ;
58- url . href = url . href . replace ( / & p a g e T o k e n = n u l l / , '' ) ;
59- const result = await new Promise ( ( resolve , reject ) => {
60- https . get ( url . toString ( ) , ( res ) => {
61- let data = '' ;
62- res . on ( 'data' , ( chunk ) => {
63- data += chunk ;
64- } ) ;
65- res . on ( 'end' , ( ) => {
66- resolve ( JSON . parse ( data ) ) ;
67- } ) ;
68- } ) . on ( 'error' , ( err ) => {
69- reject ( err ) ;
70- } ) ;
71- } ) ;
72- const availableVideos = result . items . filter ( ( item ) =>
73- item . status . privacyStatus === 'public' &&
74- ! excludedIds . includes ( item . snippet . resourceId . videoId )
75- ) ;
76- videoIds = [ ...videoIds , ...availableVideos . map ( ( item ) => item . snippet . resourceId . videoId ) ] ;
77- totalItems += result . items . length ;
78- console . log ( `Fetched ${ totalItems } items for playlist ID: ${ playlistID } ` ) ;
79- pageToken = result . nextPageToken ;
80- } while ( pageToken ) ;
81- availableVideoIds [ playlistID ] = videoIds ;
82- } catch ( error ) {
83- errorCount ++ ;
84- if ( error . response && error . response . status === 404 ) {
85- console . warn ( `Skipping invalid playlist ID: ${ playlistID } ` ) ;
86- } else {
87- console . error ( `Error fetching playlist items for playlist ID ${ playlistID } : ${ error . message } ` ) ;
55+ if ( pageToken ) params . pageToken = pageToken ;
56+ const data = await fetchUrl ( buildUrl ( 'https://www.googleapis.com/youtube/v3/playlistItems' , params ) ) ;
57+ if ( data . error ) throw new Error ( data . error . message ) ;
58+ for ( const item of data . items ) {
59+ if (
60+ item . status ?. privacyStatus !== 'private' &&
61+ ! excludedIds . has ( item . snippet . resourceId . videoId )
62+ ) {
63+ ids . push ( item . snippet . resourceId . videoId ) ;
64+ }
65+ }
66+ pageToken = data . nextPageToken || null ;
67+ } catch ( e ) {
68+ playlistErrors ++ ;
69+ console . error ( `Playlist ${ pid } : ${ e . message } ` ) ;
70+ break ;
8871 }
89- }
72+ } while ( pageToken ) ;
73+ playlistVideos [ pid ] = ids ;
9074 }
91- return { availableVideoIds, errorCount } ;
92- } ;
93- async function getEmbeddableStatus ( videoIds ) {
94- if ( videoIds . length === 0 ) return { } ;
95- const cleanToFull = { } ;
96- videoIds . forEach ( fullId => {
97- const cleanId = fullId . split ( '?' ) [ 0 ] ;
98- if ( ! cleanToFull [ cleanId ] ) cleanToFull [ cleanId ] = [ ] ;
99- cleanToFull [ cleanId ] . push ( fullId ) ;
100- } ) ;
101- const uniqueCleanIds = Object . keys ( cleanToFull ) ;
102- const map = { } ; // cleanId -> embeddable
103- for ( let i = 0 ; i < uniqueCleanIds . length ; i += 50 ) {
104- const batch = uniqueCleanIds . slice ( i , i + 50 ) ;
105- const params = {
106- part : 'status' ,
107- id : batch . join ( ',' ) ,
108- key : KEY
109- } ;
110- const url = new URL ( 'https://www.googleapis.com/youtube/v3/videos' ) ;
111- url . search = new URLSearchParams ( params ) . toString ( ) ;
75+ const allCleanIds = [ ...new Set ( [
76+ ...videoIds . map ( cleanId ) ,
77+ ...Object . values ( playlistVideos ) . flat ( )
78+ ] ) ] . filter ( id => ! excludedIds . has ( id ) ) ;
79+ const embeddableMap = { } ;
80+ let videoErrors = 0 ;
81+ for ( let i = 0 ; i < allCleanIds . length ; i += 50 ) {
82+ const batch = allCleanIds . slice ( i , i + 50 ) ;
11283 try {
113- const result = await new Promise ( ( resolve , reject ) => {
114- https . get ( url . toString ( ) , ( res ) => {
115- let data = '' ;
116- res . on ( 'data' , ( chunk ) => { data += chunk ; } ) ;
117- res . on ( 'end' , ( ) => {
118- try {
119- resolve ( JSON . parse ( data ) ) ;
120- } catch ( parseErr ) {
121- reject ( parseErr ) ;
122- }
123- } ) ;
124- } ) . on ( 'error' , ( err ) => {
125- reject ( err ) ;
126- } ) ;
127- } ) ;
128- if ( result . error ) {
129- console . error ( `Error fetching video status: ${ result . error . message } ` ) ;
130- continue ;
84+ const data = await fetchUrl ( buildUrl ( 'https://www.googleapis.com/youtube/v3/videos' , {
85+ part : 'status' ,
86+ id : batch . join ( ',' ) ,
87+ key : KEY
88+ } ) ) ;
89+ if ( data . error ) throw new Error ( data . error . message ) ;
90+ for ( const item of data . items || [ ] ) {
91+ if ( item . status ?. privacyStatus === 'private' ) continue ;
92+ embeddableMap [ item . id ] = item . status . embeddable ?? null ;
13193 }
132- result . items . forEach ( item => {
133- map [ item . id ] = item . status . embeddable ;
134- } ) ;
135- } catch ( error ) {
136- console . error ( `Error in batch fetch: ${ error . message } ` ) ;
94+ } catch ( e ) {
95+ videoErrors ++ ;
96+ console . error ( `Videos batch error: ${ e . message } ` ) ;
13797 }
13898 }
139- return map ;
140- }
141- const writeOutput = async ( ) => {
142- const { availableVideoIds, errorCount } = await getPlaylistItems ( playlistIds ) ;
143- const allVideoIds = [ ...new Set ( [ ...videoIds , ...Object . values ( availableVideoIds ) . flat ( ) ] ) ] . filter ( id => ! excludedIds . includes ( id . split ( '?' ) [ 0 ] ) ) ;
144- const embeddableMap = await getEmbeddableStatus ( allVideoIds ) ;
145- let totalVideoCount = 0 ;
146- const ytRegex = / < y - t ( [ ^ > ] * ) > / g;
147- const updatedSearchDiv = searchDiv . replace ( ytRegex , ( match , attributes ) => {
148- const pMatch = attributes . match ( / p = " ( [ ^ " ] + ) " / ) ;
149- const vMatch = attributes . match ( / v = " ( [ ^ " ] + ) " / ) ;
150- let playlistIdsInTag = pMatch ? pMatch [ 1 ] . split ( ',' ) . map ( id => id . trim ( ) ) : [ ] ;
151- let videoIdsInTag = vMatch ? vMatch [ 1 ] . split ( ',' ) . map ( id => id . trim ( ) ) : [ ] ;
152- const playlistIdsInV = videoIdsInTag . filter ( id => id . match ( / ^ ( P L | F L | O L | T L | U U ) / ) ) ;
153- playlistIdsInTag = [ ...new Set ( [ ...playlistIdsInTag , ...playlistIdsInV ] ) ] ;
154- videoIdsInTag = videoIdsInTag . filter ( id => ! id . match ( / ^ ( P L | F L | O L | T L | U U ) / ) ) ;
155- const newVideoIds = playlistIdsInTag . flatMap ( playlistId => availableVideoIds [ playlistId ] || [ ] ) ;
156- const combinedVideoIds = [ ...new Set ( [ ...videoIdsInTag , ...newVideoIds ] ) ] . filter ( id => ! excludedIds . includes ( id . split ( '?' ) [ 0 ] ) ) ;
157- const embeddableIds = combinedVideoIds . filter ( fullId => {
158- const cleanId = fullId . split ( '?' ) [ 0 ] ;
159- return embeddableMap [ cleanId ] === true ;
160- } ) ;
161- const nonEmbeddableIds = combinedVideoIds . filter ( fullId => {
162- const cleanId = fullId . split ( '?' ) [ 0 ] ;
163- return embeddableMap [ cleanId ] !== true ;
164- } ) ;
165- totalVideoCount += combinedVideoIds . length ;
166- let newAttributes = attributes ;
167- if ( playlistIdsInTag . length > 0 ) {
168- newAttributes = newAttributes . replace ( / p = " [ ^ " ] * " / , `p="${ playlistIdsInTag . join ( ',' ) } "` ) ;
169- if ( ! pMatch ) {
170- newAttributes = `p="${ playlistIdsInTag . join ( ',' ) } " ` + newAttributes ;
171- }
172- }
173- if ( vMatch ) {
174- newAttributes = newAttributes . replace ( / v = " [ ^ " ] * " / , `v="${ embeddableIds . join ( ',' ) } "` ) ;
175- } else {
176- newAttributes += ` v="${ embeddableIds . join ( ',' ) } "` ;
177- }
178- if ( nonEmbeddableIds . length > 0 ) {
179- newAttributes += ` u="${ nonEmbeddableIds . join ( ',' ) } "` ;
180- }
181- newAttributes = newAttributes . replace ( / \s + / g, ' ' ) . trim ( ) ;
182- return `<y-t ${ newAttributes } >` ;
99+ const originalMap = new Map ( allIds . map ( id => [ cleanId ( id ) , id ] ) ) ;
100+ let total = 0 ;
101+ const updated = searchDiv . replace ( / < y - t ( [ ^ > ] * ) > / g, ( _ , attrs ) => {
102+ const p = ( attrs . match ( / p = " ( [ ^ " ] * ) " / ) ?. [ 1 ] || '' ) . split ( ',' ) . map ( s => s . trim ( ) ) . filter ( isPlaylistId ) ;
103+ const v = ( attrs . match ( / v = " ( [ ^ " ] * ) " / ) ?. [ 1 ] || '' ) . split ( ',' ) . map ( s => s . trim ( ) ) ;
104+ const u = ( attrs . match ( / u = " ( [ ^ " ] * ) " / ) ?. [ 1 ] || '' ) . split ( ',' ) . map ( s => s . trim ( ) ) . filter ( Boolean ) ;
105+ const fromPlaylists = p . flatMap ( pid => playlistVideos [ cleanId ( pid ) ] || [ ] ) ;
106+ const combinedClean = [ ...new Set ( [ ...fromPlaylists , ...v . map ( cleanId ) ] ) ]
107+ . filter ( id => ! excludedIds . has ( id ) ) ;
108+ const embeddableClean = combinedClean . filter ( id => embeddableMap [ id ] === true ) ;
109+ const nonEmbeddableClean = combinedClean . filter ( id => embeddableMap [ id ] === false || embeddableMap [ id ] === null ) ;
110+ total += embeddableClean . length + nonEmbeddableClean . length ;
111+ const embeddableFull = embeddableClean . map ( id => originalMap . get ( id ) || id ) ;
112+ const nonEmbeddableFull = [ ...new Set ( [
113+ ...nonEmbeddableClean . map ( id => originalMap . get ( id ) || id ) ,
114+ ...u
115+ ] ) ] . filter ( Boolean ) ;
116+ let newAttrs = attrs
117+ . replace ( / p = " [ ^ " ] * " / g, p . length ? `p="${ p . join ( ',' ) } "` : 'p=""' )
118+ . replace ( / v = " [ ^ " ] * " / g, `v="${ embeddableFull . join ( ',' ) } "` )
119+ . replace ( / u = " [ ^ " ] * " / g, '' ) ;
120+ if ( nonEmbeddableFull . length > 0 ) newAttrs += ` u="${ nonEmbeddableFull . join ( ',' ) } "` ;
121+ return `<y-t ${ newAttrs . trim ( ) . replace ( / \s + / g, ' ' ) } >` ;
183122 } ) ;
184- const formattedTotalVideoCount = totalVideoCount . toLocaleString ( ) ;
185- const finalSearchDiv = updatedSearchDiv ;
186- const updatedHtmlContent = htmlContent
187- . replace ( searchDivRegex , `id="shuffle ">${ finalSearchDiv } </div >` )
188- . replace ( totalVideosRegex , `"total-videos"> ${ formattedTotalVideoCount } </span>` ) ;
189- fs . writeFileSync ( path . join ( __dirname , '..' , 'index.html' ) , updatedHtmlContent , 'utf8' ) ;
190- console . log ( `Total videos: ${ formattedTotalVideoCount } ` ) ;
191- console . log ( `Number of errors : ${ errorCount } ` ) ;
123+ writeFileSync ( join ( __dirname , '..' , 'index.html' ) ,
124+ htmlContent
125+ . replace ( / i d = " s h u f f l e " > [ \s \S ] * ? < \/ d i v > / , `id="shuffle"> ${ updated } </div>` )
126+ . replace ( / " t o t a l - v i d e o s " > [ ^ < ] * < \/ s p a n > / , `"total-videos ">${ total . toLocaleString ( ) } </span >` ) ,
127+ 'utf8'
128+ ) ;
129+ console . log ( `\nTotal videos processed : ${ total . toLocaleString ( ) } ` ) ;
130+ console . log ( `Errors — playlists : ${ playlistErrors } , videos: ${ videoErrors } ` ) ;
192131} ;
193- writeOutput ( ) . catch ( console . error ) ;
132+ writeOutput ( ) . catch ( console . error ) ;
0 commit comments