@@ -433,6 +433,15 @@ private static void CreateThemerrPluginInstance(bool backupUserSuppliedTheme = t
433433 plugin . Configuration . BackupUserSuppliedTheme = backupUserSuppliedTheme ;
434434 }
435435
436+ private static void SetThemerrPluginInstance ( ThemerrPlugin ? plugin )
437+ {
438+ var instanceField = typeof ( ThemerrPlugin ) . GetField (
439+ "<Instance>k__BackingField" ,
440+ BindingFlags . NonPublic | BindingFlags . Static ) ;
441+ Assert . NotNull ( instanceField ) ;
442+ instanceField . SetValue ( null , plugin ) ;
443+ }
444+
436445 private static ThemerrManager CreateThemerrManagerWithFailingYoutubeAndItemById (
437446 BaseItem item ,
438447 ThemerrRepository ? themerrRepository = null ,
@@ -969,6 +978,17 @@ private async Task TestGetThemeProvider(BaseItem item)
969978 Assert . Equal ( ThemerrThemeProvider . Themerr , themeProvider ) ;
970979 }
971980
981+ [ Fact ]
982+ [ Trait ( "Category" , "Unit" ) ]
983+ private async Task TestGetThemeProviderUnsupportedItem ( )
984+ {
985+ var manager = CreateThemerrManager ( ) ;
986+
987+ var themeProvider = await manager . GetThemeProvider ( new Audio { Name = "Unsupported Theme Provider" } ) ;
988+
989+ Assert . Null ( themeProvider ) ;
990+ }
991+
972992 [ Fact ]
973993 [ Trait ( "Category" , "Unit" ) ]
974994 private void TestContinueDownload ( )
@@ -1360,6 +1380,55 @@ private async Task TestSyncLibraryItemTracksUserThemeAndCachedThemerrDbUrl()
13601380 Assert . Null ( syncedItem . ThemeHash ) ;
13611381 }
13621382
1383+ [ Fact ]
1384+ [ Trait ( "Category" , "Unit" ) ]
1385+ private async Task TestSyncLibraryItemPreservesThemerrDownloadedTimestamp ( )
1386+ {
1387+ var repository = CreateThemerrRepository ( ) ;
1388+ var manager = CreateThemerrManager ( repository ) ;
1389+ var tempPath = CreateTempDirectory ( ) ;
1390+ var item = CreateMovie ( "sync-themerr-timestamp" ) ;
1391+ item . Path = Path . Combine ( tempPath , "Test Movie (1970).mp4" ) ;
1392+ var themePath = ThemerrManager . GetThemePath ( item ) ;
1393+ var downloadedTimestampUtc = DateTime . UtcNow . AddMinutes ( - 10 ) ;
1394+
1395+ File . Copy (
1396+ Path . Combine ( Directory . GetCurrentDirectory ( ) , "data" , "audio_stub.mp3" ) ,
1397+ themePath ,
1398+ true ) ;
1399+ var themeHash = ThemerrManager . GetThemeHash ( themePath ) ;
1400+
1401+ repository . Save (
1402+ item ,
1403+ new ThemerrMediaItemSaveOptions
1404+ {
1405+ ThemePath = themePath ,
1406+ ThemeHash = themeHash ,
1407+ ThemeHashAlgorithm = ThemerrThemeHasher . CurrentAlgorithm ,
1408+ ThemeProvider = ThemerrThemeProvider . Themerr ,
1409+ DownloadedTimestampUtc = downloadedTimestampUtc ,
1410+ InThemerrDb = true ,
1411+ InThemerrDbCheckedUtc = DateTime . UtcNow ,
1412+ YoutubeThemeUrl = "https://www.youtube.com/watch?v=sync-themerr-timestamp" ,
1413+ } ) ;
1414+
1415+ try
1416+ {
1417+ var syncedItem = await manager . SyncLibraryItem ( item ) ;
1418+
1419+ Assert . NotNull ( syncedItem ) ;
1420+ Assert . Equal ( ThemerrThemeProvider . Themerr , syncedItem . ThemeProvider ) ;
1421+ Assert . Equal ( downloadedTimestampUtc , syncedItem . DownloadedTimestampUtc ) ;
1422+ }
1423+ finally
1424+ {
1425+ if ( File . Exists ( themePath ) )
1426+ {
1427+ File . Delete ( themePath ) ;
1428+ }
1429+ }
1430+ }
1431+
13631432 [ Theory ]
13641433 [ Trait ( "Category" , "Unit" ) ]
13651434 [ MemberData ( nameof ( FixtureJellyfinServer . MockItemsData ) , MemberType = typeof ( FixtureJellyfinServer ) ) ]
@@ -1903,6 +1972,109 @@ private async Task TestReplaceWithThemerThemeNoBackupWhenNoExistingTheme()
19031972 }
19041973 }
19051974
1975+ /// <summary>
1976+ /// Test that ReplaceWithThemerTheme skips backup when the plugin instance is unavailable.
1977+ /// </summary>
1978+ [ Fact ]
1979+ [ Trait ( "Category" , "Unit" ) ]
1980+ private async Task TestReplaceWithThemerThemeNoBackupWhenPluginInstanceMissing ( )
1981+ {
1982+ var previousPlugin = ThemerrPlugin . Instance ;
1983+ SetThemerrPluginInstance ( null ) ;
1984+
1985+ var movie = CreateMovie ( "backup-plugin-missing" ) ;
1986+ var tempPath = CreateTempDirectory ( ) ;
1987+ movie . Path = Path . Combine ( tempPath , "Test Movie (1970).mp4" ) ;
1988+
1989+ var repository = CreateThemerrRepository ( ) ;
1990+ var manager = CreateThemerrManagerWithMockYoutubeAndItemById ( movie , repository ) ;
1991+ var themePath = ThemerrManager . GetThemePath ( movie ) ;
1992+ var backupPath = Path . Combine ( Path . GetDirectoryName ( themePath ) ! , "theme.backup.mp3" ) ;
1993+
1994+ File . Copy ( Path . Combine ( Directory . GetCurrentDirectory ( ) , "data" , "audio_stub.mp3" ) , themePath , true ) ;
1995+
1996+ repository . Save ( movie , new ThemerrMediaItemSaveOptions
1997+ {
1998+ ThemePath = themePath ,
1999+ ThemeProvider = ThemerrThemeProvider . User ,
2000+ InThemerrDb = true ,
2001+ InThemerrDbCheckedUtc = DateTime . UtcNow ,
2002+ YoutubeThemeUrl = "https://www.youtube.com/watch?v=dQw4w9WgXcQ" ,
2003+ } ) ;
2004+
2005+ try
2006+ {
2007+ var result = await manager . ReplaceWithThemerTheme ( movie . Id ) ;
2008+ Assert . True ( result ) ;
2009+ Assert . True ( File . Exists ( themePath ) ) ;
2010+ Assert . False ( File . Exists ( backupPath ) ) ;
2011+ }
2012+ finally
2013+ {
2014+ SetThemerrPluginInstance ( previousPlugin ) ;
2015+ if ( File . Exists ( themePath ) )
2016+ {
2017+ File . Delete ( themePath ) ;
2018+ }
2019+
2020+ if ( File . Exists ( backupPath ) )
2021+ {
2022+ File . Delete ( backupPath ) ;
2023+ }
2024+ }
2025+ }
2026+
2027+ /// <summary>
2028+ /// Test that ReplaceWithThemerTheme returns false without restore work when no backup exists.
2029+ /// </summary>
2030+ [ Fact ]
2031+ [ Trait ( "Category" , "Unit" ) ]
2032+ private async Task TestReplaceWithThemerThemeDownloadFailureWithoutBackup ( )
2033+ {
2034+ var previousPlugin = ThemerrPlugin . Instance ;
2035+ CreateThemerrPluginInstance ( backupUserSuppliedTheme : false ) ;
2036+
2037+ var movie = CreateMovie ( "backup-failure-no-backup" ) ;
2038+ var tempPath = CreateTempDirectory ( ) ;
2039+ movie . Path = Path . Combine ( tempPath , "Test Movie (1970).mp4" ) ;
2040+
2041+ var repository = CreateThemerrRepository ( ) ;
2042+ var manager = CreateThemerrManagerWithFailingYoutubeAndItemById ( movie , repository ) ;
2043+ var themePath = ThemerrManager . GetThemePath ( movie ) ;
2044+ var backupPath = Path . Combine ( Path . GetDirectoryName ( themePath ) ! , "theme.backup.mp3" ) ;
2045+
2046+ repository . Save ( movie , new ThemerrMediaItemSaveOptions
2047+ {
2048+ ThemePath = themePath ,
2049+ ThemeProvider = ThemerrThemeProvider . User ,
2050+ InThemerrDb = true ,
2051+ InThemerrDbCheckedUtc = DateTime . UtcNow ,
2052+ YoutubeThemeUrl = "https://www.youtube.com/watch?v=dQw4w9WgXcQ" ,
2053+ } ) ;
2054+
2055+ try
2056+ {
2057+ var result = await manager . ReplaceWithThemerTheme ( movie . Id ) ;
2058+
2059+ Assert . False ( result ) ;
2060+ Assert . False ( File . Exists ( themePath ) ) ;
2061+ Assert . False ( File . Exists ( backupPath ) ) ;
2062+ }
2063+ finally
2064+ {
2065+ SetThemerrPluginInstance ( previousPlugin ) ;
2066+ if ( File . Exists ( themePath ) )
2067+ {
2068+ File . Delete ( themePath ) ;
2069+ }
2070+
2071+ if ( File . Exists ( backupPath ) )
2072+ {
2073+ File . Delete ( backupPath ) ;
2074+ }
2075+ }
2076+ }
2077+
19062078 /// <summary>
19072079 /// Test that ReplaceWithThemerTheme restores the original theme from backup when the download fails.
19082080 /// </summary>
@@ -1980,6 +2152,52 @@ private void TestContinueDownloadUsesExistingThemeSongPath()
19802152 Assert . False ( manager . ContinueDownload ( item , missingThemePath ) ) ;
19812153 }
19822154
2155+ [ Fact ]
2156+ [ Trait ( "Category" , "Unit" ) ]
2157+ private void TestContinueDownloadIgnoresNullReferenceFromThemeSongs ( )
2158+ {
2159+ var repository = CreateThemerrRepository ( ) ;
2160+ var item = CreateMovie ( "theme-song-null-reference" ) ;
2161+ var missingThemePath = Path . Combine ( CreateTempDirectory ( ) , "missing-theme.mp3" ) ;
2162+ var manager = CreateThemerrManager (
2163+ repository ,
2164+ themeSongProvider : _ => throw new NullReferenceException ( "Simulated theme song failure" ) ) ;
2165+
2166+ Assert . True ( manager . ContinueDownload ( item , missingThemePath ) ) ;
2167+ }
2168+
2169+ [ Fact ]
2170+ [ Trait ( "Category" , "Unit" ) ]
2171+ private void TestContinueDownloadIgnoresThemeSongExceptionWithoutLogger ( )
2172+ {
2173+ var repository = CreateThemerrRepository ( ) ;
2174+ var item = CreateMovie ( "theme-song-null-logger" ) ;
2175+ var missingThemePath = Path . Combine ( CreateTempDirectory ( ) , "missing-theme.mp3" ) ;
2176+ var manager = new ThemerrManager (
2177+ TestHelper . GetMockApplicationPaths ( ) . Object ,
2178+ new Mock < ILibraryManager > ( ) . Object ,
2179+ null ! ,
2180+ themerrRepository : repository ,
2181+ httpClient : CreateThemerrDbHttpClient ( ) ,
2182+ themeSongProvider : _ => throw new NullReferenceException ( "Simulated theme song failure" ) ) ;
2183+
2184+ Assert . True ( manager . ContinueDownload ( item , missingThemePath ) ) ;
2185+ }
2186+
2187+ [ Fact ]
2188+ [ Trait ( "Category" , "Unit" ) ]
2189+ private void TestContinueDownloadPropagatesUnexpectedThemeSongException ( )
2190+ {
2191+ var repository = CreateThemerrRepository ( ) ;
2192+ var item = CreateMovie ( "theme-song-unexpected-exception" ) ;
2193+ var missingThemePath = Path . Combine ( CreateTempDirectory ( ) , "missing-theme.mp3" ) ;
2194+ var manager = CreateThemerrManager (
2195+ repository ,
2196+ themeSongProvider : _ => throw new ArgumentException ( "Simulated unexpected theme song failure" ) ) ;
2197+
2198+ Assert . Throws < ArgumentException > ( ( ) => manager . ContinueDownload ( item , missingThemePath ) ) ;
2199+ }
2200+
19832201 [ Fact ]
19842202 [ Trait ( "Category" , "Unit" ) ]
19852203 private void TestLegacyThemerrDataPathCandidatesIncludeRawDirectoryCandidate ( )
@@ -2011,6 +2229,8 @@ private void TestIsMatchingMediaDirectoryHandlesEmptyAndMismatchedNames()
20112229 var tempPath = CreateTempDirectory ( ) ;
20122230 var matchingDirectory = Path . Combine ( tempPath , "Empty Name (1970)" ) ;
20132231 var mismatchedDirectory = Path . Combine ( tempPath , "Different Title (1970)" ) ;
2232+ var yearlessDirectory = Path . Combine ( tempPath , "Yearless Title" ) ;
2233+ var missingYearDirectory = Path . Combine ( tempPath , "Test Movie matching-media-directory (1980)" ) ;
20142234 Directory . CreateDirectory ( matchingDirectory ) ;
20152235 Directory . CreateDirectory ( mismatchedDirectory ) ;
20162236 var method = typeof ( ThemerrManager ) . GetMethod (
@@ -2036,9 +2256,29 @@ private void TestIsMatchingMediaDirectoryHandlesEmptyAndMismatchedNames()
20362256 CreateMovie ( "matching-media-directory" ) ,
20372257 mismatchedDirectory ,
20382258 } ) ! ;
2259+ var yearlessResult = ( bool ) method . Invoke (
2260+ manager ,
2261+ new object [ ]
2262+ {
2263+ new Movie
2264+ {
2265+ Name = "Yearless Title" ,
2266+ ProductionYear = null ,
2267+ } ,
2268+ yearlessDirectory ,
2269+ } ) ! ;
2270+ var missingYearResult = ( bool ) method . Invoke (
2271+ manager ,
2272+ new object [ ]
2273+ {
2274+ CreateMovie ( "matching-media-directory" ) ,
2275+ missingYearDirectory ,
2276+ } ) ! ;
20392277
20402278 Assert . False ( emptyNameResult ) ;
20412279 Assert . False ( mismatchedNameResult ) ;
2280+ Assert . True ( yearlessResult ) ;
2281+ Assert . False ( missingYearResult ) ;
20422282 }
20432283
20442284 [ Fact ]
0 commit comments