Initial commit: YouTube Studio Flow (backend, frontend, infrastructure, docs)
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user