diff --git a/Jellyfin.Plugin.Themerr.Tests/TestThemerrManager.cs b/Jellyfin.Plugin.Themerr.Tests/TestThemerrManager.cs index 477b79f..4077330 100644 --- a/Jellyfin.Plugin.Themerr.Tests/TestThemerrManager.cs +++ b/Jellyfin.Plugin.Themerr.Tests/TestThemerrManager.cs @@ -60,6 +60,44 @@ private List FixtureThemerrDbUrls() return youtubeUrls; } + [Fact] + [Trait("Category", "Unit")] + private void TestGetExistingThemerrDataValue() + { + string themerrDataPath; + themerrDataPath = Path.Combine( + Directory.GetCurrentDirectory(), + "data", + "dummy.json"); + + // ensure correct values are returned + Assert.Equal( + "dummy_value", + _themerrManager.GetExistingThemerrDataValue("dummy_key", themerrDataPath)); + Assert.Equal( + "https://www.youtube.com/watch?v=E8nxMWr2sr4", + _themerrManager.GetExistingThemerrDataValue("youtube_theme_url", themerrDataPath)); + + // ensure null when the key does not exist + Assert.Null(_themerrManager.GetExistingThemerrDataValue("invalid_key", themerrDataPath)); + + // ensure null when the file does not exist + themerrDataPath = Path.Combine( + Directory.GetCurrentDirectory(), + "data", + "no_file.json"); + + Assert.Null(_themerrManager.GetExistingThemerrDataValue("any_key", themerrDataPath)); + + // test empty json file + themerrDataPath = Path.Combine( + Directory.GetCurrentDirectory(), + "data", + "empty.json"); + + Assert.Null(_themerrManager.GetExistingThemerrDataValue("any_key", themerrDataPath)); + } + [Fact] [Trait("Category", "Unit")] private void TestSaveMp3() @@ -180,15 +218,80 @@ private void TestSaveMp3InvalidUrl() [Fact] [Trait("Category", "Unit")] - private void TestShouldSkipDownload() + private void TestContinueDownload() { - var themePath = Path.Combine( - "theme.mp3"); - var themerrDataPath = Path.Combine( - "themerr_data.json"); - - var shouldSkipDownload = _themerrManager.ShouldSkipDownload(themePath, themerrDataPath); - Assert.False(shouldSkipDownload, "ShouldSkipDownload returned True"); + string themePath; + string themerrDataPath; + + // test when neither theme nor data file exists + themePath = Path.Combine( + "no_file.mp3"); + themerrDataPath = Path.Combine( + "no_file.json"); + Assert.True(_themerrManager.ContinueDownload(themePath, themerrDataPath), "ContinueDownload returned False"); + + // test when theme does not exist and data file does + themePath = Path.Combine( + "no_file.mp3"); + + // copy the dummy.json to a secondary location + var ogFile = Path.Combine( + Directory.GetCurrentDirectory(), + "data", + "dummy.json"); + themerrDataPath = Path.Combine( + Directory.GetCurrentDirectory(), + "data", + "dummy2.json"); + + // copy the dummy.json + File.Copy(ogFile, themerrDataPath); + Assert.True(_themerrManager.ContinueDownload(themePath, themerrDataPath), "ContinueDownload returned False"); + Assert.False(File.Exists(themerrDataPath), $"File {themerrDataPath} was not removed"); + + // test when theme file exists but data file does not + themePath = Path.Combine( + Directory.GetCurrentDirectory(), + "data", + "audio_stub.mp3"); + themerrDataPath = Path.Combine( + Directory.GetCurrentDirectory(), + "data", + "no_file.json"); + Assert.False(_themerrManager.ContinueDownload(themePath, themerrDataPath), "ContinueDownload returned True"); + + // test when both theme and data file exist, but hash is empty in data file + themePath = Path.Combine( + Directory.GetCurrentDirectory(), + "data", + "audio_stub.mp3"); + themerrDataPath = Path.Combine( + Directory.GetCurrentDirectory(), + "data", + "dummy.json"); + Assert.True(_themerrManager.ContinueDownload(themePath, themerrDataPath), "ContinueDownload returned False"); + + // test when both theme and data file exist, and md5 hashes match + themePath = Path.Combine( + Directory.GetCurrentDirectory(), + "data", + "audio_stub.mp3"); + themerrDataPath = Path.Combine( + Directory.GetCurrentDirectory(), + "data", + "audio_themerr_data.json"); + Assert.True(_themerrManager.ContinueDownload(themePath, themerrDataPath), "ContinueDownload returned False"); + + // test when both theme and data file exist, and md5 hashes do not match + themePath = Path.Combine( + Directory.GetCurrentDirectory(), + "data", + "audio_stub.mp3"); + themerrDataPath = Path.Combine( + Directory.GetCurrentDirectory(), + "data", + "audio_themerr_data_user_overwritten.json"); + Assert.False(_themerrManager.ContinueDownload(themePath, themerrDataPath), "ContinueDownload returned True"); } [Fact] @@ -273,11 +376,16 @@ private void TestSaveThemerrData() // set mock themerrDataPath using a random number var mockThemerrDataPath = $"themerr_{new Random().Next()}.json"; + var stubVideoPath = Path.Combine( + Directory.GetCurrentDirectory(), + "data", + "video_stub.mp4"); + // loop over each themerrDbLink foreach (var youtubeThemeUrl in FixtureYoutubeUrls()) { // save themerr data - var fileExists = _themerrManager.SaveThemerrData(mockThemerrDataPath, youtubeThemeUrl); + var fileExists = _themerrManager.SaveThemerrData(stubVideoPath, mockThemerrDataPath, youtubeThemeUrl); Assert.True(fileExists, $"SaveThemerrData did not return True for {youtubeThemeUrl}"); // check if file exists @@ -293,4 +401,27 @@ private void TestSaveThemerrData() $"youtubeThemeUrl {youtubeThemeUrl} does not match savedYoutubeThemeUrl {savedYoutubeThemeUrl}"); } } + + [Fact] + [Trait("Category", "Unit")] + private void TestGetMd5Hash() + { + var stubVideoPath = Path.Combine( + Directory.GetCurrentDirectory(), + "data", + "video_stub.mp4"); + + var expectedMd5HashFile = Path.Combine( + Directory.GetCurrentDirectory(), + "data", + "video_stub.mp4.md5"); + + // get expected md5 hash out of file + var expectedMd5Hash = File.ReadAllText(expectedMd5HashFile).Trim(); + + // get actual md5 hash + var actualMd5Hash = _themerrManager.GetMd5Hash(stubVideoPath); + + Assert.Equal(expectedMd5Hash, actualMd5Hash); + } } diff --git a/Jellyfin.Plugin.Themerr.Tests/data/audio_stub.mp3 b/Jellyfin.Plugin.Themerr.Tests/data/audio_stub.mp3 new file mode 100644 index 0000000..3b14f6c Binary files /dev/null and b/Jellyfin.Plugin.Themerr.Tests/data/audio_stub.mp3 differ diff --git a/Jellyfin.Plugin.Themerr.Tests/data/audio_themerr_data.json b/Jellyfin.Plugin.Themerr.Tests/data/audio_themerr_data.json new file mode 100644 index 0000000..6e3a9ce --- /dev/null +++ b/Jellyfin.Plugin.Themerr.Tests/data/audio_themerr_data.json @@ -0,0 +1,3 @@ +{ + "theme_md5": "44c5eaa73e9b362911aaaf12a5609ab7" +} diff --git a/Jellyfin.Plugin.Themerr.Tests/data/audio_themerr_data_user_overwritten.json b/Jellyfin.Plugin.Themerr.Tests/data/audio_themerr_data_user_overwritten.json new file mode 100644 index 0000000..c4d853f --- /dev/null +++ b/Jellyfin.Plugin.Themerr.Tests/data/audio_themerr_data_user_overwritten.json @@ -0,0 +1,3 @@ +{ + "theme_md5": "abcd" +} diff --git a/Jellyfin.Plugin.Themerr.Tests/data/dummy.json b/Jellyfin.Plugin.Themerr.Tests/data/dummy.json new file mode 100644 index 0000000..385f026 --- /dev/null +++ b/Jellyfin.Plugin.Themerr.Tests/data/dummy.json @@ -0,0 +1,5 @@ +{ + "downloaded_timestamp": "2023-12-12T04:07:41.7659077Z", + "youtube_theme_url": "https://www.youtube.com/watch?v=E8nxMWr2sr4", + "dummy_key": "dummy_value" +} diff --git a/Jellyfin.Plugin.Themerr.Tests/data/empty.json b/Jellyfin.Plugin.Themerr.Tests/data/empty.json new file mode 100644 index 0000000..e02abfc --- /dev/null +++ b/Jellyfin.Plugin.Themerr.Tests/data/empty.json @@ -0,0 +1 @@ + diff --git a/Jellyfin.Plugin.Themerr.Tests/data/video_stub.mp4.md5 b/Jellyfin.Plugin.Themerr.Tests/data/video_stub.mp4.md5 new file mode 100644 index 0000000..4fbe80f Binary files /dev/null and b/Jellyfin.Plugin.Themerr.Tests/data/video_stub.mp4.md5 differ diff --git a/Jellyfin.Plugin.Themerr/ThemerrManager.cs b/Jellyfin.Plugin.Themerr/ThemerrManager.cs index 78cabc6..fe9d304 100644 --- a/Jellyfin.Plugin.Themerr/ThemerrManager.cs +++ b/Jellyfin.Plugin.Themerr/ThemerrManager.cs @@ -42,20 +42,21 @@ public ThemerrManager(ILibraryManager libraryManager, ILogger lo } /// - /// Get the existing youtube theme url from the themerr data file if it exists. + /// Get a value from the themerr data file if it exists. /// + /// The key to search for. /// The path to the themerr data file. - /// The existing YouTube theme url if it exists, empty string otherwise. - public static string GetExistingYoutubeThemeUrl(string themerrDataPath) + /// The value of the key if it exists, null otherwise. + public string GetExistingThemerrDataValue(string key, string themerrDataPath) { if (!System.IO.File.Exists(themerrDataPath)) { - return string.Empty; + return null; } var jsonString = System.IO.File.ReadAllText(themerrDataPath); dynamic jsonData = JsonConvert.DeserializeObject(jsonString); - return jsonData?.youtube_theme_url; + return jsonData?[key]; } /// @@ -132,42 +133,83 @@ public void ProcessMovieTheme(Movie movie) var themePath = GetThemePath(movie); var themerrDataPath = GetThemerrDataPath(movie); - if (ShouldSkipDownload(themePath, themerrDataPath)) + if (!ContinueDownload(themePath, themerrDataPath)) { return; } - var existingYoutubeThemeUrl = GetExistingYoutubeThemeUrl(themerrDataPath); + var existingYoutubeThemeUrl = GetExistingThemerrDataValue("youtube_theme_url", themerrDataPath); // get tmdb id var tmdbId = movie.GetProviderId(MetadataProvider.Tmdb); // create themerrdb url - var themerrDbLink = CreateThemerrDbLink(tmdbId); + var themerrDbUrl = CreateThemerrDbLink(tmdbId); - var youtubeThemeUrl = GetYoutubeThemeUrl(themerrDbLink, movieTitle); + var youtubeThemeUrl = GetYoutubeThemeUrl(themerrDbUrl, movieTitle); if (string.IsNullOrEmpty(youtubeThemeUrl) || youtubeThemeUrl == existingYoutubeThemeUrl) { return; } - SaveMp3(themePath, youtubeThemeUrl); - SaveThemerrData(themerrDataPath, youtubeThemeUrl); + var successMp3 = SaveMp3(themePath, youtubeThemeUrl); + if (!successMp3) + { + return; + } + + var successThemerrData = SaveThemerrData(themePath, themerrDataPath, youtubeThemeUrl); + if (!successThemerrData) + { + return; + } + movie.RefreshMetadata(CancellationToken.None); } /// /// Check if the theme song should be downloaded. /// - /// If theme.mp3 exists and themerr.json doesn't exist, then skip to avoid overwriting user supplied themes. + /// Various checks are performed to determine if the theme song should be downloaded. /// /// The path to the theme song. /// The path to the themerr data file. - /// True if the theme song should NOT be downloaded, false otherwise. - public bool ShouldSkipDownload(string themePath, string themerrDataPath) + /// True to continue with downloaded, false otherwise. + public bool ContinueDownload(string themePath, string themerrDataPath) { - return System.IO.File.Exists(themePath) && !System.IO.File.Exists(themerrDataPath); + if (!System.IO.File.Exists(themePath) && !System.IO.File.Exists(themerrDataPath)) + { + // neither file exists, so don't skip + return true; + } + + if (!System.IO.File.Exists(themePath) && System.IO.File.Exists(themerrDataPath)) + { + // the theme is missing, so delete the themerr data file + System.IO.File.Delete(themerrDataPath); + return true; + } + + if (System.IO.File.Exists(themePath) && !System.IO.File.Exists(themerrDataPath)) + { + // the theme is user supplied, so don't overwrite it + return false; + } + + var existingThemeMd5 = GetExistingThemerrDataValue("theme_md5", themerrDataPath); + + // if existing theme md5 is empty, don't skip + if (string.IsNullOrEmpty(existingThemeMd5)) + { + return true; + } + + // check if the theme hash matches what is in the themerr data file + var themeMd5 = GetMd5Hash(themePath); + + // if hashes match, theme is supplied by themerr, otherwise it is user supplied + return themeMd5 == existingThemeMd5; } /// @@ -226,15 +268,17 @@ public string GetYoutubeThemeUrl(string themerrDbUrl, string movieTitle) /// /// Save the themerr data file. /// + /// The path to the theme song. /// The path to the themerr data file. /// The YouTube theme url. /// True if the file was saved successfully, false otherwise. - public bool SaveThemerrData(string themerrDataPath, string youtubeThemeUrl) + public bool SaveThemerrData(string themePath, string themerrDataPath, string youtubeThemeUrl) { var success = false; var themerrData = new { downloaded_timestamp = DateTime.UtcNow, + theme_md5 = GetMd5Hash(themePath), youtube_theme_url = youtubeThemeUrl }; try @@ -250,6 +294,23 @@ public bool SaveThemerrData(string themerrDataPath, string youtubeThemeUrl) return success && WaitForFile(themerrDataPath, 10000); } + /// + /// Get the MD5 hash of a file. + /// + /// The file path. + /// The MD5 hash of the file. + public string GetMd5Hash(string filePath) + { + using (var md5 = System.Security.Cryptography.MD5.Create()) + { + using (var stream = System.IO.File.OpenRead(filePath)) + { + var hash = md5.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant(); + } + } + } + /// /// Wait for file to exist on disk and is not locked by another process. ///