Files
youtube-studio-flow/backend/scripts/import-missing-videos.mjs
T

113 lines
3.8 KiB
JavaScript

import { PrismaClient } from '@prisma/client';
import { createDecipheriv, scryptSync } from 'crypto';
import { google } from 'googleapis';
const MISSING_IDS = [
'4dcg46RQqnM',
'9xsmLBY20wA',
'DmmQ7HPxBRg',
'G58lGGaMOOY',
'I5u-8xk6BHM',
'OdHheItBlyQ',
'VtfmpQXPheA',
'hIgaxCUX9ts',
'nHWtSlE-kgE',
'ye3b5J7zX0M',
'zW7yrpUs34Q',
];
const CHANNEL_ID = 'cmpfdm4el008nn3uoemascs0z';
const PRIVACY_MAP = { public: 'PUBLIC', private: 'PRIVATE', unlisted: 'UNLISTED' };
function decrypt(text) {
const key = scryptSync('7017a681f5b32c3799d1bbf58ce58064', 'studioflow-salt', 32);
const [ivHex, encHex] = text.split(':');
const decipher = createDecipheriv('aes-256-cbc', key, Buffer.from(ivHex, 'hex'));
return Buffer.concat([decipher.update(Buffer.from(encHex, 'hex')), decipher.final()]).toString('utf8');
}
const prisma = new PrismaClient();
const channel = await prisma.channel.findUniqueOrThrow({ where: { id: CHANNEL_ID } });
// Persist supplemental IDs on the channel
const existing = new Set(channel.supplementalVideoIds ?? []);
for (const id of MISSING_IDS) existing.add(id);
await prisma.channel.update({
where: { id: CHANNEL_ID },
data: { supplementalVideoIds: [...existing] },
});
console.log(`Saved ${existing.size} supplemental video IDs on channel`);
// Set up YouTube client and refresh access token
const oauth2 = new google.auth.OAuth2(
'496320251583-rpbr13i0rh1112cdvv9ui5s373735qqj.apps.googleusercontent.com',
'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',
);
oauth2.setCredentials({
access_token: decrypt(channel.youtubeAccessToken),
refresh_token: channel.youtubeRefreshToken ? decrypt(channel.youtubeRefreshToken) : undefined,
});
// Force token refresh so we always have a valid access token
const { credentials } = await oauth2.refreshAccessToken();
oauth2.setCredentials(credentials);
console.log('Access token refreshed');
const yt = google.youtube({ version: 'v3', auth: oauth2 });
// Fetch metadata for all missing IDs
const res = await yt.videos.list({
part: ['snippet', 'status'],
id: MISSING_IDS,
maxResults: 50,
});
const items = res.data.items ?? [];
console.log(`YouTube returned ${items.length} of ${MISSING_IDS.length} requested IDs`);
const notFound = MISSING_IDS.filter((id) => !items.find((i) => i.id === id));
if (notFound.length > 0) console.warn(`Not returned by YouTube API: ${notFound.join(', ')}`);
let created = 0;
let updated = 0;
for (const item of items) {
const ytId = item.id;
if (!ytId) continue;
const snippet = item.snippet;
const status = item.status;
const ytFields = {
title: snippet?.title ?? '(Untitled)',
youtubeDescription: snippet?.description ?? undefined,
tags: snippet?.tags ?? [],
categoryId: snippet?.categoryId ?? undefined,
publishedAt: snippet?.publishedAt ? new Date(snippet.publishedAt) : undefined,
privacyStatus: PRIVACY_MAP[status?.privacyStatus ?? ''] ?? 'PRIVATE',
defaultLanguage: snippet?.defaultLanguage ?? undefined,
defaultAudioLanguage: snippet?.defaultAudioLanguage ?? undefined,
selfDeclaredMadeForKids: status?.selfDeclaredMadeForKids ?? undefined,
embeddable: status?.embeddable ?? undefined,
license: status?.license ?? undefined,
};
const existing = await prisma.video.findUnique({ where: { youtubeVideoId: ytId } });
if (!existing) {
await prisma.video.create({ data: { youtubeVideoId: ytId, channelId: CHANNEL_ID, ...ytFields } });
console.log(` CREATED: ${ytId}${ytFields.title}`);
created++;
} else {
await prisma.video.update({
where: { youtubeVideoId: ytId },
data: { channelId: CHANNEL_ID, ...ytFields },
});
console.log(` UPDATED: ${ytId}${ytFields.title}`);
updated++;
}
}
console.log(`\nDone: ${created} created, ${updated} updated, ${notFound.length} not found on YouTube`);
await prisma.$disconnect();