Skip to content

Commit 92d6005

Browse files
committed
i
1 parent b9e8959 commit 92d6005

6 files changed

Lines changed: 158 additions & 222 deletions

File tree

dist/404.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

dist/index.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/index.html

Lines changed: 39 additions & 42 deletions
Large diffs are not rendered by default.

src/yt/yt-ids.js

Lines changed: 117 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -1,193 +1,132 @@
11
require('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');
55
const https = require('https');
6-
const htmlContent = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
7-
const searchDivRegex = /id="shuffle">([\s\S]*?)<\/div>/;
8-
const searchDiv = htmlContent.match(searchDivRegex)?.[1] || '';
9-
const playlistIdRegex = /(?:p="([^"]+)"|v="([^"]+)")/g;
10-
const totalVideosRegex = /"total-videos">([^<]*)<\/span>/;
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(/id="shuffle">([\s\S]*?)<\/div>/)?.[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(/&pageToken=null/, '');
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(/^(PL|FL|OL|TL|UU)/));
153-
playlistIdsInTag = [...new Set([...playlistIdsInTag, ...playlistIdsInV])];
154-
videoIdsInTag = videoIdsInTag.filter(id => !id.match(/^(PL|FL|OL|TL|UU)/));
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(/id="shuffle">[\s\S]*?<\/div>/, `id="shuffle">${updated}</div>`)
126+
.replace(/"total-videos">[^<]*<\/span>/, `"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

Comments
 (0)