Skip to content

Commit

Permalink
Auto-Organize: Added feature to remember/persist series matching in m…
Browse files Browse the repository at this point in the history
…anual organization dialog #2

When a filename cannot be auto-matched to an existing series name, the
organization must be performed manually.
Unfortunately not just once, but again and again for each episode coming
in.
This change proposes a simple but solid method to optionally persist the
matching condition from within the manual organization dialog.
This approach will make Emby "learn" how to organize files in the future
without user interaction.
  • Loading branch information
softworkz committed Oct 30, 2015
1 parent 4b7d700 commit cdb7e93
Show file tree
Hide file tree
Showing 23 changed files with 451 additions and 33 deletions.
44 changes: 44 additions & 0 deletions MediaBrowser.Api/Library/FileOrganizationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,34 @@ public class OrganizeEpisode
public bool RememberCorrection { get; set; }
}

[Route("/Library/FileOrganizationSmartMatch", "GET", Summary = "Gets smart match entries")]
public class GetSmartMatchInfos : IReturn<QueryResult<SmartMatchInfo>>
{
/// <summary>
/// Skips over a given number of items within the results. Use for paging.
/// </summary>
/// <value>The start index.</value>
[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; }

/// <summary>
/// The maximum number of items to return
/// </summary>
/// <value>The limit.</value>
[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; }
}

[Route("/Library/FileOrganizationSmartMatch/{Id}/Delete", "POST", Summary = "Deletes a smart match entry")]
public class DeleteSmartMatchEntry
{
[ApiMember(Name = "Id", Description = "Item ID", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
public string Id { get; set; }

[ApiMember(Name = "MatchString", Description = "SmartMatch String", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
public string MatchString { get; set; }
}

[Authenticated(Roles = "Admin")]
public class FileOrganizationService : BaseApiService
{
Expand Down Expand Up @@ -130,5 +158,21 @@ public void Post(OrganizeEpisode request)

Task.WaitAll(task);
}

public object Get(GetSmartMatchInfos request)
{
var result = _iFileOrganizationService.GetSmartMatchInfos(new FileOrganizationResultQuery
{
Limit = request.Limit,
StartIndex = request.StartIndex
});

return ToOptimizedSerializedResultUsingCache(result);
}

public void Post(DeleteSmartMatchEntry request)
{
_iFileOrganizationService.DeleteSmartMatchEntry(request.Id, request.MatchString);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,19 @@ public interface IFileOrganizationService
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task SaveResult(FileOrganizationResult result, CancellationToken cancellationToken);

/// <summary>
/// Returns a list of smart match entries
/// </summary>
/// <param name="query">The query.</param>
/// <returns>IEnumerable{SmartMatchInfo}.</returns>
QueryResult<SmartMatchInfo> GetSmartMatchInfos(FileOrganizationResultQuery query);

/// <summary>
/// Deletes a smart match entry.
/// </summary>
/// <param name="Id">Item Id.</param>
/// <param name="matchString">The match string to delete.</param>
void DeleteSmartMatchEntry(string Id, string matchString);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,12 @@
<Compile Include="..\MediaBrowser.Model\FileOrganization\FileSortingStatus.cs">
<Link>FileOrganization\FileSortingStatus.cs</Link>
</Compile>
<Compile Include="..\MediaBrowser.Model\FileOrganization\SmartMatchInfo.cs">
<Link>FileOrganization\SmartMatchInfo.cs</Link>
</Compile>
<Compile Include="..\MediaBrowser.Model\FileOrganization\SmartMatchOptions.cs">
<Link>FileOrganization\SmartMatchOptions.cs</Link>
</Compile>
<Compile Include="..\MediaBrowser.Model\FileOrganization\TvFileOrganizationOptions.cs">
<Link>FileOrganization\TvFileOrganizationOptions.cs</Link>
</Compile>
Expand Down
6 changes: 6 additions & 0 deletions MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,12 @@
<Compile Include="..\MediaBrowser.Model\FileOrganization\FileSortingStatus.cs">
<Link>FileOrganization\FileSortingStatus.cs</Link>
</Compile>
<Compile Include="..\MediaBrowser.Model\FileOrganization\SmartMatchInfo.cs">
<Link>FileOrganization\SmartMatchInfo.cs</Link>
</Compile>
<Compile Include="..\MediaBrowser.Model\FileOrganization\SmartMatchOptions.cs">
<Link>FileOrganization\SmartMatchOptions.cs</Link>
</Compile>
<Compile Include="..\MediaBrowser.Model\FileOrganization\TvFileOrganizationOptions.cs">
<Link>FileOrganization\TvFileOrganizationOptions.cs</Link>
</Compile>
Expand Down
7 changes: 7 additions & 0 deletions MediaBrowser.Model/FileOrganization/AutoOrganizeOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@ public class AutoOrganizeOptions
/// <value>The tv options.</value>
public TvFileOrganizationOptions TvOptions { get; set; }

/// <summary>
/// Gets or sets the smart match options.
/// </summary>
/// <value>The smart match options.</value>
public SmartMatchOptions SmartMatchOptions { get; set; }

public AutoOrganizeOptions()
{
TvOptions = new TvFileOrganizationOptions();
SmartMatchOptions = new SmartMatchOptions();
}
}
}
19 changes: 19 additions & 0 deletions MediaBrowser.Model/FileOrganization/SmartMatchInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

using System;
using System.Collections.Generic;

namespace MediaBrowser.Model.FileOrganization
{
public class SmartMatchInfo
{
public Guid Id { get; set; }
public string Name { get; set; }
public FileOrganizerType OrganizerType { get; set; }
public List<string> MatchStrings { get; set; }

public SmartMatchInfo()
{
MatchStrings = new List<string>();
}
}
}
18 changes: 18 additions & 0 deletions MediaBrowser.Model/FileOrganization/SmartMatchOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Collections.Generic;

namespace MediaBrowser.Model.FileOrganization
{
public class SmartMatchOptions
{
/// <summary>
/// Gets or sets a list of smart match entries.
/// </summary>
/// <value>The smart match entries.</value>
public List<SmartMatchInfo> SmartMatchInfos { get; set; }

public SmartMatchOptions()
{
SmartMatchInfos = new List<SmartMatchInfo>();
}
}
}
2 changes: 2 additions & 0 deletions MediaBrowser.Model/MediaBrowser.Model.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@
<Compile Include="Dto\MetadataEditorInfo.cs" />
<Compile Include="Dto\NameIdPair.cs" />
<Compile Include="Dto\NameValuePair.cs" />
<Compile Include="FileOrganization\SmartMatchInfo.cs" />
<Compile Include="FileOrganization\SmartMatchOptions.cs" />
<Compile Include="MediaInfo\LiveStreamRequest.cs" />
<Compile Include="MediaInfo\LiveStreamResponse.cs" />
<Compile Include="MediaInfo\PlaybackInfoRequest.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ public EpisodeFileOrganizer(IFileOrganizationService organizationService, IServe

public Task<FileOrganizationResult> OrganizeEpisodeFile(string path, CancellationToken cancellationToken)
{
var options = _config.GetAutoOrganizeOptions().TvOptions;
var options = _config.GetAutoOrganizeOptions();

return OrganizeEpisodeFile(path, options, false, cancellationToken);
}

public async Task<FileOrganizationResult> OrganizeEpisodeFile(string path, TvFileOrganizationOptions options, bool overwriteExisting, CancellationToken cancellationToken)
public async Task<FileOrganizationResult> OrganizeEpisodeFile(string path, AutoOrganizeOptions options, bool overwriteExisting, CancellationToken cancellationToken)
{
_logger.Info("Sorting file {0}", path);

Expand Down Expand Up @@ -93,7 +93,7 @@ public async Task<FileOrganizationResult> OrganizeEpisodeFile(string path, TvFil

result.ExtractedEndingEpisodeNumber = endingEpisodeNumber;

await OrganizeEpisode(path, seriesName, season.Value, episode.Value, endingEpisodeNumber, options, overwriteExisting, result, cancellationToken).ConfigureAwait(false);
await OrganizeEpisode(path, seriesName, season.Value, episode.Value, endingEpisodeNumber, options, overwriteExisting, false, result, cancellationToken).ConfigureAwait(false);
}
else
{
Expand Down Expand Up @@ -135,22 +135,22 @@ public async Task<FileOrganizationResult> OrganizeEpisodeFile(string path, TvFil
return result;
}

public async Task<FileOrganizationResult> OrganizeWithCorrection(EpisodeFileOrganizationRequest request, TvFileOrganizationOptions options, CancellationToken cancellationToken)
public async Task<FileOrganizationResult> OrganizeWithCorrection(EpisodeFileOrganizationRequest request, AutoOrganizeOptions options, CancellationToken cancellationToken)
{
var result = _organizationService.GetResult(request.ResultId);

var series = (Series)_libraryManager.GetItemById(new Guid(request.SeriesId));

await OrganizeEpisode(result.OriginalPath, series, request.SeasonNumber, request.EpisodeNumber, request.EndingEpisodeNumber, options, true, result, cancellationToken).ConfigureAwait(false);
await OrganizeEpisode(result.OriginalPath, series, request.SeasonNumber, request.EpisodeNumber, request.EndingEpisodeNumber, options, true, request.RememberCorrection, result, cancellationToken).ConfigureAwait(false);

await _organizationService.SaveResult(result, CancellationToken.None).ConfigureAwait(false);

return result;
}

private Task OrganizeEpisode(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int? endingEpiosdeNumber, TvFileOrganizationOptions options, bool overwriteExisting, FileOrganizationResult result, CancellationToken cancellationToken)
private Task OrganizeEpisode(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int? endingEpiosdeNumber, AutoOrganizeOptions options, bool overwriteExisting, bool rememberCorrection, FileOrganizationResult result, CancellationToken cancellationToken)
{
var series = GetMatchingSeries(seriesName, result);
var series = GetMatchingSeries(seriesName, result, options);

if (series == null)
{
Expand All @@ -161,15 +161,17 @@ private Task OrganizeEpisode(string sourcePath, string seriesName, int seasonNum
return Task.FromResult(true);
}

return OrganizeEpisode(sourcePath, series, seasonNumber, episodeNumber, endingEpiosdeNumber, options, overwriteExisting, result, cancellationToken);
return OrganizeEpisode(sourcePath, series, seasonNumber, episodeNumber, endingEpiosdeNumber, options, overwriteExisting, rememberCorrection, result, cancellationToken);
}

private async Task OrganizeEpisode(string sourcePath, Series series, int seasonNumber, int episodeNumber, int? endingEpiosdeNumber, TvFileOrganizationOptions options, bool overwriteExisting, FileOrganizationResult result, CancellationToken cancellationToken)
private async Task OrganizeEpisode(string sourcePath, Series series, int seasonNumber, int episodeNumber, int? endingEpiosdeNumber, AutoOrganizeOptions options, bool overwriteExisting, bool rememberCorrection, FileOrganizationResult result, CancellationToken cancellationToken)
{
_logger.Info("Sorting file {0} into series {1}", sourcePath, series.Path);

var originalExtractedSeriesString = result.ExtractedName;

// Proceed to sort the file
var newPath = await GetNewPath(sourcePath, series, seasonNumber, episodeNumber, endingEpiosdeNumber, options, cancellationToken).ConfigureAwait(false);
var newPath = await GetNewPath(sourcePath, series, seasonNumber, episodeNumber, endingEpiosdeNumber, options.TvOptions, cancellationToken).ConfigureAwait(false);

if (string.IsNullOrEmpty(newPath))
{
Expand All @@ -188,7 +190,7 @@ private async Task OrganizeEpisode(string sourcePath, Series series, int seasonN

if (!overwriteExisting)
{
if (options.CopyOriginalFile && fileExists && IsSameEpisode(sourcePath, newPath))
if (options.TvOptions.CopyOriginalFile && fileExists && IsSameEpisode(sourcePath, newPath))
{
_logger.Info("File {0} already copied to new path {1}, stopping organization", sourcePath, newPath);
result.Status = FileSortingStatus.SkippedExisting;
Expand All @@ -205,7 +207,7 @@ private async Task OrganizeEpisode(string sourcePath, Series series, int seasonN
}
}

PerformFileSorting(options, result);
PerformFileSorting(options.TvOptions, result);

if (overwriteExisting)
{
Expand Down Expand Up @@ -239,6 +241,31 @@ private async Task OrganizeEpisode(string sourcePath, Series series, int seasonN
}
}
}

if (rememberCorrection)
{
SaveSmartMatchString(originalExtractedSeriesString, series, options);
}
}

private void SaveSmartMatchString(string matchString, Series series, AutoOrganizeOptions options)
{
SmartMatchInfo info = options.SmartMatchOptions.SmartMatchInfos.Find(i => i.Id == series.Id);

if (info == null)
{
info = new SmartMatchInfo();
info.Id = series.Id;
info.OrganizerType = FileOrganizerType.Episode;
info.Name = series.Name;
options.SmartMatchOptions.SmartMatchInfos.Add(info);
}

if (!info.MatchStrings.Contains(matchString, StringComparer.OrdinalIgnoreCase))
{
info.MatchStrings.Add(matchString);
_config.SaveAutoOrganizeOptions(options);
}
}

private void DeleteLibraryFile(string path, bool renameRelatedFiles, string targetPath)
Expand Down Expand Up @@ -379,7 +406,7 @@ private void PerformFileSorting(TvFileOrganizationOptions options, FileOrganizat
}
}

private Series GetMatchingSeries(string seriesName, FileOrganizationResult result)
private Series GetMatchingSeries(string seriesName, FileOrganizationResult result, AutoOrganizeOptions options)
{
var parsedName = _libraryManager.ParseName(seriesName);

Expand All @@ -389,13 +416,28 @@ private Series GetMatchingSeries(string seriesName, FileOrganizationResult resul
result.ExtractedName = nameWithoutYear;
result.ExtractedYear = yearInName;

return _libraryManager.RootFolder.GetRecursiveChildren(i => i is Series)
var series = _libraryManager.RootFolder.GetRecursiveChildren(i => i is Series)
.Cast<Series>()
.Select(i => NameUtils.GetMatchScore(nameWithoutYear, yearInName, i))
.Where(i => i.Item2 > 0)
.OrderByDescending(i => i.Item2)
.Select(i => i.Item1)
.FirstOrDefault();

if (series == null)
{
SmartMatchInfo info = options.SmartMatchOptions.SmartMatchInfos.Where(e => e.MatchStrings.Contains(seriesName, StringComparer.OrdinalIgnoreCase)).FirstOrDefault();

if (info != null)
{
series = _libraryManager.RootFolder.GetRecursiveChildren(i => i is Series)
.Cast<Series>()
.Where(i => i.Id == info.Id)
.FirstOrDefault();
}
}

return series ?? new Series();
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ public static AutoOrganizeOptions GetAutoOrganizeOptions(this IConfigurationMana
{
return manager.GetConfiguration<AutoOrganizeOptions>("autoorganize");
}
public static void SaveAutoOrganizeOptions(this IConfigurationManager manager, AutoOrganizeOptions options)
{
manager.SaveConfiguration("autoorganize", options);
}
}

public class AutoOrganizeOptionsFactory : IConfigurationFactory
Expand Down
Loading

0 comments on commit cdb7e93

Please sign in to comment.