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
------