diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..73da3e4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,48 @@ +[*.cs] +csharp_style_var_for_built_in_types=true:silent +csharp_style_var_when_type_is_apparent=true:silent +csharp_style_var_elsewhere=true:silent + +## SonarAnalyzers.CSharp + +# Remove this commented out code. +dotnet_diagnostic.S125.severity = None + +# Complete the task associated to this 'TODO' comment. +dotnet_diagnostic.S1135.severity = None + +# Remove this empty class, write its code or make it an "interface". +dotnet_diagnostic.S2094.severity = None + +# Fix this implementation of 'IDisposable' to conform to the dispose pattern. +dotnet_diagnostic.S3881.severity = None + + +## StyleCop.Analyzers + +# XML comment analysis is disabled due to project configuration +dotnet_diagnostic.SA0001.severity = None + +# Prefix local calls with this +dotnet_diagnostic.SA1101.severity = None + +# Opening brace should be followed by a space +dotnet_diagnostic.SA1012.severity = None + +# Closing brace should be preceded by a space +dotnet_diagnostic.SA1013.severity = None + +# Using directive should appear within a namespace declaration +dotnet_diagnostic.SA1200.severity = None + +# Field '_blah' should not begin with an underscore +dotnet_diagnostic.SA1309.severity = None + +# Use trailing comma in multi-line initializers +dotnet_diagnostic.SA1413.severity = None + +# Single-line comments should not be followed by blank line +dotnet_diagnostic.SA1512.severity = None + +# The file header is missing or not located at the top of the file +dotnet_diagnostic.SA1633.severity = None diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..bede98c --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,16 @@ + + + + + + diff --git a/Jellyfin.Plugin.Themerr.Tests/FixtureCollection.cs b/Jellyfin.Plugin.Themerr.Tests/FixtureCollection.cs new file mode 100644 index 0000000..f96e545 --- /dev/null +++ b/Jellyfin.Plugin.Themerr.Tests/FixtureCollection.cs @@ -0,0 +1,14 @@ +using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider; +using Movie = MediaBrowser.Controller.Entities.Movies.Movie; + +namespace Jellyfin.Plugin.Themerr.Tests; + +/// +/// This class is used to create a collection of tests. +/// +[CollectionDefinition("Fixture Collection")] +public class FixtureCollection : 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. +} diff --git a/Jellyfin.Plugin.Themerr.Tests/BootstrapJellyfinServer.cs b/Jellyfin.Plugin.Themerr.Tests/FixtureJellyfinServer.cs similarity index 81% rename from Jellyfin.Plugin.Themerr.Tests/BootstrapJellyfinServer.cs rename to Jellyfin.Plugin.Themerr.Tests/FixtureJellyfinServer.cs index c843718..8e19264 100644 --- a/Jellyfin.Plugin.Themerr.Tests/BootstrapJellyfinServer.cs +++ b/Jellyfin.Plugin.Themerr.Tests/FixtureJellyfinServer.cs @@ -3,22 +3,15 @@ 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 +/// This class is used as a fixture for the Jellyfin server with mock movies /// -public class BootstrapJellyfinServer +public class FixtureJellyfinServer { /// /// Mock movies to use for testing /// - /// + /// List containing mock objects. public static List MockMovies() { return new List @@ -66,7 +59,6 @@ public static List MockMovies() }; } - /// /// Create mock movies from stub video /// @@ -75,14 +67,13 @@ public static List MockMovies() 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" - ); - + "video_stub.mp4"); + Assert.True(File.Exists(stubVideoPath), "Could not find ./data/video_stub.mp4"); foreach (var movie in mockMovies) @@ -90,25 +81,23 @@ private void CreateMockMovies() // 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})" - ); - + $"{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" - ); - + $"{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/TestThemerrManager.cs b/Jellyfin.Plugin.Themerr.Tests/TestThemerrManager.cs index 959991e..477b79f 100644 --- a/Jellyfin.Plugin.Themerr.Tests/TestThemerrManager.cs +++ b/Jellyfin.Plugin.Themerr.Tests/TestThemerrManager.cs @@ -1,26 +1,34 @@ using MediaBrowser.Controller.Library; -using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider; using Microsoft.Extensions.Logging; using Moq; using Newtonsoft.Json; +using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider; + namespace Jellyfin.Plugin.Themerr.Tests; -[Collection("Bootstrapped Collection")] +/// +/// This class is responsible for testing . +/// +[Collection("Fixture Collection")] public class TestThemerrManager { private readonly ThemerrManager _themerrManager; + /// + /// Initializes a new instance of the class. + /// + /// An instance. public TestThemerrManager(ITestOutputHelper output) { 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 @@ -31,59 +39,56 @@ private static List FixtureYoutubeUrls() "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()) + + foreach (var movie in FixtureJellyfinServer.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() + private void TestSaveMp3() { // set destination with themerr_jellyfin_tests as the folder name var destinationFile = Path.Combine( - // "themerr_jellyfin_tests", - "theme.mp3" - ); - - + "theme.mp3"); + foreach (var videoUrl in FixtureYoutubeUrls()) { // log TestLogger.Info($"Attempting to download {videoUrl}"); - + // run and wait 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), $"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 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 { @@ -92,34 +97,34 @@ public void TestSaveMp3() {"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 + {"FF-F2", 0}, // MPEG-1 Layer 3 }; - + // log beginning of fileBytesHex TestLogger.Debug($"Beginning of fileBytesHex: {fileBytesHex.Substring(0, 40)}"); - + // check if the file is an actual mp3 var isMp3 = false; - + // loop through validMp3Signatures foreach (var (signature, offset) in validMp3Signatures) { // log TestLogger.Debug($"Checking for {signature} at offset of {offset} bytes"); - + // remove the offset bytes var fileBytesHexWithoutOffset = fileBytesHex.Substring(offset * 3); - + // check if the beginning of the fileBytesHexWithoutOffset matches the signature var isSignature = fileBytesHexWithoutOffset.StartsWith(signature); if (isSignature) { // log TestLogger.Info($"Found {signature} at offset {offset}"); - + // set isMp3 to true isMp3 = true; - + // break out of loop break; } @@ -127,30 +132,29 @@ public void TestSaveMp3() // log TestLogger.Debug($"Did not find {signature} at offset {offset}"); } + Assert.True(isMp3, $"File {destinationFile} is not an mp3"); - + // delete file File.Delete(destinationFile); } } - + [Fact] [Trait("Category", "Unit")] - public void TestSaveMp3InvalidUrl() + private void TestSaveMp3InvalidUrl() { // set destination with themerr_jellyfin_tests as the folder name var destinationFile = Path.Combine( - // "themerr_jellyfin_tests", - "theme.mp3" - ); - + "theme.mp3"); + // set invalid url var invalidUrl = "https://www.youtube.com/watch?v=invalid"; - + // run and wait var themeExists = _themerrManager.SaveMp3(destinationFile, invalidUrl); Assert.False(themeExists, $"SaveMp3 did not return False for {invalidUrl}"); - + // check if file exists Assert.False(File.Exists(destinationFile), $"File {destinationFile} exists"); } @@ -158,70 +162,68 @@ public void TestSaveMp3InvalidUrl() // todo: fix this test // [Fact] // [Trait("Category", "Unit")] - // public void TestProcessMovieTheme() + // private void TestProcessMovieTheme() // { - // // get bootstrapped movies - // var mockMovies = BootstrapJellyfinServer.MockMovies(); - // + // // 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")] - public void TestShouldSkipDownload() + private void TestShouldSkipDownload() { var themePath = Path.Combine( - "theme.mp3" - ); + "theme.mp3"); var themerrDataPath = Path.Combine( - "themerr_data.json" - ); - + "themerr_data.json"); + var shouldSkipDownload = _themerrManager.ShouldSkipDownload(themePath, themerrDataPath); Assert.False(shouldSkipDownload, "ShouldSkipDownload returned True"); } [Fact] [Trait("Category", "Unit")] - public void TestGetThemePath() + private void TestGetThemePath() { - // get bootstrapped movies - var mockMovies = BootstrapJellyfinServer.MockMovies(); - + // 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 var themePath = _themerrManager.GetThemePath(movie); - + // ensure path ends with theme.mp3 Assert.EndsWith("theme.mp3", themePath); } } - + [Fact] [Trait("Category", "Unit")] - public void TestGetThemerrDataPath() + private void TestGetThemerrDataPath() { - // get bootstrapped movies - var mockMovies = BootstrapJellyfinServer.MockMovies(); - + // 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 var themerrDataPath = _themerrManager.GetThemerrDataPath(movie); - + // ensure path ends with theme.mp3 Assert.EndsWith("themerr.json", themerrDataPath); } @@ -229,18 +231,18 @@ public void TestGetThemerrDataPath() [Fact] [Trait("Category", "Unit")] - public void TestCreateThemerrDbLink() + private void TestCreateThemerrDbLink() { - // get bootstrapped movies - var mockMovies = BootstrapJellyfinServer.MockMovies(); - + // get fixture movies + var mockMovies = FixtureJellyfinServer.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 themerrDbUrl = _themerrManager.CreateThemerrDbLink(tmdbId); - + TestLogger.Info($"themerrDbLink: {themerrDbUrl}"); Assert.EndsWith($"themoviedb/{tmdbId}.json", themerrDbUrl); @@ -249,9 +251,8 @@ public void TestCreateThemerrDbLink() [Fact] [Trait("Category", "Unit")] - public void TestGetYoutubeThemeUrl() + private void TestGetYoutubeThemeUrl() { - // loop over each themerrDbLink foreach (var themerrDbLink in FixtureThemerrDbUrls()) { @@ -267,29 +268,28 @@ public void TestGetYoutubeThemeUrl() [Fact] [Trait("Category", "Unit")] - public void TestSaveThemerrData() + private 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, + Assert.True( + youtubeThemeUrl == savedYoutubeThemeUrl, $"youtubeThemeUrl {youtubeThemeUrl} does not match savedYoutubeThemeUrl {savedYoutubeThemeUrl}"); } } diff --git a/Jellyfin.Plugin.Themerr/Api/ThemerrController.cs b/Jellyfin.Plugin.Themerr/Api/ThemerrController.cs index 228d24f..1ab838e 100644 --- a/Jellyfin.Plugin.Themerr/Api/ThemerrController.cs +++ b/Jellyfin.Plugin.Themerr/Api/ThemerrController.cs @@ -15,16 +15,17 @@ namespace Jellyfin.Plugin.Themerr.Api [Authorize(Policy = "DefaultAuthorization")] [Route("Themerr")] [Produces(MediaTypeNames.Application.Json)] - public class ThemerrController : ControllerBase { private readonly ThemerrManager _themerrManager; private readonly ILogger _logger; - /// - /// Initializes a new instance of . + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. public ThemerrController( ILibraryManager libraryManager, ILogger logger) @@ -33,11 +34,10 @@ public ThemerrController( _logger = logger; } - /// /// Downloads all Movie theme songs. /// - /// Theme song download started successfully. + /// Theme song download started successfully. /// A indicating success. [HttpPost("DownloadMovies")] [ProducesResponseType(StatusCodes.Status204NoContent)] diff --git a/Jellyfin.Plugin.Themerr/Configuration/PluginConfiguration.cs b/Jellyfin.Plugin.Themerr/Configuration/PluginConfiguration.cs index b992d23..027aaeb 100644 --- a/Jellyfin.Plugin.Themerr/Configuration/PluginConfiguration.cs +++ b/Jellyfin.Plugin.Themerr/Configuration/PluginConfiguration.cs @@ -2,6 +2,9 @@ namespace Jellyfin.Plugin.Themerr.Configuration { + /// + /// Initializes a new instance of the class. + /// public class PluginConfiguration : BasePluginConfiguration { } diff --git a/Jellyfin.Plugin.Themerr/Plugin.cs b/Jellyfin.Plugin.Themerr/Plugin.cs index 737153f..9314d41 100644 --- a/Jellyfin.Plugin.Themerr/Plugin.cs +++ b/Jellyfin.Plugin.Themerr/Plugin.cs @@ -8,23 +8,48 @@ namespace Jellyfin.Plugin.Themerr { - public class Plugin : BasePlugin, IHasWebPages + /// + /// The Themerr plugin class. + /// + public class Plugin : BasePlugin, IHasWebPages { + private readonly Guid _id = new Guid("84b59a39-bde4-42f4-adbd-c39882cbb772"); + + /// + /// Initializes a new instance of the class. + /// + /// The application paths. + /// The xml serializer. public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer) { Instance = this; } - public override string Name => "Themerr"; - + /// + /// Gets the plugin instance. + /// public static Plugin Instance { get; private set; } + /// + /// Gets the name of the plugin. + /// + public override string Name => "Themerr"; + + /// + /// Gets the description of the plugin. + /// public override string Description => "Downloads Theme Songs"; - private readonly Guid _id = new Guid("84b59a39-bde4-42f4-adbd-c39882cbb772"); + /// + /// Gets the plugin instance id. + /// public override Guid Id => _id; + /// + /// Get the plugin's html pages. + /// + /// A list of . public IEnumerable GetPages() { return new[] diff --git a/Jellyfin.Plugin.Themerr/ScheduledTasks/ThemerrTasks.cs b/Jellyfin.Plugin.Themerr/ScheduledTasks/ThemerrTasks.cs index 9c95ec0..aafac2c 100644 --- a/Jellyfin.Plugin.Themerr/ScheduledTasks/ThemerrTasks.cs +++ b/Jellyfin.Plugin.Themerr/ScheduledTasks/ThemerrTasks.cs @@ -8,17 +8,51 @@ namespace Jellyfin.Plugin.Themerr.ScheduledTasks { - public class DownloadThemerrTask : IScheduledTask + /// + /// The Themerr scheduled task. + /// + public class ThemerrTasks : IScheduledTask { private readonly ILogger _logger; private readonly ThemerrManager _themerrManager; - public DownloadThemerrTask(ILibraryManager libraryManager, ILogger logger) + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. + public ThemerrTasks(ILibraryManager libraryManager, ILogger logger) { _logger = logger; _themerrManager = new ThemerrManager(libraryManager, logger); } + /// + /// Gets the name of the task. + /// + public string Name => "Download Theme Songs"; + + /// + /// Gets the key of the task. + /// + public string Key => "Download ThemeSongs"; + + /// + /// Gets the description of the task. + /// + public string Description => "Scans all libraries to download Movie Theme Songs"; + + /// + /// Gets the category of the task. + /// + public string Category => "Themerr"; + + /// + /// Execute the task, asynchronously. + /// + /// The progress reporter. + /// The cancellation token. + /// A representing the asynchronous operation. public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { _logger.LogInformation("Starting plugin, Downloading Movie Theme Songs..."); @@ -26,19 +60,18 @@ public async Task ExecuteAsync(IProgress progress, CancellationToken can _logger.LogInformation("All theme songs downloaded"); } + /// + /// Gets the default triggers. + /// + /// A list of . public IEnumerable GetDefaultTriggers() { // Run this task every 24 hours yield return new TaskTriggerInfo { - Type = TaskTriggerInfo.TriggerInterval, + Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }; } - - public string Name => "Download Theme Songs"; - public string Key => "Download ThemeSongs"; - public string Description => "Scans all libraries to download Movie Theme Songs"; - public string Category => "Themerr"; } } diff --git a/Jellyfin.Plugin.Themerr/ThemerrManager.cs b/Jellyfin.Plugin.Themerr/ThemerrManager.cs index 8744d3a..78cabc6 100644 --- a/Jellyfin.Plugin.Themerr/ThemerrManager.cs +++ b/Jellyfin.Plugin.Themerr/ThemerrManager.cs @@ -4,21 +4,21 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; +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; using Microsoft.Extensions.Logging; -using Jellyfin.Data.Enums; using Newtonsoft.Json; using YoutubeExplode; using YoutubeExplode.Videos.Streams; +// TODO: Add support for TV shows +// using MediaBrowser.Controller.Entities.TV; namespace Jellyfin.Plugin.Themerr - { /// /// The main entry point for the plugin. @@ -30,22 +30,40 @@ public class ThemerrManager : IServerEntryPoint private readonly ILogger _logger; /// - /// Constructor + /// Initializes a new instance of the class. /// - /// - /// + /// The library manager. + /// The logger. public ThemerrManager(ILibraryManager libraryManager, ILogger logger) { _libraryManager = libraryManager; _logger = logger; _timer = new Timer(_ => OnTimerElapsed(), null, Timeout.Infinite, Timeout.Infinite); } - + + /// + /// Get the existing youtube theme url from the themerr data file if it exists. + /// + /// The path to the themerr data file. + /// The existing YouTube theme url if it exists, empty string otherwise. + 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; + } + /// /// Save a mp3 file from a youtube video url. /// - /// - /// + /// The destination path. + /// The YouTube video url. + /// True if the file was saved successfully, false otherwise. public bool SaveMp3(string destination, string videoUrl) { try @@ -73,11 +91,11 @@ public bool SaveMp3(string destination, string videoUrl) return WaitForFile(destination, 30000); } - + /// /// Get all movies from the library that have a tmdb id. /// - /// + /// List of . public IEnumerable GetMoviesFromLibrary() { return _libraryManager.GetItemList(new InternalItemsQuery @@ -88,11 +106,11 @@ public IEnumerable GetMoviesFromLibrary() HasTmdbId = true }).Select(m => m as Movie); } - + /// /// Enumerate through all movies in the library and downloads their theme songs as required. /// - /// + /// A representing the asynchronous operation. public Task DownloadAllThemerr() { var movies = GetMoviesFromLibrary(); @@ -107,7 +125,7 @@ public Task DownloadAllThemerr() /// /// Download the theme song for a movie if it doesn't already exist. /// - /// + /// The Jellyfin movie object. public void ProcessMovieTheme(Movie movie) { var movieTitle = movie.Name; @@ -123,6 +141,7 @@ public void ProcessMovieTheme(Movie movie) // get tmdb id var tmdbId = movie.GetProviderId(MetadataProvider.Tmdb); + // create themerrdb url var themerrDbLink = CreateThemerrDbLink(tmdbId); @@ -132,7 +151,7 @@ public void ProcessMovieTheme(Movie movie) { return; } - + SaveMp3(themePath, youtubeThemeUrl); SaveThemerrData(themerrDataPath, youtubeThemeUrl); movie.RefreshMetadata(CancellationToken.None); @@ -143,9 +162,9 @@ public void ProcessMovieTheme(Movie movie) /// /// If theme.mp3 exists and themerr.json doesn't exist, then skip to avoid overwriting user supplied themes. /// - /// - /// - /// + /// 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) { return System.IO.File.Exists(themePath) && !System.IO.File.Exists(themerrDataPath); @@ -154,8 +173,8 @@ public bool ShouldSkipDownload(string themePath, string themerrDataPath) /// /// Get the path to the theme song. /// - /// - /// + /// The Jellyfin movie object. + /// The path to the theme song. public string GetThemePath(Movie movie) { return $"{movie.ContainingFolderPath}/theme.mp3"; @@ -164,33 +183,18 @@ public string GetThemePath(Movie movie) /// /// Get the path to the themerr data file. /// - /// - /// + /// The Jellyfin movie object. + /// The path to the themerr data file. public 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. /// - /// - /// + /// The tmdb id. + /// The themerr database link. public string CreateThemerrDbLink(string tmdbId) { return $"https://app.lizardbyte.dev/ThemerrDB/movies/themoviedb/{tmdbId}.json"; @@ -199,9 +203,9 @@ public string CreateThemerrDbLink(string tmdbId) /// /// Get the YouTube theme url from the themerr database. /// - /// - /// - /// + /// The themerr database url. + /// The movie title. + /// The YouTube theme url. public string GetYoutubeThemeUrl(string themerrDbUrl, string movieTitle) { var client = new HttpClient(); @@ -222,9 +226,9 @@ public string GetYoutubeThemeUrl(string themerrDbUrl, string movieTitle) /// /// Save the themerr data file. /// - /// - /// - /// + /// 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) { var success = false; @@ -245,13 +249,13 @@ public bool SaveThemerrData(string themerrDataPath, string youtubeThemeUrl) return success && WaitForFile(themerrDataPath, 10000); } - + /// - /// Wait for file to exist on disk. + /// Wait for file to exist on disk and is not locked by another process. /// - /// - /// - /// + /// The file path to check. + /// The maximum amount of time (in milliseconds) to wait. + /// True if the file exists and is not locked, false otherwise. public bool WaitForFile(string filePath, int timeout) { var startTime = DateTime.UtcNow; @@ -261,17 +265,22 @@ public bool WaitForFile(string filePath, int timeout) { return false; } + Thread.Sleep(100); } - - // wait until file is not being used by another process - var fileIsLocked = true; - while (fileIsLocked) + + // Wait until the file is not being used by another process + while (true) { try { - using (System.IO.File.Open(filePath, System.IO.FileMode.Open)) { } - fileIsLocked = false; + // Attempt to open and close the file to check for locks + using (var stream = System.IO.File.Open(filePath, System.IO.FileMode.Open)) + { + stream.Close(); + } + + return true; } catch (System.IO.IOException) { @@ -279,36 +288,36 @@ public bool WaitForFile(string filePath, int timeout) { return false; } + Thread.Sleep(100); } } - - return true; } - + /// - /// Called when the plugin is loaded. + /// Run the task, asynchronously. /// - private void OnTimerElapsed() + /// A representing the asynchronous operation. + public Task RunAsync() { - // Stop the timer until next update - _timer.Change(Timeout.Infinite, Timeout.Infinite); + return Task.CompletedTask; } - + /// - /// Todo + /// Cleanup. /// - /// - public Task RunAsync() + public void Dispose() { - return Task.CompletedTask; + // Cleanup } - + /// - /// Todo + /// Called when the plugin is loaded. /// - public void Dispose() + private void OnTimerElapsed() { + // Stop the timer until next update + _timer.Change(Timeout.Infinite, Timeout.Infinite); } } } diff --git a/docs/source/contributing/testing.rst b/docs/source/contributing/testing.rst index 52c3687..be00be6 100644 --- a/docs/source/contributing/testing.rst +++ b/docs/source/contributing/testing.rst @@ -1,17 +1,19 @@ Testing ======= -flake8 ------- -Themerr-jellyfin uses `flake8 `__ for enforcing consistent code styling. flake8 is -included in the ``requirements-dev.txt``. +SonarAnalyzer.CSharp +-------------------- +Themerr-jellyfin uses `SonarAnalyzers.CSharp `__ to spot Bugs, +Vulnerabilities, and Code Smells in the project. This is run automatically as part of the build process. -The config file for flake8 is ``.flake8``. This is already included in the root of the repo and should not be modified. +The config file for SonarAnalyzers.CSharp is ``.editorconfig``. -Test with flake8 - .. code-block:: bash +StyleCop.Analyzers +------------------ +Themerr-jellyfin uses `StyleCop.Analyzers `__ to enforce consistent +code styling. This is run automatically as part of the build process. - python -m flake8 +The config file for StyleCop.Analyzers is ``.editorconfig``. Sphinx ------