pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Refactor externalSubtitles into modular files

Split the monolithic externalSubtitles.ts into separate modules for each provider (febbox, opensubtitles, vdrk, wyzie) and a new index.ts for orchestration and exports. This improves maintainability and clarity by isolating provider-specific logic.

Pas d2768f55 b875bc93

+658 -440
-440
src/utils/externalSubtitles.ts
··· 1 - /* eslint-disable no-console */ 2 - import { type SubtitleData, searchSubtitles } from "wyzie-lib"; 3 - 4 - import { CaptionListItem, PlayerMeta } from "@/stores/player/slices/source"; 5 - 6 - // Helper function to convert language names to language codes 7 - function labelToLanguageCode(languageName: string): string { 8 - const languageMap: Record<string, string> = { 9 - English: "en", 10 - Spanish: "es", 11 - French: "fr", 12 - German: "de", 13 - Italian: "it", 14 - Portuguese: "pt", 15 - Russian: "ru", 16 - Japanese: "ja", 17 - Korean: "ko", 18 - Chinese: "zh", 19 - Arabic: "ar", 20 - Hindi: "hi", 21 - Turkish: "tr", 22 - Dutch: "nl", 23 - Polish: "pl", 24 - Swedish: "sv", 25 - Norwegian: "no", 26 - Danish: "da", 27 - Finnish: "fi", 28 - Greek: "el", 29 - Hebrew: "he", 30 - Thai: "th", 31 - Vietnamese: "vi", 32 - Indonesian: "id", 33 - Malay: "ms", 34 - Filipino: "tl", 35 - Ukrainian: "uk", 36 - Romanian: "ro", 37 - Czech: "cs", 38 - Hungarian: "hu", 39 - Bulgarian: "bg", 40 - Croatian: "hr", 41 - Serbian: "sr", 42 - Slovak: "sk", 43 - Slovenian: "sl", 44 - Estonian: "et", 45 - Latvian: "lv", 46 - Lithuanian: "lt", 47 - Icelandic: "is", 48 - Maltese: "mt", 49 - Georgian: "ka", 50 - Armenian: "hy", 51 - Azerbaijani: "az", 52 - Kazakh: "kk", 53 - Kyrgyz: "ky", 54 - Uzbek: "uz", 55 - Tajik: "tg", 56 - Turkmen: "tk", 57 - Mongolian: "mn", 58 - Persian: "fa", 59 - Urdu: "ur", 60 - Bengali: "bn", 61 - Tamil: "ta", 62 - Telugu: "te", 63 - Marathi: "mr", 64 - Gujarati: "gu", 65 - Kannada: "kn", 66 - Malayalam: "ml", 67 - Punjabi: "pa", 68 - Sinhala: "si", 69 - Nepali: "ne", 70 - Burmese: "my", 71 - Khmer: "km", 72 - Lao: "lo", 73 - Tibetan: "bo", 74 - Uyghur: "ug", 75 - Kurdish: "ku", 76 - Pashto: "ps", 77 - Dari: "prs", 78 - Sindhi: "sd", 79 - Kashmiri: "ks", 80 - Dogri: "doi", 81 - Konkani: "kok", 82 - Manipuri: "mni", 83 - Bodo: "brx", 84 - Sanskrit: "sa", 85 - Santhali: "sat", 86 - Maithili: "mai", 87 - Bhojpuri: "bho", 88 - Awadhi: "awa", 89 - Chhattisgarhi: "hne", 90 - Magahi: "mag", 91 - Rajasthani: "raj", 92 - Malvi: "mup", 93 - Bundeli: "bns", 94 - Bagheli: "bfy", 95 - Pahari: "phr", 96 - Kumaoni: "kfy", 97 - Garhwali: "gbm", 98 - Kangri: "xnr", 99 - }; 100 - 101 - return languageMap[languageName] || languageName.toLowerCase(); 102 - } 103 - 104 - export async function scrapeWyzieCaptions( 105 - tmdbId: string | number, 106 - imdbId: string, 107 - season?: number, 108 - episode?: number, 109 - ): Promise<CaptionListItem[]> { 110 - try { 111 - const searchParams: any = { 112 - encoding: "utf-8", 113 - source: "all", 114 - imdb_id: imdbId, 115 - }; 116 - 117 - if (tmdbId && !imdbId) { 118 - searchParams.tmdb_id = 119 - typeof tmdbId === "string" ? parseInt(tmdbId, 10) : tmdbId; 120 - } 121 - 122 - if (season && episode) { 123 - searchParams.season = season; 124 - searchParams.episode = episode; 125 - } 126 - 127 - console.log("Searching Wyzie subtitles with params:", searchParams); 128 - const wyzieSubtitles: SubtitleData[] = await searchSubtitles(searchParams); 129 - 130 - const wyzieCaptions: CaptionListItem[] = wyzieSubtitles.map((subtitle) => ({ 131 - id: subtitle.id, 132 - language: subtitle.language, 133 - url: subtitle.url, 134 - type: 135 - subtitle.format === "srt" || subtitle.format === "vtt" 136 - ? subtitle.format 137 - : "srt", 138 - needsProxy: false, 139 - opensubtitles: true, 140 - // Additional metadata from Wyzie 141 - display: subtitle.display, 142 - media: subtitle.media, 143 - isHearingImpaired: subtitle.isHearingImpaired, 144 - source: `wyzie ${subtitle.source.toString() === "opensubtitles" ? "opensubs" : subtitle.source}`, 145 - encoding: subtitle.encoding, 146 - })); 147 - 148 - return wyzieCaptions; 149 - } catch (error) { 150 - console.error("Error fetching Wyzie subtitles:", error); 151 - return []; 152 - } 153 - } 154 - 155 - export async function scrapeOpenSubtitlesCaptions( 156 - imdbId: string, 157 - season?: number, 158 - episode?: number, 159 - ): Promise<CaptionListItem[]> { 160 - try { 161 - const url = `https://rest.opensubtitles.org/search/${ 162 - season && episode ? `episode-${episode}/` : "" 163 - }imdbid-${imdbId.slice(2)}${season && episode ? `/season-${season}` : ""}`; 164 - 165 - const response = await fetch(url, { 166 - headers: { 167 - "X-User-Agent": "VLSub 0.10.2", 168 - }, 169 - }); 170 - 171 - if (!response.ok) { 172 - throw new Error(`OpenSubtitles API returned ${response.status}`); 173 - } 174 - 175 - const data = await response.json(); 176 - const openSubtitlesCaptions: CaptionListItem[] = []; 177 - 178 - for (const caption of data) { 179 - const downloadUrl = caption.SubDownloadLink.replace(".gz", "").replace( 180 - "download/", 181 - "download/subencoding-utf8/", 182 - ); 183 - const language = labelToLanguageCode(caption.LanguageName); 184 - 185 - if (!downloadUrl || !language) continue; 186 - 187 - openSubtitlesCaptions.push({ 188 - id: downloadUrl, 189 - language, 190 - url: downloadUrl, 191 - type: caption.SubFormat || "srt", 192 - needsProxy: false, 193 - opensubtitles: true, 194 - source: "opensubs", // shortened becuase used on CaptionView for badge 195 - }); 196 - } 197 - 198 - return openSubtitlesCaptions; 199 - } catch (error) { 200 - console.error("Error fetching OpenSubtitles:", error); 201 - return []; 202 - } 203 - } 204 - 205 - export async function scrapeFebboxCaptions( 206 - imdbId: string, 207 - season?: number, 208 - episode?: number, 209 - ): Promise<CaptionListItem[]> { 210 - try { 211 - let url: string; 212 - if (season && episode) { 213 - url = `https://fed-subs.pstream.mov/tv/${imdbId}/${season}/${episode}`; 214 - } else { 215 - url = `https://fed-subs.pstream.mov/movie/${imdbId}`; 216 - } 217 - 218 - // console.log("Searching Febbox subtitles with URL:", url); 219 - 220 - const response = await fetch(url); 221 - 222 - if (!response.ok) { 223 - throw new Error(`Febbox API returned ${response.status}`); 224 - } 225 - 226 - const data = await response.json(); 227 - 228 - // Check for error response 229 - if (data.error) { 230 - console.log("Febbox API error:", data.error); 231 - return []; 232 - } 233 - 234 - // Check if subtitles exist 235 - if (!data.subtitles || typeof data.subtitles !== "object") { 236 - console.log("No subtitles found in Febbox response"); 237 - return []; 238 - } 239 - 240 - const febboxCaptions: CaptionListItem[] = []; 241 - 242 - // Iterate through all available languages 243 - for (const [languageName, subtitleData] of Object.entries(data.subtitles)) { 244 - if (typeof subtitleData === "object" && subtitleData !== null) { 245 - const subtitle = subtitleData as { 246 - subtitle_link: string; 247 - subtitle_name: string; 248 - }; 249 - 250 - if (subtitle.subtitle_link) { 251 - const language = labelToLanguageCode(languageName); 252 - const fileExtension = subtitle.subtitle_link 253 - .split(".") 254 - .pop() 255 - ?.toLowerCase(); 256 - 257 - // Determine subtitle type based on file extension 258 - let type: string = "srt"; 259 - if (fileExtension === "vtt") { 260 - type = "vtt"; 261 - } else if (fileExtension === "sub") { 262 - type = "sub"; 263 - } 264 - 265 - febboxCaptions.push({ 266 - id: subtitle.subtitle_link, 267 - language, 268 - url: subtitle.subtitle_link, 269 - type, 270 - needsProxy: false, 271 - opensubtitles: true, 272 - display: subtitle.subtitle_name, 273 - source: "febbox", 274 - }); 275 - } 276 - } 277 - } 278 - 279 - console.log(`Found ${febboxCaptions.length} Febbox subtitles`); 280 - return febboxCaptions; 281 - } catch (error) { 282 - console.error("Error fetching Febbox subtitles:", error); 283 - return []; 284 - } 285 - } 286 - 287 - export async function scrapeVdrkCaptions( 288 - tmdbId: string | number, 289 - season?: number, 290 - episode?: number, 291 - ): Promise<CaptionListItem[]> { 292 - try { 293 - const tmdbIdNum = 294 - typeof tmdbId === "string" ? parseInt(tmdbId, 10) : tmdbId; 295 - 296 - let url: string; 297 - if (season && episode) { 298 - // For TV shows: https://sub.vdrk.site/v1/tv/{tmdb_id}/{season}/{episode} 299 - url = `https://sub.vdrk.site/v1/tv/${tmdbIdNum}/${season}/${episode}`; 300 - } else { 301 - // For movies: https://sub.vdrk.site/v1/movie/{tmdb_id} 302 - url = `https://sub.vdrk.site/v1/movie/${tmdbIdNum}`; 303 - } 304 - 305 - console.log("Searching VDRK subtitles with URL:", url); 306 - 307 - const response = await fetch(url); 308 - 309 - if (!response.ok) { 310 - throw new Error(`VDRK API returned ${response.status}`); 311 - } 312 - 313 - const data = await response.json(); 314 - 315 - // Check if response is an array 316 - if (!Array.isArray(data)) { 317 - console.log("Invalid VDRK response format"); 318 - return []; 319 - } 320 - 321 - const vdrkCaptions: CaptionListItem[] = []; 322 - 323 - for (const subtitle of data) { 324 - if (subtitle.file && subtitle.label) { 325 - // Parse label to extract language and hearing impaired info 326 - const label = subtitle.label; 327 - const isHearingImpaired = label.includes(" Hi") || label.includes("Hi"); 328 - const languageName = label 329 - .replace(/\s*Hi\d*$/, "") 330 - .replace(/\s*Hi$/, "") 331 - .replace(/\d+$/, ""); 332 - const language = labelToLanguageCode(languageName); 333 - 334 - if (!language) continue; 335 - 336 - vdrkCaptions.push({ 337 - id: subtitle.file, 338 - language, 339 - url: subtitle.file, 340 - type: "vtt", // VDRK provides VTT files 341 - needsProxy: false, 342 - opensubtitles: true, 343 - display: subtitle.label, 344 - isHearingImpaired, 345 - source: "granite", 346 - }); 347 - } 348 - } 349 - 350 - console.log(`Found ${vdrkCaptions.length} VDRK subtitles`); 351 - return vdrkCaptions; 352 - } catch (error) { 353 - console.error("Error fetching VDRK subtitles:", error); 354 - return []; 355 - } 356 - } 357 - 358 - export async function scrapeExternalSubtitles( 359 - meta: PlayerMeta, 360 - ): Promise<CaptionListItem[]> { 361 - try { 362 - // Extract IMDb ID from meta 363 - const imdbId = meta.imdbId; 364 - if (!imdbId) { 365 - console.log("No IMDb ID available for external subtitle scraping"); 366 - return []; 367 - } 368 - 369 - const season = meta.season?.number; 370 - const episode = meta.episode?.number; 371 - const tmdbId = meta.tmdbId; 372 - 373 - // Set a reasonable timeout for each source (10 seconds) 374 - const timeout = 10000; 375 - 376 - // Create promises for each source with individual timeouts 377 - const wyziePromise = scrapeWyzieCaptions(tmdbId, imdbId, season, episode); 378 - const openSubsPromise = scrapeOpenSubtitlesCaptions( 379 - imdbId, 380 - season, 381 - episode, 382 - ); 383 - // const febboxPromise = scrapeFebboxCaptions(imdbId, season, episode); 384 - const vdrkPromise = scrapeVdrkCaptions(tmdbId, season, episode); 385 - 386 - // Create timeout promises 387 - const timeoutPromise = new Promise<CaptionListItem[]>((resolve) => { 388 - setTimeout(() => resolve([]), timeout); 389 - }); 390 - 391 - // Start all promises and collect results as they complete 392 - const allCaptions: CaptionListItem[] = []; 393 - let completedSources = 0; 394 - const totalSources = 3; 395 - 396 - // Helper function to handle individual source completion 397 - const handleSourceCompletion = ( 398 - sourceName: string, 399 - captions: CaptionListItem[], 400 - ) => { 401 - allCaptions.push(...captions); 402 - completedSources += 1; 403 - console.log( 404 - `${sourceName} completed with ${captions.length} captions (${completedSources}/${totalSources} sources done)`, 405 - ); 406 - }; 407 - 408 - // Start all sources concurrently and handle them as they complete 409 - const promises = [ 410 - Promise.race([wyziePromise, timeoutPromise]).then((captions) => { 411 - handleSourceCompletion("Wyzie", captions); 412 - return captions; 413 - }), 414 - Promise.race([openSubsPromise, timeoutPromise]).then((captions) => { 415 - handleSourceCompletion("OpenSubtitles", captions); 416 - return captions; 417 - }), 418 - // Promise.race([febboxPromise, timeoutPromise]).then((captions) => { 419 - // handleSourceCompletion("Febbox", captions); 420 - // return captions; 421 - // }), 422 - Promise.race([vdrkPromise, timeoutPromise]).then((captions) => { 423 - handleSourceCompletion("Granite", captions); 424 - return captions; 425 - }), 426 - ]; 427 - 428 - // Wait for all sources to complete (with timeouts) 429 - await Promise.allSettled(promises); 430 - 431 - console.log( 432 - `Found ${allCaptions.length} total external captions from all sources`, 433 - ); 434 - 435 - return allCaptions; 436 - } catch (error) { 437 - console.error("Error in scrapeExternalSubtitles:", error); 438 - return []; 439 - } 440 - }
+182
src/utils/externalSubtitles/febbox.ts
··· 1 + /* eslint-disable no-console */ 2 + import { CaptionListItem } from "@/stores/player/slices/source"; 3 + 4 + // Helper function to convert language names to language codes 5 + function labelToLanguageCode(languageName: string): string { 6 + const languageMap: Record<string, string> = { 7 + English: "en", 8 + Spanish: "es", 9 + French: "fr", 10 + German: "de", 11 + Italian: "it", 12 + Portuguese: "pt", 13 + Russian: "ru", 14 + Japanese: "ja", 15 + Korean: "ko", 16 + Chinese: "zh", 17 + Arabic: "ar", 18 + Hindi: "hi", 19 + Turkish: "tr", 20 + Dutch: "nl", 21 + Polish: "pl", 22 + Swedish: "sv", 23 + Norwegian: "no", 24 + Danish: "da", 25 + Finnish: "fi", 26 + Greek: "el", 27 + Hebrew: "he", 28 + Thai: "th", 29 + Vietnamese: "vi", 30 + Indonesian: "id", 31 + Malay: "ms", 32 + Filipino: "tl", 33 + Ukrainian: "uk", 34 + Romanian: "ro", 35 + Czech: "cs", 36 + Hungarian: "hu", 37 + Bulgarian: "bg", 38 + Croatian: "hr", 39 + Serbian: "sr", 40 + Slovak: "sk", 41 + Slovenian: "sl", 42 + Estonian: "et", 43 + Latvian: "lv", 44 + Lithuanian: "lt", 45 + Icelandic: "is", 46 + Maltese: "mt", 47 + Georgian: "ka", 48 + Armenian: "hy", 49 + Azerbaijani: "az", 50 + Kazakh: "kk", 51 + Kyrgyz: "ky", 52 + Uzbek: "uz", 53 + Tajik: "tg", 54 + Turkmen: "tk", 55 + Mongolian: "mn", 56 + Persian: "fa", 57 + Urdu: "ur", 58 + Bengali: "bn", 59 + Tamil: "ta", 60 + Telugu: "te", 61 + Marathi: "mr", 62 + Gujarati: "gu", 63 + Kannada: "kn", 64 + Malayalam: "ml", 65 + Punjabi: "pa", 66 + Sinhala: "si", 67 + Nepali: "ne", 68 + Burmese: "my", 69 + Khmer: "km", 70 + Lao: "lo", 71 + Tibetan: "bo", 72 + Uyghur: "ug", 73 + Kurdish: "ku", 74 + Pashto: "ps", 75 + Dari: "prs", 76 + Sindhi: "sd", 77 + Kashmiri: "ks", 78 + Dogri: "doi", 79 + Konkani: "kok", 80 + Manipuri: "mni", 81 + Bodo: "brx", 82 + Sanskrit: "sa", 83 + Santhali: "sat", 84 + Maithili: "mai", 85 + Bhojpuri: "bho", 86 + Awadhi: "awa", 87 + Chhattisgarhi: "hne", 88 + Magahi: "mag", 89 + Rajasthani: "raj", 90 + Malvi: "mup", 91 + Bundeli: "bns", 92 + Bagheli: "bfy", 93 + Pahari: "phr", 94 + Kumaoni: "kfy", 95 + Garhwali: "gbm", 96 + Kangri: "xnr", 97 + }; 98 + 99 + return languageMap[languageName] || languageName.toLowerCase(); 100 + } 101 + 102 + export async function scrapeFebboxCaptions( 103 + imdbId: string, 104 + season?: number, 105 + episode?: number, 106 + ): Promise<CaptionListItem[]> { 107 + try { 108 + let url: string; 109 + if (season && episode) { 110 + url = `https://fed-subs.pstream.mov/tv/${imdbId}/${season}/${episode}`; 111 + } else { 112 + url = `https://fed-subs.pstream.mov/movie/${imdbId}`; 113 + } 114 + 115 + // console.log("Searching Febbox subtitles with URL:", url); 116 + 117 + const response = await fetch(url); 118 + 119 + if (!response.ok) { 120 + throw new Error(`Febbox API returned ${response.status}`); 121 + } 122 + 123 + const data = await response.json(); 124 + 125 + // Check for error response 126 + if (data.error) { 127 + console.log("Febbox API error:", data.error); 128 + return []; 129 + } 130 + 131 + // Check if subtitles exist 132 + if (!data.subtitles || typeof data.subtitles !== "object") { 133 + console.log("No subtitles found in Febbox response"); 134 + return []; 135 + } 136 + 137 + const febboxCaptions: CaptionListItem[] = []; 138 + 139 + // Iterate through all available languages 140 + for (const [languageName, subtitleData] of Object.entries(data.subtitles)) { 141 + if (typeof subtitleData === "object" && subtitleData !== null) { 142 + const subtitle = subtitleData as { 143 + subtitle_link: string; 144 + subtitle_name: string; 145 + }; 146 + 147 + if (subtitle.subtitle_link) { 148 + const language = labelToLanguageCode(languageName); 149 + const fileExtension = subtitle.subtitle_link 150 + .split(".") 151 + .pop() 152 + ?.toLowerCase(); 153 + 154 + // Determine subtitle type based on file extension 155 + let type: string = "srt"; 156 + if (fileExtension === "vtt") { 157 + type = "vtt"; 158 + } else if (fileExtension === "sub") { 159 + type = "sub"; 160 + } 161 + 162 + febboxCaptions.push({ 163 + id: subtitle.subtitle_link, 164 + language, 165 + url: subtitle.subtitle_link, 166 + type, 167 + needsProxy: false, 168 + opensubtitles: true, 169 + display: subtitle.subtitle_name, 170 + source: "febbox", 171 + }); 172 + } 173 + } 174 + } 175 + 176 + console.log(`Found ${febboxCaptions.length} Febbox subtitles`); 177 + return febboxCaptions; 178 + } catch (error) { 179 + console.error("Error fetching Febbox subtitles:", error); 180 + return []; 181 + } 182 + }
+100
src/utils/externalSubtitles/index.ts
··· 1 + /* eslint-disable no-console */ 2 + import { PlayerMeta } from "@/stores/player/slices/source"; 3 + 4 + import { scrapeFebboxCaptions as _scrapeFebboxCaptions } from "./febbox"; 5 + import { scrapeOpenSubtitlesCaptions } from "./opensubtitles"; 6 + import { scrapeVdrkCaptions } from "./vdrk"; 7 + import { scrapeWyzieCaptions } from "./wyzie"; 8 + 9 + export async function scrapeExternalSubtitles( 10 + meta: PlayerMeta, 11 + ): Promise<import("@/stores/player/slices/source").CaptionListItem[]> { 12 + try { 13 + // Extract IMDb ID from meta 14 + const imdbId = meta.imdbId; 15 + if (!imdbId) { 16 + console.log("No IMDb ID available for external subtitle scraping"); 17 + return []; 18 + } 19 + 20 + const season = meta.season?.number; 21 + const episode = meta.episode?.number; 22 + const tmdbId = meta.tmdbId; 23 + 24 + // Set a reasonable timeout for each source (10 seconds) 25 + const timeout = 10000; 26 + 27 + // Create promises for each source with individual timeouts 28 + const wyziePromise = scrapeWyzieCaptions(tmdbId, imdbId, season, episode); 29 + const openSubsPromise = scrapeOpenSubtitlesCaptions( 30 + imdbId, 31 + season, 32 + episode, 33 + ); 34 + // const febboxPromise = scrapeFebboxCaptions(imdbId, season, episode); 35 + const vdrkPromise = scrapeVdrkCaptions(tmdbId, season, episode); 36 + 37 + // Create timeout promises 38 + const timeoutPromise = new Promise< 39 + import("@/stores/player/slices/source").CaptionListItem[] 40 + >((resolve) => { 41 + setTimeout(() => resolve([]), timeout); 42 + }); 43 + 44 + // Start all promises and collect results as they complete 45 + const allCaptions: import("@/stores/player/slices/source").CaptionListItem[] = 46 + []; 47 + let completedSources = 0; 48 + const totalSources = 3; 49 + 50 + // Helper function to handle individual source completion 51 + const handleSourceCompletion = ( 52 + sourceName: string, 53 + captions: import("@/stores/player/slices/source").CaptionListItem[], 54 + ) => { 55 + allCaptions.push(...captions); 56 + completedSources += 1; 57 + console.log( 58 + `${sourceName} completed with ${captions.length} captions (${completedSources}/${totalSources} sources done)`, 59 + ); 60 + }; 61 + 62 + // Start all sources concurrently and handle them as they complete 63 + const promises = [ 64 + Promise.race([wyziePromise, timeoutPromise]).then((captions) => { 65 + handleSourceCompletion("Wyzie", captions); 66 + return captions; 67 + }), 68 + Promise.race([openSubsPromise, timeoutPromise]).then((captions) => { 69 + handleSourceCompletion("OpenSubtitles", captions); 70 + return captions; 71 + }), 72 + // Promise.race([febboxPromise, timeoutPromise]).then((captions) => { 73 + // handleSourceCompletion("Febbox", captions); 74 + // return captions; 75 + // }), 76 + Promise.race([vdrkPromise, timeoutPromise]).then((captions) => { 77 + handleSourceCompletion("Granite", captions); 78 + return captions; 79 + }), 80 + ]; 81 + 82 + // Wait for all sources to complete (with timeouts) 83 + await Promise.allSettled(promises); 84 + 85 + console.log( 86 + `Found ${allCaptions.length} total external captions from all sources`, 87 + ); 88 + 89 + return allCaptions; 90 + } catch (error) { 91 + console.error("Error in scrapeExternalSubtitles:", error); 92 + return []; 93 + } 94 + } 95 + 96 + // Re-export individual functions for direct access if needed 97 + export { scrapeWyzieCaptions } from "./wyzie"; 98 + export { scrapeOpenSubtitlesCaptions } from "./opensubtitles"; 99 + export { scrapeFebboxCaptions } from "./febbox"; 100 + export { scrapeVdrkCaptions } from "./vdrk";
+150
src/utils/externalSubtitles/opensubtitles.ts
··· 1 + /* eslint-disable no-console */ 2 + import { CaptionListItem } from "@/stores/player/slices/source"; 3 + 4 + // Helper function to convert language names to language codes 5 + function labelToLanguageCode(languageName: string): string { 6 + const languageMap: Record<string, string> = { 7 + English: "en", 8 + Spanish: "es", 9 + French: "fr", 10 + German: "de", 11 + Italian: "it", 12 + Portuguese: "pt", 13 + Russian: "ru", 14 + Japanese: "ja", 15 + Korean: "ko", 16 + Chinese: "zh", 17 + Arabic: "ar", 18 + Hindi: "hi", 19 + Turkish: "tr", 20 + Dutch: "nl", 21 + Polish: "pl", 22 + Swedish: "sv", 23 + Norwegian: "no", 24 + Danish: "da", 25 + Finnish: "fi", 26 + Greek: "el", 27 + Hebrew: "he", 28 + Thai: "th", 29 + Vietnamese: "vi", 30 + Indonesian: "id", 31 + Malay: "ms", 32 + Filipino: "tl", 33 + Ukrainian: "uk", 34 + Romanian: "ro", 35 + Czech: "cs", 36 + Hungarian: "hu", 37 + Bulgarian: "bg", 38 + Croatian: "hr", 39 + Serbian: "sr", 40 + Slovak: "sk", 41 + Slovenian: "sl", 42 + Estonian: "et", 43 + Latvian: "lv", 44 + Lithuanian: "lt", 45 + Icelandic: "is", 46 + Maltese: "mt", 47 + Georgian: "ka", 48 + Armenian: "hy", 49 + Azerbaijani: "az", 50 + Kazakh: "kk", 51 + Kyrgyz: "ky", 52 + Uzbek: "uz", 53 + Tajik: "tg", 54 + Turkmen: "tk", 55 + Mongolian: "mn", 56 + Persian: "fa", 57 + Urdu: "ur", 58 + Bengali: "bn", 59 + Tamil: "ta", 60 + Telugu: "te", 61 + Marathi: "mr", 62 + Gujarati: "gu", 63 + Kannada: "kn", 64 + Malayalam: "ml", 65 + Punjabi: "pa", 66 + Sinhala: "si", 67 + Nepali: "ne", 68 + Burmese: "my", 69 + Khmer: "km", 70 + Lao: "lo", 71 + Tibetan: "bo", 72 + Uyghur: "ug", 73 + Kurdish: "ku", 74 + Pashto: "ps", 75 + Dari: "prs", 76 + Sindhi: "sd", 77 + Kashmiri: "ks", 78 + Dogri: "doi", 79 + Konkani: "kok", 80 + Manipuri: "mni", 81 + Bodo: "brx", 82 + Sanskrit: "sa", 83 + Santhali: "sat", 84 + Maithili: "mai", 85 + Bhojpuri: "bho", 86 + Awadhi: "awa", 87 + Chhattisgarhi: "hne", 88 + Magahi: "mag", 89 + Rajasthani: "raj", 90 + Malvi: "mup", 91 + Bundeli: "bns", 92 + Bagheli: "bfy", 93 + Pahari: "phr", 94 + Kumaoni: "kfy", 95 + Garhwali: "gbm", 96 + Kangri: "xnr", 97 + }; 98 + 99 + return languageMap[languageName] || languageName.toLowerCase(); 100 + } 101 + 102 + export async function scrapeOpenSubtitlesCaptions( 103 + imdbId: string, 104 + season?: number, 105 + episode?: number, 106 + ): Promise<CaptionListItem[]> { 107 + try { 108 + const url = `https://rest.opensubtitles.org/search/${ 109 + season && episode ? `episode-${episode}/` : "" 110 + }imdbid-${imdbId.slice(2)}${season && episode ? `/season-${season}` : ""}`; 111 + 112 + const response = await fetch(url, { 113 + headers: { 114 + "X-User-Agent": "VLSub 0.10.2", 115 + }, 116 + }); 117 + 118 + if (!response.ok) { 119 + throw new Error(`OpenSubtitles API returned ${response.status}`); 120 + } 121 + 122 + const data = await response.json(); 123 + const openSubtitlesCaptions: CaptionListItem[] = []; 124 + 125 + for (const caption of data) { 126 + const downloadUrl = caption.SubDownloadLink.replace(".gz", "").replace( 127 + "download/", 128 + "download/subencoding-utf8/", 129 + ); 130 + const language = labelToLanguageCode(caption.LanguageName); 131 + 132 + if (!downloadUrl || !language) continue; 133 + 134 + openSubtitlesCaptions.push({ 135 + id: downloadUrl, 136 + language, 137 + url: downloadUrl, 138 + type: caption.SubFormat || "srt", 139 + needsProxy: false, 140 + opensubtitles: true, 141 + source: "opensubs", // shortened becuase used on CaptionView for badge 142 + }); 143 + } 144 + 145 + return openSubtitlesCaptions; 146 + } catch (error) { 147 + console.error("Error fetching OpenSubtitles:", error); 148 + return []; 149 + } 150 + }
+171
src/utils/externalSubtitles/vdrk.ts
··· 1 + /* eslint-disable no-console */ 2 + import { CaptionListItem } from "@/stores/player/slices/source"; 3 + 4 + // Helper function to convert language names to language codes 5 + function labelToLanguageCode(languageName: string): string { 6 + const languageMap: Record<string, string> = { 7 + English: "en", 8 + Spanish: "es", 9 + French: "fr", 10 + German: "de", 11 + Italian: "it", 12 + Portuguese: "pt", 13 + Russian: "ru", 14 + Japanese: "ja", 15 + Korean: "ko", 16 + Chinese: "zh", 17 + Arabic: "ar", 18 + Hindi: "hi", 19 + Turkish: "tr", 20 + Dutch: "nl", 21 + Polish: "pl", 22 + Swedish: "sv", 23 + Norwegian: "no", 24 + Danish: "da", 25 + Finnish: "fi", 26 + Greek: "el", 27 + Hebrew: "he", 28 + Thai: "th", 29 + Vietnamese: "vi", 30 + Indonesian: "id", 31 + Malay: "ms", 32 + Filipino: "tl", 33 + Ukrainian: "uk", 34 + Romanian: "ro", 35 + Czech: "cs", 36 + Hungarian: "hu", 37 + Bulgarian: "bg", 38 + Croatian: "hr", 39 + Serbian: "sr", 40 + Slovak: "sk", 41 + Slovenian: "sl", 42 + Estonian: "et", 43 + Latvian: "lv", 44 + Lithuanian: "lt", 45 + Icelandic: "is", 46 + Maltese: "mt", 47 + Georgian: "ka", 48 + Armenian: "hy", 49 + Azerbaijani: "az", 50 + Kazakh: "kk", 51 + Kyrgyz: "ky", 52 + Uzbek: "uz", 53 + Tajik: "tg", 54 + Turkmen: "tk", 55 + Mongolian: "mn", 56 + Persian: "fa", 57 + Urdu: "ur", 58 + Bengali: "bn", 59 + Tamil: "ta", 60 + Telugu: "te", 61 + Marathi: "mr", 62 + Gujarati: "gu", 63 + Kannada: "kn", 64 + Malayalam: "ml", 65 + Punjabi: "pa", 66 + Sinhala: "si", 67 + Nepali: "ne", 68 + Burmese: "my", 69 + Khmer: "km", 70 + Lao: "lo", 71 + Tibetan: "bo", 72 + Uyghur: "ug", 73 + Kurdish: "ku", 74 + Pashto: "ps", 75 + Dari: "prs", 76 + Sindhi: "sd", 77 + Kashmiri: "ks", 78 + Dogri: "doi", 79 + Konkani: "kok", 80 + Manipuri: "mni", 81 + Bodo: "brx", 82 + Sanskrit: "sa", 83 + Santhali: "sat", 84 + Maithili: "mai", 85 + Bhojpuri: "bho", 86 + Awadhi: "awa", 87 + Chhattisgarhi: "hne", 88 + Magahi: "mag", 89 + Rajasthani: "raj", 90 + Malvi: "mup", 91 + Bundeli: "bns", 92 + Bagheli: "bfy", 93 + Pahari: "phr", 94 + Kumaoni: "kfy", 95 + Garhwali: "gbm", 96 + Kangri: "xnr", 97 + }; 98 + 99 + return languageMap[languageName] || languageName.toLowerCase(); 100 + } 101 + 102 + export async function scrapeVdrkCaptions( 103 + tmdbId: string | number, 104 + season?: number, 105 + episode?: number, 106 + ): Promise<CaptionListItem[]> { 107 + try { 108 + const tmdbIdNum = 109 + typeof tmdbId === "string" ? parseInt(tmdbId, 10) : tmdbId; 110 + 111 + let url: string; 112 + if (season && episode) { 113 + // For TV shows: https://sub.vdrk.site/v1/tv/{tmdb_id}/{season}/{episode} 114 + url = `https://sub.vdrk.site/v1/tv/${tmdbIdNum}/${season}/${episode}`; 115 + } else { 116 + // For movies: https://sub.vdrk.site/v1/movie/{tmdb_id} 117 + url = `https://sub.vdrk.site/v1/movie/${tmdbIdNum}`; 118 + } 119 + 120 + console.log("Searching VDRK subtitles with URL:", url); 121 + 122 + const response = await fetch(url); 123 + 124 + if (!response.ok) { 125 + throw new Error(`VDRK API returned ${response.status}`); 126 + } 127 + 128 + const data = await response.json(); 129 + 130 + // Check if response is an array 131 + if (!Array.isArray(data)) { 132 + console.log("Invalid VDRK response format"); 133 + return []; 134 + } 135 + 136 + const vdrkCaptions: CaptionListItem[] = []; 137 + 138 + for (const subtitle of data) { 139 + if (subtitle.file && subtitle.label) { 140 + // Parse label to extract language and hearing impaired info 141 + const label = subtitle.label; 142 + const isHearingImpaired = label.includes(" Hi") || label.includes("Hi"); 143 + const languageName = label 144 + .replace(/\s*Hi\d*$/, "") 145 + .replace(/\s*Hi$/, "") 146 + .replace(/\d+$/, ""); 147 + const language = labelToLanguageCode(languageName); 148 + 149 + if (!language) continue; 150 + 151 + vdrkCaptions.push({ 152 + id: subtitle.file, 153 + language, 154 + url: subtitle.file, 155 + type: "vtt", // VDRK provides VTT files 156 + needsProxy: false, 157 + opensubtitles: true, 158 + display: subtitle.label, 159 + isHearingImpaired, 160 + source: "granite", 161 + }); 162 + } 163 + } 164 + 165 + console.log(`Found ${vdrkCaptions.length} VDRK subtitles`); 166 + return vdrkCaptions; 167 + } catch (error) { 168 + console.error("Error fetching VDRK subtitles:", error); 169 + return []; 170 + } 171 + }
+55
src/utils/externalSubtitles/wyzie.ts
··· 1 + /* eslint-disable no-console */ 2 + import { type SubtitleData, searchSubtitles } from "wyzie-lib"; 3 + 4 + import { CaptionListItem } from "@/stores/player/slices/source"; 5 + 6 + export async function scrapeWyzieCaptions( 7 + tmdbId: string | number, 8 + imdbId: string, 9 + season?: number, 10 + episode?: number, 11 + ): Promise<CaptionListItem[]> { 12 + try { 13 + const searchParams: any = { 14 + encoding: "utf-8", 15 + source: "all", 16 + imdb_id: imdbId, 17 + }; 18 + 19 + if (tmdbId && !imdbId) { 20 + searchParams.tmdb_id = 21 + typeof tmdbId === "string" ? parseInt(tmdbId, 10) : tmdbId; 22 + } 23 + 24 + if (season && episode) { 25 + searchParams.season = season; 26 + searchParams.episode = episode; 27 + } 28 + 29 + console.log("Searching Wyzie subtitles with params:", searchParams); 30 + const wyzieSubtitles: SubtitleData[] = await searchSubtitles(searchParams); 31 + 32 + const wyzieCaptions: CaptionListItem[] = wyzieSubtitles.map((subtitle) => ({ 33 + id: subtitle.id, 34 + language: subtitle.language, 35 + url: subtitle.url, 36 + type: 37 + subtitle.format === "srt" || subtitle.format === "vtt" 38 + ? subtitle.format 39 + : "srt", 40 + needsProxy: false, 41 + opensubtitles: true, 42 + // Additional metadata from Wyzie 43 + display: subtitle.display, 44 + media: subtitle.media, 45 + isHearingImpaired: subtitle.isHearingImpaired, 46 + source: `wyzie ${subtitle.source.toString() === "opensubtitles" ? "opensubs" : subtitle.source}`, 47 + encoding: subtitle.encoding, 48 + })); 49 + 50 + return wyzieCaptions; 51 + } catch (error) { 52 + console.error("Error fetching Wyzie subtitles:", error); 53 + return []; 54 + } 55 + }