diff --git a/Jellyfin.Plugin.Themerr.Tests/BootstrapJellyfinServer.cs b/Jellyfin.Plugin.Themerr.Tests/BootstrapJellyfinServer.cs new file mode 100644 index 00000000..c843718d --- /dev/null +++ b/Jellyfin.Plugin.Themerr.Tests/BootstrapJellyfinServer.cs @@ -0,0 +1,115 @@ +using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider; +using Movie = MediaBrowser.Controller.Entities.Movies.Movie; + +namespace Jellyfin.Plugin.Themerr.Tests; + +[CollectionDefinition("Bootstrapped Collection")] +public class BootstrappedCollection : ICollectionFixture +{ + // This class doesn't need to have any code, or even be long-lived. + // All it needs is to just exist, and be annotated with CollectionDefinition. +} + +/// +/// This class is used to bootstrap a Jellyfin server with mock movies +/// +public class BootstrapJellyfinServer +{ + /// + /// Mock movies to use for testing + /// + /// + public static List MockMovies() + { + return new List + { + new() + { + Name = "Elephants Dream", + ProductionYear = 2006, + ProviderIds = new Dictionary + { + { MetadataProvider.Imdb.ToString(), "tt0807840"}, + { MetadataProvider.Tmdb.ToString(), "9761"}, + } + }, + new() + { + Name = "Sita Sings the Blues", + ProductionYear = 2008, + ProviderIds = new Dictionary + { + { MetadataProvider.Imdb.ToString(), "tt1172203"}, + { MetadataProvider.Tmdb.ToString(), "20529"}, + } + }, + new() + { + Name = "Big Buck Bunny", + ProductionYear = 2008, + ProviderIds = new Dictionary + { + { MetadataProvider.Imdb.ToString(), "tt1254207"}, + { MetadataProvider.Tmdb.ToString(), "10378"}, + } + }, + new() + { + Name = "Sintel", + ProductionYear = 2010, + ProviderIds = new Dictionary + { + { MetadataProvider.Imdb.ToString(), "tt1727587"}, + { MetadataProvider.Tmdb.ToString(), "45745"}, + } + }, + }; + } + + + /// + /// Create mock movies from stub video + /// + [Fact] + [Trait("Category", "Init")] + private void CreateMockMovies() + { + var mockMovies = MockMovies(); + + // get the stub video path based on the directory of this file + var stubVideoPath = Path.Combine( + Directory.GetCurrentDirectory(), + "data", + "video_stub.mp4" + ); + + Assert.True(File.Exists(stubVideoPath), "Could not find ./data/video_stub.mp4"); + + foreach (var movie in mockMovies) + { + // copy the ./data/video_stub.mp4 to the movie folder "movie.Name (movie.ProductionYear)" + var movieFolder = Path.Combine( + "themerr_jellyfin_tests", + $"{movie.Name} ({movie.ProductionYear})" + ); + + // create the movie folder + Directory.CreateDirectory(movieFolder); + + // copy the video_stub.mp4 to the movie folder, renaming it based on movie name + var movieVideoPath = Path.Combine( + movieFolder, + $"{movie.Name} ({movie.ProductionYear}).mp4" + ); + + // if file does not exist + if (!File.Exists(movieVideoPath)) + { + // copy the stub video to the movie folder + File.Copy(stubVideoPath, movieVideoPath); + } + + Assert.True(File.Exists(movieVideoPath), $"Could not find {movieVideoPath}"); + } + } +} diff --git a/Jellyfin.Plugin.Themerr.Tests/Jellyfin.Plugin.Themerr.Tests.csproj b/Jellyfin.Plugin.Themerr.Tests/Jellyfin.Plugin.Themerr.Tests.csproj index e9c050a4..b3ec9b26 100644 --- a/Jellyfin.Plugin.Themerr.Tests/Jellyfin.Plugin.Themerr.Tests.csproj +++ b/Jellyfin.Plugin.Themerr.Tests/Jellyfin.Plugin.Themerr.Tests.csproj @@ -18,6 +18,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -29,4 +30,10 @@ + + + PreserveNewest + + + diff --git a/Jellyfin.Plugin.Themerr.Tests/TestLogger.cs b/Jellyfin.Plugin.Themerr.Tests/TestLogger.cs new file mode 100644 index 00000000..64c3110e --- /dev/null +++ b/Jellyfin.Plugin.Themerr.Tests/TestLogger.cs @@ -0,0 +1,71 @@ +namespace Jellyfin.Plugin.Themerr.Tests +{ + /// + /// A simple logger for tests + /// + public static class TestLogger + { + // log a message to console + private static ITestOutputHelper? _output; + + public static void Initialize(ITestOutputHelper output) + { + _output = output ?? throw new ArgumentNullException(nameof(output)); + } + + /// + /// Logs a message to the test output + /// + /// + /// + public static void Log(string message, string type = "INFO") + { + _output?.WriteLine($"[{type}] {message}"); + } + + /// + /// Logs a critical message to the test output + /// + /// + public static void Critical(string message) + { + Log(message, "CRITICAL"); + } + + /// + /// Logs a debug message to the test output + /// + /// + public static void Debug(string message) + { + Log(message, "DEBUG"); + } + + /// + /// Logs an error message to the test output + /// + /// + public static void Error(string message) + { + Log(message, "ERROR"); + } + + /// + /// Logs an info message to the test output + /// + /// + public static void Info(string message) + { + Log(message, "INFO"); + } + + /// + /// Logs a warning message to the test output + /// + /// + public static void Warn(string message) + { + Log(message, "WARN"); + } + } +} diff --git a/Jellyfin.Plugin.Themerr.Tests/TestThemerrManager.cs b/Jellyfin.Plugin.Themerr.Tests/TestThemerrManager.cs index b25f53bb..3c7399e0 100644 --- a/Jellyfin.Plugin.Themerr.Tests/TestThemerrManager.cs +++ b/Jellyfin.Plugin.Themerr.Tests/TestThemerrManager.cs @@ -1,17 +1,59 @@ -using Xunit.Abstractions; +using MediaBrowser.Controller.Library; +using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider; +using Microsoft.Extensions.Logging; +using Moq; +using Newtonsoft.Json; namespace Jellyfin.Plugin.Themerr.Tests; +[Collection("Bootstrapped Collection")] public class TestThemerrManager { - private readonly ITestOutputHelper _testOutputHelper; + private readonly ThemerrManager _themerrManager; - public TestThemerrManager(ITestOutputHelper testOutputHelper) + public TestThemerrManager(ITestOutputHelper output) { - _testOutputHelper = testOutputHelper; + TestLogger.Initialize(output); + + Mock mockLibraryManager = new(); + Mock> mockLogger = new(); + + _themerrManager = new ThemerrManager(mockLibraryManager.Object, mockLogger.Object); } - + + private static List FixtureYoutubeUrls() + { + // create a list and return it + var youtubeUrls = new List() + { + "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + "https://www.youtube.com/watch?v=yPYZpwSpKmA", + "https://www.youtube.com/watch?v=Ghmd4QzT9YY", + "https://www.youtube.com/watch?v=LVEWkghDh9A" + }; + + // return the list + return youtubeUrls; + } + + private List FixtureThemerrDbUrls() + { + // make list of youtubeUrls to populate + var youtubeUrls = new List(); + + foreach (var movie in BootstrapJellyfinServer.MockMovies()) + { + var tmdbId = movie.ProviderIds[MetadataProvider.Tmdb.ToString()]; + var themerrDbLink = _themerrManager.CreateThemerrDbLink(tmdbId); + youtubeUrls.Add(themerrDbLink); + } + + // return the list + return youtubeUrls; + } + [Fact] + [Trait("Category", "Unit")] public void TestSaveMp3() { // set destination with themerr_jellyfin_tests as the folder name @@ -20,50 +62,41 @@ public void TestSaveMp3() "theme.mp3" ); - // create a list of youtube urls - var videoUrls = new List - { - "https://www.youtube.com/watch?v=dQw4w9WgXcQ", - "https://www.youtube.com/watch?v=yPYZpwSpKmA", - "https://www.youtube.com/watch?v=Ghmd4QzT9YY", - "https://www.youtube.com/watch?v=LVEWkghDh9A" - }; - foreach (var videoUrl in videoUrls) + + foreach (var videoUrl in FixtureYoutubeUrls()) { // log - _testOutputHelper.WriteLine($"Attempting to download {videoUrl}"); + TestLogger.Info($"Attempting to download {videoUrl}"); // run and wait - ThemerrManager.SaveMp3(destinationFile, videoUrl); - - // wait until the file is downloaded - Thread.Sleep(5000); // 5 seconds + var themeExists = _themerrManager.SaveMp3(destinationFile, videoUrl); + Assert.True(themeExists, $"SaveMp3 did not return True for {videoUrl}"); // check if file exists - Assert.True(File.Exists(destinationFile)); + Assert.True(File.Exists(destinationFile), $"File {destinationFile} does not exist"); // check if the file is an actual mp3 // https://en.wikipedia.org/wiki/List_of_file_signatures var fileBytes = File.ReadAllBytes(destinationFile); var fileBytesHex = BitConverter.ToString(fileBytes); - // make sure the file does is not WebM, starts with `1A 45 DF A3` - var isNotWebM = !fileBytesHex.StartsWith("1A-45-DF-A3"); - Assert.True(isNotWebM); + // make sure the file is not WebM, starts with `1A 45 DF A3` + var isWebM = fileBytesHex.StartsWith("1A-45-DF-A3"); + Assert.False(isWebM, $"File {destinationFile} is WebM"); // valid mp3 signatures dictionary with offsets var validMp3Signatures = new Dictionary { - {"66-74-79-70-64-61-73-68", 4}, // Mp4 container? - {"66-74-79-70-69-73-6F-6D", 4}, // Mp4 container - {"49-44-33", 0}, // ID3 - {"FF-FB", 0}, // MPEG-1 Layer 3 - {"FF-F3", 0}, // MPEG-1 Layer 3 - {"FF-F2", 0} // MPEG-1 Layer 3 + {"66-74-79-70-64-61-73-68", 4}, // Mp4 container? + {"66-74-79-70-69-73-6F-6D", 4}, // Mp4 container + {"49-44-33", 0}, // ID3 + {"FF-FB", 0}, // MPEG-1 Layer 3 + {"FF-F3", 0}, // MPEG-1 Layer 3 + {"FF-F2", 0} // MPEG-1 Layer 3 }; // log beginning of fileBytesHex - _testOutputHelper.WriteLine($"Beginning of fileBytesHex: {fileBytesHex.Substring(0, 40)}"); + TestLogger.Debug($"Beginning of fileBytesHex: {fileBytesHex.Substring(0, 40)}"); // check if the file is an actual mp3 var isMp3 = false; @@ -72,7 +105,7 @@ public void TestSaveMp3() foreach (var (signature, offset) in validMp3Signatures) { // log - _testOutputHelper.WriteLine($"Checking for {signature} at offset of {offset} bytes"); + TestLogger.Debug($"Checking for {signature} at offset of {offset} bytes"); // remove the offset bytes var fileBytesHexWithoutOffset = fileBytesHex.Substring(offset * 3); @@ -82,7 +115,7 @@ public void TestSaveMp3() if (isSignature) { // log - _testOutputHelper.WriteLine($"Found {signature} at offset {offset}"); + TestLogger.Info($"Found {signature} at offset {offset}"); // set isMp3 to true isMp3 = true; @@ -92,12 +125,79 @@ public void TestSaveMp3() } // log - _testOutputHelper.WriteLine($"Did not find {signature} at offset {offset}"); + TestLogger.Debug($"Did not find {signature} at offset {offset}"); } - Assert.True(isMp3); + Assert.True(isMp3, $"File {destinationFile} is not an mp3"); // delete file File.Delete(destinationFile); } } + + [Fact] + [Trait("Category", "Unit")] + public void TestCreateThemerrDbLink() + { + // get bootstrapped movies + var mockMovies = BootstrapJellyfinServer.MockMovies(); + + Assert.True(mockMovies.Count > 0, "mockMovies.Count is not greater than 0"); + + foreach (var movie in mockMovies) + { + var tmdbId = movie.ProviderIds[MetadataProvider.Tmdb.ToString()]; + var themerrDbLink = _themerrManager.CreateThemerrDbLink(tmdbId); + + TestLogger.Info($"themerrDbLink: {themerrDbLink}"); + + Assert.EndsWith($"themoviedb/{tmdbId}.json", themerrDbLink); + } + } + + [Fact] + [Trait("Category", "Unit")] + public void TestGetYoutubeThemeUrl() + { + + // loop over each themerrDbLink + foreach (var themerrDbLink in FixtureThemerrDbUrls()) + { + // get the new youtube theme url + var youtubeThemeUrl = _themerrManager.GetYoutubeThemeUrl(themerrDbLink, $"test{themerrDbLink}"); + + // log + TestLogger.Info($"youtubeThemeUrl: {youtubeThemeUrl}"); + + Assert.NotEmpty(youtubeThemeUrl); + } + } + + [Fact] + [Trait("Category", "Unit")] + public void TestSaveThemerrData() + { + + // set mock themerrDataPath using a random number + var mockThemerrDataPath = $"themerr_{new Random().Next()}.json"; + + // loop over each themerrDbLink + foreach (var youtubeThemeUrl in FixtureYoutubeUrls()) + { + + // save themerr data + var fileExists = _themerrManager.SaveThemerrData(mockThemerrDataPath, youtubeThemeUrl); + Assert.True(fileExists, $"SaveThemerrData did not return True for {youtubeThemeUrl}"); + + // check if file exists + Assert.True(File.Exists(mockThemerrDataPath), $"File {mockThemerrDataPath} does not exist"); + + // make sure the saved json file contains a key named "youtube_theme_url", and value is correct + var jsonString = File.ReadAllText(mockThemerrDataPath); + File.Delete(mockThemerrDataPath); // delete the file + dynamic jsonData = JsonConvert.DeserializeObject(jsonString) ?? throw new InvalidOperationException(); + var savedYoutubeThemeUrl = jsonData.youtube_theme_url.ToString(); + Assert.True(youtubeThemeUrl == savedYoutubeThemeUrl, + $"youtubeThemeUrl {youtubeThemeUrl} does not match savedYoutubeThemeUrl {savedYoutubeThemeUrl}"); + } + } } diff --git a/Jellyfin.Plugin.Themerr.Tests/Usings.cs b/Jellyfin.Plugin.Themerr.Tests/Usings.cs index c802f448..89702116 100644 --- a/Jellyfin.Plugin.Themerr.Tests/Usings.cs +++ b/Jellyfin.Plugin.Themerr.Tests/Usings.cs @@ -1 +1,2 @@ global using Xunit; +global using Xunit.Abstractions; diff --git a/Jellyfin.Plugin.Themerr.Tests/data/video_stub.mp4 b/Jellyfin.Plugin.Themerr.Tests/data/video_stub.mp4 new file mode 100644 index 00000000..d9a10e31 Binary files /dev/null and b/Jellyfin.Plugin.Themerr.Tests/data/video_stub.mp4 differ diff --git a/Jellyfin.Plugin.Themerr/Jellyfin.Plugin.Themerr.csproj b/Jellyfin.Plugin.Themerr/Jellyfin.Plugin.Themerr.csproj index 3b37043e..e0f9694a 100644 --- a/Jellyfin.Plugin.Themerr/Jellyfin.Plugin.Themerr.csproj +++ b/Jellyfin.Plugin.Themerr/Jellyfin.Plugin.Themerr.csproj @@ -25,8 +25,4 @@ - - - - diff --git a/Jellyfin.Plugin.Themerr/ScheduledTasks/ThemerrTasks.cs b/Jellyfin.Plugin.Themerr/ScheduledTasks/ThemerrTasks.cs index 837496a5..9c95ec01 100644 --- a/Jellyfin.Plugin.Themerr/ScheduledTasks/ThemerrTasks.cs +++ b/Jellyfin.Plugin.Themerr/ScheduledTasks/ThemerrTasks.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; diff --git a/Jellyfin.Plugin.Themerr/ThemerrManager.cs b/Jellyfin.Plugin.Themerr/ThemerrManager.cs index 90980226..d7b532c5 100644 --- a/Jellyfin.Plugin.Themerr/ThemerrManager.cs +++ b/Jellyfin.Plugin.Themerr/ThemerrManager.cs @@ -20,40 +20,64 @@ namespace Jellyfin.Plugin.Themerr { + /// + /// The main entry point for the plugin. + /// public class ThemerrManager : IServerEntryPoint { private readonly ILibraryManager _libraryManager; private readonly Timer _timer; private readonly ILogger _logger; + /// + /// Constructor + /// + /// + /// public ThemerrManager(ILibraryManager libraryManager, ILogger logger) { _libraryManager = libraryManager; _logger = logger; _timer = new Timer(_ => OnTimerElapsed(), null, Timeout.Infinite, Timeout.Infinite); } - - public static void SaveMp3(string destination, string videoUrl) + /// + /// Save a mp3 file from a youtube video url. + /// + /// + /// + public bool SaveMp3(string destination, string videoUrl) { - Task.Run(async () => + try { - var youtube = new YoutubeClient(); - var streamManifest = await youtube.Videos.Streams.GetManifestAsync(videoUrl); + Task.Run(async () => + { + var youtube = new YoutubeClient(); + var streamManifest = await youtube.Videos.Streams.GetManifestAsync(videoUrl); + + // highest bitrate audio mp3 stream + var streamInfo = streamManifest + .GetAudioOnlyStreams() + .Where(s => s.Container == Container.Mp4) + .GetWithHighestBitrate(); - // highest bitrate audio mp3 stream - var streamInfo = streamManifest - .GetAudioOnlyStreams() - .Where(s => s.Container == Container.Mp4) - .GetWithHighestBitrate(); + // Download the stream to a file + await youtube.Videos.Streams.DownloadAsync(streamInfo, destination); + }); + } + catch (Exception e) + { + _logger.LogError("Unable to download {VideoUrl} to {Destination}: {Error}", videoUrl, destination, e); + } - // Download the stream to a file - await youtube.Videos.Streams.DownloadAsync(streamInfo, destination); - }); + return WaitForFile(destination, 30000); } - - private IEnumerable GetMoviesFromLibrary() + /// + /// Get all movies from the library that have a tmdb id. + /// + /// + public IEnumerable GetMoviesFromLibrary() { return _libraryManager.GetItemList(new InternalItemsQuery { @@ -63,115 +87,225 @@ private IEnumerable GetMoviesFromLibrary() HasTmdbId = true }).Select(m => m as Movie); } - - + + /// + /// Enumerate through all movies in the library and downloads their theme songs as required. + /// + /// public Task DownloadAllThemerr() { var movies = GetMoviesFromLibrary(); foreach (var movie in movies) { - // set paths - var themePath = $"{movie.ContainingFolderPath}/theme.mp3"; - var themerrDataPath = $"{movie.ContainingFolderPath}/themerr.json"; - - // if theme.mp3 exists and themerr.json does not exist then skip - // don't overwrite user supplied theme files - if (System.IO.File.Exists(themePath) && !System.IO.File.Exists(themerrDataPath)) + ProcessMovieTheme(movie); + } + + return Task.CompletedTask; + } + + /// + /// Download the theme song for a movie if it doesn't already exist. + /// + /// + public void ProcessMovieTheme(Movie movie) + { + var movieTitle = movie.Name; + var themePath = GetThemePath(movie); + var themerrDataPath = GetThemerrDataPath(movie); + + if (ShouldSkipDownload(themePath, themerrDataPath)) + { + return; + } + + var existingYoutubeThemeUrl = GetExistingYoutubeThemeUrl(themerrDataPath); + + // get tmdb id + var tmdbId = movie.GetProviderId(MetadataProvider.Tmdb); + // create themerrdb url + var themerrDbLink = CreateThemerrDbLink(tmdbId); + + var youtubeThemeUrl = GetYoutubeThemeUrl(themerrDbLink, movieTitle); + + if (string.IsNullOrEmpty(youtubeThemeUrl) || youtubeThemeUrl == existingYoutubeThemeUrl) + { + return; + } + + SaveMp3(themePath, youtubeThemeUrl); + SaveThemerrData(themerrDataPath, youtubeThemeUrl); + 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. + /// + /// + /// + /// + public static bool ShouldSkipDownload(string themePath, string themerrDataPath) + { + return System.IO.File.Exists(themePath) && !System.IO.File.Exists(themerrDataPath); + } + + /// + /// Get the path to the theme song. + /// + /// + /// + public static string GetThemePath(Movie movie) + { + return $"{movie.ContainingFolderPath}/theme.mp3"; + } + + /// + /// Get the path to the themerr data file. + /// + /// + /// + public static string GetThemerrDataPath(Movie movie) + { + return $"{movie.ContainingFolderPath}/themerr.json"; + } + + /// + /// Get the existing youtube theme url from the themerr data file if it exists. + /// + /// + /// + public static string GetExistingYoutubeThemeUrl(string themerrDataPath) + { + if (!System.IO.File.Exists(themerrDataPath)) + return string.Empty; + + var jsonString = System.IO.File.ReadAllText(themerrDataPath); + dynamic jsonData = JsonConvert.DeserializeObject(jsonString); + return jsonData?.youtube_theme_url; + } + + /// + /// Create a link to the themerr database. + /// + /// + /// + public string CreateThemerrDbLink(string tmdbId) + { + return $"https://app.lizardbyte.dev/ThemerrDB/movies/themoviedb/{tmdbId}.json"; + } + + /// + /// Get the YouTube theme url from the themerr database. + /// + /// + /// + /// + public string GetYoutubeThemeUrl(string themerrDbUrl, string movieTitle) + { + var client = new HttpClient(); + + try + { + var jsonString = client.GetStringAsync(themerrDbUrl).Result; + dynamic jsonData = JsonConvert.DeserializeObject(jsonString); + return jsonData?.youtube_theme_url; + } + catch (Exception e) + { + _logger.LogError("Unable to get theme song for {MovieTitle}: {Error}", movieTitle, e); + return string.Empty; + } + } + + /// + /// Save the themerr data file. + /// + /// + /// + /// + public bool SaveThemerrData(string themerrDataPath, string youtubeThemeUrl) + { + var success = false; + var themerrData = new + { + downloaded_timestamp = DateTime.UtcNow, + youtube_theme_url = youtubeThemeUrl + }; + try + { + System.IO.File.WriteAllText(themerrDataPath, JsonConvert.SerializeObject(themerrData)); + success = true; + } + catch (Exception e) + { + _logger.LogError("Unable to save themerr data to {ThemerrDataPath}: {Error}", themerrDataPath, e); + } + + return success && WaitForFile(themerrDataPath, 10000); + } + + /// + /// Wait for file to exist on disk. + /// + /// + /// + /// + public bool WaitForFile(string filePath, int timeout) + { + var startTime = DateTime.UtcNow; + while (!System.IO.File.Exists(filePath)) + { + if (DateTime.UtcNow - startTime > TimeSpan.FromMilliseconds(timeout)) { - continue; + return false; } - - // open themerr.json and check if theme song is already downloaded - var existingYoutubeThemeUrl = ""; - if (System.IO.File.Exists(themerrDataPath)) + Thread.Sleep(100); + } + + // wait until file is not being used by another process + var fileIsLocked = true; + while (fileIsLocked) + { + try { - var jsonString = System.IO.File.ReadAllText(themerrDataPath); - dynamic jsonData = JsonConvert.DeserializeObject(jsonString); - if (jsonData != null) - { - existingYoutubeThemeUrl = jsonData.youtube_theme_url; - } + using (System.IO.File.Open(filePath, System.IO.FileMode.Open)) { } + fileIsLocked = false; } - - // get tmdb id - var tmdb = movie.GetProviderId(MetadataProvider.Tmdb); - // create themerrdb_link - var themerrDbLink = $"https://app.lizardbyte.dev/ThemerrDB/movies/themoviedb/{tmdb}.json"; - - // download themerrdb_link as a json object - var client = new HttpClient(); - try + catch (System.IO.IOException) { - var jsonString = client.GetStringAsync(themerrDbLink).Result; - // serialize the json object - dynamic jsonData = JsonConvert.DeserializeObject(jsonString); - if (jsonData != null) + if (DateTime.UtcNow - startTime > TimeSpan.FromMilliseconds(timeout)) { - // extract the youtube_theme_url key (string) - string youtubeThemeUrl = jsonData.youtube_theme_url; - - // if youtubeThemeUrl is not equal to existingYoutubeThemeUrl then download - if (youtubeThemeUrl == existingYoutubeThemeUrl) - { - continue; - } - - _logger.LogDebug("Trying to download {movieName}, {youtubeThemeUrl}", - movie.Name, youtubeThemeUrl); - - try - { - SaveMp3(themePath, youtubeThemeUrl); - _logger.LogInformation("{movieName} theme song successfully downloaded", - movie.Name); - // create themerr.json (json object) with these keys, youtube_theme_url, downloaded_timestamp - var themerrData = new - { - downloaded_timestamp = DateTime.UtcNow, - youtube_theme_url = youtubeThemeUrl - }; - // write themerr.json to disk - System.IO.File.WriteAllText(themerrDataPath, JsonConvert.SerializeObject(themerrData)); - - // update the metadata - movie.RefreshMetadata(CancellationToken.None); - } - catch (Exception e) - { - _logger.LogError("Unable to download {movieName} theme song: {error}", - movie.Name, e); - } + return false; } - else - { - _logger.LogInformation("{movieName} theme song not in database, or no internet connection", - movie.Name); - } - - } - catch (Exception) - { - _logger.LogInformation("{movieName} theme song not in database, or no internet connection", - movie.Name); + Thread.Sleep(100); } } - return Task.CompletedTask; + + return true; } - - + + /// + /// Called when the plugin is loaded. + /// private void OnTimerElapsed() { // Stop the timer until next update _timer.Change(Timeout.Infinite, Timeout.Infinite); } - + /// + /// Todo + /// + /// public Task RunAsync() { return Task.CompletedTask; } - + /// + /// Todo + /// public void Dispose() { } diff --git a/docs/source/contributing/testing.rst b/docs/source/contributing/testing.rst index 19632d04..52c36875 100644 --- a/docs/source/contributing/testing.rst +++ b/docs/source/contributing/testing.rst @@ -50,4 +50,4 @@ Themerr-jellyfin uses `xUnit `__ for unit Test with xUnit .. code-block:: bash - dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover + dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover --logger "console;verbosity=detailed"