@@ -102,7 +102,7 @@ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionE
102102 _logger . LogInformation ( "[DynamicLibrary] PlaybackInfoFilter: Found AIOStreams mapping for {MediaSourceId}, returning stream URL" ,
103103 selectedMediaSourceId ) ;
104104
105- var response = BuildAIOStreamsDirectResponse ( itemId , aioStreamUrl , selectedMediaSourceId ) ;
105+ var response = await BuildAIOStreamsDirectResponseAsync ( itemId , aioStreamUrl , selectedMediaSourceId , context . HttpContext . RequestAborted ) ;
106106 context . Result = new OkObjectResult ( response ) ;
107107 return ;
108108 }
@@ -698,15 +698,14 @@ private string ReplacePlaceholders(string template, BaseItemDto item, int? seaso
698698 /// <summary>
699699 /// Get the series ID for an episode based on preference with fallback.
700700 /// TV/Anime can have: IMDB, TVDB, TMDB
701+ /// For IMDB, we NEVER return episode.ProviderIds["Imdb"] as it could be an episode-specific IMDB ID.
701702 /// </summary>
702703 private ( string ? Id , string IdType ) GetSeriesId ( BaseItemDto episode , BaseItemDto ? series , PreferredProviderId preference )
703704 {
704- // Try to get IDs from series first (preferred), then from episode
705- var providerIds = series ? . ProviderIds ?? episode . ProviderIds ;
706-
707- if ( providerIds == null )
705+ // For IMDB preference, first check if episode has SeriesImdb stored (most reliable)
706+ if ( preference == PreferredProviderId . Imdb && episode . ProviderIds ? . TryGetValue ( "SeriesImdb" , out var storedSeriesImdb ) == true && ! string . IsNullOrEmpty ( storedSeriesImdb ) )
708707 {
709- return ( null , "none " ) ;
708+ return ( storedSeriesImdb , "IMDB " ) ;
710709 }
711710
712711 // Try preferred ID first, then fall back to others
@@ -721,9 +720,32 @@ private string ReplacePlaceholders(string template, BaseItemDto item, int? seaso
721720
722721 foreach ( var provider in fallbackOrder )
723722 {
724- if ( providerIds . TryGetValue ( provider , out var id ) && ! string . IsNullOrEmpty ( id ) )
723+ // For IMDB: ONLY use SeriesImdb or series.ProviderIds - NEVER episode.ProviderIds["Imdb"]
724+ // because TVDB sometimes returns episode-level IMDB IDs which we don't want
725+ if ( provider == "Imdb" )
725726 {
726- return ( id , provider . ToUpperInvariant ( ) ) ;
727+ // Check SeriesImdb first (stored in episode during creation)
728+ if ( episode . ProviderIds ? . TryGetValue ( "SeriesImdb" , out var seriesImdb ) == true && ! string . IsNullOrEmpty ( seriesImdb ) )
729+ {
730+ return ( seriesImdb , "IMDB" ) ;
731+ }
732+ // Only check series provider IDs, NOT episode provider IDs
733+ if ( series ? . ProviderIds ? . TryGetValue ( "Imdb" , out var seriesProviderImdb ) == true && ! string . IsNullOrEmpty ( seriesProviderImdb ) )
734+ {
735+ return ( seriesProviderImdb , "IMDB" ) ;
736+ }
737+ // Skip to next provider type - explicitly don't check episode.ProviderIds["Imdb"]
738+ continue ;
739+ }
740+
741+ // For non-IMDB providers, check series first, then episode
742+ if ( series ? . ProviderIds ? . TryGetValue ( provider , out var seriesId ) == true && ! string . IsNullOrEmpty ( seriesId ) )
743+ {
744+ return ( seriesId , provider . ToUpperInvariant ( ) ) ;
745+ }
746+ if ( episode . ProviderIds ? . TryGetValue ( provider , out var episodeId ) == true && ! string . IsNullOrEmpty ( episodeId ) )
747+ {
748+ return ( episodeId , provider . ToUpperInvariant ( ) ) ;
727749 }
728750 }
729751
@@ -1807,16 +1829,12 @@ private PlaybackInfoResponse BuildAIOStreamsSingleResponseForPersistedItem(
18071829
18081830 /// <summary>
18091831 /// Get the IMDB ID from a library item, checking item and series provider IDs.
1832+ /// For episodes, always prefer the series IMDB ID (required for streaming APIs).
18101833 /// </summary>
18111834 private string ? GetImdbIdFromLibraryItem ( MediaBrowser . Controller . Entities . BaseItem item )
18121835 {
1813- // Try item's IMDB ID first
1814- if ( item . ProviderIds ? . TryGetValue ( "Imdb" , out var imdbId ) == true && ! string . IsNullOrEmpty ( imdbId ) )
1815- {
1816- return imdbId ;
1817- }
1818-
1819- // For episodes, try the series IMDB ID
1836+ // For episodes, ALWAYS check series IMDB first (required for streaming APIs)
1837+ // Episode items can have episode-level IMDB IDs which don't work with streaming services
18201838 if ( item is Episode episode && episode . SeriesId != Guid . Empty )
18211839 {
18221840 var series = _libraryManager . GetItemById ( episode . SeriesId ) ;
@@ -1826,6 +1844,12 @@ private PlaybackInfoResponse BuildAIOStreamsSingleResponseForPersistedItem(
18261844 }
18271845 }
18281846
1847+ // For non-episodes, or if series IMDB not found, use item's own IMDB
1848+ if ( item . ProviderIds ? . TryGetValue ( "Imdb" , out var imdbId ) == true && ! string . IsNullOrEmpty ( imdbId ) )
1849+ {
1850+ return imdbId ;
1851+ }
1852+
18291853 return null ;
18301854 }
18311855
@@ -1880,33 +1904,34 @@ private PlaybackInfoResponse BuildAIOStreamsSingleResponse(BaseItemDto item, str
18801904 /// Build a PlaybackInfoResponse directly from AIOStreams stream URL mapping.
18811905 /// Used when the item isn't in cache but we have the MediaSource → URL mapping.
18821906 /// </summary>
1883- private PlaybackInfoResponse BuildAIOStreamsDirectResponse ( Guid itemId , string streamUrl , string mediaSourceId )
1907+ private async Task < PlaybackInfoResponse > BuildAIOStreamsDirectResponseAsync ( Guid itemId , string streamUrl , string mediaSourceId , CancellationToken cancellationToken )
18841908 {
18851909 // Get the mapping to retrieve filename hint for container detection
18861910 var mapping = _itemCache . GetAIOStreamsMapping ( mediaSourceId ) ;
18871911 var container = StreamContainerHelper . DetectContainer ( streamUrl , mapping ? . Filename ) ;
18881912
1889- // Try to get runtime from library item, otherwise use default
1913+ // Try to get runtime from library item or API
18901914 // This is critical for Android TV which won't play without RunTimeTicks
18911915 long ? runTimeTicks = null ;
18921916 string itemName = "Stream" ;
18931917
18941918 var libraryItem = _libraryManager . GetItemById ( itemId ) ;
18951919 if ( libraryItem != null )
18961920 {
1897- runTimeTicks = libraryItem . RunTimeTicks ;
18981921 itemName = libraryItem . Name ;
1899- _logger . LogInformation ( "[DynamicLibrary] BuildAIOStreamsDirectResponse: Library item {Name} has RunTimeTicks={Ticks}" ,
1922+ // Use the async method that queries TVDB/TMDB APIs for runtime
1923+ runTimeTicks = await GetRuntimeForPersistedItemAsync ( libraryItem , cancellationToken ) ;
1924+ _logger . LogInformation ( "[DynamicLibrary] BuildAIOStreamsDirectResponseAsync: Library item {Name} has RunTimeTicks={Ticks} (from API lookup)" ,
19001925 itemName , runTimeTicks ) ;
19011926 }
19021927
1903- // Fallback to default duration if we don't have a runtime
1928+ // Fallback to default duration if API lookup failed or no library item
19041929 if ( ! runTimeTicks . HasValue || runTimeTicks . Value <= 0 )
19051930 {
1906- // Default to 2 hours for movies (most common case for direct mapping)
1907- var defaultMinutes = 120 ;
1931+ // Use smart defaults based on item type
1932+ var defaultMinutes = libraryItem is MediaBrowser . Controller . Entities . TV . Episode ? 24 : 120 ;
19081933 runTimeTicks = defaultMinutes * 60L * 10_000_000L ;
1909- _logger . LogInformation ( "[DynamicLibrary] BuildAIOStreamsDirectResponse : Using default runtime {Minutes} min for {Name}" ,
1934+ _logger . LogWarning ( "[DynamicLibrary] BuildAIOStreamsDirectResponseAsync : Using fallback runtime {Minutes} min for {Name}" ,
19101935 defaultMinutes , itemName ) ;
19111936 }
19121937
@@ -1950,25 +1975,36 @@ private PlaybackInfoResponse BuildAIOStreamsDirectResponse(Guid itemId, string s
19501975
19511976 /// <summary>
19521977 /// Get the IMDB ID for an item, checking both item and series provider IDs.
1978+ /// For episodes, always prefer the series IMDB ID (required for AIOStreams).
19531979 /// </summary>
19541980 private string ? GetImdbIdForItem ( BaseItemDto item )
19551981 {
1956- // Try item's IMDB ID first
1957- if ( item . ProviderIds ? . TryGetValue ( "Imdb" , out var imdbId ) == true && ! string . IsNullOrEmpty ( imdbId ) )
1982+ // For episodes, ALWAYS prefer series IMDB ID (required for AIOStreams)
1983+ if ( item . Type == BaseItemKind . Episode )
19581984 {
1959- return imdbId ;
1960- }
1985+ // First check if episode has SeriesImdb stored directly (most reliable)
1986+ if ( item . ProviderIds ? . TryGetValue ( "SeriesImdb" , out var storedSeriesImdb ) == true && ! string . IsNullOrEmpty ( storedSeriesImdb ) )
1987+ {
1988+ return storedSeriesImdb ;
1989+ }
19611990
1962- // For episodes, try the series IMDB ID
1963- if ( item . Type == BaseItemKind . Episode && item . SeriesId . HasValue )
1964- {
1965- var series = _itemCache . GetItem ( item . SeriesId . Value ) ;
1966- if ( series ? . ProviderIds ? . TryGetValue ( "Imdb" , out var seriesImdbId ) == true && ! string . IsNullOrEmpty ( seriesImdbId ) )
1991+ // Fall back to cache lookup if episode doesn't have SeriesImdb
1992+ if ( item . SeriesId . HasValue )
19671993 {
1968- return seriesImdbId ;
1994+ var series = _itemCache . GetItem ( item . SeriesId . Value ) ;
1995+ if ( series ? . ProviderIds ? . TryGetValue ( "Imdb" , out var seriesImdbId ) == true && ! string . IsNullOrEmpty ( seriesImdbId ) )
1996+ {
1997+ return seriesImdbId ;
1998+ }
19691999 }
19702000 }
19712001
2002+ // For non-episodes, or if series IMDB not found, try item's own IMDB ID
2003+ if ( item . ProviderIds ? . TryGetValue ( "Imdb" , out var imdbId ) == true && ! string . IsNullOrEmpty ( imdbId ) )
2004+ {
2005+ return imdbId ;
2006+ }
2007+
19722008 return null ;
19732009 }
19742010
0 commit comments