diff --git a/Jellyfin.Plugin.Themerr.Tests/FixtureJellyfinServer.cs b/Jellyfin.Plugin.Themerr.Tests/FixtureJellyfinServer.cs index 2a76c94..25ab6a6 100644 --- a/Jellyfin.Plugin.Themerr.Tests/FixtureJellyfinServer.cs +++ b/Jellyfin.Plugin.Themerr.Tests/FixtureJellyfinServer.cs @@ -1,22 +1,25 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider; -using Movie = MediaBrowser.Controller.Entities.Movies.Movie; namespace Jellyfin.Plugin.Themerr.Tests; /// -/// This class is used as a fixture for the Jellyfin server with mock movies +/// This class is used as a fixture for the Jellyfin server with mock media items (movies and series). /// public class FixtureJellyfinServer { /// - /// Mock movies to use for testing + /// Mock media items to use for testing /// - /// List containing mock objects. - public static List MockMovies() + /// List containing mock objects. + public static List MockItems() { - return new List + return new List { - new() + new Movie { Name = "Elephants Dream", ProductionYear = 2006, @@ -26,7 +29,7 @@ public static List MockMovies() { MetadataProvider.Tmdb.ToString(), "9761"}, } }, - new() + new Movie { Name = "Sita Sings the Blues", ProductionYear = 2008, @@ -36,7 +39,7 @@ public static List MockMovies() { MetadataProvider.Tmdb.ToString(), "20529"}, } }, - new() + new Movie { Name = "Big Buck Bunny", ProductionYear = 2008, @@ -46,7 +49,7 @@ public static List MockMovies() { MetadataProvider.Tmdb.ToString(), "10378"}, } }, - new() + new Movie { Name = "Sintel", ProductionYear = 2010, @@ -56,18 +59,38 @@ public static List MockMovies() { MetadataProvider.Tmdb.ToString(), "45745"}, } }, + new Series + { + Name = "Game of Thrones", + ProductionYear = 2011, + ProviderIds = new Dictionary + { + { MetadataProvider.Imdb.ToString(), "tt0944947"}, + { MetadataProvider.Tmdb.ToString(), "1399"}, + } + }, + new Series + { + Name = "The 100", + ProductionYear = 2014, + ProviderIds = new Dictionary + { + { MetadataProvider.Imdb.ToString(), "tt2661044"}, + { MetadataProvider.Tmdb.ToString(), "48866"}, + } + } }; } /// - /// Mock movies without an associated theme in ThemerrDB to use for testing + /// Mock items without an associated theme in ThemerrDB to use for testing /// - /// List containing mock objects. - public static List MockMovies2() + /// List containing mock objects. + public static List MockItems2() { - return new List + return new List { - new() + new Movie { Name = "Themerr Test Movie", ProductionYear = 1970, @@ -77,17 +100,46 @@ public static List MockMovies2() { MetadataProvider.Tmdb.ToString(), "0"}, } }, + new Series + { + Name = "Themerr Test Show", + ProductionYear = 1970, + ProviderIds = new Dictionary + { + { MetadataProvider.Imdb.ToString(), "tt0000000"}, + { MetadataProvider.Tmdb.ToString(), "0"}, + } + }, }; } /// - /// Create mock movies from stub video + /// Mock items which are not supported by Themerr + /// + /// List containing mock objects. + public static List UnsupportedMockItems() + { + return new List + { + new MusicAlbum + { + Name = "Themerr Test Album", + }, + new MusicArtist + { + Name = "Themerr Test Artist", + }, + }; + } + + /// + /// Create mock items from stub video /// [Fact] [Trait("Category", "Init")] - private void CreateMockMovies() + private void CreateMockItems() { - var mockMovies = MockMovies(); + var mockItems = MockItems(); // get the stub video path based on the directory of this file var stubVideoPath = Path.Combine( @@ -97,29 +149,50 @@ private void CreateMockMovies() Assert.True(File.Exists(stubVideoPath), "Could not find ./data/video_stub.mp4"); - foreach (var movie in mockMovies) + foreach (var item in mockItems) { // copy the ./data/video_stub.mp4 to the movie folder "movie.Name (movie.ProductionYear)" - var movieFolder = Path.Combine( + var itemFolder = Path.Combine( "themerr_jellyfin_tests", - $"{movie.Name} ({movie.ProductionYear})"); + $"{item.Name} ({item.ProductionYear})"); // create the movie folder - Directory.CreateDirectory(movieFolder); + Directory.CreateDirectory(itemFolder); - // 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"); + string? itemVideoPath = null; + if (item is Movie) + { + // copy the video_stub.mp4 to the movie folder, renaming it based on movie name + itemVideoPath = Path.Combine( + itemFolder, + $"{item.Name} ({item.ProductionYear}).mp4"); + } + else if (item is Series) + { + // season folder + var seasonFolder = Path.Combine( + itemFolder, + "Season 01"); + Directory.CreateDirectory(seasonFolder); + + // copy the video_stub.mp4 to the season folder, renaming it based on series name + itemVideoPath = Path.Combine( + seasonFolder, + $"{item.Name} ({item.ProductionYear}) - S01E01 - Episode Name.mp4"); + } + else + { + Assert.Fail($"Unknown item type: {item.GetType()}"); + } // if file does not exist - if (!File.Exists(movieVideoPath)) + if (!File.Exists(itemVideoPath)) { - // copy the stub video to the movie folder - File.Copy(stubVideoPath, movieVideoPath); + // copy the stub video to the item folder + File.Copy(stubVideoPath, itemVideoPath); } - Assert.True(File.Exists(movieVideoPath), $"Could not find {movieVideoPath}"); + Assert.True(File.Exists(itemVideoPath), $"Could not find {itemVideoPath}"); } } } diff --git a/Jellyfin.Plugin.Themerr.Tests/TestThemerrManager.cs b/Jellyfin.Plugin.Themerr.Tests/TestThemerrManager.cs index 7dd351d..9285f31 100644 --- a/Jellyfin.Plugin.Themerr.Tests/TestThemerrManager.cs +++ b/Jellyfin.Plugin.Themerr.Tests/TestThemerrManager.cs @@ -1,3 +1,5 @@ +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using Microsoft.Extensions.Logging; using Moq; @@ -183,52 +185,70 @@ private void TestSaveMp3InvalidUrl() [Fact] [Trait("Category", "Unit")] - private void TestGetMoviesFromLibrary() + private void TestGetTmdbItemsFromLibrary() { - var movies = _themerrManager.GetMoviesFromLibrary(); + var items = _themerrManager.GetTmdbItemsFromLibrary(); - // movies list should be empty - Assert.Empty(movies); + // items list should be empty + Assert.Empty(items); - // todo: test with actual movies + // todo: test with actual items } - // todo: fix this test - // [Fact] - // [Trait("Category", "Unit")] - // private void TestProcessMovieTheme() - // { - // // get fixture movies - // var mockMovies = FixtureJellyfinServer.MockMovies(); - // - // Assert.True(mockMovies.Count > 0, "mockMovies.Count is not greater than 0"); - // - // foreach (var movie in mockMovies) - // { - // // get the movie theme - // _themerrManager.ProcessMovieTheme(movie); - // - // Assert.True(File.Exists(_themerrManager.GetThemePath(movie)), $"File {_themerrManager.GetThemePath(movie)} does not exist"); - // } - // } + [Fact] + [Trait("Category", "Unit")] + private void TestProcessItemTheme() + { + // get fixture movies + var mockItems = FixtureJellyfinServer.MockItems(); + + Assert.True(mockItems.Count > 0, "mockItems.Count is not greater than 0"); + + foreach (var item in mockItems) + { + // get the item theme + _themerrManager.ProcessItemTheme(item); + + Assert.True(File.Exists(_themerrManager.GetThemePath(item)), $"File {_themerrManager.GetThemePath(item)} does not exist"); + + // cleanup and delete the file + File.Delete(_themerrManager.GetThemePath(item)); + } + } + + [Fact] + [Trait("Category", "Unit")] + private void TestProcessItemThemeUnsupportedType() + { + // get fixture items + var mockItems = FixtureJellyfinServer.UnsupportedMockItems(); + + foreach (var item in mockItems) + { + // get the item theme + _themerrManager.ProcessItemTheme(item); + + Assert.False(File.Exists(_themerrManager.GetThemePath(item)), $"File {_themerrManager.GetThemePath(item)} exists"); + } + } [Fact] [Trait("Category", "Unit")] private void TestGetTmdbId() { - // get fixture movies - var mockMovies = FixtureJellyfinServer.MockMovies(); + // get fixture items + var mockItems = FixtureJellyfinServer.MockItems(); - foreach (var movie in mockMovies) + foreach (var item in mockItems) { - // get the movie theme - var tmdbId = _themerrManager.GetTmdbId(movie); + // get the item theme + var tmdbId = _themerrManager.GetTmdbId(item); // 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); + // ensure tmdbId is the same as the one in the item fixture + Assert.Equal(item.ProviderIds[MetadataProvider.Tmdb.ToString()], tmdbId); } } @@ -237,19 +257,19 @@ private void TestGetTmdbId() // [Trait("Category", "Unit")] // private void TestGetThemeProvider() // { - // // get fixture movies - // var mockMovies = FixtureJellyfinServer.MockMovies(); + // // get fixture items + // var mockItems = FixtureJellyfinServer.MockItems(); // - // foreach (var movie in mockMovies) + // foreach (var item in mockItems) // { - // // get the movie theme - // var themeProvider = _themerrManager.GetThemeProvider(movie); + // // get the item theme + // var themeProvider = _themerrManager.GetThemeProvider(item); // // // ensure themeProvider null // Assert.Null(themeProvider); // } // - // // todo: test with actual movies + // // todo: test with actual items // } [Fact] @@ -334,53 +354,104 @@ private void TestContinueDownload() [Trait("Category", "Unit")] private void TestGetThemePath() { - // get fixture movies - var mockMovies = FixtureJellyfinServer.MockMovies(); + // get fixture items + var mockItems = FixtureJellyfinServer.MockItems(); - Assert.True(mockMovies.Count > 0, "mockMovies.Count is not greater than 0"); + Assert.True(mockItems.Count > 0, "mockItems.Count is not greater than 0"); - foreach (var movie in mockMovies) + foreach (var item in mockItems) { - // get the movie theme - var themePath = _themerrManager.GetThemePath(movie); + // get the item theme + var themePath = _themerrManager.GetThemePath(item); // ensure path ends with theme.mp3 Assert.EndsWith("theme.mp3", themePath); } } + [Fact] + [Trait("Category", "Unit")] + private void TestGetThemePathUnsupportedType() + { + // get fixture items + var mockItems = FixtureJellyfinServer.UnsupportedMockItems(); + + Assert.True(mockItems.Count > 0, "mockItems.Count is not greater than 0"); + + foreach (var item in mockItems) + { + // get the item theme + var themePath = _themerrManager.GetThemePath(item); + + // ensure path is null + Assert.Null(themePath); + } + } + [Fact] [Trait("Category", "Unit")] private void TestGetThemerrDataPath() { - // get fixture movies - var mockMovies = FixtureJellyfinServer.MockMovies(); + // get fixture items + var mockItems = FixtureJellyfinServer.MockItems(); - Assert.True(mockMovies.Count > 0, "mockMovies.Count is not greater than 0"); + Assert.True(mockItems.Count > 0, "mockItems.Count is not greater than 0"); - foreach (var movie in mockMovies) + foreach (var item in mockItems) { - // get the movie theme - var themerrDataPath = _themerrManager.GetThemerrDataPath(movie); + // get the item theme + var themerrDataPath = _themerrManager.GetThemerrDataPath(item); // ensure path ends with theme.mp3 Assert.EndsWith("themerr.json", themerrDataPath); } } + [Fact] + [Trait("Category", "Unit")] + private void TestGetThemerrDataPathUnsupportedType() + { + // get fixture items + var mockItems = FixtureJellyfinServer.UnsupportedMockItems(); + + Assert.True(mockItems.Count > 0, "mockItems.Count is not greater than 0"); + + foreach (var item in mockItems) + { + // get the item theme + var themerrDataPath = _themerrManager.GetThemerrDataPath(item); + + // ensure path is null + Assert.Null(themerrDataPath); + } + } + [Fact] [Trait("Category", "Unit")] private void TestCreateThemerrDbLink() { - // get fixture movies - var mockMovies = FixtureJellyfinServer.MockMovies(); + // get fixture items + var mockItems = FixtureJellyfinServer.MockItems(); - Assert.True(mockMovies.Count > 0, "mockMovies.Count is not greater than 0"); + Assert.True(mockItems.Count > 0, "mockItems.Count is not greater than 0"); - foreach (var movie in mockMovies) + foreach (var item in mockItems) { - var tmdbId = movie.ProviderIds[MetadataProvider.Tmdb.ToString()]; - var themerrDbUrl = _themerrManager.CreateThemerrDbLink(tmdbId); + var dbType = item switch + { + Movie _ => "movies", + Series _ => "tv_shows", + _ => null + }; + + // return if dbType is null + if (string.IsNullOrEmpty(dbType)) + { + Assert.Fail($"Unknown item type: {item.GetType()}"); + } + + var tmdbId = item.ProviderIds[MetadataProvider.Tmdb.ToString()]; + var themerrDbUrl = _themerrManager.CreateThemerrDbLink(tmdbId, dbType); TestLogger.Info($"themerrDbLink: {themerrDbUrl}"); @@ -392,20 +463,33 @@ private void TestCreateThemerrDbLink() [Trait("Category", "Unit")] private void TestGetYoutubeThemeUrl() { - // get fixture movies - var mockMovies = FixtureJellyfinServer.MockMovies(); + // get fixture items + var mockItems = FixtureJellyfinServer.MockItems(); - Assert.True(mockMovies.Count > 0, "mockMovies.Count is not greater than 0"); + Assert.True(mockItems.Count > 0, "mockItems.Count is not greater than 0"); - // loop over each movie - foreach (var movie in mockMovies) + // loop over each item + foreach (var item in mockItems) { + var dbType = item switch + { + Movie _ => "movies", + Series _ => "tv_shows", + _ => null + }; + + // return if dbType is null + if (string.IsNullOrEmpty(dbType)) + { + Assert.Fail($"Unknown item type: {item.GetType()}"); + } + // get themerrDbUrl - var tmdbId = _themerrManager.GetTmdbId(movie); - var themerrDbLink = _themerrManager.CreateThemerrDbLink(tmdbId); + var tmdbId = _themerrManager.GetTmdbId(item); + var themerrDbLink = _themerrManager.CreateThemerrDbLink(tmdbId, dbType); // get the new youtube theme url - var youtubeThemeUrl = _themerrManager.GetYoutubeThemeUrl(themerrDbLink, movie); + var youtubeThemeUrl = _themerrManager.GetYoutubeThemeUrl(themerrDbLink, item); // log TestLogger.Info($"youtubeThemeUrl: {youtubeThemeUrl}"); @@ -418,20 +502,33 @@ private void TestGetYoutubeThemeUrl() [Trait("Category", "Unit")] private void TestGetYoutubeThemeUrlExceptions() { - // get fixture movies - var mockMovies = FixtureJellyfinServer.MockMovies2(); + // get fixture items + var mockItems = FixtureJellyfinServer.MockItems2(); - Assert.True(mockMovies.Count > 0, "mockMovies.Count is not greater than 0"); + Assert.True(mockItems.Count > 0, "mockItems.Count is not greater than 0"); - // loop over each movie - foreach (var movie in mockMovies) + // loop over each item + foreach (var item in mockItems) { + var dbType = item switch + { + Movie _ => "movies", + Series _ => "tv_shows", + _ => null + }; + + // return if dbType is null + if (string.IsNullOrEmpty(dbType)) + { + Assert.Fail($"Unknown item type: {item.GetType()}"); + } + // get themerrDbUrl - var tmdbId = _themerrManager.GetTmdbId(movie); - var themerrDbLink = _themerrManager.CreateThemerrDbLink(tmdbId); + var tmdbId = _themerrManager.GetTmdbId(item); + var themerrDbLink = _themerrManager.CreateThemerrDbLink(tmdbId, dbType); // get the new youtube theme url - var youtubeThemeUrl = _themerrManager.GetYoutubeThemeUrl(themerrDbLink, movie); + var youtubeThemeUrl = _themerrManager.GetYoutubeThemeUrl(themerrDbLink, item); Assert.Empty(youtubeThemeUrl); } @@ -441,22 +538,42 @@ private void TestGetYoutubeThemeUrlExceptions() [Trait("Category", "Unit")] private void TestGetIssueUrl() { - // get fixture movies - var mockMovies = FixtureJellyfinServer.MockMovies(); + // get fixture items + var mockItems = FixtureJellyfinServer.MockItems(); - Assert.True(mockMovies.Count > 0, "mockMovies.Count is not greater than 0"); + Assert.True(mockItems.Count > 0, "mockItems.Count is not greater than 0"); - // loop over each movie - foreach (var movie in mockMovies) + // loop over each item + foreach (var item in mockItems) { + var issueType = item switch + { + Movie _ => "MOVIE", + Series _ => "TV SHOW", + _ => null + }; + + var tmdbEndpoint = item switch + { + Movie _ => "movie", + Series _ => "tv", + _ => null + }; + + // return if dbType is null + if (string.IsNullOrEmpty(issueType) || string.IsNullOrEmpty(tmdbEndpoint)) + { + Assert.Fail($"Unknown item type: {item.GetType()}"); + } + // parts of expected url - var tmdbId = _themerrManager.GetTmdbId(movie); - var encodedName = movie.Name.Replace(" ", "%20"); - var year = movie.ProductionYear; - var expectedUrl = $"https://github.com/LizardByte/ThemerrDB/issues/new?assignees=&labels=request-theme&template=theme.yml&title=[MOVIE]:%20{encodedName}%20({year})&database_url=https://www.themoviedb.org/movie/{tmdbId}"; + var tmdbId = _themerrManager.GetTmdbId(item); + var encodedName = item.Name.Replace(" ", "%20"); + var year = item.ProductionYear; + var expectedUrl = $"https://github.com/LizardByte/ThemerrDB/issues/new?assignees=&labels=request-theme&template=theme.yml&title=[{issueType}]:%20{encodedName}%20({year})&database_url=https://www.themoviedb.org/{tmdbEndpoint}/{tmdbId}"; // get the new youtube theme url - var issueUrl = _themerrManager.GetIssueUrl(movie); + var issueUrl = _themerrManager.GetIssueUrl(item); Assert.NotEmpty(issueUrl); @@ -468,6 +585,25 @@ private void TestGetIssueUrl() } } + [Fact] + [Trait("Category", "Unit")] + private void TestGetIssueUrlUnsupportedType() + { + // get fixture items + var mockItems = FixtureJellyfinServer.UnsupportedMockItems(); + + Assert.True(mockItems.Count > 0, "mockItems.Count is not greater than 0"); + + // loop over each item + foreach (var item in mockItems) + { + // get the new youtube theme url + var issueUrl = _themerrManager.GetIssueUrl(item); + + Assert.Null(issueUrl); + } + } + [Fact] [Trait("Category", "Unit")] private void TestSaveThemerrData() diff --git a/Jellyfin.Plugin.Themerr/Api/ThemerrController.cs b/Jellyfin.Plugin.Themerr/Api/ThemerrController.cs index 64eace8..a7b21b9 100644 --- a/Jellyfin.Plugin.Themerr/Api/ThemerrController.cs +++ b/Jellyfin.Plugin.Themerr/Api/ThemerrController.cs @@ -3,7 +3,6 @@ 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; @@ -49,7 +48,7 @@ public ThemerrController( [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task TriggerUpdateRequest() { - _logger.LogInformation("Updating Movie Theme Songs"); + _logger.LogInformation("Updating Theme Songs"); await _themerrManager.UpdateAll(); _logger.LogInformation("Completed"); } @@ -57,11 +56,11 @@ public async Task TriggerUpdateRequest() /// /// Get the data required to populate the progress dashboard. /// - /// Loop over all Jellyfin libraries and movies, creating a json object with the following structure: + /// Loop over all Jellyfin libraries and supported items, creating a json object with the following structure: /// { - /// "items": [Movies], - /// "media_count": Movies.Count, - /// "media_percent_complete": ThemedMovies.Count / Movies.Count * 100, + /// "items": [BaseItems], + /// "media_count": BaseItems.Count, + /// "media_percent_complete": ThemedItems.Count / BaseItems.Count * 100, /// } /// /// JSON object containing progress data. @@ -75,29 +74,30 @@ public ActionResult GetProgress() var mediaWithThemes = 0; var mediaPercentComplete = 0; - var movies = _themerrManager.GetMoviesFromLibrary(); + var items = _themerrManager.GetTmdbItemsFromLibrary(); - // sort movies by name, then year - var enumerable = movies.OrderBy(m => m.Name).ThenBy(m => m.ProductionYear); + // sort items by name, then year + var enumerable = items.OrderBy(i => i.Name).ThenBy(i => i.ProductionYear); - foreach (var movie in enumerable) + foreach (var item in enumerable) { - var year = movie.ProductionYear; - var issueUrl = _themerrManager.GetIssueUrl(movie); - var themeProvider = _themerrManager.GetThemeProvider(movie); - var item = new + var year = item.ProductionYear; + var issueUrl = _themerrManager.GetIssueUrl(item); + var themeProvider = _themerrManager.GetThemeProvider(item); + var tmpItem = new { - name = movie.Name, - id = movie.Id, + name = item.Name, + id = item.Id, issue_url = issueUrl, theme_provider = themeProvider, + type = item.GetType().Name, // Movie, Series, etc. year = year }; - tmpItems.Add(item); + tmpItems.Add(tmpItem); mediaCount++; - var themeSongs = movie.GetThemeSongs(); + var themeSongs = item.GetThemeSongs(); if (themeSongs.Count > 0) { mediaWithThemes++; diff --git a/Jellyfin.Plugin.Themerr/Configuration/configPage.html b/Jellyfin.Plugin.Themerr/Configuration/configPage.html index 3ca35a0..8907203 100644 --- a/Jellyfin.Plugin.Themerr/Configuration/configPage.html +++ b/Jellyfin.Plugin.Themerr/Configuration/configPage.html @@ -64,7 +64,7 @@

Themerr



- +
@@ -204,7 +204,7 @@

Themerr

let tableHeaderRow = document.createElement('tr') tableHeader.appendChild(tableHeaderRow) - let columns = ['Title', 'Year', 'Contribute', 'Status'] + let columns = ['Title', 'Year', 'Type', 'Contribute', 'Status'] // create table header columns for (let column in columns) { @@ -216,10 +216,12 @@

Themerr

// loop over items for (let item in response["items"]) { + console.log(LogPrefix + "------------------") console.log(LogPrefix + "item: " + item) console.log(LogPrefix + "name: " + response["items"][item]["name"]) console.log(LogPrefix + "id: " + response["items"][item]["id"]) console.log(LogPrefix + "issue_url: " + response["items"][item]["issue_url"]) + console.log(LogPrefix + "type: " + response["items"][item]["type"]) console.log(LogPrefix + "theme_provider: " + response["items"][item]["theme_provider"]) console.log(LogPrefix + "year: " + response["items"][item]["year"]) @@ -235,6 +237,10 @@

Themerr

let tableColumnYear = document.createElement('td') tableColumnYear.innerHTML = response["items"][item]["year"] tableRow.appendChild(tableColumnYear) + + let tableColumnType = document.createElement('td') + tableColumnType.innerHTML = response["items"][item]["type"] + tableRow.appendChild(tableColumnType) let contributeButton = document.createElement('a') contributeButton.setAttribute('is', 'emby-linkbutton') diff --git a/Jellyfin.Plugin.Themerr/ScheduledTasks/ThemerrTasks.cs b/Jellyfin.Plugin.Themerr/ScheduledTasks/ThemerrTasks.cs index c51946d..2f267e7 100644 --- a/Jellyfin.Plugin.Themerr/ScheduledTasks/ThemerrTasks.cs +++ b/Jellyfin.Plugin.Themerr/ScheduledTasks/ThemerrTasks.cs @@ -40,7 +40,7 @@ public ThemerrTasks(ILibraryManager libraryManager, ILogger logg /// /// Gets the description of the task. /// - public string Description => "Scans all libraries to download Movie Theme Songs"; + public string Description => "Scans all libraries to download supported Theme Songs"; /// /// Gets the category of the task. @@ -55,7 +55,7 @@ public ThemerrTasks(ILibraryManager libraryManager, ILogger logg /// A representing the asynchronous operation. public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { - _logger.LogInformation("Starting plugin, Downloading Movie Theme Songs..."); + _logger.LogInformation("Starting plugin, Downloading supported Theme Songs..."); await _themerrManager.UpdateAll(); _logger.LogInformation("All theme songs downloaded"); } diff --git a/Jellyfin.Plugin.Themerr/ThemerrManager.cs b/Jellyfin.Plugin.Themerr/ThemerrManager.cs index 681dbe6..3d8d6bc 100644 --- a/Jellyfin.Plugin.Themerr/ThemerrManager.cs +++ b/Jellyfin.Plugin.Themerr/ThemerrManager.cs @@ -7,6 +7,7 @@ using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Entities; @@ -15,9 +16,6 @@ using YoutubeExplode; using YoutubeExplode.Videos.Streams; -// TODO: Add support for TV shows -// using MediaBrowser.Controller.Entities.TV; - namespace Jellyfin.Plugin.Themerr { /// @@ -94,59 +92,71 @@ public bool SaveMp3(string destination, string videoUrl) } /// - /// Get all movies from the library that have a tmdb id. + /// Get all supported items from the library that have a tmdb id. /// - /// List of . - public IEnumerable GetMoviesFromLibrary() + /// List of objects. + public IEnumerable GetTmdbItemsFromLibrary() { - var movies = _libraryManager.GetItemList(new InternalItemsQuery + var items = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] {BaseItemKind.Movie}, + IncludeItemTypes = new[] + { + BaseItemKind.Movie, + BaseItemKind.Series + }, IsVirtualItem = false, Recursive = true, HasTmdbId = true }); - var movieList = new List(); - if (movies == null || movies.Count == 0) + var itemList = new List(); + if (items == null || items.Count == 0) { - return movieList; + return itemList; } - foreach (var movie in movies) - { - if (movie is Movie m) - { - movieList.Add(m); - } - } + itemList.AddRange(items.Where(item => item is Movie or Series)); - return movieList; + return itemList; } /// - /// Enumerate through all movies in the library and downloads their theme songs as required. + /// Enumerate through all supported items in the library and downloads their theme songs as required. /// /// A representing the asynchronous operation. public Task UpdateAll() { - var movies = GetMoviesFromLibrary(); - foreach (var movie in movies) + var items = GetTmdbItemsFromLibrary(); + foreach (var item in items) { - ProcessMovieTheme(movie); + ProcessItemTheme(item); } return Task.CompletedTask; } /// - /// Download the theme song for a movie if it doesn't already exist. + /// Download the theme song for a media item if it doesn't already exist. /// - /// The Jellyfin movie object. - public void ProcessMovieTheme(Movie movie) + /// The Jellyfin media object. + public void ProcessItemTheme(BaseItem item) { - var themePath = GetThemePath(movie); - var themerrDataPath = GetThemerrDataPath(movie); + // get themerrDB database type, used to create the themerrdb url + var dbType = item switch + { + Movie _ => "movies", + Series _ => "tv_shows", + _ => null + }; + + // return if dbType is null + if (string.IsNullOrEmpty(dbType)) + { + return; + } + + var themePath = GetThemePath(item); + var themerrDataPath = GetThemerrDataPath(item); if (!ContinueDownload(themePath, themerrDataPath)) { @@ -156,12 +166,12 @@ public void ProcessMovieTheme(Movie movie) var existingYoutubeThemeUrl = GetExistingThemerrDataValue("youtube_theme_url", themerrDataPath); // get tmdb id - var tmdbId = GetTmdbId(movie); + var tmdbId = GetTmdbId(item); // create themerrdb url - var themerrDbUrl = CreateThemerrDbLink(tmdbId); + var themerrDbUrl = CreateThemerrDbLink(tmdbId, dbType); - var youtubeThemeUrl = GetYoutubeThemeUrl(themerrDbUrl, movie); + var youtubeThemeUrl = GetYoutubeThemeUrl(themerrDbUrl, item); // skip if no youtube theme url in ThemerrDB or // if the youtube themes match AND the theme_md5 is unknown @@ -184,35 +194,35 @@ public void ProcessMovieTheme(Movie movie) return; } - movie.RefreshMetadata(CancellationToken.None); + item.RefreshMetadata(CancellationToken.None); } /// - /// Get TMDB id from a movie. + /// Get TMDB id from an item. /// - /// The Jellyfin movie object. + /// The Jellyfin media object. /// TMDB id. - public string GetTmdbId(Movie movie) + public string GetTmdbId(BaseItem item) { - var tmdbId = movie.GetProviderId(MetadataProvider.Tmdb); + var tmdbId = item.GetProviderId(MetadataProvider.Tmdb); return tmdbId; } /// /// Get the theme provider. /// - /// The Jellyfin movie object. + /// The Jellyfin media object. /// The theme provider. - public string GetThemeProvider(Movie movie) + public string GetThemeProvider(BaseItem item) { // check if item has a theme song - var themeSongs = movie.GetThemeSongs(); + var themeSongs = item.GetThemeSongs(); if (themeSongs == null || themeSongs.Count == 0) { return null; } - var themerrDataPath = GetThemerrDataPath(movie); + var themerrDataPath = GetThemerrDataPath(item); var themerrHash = GetExistingThemerrDataValue("theme_md5", themerrDataPath); var themeHash = GetMd5Hash(themeSongs[0].Path); @@ -267,40 +277,51 @@ public bool ContinueDownload(string themePath, string themerrDataPath) /// /// Get the path to the theme song. /// - /// The Jellyfin movie object. + /// The Jellyfin media object. /// The path to the theme song. - public string GetThemePath(Movie movie) + public string GetThemePath(BaseItem item) { - return $"{movie.ContainingFolderPath}/theme.mp3"; + return item switch + { + Movie movie => System.IO.Path.Join(movie.ContainingFolderPath, "theme.mp3"), + Series series => System.IO.Path.Join(series.Path, "theme.mp3"), + _ => null + }; } /// /// Get the path to the themerr data file. /// - /// The Jellyfin movie object. + /// The Jellyfin media object. /// The path to the themerr data file. - public string GetThemerrDataPath(Movie movie) + public string GetThemerrDataPath(BaseItem item) { - return $"{movie.ContainingFolderPath}/themerr.json"; + return item switch + { + Movie movie => System.IO.Path.Join(movie.ContainingFolderPath, "themerr.json"), + Series series => System.IO.Path.Join(series.Path, "themerr.json"), + _ => null + }; } /// /// Create a link to the themerr database. /// /// The tmdb id. + /// The database type. /// The themerr database link. - public string CreateThemerrDbLink(string tmdbId) + public string CreateThemerrDbLink(string tmdbId, string dbType) { - return $"https://app.lizardbyte.dev/ThemerrDB/movies/themoviedb/{tmdbId}.json"; + return $"https://app.lizardbyte.dev/ThemerrDB/{dbType}/themoviedb/{tmdbId}.json"; } /// /// Get the YouTube theme url from the themerr database. /// /// The themerr database url. - /// The Jellyfin movie object. + /// The Jellyfin media object. /// The YouTube theme url. - public string GetYoutubeThemeUrl(string themerrDbUrl, Movie movie) + public string GetYoutubeThemeUrl(string themerrDbUrl, BaseItem item) { var client = new HttpClient(); @@ -313,9 +334,9 @@ public string GetYoutubeThemeUrl(string themerrDbUrl, Movie movie) catch (Exception) { _logger.LogWarning( - "Missing from ThemerrDB: {MovieTitle}, contribute:\n {IssueUrl}", - movie.Name, - GetIssueUrl(movie)); + "Missing from ThemerrDB: {ItemTitle}, contribute:\n {IssueUrl}", + item.Name, + GetIssueUrl(item)); return string.Empty; } } @@ -325,18 +346,37 @@ public string GetYoutubeThemeUrl(string themerrDbUrl, Movie movie) /// /// This url can be used to easily add/edit theme songs in ThemerrDB. /// - /// The Jellyfin movie object. + /// The Jellyfin media object. /// The ThemerrDB issue url. - public string GetIssueUrl(Movie movie) + public string GetIssueUrl(BaseItem item) { + string issueBaseTitle = item switch + { + Movie _ => "MOVIE", + Series _ => "TV SHOW", + _ => null + }; + + string tmdbEndpoint = item switch + { + Movie _ => "movie", + Series _ => "tv", + _ => null + }; + + // return if either is null + if (string.IsNullOrEmpty(issueBaseTitle) || string.IsNullOrEmpty(tmdbEndpoint)) + { + return null; + } + // 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/"; + string issueBase = $"https://github.com/LizardByte/ThemerrDB/issues/new?assignees=&labels=request-theme&template=theme.yml&title=[{issueBaseTitle}]:%20"; + string databaseBase = $"https://www.themoviedb.org/{tmdbEndpoint}/"; - var urlEncodedName = movie.Name.Replace(" ", "%20"); - var year = movie.ProductionYear; - var tmdbId = GetTmdbId(movie); + var urlEncodedName = item.Name.Replace(" ", "%20"); + var year = item.ProductionYear; + var tmdbId = GetTmdbId(item); var issueUrl = $"{issueBase}{urlEncodedName}%20({year})&database_url={databaseBase}{tmdbId}"; return issueUrl; diff --git a/docs/Doxyfile b/docs/Doxyfile index 6d1d8c4..d8b0a4e 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -54,7 +54,7 @@ PROJECT_NUMBER = # for a project that appears at the top of each page and should give viewer a # quick idea about the purpose of the project. Keep the description short. -PROJECT_BRIEF = "Plugin for Jellyfin that adds theme songs to movies using ThemerrDB." +PROJECT_BRIEF = "Plugin for Jellyfin that adds theme songs to movies and tv shows using ThemerrDB." # With the PROJECT_LOGO tag one can specify a logo or an icon that is included # in the documentation. The maximum height of the logo should not exceed 55 diff --git a/docs/source/about/usage.rst b/docs/source/about/usage.rst index db587fd..8758746 100644 --- a/docs/source/about/usage.rst +++ b/docs/source/about/usage.rst @@ -11,10 +11,12 @@ Enable Themes #. Select `Display` from the user section. #. Within the `Library` section, ensure `Theme songs` is enabled. -Movie Directory Structure -------------------------- +Directory Structure +------------------- -.. Attention:: Jellyfin requires movies to be stored in separate subdirectories, with each movie in its own folder. +.. Attention:: Jellyfin requires your media to be stored in separate subdirectories, with each movie/show in its + own folder. See `Movies `__ + or `TV Shows `__ for more information. Task Activation --------------- diff --git a/docs/source/conf.py b/docs/source/conf.py index ef7a53f..df97880 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -118,9 +118,15 @@ 'Jellyfin.Controller.MediaBrowser.Model.Tasks': ( 'https://github.com/jellyfin/jellyfin/blob/v10.8.13/MediaBrowser.Model/Tasks/%s.cs', ), + 'Jellyfin.Controller.MediaBrowser.Controller.Entities': ( + 'https://github.com/jellyfin/jellyfin/blob/v10.8.13/MediaBrowser.Controller/Entities/%s.cs', + ), 'Jellyfin.Controller.MediaBrowser.Controller.Entities.Movies': ( 'https://github.com/jellyfin/jellyfin/blob/v10.8.13/MediaBrowser.Controller/Entities/Movies/%s.cs', ), + 'Jellyfin.Controller.MediaBrowser.Controller.Entities.TV': ( + 'https://github.com/jellyfin/jellyfin/blob/v10.8.13/MediaBrowser.Controller/Entities/TV/%s.cs', + ), 'Jellyfin.Controller.MediaBrowser.Controller.Entities.Library': ( 'https://github.com/jellyfin/jellyfin/blob/v10.8.13/MediaBrowser.Controller/Library/%s.cs', ), @@ -209,15 +215,25 @@ 'TaskTriggerInfo', ], }, + 'Jellyfin.Controller.MediaBrowser.Controller.Entities': { + '': [ + 'BaseItem', + ], + }, + 'Jellyfin.Controller.MediaBrowser.Controller.Entities.Library': { + '': [ + 'ILibraryManager', + ], + }, 'Jellyfin.Controller.MediaBrowser.Controller.Entities.Movies': { '': [ 'BoxSet', 'Movie', ], }, - 'Jellyfin.Controller.MediaBrowser.Controller.Entities.Library': { + 'Jellyfin.Controller.MediaBrowser.Controller.Entities.TV': { '': [ - 'ILibraryManager', + 'Series', ], }, 'Jellyfin.Controller.MediaBrowser.Controller.Plugins': { diff --git a/docs/source/contributing/testing.rst b/docs/source/contributing/testing.rst index be00be6..6ba52ad 100644 --- a/docs/source/contributing/testing.rst +++ b/docs/source/contributing/testing.rst @@ -21,9 +21,6 @@ Themerr-jellyfin uses `Sphinx `__ for doc other required python dependencies are included in the `./docs/requirements.txt` file. Python is required to build sphinx docs. Installation and setup of python will not be covered here. -.. todo:: - Add documentation within C# code to be included in sphinx docs. - The config file for Sphinx is ``docs/source/conf.py``. This is already included in the root of the repo and should not be modified.