From cb4baf1602ce4b1f569f489cb46f9c349ca42dd9 Mon Sep 17 00:00:00 2001 From: Arhan Busam Date: Tue, 15 Jul 2025 16:32:29 +1000 Subject: [PATCH 01/12] Fetches text tracks and downloads captions --- src/float.ts | 33 +++++++++++++++++++++++++++++++++ src/lib/Subscription.ts | 2 +- src/lib/Video.ts | 34 ++++++++++++++++++++++++++++++++++ src/lib/defaults.ts | 1 + src/lib/types.ts | 1 + wiki/settings.md | 12 ++++++++++++ 6 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/float.ts b/src/float.ts index 5d809c46..edf1cc02 100644 --- a/src/float.ts +++ b/src/float.ts @@ -8,6 +8,10 @@ import chalk from "chalk-template"; import { loginFloatplane, User } from "./logins"; import { fetchSubscriptions } from "./subscriptionFetching"; +import { Attachment } from "./lib/Attachment"; +import { Video } from "./lib/Video"; +import Subscription from "./lib/Subscription"; +import type { ChannelOptions } from "./lib/types"; import semver from "semver"; const { gt, diff } = semver; @@ -41,6 +45,7 @@ const downloadNewVideos = async () => { } await subscription.deleteOldVideos(); for await (const video of subscription.fetchNewVideos()) inProgress.push(video.download()); + await findTextTracks(subscription); } console.log(chalk`Queued {green ${inProgress.length}} videos...`); @@ -105,3 +110,31 @@ process.on("SIGTERM", () => process.exit(143)); await downloadNewVideos(); })(); + +const findTextTracks = async (subscription: Subscription) => { + console.log(chalk`Checking for missing text tracks in {yellow ${subscription.plan}}...`); + let tracksAdded = 0; + + for (const videoData of Attachment.find((attachment: any) => { + return subscription.channels.some((channel: ChannelOptions) => channel.title === attachment.channelTitle); + })) { + const video = Video.Videos[videoData.attachmentId]; + + if (!video || (video.textTracks && video.textTracks.length > 0)) continue; + + try { + const textTracksCount = await video.updateTextTracks(); + + if (textTracksCount && textTracksCount > 0) { + tracksAdded++; + console.log(chalk`Found {green ${textTracksCount}} text track(s) for {cyan ${video.videoTitle}}`); + } + } catch (error) { + console.error(chalk`Failed to update text tracks for {red ${videoData.attachmentId}}: ${error}`); + } + } + + if (tracksAdded > 0) { + console.log(chalk`Added text tracks to {green ${tracksAdded}} videos in {yellow ${subscription.plan}}`); + } +}; diff --git a/src/lib/Subscription.ts b/src/lib/Subscription.ts index 2467d4c2..3deb867b 100644 --- a/src/lib/Subscription.ts +++ b/src/lib/Subscription.ts @@ -59,7 +59,7 @@ export default class Subscription { private static isChannelCache: Record = {}; private static isChannelHelper = `const isChannel = (post, channelId) => (typeof post.channel !== 'string' ? post.channel.id : post.channel) === channelId`; - private async fetchTextTracks(attachmentId: string) { + public async fetchTextTracks(attachmentId: string) { const video = await fApi.content.video(attachmentId); return video.textTracks?.filter((track) => track.kind === "captions") ?? []; } diff --git a/src/lib/Video.ts b/src/lib/Video.ts index 45956fc5..8ac60352 100644 --- a/src/lib/Video.ts +++ b/src/lib/Video.ts @@ -114,6 +114,9 @@ export class Video extends Attachment { if (settings.extras.downloadArtwork) { await this.downloadArtwork().catch(withContext(`Saving artwork`)).catch(this.onError); } + if (settings.extras.downloadCaptions && this.textTracks && this.textTracks.length > 0) { + await this.downloadCaptions().catch(withContext(`Downloading captions`)).catch(this.onError); + } if ((await this.getState()) === Video.State.Muxed) { this.logger.done(chalk`{green Exists! Skipping}`); return; @@ -292,6 +295,7 @@ export class Video extends Attachment { } private async downloadCaptions() { + if (!settings.extras.downloadCaptions) return; if (this.textTracks === undefined) return; const captions = this.textTracks.filter((track) => track.kind === "captions"); if (captions.length === 0) return; @@ -306,6 +310,36 @@ export class Video extends Attachment { this.logger.log("Saved captions"); } + public async updateTextTracks() { + if (this.textTracks && this.textTracks.length > 0) return 0; + + try { + const video = await fApi.content.video(this.attachmentId); + const newTextTracks = video.textTracks?.filter((track) => track.kind === "captions") ?? []; + + if (newTextTracks.length > 0) { + Object.defineProperty(this, "textTracks", { + value: newTextTracks, + writable: false, + enumerable: true, + configurable: false, + }); + + if (settings.extras.downloadCaptions) { + await this.downloadCaptions().catch((error) => { + console.error(`Failed to download captions for ${this.attachmentId}:`, error); + }); + } + + return newTextTracks.length; + } + } catch (error) { + console.error(`Failed to fetch text tracks for ${this.attachmentId}:`, error); + } + + return 0; + } + // The number of available slots for making delivery requests, // limiting the rate of requests to avoid exceeding the API rate limit. private static DeliveryTimeout = 65000; diff --git a/src/lib/defaults.ts b/src/lib/defaults.ts index 73cdbf0b..b74486d6 100644 --- a/src/lib/defaults.ts +++ b/src/lib/defaults.ts @@ -60,6 +60,7 @@ export const defaultSettings: Settings = { downloadArtwork: true, saveNfo: true, considerAllNonPartialDownloaded: false, + downloadCaptions: true, }, artworkSuffix: "", postProcessingCommand: "", diff --git a/src/lib/types.ts b/src/lib/types.ts index 125294f5..55c8afec 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -45,6 +45,7 @@ export interface Extras extends Record { downloadArtwork: boolean; saveNfo: boolean; considerAllNonPartialDownloaded: boolean; + downloadCaptions: boolean; } export type Resolution = ValueOfA; diff --git a/wiki/settings.md b/wiki/settings.md index 3337e925..a8eb8718 100644 --- a/wiki/settings.md +++ b/wiki/settings.md @@ -168,6 +168,18 @@ This may result in files without muxed metadata and should only be used for reco
+**extras.downloadCaptions**: +Controls whether video captions/subtitles are downloaded when available. +Caption files are saved as `.vtt` files in the same directory as the video. + +```json +"extras": { + "downloadCaptions": true +} +``` + +
+ ## Plex Use **quickstartPrompts** to easily set plex settings. From af5b10f24445ca0d863f6c935bff2c425dbd19f4 Mon Sep 17 00:00:00 2001 From: Arhan Busam Date: Tue, 15 Jul 2025 16:58:06 +1000 Subject: [PATCH 02/12] Changed downloadCaptions to only print if captions are actually being saved --- src/lib/Video.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/lib/Video.ts b/src/lib/Video.ts index 8ac60352..afdc2e57 100644 --- a/src/lib/Video.ts +++ b/src/lib/Video.ts @@ -298,14 +298,21 @@ export class Video extends Attachment { if (!settings.extras.downloadCaptions) return; if (this.textTracks === undefined) return; const captions = this.textTracks.filter((track) => track.kind === "captions"); - if (captions.length === 0) return; - this.logger.log("Saving captions"); + if (captions.length === 0) return; + const toDownload: Array<{ src: string; path: string }> = []; for (const caption of captions) { const captionPath = `${this.filePath}${caption.language ? `.${caption.language}` : ""}.vtt`; - if (await fileExists(captionPath)) continue; - const captionContent = await fetch(caption.src).then((res) => res.text()); - await writeFile(captionPath, captionContent, "utf8"); + if (!(await fileExists(captionPath))) { + toDownload.push({ src: caption.src, path: captionPath }); + } + } + if (toDownload.length === 0) return; + + this.logger.log("Saving captions"); + for (const { src, path } of toDownload) { + const captionContent = await fetch(src).then((res) => res.text()); + await writeFile(path, captionContent, "utf8"); } this.logger.log("Saved captions"); } From ce138c77d4cf377fb9efdcb6099e904785f18954 Mon Sep 17 00:00:00 2001 From: Arhan Busam Date: Tue, 15 Jul 2025 22:13:10 +1000 Subject: [PATCH 03/12] Stopped fetching text tracks when flag is disabled --- src/float.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/float.ts b/src/float.ts index edf1cc02..ecf17d19 100644 --- a/src/float.ts +++ b/src/float.ts @@ -45,7 +45,7 @@ const downloadNewVideos = async () => { } await subscription.deleteOldVideos(); for await (const video of subscription.fetchNewVideos()) inProgress.push(video.download()); - await findTextTracks(subscription); + if (settings.extras.downloadCaptions) await findTextTracks(subscription); } console.log(chalk`Queued {green ${inProgress.length}} videos...`); From b7d3a9d340e2a3d09526eccae080d6834ddf98ca Mon Sep 17 00:00:00 2001 From: Arhan Busam Date: Tue, 15 Jul 2025 22:50:32 +1000 Subject: [PATCH 04/12] Removed unused method --- src/lib/Subscription.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/lib/Subscription.ts b/src/lib/Subscription.ts index 3deb867b..c991e11b 100644 --- a/src/lib/Subscription.ts +++ b/src/lib/Subscription.ts @@ -59,11 +59,6 @@ export default class Subscription { private static isChannelCache: Record = {}; private static isChannelHelper = `const isChannel = (post, channelId) => (typeof post.channel !== 'string' ? post.channel.id : post.channel) === channelId`; - public async fetchTextTracks(attachmentId: string) { - const video = await fApi.content.video(attachmentId); - return video.textTracks?.filter((track) => track.kind === "captions") ?? []; - } - private async *matchChannel(blogPost: BlogPost): AsyncGenerator