diff --git a/Jellyfin.Plugin.Themerr.Tests/TestPluginConfiguration.cs b/Jellyfin.Plugin.Themerr.Tests/TestPluginConfiguration.cs index 49ee423..9133774 100644 --- a/Jellyfin.Plugin.Themerr.Tests/TestPluginConfiguration.cs +++ b/Jellyfin.Plugin.Themerr.Tests/TestPluginConfiguration.cs @@ -23,6 +23,7 @@ public TestPluginConfiguration(ITestOutputHelper output) /// Test getting the default PluginConfiguration. /// [Fact] + [Trait("Category", "Unit")] public void TestPluginConfigurationInstance() { // ensure UpdateInterval is an int diff --git a/Jellyfin.Plugin.Themerr.Tests/TestThemerrController.cs b/Jellyfin.Plugin.Themerr.Tests/TestThemerrController.cs new file mode 100644 index 0000000..cc17cef --- /dev/null +++ b/Jellyfin.Plugin.Themerr.Tests/TestThemerrController.cs @@ -0,0 +1,59 @@ +using System.Collections; +using Jellyfin.Plugin.Themerr.Api; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; + +namespace Jellyfin.Plugin.Themerr.Tests; + +/// +/// This class is responsible for testing the . +/// +[Collection("Fixture Collection")] +public class TestThemerrController +{ + private readonly ThemerrController _controller; + + /// + /// Initializes a new instance of the class. + /// + /// An instance. + public TestThemerrController(ITestOutputHelper output) + { + TestLogger.Initialize(output); + + Mock mockLibraryManager = new(); + Mock> mockLogger = new(); + _controller = new ThemerrController(mockLibraryManager.Object, mockLogger.Object); + } + + /// + /// Test GetProgress from API. + /// + [Fact] + [Trait("Category", "Unit")] + public void TestGetProgress() + { + var result = _controller.GetProgress(); + Assert.IsType(result); + + // ensure result["media_count"] is an int + Assert.IsType(((JsonResult)result).Value?.GetType().GetProperty("media_count")?.GetValue(((JsonResult)result).Value, null)); + + // ensure result["media_percent_complete"] is an int + Assert.IsType(((JsonResult)result).Value?.GetType().GetProperty("media_percent_complete")?.GetValue(((JsonResult)result).Value, null)); + + // ensure result["items"] is a an array list + Assert.IsType(((JsonResult)result).Value?.GetType().GetProperty("items")?.GetValue(((JsonResult)result).Value, null)); + + // ensure int values are 0 + Assert.Equal(0, ((JsonResult)result).Value?.GetType().GetProperty("media_count")?.GetValue(((JsonResult)result).Value, null)); + Assert.Equal(0, ((JsonResult)result).Value?.GetType().GetProperty("media_percent_complete")?.GetValue(((JsonResult)result).Value, null)); + + // ensure array list has no items + Assert.Equal(0, (((JsonResult)result).Value?.GetType().GetProperty("items")?.GetValue(((JsonResult)result).Value, null) as ArrayList)?.Count); + + // todo: add tests for when there are items + } +} diff --git a/Jellyfin.Plugin.Themerr.Tests/TestThemerrManager.cs b/Jellyfin.Plugin.Themerr.Tests/TestThemerrManager.cs index 4077330..be122ab 100644 --- a/Jellyfin.Plugin.Themerr.Tests/TestThemerrManager.cs +++ b/Jellyfin.Plugin.Themerr.Tests/TestThemerrManager.cs @@ -197,6 +197,18 @@ private void TestSaveMp3InvalidUrl() Assert.False(File.Exists(destinationFile), $"File {destinationFile} exists"); } + [Fact] + [Trait("Category", "Unit")] + private void TestGetMoviesFromLibrary() + { + var movies = _themerrManager.GetMoviesFromLibrary(); + + // movies list should be empty + Assert.Empty(movies); + + // todo: test with actual movies + } + // todo: fix this test // [Fact] // [Trait("Category", "Unit")] @@ -216,6 +228,46 @@ private void TestSaveMp3InvalidUrl() // } // } + [Fact] + [Trait("Category", "Unit")] + private void TestGetTmdbId() + { + // get fixture movies + var mockMovies = FixtureJellyfinServer.MockMovies(); + + foreach (var movie in mockMovies) + { + // get the movie theme + var tmdbId = _themerrManager.GetTmdbId(movie); + + // ensure tmdbId is not empty + Assert.NotEmpty(tmdbId); + + // ensure tmdbId is the same as the one in the movie fixture + Assert.Equal(movie.ProviderIds[MetadataProvider.Tmdb.ToString()], tmdbId); + } + } + + // todo: fix this test + // [Fact] + // [Trait("Category", "Unit")] + // private void TestGetThemeProvider() + // { + // // get fixture movies + // var mockMovies = FixtureJellyfinServer.MockMovies(); + // + // foreach (var movie in mockMovies) + // { + // // get the movie theme + // var themeProvider = _themerrManager.GetThemeProvider(movie); + // + // // ensure themeProvider null + // Assert.Null(themeProvider); + // } + // + // // todo: test with actual movies + // } + [Fact] [Trait("Category", "Unit")] private void TestContinueDownload() diff --git a/Jellyfin.Plugin.Themerr.Tests/TestThemerrPlugin.cs b/Jellyfin.Plugin.Themerr.Tests/TestThemerrPlugin.cs index 5b61644..8d5d32b 100644 --- a/Jellyfin.Plugin.Themerr.Tests/TestThemerrPlugin.cs +++ b/Jellyfin.Plugin.Themerr.Tests/TestThemerrPlugin.cs @@ -29,6 +29,7 @@ public TestThemerrPlugin(ITestOutputHelper output) /// Test getting the plugin name. /// [Fact] + [Trait("Category", "Unit")] public void TestPluginInstance() { Assert.NotNull(_plugin.Name); @@ -39,6 +40,7 @@ public void TestPluginInstance() /// Test getting the plugin description. /// [Fact] + [Trait("Category", "Unit")] public void TestPluginDescription() { Assert.NotNull(_plugin.Description); @@ -49,6 +51,7 @@ public void TestPluginDescription() /// Test get the plugin id. /// [Fact] + [Trait("Category", "Unit")] public void TestPluginId() { Assert.Equal(new Guid("84b59a39-bde4-42f4-adbd-c39882cbb772"), _plugin.Id); @@ -58,6 +61,7 @@ public void TestPluginId() /// Test getting the plugin configuration page. /// [Fact] + [Trait("Category", "Unit")] public void TestPluginConfigurationPage() { var pages = _plugin.GetPages(); diff --git a/Jellyfin.Plugin.Themerr/Api/ThemerrController.cs b/Jellyfin.Plugin.Themerr/Api/ThemerrController.cs index 5c60a76..15668ad 100644 --- a/Jellyfin.Plugin.Themerr/Api/ThemerrController.cs +++ b/Jellyfin.Plugin.Themerr/Api/ThemerrController.cs @@ -1,10 +1,15 @@ +using System; +using System.Collections; +using System.Linq; using System.Net.Mime; using System.Threading.Tasks; +using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using Newtonsoft.Json; namespace Jellyfin.Plugin.Themerr.Api { @@ -48,5 +53,77 @@ public async Task TriggerUpdateRequest() await _themerrManager.UpdateAll(); _logger.LogInformation("Completed"); } + + /// + /// Get the data required to populate the progress dashboard. + /// + /// Loop over all Jellyfin libraries and movies, creating a json object with the following structure: + /// { + /// "items": [Movies], + /// "media_count": Movies.Count, + /// "media_percent_complete": ThemedMovies.Count / Movies.Count * 100, + /// } + /// + /// JSON object containing progress data. + [HttpGet("GetProgress")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetProgress() + { + // url components + const string issueBase = "https://github.com/LizardByte/ThemerrDB/issues/new?assignees=&labels=request-theme&template=theme.yml&title=[MOVIE]:%20"; + const string databaseBase = "https://www.themoviedb.org/movie/"; + + var tmpItems = new ArrayList(); + + var mediaCount = 0; + var mediaWithThemes = 0; + var mediaPercentComplete = 0; + + var movies = _themerrManager.GetMoviesFromLibrary(); + + // sort movies by name, then year + var enumerable = movies.OrderBy(m => m.Name).ThenBy(m => m.ProductionYear); + + foreach (var movie in enumerable) + { + var urlEncodedName = movie.Name.Replace(" ", "%20"); + var year = movie.ProductionYear; + var tmdbId = _themerrManager.GetTmdbId(movie); + var themeProvider = _themerrManager.GetThemeProvider(movie); + var item = new + { + name = movie.Name, + id = movie.Id, + issue_url = $"{issueBase}{urlEncodedName}%20({year})&database_url={databaseBase}{tmdbId}", + theme_provider = themeProvider, + year = year + }; + tmpItems.Add(item); + + mediaCount++; + + var themeSongs = movie.GetThemeSongs(); + if (themeSongs.Count > 0) + { + mediaWithThemes++; + } + } + + if (mediaCount > 0) + { + mediaPercentComplete = (int)Math.Round((double)mediaWithThemes / mediaCount * 100); + } + + var tmpObject = new + { + items = tmpItems, + media_count = mediaCount, + media_percent_complete = mediaPercentComplete + }; + + _logger.LogInformation("Progress Items: {Items}", JsonConvert.SerializeObject(tmpObject)); + + return new JsonResult(tmpObject); + } } } diff --git a/Jellyfin.Plugin.Themerr/Configuration/configPage.html b/Jellyfin.Plugin.Themerr/Configuration/configPage.html index 0d39bcd..7b81953 100644 --- a/Jellyfin.Plugin.Themerr/Configuration/configPage.html +++ b/Jellyfin.Plugin.Themerr/Configuration/configPage.html @@ -21,7 +21,7 @@

Themerr


- +

+
+ +
+
+
diff --git a/Jellyfin.Plugin.Themerr/ThemerrManager.cs b/Jellyfin.Plugin.Themerr/ThemerrManager.cs index 897fc7c..d8ee954 100644 --- a/Jellyfin.Plugin.Themerr/ThemerrManager.cs +++ b/Jellyfin.Plugin.Themerr/ThemerrManager.cs @@ -99,13 +99,29 @@ public bool SaveMp3(string destination, string videoUrl) /// List of . public IEnumerable GetMoviesFromLibrary() { - return _libraryManager.GetItemList(new InternalItemsQuery + var movies = _libraryManager.GetItemList(new InternalItemsQuery { IncludeItemTypes = new[] {BaseItemKind.Movie}, IsVirtualItem = false, Recursive = true, HasTmdbId = true - }).Select(m => m as Movie); + }); + + var movieList = new List(); + if (movies == null || movies.Count == 0) + { + return movieList; + } + + foreach (var movie in movies) + { + if (movie is Movie m) + { + movieList.Add(m); + } + } + + return movieList; } /// @@ -141,7 +157,7 @@ public void ProcessMovieTheme(Movie movie) var existingYoutubeThemeUrl = GetExistingThemerrDataValue("youtube_theme_url", themerrDataPath); // get tmdb id - var tmdbId = movie.GetProviderId(MetadataProvider.Tmdb); + var tmdbId = GetTmdbId(movie); // create themerrdb url var themerrDbUrl = CreateThemerrDbLink(tmdbId); @@ -172,6 +188,39 @@ public void ProcessMovieTheme(Movie movie) movie.RefreshMetadata(CancellationToken.None); } + /// + /// Get TMDB id from a movie. + /// + /// The Jellyfin movie object. + /// TMDB id. + public string GetTmdbId(Movie movie) + { + var tmdbId = movie.GetProviderId(MetadataProvider.Tmdb); + return tmdbId; + } + + /// + /// Get the theme provider. + /// + /// The Jellyfin movie object. + /// The theme provider. + public string GetThemeProvider(Movie movie) + { + // check if item has a theme song + var themeSongs = movie.GetThemeSongs(); + if (themeSongs == null || themeSongs.Count == 0) + { + return null; + } + + var themerrDataPath = GetThemerrDataPath(movie); + var themerrHash = GetExistingThemerrDataValue("theme_md5", themerrDataPath); + var themeHash = GetMd5Hash(themeSongs[0].Path); + + // if hashes match, theme is supplied by themerr, otherwise it is user supplied + return themerrHash == themeHash ? "themerr" : "user"; + } + /// /// Check if the theme song should be downloaded. /// diff --git a/docs/source/conf.py b/docs/source/conf.py index 7c18e2a..ef7a53f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -148,6 +148,7 @@ }, 'Microsoft': { 'AspNetCore.Mvc': [ + 'ActionResult', 'ControllerBase', ], 'Extensions.Logging': [