diff --git a/Emby.AutoOrganize/Api/FileOrganizationService.cs b/Emby.AutoOrganize/Api/FileOrganizationService.cs index 9897c69..38adc47 100644 --- a/Emby.AutoOrganize/Api/FileOrganizationService.cs +++ b/Emby.AutoOrganize/Api/FileOrganizationService.cs @@ -14,17 +14,16 @@ namespace Emby.AutoOrganize.Api public class GetFileOrganizationActivity : IReturn> { /// - /// Skips over a given number of items within the results. Use for paging. + /// Gets or sets a value indicating the number of items to skip over in the query. Use to specify a page + /// number. /// - /// The start index. [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? StartIndex { get; set; } /// - /// The maximum number of items to return + /// Gets or sets the maximum number of items to return. Use to specify a page size. /// - /// The limit. - [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? Limit { get; set; } } @@ -119,19 +118,18 @@ public class OrganizeMovie } [Route("/Library/FileOrganizations/SmartMatches", "GET", Summary = "Gets smart match entries")] - public class GetSmartMatchInfos : IReturn> + public class GetSmartMatchInfos : IReturn> { /// - /// Skips over a given number of items within the results. Use for paging. + /// Gets or sets a value indicating the number of items to skips over in the query. Use to specify a page + /// number. /// - /// The start index. [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? StartIndex { get; set; } /// - /// The maximum number of items to return + /// Gets or sets the maximum number of items to return. Use to specify a page size. /// - /// The limit. [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? Limit { get; set; } } @@ -148,13 +146,17 @@ public class FileOrganizationService : IService, IRequiresRequest { private readonly IHttpResultFactory _resultFactory; - public IRequest Request { get; set; } - + /// + /// Initializes a new instance of the class. + /// + /// HTTP result factory to use for making requests. public FileOrganizationService(IHttpResultFactory resultFactory) { _resultFactory = resultFactory; } + public IRequest Request { get; set; } + private IFileOrganizationService InternalFileOrganizationService { get { return PluginEntryPoint.Current.FileOrganizationService; } @@ -194,7 +196,6 @@ public void Delete(ClearOrganizationCompletedLog request) Task.WaitAll(task); } - public void Post(PerformOrganization request) { // Don't await this diff --git a/Emby.AutoOrganize/Plugin.cs b/Emby.AutoOrganize/AutoOrganizePlugin.cs similarity index 78% rename from Emby.AutoOrganize/Plugin.cs rename to Emby.AutoOrganize/AutoOrganizePlugin.cs index a16ac7b..021ca1e 100644 --- a/Emby.AutoOrganize/Plugin.cs +++ b/Emby.AutoOrganize/AutoOrganizePlugin.cs @@ -5,32 +5,34 @@ using MediaBrowser.Common.Plugins; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; -using System.IO; -using MediaBrowser.Model.Drawing; namespace Emby.AutoOrganize { - public class Plugin : BasePlugin, IHasWebPages + /// + /// The auto-organize plugin. + /// + public class AutoOrganizePlugin : BasePlugin, IHasWebPages { - public Plugin(IApplicationPaths appPaths, IXmlSerializer xmlSerializer) + /// + /// Initializes a new instance of the class. + /// + /// The application paths. + /// The XML serializer. + public AutoOrganizePlugin(IApplicationPaths appPaths, IXmlSerializer xmlSerializer) : base(appPaths, xmlSerializer) { } - public override string Name => "Auto Organize"; - - - public override string Description - => "Automatically organize new media"; + /// + public override Guid Id => new Guid("70b7b43b-471b-4159-b4be-56750c795499"); - public PluginConfiguration PluginConfiguration => Configuration; + /// + public override string Name => "Auto Organize"; - private Guid _id = new Guid("70b7b43b-471b-4159-b4be-56750c795499"); - public override Guid Id - { - get { return _id; } - } + /// + public override string Description => "Automatically organize new media"; + /// public IEnumerable GetPages() { return new[] diff --git a/Emby.AutoOrganize/Configuration/PluginConfiguration.cs b/Emby.AutoOrganize/Configuration/PluginConfiguration.cs index 5e44d0a..544a7c5 100644 --- a/Emby.AutoOrganize/Configuration/PluginConfiguration.cs +++ b/Emby.AutoOrganize/Configuration/PluginConfiguration.cs @@ -2,6 +2,9 @@ namespace Emby.AutoOrganize.Configuration { + /// + /// Configuration for . + /// public class PluginConfiguration : BasePluginConfiguration { } diff --git a/Emby.AutoOrganize/Core/EpisodeFileOrganizer.cs b/Emby.AutoOrganize/Core/EpisodeFileOrganizer.cs index b91ca6b..ddc7312 100644 --- a/Emby.AutoOrganize/Core/EpisodeFileOrganizer.cs +++ b/Emby.AutoOrganize/Core/EpisodeFileOrganizer.cs @@ -51,6 +51,7 @@ public EpisodeFileOrganizer( } private NamingOptions _namingOptions; + private NamingOptions GetNamingOptionsInternal() { if (_namingOptions == null) @@ -132,13 +133,11 @@ public async Task OrganizeEpisodeFile( { if (episodeInfo.IsByDate) { - _logger.LogDebug("Extracted information from {0}. Series name {1}, Date {2}", - path, seriesName, premiereDate.Value); + _logger.LogDebug("Extracted information from {0}. Series name {1}, Date {2}", path, seriesName, premiereDate.Value); } else { - _logger.LogDebug("Extracted information from {0}. Series name {1}, Season {2}, Episode {3}", - path, seriesName, seasonNumber, episodeNumber); + _logger.LogDebug("Extracted information from {0}. Series name {1}, Season {2}, Episode {3}", path, seriesName, seasonNumber, episodeNumber); } // We detected an airdate or (an season number and an episode number) @@ -150,7 +149,8 @@ public async Task OrganizeEpisodeFile( result.ExtractedEndingEpisodeNumber = endingEpisodeNumber; - await OrganizeEpisode(path, + await OrganizeEpisode( + path, seriesName, seriesYear, seasonNumber, @@ -188,7 +188,6 @@ await OrganizeEpisode(path, // Don't keep saving the same result over and over if nothing has changed return previousResult; } - } catch (OrganizationException ex) { @@ -217,21 +216,14 @@ private async Task AutoDetectSeries( { RemoteSearchResult finalResult = null; - #region Search One - - var seriesInfo = new SeriesInfo - { - Name = seriesName, - Year = seriesYear - }; - + // Perform remote search + var seriesInfo = new SeriesInfo { Name = seriesName, Year = seriesYear }; var searchQuery = new RemoteSearchQuery { SearchInfo = seriesInfo }; var searchResults = await _providerManager.GetRemoteSearchResults(searchQuery, cancellationToken).ConfigureAwait(false); - #endregion - // Group series by name and year (if 2 series with the exact same name, the same year ...) - var groupedResult = searchResults.GroupBy(p => new { p.Name, p.ProductionYear }, + var groupedResult = searchResults.GroupBy( + p => new { p.Name, p.ProductionYear }, p => p, (key, g) => new { Key = key, Result = g.ToList() }).ToList(); @@ -276,9 +268,8 @@ private async Task CreateNewSeries( { Series series; - // Ensure that we don't create the same series multiple time - // We create series one at a time - var seriesCreationLock = new Object(); + // Ensure that we don't create the same series multiple times; create one at a time + var seriesCreationLock = new object(); lock (seriesCreationLock) { series = GetMatchingSeries(request.NewSeriesName, request.NewSeriesYear, request.TargetFolder, null); @@ -337,10 +328,11 @@ public async Task OrganizeWithCorrection( series = (Series)_libraryManager.GetItemById(request.SeriesId); } - // We manually set the media as Series + // We manually set the media as Series result.Type = CurrentFileOrganizerType; - await OrganizeEpisode(result.OriginalPath, + await OrganizeEpisode( + result.OriginalPath, series, request.SeasonNumber, request.EpisodeNumber, @@ -362,7 +354,8 @@ await OrganizeEpisode(result.OriginalPath, return result; } - private Task OrganizeEpisode(string sourcePath, + private Task OrganizeEpisode( + string sourcePath, string seriesName, int? seriesYear, int? seasonNumber, @@ -374,7 +367,7 @@ private Task OrganizeEpisode(string sourcePath, FileOrganizationResult result, CancellationToken cancellationToken) { - var series = GetMatchingSeries(seriesName, seriesYear, "", result); + var series = GetMatchingSeries(seriesName, seriesYear, string.Empty, result); if (series == null) { @@ -390,7 +383,8 @@ private Task OrganizeEpisode(string sourcePath, } } - return OrganizeEpisode(sourcePath, + return OrganizeEpisode( + sourcePath, series, seasonNumber, episodeNumber, @@ -403,20 +397,10 @@ private Task OrganizeEpisode(string sourcePath, } /// - /// Organize part responsible of Season AND Episode recognition + /// Organize part responsible of Season AND Episode recognition. /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - private async Task OrganizeEpisode(string sourcePath, + private async Task OrganizeEpisode( + string sourcePath, Series series, int? seasonNumber, int? episodeNumber, @@ -639,8 +623,7 @@ private void DeleteLibraryFile(string path, bool renameRelatedFiles, string targ directory = Path.GetDirectoryName(file); var filename = Path.GetFileName(file); - filename = filename.Replace(originalFilenameWithoutExtension, targetFilenameWithoutExtension, - StringComparison.OrdinalIgnoreCase); + filename = filename.Replace(originalFilenameWithoutExtension, targetFilenameWithoutExtension, StringComparison.OrdinalIgnoreCase); var destination = Path.Combine(directory, filename); @@ -649,7 +632,8 @@ private void DeleteLibraryFile(string path, bool renameRelatedFiles, string targ } } - private List GetOtherDuplicatePaths(string targetPath, + private List GetOtherDuplicatePaths( + string targetPath, Series series, Episode episode) { @@ -673,7 +657,6 @@ private List GetOtherDuplicatePaths(string targetPath, i.IndexNumber.HasValue && i.IndexNumber.Value == episode.IndexNumber) { - if (episode.IndexNumberEnd.HasValue || i.IndexNumberEnd.HasValue) { return episode.IndexNumberEnd.HasValue && i.IndexNumberEnd.HasValue && @@ -764,7 +747,8 @@ private void PerformFileSorting(TvFileOrganizationOptions options, FileOrganizat } } - private async Task GetMatchingEpisode(Series series, + private async Task GetMatchingEpisode( + Series series, int? seasonNumber, int? episodeNumber, int? endingEpiosdeNumber, @@ -817,10 +801,10 @@ private Season GetMatchingSeason(Series series, Episode episode, TvFileOrganizat } } + // If the season path is missing, compute it and create the directory on the filesystem if (string.IsNullOrEmpty(season.Path)) { season.Path = GetSeasonFolderPath(series, episode.ParentIndexNumber.Value, options); - // Create the folder Directory.CreateDirectory(season.Path); } @@ -860,7 +844,6 @@ private Series GetMatchingSeries(string seriesName, int? seriesYear, string targ Recursive = true, Name = info.ItemName, DtoOptions = new DtoOptions(true) - }).Cast().FirstOrDefault(s => s.Path.StartsWith(targetFolder, StringComparison.Ordinal)); } } @@ -869,11 +852,8 @@ private Series GetMatchingSeries(string seriesName, int? seriesYear, string targ } /// - /// Get the new series name + /// Get the new series name. /// - /// - /// - /// private string GetSeriesDirectoryName(Series series, TvFileOrganizationOptions options) { var seriesName = series.Name; @@ -895,15 +875,16 @@ private string GetSeriesDirectoryName(Series series, TvFileOrganizationOptions o } /// - /// CreateNewEpisode + /// Look up metadata for an episode and use it to create an object. /// - /// The series. - /// The season number. + /// The series the episode is in. + /// The season number the episode is in. /// The episode number. /// The ending episode number. /// The premiere date. /// The cancellation token. - /// System.String. + /// A task representing the creation of the object. + /// If no metadata can be found for the specified episode parameters. private async Task CreateNewEpisode( Series series, int? seasonNumber, @@ -923,11 +904,9 @@ private async Task CreateNewEpisode( PremiereDate = premiereDate }; - var searchResults = await _providerManager.GetRemoteSearchResults(new RemoteSearchQuery - { - SearchInfo = episodeInfo - - }, cancellationToken).ConfigureAwait(false); + var searchResults = await _providerManager.GetRemoteSearchResults( + new RemoteSearchQuery { SearchInfo = episodeInfo }, + cancellationToken).ConfigureAwait(false); var episodeSearch = searchResults.FirstOrDefault(); @@ -994,6 +973,7 @@ private bool ContainsEpisodesWithoutSeasonFolders(Series series) return true; } } + return false; } diff --git a/Emby.AutoOrganize/Core/Extensions.cs b/Emby.AutoOrganize/Core/Extensions.cs index aaeb968..e12f27a 100644 --- a/Emby.AutoOrganize/Core/Extensions.cs +++ b/Emby.AutoOrganize/Core/Extensions.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using Emby.AutoOrganize.Model; @@ -6,11 +8,21 @@ namespace Emby.AutoOrganize.Core { + /// + /// Static class containing extension methods helpful for working with configuration for this plugin. + /// public static class ConfigurationExtension { + /// + /// The key to use with for storing the configuration of this plugin. + /// public const string AutoOrganizeOptionsKey = "autoorganize"; - public static void Convert(this IConfigurationManager manager, IFileOrganizationService service) + /// + /// Perform a one-time migration of smart match info from the plugin configuration to the SQLite database. + /// + [SuppressMessage("Compiler", "CS0618:Type or member is obsolete", Justification = "This method is used to migrates configuration away from the obsolete property.")] + public static void ConvertSmartMatchInfo(this IConfigurationManager manager, IFileOrganizationService service) { var options = manager.GetConfiguration(AutoOrganizeOptionsKey); if (!options.Converted) @@ -33,19 +45,34 @@ public static void Convert(this IConfigurationManager manager, IFileOrganization } } + /// + /// Get the stored in the configuration manager using the + /// . + /// + /// The manager to retrieve the options from. + /// The retrieved options. public static AutoOrganizeOptions GetAutoOrganizeOptions(this IConfigurationManager manager) { return manager.GetConfiguration(AutoOrganizeOptionsKey); } + /// + /// Save into the configuration manager. + /// + /// The configuration manager to store the options into. + /// The options to store. public static void SaveAutoOrganizeOptions(this IConfigurationManager manager, AutoOrganizeOptions options) { manager.SaveConfiguration(AutoOrganizeOptionsKey, options); } } + /// + /// A configuration factory that registers the configuration entry required for the . + /// public class AutoOrganizeOptionsFactory : IConfigurationFactory { + /// public IEnumerable GetConfigurations() { return new List @@ -53,7 +80,7 @@ public IEnumerable GetConfigurations() new ConfigurationStore { Key = ConfigurationExtension.AutoOrganizeOptionsKey, - ConfigurationType = typeof (AutoOrganizeOptions) + ConfigurationType = typeof(AutoOrganizeOptions) } }; } diff --git a/Emby.AutoOrganize/Core/FileOrganizationService.cs b/Emby.AutoOrganize/Core/FileOrganizationService.cs index 8be21b1..a17d459 100644 --- a/Emby.AutoOrganize/Core/FileOrganizationService.cs +++ b/Emby.AutoOrganize/Core/FileOrganizationService.cs @@ -30,11 +30,9 @@ public class FileOrganizationService : IFileOrganizationService private readonly IProviderManager _providerManager; private readonly ConcurrentDictionary _inProgressItemIds = new ConcurrentDictionary(); - public event EventHandler> ItemAdded; - public event EventHandler> ItemUpdated; - public event EventHandler> ItemRemoved; - public event EventHandler LogReset; - + /// + /// Initializes a new instance of the class. + /// public FileOrganizationService( ITaskManager taskManager, IFileOrganizationRepository repo, @@ -55,11 +53,25 @@ public FileOrganizationService( _providerManager = providerManager; } + /// + public event EventHandler> ItemAdded; + + /// + public event EventHandler> ItemUpdated; + + /// + public event EventHandler> ItemRemoved; + + /// + public event EventHandler LogReset; + + /// public void BeginProcessNewFiles() { _taskManager.CancelIfRunningAndQueue(); } + /// public void SaveResult(FileOrganizationResult result, CancellationToken cancellationToken) { if (result == null || string.IsNullOrEmpty(result.OriginalPath)) @@ -72,6 +84,7 @@ public void SaveResult(FileOrganizationResult result, CancellationToken cancella _repo.SaveResult(result, cancellationToken); } + /// public void SaveResult(SmartMatchResult result, CancellationToken cancellationToken) { if (result == null) @@ -82,6 +95,7 @@ public void SaveResult(SmartMatchResult result, CancellationToken cancellationTo _repo.SaveResult(result, cancellationToken); } + /// public QueryResult GetResults(FileOrganizationResultQuery query) { var results = _repo.GetResults(query); @@ -94,6 +108,7 @@ public QueryResult GetResults(FileOrganizationResultQuer return results; } + /// public FileOrganizationResult GetResult(string id) { var result = _repo.GetResult(id); @@ -106,6 +121,7 @@ public FileOrganizationResult GetResult(string id) return result; } + /// public FileOrganizationResult GetResultBySourcePath(string path) { if (string.IsNullOrEmpty(path)) @@ -118,6 +134,7 @@ public FileOrganizationResult GetResultBySourcePath(string path) return GetResult(id); } + /// public async Task DeleteOriginalFile(string resultId) { var result = _repo.GetResult(resultId); @@ -147,16 +164,12 @@ public async Task DeleteOriginalFile(string resultId) ItemRemoved?.Invoke(this, new GenericEventArgs(result)); } - private AutoOrganizeOptions GetAutoOrganizeOptions() - { - return _config.GetAutoOrganizeOptions(); - } - + /// public async Task PerformOrganization(string resultId) { var result = _repo.GetResult(resultId); - var options = GetAutoOrganizeOptions(); + var options = _config.GetAutoOrganizeOptions(); if (string.IsNullOrEmpty(result.TargetPath)) { @@ -186,23 +199,26 @@ public async Task PerformOrganization(string resultId) } } + /// public async Task ClearLog() { await _repo.DeleteAll().ConfigureAwait(false); LogReset?.Invoke(this, EventArgs.Empty); } + /// public async Task ClearCompleted() { await _repo.DeleteCompleted().ConfigureAwait(false); LogReset?.Invoke(this, EventArgs.Empty); } + /// public async Task PerformOrganization(EpisodeFileOrganizationRequest request) { var organizer = new EpisodeFileOrganizer(this, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager); - var options = GetAutoOrganizeOptions(); + var options = _config.GetAutoOrganizeOptions(); var result = await organizer.OrganizeWithCorrection(request, options.TvOptions, CancellationToken.None).ConfigureAwait(false); if (result.Status != FileSortingStatus.Success) @@ -211,11 +227,12 @@ public async Task PerformOrganization(EpisodeFileOrganizationRequest request) } } + /// public async Task PerformOrganization(MovieFileOrganizationRequest request) { var organizer = new MovieFileOrganizer(this, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager); - var options = GetAutoOrganizeOptions(); + var options = _config.GetAutoOrganizeOptions(); var result = await organizer.OrganizeWithCorrection(request, options.MovieOptions, CancellationToken.None).ConfigureAwait(false); if (result.Status != FileSortingStatus.Success) @@ -224,17 +241,19 @@ public async Task PerformOrganization(MovieFileOrganizationRequest request) } } + /// public QueryResult GetSmartMatchInfos(FileOrganizationResultQuery query) { return _repo.GetSmartMatch(query); } - + /// public QueryResult GetSmartMatchInfos() { return _repo.GetSmartMatch(new FileOrganizationResultQuery()); } + /// public void DeleteSmartMatchEntry(string id, string matchString) { if (string.IsNullOrEmpty(id)) @@ -250,12 +269,7 @@ public void DeleteSmartMatchEntry(string id, string matchString) _repo.DeleteSmartMatch(id, matchString); } - /// - /// Attempts to add a an item to the list of currently processed items. - /// - /// The result item. - /// Passing true will notify the client to reload all items, otherwise only a single item will be refreshed. - /// True if the item was added, False if the item is already contained in the list. + /// public bool AddToInProgressList(FileOrganizationResult result, bool isNewItem) { if (string.IsNullOrWhiteSpace(result.Id)) @@ -282,11 +296,7 @@ public bool AddToInProgressList(FileOrganizationResult result, bool isNewItem) return true; } - /// - /// Removes an item from the list of currently processed items. - /// - /// The result item. - /// True if the item was removed, False if the item was not contained in the list. + /// public bool RemoveFromInprogressList(FileOrganizationResult result) { bool itemValue; @@ -298,6 +308,5 @@ public bool RemoveFromInprogressList(FileOrganizationResult result) return retval; } - } } diff --git a/Emby.AutoOrganize/Core/IFileOrganizationService.cs b/Emby.AutoOrganize/Core/IFileOrganizationService.cs index 3db0b0b..e2f4201 100644 --- a/Emby.AutoOrganize/Core/IFileOrganizationService.cs +++ b/Emby.AutoOrganize/Core/IFileOrganizationService.cs @@ -1,17 +1,20 @@ -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Querying; using System; using System.Threading; using System.Threading.Tasks; using Emby.AutoOrganize.Model; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.Querying; namespace Emby.AutoOrganize.Core { public interface IFileOrganizationService { event EventHandler> ItemAdded; + event EventHandler> ItemUpdated; + event EventHandler> ItemRemoved; + event EventHandler LogReset; /// @@ -85,7 +88,6 @@ public interface IFileOrganizationService /// /// The result. /// The cancellation token. - /// Task. void SaveResult(FileOrganizationResult result, CancellationToken cancellationToken); /// @@ -93,32 +95,31 @@ public interface IFileOrganizationService /// /// The result. /// The cancellation token. - /// Task. void SaveResult(SmartMatchResult result, CancellationToken cancellationToken); /// - /// Returns a list of smart match entries + /// Returns a list of smart match entries. /// /// The query. /// IEnumerable{SmartMatchInfo}. QueryResult GetSmartMatchInfos(FileOrganizationResultQuery query); /// - /// Returns a list of smart match entries + /// Returns a list of smart match entries. /// - /// The query. /// IEnumerable{SmartMatchInfo}. QueryResult GetSmartMatchInfos(); /// - /// Deletes a smart match entry. + /// Deletes a match string entry from a . If there are no match strings remaining + /// then the itself will also be deleted. /// - /// Item name. + /// The id of the to delete. /// The match string to delete. void DeleteSmartMatchEntry(string id, string matchString); /// - /// Attempts to add a an item to the list of currently processed items. + /// Attempts to add an item to the list of currently processed items. /// /// The result item. /// Passing true will notify the client to reload all items, otherwise only a single item will be refreshed. diff --git a/Emby.AutoOrganize/Core/MovieFileOrganizer.cs b/Emby.AutoOrganize/Core/MovieFileOrganizer.cs index 6ad9577..062a3d4 100644 --- a/Emby.AutoOrganize/Core/MovieFileOrganizer.cs +++ b/Emby.AutoOrganize/Core/MovieFileOrganizer.cs @@ -8,7 +8,6 @@ using Emby.AutoOrganize.Model; using Emby.Naming.Common; using Emby.Naming.Video; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; @@ -28,7 +27,11 @@ public class MovieFileOrganizer private readonly IFileSystem _fileSystem; private readonly IFileOrganizationService _organizationService; private readonly IProviderManager _providerManager; + private NamingOptions _namingOptions; + /// + /// Initializes a new instance of the class. + /// public MovieFileOrganizer( IFileOrganizationService organizationService, IFileSystem fileSystem, @@ -45,19 +48,6 @@ public MovieFileOrganizer( _providerManager = providerManager; } - private NamingOptions _namingOptions; - private NamingOptions GetNamingOptionsInternal() - { - if (_namingOptions == null) - { - var options = new NamingOptions(); - - _namingOptions = options; - } - - return _namingOptions; - } - private FileOrganizerType CurrentFileOrganizerType => FileOrganizerType.Movie; public async Task OrganizeMovieFile( @@ -87,8 +77,8 @@ public async Task OrganizeMovieFile( return result; } - var namingOptions = GetNamingOptionsInternal(); - var resolver = new VideoResolver(namingOptions); + _namingOptions = _namingOptions ?? new NamingOptions(); + var resolver = new VideoResolver(_namingOptions); var movieInfo = resolver.Resolve(path, false) ?? new VideoFileInfo(); @@ -101,7 +91,8 @@ public async Task OrganizeMovieFile( _logger.LogDebug("Extracted information from {0}. Movie {1}, Year {2}", path, movieName, movieYear); - await OrganizeMovie(path, + await OrganizeMovie( + path, movieName, movieYear, options, @@ -191,7 +182,7 @@ public async Task OrganizeWithCorrection(MovieFileOrgani movie = (Movie)_libraryManager.GetItemById(request.MovieId); } - // We manually set the media as Movie + // We manually set the media as Movie result.Type = CurrentFileOrganizerType; await OrganizeMovie( @@ -213,7 +204,8 @@ await OrganizeMovie( return result; } - private async Task OrganizeMovie(string sourcePath, + private async Task OrganizeMovie( + string sourcePath, string movieName, int? movieYear, MovieFileOrganizationOptions options, @@ -404,24 +396,15 @@ private async Task> AutoDetectMovie(string movi yearInName = movieYear; } - #region Search One - - var movieInfo = new MovieInfo - { - Name = nameWithoutYear, - Year = yearInName, - }; - - var searchResultsTask = await _providerManager.GetRemoteSearchResults(new RemoteSearchQuery - { - SearchInfo = movieInfo - - }, cancellationToken).ConfigureAwait(false); - - #endregion + // Perform remote search + var movieInfo = new MovieInfo { Name = nameWithoutYear, Year = yearInName, }; + var searchResultsTask = await _providerManager.GetRemoteSearchResults( + new RemoteSearchQuery { SearchInfo = movieInfo }, + cancellationToken).ConfigureAwait(false); // Group movies by name and year (if 2 movie with the exact same name, the same year ...) - var groupedResult = searchResultsTask.GroupBy(p => new { p.Name, p.ProductionYear }, + var groupedResult = searchResultsTask.GroupBy( + p => new { p.Name, p.ProductionYear }, p => p, (key, g) => new { Key = key, Result = g.ToList() }).ToList(); @@ -491,8 +474,9 @@ private Movie GetMatchingMovie(string movieName, int? movieYear, string targetFo .Where(i => i.Item2 > 0) .OrderByDescending(i => i.Item2) .Select(i => i.Item1) - // Check For the right folder AND the right extension (to handle quality upgrade) .FirstOrDefault(m => + + // Check For the right folder AND the right extension (to handle quality upgrade) m.Path.StartsWith(targetFolder, StringComparison.Ordinal) && Path.GetExtension(m.Path) == Path.GetExtension(result.OriginalPath)); @@ -506,11 +490,12 @@ private Movie GetMatchingMovie(string movieName, int? movieYear, string targetFo /// The movie. /// The options. /// System.String. - private string GetMoviePath(string sourcePath, + private string GetMoviePath( + string sourcePath, Movie movie, MovieFileOrganizationOptions options) { - var movieFileName = ""; + var movieFileName = string.Empty; if (options.MovieFolder) { diff --git a/Emby.AutoOrganize/Core/MovieFolderOrganizer.cs b/Emby.AutoOrganize/Core/MovieFolderOrganizer.cs index b741df6..421a061 100644 --- a/Emby.AutoOrganize/Core/MovieFolderOrganizer.cs +++ b/Emby.AutoOrganize/Core/MovieFolderOrganizer.cs @@ -125,21 +125,19 @@ public async Task Organize( double percent = numComplete; percent /= eligibleFiles.Count; - progress.Report(10 + 89 * percent); + progress.Report(10 + (89 * percent)); } } cancellationToken.ThrowIfCancellationRequested(); progress.Report(99); - List deleteExtensions = options.LeftOverFileExtensionsToDelete .Select(i => i.Trim().TrimStart('.')) .Where(i => !string.IsNullOrEmpty(i)) .Select(i => "." + i) .ToList(); - // Normal Clean Clean(processedFolders, watchLocations, options.DeleteEmptyFolders, deleteExtensions); @@ -165,7 +163,6 @@ private void Clean(IEnumerable paths, List watchLocations, bool { DeleteEmptyFolders(path, watchLocations); } - } } @@ -216,34 +213,33 @@ private void DeleteLeftOverFiles(string path, IEnumerable extensions) /// Deletes the empty folders. /// /// The path. - /// The path. - private void DeleteEmptyFolders(string path, List watchLocations) + /// A set of paths to ignore and not delete. + private void DeleteEmptyFolders(string path, List ignorePaths) { try { + // Recurse and delete all sub-folders foreach (var d in _fileSystem.GetDirectoryPaths(path)) { - DeleteEmptyFolders(d, watchLocations); + DeleteEmptyFolders(d, ignorePaths); } var entries = _fileSystem.GetFileSystemEntryPaths(path); - if (!entries.Any() && !IsWatchFolder(path, watchLocations)) + if (!entries.Any() && !IsWatchFolder(path, ignorePaths)) { - try - { - _logger.LogDebug("Deleting empty directory {0}", path); - Directory.Delete(path, false); - } - catch (UnauthorizedAccessException) { } - catch (IOException) { } + _logger.LogDebug("Deleting empty directory {0}", path); + Directory.Delete(path, false); } } - catch (UnauthorizedAccessException) { } + catch (Exception ex) when (ex is UnauthorizedAccessException || ex is IOException) + { + _logger.LogError(ex, "Failed to delete empty movie directory {Directory}", path); + } } /// - /// Determines if a given folder path is contained in a folder list + /// Determines if a given folder path is contained in a folder list. /// /// The folder path to check. /// A list of folders. diff --git a/Emby.AutoOrganize/Core/NameUtils.cs b/Emby.AutoOrganize/Core/NameUtils.cs index cf8ad20..3e31694 100644 --- a/Emby.AutoOrganize/Core/NameUtils.cs +++ b/Emby.AutoOrganize/Core/NameUtils.cs @@ -17,7 +17,7 @@ internal static int GetMatchScore(string sortedName, int? year, string itemName, var seriesNameWithoutYear = itemName; if (itemProductionYear.HasValue) { - seriesNameWithoutYear = seriesNameWithoutYear.Replace(itemProductionYear.Value.ToString(UsCulture), String.Empty); + seriesNameWithoutYear = seriesNameWithoutYear.Replace(itemProductionYear.Value.ToString(UsCulture), string.Empty); } if (IsNameMatch(sortedName, seriesNameWithoutYear)) @@ -47,13 +47,12 @@ internal static Tuple GetMatchScore(string sortedName, int? year, T i return new Tuple(item, GetMatchScore(sortedName, year, item.Name, item.ProductionYear)); } - private static bool IsNameMatch(string name1, string name2) { name1 = GetComparableName(name1); name2 = GetComparableName(name2); - return String.Equals(name1, name2, StringComparison.OrdinalIgnoreCase); + return string.Equals(name1, name2, StringComparison.OrdinalIgnoreCase); } private static string GetComparableName(string name) @@ -76,9 +75,9 @@ private static string GetComparableName(string name) .Replace("'", " ") .Replace("[", " ") .Replace("]", " ") - .Replace(" a ", String.Empty, StringComparison.OrdinalIgnoreCase) - .Replace(" the ", String.Empty, StringComparison.OrdinalIgnoreCase) - .Replace(" ", String.Empty); + .Replace(" a ", string.Empty, StringComparison.OrdinalIgnoreCase) + .Replace(" the ", string.Empty, StringComparison.OrdinalIgnoreCase) + .Replace(" ", string.Empty); return name.Trim(); } diff --git a/Emby.AutoOrganize/Core/OrganizationException.cs b/Emby.AutoOrganize/Core/OrganizationException.cs index 432f3e9..9e2814c 100644 --- a/Emby.AutoOrganize/Core/OrganizationException.cs +++ b/Emby.AutoOrganize/Core/OrganizationException.cs @@ -8,13 +8,10 @@ public class OrganizationException : Exception { public OrganizationException() { - } public OrganizationException(string msg) : base(msg) { - } - } } diff --git a/Emby.AutoOrganize/Core/OrganizerScheduledTask.cs b/Emby.AutoOrganize/Core/OrganizerScheduledTask.cs index 3a558f0..b6770eb 100644 --- a/Emby.AutoOrganize/Core/OrganizerScheduledTask.cs +++ b/Emby.AutoOrganize/Core/OrganizerScheduledTask.cs @@ -21,6 +21,9 @@ public class OrganizerScheduledTask : IScheduledTask, IConfigurableScheduledTask private readonly IServerConfigurationManager _config; private readonly IProviderManager _providerManager; + /// + /// Initializes a new instance of the class. + /// public OrganizerScheduledTask(ILibraryMonitor libraryMonitor, ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem, IServerConfigurationManager config, IProviderManager providerManager) { _libraryMonitor = libraryMonitor; @@ -31,31 +34,37 @@ public OrganizerScheduledTask(ILibraryMonitor libraryMonitor, ILibraryManager li _providerManager = providerManager; } - public string Name - { - get { return "Organize new media files"; } - } + /// + public string Key => "AutoOrganize"; - public string Description - { - get { return "Processes new files available in the configured watch folder."; } - } + /// + public string Name => "Organize new media files"; - public string Category - { - get { return "Library"; } - } + /// + public string Description => "Processes new files available in the configured watch folder."; - private AutoOrganizeOptions GetAutoOrganizeOptions() - { - return _config.GetAutoOrganizeOptions(); - } + /// + public string Category => "Library"; + + /// + public bool IsHidden => + !_config.GetAutoOrganizeOptions().TvOptions.IsEnabled + && !_config.GetAutoOrganizeOptions().MovieOptions.IsEnabled; + /// + public bool IsEnabled => + _config.GetAutoOrganizeOptions().TvOptions.IsEnabled + || _config.GetAutoOrganizeOptions().MovieOptions.IsEnabled; + + /// + public bool IsLogged => false; + + /// public async Task Execute(CancellationToken cancellationToken, IProgress progress) { bool queueTv = false, queueMovie = false; - var options = GetAutoOrganizeOptions(); + var options = _config.GetAutoOrganizeOptions(); if (options.TvOptions.IsEnabled) { @@ -66,7 +75,6 @@ public async Task Execute(CancellationToken cancellationToken, IProgress .Organize(options.TvOptions, progress, cancellationToken).ConfigureAwait(false); } - //var queueMovie = false; if (options.MovieOptions.IsEnabled) { queueMovie = options.MovieOptions.QueueLibraryScan; @@ -82,37 +90,14 @@ public async Task Execute(CancellationToken cancellationToken, IProgress } } - /// - /// Creates the triggers that define when the task will run - /// - /// IEnumerable{BaseTaskTrigger}. + /// public IEnumerable GetDefaultTriggers() { - return new[] { - + return new[] + { // Every so often - new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromMinutes(5).Ticks} + new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromMinutes(5).Ticks } }; } - - public bool IsHidden - { - get { return !GetAutoOrganizeOptions().TvOptions.IsEnabled && !GetAutoOrganizeOptions().MovieOptions.IsEnabled; } - } - - public bool IsEnabled - { - get { return GetAutoOrganizeOptions().TvOptions.IsEnabled || GetAutoOrganizeOptions().MovieOptions.IsEnabled; } - } - - public bool IsLogged - { - get { return false; } - } - - public string Key - { - get { return "AutoOrganize"; } - } } } diff --git a/Emby.AutoOrganize/Core/TvFolderOrganizer.cs b/Emby.AutoOrganize/Core/TvFolderOrganizer.cs index c38f8e6..b0d8cfb 100644 --- a/Emby.AutoOrganize/Core/TvFolderOrganizer.cs +++ b/Emby.AutoOrganize/Core/TvFolderOrganizer.cs @@ -118,7 +118,7 @@ public async Task Organize( double percent = numComplete; percent /= eligibleFiles.Count; - progress.Report(10 + 89 * percent); + progress.Report(10 + (89 * percent)); } } @@ -156,7 +156,6 @@ private void Clean(IEnumerable paths, List watchLocations, bool { DeleteEmptyFolders(path, watchLocations); } - } } @@ -207,34 +206,32 @@ private void DeleteLeftOverFiles(string path, IEnumerable extensions) /// Deletes the empty folders. /// /// The path. - /// The path. - private void DeleteEmptyFolders(string path, List watchLocations) + /// A set of paths to ignore and not delete. + private void DeleteEmptyFolders(string path, List ignorePaths) { try { foreach (var d in _fileSystem.GetDirectoryPaths(path)) { - DeleteEmptyFolders(d, watchLocations); + DeleteEmptyFolders(d, ignorePaths); } var entries = _fileSystem.GetFileSystemEntryPaths(path); - if (!entries.Any() && !IsWatchFolder(path, watchLocations)) + if (!entries.Any() && !IsWatchFolder(path, ignorePaths)) { - try - { - _logger.LogDebug("Deleting empty directory {0}", path); - Directory.Delete(path, false); - } - catch (UnauthorizedAccessException) { } - catch (IOException) { } + _logger.LogDebug("Deleting empty directory {0}", path); + Directory.Delete(path, false); } } - catch (UnauthorizedAccessException) { } + catch (Exception ex) when (ex is UnauthorizedAccessException || ex is IOException) + { + _logger.LogError("Failed to delete empty TV directory"); + } } /// - /// Determines if a given folder path is contained in a folder list + /// Determines if a given folder path is contained in a folder list. /// /// The folder path to check. /// A list of folders. diff --git a/Emby.AutoOrganize/Data/BaseSqliteRepository.cs b/Emby.AutoOrganize/Data/BaseSqliteRepository.cs index 7e5602c..bb4a90f 100644 --- a/Emby.AutoOrganize/Data/BaseSqliteRepository.cs +++ b/Emby.AutoOrganize/Data/BaseSqliteRepository.cs @@ -12,57 +12,50 @@ namespace Emby.AutoOrganize.Data { public abstract class BaseSqliteRepository : IDisposable { - protected string DbFilePath { get; set; } - protected ReaderWriterLockSlim WriteLock { get; } - private readonly ILogger _logger; + private readonly object _disposeLock = new object(); - protected BaseSqliteRepository(ILogger logger) - { - _logger = logger; - WriteLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); - } - - protected TransactionMode TransactionMode - { - get { return TransactionMode.Deferred; } - } - - protected TransactionMode ReadTransactionMode - { - get { return TransactionMode.Deferred; } - } - - internal static int ThreadSafeMode { get; set; } + private static bool _versionLogged; + private string _defaultWal; + private SQLiteDatabaseConnection _dbConnection; + private ManagedConnection _connection; + private bool _disposed = false; static BaseSqliteRepository() { SQLite3.EnableSharedCache = false; int rc = raw.sqlite3_config(raw.SQLITE_CONFIG_MEMSTATUS, 0); - //CheckOk(rc); rc = raw.sqlite3_config(raw.SQLITE_CONFIG_MULTITHREAD, 1); - //rc = raw.sqlite3_config(raw.SQLITE_CONFIG_SINGLETHREAD, 1); - //rc = raw.sqlite3_config(raw.SQLITE_CONFIG_SERIALIZED, 1); - //CheckOk(rc); rc = raw.sqlite3_enable_shared_cache(1); ThreadSafeMode = raw.sqlite3_threadsafe(); } - private static bool _versionLogged; - - private string _defaultWal; - private SQLiteDatabaseConnection _dbConnection; - private ManagedConnection _connection; - - protected virtual bool EnableSingleConnection + protected BaseSqliteRepository(ILogger logger) { - get { return true; } + _logger = logger; + WriteLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); } + protected string DbFilePath { get; set; } + + protected ReaderWriterLockSlim WriteLock { get; } + + protected TransactionMode TransactionMode => TransactionMode.Deferred; + + protected TransactionMode ReadTransactionMode => TransactionMode.Deferred; + + internal static int ThreadSafeMode { get; set; } + + protected virtual bool EnableSingleConnection => true; + + protected virtual bool EnableTempStoreMemory => false; + + protected virtual int? CacheSize => null; + protected ManagedConnection CreateConnection(bool isReadOnly = false) { if (_connection != null) @@ -83,14 +76,15 @@ protected ManagedConnection CreateConnection(bool isReadOnly = false) if (isReadOnly) { - //_logger.LogInformation("Opening read connection"); - //connectionFlags = ConnectionFlags.ReadOnly; + // TODO: set connection flags correctly + // connectionFlags = ConnectionFlags.ReadOnly + _logger.LogDebug("Opening read connection to database"); connectionFlags = ConnectionFlags.Create; connectionFlags |= ConnectionFlags.ReadWrite; } else { - //_logger.LogInformation("Opening write connection"); + _logger.LogDebug("Opening write connection to database."); connectionFlags = ConnectionFlags.Create; connectionFlags |= ConnectionFlags.ReadWrite; } @@ -119,8 +113,8 @@ protected ManagedConnection CreateConnection(bool isReadOnly = false) var queries = new List { - //"PRAGMA cache size=-10000" - //"PRAGMA read_uncommitted = true", + // "PRAGMA cache size=-10000" + // "PRAGMA read_uncommitted = true", "PRAGMA synchronous=Normal" }; @@ -184,11 +178,7 @@ public List PrepareAllSafe(IDatabaseConnection connection, IEnumerab protected bool TableExists(ManagedConnection connection, string name) { - return connection.RunInTransaction(db => - { - return TableExists(db, name); - - }, ReadTransactionMode); + return connection.RunInTransaction(db => TableExists(db, name), ReadTransactionMode); } protected bool TableExists(IDatabaseConnection db, string name) @@ -236,26 +226,6 @@ protected void RunDefaultInitialization(ManagedConnection db) _logger.LogInformation("PRAGMA synchronous={sync}", db.Query("PRAGMA synchronous").SelectScalarString().First()); } - protected virtual bool EnableTempStoreMemory - { - get - { - return false; - } - } - - protected virtual int? CacheSize - { - get - { - return null; - } - } - - #region IDisposable Support - private bool _disposed = false; - private readonly object _disposeLock = new object(); - public void Dispose() { Dispose(true); @@ -265,10 +235,13 @@ public void Dispose() /// /// Releases unmanaged and - optionally - managed resources. /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { - if (_disposed) return; + if (_disposed) + { + return; + } if (disposing) { @@ -297,7 +270,6 @@ private void DisposeConnection() _logger.LogError(ex, "Error disposing database", ex); } } - #endregion protected List GetColumnNames(IDatabaseConnection connection, string table) { @@ -332,11 +304,13 @@ public static class ReaderWriterLockSlimExtensions private sealed class ReadLockToken : IDisposable { private ReaderWriterLockSlim _sync; + public ReadLockToken(ReaderWriterLockSlim sync) { _sync = sync; sync.EnterReadLock(); } + public void Dispose() { if (_sync != null) @@ -346,14 +320,17 @@ public void Dispose() } } } + private sealed class WriteLockToken : IDisposable { private ReaderWriterLockSlim _sync; + public WriteLockToken(ReaderWriterLockSlim sync) { _sync = sync; sync.EnterWriteLock(); } + public void Dispose() { if (_sync != null) @@ -366,18 +343,19 @@ public void Dispose() public static IDisposable Read(this ReaderWriterLockSlim obj) { - //if (BaseSqliteRepository.ThreadSafeMode > 0) - //{ + // if (BaseSqliteRepository.ThreadSafeMode > 0) + // { // return new DummyToken(); - //} + // } return new WriteLockToken(obj); } + public static IDisposable Write(this ReaderWriterLockSlim obj) { - //if (BaseSqliteRepository.ThreadSafeMode > 0) - //{ + // if (BaseSqliteRepository.ThreadSafeMode > 0) + // { // return new DummyToken(); - //} + // } return new WriteLockToken(obj); } } diff --git a/Emby.AutoOrganize/Data/IFileOrganizationRepository.cs b/Emby.AutoOrganize/Data/IFileOrganizationRepository.cs index 5f42e56..3f36507 100644 --- a/Emby.AutoOrganize/Data/IFileOrganizationRepository.cs +++ b/Emby.AutoOrganize/Data/IFileOrganizationRepository.cs @@ -1,27 +1,24 @@ -using MediaBrowser.Model.Querying; using System.Threading; using System.Threading.Tasks; using Emby.AutoOrganize.Model; +using MediaBrowser.Model.Querying; namespace Emby.AutoOrganize.Data { public interface IFileOrganizationRepository { - #region FileOrganizationResult - /// /// Saves the result. /// /// The result. /// The cancellation token. - /// Task. void SaveResult(FileOrganizationResult result, CancellationToken cancellationToken); /// /// Deletes the specified identifier. /// /// The identifier. - /// Task. + /// A task representing the delete operation. Task Delete(string id); /// @@ -50,10 +47,6 @@ public interface IFileOrganizationRepository /// Task. Task DeleteCompleted(); - #endregion - - #region SmartMatch - void SaveResult(SmartMatchResult result, CancellationToken cancellationToken); void DeleteSmartMatch(string id); @@ -63,7 +56,5 @@ public interface IFileOrganizationRepository void DeleteAllSmartMatch(); QueryResult GetSmartMatch(FileOrganizationResultQuery query); - - #endregion } } diff --git a/Emby.AutoOrganize/Data/SqliteExtensions.cs b/Emby.AutoOrganize/Data/SqliteExtensions.cs index 8fdbfcd..f37b17c 100644 --- a/Emby.AutoOrganize/Data/SqliteExtensions.cs +++ b/Emby.AutoOrganize/Data/SqliteExtensions.cs @@ -1,15 +1,55 @@ using System; using System.Collections.Generic; using System.Globalization; -using MediaBrowser.Model.IO; +using System.IO; using MediaBrowser.Model.Serialization; using SQLitePCL.pretty; -using System.IO; namespace Emby.AutoOrganize.Data { public static class SqliteExtensions { + /// + /// An array of ISO-8601 DateTime formats that we support parsing. + /// + private static readonly string[] _datetimeFormats = new string[] + { + "THHmmssK", + "THHmmK", + "HH:mm:ss.FFFFFFFK", + "HH:mm:ssK", + "HH:mmK", + "yyyy-MM-dd HH:mm:ss.FFFFFFFK", /* NOTE: UTC default (5). */ + "yyyy-MM-dd HH:mm:ssK", + "yyyy-MM-dd HH:mmK", + "yyyy-MM-ddTHH:mm:ss.FFFFFFFK", + "yyyy-MM-ddTHH:mmK", + "yyyy-MM-ddTHH:mm:ssK", + "yyyyMMddHHmmssK", + "yyyyMMddHHmmK", + "yyyyMMddTHHmmssFFFFFFFK", + "THHmmss", + "THHmm", + "HH:mm:ss.FFFFFFF", + "HH:mm:ss", + "HH:mm", + "yyyy-MM-dd HH:mm:ss.FFFFFFF", /* NOTE: Non-UTC default (19). */ + "yyyy-MM-dd HH:mm:ss", + "yyyy-MM-dd HH:mm", + "yyyy-MM-ddTHH:mm:ss.FFFFFFF", + "yyyy-MM-ddTHH:mm", + "yyyy-MM-ddTHH:mm:ss", + "yyyyMMddHHmmss", + "yyyyMMddHHmm", + "yyyyMMddTHHmmssFFFFFFF", + "yyyy-MM-dd", + "yyyyMMdd", + "yy-MM-dd" + }; + + private static string _datetimeFormatUtc = _datetimeFormats[5]; + private static string _datetimeFormatLocal = _datetimeFormats[19]; + public static void RunQueries(this SQLiteDatabaseConnection connection, string[] queries) { if (queries == null) @@ -57,61 +97,24 @@ private static string GetDateTimeKindFormat( return (kind == DateTimeKind.Utc) ? _datetimeFormatUtc : _datetimeFormatLocal; } - /// - /// An array of ISO-8601 DateTime formats that we support parsing. - /// - private static string[] _datetimeFormats = new string[] { - "THHmmssK", - "THHmmK", - "HH:mm:ss.FFFFFFFK", - "HH:mm:ssK", - "HH:mmK", - "yyyy-MM-dd HH:mm:ss.FFFFFFFK", /* NOTE: UTC default (5). */ - "yyyy-MM-dd HH:mm:ssK", - "yyyy-MM-dd HH:mmK", - "yyyy-MM-ddTHH:mm:ss.FFFFFFFK", - "yyyy-MM-ddTHH:mmK", - "yyyy-MM-ddTHH:mm:ssK", - "yyyyMMddHHmmssK", - "yyyyMMddHHmmK", - "yyyyMMddTHHmmssFFFFFFFK", - "THHmmss", - "THHmm", - "HH:mm:ss.FFFFFFF", - "HH:mm:ss", - "HH:mm", - "yyyy-MM-dd HH:mm:ss.FFFFFFF", /* NOTE: Non-UTC default (19). */ - "yyyy-MM-dd HH:mm:ss", - "yyyy-MM-dd HH:mm", - "yyyy-MM-ddTHH:mm:ss.FFFFFFF", - "yyyy-MM-ddTHH:mm", - "yyyy-MM-ddTHH:mm:ss", - "yyyyMMddHHmmss", - "yyyyMMddHHmm", - "yyyyMMddTHHmmssFFFFFFF", - "yyyy-MM-dd", - "yyyyMMdd", - "yy-MM-dd" - }; - - private static string _datetimeFormatUtc = _datetimeFormats[5]; - private static string _datetimeFormatLocal = _datetimeFormats[19]; - public static DateTime ReadDateTime(this IResultSetValue result) { var dateText = result.ToString(); return DateTime.ParseExact( - dateText, _datetimeFormats, + dateText, + _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None).ToUniversalTime(); } /// - /// Serializes to bytes. + /// Serializes an object to JSON bytes. /// - /// System.Byte[][]. - /// obj + /// The JSON serializer to use. + /// The object to serialize. + /// The serialized array. + /// If is null. public static byte[] SerializeToBytes(this IJsonSerializer json, object obj) { if (obj == null) @@ -172,13 +175,10 @@ public static Guid GetGuid(this IReadOnlyList result, int index return result[index].ReadGuidFromBlob(); } - private static void CheckName(string name) + private static void ThrowInvalidParamName(string name) { #if DEBUG - //if (!name.IndexOf("@", StringComparison.OrdinalIgnoreCase) != 0) - { - throw new Exception("Invalid param name: " + name); - } + throw new Exception("Invalid param name: " + name); #endif } @@ -191,7 +191,7 @@ public static void TryBind(this IStatement statement, string name, double value) } else { - CheckName(name); + ThrowInvalidParamName(name); } } @@ -211,7 +211,7 @@ public static void TryBind(this IStatement statement, string name, string value) } else { - CheckName(name); + ThrowInvalidParamName(name); } } @@ -224,7 +224,7 @@ public static void TryBind(this IStatement statement, string name, bool value) } else { - CheckName(name); + ThrowInvalidParamName(name); } } @@ -237,7 +237,7 @@ public static void TryBind(this IStatement statement, string name, float value) } else { - CheckName(name); + ThrowInvalidParamName(name); } } @@ -250,7 +250,7 @@ public static void TryBind(this IStatement statement, string name, int value) } else { - CheckName(name); + ThrowInvalidParamName(name); } } @@ -263,7 +263,7 @@ public static void TryBind(this IStatement statement, string name, Guid value) } else { - CheckName(name); + ThrowInvalidParamName(name); } } @@ -276,7 +276,7 @@ public static void TryBind(this IStatement statement, string name, DateTime valu } else { - CheckName(name); + ThrowInvalidParamName(name); } } @@ -289,7 +289,7 @@ public static void TryBind(this IStatement statement, string name, long value) } else { - CheckName(name); + ThrowInvalidParamName(name); } } @@ -302,7 +302,7 @@ public static void TryBind(this IStatement statement, string name, byte[] value) } else { - CheckName(name); + ThrowInvalidParamName(name); } } @@ -315,7 +315,7 @@ public static void TryBindNull(this IStatement statement, string name) } else { - CheckName(name); + ThrowInvalidParamName(name); } } @@ -391,12 +391,11 @@ public static void TryBind(this IStatement statement, string name, bool? value) } } - public static IEnumerable> ExecuteQuery( - this IStatement This) + public static IEnumerable> ExecuteQuery(this IStatement statement) { - while (This.MoveNext()) + while (statement.MoveNext()) { - yield return This.Current; + yield return statement.Current; } } } diff --git a/Emby.AutoOrganize/Data/SqliteFileOrganizationRepository.cs b/Emby.AutoOrganize/Data/SqliteFileOrganizationRepository.cs index 8aab6d5..1e2febb 100644 --- a/Emby.AutoOrganize/Data/SqliteFileOrganizationRepository.cs +++ b/Emby.AutoOrganize/Data/SqliteFileOrganizationRepository.cs @@ -17,39 +17,46 @@ namespace Emby.AutoOrganize.Data public class SqliteFileOrganizationRepository : BaseSqliteRepository, IFileOrganizationRepository { private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private readonly IJsonSerializer _json; + private readonly IJsonSerializer _jsonSerializer; - public SqliteFileOrganizationRepository(ILogger logger, IServerApplicationPaths appPaths, IJsonSerializer json) + /// + /// Initializes a new instance of the class. + /// + /// The application logger. + /// The server application paths. + /// A JSON serializer. + public SqliteFileOrganizationRepository( + ILogger logger, + IServerApplicationPaths appPaths, + IJsonSerializer jsonSerializer) : base(logger) { - _json = json; + _jsonSerializer = jsonSerializer; DbFilePath = Path.Combine(appPaths.DataPath, "fileorganization.db"); } /// - /// Opens the connection to the database + /// Opens the connection to the database. /// - /// Task. public void Initialize() { using (var connection = CreateConnection()) { RunDefaultInitialization(connection); - string[] queries = { - - "create table if not exists FileOrganizerResults (ResultId GUID PRIMARY KEY, OriginalPath TEXT, TargetPath TEXT, FileLength INT, OrganizationDate datetime, Status TEXT, OrganizationType TEXT, StatusMessage TEXT, ExtractedName TEXT, ExtractedYear int null, ExtractedSeasonNumber int null, ExtractedEpisodeNumber int null, ExtractedEndingEpisodeNumber, DuplicatePaths TEXT int null)", - "create index if not exists idx_FileOrganizerResults on FileOrganizerResults(ResultId)", - "create table if not exists SmartMatch (Id GUID PRIMARY KEY, ItemName TEXT, DisplayName TEXT, OrganizerType TEXT, MatchStrings TEXT null)", - "create index if not exists idx_SmartMatch on SmartMatch(Id)", - }; + string[] queries = + { + "create table if not exists FileOrganizerResults (ResultId GUID PRIMARY KEY, OriginalPath TEXT, TargetPath TEXT, FileLength INT, OrganizationDate datetime, Status TEXT, OrganizationType TEXT, StatusMessage TEXT, ExtractedName TEXT, ExtractedYear int null, ExtractedSeasonNumber int null, ExtractedEpisodeNumber int null, ExtractedEndingEpisodeNumber, DuplicatePaths TEXT int null)", + "create index if not exists idx_FileOrganizerResults on FileOrganizerResults(ResultId)", + "create table if not exists SmartMatch (Id GUID PRIMARY KEY, ItemName TEXT, DisplayName TEXT, OrganizerType TEXT, MatchStrings TEXT null)", + "create index if not exists idx_SmartMatch on SmartMatch(Id)", + }; connection.RunQueries(queries); } } - #region FileOrganizationResult - + /// public void SaveResult(FileOrganizationResult result, CancellationToken cancellationToken) { if (result == null) @@ -63,35 +70,38 @@ public void SaveResult(FileOrganizationResult result, CancellationToken cancella { using (var connection = CreateConnection()) { - connection.RunInTransaction(db => - { - var commandText = "replace into FileOrganizerResults (ResultId, OriginalPath, TargetPath, FileLength, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber, DuplicatePaths) values (@ResultId, @OriginalPath, @TargetPath, @FileLength, @OrganizationDate, @Status, @OrganizationType, @StatusMessage, @ExtractedName, @ExtractedYear, @ExtractedSeasonNumber, @ExtractedEpisodeNumber, @ExtractedEndingEpisodeNumber, @DuplicatePaths)"; - - using (var statement = db.PrepareStatement(commandText)) + connection.RunInTransaction( + db => { - statement.TryBind("@ResultId", result.Id.ToGuidBlob()); - statement.TryBind("@OriginalPath", result.OriginalPath); - - statement.TryBind("@TargetPath", result.TargetPath); - statement.TryBind("@FileLength", result.FileSize); - statement.TryBind("@OrganizationDate", result.Date.ToDateTimeParamValue()); - statement.TryBind("@Status", result.Status.ToString()); - statement.TryBind("@OrganizationType", result.Type.ToString()); - statement.TryBind("@StatusMessage", result.StatusMessage); - statement.TryBind("@ExtractedName", result.ExtractedName); - statement.TryBind("@ExtractedYear", result.ExtractedYear); - statement.TryBind("@ExtractedSeasonNumber", result.ExtractedSeasonNumber); - statement.TryBind("@ExtractedEpisodeNumber", result.ExtractedEpisodeNumber); - statement.TryBind("@ExtractedEndingEpisodeNumber", result.ExtractedEndingEpisodeNumber); - statement.TryBind("@DuplicatePaths", string.Join("|", result.DuplicatePaths.ToArray())); - - statement.MoveNext(); - } - }, TransactionMode); + var commandText = "replace into FileOrganizerResults (ResultId, OriginalPath, TargetPath, FileLength, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber, DuplicatePaths) values (@ResultId, @OriginalPath, @TargetPath, @FileLength, @OrganizationDate, @Status, @OrganizationType, @StatusMessage, @ExtractedName, @ExtractedYear, @ExtractedSeasonNumber, @ExtractedEpisodeNumber, @ExtractedEndingEpisodeNumber, @DuplicatePaths)"; + + using (var statement = db.PrepareStatement(commandText)) + { + statement.TryBind("@ResultId", result.Id.ToGuidBlob()); + statement.TryBind("@OriginalPath", result.OriginalPath); + + statement.TryBind("@TargetPath", result.TargetPath); + statement.TryBind("@FileLength", result.FileSize); + statement.TryBind("@OrganizationDate", result.Date.ToDateTimeParamValue()); + statement.TryBind("@Status", result.Status.ToString()); + statement.TryBind("@OrganizationType", result.Type.ToString()); + statement.TryBind("@StatusMessage", result.StatusMessage); + statement.TryBind("@ExtractedName", result.ExtractedName); + statement.TryBind("@ExtractedYear", result.ExtractedYear); + statement.TryBind("@ExtractedSeasonNumber", result.ExtractedSeasonNumber); + statement.TryBind("@ExtractedEpisodeNumber", result.ExtractedEpisodeNumber); + statement.TryBind("@ExtractedEndingEpisodeNumber", result.ExtractedEndingEpisodeNumber); + statement.TryBind("@DuplicatePaths", string.Join("|", result.DuplicatePaths.ToArray())); + + statement.MoveNext(); + } + }, + TransactionMode); } } } + /// public Task Delete(string id) { if (string.IsNullOrEmpty(id)) @@ -103,58 +113,64 @@ public Task Delete(string id) { using (var connection = CreateConnection()) { - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("delete from FileOrganizerResults where ResultId = @ResultId")) + connection.RunInTransaction( + db => { - statement.TryBind("@ResultId", id.ToGuidBlob()); - statement.MoveNext(); - } - }, TransactionMode); + string command = "delete from FileOrganizerResults where ResultId = @ResultId"; + using (var statement = db.PrepareStatement(command)) + { + statement.TryBind("@ResultId", id.ToGuidBlob()); + statement.MoveNext(); + } + }, + TransactionMode); } } return Task.CompletedTask; } + /// public Task DeleteAll() { using (WriteLock.Write()) { using (var connection = CreateConnection()) { - connection.RunInTransaction(db => - { - var commandText = "delete from FileOrganizerResults"; - - db.Execute(commandText); - }, TransactionMode); + connection.RunInTransaction( + db => db.Execute("delete from FileOrganizerResults"), + TransactionMode); } } return Task.CompletedTask; } + /// public Task DeleteCompleted() { using (WriteLock.Write()) { using (var connection = CreateConnection()) { - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("delete from FileOrganizerResults where Status = @Status")) + connection.RunInTransaction( + db => { - statement.TryBind("@Status", FileSortingStatus.Success.ToString()); - statement.MoveNext(); - } - }, TransactionMode); + string command = "delete from FileOrganizerResults where Status = @Status"; + using (var statement = db.PrepareStatement(command)) + { + statement.TryBind("@Status", FileSortingStatus.Success.ToString()); + statement.MoveNext(); + } + }, + TransactionMode); } } return Task.CompletedTask; } + /// public QueryResult GetResults(FileOrganizationResultQuery query) { if (query == null) @@ -205,6 +221,8 @@ public QueryResult GetResults(FileOrganizationResultQuer } } } + + /// public FileOrganizationResult GetResult(string id) { if (string.IsNullOrEmpty(id)) @@ -230,7 +248,8 @@ public FileOrganizationResult GetResult(string id) } } } - public FileOrganizationResult GetResult(IReadOnlyList reader) + + private FileOrganizationResult GetResult(IReadOnlyList reader) { var index = 0; @@ -310,10 +329,7 @@ public FileOrganizationResult GetResult(IReadOnlyList reader) return result; } - #endregion - - #region SmartMatch - + /// public void SaveResult(SmartMatchResult result, CancellationToken cancellationToken) { if (result == null) @@ -327,26 +343,29 @@ public void SaveResult(SmartMatchResult result, CancellationToken cancellationTo { using (var connection = CreateConnection()) { - connection.RunInTransaction(db => - { - var commandText = "replace into SmartMatch (Id, ItemName, DisplayName, OrganizerType, MatchStrings) values (@Id, @ItemName, @DisplayName, @OrganizerType, @MatchStrings)"; - - using (var statement = db.PrepareStatement(commandText)) + connection.RunInTransaction( + db => { - statement.TryBind("@Id", result.Id.ToGuidBlob()); + var commandText = "replace into SmartMatch (Id, ItemName, DisplayName, OrganizerType, MatchStrings) values (@Id, @ItemName, @DisplayName, @OrganizerType, @MatchStrings)"; - statement.TryBind("@ItemName", result.ItemName); - statement.TryBind("@DisplayName", result.DisplayName); - statement.TryBind("@OrganizerType", result.OrganizerType.ToString()); - statement.TryBind("@MatchStrings", _json.SerializeToString(result.MatchStrings)); + using (var statement = db.PrepareStatement(commandText)) + { + statement.TryBind("@Id", result.Id.ToGuidBlob()); - statement.MoveNext(); - } - }, TransactionMode); + statement.TryBind("@ItemName", result.ItemName); + statement.TryBind("@DisplayName", result.DisplayName); + statement.TryBind("@OrganizerType", result.OrganizerType.ToString()); + statement.TryBind("@MatchStrings", _jsonSerializer.SerializeToString(result.MatchStrings)); + + statement.MoveNext(); + } + }, + TransactionMode); } } } + /// public void DeleteSmartMatch(string id) { if (string.IsNullOrEmpty(id)) @@ -358,18 +377,21 @@ public void DeleteSmartMatch(string id) { using (var connection = CreateConnection()) { - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("delete from SmartMatch where Id = @Id")) + connection.RunInTransaction( + db => { - statement.TryBind("@Id", id.ToGuidBlob()); - statement.MoveNext(); - } - }, TransactionMode); + using (var statement = db.PrepareStatement("delete from SmartMatch where Id = @Id")) + { + statement.TryBind("@Id", id.ToGuidBlob()); + statement.MoveNext(); + } + }, + TransactionMode); } } } + /// public Task DeleteSmartMatch(string id, string matchString) { if (string.IsNullOrEmpty(id)) @@ -393,23 +415,21 @@ public Task DeleteSmartMatch(string id, string matchString) return Task.CompletedTask; } + /// public void DeleteAllSmartMatch() { using (WriteLock.Write()) { using (var connection = CreateConnection()) { - connection.RunInTransaction(db => - { - var commandText = "delete from SmartMatch"; - - db.Execute(commandText); - }, TransactionMode); + connection.RunInTransaction( + db => db.Execute("delete from SmartMatch"), + TransactionMode); } } } - public SmartMatchResult GetSmartMatch(string id) + private SmartMatchResult GetSmartMatch(string id) { if (string.IsNullOrEmpty(id)) { @@ -435,6 +455,7 @@ public SmartMatchResult GetSmartMatch(string id) } } + /// public QueryResult GetSmartMatch(FileOrganizationResultQuery query) { if (query == null) @@ -507,12 +528,10 @@ private SmartMatchResult GetResultSmartMatch(IReadOnlyList read index++; if (reader[index].SQLiteType != SQLiteType.Null) { - result.MatchStrings.AddRange(_json.DeserializeFromString>(reader[index].ToString())); + result.MatchStrings.AddRange(_jsonSerializer.DeserializeFromString>(reader[index].ToString())); } return result; } - - #endregion } } diff --git a/Emby.AutoOrganize/Emby.AutoOrganize.csproj b/Emby.AutoOrganize/Emby.AutoOrganize.csproj index 21c34d7..460017e 100644 --- a/Emby.AutoOrganize/Emby.AutoOrganize.csproj +++ b/Emby.AutoOrganize/Emby.AutoOrganize.csproj @@ -4,6 +4,7 @@ netstandard2.0 5.0.0 5.0.0 + true @@ -43,7 +44,7 @@ - + diff --git a/Emby.AutoOrganize/Model/AutoOrganizeOptions.cs b/Emby.AutoOrganize/Model/AutoOrganizeOptions.cs index 379ad63..ca7cd17 100644 --- a/Emby.AutoOrganize/Model/AutoOrganizeOptions.cs +++ b/Emby.AutoOrganize/Model/AutoOrganizeOptions.cs @@ -4,35 +4,42 @@ namespace Emby.AutoOrganize.Model { + /// + /// Configuration options for the . + /// public class AutoOrganizeOptions { /// - /// Gets or sets the tv options. + /// Initializes a new instance of the class. + /// + public AutoOrganizeOptions() + { + TvOptions = new TvFileOrganizationOptions(); + MovieOptions = new MovieFileOrganizationOptions(); + Converted = false; + } + + /// + /// Gets or sets options used to auto-organize TV media. /// - /// The tv options. public TvFileOrganizationOptions TvOptions { get; set; } /// - /// Gets or sets the tv options. + /// Gets or sets the options used to auto-organize movie media. /// - /// The tv options. public MovieFileOrganizationOptions MovieOptions { get; set; } /// /// Gets or sets a list of smart match entries. /// - /// The smart match entries. + [Obsolete("This configuration is now stored in the SQLite database.")] [SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This property needs to support serialization by both ServiceStack and XmlSerializer")] - public List SmartMatchInfos { get; set; } + public List SmartMatchInfos { get; set; } = new List(); + /// + /// Gets or sets a value indicating whether the smart match info has been moved from this configuration into + /// the SQLite database. + /// public bool Converted { get; set; } - - public AutoOrganizeOptions() - { - TvOptions = new TvFileOrganizationOptions(); - MovieOptions = new MovieFileOrganizationOptions(); - SmartMatchInfos = new List(); - Converted = false; - } } } diff --git a/Emby.AutoOrganize/Model/EpisodeFileOrganizationRequest.cs b/Emby.AutoOrganize/Model/EpisodeFileOrganizationRequest.cs index 2aa75a0..1e60f14 100644 --- a/Emby.AutoOrganize/Model/EpisodeFileOrganizationRequest.cs +++ b/Emby.AutoOrganize/Model/EpisodeFileOrganizationRequest.cs @@ -5,7 +5,7 @@ namespace Emby.AutoOrganize.Model public class EpisodeFileOrganizationRequest { public string ResultId { get; set; } - + public string SeriesId { get; set; } public int SeasonNumber { get; set; } @@ -15,6 +15,7 @@ public class EpisodeFileOrganizationRequest public int? EndingEpisodeNumber { get; set; } public bool RememberCorrection { get; set; } + public string NewSeriesName { get; set; } public int? NewSeriesYear { get; set; } diff --git a/Emby.AutoOrganize/Model/FileOrganizationResult.cs b/Emby.AutoOrganize/Model/FileOrganizationResult.cs index 2f76d36..14fb371 100644 --- a/Emby.AutoOrganize/Model/FileOrganizationResult.cs +++ b/Emby.AutoOrganize/Model/FileOrganizationResult.cs @@ -5,12 +5,20 @@ namespace Emby.AutoOrganize.Model { public class FileOrganizationResult { + /// + /// Initializes a new instance of the class. + /// + public FileOrganizationResult() + { + DuplicatePaths = new List(); + } + /// /// Gets or sets the result identifier. /// /// The result identifier. public string Id { get; set; } - + /// /// Gets or sets the original path. /// @@ -52,7 +60,7 @@ public class FileOrganizationResult /// /// The extracted ending episode number. public int? ExtractedEndingEpisodeNumber { get; set; } - + /// /// Gets or sets the target path. /// @@ -96,14 +104,9 @@ public class FileOrganizationResult public long FileSize { get; set; } /// - /// Indicates if the item is currently being processed. + /// Gets or sets a value indicating whether the item is currently being processed. /// - /// Runtime property not persisted to the store. + /// This is a runtime property that is not persisted to the store. public bool IsInProgress { get; set; } - - public FileOrganizationResult() - { - DuplicatePaths = new List().AsReadOnly(); - } } } diff --git a/Emby.AutoOrganize/Model/FileOrganizationResultQuery.cs b/Emby.AutoOrganize/Model/FileOrganizationResultQuery.cs index e170e1e..1744a05 100644 --- a/Emby.AutoOrganize/Model/FileOrganizationResultQuery.cs +++ b/Emby.AutoOrganize/Model/FileOrganizationResultQuery.cs @@ -3,15 +3,14 @@ namespace Emby.AutoOrganize.Model public class FileOrganizationResultQuery { /// - /// Skips over a given number of items within the results. Use for paging. + /// Gets or sets a value indicating the number of items to skips over in the query. Use to specify a page + /// number. /// - /// The start index. public int? StartIndex { get; set; } /// - /// The maximum number of items to return + /// Gets or sets the maximum number of items to return. Use to specify a page size. /// - /// The limit. public int? Limit { get; set; } } } diff --git a/Emby.AutoOrganize/Model/FileOrganizerType.cs b/Emby.AutoOrganize/Model/FileOrganizerType.cs index f7ff2e0..13b31f0 100644 --- a/Emby.AutoOrganize/Model/FileOrganizerType.cs +++ b/Emby.AutoOrganize/Model/FileOrganizerType.cs @@ -1,10 +1,28 @@ namespace Emby.AutoOrganize.Model { + /// + /// Contains the supported file organization types. + /// public enum FileOrganizerType { + /// + /// Unknown. + /// Unknown, + + /// + /// Movie. + /// Movie, + + /// + /// TV Episode. + /// Episode, + + /// + /// Song. + /// Song } } diff --git a/Emby.AutoOrganize/Model/FileSortingStatus.cs b/Emby.AutoOrganize/Model/FileSortingStatus.cs index 39fa37a..4486559 100644 --- a/Emby.AutoOrganize/Model/FileSortingStatus.cs +++ b/Emby.AutoOrganize/Model/FileSortingStatus.cs @@ -1,9 +1,23 @@ namespace Emby.AutoOrganize.Model { + /// + /// The result status of sorting a single item. + /// public enum FileSortingStatus { + /// + /// File sorting completed successfully. + /// Success, + + /// + /// File sorting failed due to an error. + /// Failure, + + /// + /// File sorting was skipped on this item because it already exists in the target library. + /// SkippedExisting } } diff --git a/Emby.AutoOrganize/Model/MovieFileOrganizationOptions.cs b/Emby.AutoOrganize/Model/MovieFileOrganizationOptions.cs index 7f517c2..d420ede 100644 --- a/Emby.AutoOrganize/Model/MovieFileOrganizationOptions.cs +++ b/Emby.AutoOrganize/Model/MovieFileOrganizationOptions.cs @@ -6,6 +6,22 @@ namespace Emby.AutoOrganize.Model { public class MovieFileOrganizationOptions { + /// + /// Initializes a new instance of the class. + /// + public MovieFileOrganizationOptions() + { + MinFileSizeMb = 50; + LeftOverFileExtensionsToDelete = new List(); + MoviePattern = "%fn.%ext"; + WatchLocations = new List(); + CopyOriginalFile = false; + MovieFolder = false; + MovieFolderPattern = "%mn (%my)"; + QueueLibraryScan = false; + ExtendedClean = false; + } + public bool IsEnabled { get; set; } public int MinFileSizeMb { get; set; } @@ -35,26 +51,5 @@ public class MovieFileOrganizationOptions public string MovieFolderPattern { get; set; } public bool QueueLibraryScan { get; set; } - - public MovieFileOrganizationOptions() - { - MinFileSizeMb = 50; - - LeftOverFileExtensionsToDelete = new List(); - - MoviePattern = "%fn.%ext"; - - WatchLocations = new List(); - - CopyOriginalFile = false; - - MovieFolder = false; - - MovieFolderPattern = "%mn (%my)"; - - QueueLibraryScan = false; - - ExtendedClean = false; - } } } diff --git a/Emby.AutoOrganize/Model/MovieFileOrganizationRequest.cs b/Emby.AutoOrganize/Model/MovieFileOrganizationRequest.cs index 1c685db..13844d4 100644 --- a/Emby.AutoOrganize/Model/MovieFileOrganizationRequest.cs +++ b/Emby.AutoOrganize/Model/MovieFileOrganizationRequest.cs @@ -5,7 +5,7 @@ namespace Emby.AutoOrganize.Model public class MovieFileOrganizationRequest { public string ResultId { get; set; } - + public string MovieId { get; set; } public string NewMovieName { get; set; } diff --git a/Emby.AutoOrganize/Model/SmartMatchInfo.cs b/Emby.AutoOrganize/Model/SmartMatchInfo.cs index 13bef21..b09d131 100644 --- a/Emby.AutoOrganize/Model/SmartMatchInfo.cs +++ b/Emby.AutoOrganize/Model/SmartMatchInfo.cs @@ -4,17 +4,21 @@ namespace Emby.AutoOrganize.Model { + [Obsolete("This has been replaced by SmartMatchResult")] public class SmartMatchInfo { + public SmartMatchInfo() + { + MatchStrings = new List(); + } + public string ItemName { get; set; } + public string DisplayName { get; set; } + public FileOrganizerType OrganizerType { get; set; } + [SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This property needs to support serialization by both ServiceStack and XmlSerializer")] public List MatchStrings { get; set; } - - public SmartMatchInfo() - { - MatchStrings = new List(); - } } } diff --git a/Emby.AutoOrganize/Model/SmartMatchQuery.cs b/Emby.AutoOrganize/Model/SmartMatchQuery.cs index b833625..2aff233 100644 --- a/Emby.AutoOrganize/Model/SmartMatchQuery.cs +++ b/Emby.AutoOrganize/Model/SmartMatchQuery.cs @@ -7,15 +7,14 @@ namespace Emby.AutoOrganize.Model public class SmartMatchQuery { /// - /// Skips over a given number of items within the results. Use for paging. + /// Gets or sets a value indicating the number of items to skips over in the query. Use to specify a page + /// number. /// - /// The start index. public int? StartIndex { get; set; } /// - /// The maximum number of items to return + /// Gets or sets the maximum number of items to return. Use to specify a page size. /// - /// The limit. public int? Limit { get; set; } } } diff --git a/Emby.AutoOrganize/Model/SmartMatchResult.cs b/Emby.AutoOrganize/Model/SmartMatchResult.cs index d789930..0480ead 100644 --- a/Emby.AutoOrganize/Model/SmartMatchResult.cs +++ b/Emby.AutoOrganize/Model/SmartMatchResult.cs @@ -3,22 +3,48 @@ namespace Emby.AutoOrganize.Model { + /// + /// Used to customize matching of media files using a set of match strings. + /// public class SmartMatchResult { + /// + /// Initializes a new instance of the class. + /// + public SmartMatchResult() + { + Id = Guid.NewGuid(); + MatchStrings = new List(); + } + + /// + /// Gets or sets the unique identifier for this record. + /// public Guid Id { get; set; } + /// + /// Gets or sets the item name. This should match the name of the target media item (movie, series, etc.) + /// public string ItemName { get; set; } - + + /// + /// Gets or sets the display name of the smart match. + /// + /// Currently, this is always the same as . public string DisplayName { get; set; } - + + /// + /// Gets or sets the media type this smart match is meant to organize. + /// public FileOrganizerType OrganizerType { get; set; } + /// + /// Gets the match strings used for auto-organizing. + /// + /// + /// When organizing, a media name (series, movie, etc.) is first extracted from the filepath. The extracted + /// media name will be checked against this set of strings for a match. + /// public List MatchStrings { get; } - - public SmartMatchResult() - { - Id = Guid.NewGuid(); - MatchStrings = new List(); - } } } diff --git a/Emby.AutoOrganize/Model/TvFileOrganizationOptions.cs b/Emby.AutoOrganize/Model/TvFileOrganizationOptions.cs index 602e957..dd0f617 100644 --- a/Emby.AutoOrganize/Model/TvFileOrganizationOptions.cs +++ b/Emby.AutoOrganize/Model/TvFileOrganizationOptions.cs @@ -6,6 +6,24 @@ namespace Emby.AutoOrganize.Model { public class TvFileOrganizationOptions { + /// + /// Initializes a new instance of the class. + /// + public TvFileOrganizationOptions() + { + MinFileSizeMb = 50; + LeftOverFileExtensionsToDelete = new List(); + WatchLocations = new List(); + EpisodeNamePattern = "%sn - %sx%0e - %en.%ext"; + MultiEpisodeNamePattern = "%sn - %sx%0e-x%0ed - %en.%ext"; + SeasonFolderPattern = "Season %s"; + SeasonZeroFolderName = "Season 0"; + SeriesFolderPattern = "%fn"; + CopyOriginalFile = false; + QueueLibraryScan = false; + ExtendedClean = false; + } + public bool IsEnabled { get; set; } public int MinFileSizeMb { get; set; } @@ -21,6 +39,7 @@ public class TvFileOrganizationOptions public string SeasonZeroFolderName { get; set; } public string EpisodeNamePattern { get; set; } + public string MultiEpisodeNamePattern { get; set; } public bool OverwriteExistingEpisodes { get; set; } @@ -38,26 +57,5 @@ public class TvFileOrganizationOptions public string SeriesFolderPattern { get; set; } public bool QueueLibraryScan { get; set; } - - public TvFileOrganizationOptions() - { - MinFileSizeMb = 50; - - LeftOverFileExtensionsToDelete = new List(); - - WatchLocations = new List(); - - EpisodeNamePattern = "%sn - %sx%0e - %en.%ext"; - MultiEpisodeNamePattern = "%sn - %sx%0e-x%0ed - %en.%ext"; - SeasonFolderPattern = "Season %s"; - SeasonZeroFolderName = "Season 0"; - SeriesFolderPattern = "%fn"; - - CopyOriginalFile = false; - - QueueLibraryScan = false; - - ExtendedClean = false; - } } } diff --git a/Emby.AutoOrganize/PluginEntryPoint.cs b/Emby.AutoOrganize/PluginEntryPoint.cs index f3d14b0..7767b5e 100644 --- a/Emby.AutoOrganize/PluginEntryPoint.cs +++ b/Emby.AutoOrganize/PluginEntryPoint.cs @@ -17,12 +17,11 @@ namespace Emby.AutoOrganize { + /// + /// Entry point for the . + /// public sealed class PluginEntryPoint : IServerEntryPoint { - public static PluginEntryPoint Current { get; private set; } - - public IFileOrganizationService FileOrganizationService { get; private set; } - private readonly ISessionManager _sessionManager; private readonly ITaskManager _taskManager; private readonly ILogger _logger; @@ -35,6 +34,9 @@ public sealed class PluginEntryPoint : IServerEntryPoint private IFileOrganizationRepository _repository; + /// + /// Initializes a new instance of the class. + /// public PluginEntryPoint( ISessionManager sessionManager, ITaskManager taskManager, @@ -57,6 +59,11 @@ public PluginEntryPoint( _json = json; } + public static PluginEntryPoint Current { get; private set; } + + public IFileOrganizationService FileOrganizationService { get; private set; } + + /// public Task RunAsync() { try @@ -71,13 +78,13 @@ public Task RunAsync() Current = this; FileOrganizationService = new FileOrganizationService(_taskManager, _repository, _logger, _libraryMonitor, _libraryManager, _config, _fileSystem, _providerManager); - FileOrganizationService.ItemAdded += _organizationService_ItemAdded; - FileOrganizationService.ItemRemoved += _organizationService_ItemRemoved; - FileOrganizationService.ItemUpdated += _organizationService_ItemUpdated; - FileOrganizationService.LogReset += _organizationService_LogReset; + FileOrganizationService.ItemAdded += OnOrganizationServiceItemAdded; + FileOrganizationService.ItemRemoved += OnOrganizationServiceItemRemoved; + FileOrganizationService.ItemUpdated += OnOrganizationServiceItemUpdated; + FileOrganizationService.LogReset += OnOrganizationServiceLogReset; // Convert Config - _config.Convert(FileOrganizationService); + _config.ConvertSmartMatchInfo(FileOrganizationService); return Task.CompletedTask; } @@ -91,32 +98,33 @@ private IFileOrganizationRepository GetRepository() return repo; } - private void _organizationService_LogReset(object sender, EventArgs e) + private void OnOrganizationServiceLogReset(object sender, EventArgs e) { _sessionManager.SendMessageToAdminSessions("AutoOrganize_LogReset", (FileOrganizationResult)null, CancellationToken.None); } - private void _organizationService_ItemUpdated(object sender, GenericEventArgs e) + private void OnOrganizationServiceItemUpdated(object sender, GenericEventArgs e) { _sessionManager.SendMessageToAdminSessions("AutoOrganize_ItemUpdated", e.Argument, CancellationToken.None); } - private void _organizationService_ItemRemoved(object sender, GenericEventArgs e) + private void OnOrganizationServiceItemRemoved(object sender, GenericEventArgs e) { _sessionManager.SendMessageToAdminSessions("AutoOrganize_ItemRemoved", e.Argument, CancellationToken.None); } - private void _organizationService_ItemAdded(object sender, GenericEventArgs e) + private void OnOrganizationServiceItemAdded(object sender, GenericEventArgs e) { _sessionManager.SendMessageToAdminSessions("AutoOrganize_ItemAdded", e.Argument, CancellationToken.None); } + /// public void Dispose() { - FileOrganizationService.ItemAdded -= _organizationService_ItemAdded; - FileOrganizationService.ItemRemoved -= _organizationService_ItemRemoved; - FileOrganizationService.ItemUpdated -= _organizationService_ItemUpdated; - FileOrganizationService.LogReset -= _organizationService_LogReset; + FileOrganizationService.ItemAdded -= OnOrganizationServiceItemAdded; + FileOrganizationService.ItemRemoved -= OnOrganizationServiceItemRemoved; + FileOrganizationService.ItemUpdated -= OnOrganizationServiceItemUpdated; + FileOrganizationService.LogReset -= OnOrganizationServiceLogReset; var repo = _repository as IDisposable; if (repo != null) diff --git a/jellyfin.ruleset b/jellyfin.ruleset index a2af73c..b3b9c7f 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -53,4 +53,18 @@ + + + + + + + + + + + + + +