import { config } from 'dotenv'; config(); process.env.DEBUG += ',-db:*'; import { readdirSync, readFileSync } from 'fs'; import { createInterface } from 'readline'; import { MemberObject, MemberProps, PlatformId } from '../database/types/members'; import { Counter, debug, Members } from '../src/modules'; import { ChannelId } from '../src/modules/types/youtube'; import youtubeChannelScraper from './apps/scrapers/youtube-scraper'; import updateYoutube from './apps/updaters/youtube-updater'; if (!process.env.GOOGLE_API_KEY) throw new Error('GOOGLE_API_KEY is undefined!'); export function channelManager() { console.clear(); console.log( '---------------------------- Manage Channels ----------------------------\n' + ' Make sure you\'ve set up the .json files in channels/organizations directory.\n' + ' Check templates.json to see how to make custom channels, or move the files\n' + ' from the default directory to the organizations directory.\n' + '-----------------------------------------------------------------------------\n' + ' [1] Initialize (Run Everything)\n' + ' [2] Validate JSON Files\n' + ' [3] Save + Update\n' + ' [4] Save Channels\n' + ' [5] Update Channels\n' + ' [6] Scrape Channels\n' + ' [7] Drop Members and Channels Collection\n' + ' [8] Drop vt-api Database\n' + ' [9] Exit\n' ); const rl = createInterface({ input: process.stdin, output: process.stdout }); rl.question('Selection: ', async input => { process.env.DEBUG = process.env.DEBUG.slice(0, -6); rl.close(); switch (input) { default: return channelManager(); case '1': await init(); break; case '2': validateChannels(); break; case '3': case '4': await Promise.all(saveChannels({}, true)); if (input === '4') break; case '5': await updateChannels(); break; case '6': await scrapeChannels(); break; case '7': await dropCollections(); break; case '8': await dropDatabase(); break; case '9': process.exit(); } delayEnd(); }); } const delayEnd = () => setTimeout(() => { console.log('Press any key to continue: '); process.stdin.setRawMode(true); process.stdin.resume(); process.stdin.on('data', process.exit.bind(process, 0)); }, 600); const logger = debug('channels'); const ROOT_DIR = 'channels/organizations'; type ChannelPlatform<T> = {[key in PlatformId]: T[]}; type BasicChannelData = [ChannelId, string, PlatformId]; function saveChannel(filename: string, dry = false, save = true, async = false) { const groupName = filename.slice(0, -5); const channelList: MemberObject[] = JSON.parse(readFileSync(`${ROOT_DIR}/${filename}`, 'utf-8')); const parseChannel = (channel: MemberObject): any => { channel.organization = groupName; return channel; }; const parsedChannels: MemberObject[] = channelList.map(parseChannel); if (dry) return parsedChannels; if (save) { const writeOp = Members .create(<any[]>parsedChannels) .then(() => logger.info(`${filename} OK`)) .catch(err => logger.error(`${filename} CODE: ${err.code}`, err?.keyValue ?? '')); if (async) return writeOp; } return channelList.map((channel): BasicChannelData => [channel.channel_id, groupName, channel.platform_id]); } function checkChannels<T>(channelList: T[]): T[]|never { if (!channelList.length) { throw new Error('No channels found.'); } else { return channelList; } } function saveChannels<T1 extends boolean, T2 extends boolean = false>( options: { dry?: T1; save?: boolean; } = { dry: <T1>false, save: true }, async: T2 = <T2>false ): T2 extends true ? Promise<MemberProps[]>[] : T1 extends true ? MemberObject[] : BasicChannelData[] { return checkChannels(readdirSync(ROOT_DIR) .filter(file => file.endsWith('.json')) .flatMap((group): any => saveChannel(group, options.dry, options.save, async)) ) as T2 extends true ? Promise<MemberProps[]>[] : T1 extends true ? MemberObject[] : BasicChannelData[]; } function validateChannels() { try { const channels = saveChannels({ dry: true }); if (!channels.length) { logger.error(new Error('No channel jsons found.')); return; } logger.info(`Found ${channels.length} channels.`); let errorCount = 0; for (let i = channels.length; i--;) { const err = new Members(channels[i]).validateSync(); if (!err) continue; logger.error({ error: err.message, channel: channels[i] }); errorCount++; } if (errorCount) { logger.info(`Failed to validate ${errorCount} channels.`); return false; } else { logger.info('All channels validated successfully.'); return true; } } catch(err) { logger.error(err); return false; } } async function scrapeChannels() { const channelList = await Members .find({ crawled_at: { $exists: false } }) .then(groupMemberObject); if (!Object.values(channelList).flat().length) { logger.error(new Error('No saved members found.')); return; } const scraper = { RESULTS: { OK: [], FAIL: [], videoCount: 0 }, async youtube(channels: MemberObject[]) { for (let i = channels.length; i--;) { const currentChannel = channels[i]; const [STATUS, VIDEO_COUNT] = await youtubeChannelScraper(currentChannel); this.RESULTS[STATUS].push(currentChannel.channel_id); this.RESULTS.videoCount += VIDEO_COUNT; } }, // async bilibili(channels: MemberObject[]) { // }, // async twitchtv(channels: MemberObject[]) { // } }; await Promise.all([ scraper.youtube(channelList.yt), // scraper.bilibili(channelList.bb), // scraper.twitchtv(channelList.tt) ]); logger.info(scraper.RESULTS); } async function updateChannels() { const CHANNEL_PLATFORMS = await Members.find() .then(groupMemberObject) as ChannelPlatform<MemberProps>; await Promise.all([ updateYoutube(CHANNEL_PLATFORMS.yt), // @TODO: Implement bb and ttv apis // updateBilibili(CHANNEL_PLATFORMS.bb), // updateTwitch(CHANNEL_PLATFORMS.tt) ]); } async function dropCollections() { const { connection } = await require('mongoose'); logger.info('Dropping channel related collections...'); await Promise.all([ connection.dropCollection('members'), connection.dropCollection('channels'), Counter.deleteOne({ _id: 'member_id' }) ]); logger.info('Dropped members and channels collection.'); } async function dropDatabase() { const { connection } = await require('mongoose'); logger.info('Dropping vt-api database...'); await connection.dropDatabase(); logger.info('Dropped vt-api database.'); } function groupMemberObject(memberList: MemberObject[]): ChannelPlatform<MemberObject> { return memberList.reduce( (platforms, channel) => { platforms[channel.platform_id].push(channel); return platforms; }, { yt: [], bb: [], tt: [] } ); } export async function init(script = false) { if(!validateChannels()) return; await Promise.all(saveChannels({}, true)); await updateChannels(); await scrapeChannels(); if (script) delayEnd(); }