()
| 621 | |
| 622 | // Backfill Videos and channelvideos tables from complete.list and jobs info JSON |
| 623 | async backfillFromCompleteList() { |
| 624 | try { |
| 625 | const archivePath = path.join(__dirname, '../../config', 'complete.list'); |
| 626 | let archiveContent; |
| 627 | try { |
| 628 | archiveContent = await fsPromises.readFile(archivePath, 'utf-8'); |
| 629 | } catch (e) { |
| 630 | if (e && e.code === 'ENOENT') { |
| 631 | logger.info('No complete.list found for backfill. Skipping.'); |
| 632 | return; |
| 633 | } |
| 634 | throw e; |
| 635 | } |
| 636 | |
| 637 | const lines = archiveContent |
| 638 | .split(/\r?\n/) |
| 639 | .map((l) => l.trim()) |
| 640 | .filter(Boolean); |
| 641 | |
| 642 | const ids = lines |
| 643 | .map((line) => line.split(' ')[1]) |
| 644 | .filter(Boolean); |
| 645 | |
| 646 | let videosUpserts = 0; |
| 647 | let channelVideosUpserts = 0; |
| 648 | const missingInfoIds = []; |
| 649 | |
| 650 | // Build fast lookup sets of existing youtube IDs to avoid overwriting fresher DB data |
| 651 | const existingVideos = await Video.findAll({ attributes: ['youtubeId'] }); |
| 652 | const existingVideoIdSet = new Set(existingVideos.map(v => v.youtubeId)); |
| 653 | const existingChannelVideos = await ChannelVideo.findAll({ attributes: ['youtube_id'] }); |
| 654 | const existingChannelVideoIdSet = new Set(existingChannelVideos.map(cv => cv.youtube_id)); |
| 655 | |
| 656 | // Build candidate list first (IDs needing backfill in either table), |
| 657 | // starting from newest entries at the end of complete.list |
| 658 | const candidates = []; |
| 659 | for (let i = ids.length - 1; i >= 0; i--) { |
| 660 | const id = ids[i]; |
| 661 | const needsVideo = !existingVideoIdSet.has(id); |
| 662 | const needsChannelVideo = !existingChannelVideoIdSet.has(id); |
| 663 | // Always include in candidates - we may need to update media_type on existing records |
| 664 | candidates.push({ id, needsVideo, needsChannelVideo }); |
| 665 | } |
| 666 | |
| 667 | // Cap per run to 300 items |
| 668 | const maxPerRun = 300; |
| 669 | const capped = candidates.slice(0, maxPerRun); |
| 670 | |
| 671 | let processed = 0; |
| 672 | for (const { id, needsVideo, needsChannelVideo } of capped) { |
| 673 | const infoPath = path.join(__dirname, `../../jobs/info/${id}.info.json`); |
| 674 | |
| 675 | let info; |
| 676 | try { |
| 677 | const content = await fsPromises.readFile(infoPath, 'utf-8'); |
| 678 | info = JSON.parse(content); |
| 679 | } catch (e) { |
| 680 | if (e && e.code === 'ENOENT') { |
no test coverage detected