@@ -7,12 +7,13 @@ import type {
77 TmdbTvEpisodeResult ,
88 TmdbTvSeasonResult ,
99} from '@server/api/themoviedb/interfaces' ;
10- import type {
11- TvdbBaseResponse ,
12- TvdbEpisode ,
13- TvdbLoginResponse ,
14- TvdbSeasonDetails ,
15- TvdbTvDetails ,
10+ import {
11+ convertTmdbLanguageToTvdbWithFallback ,
12+ type TvdbBaseResponse ,
13+ type TvdbEpisode ,
14+ type TvdbLoginResponse ,
15+ type TvdbSeasonDetails ,
16+ type TvdbTvDetails ,
1617} from '@server/api/tvdb/interfaces' ;
1718import cacheManager , { type AvailableCacheIds } from '@server/lib/cache' ;
1819import logger from '@server/logger' ;
@@ -215,7 +216,12 @@ class Tvdb extends ExternalAPI implements TvShowProvider {
215216 return await this . tmdb . getTvSeason ( { tvId, seasonNumber, language } ) ;
216217 }
217218
218- return await this . getTvdbSeasonData ( tvdbId , seasonNumber , tvId ) ;
219+ return await this . getTvdbSeasonData (
220+ tvdbId ,
221+ seasonNumber ,
222+ tvId ,
223+ language
224+ ) ;
219225 } catch ( error ) {
220226 this . handleError ( 'Failed to fetch TV season details' , error ) ;
221227 return await this . tmdb . getTvSeason ( { tvId, seasonNumber, language } ) ;
@@ -316,8 +322,8 @@ class Tvdb extends ExternalAPI implements TvShowProvider {
316322 private async getTvdbSeasonData (
317323 tvdbId : number ,
318324 seasonNumber : number ,
319- tvId : number
320- // language: string = Tvdb.DEFAULT_LANGUAGE
325+ tvId : number ,
326+ language : string = Tvdb . DEFAULT_LANGUAGE
321327 ) : Promise < TmdbSeasonWithEpisodes > {
322328 const tvdbData = await this . fetchTvdbShowData ( tvdbId ) ;
323329
@@ -341,6 +347,132 @@ class Tvdb extends ExternalAPI implements TvShowProvider {
341347 return this . createEmptySeasonResponse ( tvId ) ;
342348 }
343349
350+ const wantedTranslation = convertTmdbLanguageToTvdbWithFallback (
351+ language ,
352+ Tvdb . DEFAULT_LANGUAGE
353+ ) ;
354+
355+ // check if translation is available for the season
356+ const availableTranslation = season . nameTranslations . filter (
357+ ( translation ) =>
358+ translation === wantedTranslation ||
359+ translation === Tvdb . DEFAULT_LANGUAGE
360+ ) ;
361+
362+ if ( ! availableTranslation ) {
363+ return this . getSeasonWithOriginalLanguage (
364+ tvdbId ,
365+ tvId ,
366+ seasonNumber ,
367+ season
368+ ) ;
369+ }
370+
371+ return this . getSeasonWithTranslation (
372+ tvdbId ,
373+ tvId ,
374+ seasonNumber ,
375+ season ,
376+ wantedTranslation
377+ ) ;
378+ }
379+
380+ private async getSeasonWithTranslation (
381+ tvdbId : number ,
382+ tvId : number ,
383+ seasonNumber : number ,
384+ season : TvdbSeasonDetails ,
385+ language : string
386+ ) : Promise < TmdbSeasonWithEpisodes > {
387+ if ( ! season ) {
388+ logger . error (
389+ `Failed to find season ${ seasonNumber } for TVDB ID: ${ tvdbId } `
390+ ) ;
391+ return this . createEmptySeasonResponse ( tvId ) ;
392+ }
393+
394+ const allEpisodes = [ ] as TvdbEpisode [ ] ;
395+ let page = 0 ;
396+ // Limit to max 50 pages to avoid infinite loops.
397+ // 50 pages with 500 items per page = 25_000 episodes in a series which should be more than enough
398+ const maxPages = 50 ;
399+
400+ while ( page < maxPages ) {
401+ const resp = await this . get < TvdbBaseResponse < TvdbSeasonDetails > > (
402+ `/series/${ tvdbId } /episodes/default/${ language } ` ,
403+ {
404+ headers : {
405+ Authorization : `Bearer ${ this . token } ` ,
406+ } ,
407+ params : {
408+ page : page ,
409+ } ,
410+ }
411+ ) ;
412+
413+ if ( ! resp ?. data ?. episodes ) {
414+ logger . warn (
415+ `No episodes found for TVDB ID: ${ tvdbId } on page ${ page } for season ${ seasonNumber } `
416+ ) ;
417+ break ;
418+ }
419+
420+ const { episodes } = resp . data ;
421+
422+ if ( ! episodes ) {
423+ logger . debug (
424+ `No more episodes found for TVDB ID: ${ tvdbId } on page ${ page } for season ${ seasonNumber } `
425+ ) ;
426+ break ;
427+ }
428+
429+ allEpisodes . push ( ...episodes ) ;
430+
431+ const hasNextPage = resp . links ?. next && episodes . length > 0 ;
432+
433+ if ( ! hasNextPage ) {
434+ break ;
435+ }
436+
437+ page ++ ;
438+ }
439+
440+ if ( page >= maxPages ) {
441+ logger . warn (
442+ `Reached max pages (${ maxPages } ) for TVDB ID: ${ tvdbId } on season ${ seasonNumber } with language ${ language } . There might be more episodes available.`
443+ ) ;
444+ }
445+
446+ const episodes = this . processEpisodes (
447+ { ...season , episodes : allEpisodes } ,
448+ seasonNumber ,
449+ tvId
450+ ) ;
451+
452+ return {
453+ episodes,
454+ external_ids : { tvdb_id : tvdbId } ,
455+ name : '' ,
456+ overview : '' ,
457+ id : season . id ,
458+ air_date : season . firstAired ,
459+ season_number : episodes . length ,
460+ } ;
461+ }
462+
463+ private async getSeasonWithOriginalLanguage (
464+ tvdbId : number ,
465+ tvId : number ,
466+ seasonNumber : number ,
467+ season : TvdbSeasonDetails
468+ ) : Promise < TmdbSeasonWithEpisodes > {
469+ if ( ! season ) {
470+ logger . error (
471+ `Failed to find season ${ seasonNumber } for TVDB ID: ${ tvdbId } `
472+ ) ;
473+ return this . createEmptySeasonResponse ( tvId ) ;
474+ }
475+
344476 const resp = await this . get < TvdbBaseResponse < TvdbSeasonDetails > > (
345477 `/seasons/${ season . id } /extended` ,
346478 {
@@ -394,7 +526,10 @@ class Tvdb extends ExternalAPI implements TvShowProvider {
394526 season_number : episode . seasonNumber ,
395527 production_code : '' ,
396528 show_id : tvId ,
397- still_path : episode . image ? episode . image : '' ,
529+ still_path :
530+ episode . image && ! episode . image . startsWith ( 'https://' )
531+ ? 'https://artworks.thetvdb.com' + episode . image
532+ : '' ,
398533 vote_average : 1 ,
399534 vote_count : 1 ,
400535 } ;
0 commit comments