mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-11-15 09:59:06 -07:00
#680 - episode organization
This commit is contained in:
parent
cf1dac60f6
commit
9d40b684bf
@ -48,8 +48,13 @@
|
||||
<RunPostBuildEvent>Always</RunPostBuildEvent>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="SimpleInjector.Diagnostics">
|
||||
<HintPath>..\packages\SimpleInjector.2.4.0\lib\net45\SimpleInjector.Diagnostics.dll</HintPath>
|
||||
<Reference Include="SimpleInjector, Version=2.4.1.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\SimpleInjector.2.4.1\lib\net45\SimpleInjector.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SimpleInjector.Diagnostics, Version=2.4.1.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\SimpleInjector.2.4.1\lib\net45\SimpleInjector.Diagnostics.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Configuration" />
|
||||
@ -67,9 +72,6 @@
|
||||
<Reference Include="ServiceStack.Text">
|
||||
<HintPath>..\ThirdParty\ServiceStack.Text\ServiceStack.Text.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SimpleInjector">
|
||||
<HintPath>..\packages\SimpleInjector.2.4.0\lib\net45\SimpleInjector.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SharedVersion.cs">
|
||||
|
@ -2,5 +2,5 @@
|
||||
<packages>
|
||||
<package id="NLog" version="2.1.0" targetFramework="net45" />
|
||||
<package id="sharpcompress" version="0.10.2" targetFramework="net45" />
|
||||
<package id="SimpleInjector" version="2.4.0" targetFramework="net45" />
|
||||
<package id="SimpleInjector" version="2.4.1" targetFramework="net45" />
|
||||
</packages>
|
@ -12,14 +12,6 @@ namespace MediaBrowser.Controller.FileOrganization
|
||||
/// </summary>
|
||||
void BeginProcessNewFiles();
|
||||
|
||||
/// <summary>
|
||||
/// Saves the result.
|
||||
/// </summary>
|
||||
/// <param name="result">The result.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task SaveResult(FileOrganizationResult result, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the original file.
|
||||
/// </summary>
|
||||
@ -27,12 +19,25 @@ namespace MediaBrowser.Controller.FileOrganization
|
||||
/// <returns>Task.</returns>
|
||||
Task DeleteOriginalFile(string resultId);
|
||||
|
||||
/// <summary>
|
||||
/// Clears the log.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
Task ClearLog();
|
||||
|
||||
/// <summary>
|
||||
/// Performs the organization.
|
||||
/// </summary>
|
||||
/// <param name="resultId">The result identifier.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task PerformOrganization(string resultId);
|
||||
|
||||
/// <summary>
|
||||
/// Performs the episode organization.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task PerformEpisodeOrganization(EpisodeFileOrganizationRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the results.
|
||||
@ -40,5 +45,20 @@ namespace MediaBrowser.Controller.FileOrganization
|
||||
/// <param name="query">The query.</param>
|
||||
/// <returns>IEnumerable{FileOrganizationResult}.</returns>
|
||||
QueryResult<FileOrganizationResult> GetResults(FileOrganizationResultQuery query);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the result.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <returns>FileOrganizationResult.</returns>
|
||||
FileOrganizationResult GetResult(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the result.
|
||||
/// </summary>
|
||||
/// <param name="result">The result.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task SaveResult(FileOrganizationResult result, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
@ -35,5 +35,11 @@ namespace MediaBrowser.Controller.Persistence
|
||||
/// <param name="query">The query.</param>
|
||||
/// <returns>IEnumerable{FileOrganizationResult}.</returns>
|
||||
QueryResult<FileOrganizationResult> GetResults(FileOrganizationResultQuery query);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
Task DeleteAll();
|
||||
}
|
||||
}
|
||||
|
@ -15,4 +15,19 @@ namespace MediaBrowser.Model.FileOrganization
|
||||
/// <value>The limit.</value>
|
||||
public int? Limit { get; set; }
|
||||
}
|
||||
|
||||
public class EpisodeFileOrganizationRequest
|
||||
{
|
||||
public string ResultId { get; set; }
|
||||
|
||||
public string SeriesId { get; set; }
|
||||
|
||||
public int SeasonNumber { get; set; }
|
||||
|
||||
public int EpisodeNumber { get; set; }
|
||||
|
||||
public int? EndingEpisodeNumber { get; set; }
|
||||
|
||||
public bool RememberCorrection { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -39,12 +39,6 @@ namespace MediaBrowser.Providers.TV
|
||||
|
||||
private async Task RunInternal(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_config.Configuration.EnableInternetProviders)
|
||||
{
|
||||
progress.Report(100);
|
||||
return;
|
||||
}
|
||||
|
||||
var seriesList = _libraryManager.RootFolder
|
||||
.RecursiveChildren
|
||||
.OfType<Series>()
|
||||
@ -288,7 +282,7 @@ namespace MediaBrowser.Providers.TV
|
||||
return hasChanges;
|
||||
}
|
||||
|
||||
private Series DetermineAppropriateSeries(IEnumerable<Series> series, int seasonNumber)
|
||||
private Series DetermineAppropriateSeries(List<Series> series, int seasonNumber)
|
||||
{
|
||||
return series.FirstOrDefault(s => s.RecursiveChildren.OfType<Season>().Any(season => season.IndexNumber == seasonNumber)) ??
|
||||
series.FirstOrDefault(s => s.RecursiveChildren.OfType<Season>().Any(season => season.IndexNumber == 1)) ??
|
||||
|
@ -0,0 +1,357 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.FileOrganization;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.FileOrganization;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.FileOrganization
|
||||
{
|
||||
public class EpisodeFileOrganizer
|
||||
{
|
||||
private readonly IDirectoryWatchers _directoryWatchers;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IFileOrganizationService _organizationService;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
|
||||
public EpisodeFileOrganizer(IFileOrganizationService organizationService, IServerConfigurationManager config, IFileSystem fileSystem, ILogger logger, ILibraryManager libraryManager, IDirectoryWatchers directoryWatchers)
|
||||
{
|
||||
_organizationService = organizationService;
|
||||
_config = config;
|
||||
_fileSystem = fileSystem;
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
_directoryWatchers = directoryWatchers;
|
||||
}
|
||||
|
||||
public async Task<FileOrganizationResult> OrganizeEpisodeFile(string path, TvFileOrganizationOptions options, bool overwriteExisting)
|
||||
{
|
||||
_logger.Info("Sorting file {0}", path);
|
||||
|
||||
var result = new FileOrganizationResult
|
||||
{
|
||||
Date = DateTime.UtcNow,
|
||||
OriginalPath = path,
|
||||
OriginalFileName = Path.GetFileName(path),
|
||||
Type = FileOrganizerType.Episode
|
||||
};
|
||||
|
||||
var seriesName = TVUtils.GetSeriesNameFromEpisodeFile(path);
|
||||
|
||||
if (!string.IsNullOrEmpty(seriesName))
|
||||
{
|
||||
var season = TVUtils.GetSeasonNumberFromEpisodeFile(path);
|
||||
|
||||
result.ExtractedSeasonNumber = season;
|
||||
|
||||
if (season.HasValue)
|
||||
{
|
||||
// Passing in true will include a few extra regex's
|
||||
var episode = TVUtils.GetEpisodeNumberFromFile(path, true);
|
||||
|
||||
result.ExtractedEpisodeNumber = episode;
|
||||
|
||||
if (episode.HasValue)
|
||||
{
|
||||
_logger.Debug("Extracted information from {0}. Series name {1}, Season {2}, Episode {3}", path, seriesName, season, episode);
|
||||
|
||||
var endingEpisodeNumber = TVUtils.GetEndingEpisodeNumberFromFile(path);
|
||||
|
||||
result.ExtractedEndingEpisodeNumber = endingEpisodeNumber;
|
||||
|
||||
OrganizeEpisode(path, seriesName, season.Value, episode.Value, endingEpisodeNumber, options, overwriteExisting, result);
|
||||
}
|
||||
else
|
||||
{
|
||||
var msg = string.Format("Unable to determine episode number from {0}", path);
|
||||
result.Status = FileSortingStatus.Failure;
|
||||
result.StatusMessage = msg;
|
||||
_logger.Warn(msg);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var msg = string.Format("Unable to determine season number from {0}", path);
|
||||
result.Status = FileSortingStatus.Failure;
|
||||
result.StatusMessage = msg;
|
||||
_logger.Warn(msg);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var msg = string.Format("Unable to determine series name from {0}", path);
|
||||
result.Status = FileSortingStatus.Failure;
|
||||
result.StatusMessage = msg;
|
||||
_logger.Warn(msg);
|
||||
}
|
||||
|
||||
await _organizationService.SaveResult(result, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<FileOrganizationResult> OrganizeWithCorrection(EpisodeFileOrganizationRequest request, TvFileOrganizationOptions options)
|
||||
{
|
||||
var result = _organizationService.GetResult(request.ResultId);
|
||||
|
||||
var series = (Series)_libraryManager.GetItemById(new Guid(request.SeriesId));
|
||||
|
||||
OrganizeEpisode(result.OriginalPath, series, request.SeasonNumber, request.EpisodeNumber, request.EndingEpisodeNumber, _config.Configuration.TvFileOrganizationOptions, true, result);
|
||||
|
||||
await _organizationService.SaveResult(result, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void OrganizeEpisode(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int? endingEpiosdeNumber, TvFileOrganizationOptions options, bool overwriteExisting, FileOrganizationResult result)
|
||||
{
|
||||
var series = GetMatchingSeries(seriesName, result);
|
||||
|
||||
if (series == null)
|
||||
{
|
||||
var msg = string.Format("Unable to find series in library matching name {0}", seriesName);
|
||||
result.Status = FileSortingStatus.Failure;
|
||||
result.StatusMessage = msg;
|
||||
_logger.Warn(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
OrganizeEpisode(sourcePath, series, seasonNumber, episodeNumber, endingEpiosdeNumber, options, overwriteExisting, result);
|
||||
}
|
||||
|
||||
private void OrganizeEpisode(string sourcePath, Series series, int seasonNumber, int episodeNumber, int? endingEpiosdeNumber, TvFileOrganizationOptions options, bool overwriteExisting, FileOrganizationResult result)
|
||||
{
|
||||
_logger.Info("Sorting file {0} into series {1}", sourcePath, series.Path);
|
||||
|
||||
// Proceed to sort the file
|
||||
var newPath = GetNewPath(sourcePath, series, seasonNumber, episodeNumber, endingEpiosdeNumber, options);
|
||||
|
||||
if (string.IsNullOrEmpty(newPath))
|
||||
{
|
||||
var msg = string.Format("Unable to sort {0} because target path could not be determined.", sourcePath);
|
||||
result.Status = FileSortingStatus.Failure;
|
||||
result.StatusMessage = msg;
|
||||
_logger.Warn(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Info("Sorting file {0} to new path {1}", sourcePath, newPath);
|
||||
result.TargetPath = newPath;
|
||||
|
||||
var existing = GetDuplicatePaths(result.TargetPath, series, seasonNumber, episodeNumber);
|
||||
|
||||
if (!overwriteExisting && existing.Count > 0)
|
||||
{
|
||||
result.Status = FileSortingStatus.SkippedExisting;
|
||||
result.StatusMessage = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
PerformFileSorting(options, result);
|
||||
}
|
||||
|
||||
private List<string> GetDuplicatePaths(string targetPath, Series series, int seasonNumber, int episodeNumber)
|
||||
{
|
||||
var list = new List<string>();
|
||||
|
||||
if (File.Exists(targetPath))
|
||||
{
|
||||
list.Add(targetPath);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private void PerformFileSorting(TvFileOrganizationOptions options, FileOrganizationResult result)
|
||||
{
|
||||
_directoryWatchers.TemporarilyIgnore(result.TargetPath);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(result.TargetPath));
|
||||
|
||||
var copy = File.Exists(result.TargetPath);
|
||||
|
||||
try
|
||||
{
|
||||
if (copy)
|
||||
{
|
||||
File.Copy(result.OriginalPath, result.TargetPath, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
File.Move(result.OriginalPath, result.TargetPath);
|
||||
}
|
||||
|
||||
result.Status = FileSortingStatus.Success;
|
||||
result.StatusMessage = string.Empty;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errorMsg = string.Format("Failed to move file from {0} to {1}", result.OriginalPath, result.TargetPath);
|
||||
|
||||
result.Status = FileSortingStatus.Failure;
|
||||
result.StatusMessage = errorMsg;
|
||||
_logger.ErrorException(errorMsg, ex);
|
||||
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_directoryWatchers.RemoveTempIgnore(result.TargetPath);
|
||||
}
|
||||
|
||||
if (copy)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(result.OriginalPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error deleting {0}", ex, result.OriginalPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Series GetMatchingSeries(string seriesName, FileOrganizationResult result)
|
||||
{
|
||||
int? yearInName;
|
||||
var nameWithoutYear = seriesName;
|
||||
NameParser.ParseName(nameWithoutYear, out nameWithoutYear, out yearInName);
|
||||
|
||||
result.ExtractedName = nameWithoutYear;
|
||||
result.ExtractedYear = yearInName;
|
||||
|
||||
return _libraryManager.RootFolder.RecursiveChildren
|
||||
.OfType<Series>()
|
||||
.Select(i => NameUtils.GetMatchScore(nameWithoutYear, yearInName, i))
|
||||
.Where(i => i.Item2 > 0)
|
||||
.OrderByDescending(i => i.Item2)
|
||||
.Select(i => i.Item1)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the new path.
|
||||
/// </summary>
|
||||
/// <param name="sourcePath">The source path.</param>
|
||||
/// <param name="series">The series.</param>
|
||||
/// <param name="seasonNumber">The season number.</param>
|
||||
/// <param name="episodeNumber">The episode number.</param>
|
||||
/// <param name="endingEpisodeNumber">The ending episode number.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private string GetNewPath(string sourcePath, Series series, int seasonNumber, int episodeNumber, int? endingEpisodeNumber, TvFileOrganizationOptions options)
|
||||
{
|
||||
// If season and episode numbers match
|
||||
var currentEpisodes = series.RecursiveChildren.OfType<Episode>()
|
||||
.Where(i => i.IndexNumber.HasValue &&
|
||||
i.IndexNumber.Value == episodeNumber &&
|
||||
i.ParentIndexNumber.HasValue &&
|
||||
i.ParentIndexNumber.Value == seasonNumber)
|
||||
.ToList();
|
||||
|
||||
if (currentEpisodes.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var newPath = GetSeasonFolderPath(series, seasonNumber, options);
|
||||
|
||||
var episode = currentEpisodes.First();
|
||||
|
||||
var episodeFileName = GetEpisodeFileName(sourcePath, series.Name, seasonNumber, episodeNumber, endingEpisodeNumber, episode.Name, options);
|
||||
|
||||
newPath = Path.Combine(newPath, episodeFileName);
|
||||
|
||||
return newPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the season folder path.
|
||||
/// </summary>
|
||||
/// <param name="series">The series.</param>
|
||||
/// <param name="seasonNumber">The season number.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private string GetSeasonFolderPath(Series series, int seasonNumber, TvFileOrganizationOptions options)
|
||||
{
|
||||
// If there's already a season folder, use that
|
||||
var season = series
|
||||
.RecursiveChildren
|
||||
.OfType<Season>()
|
||||
.FirstOrDefault(i => i.LocationType == LocationType.FileSystem && i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber);
|
||||
|
||||
if (season != null)
|
||||
{
|
||||
return season.Path;
|
||||
}
|
||||
|
||||
var path = series.Path;
|
||||
|
||||
if (series.ContainsEpisodesWithoutSeasonFolders)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
if (seasonNumber == 0)
|
||||
{
|
||||
return Path.Combine(path, _fileSystem.GetValidFilename(options.SeasonZeroFolderName));
|
||||
}
|
||||
|
||||
var seasonFolderName = options.SeasonFolderPattern
|
||||
.Replace("%s", seasonNumber.ToString(_usCulture))
|
||||
.Replace("%0s", seasonNumber.ToString("00", _usCulture))
|
||||
.Replace("%00s", seasonNumber.ToString("000", _usCulture));
|
||||
|
||||
return Path.Combine(path, _fileSystem.GetValidFilename(seasonFolderName));
|
||||
}
|
||||
|
||||
private string GetEpisodeFileName(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int? endingEpisodeNumber, string episodeTitle, TvFileOrganizationOptions options)
|
||||
{
|
||||
seriesName = _fileSystem.GetValidFilename(seriesName);
|
||||
episodeTitle = _fileSystem.GetValidFilename(episodeTitle);
|
||||
|
||||
var sourceExtension = (Path.GetExtension(sourcePath) ?? string.Empty).TrimStart('.');
|
||||
|
||||
var pattern = endingEpisodeNumber.HasValue ? options.MultiEpisodeNamePattern : options.EpisodeNamePattern;
|
||||
|
||||
var result = pattern.Replace("%sn", seriesName)
|
||||
.Replace("%s.n", seriesName.Replace(" ", "."))
|
||||
.Replace("%s_n", seriesName.Replace(" ", "_"))
|
||||
.Replace("%s", seasonNumber.ToString(_usCulture))
|
||||
.Replace("%0s", seasonNumber.ToString("00", _usCulture))
|
||||
.Replace("%00s", seasonNumber.ToString("000", _usCulture))
|
||||
.Replace("%ext", sourceExtension)
|
||||
.Replace("%en", episodeTitle)
|
||||
.Replace("%e.n", episodeTitle.Replace(" ", "."))
|
||||
.Replace("%e_n", episodeTitle.Replace(" ", "_"));
|
||||
|
||||
if (endingEpisodeNumber.HasValue)
|
||||
{
|
||||
result = result.Replace("%ed", endingEpisodeNumber.Value.ToString(_usCulture))
|
||||
.Replace("%0ed", endingEpisodeNumber.Value.ToString("00", _usCulture))
|
||||
.Replace("%00ed", endingEpisodeNumber.Value.ToString("000", _usCulture));
|
||||
}
|
||||
|
||||
return result.Replace("%e", episodeNumber.ToString(_usCulture))
|
||||
.Replace("%0e", episodeNumber.ToString("00", _usCulture))
|
||||
.Replace("%00e", episodeNumber.ToString("000", _usCulture));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.ScheduledTasks;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.FileOrganization;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@ -21,14 +23,18 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
|
||||
private readonly ILogger _logger;
|
||||
private readonly IDirectoryWatchers _directoryWatchers;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
public FileOrganizationService(ITaskManager taskManager, IFileOrganizationRepository repo, ILogger logger, IDirectoryWatchers directoryWatchers, ILibraryManager libraryManager)
|
||||
public FileOrganizationService(ITaskManager taskManager, IFileOrganizationRepository repo, ILogger logger, IDirectoryWatchers directoryWatchers, ILibraryManager libraryManager, IServerConfigurationManager config, IFileSystem fileSystem)
|
||||
{
|
||||
_taskManager = taskManager;
|
||||
_repo = repo;
|
||||
_logger = logger;
|
||||
_directoryWatchers = directoryWatchers;
|
||||
_libraryManager = libraryManager;
|
||||
_config = config;
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
public void BeginProcessNewFiles()
|
||||
@ -53,6 +59,11 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
|
||||
return _repo.GetResults(query);
|
||||
}
|
||||
|
||||
public FileOrganizationResult GetResult(string id)
|
||||
{
|
||||
return _repo.GetResult(id);
|
||||
}
|
||||
|
||||
public Task DeleteOriginalFile(string resultId)
|
||||
{
|
||||
var result = _repo.GetResult(resultId);
|
||||
@ -79,44 +90,27 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
|
||||
throw new ArgumentException("No target path available.");
|
||||
}
|
||||
|
||||
_logger.Info("Moving {0} to {1}", result.OriginalPath, result.TargetPath);
|
||||
var organizer = new EpisodeFileOrganizer(this, _config, _fileSystem, _logger, _libraryManager,
|
||||
_directoryWatchers);
|
||||
|
||||
_directoryWatchers.TemporarilyIgnore(result.TargetPath);
|
||||
await organizer.OrganizeEpisodeFile(result.OriginalPath, _config.Configuration.TvFileOrganizationOptions, true)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var copy = File.Exists(result.TargetPath);
|
||||
await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (copy)
|
||||
{
|
||||
File.Copy(result.OriginalPath, result.TargetPath, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
File.Move(result.OriginalPath, result.TargetPath);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_directoryWatchers.RemoveTempIgnore(result.TargetPath);
|
||||
}
|
||||
public Task ClearLog()
|
||||
{
|
||||
return _repo.DeleteAll();
|
||||
}
|
||||
|
||||
if (copy)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(result.OriginalPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error deleting {0}", ex, result.OriginalPath);
|
||||
}
|
||||
}
|
||||
public async Task PerformEpisodeOrganization(EpisodeFileOrganizationRequest request)
|
||||
{
|
||||
var organizer = new EpisodeFileOrganizer(this, _config, _fileSystem, _logger, _libraryManager,
|
||||
_directoryWatchers);
|
||||
|
||||
result.Status = FileSortingStatus.Success;
|
||||
result.StatusMessage = string.Empty;
|
||||
|
||||
await SaveResult(result, CancellationToken.None).ConfigureAwait(false);
|
||||
await organizer.OrganizeWithCorrection(request, _config.Configuration.TvFileOrganizationOptions).ConfigureAwait(false);
|
||||
|
||||
await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
@ -0,0 +1,92 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.FileOrganization
|
||||
{
|
||||
public static class NameUtils
|
||||
{
|
||||
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
|
||||
internal static Tuple<T, int> GetMatchScore<T>(string sortedName, int? year, T series)
|
||||
where T : BaseItem
|
||||
{
|
||||
var score = 0;
|
||||
|
||||
var seriesNameWithoutYear = series.Name;
|
||||
if (series.ProductionYear.HasValue)
|
||||
{
|
||||
seriesNameWithoutYear = seriesNameWithoutYear.Replace(series.ProductionYear.Value.ToString(UsCulture), String.Empty);
|
||||
}
|
||||
|
||||
if (IsNameMatch(sortedName, seriesNameWithoutYear))
|
||||
{
|
||||
score++;
|
||||
|
||||
if (year.HasValue && series.ProductionYear.HasValue)
|
||||
{
|
||||
if (year.Value == series.ProductionYear.Value)
|
||||
{
|
||||
score++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Regardless of name, return a 0 score if the years don't match
|
||||
return new Tuple<T, int>(series, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Tuple<T, int>(series, score);
|
||||
}
|
||||
|
||||
|
||||
private static bool IsNameMatch(string name1, string name2)
|
||||
{
|
||||
name1 = GetComparableName(name1);
|
||||
name2 = GetComparableName(name2);
|
||||
|
||||
return String.Equals(name1, name2, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string GetComparableName(string name)
|
||||
{
|
||||
// TODO: Improve this - should ignore spaces, periods, underscores, most likely all symbols and
|
||||
// possibly remove sorting words like "the", "and", etc.
|
||||
|
||||
name = RemoveDiacritics(name);
|
||||
|
||||
name = " " + name.ToLower() + " ";
|
||||
|
||||
name = name.Replace(".", " ")
|
||||
.Replace("_", " ")
|
||||
.Replace("&", " ")
|
||||
.Replace("!", " ")
|
||||
.Replace("(", " ")
|
||||
.Replace(")", " ")
|
||||
.Replace(",", " ")
|
||||
.Replace("-", " ")
|
||||
.Replace(" a ", String.Empty)
|
||||
.Replace(" the ", String.Empty)
|
||||
.Replace(" ", String.Empty);
|
||||
|
||||
return name.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the diacritics.
|
||||
/// </summary>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private static string RemoveDiacritics(string text)
|
||||
{
|
||||
return String.Concat(
|
||||
text.Normalize(NormalizationForm.FormD)
|
||||
.Where(ch => CharUnicodeInfo.GetUnicodeCategory(ch) !=
|
||||
UnicodeCategory.NonSpacingMark)
|
||||
).Normalize(NormalizationForm.FormC);
|
||||
}
|
||||
}
|
||||
}
|
@ -14,21 +14,21 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
|
||||
{
|
||||
public class OrganizerScheduledTask : IScheduledTask, IConfigurableScheduledTask
|
||||
{
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IFileOrganizationService _iFileSortingRepository;
|
||||
private readonly IDirectoryWatchers _directoryWatchers;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IFileOrganizationService _organizationService;
|
||||
|
||||
public OrganizerScheduledTask(IServerConfigurationManager config, ILogger logger, ILibraryManager libraryManager, IFileSystem fileSystem, IFileOrganizationService iFileSortingRepository, IDirectoryWatchers directoryWatchers)
|
||||
public OrganizerScheduledTask(IDirectoryWatchers directoryWatchers, ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem, IServerConfigurationManager config, IFileOrganizationService organizationService)
|
||||
{
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
_fileSystem = fileSystem;
|
||||
_iFileSortingRepository = iFileSortingRepository;
|
||||
_directoryWatchers = directoryWatchers;
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
_config = config;
|
||||
_organizationService = organizationService;
|
||||
}
|
||||
|
||||
public string Name
|
||||
@ -48,7 +48,8 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
|
||||
|
||||
public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
return new TvFileSorter(_libraryManager, _logger, _fileSystem, _iFileSortingRepository, _directoryWatchers).Sort(_config.Configuration.TvFileOrganizationOptions, cancellationToken, progress);
|
||||
return new TvFolderOrganizer(_libraryManager, _logger, _fileSystem, _directoryWatchers, _organizationService, _config)
|
||||
.Organize(_config.Configuration.TvFileOrganizationOptions, cancellationToken, progress);
|
||||
}
|
||||
|
||||
public IEnumerable<ITaskTrigger> GetDefaultTriggers()
|
||||
|
@ -1,563 +0,0 @@
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.FileOrganization;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.FileOrganization;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.FileOrganization
|
||||
{
|
||||
public class TvFileSorter
|
||||
{
|
||||
private readonly IDirectoryWatchers _directoryWatchers;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IFileOrganizationService _iFileSortingRepository;
|
||||
|
||||
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
|
||||
public TvFileSorter(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem, IFileOrganizationService iFileSortingRepository, IDirectoryWatchers directoryWatchers)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
_iFileSortingRepository = iFileSortingRepository;
|
||||
_directoryWatchers = directoryWatchers;
|
||||
}
|
||||
|
||||
public async Task Sort(TvFileOrganizationOptions options, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
var minFileBytes = options.MinFileSizeMb * 1024 * 1024;
|
||||
|
||||
var watchLocations = options.WatchLocations.ToList();
|
||||
|
||||
var eligibleFiles = watchLocations.SelectMany(GetFilesToSort)
|
||||
.OrderBy(_fileSystem.GetCreationTimeUtc)
|
||||
.Where(i => EntityResolutionHelper.IsVideoFile(i.FullName) && i.Length >= minFileBytes)
|
||||
.ToList();
|
||||
|
||||
progress.Report(10);
|
||||
|
||||
var scanLibrary = false;
|
||||
|
||||
if (eligibleFiles.Count > 0)
|
||||
{
|
||||
var allSeries = _libraryManager.RootFolder
|
||||
.RecursiveChildren.OfType<Series>()
|
||||
.Where(i => i.LocationType == LocationType.FileSystem)
|
||||
.ToList();
|
||||
|
||||
var numComplete = 0;
|
||||
|
||||
foreach (var file in eligibleFiles)
|
||||
{
|
||||
var result = await SortFile(file.FullName, options, allSeries).ConfigureAwait(false);
|
||||
|
||||
if (result.Status == FileSortingStatus.Success)
|
||||
{
|
||||
scanLibrary = true;
|
||||
}
|
||||
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= eligibleFiles.Count;
|
||||
|
||||
progress.Report(10 + (89 * percent));
|
||||
}
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
progress.Report(99);
|
||||
|
||||
foreach (var path in watchLocations)
|
||||
{
|
||||
if (options.LeftOverFileExtensionsToDelete.Length > 0)
|
||||
{
|
||||
DeleteLeftOverFiles(path, options.LeftOverFileExtensionsToDelete);
|
||||
}
|
||||
|
||||
if (options.DeleteEmptyFolders)
|
||||
{
|
||||
DeleteEmptyFolders(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (scanLibrary)
|
||||
{
|
||||
await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the eligible files.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>IEnumerable{FileInfo}.</returns>
|
||||
private IEnumerable<FileInfo> GetFilesToSort(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new DirectoryInfo(path)
|
||||
.EnumerateFiles("*", SearchOption.AllDirectories)
|
||||
.ToList();
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.ErrorException("Error getting files from {0}", ex, path);
|
||||
|
||||
return new List<FileInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sorts the file.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="allSeries">All series.</param>
|
||||
private async Task<FileOrganizationResult> SortFile(string path, TvFileOrganizationOptions options, IEnumerable<Series> allSeries)
|
||||
{
|
||||
_logger.Info("Sorting file {0}", path);
|
||||
|
||||
var result = new FileOrganizationResult
|
||||
{
|
||||
Date = DateTime.UtcNow,
|
||||
OriginalPath = path,
|
||||
OriginalFileName = Path.GetFileName(path),
|
||||
Type = FileOrganizerType.Episode
|
||||
};
|
||||
|
||||
var seriesName = TVUtils.GetSeriesNameFromEpisodeFile(path);
|
||||
|
||||
if (!string.IsNullOrEmpty(seriesName))
|
||||
{
|
||||
var season = TVUtils.GetSeasonNumberFromEpisodeFile(path);
|
||||
|
||||
result.ExtractedSeasonNumber = season;
|
||||
|
||||
if (season.HasValue)
|
||||
{
|
||||
// Passing in true will include a few extra regex's
|
||||
var episode = TVUtils.GetEpisodeNumberFromFile(path, true);
|
||||
|
||||
result.ExtractedEpisodeNumber = episode;
|
||||
|
||||
if (episode.HasValue)
|
||||
{
|
||||
_logger.Debug("Extracted information from {0}. Series name {1}, Season {2}, Episode {3}", path, seriesName, season, episode);
|
||||
|
||||
var endingEpisodeNumber = TVUtils.GetEndingEpisodeNumberFromFile(path);
|
||||
|
||||
result.ExtractedEndingEpisodeNumber = endingEpisodeNumber;
|
||||
|
||||
SortFile(path, seriesName, season.Value, episode.Value, endingEpisodeNumber, options, allSeries, result);
|
||||
}
|
||||
else
|
||||
{
|
||||
var msg = string.Format("Unable to determine episode number from {0}", path);
|
||||
result.Status = FileSortingStatus.Failure;
|
||||
result.StatusMessage = msg;
|
||||
_logger.Warn(msg);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var msg = string.Format("Unable to determine season number from {0}", path);
|
||||
result.Status = FileSortingStatus.Failure;
|
||||
result.StatusMessage = msg;
|
||||
_logger.Warn(msg);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var msg = string.Format("Unable to determine series name from {0}", path);
|
||||
result.Status = FileSortingStatus.Failure;
|
||||
result.StatusMessage = msg;
|
||||
_logger.Warn(msg);
|
||||
}
|
||||
|
||||
await LogResult(result).ConfigureAwait(false);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sorts the file.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="seriesName">Name of the series.</param>
|
||||
/// <param name="seasonNumber">The season number.</param>
|
||||
/// <param name="episodeNumber">The episode number.</param>
|
||||
/// <param name="endingEpiosdeNumber">The ending epiosde number.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="allSeries">All series.</param>
|
||||
/// <param name="result">The result.</param>
|
||||
private void SortFile(string path, string seriesName, int seasonNumber, int episodeNumber, int? endingEpiosdeNumber, TvFileOrganizationOptions options, IEnumerable<Series> allSeries, FileOrganizationResult result)
|
||||
{
|
||||
var series = GetMatchingSeries(seriesName, allSeries, result);
|
||||
|
||||
if (series == null)
|
||||
{
|
||||
var msg = string.Format("Unable to find series in library matching name {0}", seriesName);
|
||||
result.Status = FileSortingStatus.Failure;
|
||||
result.StatusMessage = msg;
|
||||
_logger.Warn(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Info("Sorting file {0} into series {1}", path, series.Path);
|
||||
|
||||
// Proceed to sort the file
|
||||
var newPath = GetNewPath(path, series, seasonNumber, episodeNumber, endingEpiosdeNumber, options);
|
||||
|
||||
if (string.IsNullOrEmpty(newPath))
|
||||
{
|
||||
var msg = string.Format("Unable to sort {0} because target path could not be determined.", path);
|
||||
result.Status = FileSortingStatus.Failure;
|
||||
result.StatusMessage = msg;
|
||||
_logger.Warn(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Info("Sorting file {0} to new path {1}", path, newPath);
|
||||
result.TargetPath = newPath;
|
||||
|
||||
var targetExists = File.Exists(result.TargetPath);
|
||||
if (!options.OverwriteExistingEpisodes && targetExists)
|
||||
{
|
||||
result.Status = FileSortingStatus.SkippedExisting;
|
||||
return;
|
||||
}
|
||||
|
||||
PerformFileSorting(options, result, targetExists);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs the file sorting.
|
||||
/// </summary>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="result">The result.</param>
|
||||
/// <param name="copy">if set to <c>true</c> [copy].</param>
|
||||
private void PerformFileSorting(TvFileOrganizationOptions options, FileOrganizationResult result, bool copy)
|
||||
{
|
||||
_directoryWatchers.TemporarilyIgnore(result.TargetPath);
|
||||
|
||||
try
|
||||
{
|
||||
if (copy)
|
||||
{
|
||||
File.Copy(result.OriginalPath, result.TargetPath, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
File.Move(result.OriginalPath, result.TargetPath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errorMsg = string.Format("Failed to move file from {0} to {1}", result.OriginalPath, result.TargetPath);
|
||||
|
||||
result.Status = FileSortingStatus.Failure;
|
||||
result.StatusMessage = errorMsg;
|
||||
_logger.ErrorException(errorMsg, ex);
|
||||
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_directoryWatchers.RemoveTempIgnore(result.TargetPath);
|
||||
}
|
||||
|
||||
if (copy)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(result.OriginalPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error deleting {0}", ex, result.OriginalPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs the result.
|
||||
/// </summary>
|
||||
/// <param name="result">The result.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private Task LogResult(FileOrganizationResult result)
|
||||
{
|
||||
return _iFileSortingRepository.SaveResult(result, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the new path.
|
||||
/// </summary>
|
||||
/// <param name="sourcePath">The source path.</param>
|
||||
/// <param name="series">The series.</param>
|
||||
/// <param name="seasonNumber">The season number.</param>
|
||||
/// <param name="episodeNumber">The episode number.</param>
|
||||
/// <param name="endingEpisodeNumber">The ending episode number.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private string GetNewPath(string sourcePath, Series series, int seasonNumber, int episodeNumber, int? endingEpisodeNumber, TvFileOrganizationOptions options)
|
||||
{
|
||||
// If season and episode numbers match
|
||||
var currentEpisodes = series.RecursiveChildren.OfType<Episode>()
|
||||
.Where(i => i.IndexNumber.HasValue &&
|
||||
i.IndexNumber.Value == episodeNumber &&
|
||||
i.ParentIndexNumber.HasValue &&
|
||||
i.ParentIndexNumber.Value == seasonNumber)
|
||||
.ToList();
|
||||
|
||||
if (currentEpisodes.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var newPath = GetSeasonFolderPath(series, seasonNumber, options);
|
||||
|
||||
var episode = currentEpisodes.First();
|
||||
|
||||
var episodeFileName = GetEpisodeFileName(sourcePath, series.Name, seasonNumber, episodeNumber, endingEpisodeNumber, episode.Name, options);
|
||||
|
||||
newPath = Path.Combine(newPath, episodeFileName);
|
||||
|
||||
return newPath;
|
||||
}
|
||||
|
||||
private string GetEpisodeFileName(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int? endingEpisodeNumber, string episodeTitle, TvFileOrganizationOptions options)
|
||||
{
|
||||
seriesName = _fileSystem.GetValidFilename(seriesName);
|
||||
episodeTitle = _fileSystem.GetValidFilename(episodeTitle);
|
||||
|
||||
var sourceExtension = (Path.GetExtension(sourcePath) ?? string.Empty).TrimStart('.');
|
||||
|
||||
var pattern = endingEpisodeNumber.HasValue ? options.MultiEpisodeNamePattern : options.EpisodeNamePattern;
|
||||
|
||||
var result = pattern.Replace("%sn", seriesName)
|
||||
.Replace("%s.n", seriesName.Replace(" ", "."))
|
||||
.Replace("%s_n", seriesName.Replace(" ", "_"))
|
||||
.Replace("%s", seasonNumber.ToString(UsCulture))
|
||||
.Replace("%0s", seasonNumber.ToString("00", UsCulture))
|
||||
.Replace("%00s", seasonNumber.ToString("000", UsCulture))
|
||||
.Replace("%ext", sourceExtension)
|
||||
.Replace("%en", episodeTitle)
|
||||
.Replace("%e.n", episodeTitle.Replace(" ", "."))
|
||||
.Replace("%e_n", episodeTitle.Replace(" ", "_"));
|
||||
|
||||
if (endingEpisodeNumber.HasValue)
|
||||
{
|
||||
result = result.Replace("%ed", endingEpisodeNumber.Value.ToString(UsCulture))
|
||||
.Replace("%0ed", endingEpisodeNumber.Value.ToString("00", UsCulture))
|
||||
.Replace("%00ed", endingEpisodeNumber.Value.ToString("000", UsCulture));
|
||||
}
|
||||
|
||||
return result.Replace("%e", episodeNumber.ToString(UsCulture))
|
||||
.Replace("%0e", episodeNumber.ToString("00", UsCulture))
|
||||
.Replace("%00e", episodeNumber.ToString("000", UsCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the season folder path.
|
||||
/// </summary>
|
||||
/// <param name="series">The series.</param>
|
||||
/// <param name="seasonNumber">The season number.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private string GetSeasonFolderPath(Series series, int seasonNumber, TvFileOrganizationOptions options)
|
||||
{
|
||||
// If there's already a season folder, use that
|
||||
var season = series
|
||||
.RecursiveChildren
|
||||
.OfType<Season>()
|
||||
.FirstOrDefault(i => i.LocationType == LocationType.FileSystem && i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber);
|
||||
|
||||
if (season != null)
|
||||
{
|
||||
return season.Path;
|
||||
}
|
||||
|
||||
var path = series.Path;
|
||||
|
||||
if (series.ContainsEpisodesWithoutSeasonFolders)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
if (seasonNumber == 0)
|
||||
{
|
||||
return Path.Combine(path, _fileSystem.GetValidFilename(options.SeasonZeroFolderName));
|
||||
}
|
||||
|
||||
var seasonFolderName = options.SeasonFolderPattern
|
||||
.Replace("%s", seasonNumber.ToString(UsCulture))
|
||||
.Replace("%0s", seasonNumber.ToString("00", UsCulture))
|
||||
.Replace("%00s", seasonNumber.ToString("000", UsCulture));
|
||||
|
||||
return Path.Combine(path, _fileSystem.GetValidFilename(seasonFolderName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the matching series.
|
||||
/// </summary>
|
||||
/// <param name="seriesName">Name of the series.</param>
|
||||
/// <param name="allSeries">All series.</param>
|
||||
/// <returns>Series.</returns>
|
||||
private Series GetMatchingSeries(string seriesName, IEnumerable<Series> allSeries, FileOrganizationResult result)
|
||||
{
|
||||
int? yearInName;
|
||||
var nameWithoutYear = seriesName;
|
||||
NameParser.ParseName(nameWithoutYear, out nameWithoutYear, out yearInName);
|
||||
|
||||
result.ExtractedName = nameWithoutYear;
|
||||
result.ExtractedYear = yearInName;
|
||||
|
||||
return allSeries.Select(i => GetMatchScore(nameWithoutYear, yearInName, i))
|
||||
.Where(i => i.Item2 > 0)
|
||||
.OrderByDescending(i => i.Item2)
|
||||
.Select(i => i.Item1)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private Tuple<Series, int> GetMatchScore(string sortedName, int? year, Series series)
|
||||
{
|
||||
var score = 0;
|
||||
|
||||
if (IsNameMatch(sortedName, series.Name))
|
||||
{
|
||||
score++;
|
||||
|
||||
if (year.HasValue && series.ProductionYear.HasValue)
|
||||
{
|
||||
if (year.Value == series.ProductionYear.Value)
|
||||
{
|
||||
score++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Regardless of name, return a 0 score if the years don't match
|
||||
return new Tuple<Series, int>(series, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Tuple<Series, int>(series, score);
|
||||
}
|
||||
|
||||
private bool IsNameMatch(string name1, string name2)
|
||||
{
|
||||
name1 = GetComparableName(name1);
|
||||
name2 = GetComparableName(name2);
|
||||
|
||||
return string.Equals(name1, name2, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private string GetComparableName(string name)
|
||||
{
|
||||
// TODO: Improve this - should ignore spaces, periods, underscores, most likely all symbols and
|
||||
// possibly remove sorting words like "the", "and", etc.
|
||||
|
||||
name = RemoveDiacritics(name);
|
||||
|
||||
name = " " + name.ToLower() + " ";
|
||||
|
||||
name = name.Replace(".", " ")
|
||||
.Replace("_", " ")
|
||||
.Replace("&", " ")
|
||||
.Replace("!", " ")
|
||||
.Replace(",", " ")
|
||||
.Replace("-", " ")
|
||||
.Replace(" a ", string.Empty)
|
||||
.Replace(" the ", string.Empty)
|
||||
.Replace(" ", string.Empty);
|
||||
|
||||
return name.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the diacritics.
|
||||
/// </summary>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private string RemoveDiacritics(string text)
|
||||
{
|
||||
return string.Concat(
|
||||
text.Normalize(NormalizationForm.FormD)
|
||||
.Where(ch => CharUnicodeInfo.GetUnicodeCategory(ch) !=
|
||||
UnicodeCategory.NonSpacingMark)
|
||||
).Normalize(NormalizationForm.FormC);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the left over files.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="extensions">The extensions.</param>
|
||||
private void DeleteLeftOverFiles(string path, IEnumerable<string> extensions)
|
||||
{
|
||||
var eligibleFiles = new DirectoryInfo(path)
|
||||
.EnumerateFiles("*", SearchOption.AllDirectories)
|
||||
.Where(i => extensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
foreach (var file in eligibleFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(file.FullName);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.ErrorException("Error deleting file {0}", ex, file.FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the empty folders.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
private void DeleteEmptyFolders(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var d in Directory.EnumerateDirectories(path))
|
||||
{
|
||||
DeleteEmptyFolders(d);
|
||||
}
|
||||
|
||||
var entries = Directory.EnumerateFileSystemEntries(path);
|
||||
|
||||
if (!entries.Any())
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(path);
|
||||
}
|
||||
catch (UnauthorizedAccessException) { }
|
||||
catch (DirectoryNotFoundException) { }
|
||||
}
|
||||
}
|
||||
catch (UnauthorizedAccessException) { }
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,176 @@
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.FileOrganization;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.FileOrganization;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.FileOrganization
|
||||
{
|
||||
public class TvFolderOrganizer
|
||||
{
|
||||
private readonly IDirectoryWatchers _directoryWatchers;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IFileOrganizationService _organizationService;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
|
||||
public TvFolderOrganizer(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem, IDirectoryWatchers directoryWatchers, IFileOrganizationService organizationService, IServerConfigurationManager config)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
_directoryWatchers = directoryWatchers;
|
||||
_organizationService = organizationService;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public async Task Organize(TvFileOrganizationOptions options, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
var minFileBytes = options.MinFileSizeMb * 1024 * 1024;
|
||||
|
||||
var watchLocations = options.WatchLocations.ToList();
|
||||
|
||||
var eligibleFiles = watchLocations.SelectMany(GetFilesToOrganize)
|
||||
.OrderBy(_fileSystem.GetCreationTimeUtc)
|
||||
.Where(i => EntityResolutionHelper.IsVideoFile(i.FullName) && i.Length >= minFileBytes)
|
||||
.ToList();
|
||||
|
||||
progress.Report(10);
|
||||
|
||||
var scanLibrary = false;
|
||||
|
||||
if (eligibleFiles.Count > 0)
|
||||
{
|
||||
var numComplete = 0;
|
||||
|
||||
foreach (var file in eligibleFiles)
|
||||
{
|
||||
var organizer = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager,
|
||||
_directoryWatchers);
|
||||
|
||||
var result = await organizer.OrganizeEpisodeFile(file.FullName, options, false).ConfigureAwait(false);
|
||||
|
||||
if (result.Status == FileSortingStatus.Success)
|
||||
{
|
||||
scanLibrary = true;
|
||||
}
|
||||
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= eligibleFiles.Count;
|
||||
|
||||
progress.Report(10 + (89 * percent));
|
||||
}
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
progress.Report(99);
|
||||
|
||||
foreach (var path in watchLocations)
|
||||
{
|
||||
if (options.LeftOverFileExtensionsToDelete.Length > 0)
|
||||
{
|
||||
DeleteLeftOverFiles(path, options.LeftOverFileExtensionsToDelete);
|
||||
}
|
||||
|
||||
if (options.DeleteEmptyFolders)
|
||||
{
|
||||
DeleteEmptyFolders(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (scanLibrary)
|
||||
{
|
||||
await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the files to organize.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>IEnumerable{FileInfo}.</returns>
|
||||
private IEnumerable<FileInfo> GetFilesToOrganize(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new DirectoryInfo(path)
|
||||
.EnumerateFiles("*", SearchOption.AllDirectories)
|
||||
.ToList();
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.ErrorException("Error getting files from {0}", ex, path);
|
||||
|
||||
return new List<FileInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the left over files.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="extensions">The extensions.</param>
|
||||
private void DeleteLeftOverFiles(string path, IEnumerable<string> extensions)
|
||||
{
|
||||
var eligibleFiles = new DirectoryInfo(path)
|
||||
.EnumerateFiles("*", SearchOption.AllDirectories)
|
||||
.Where(i => extensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
foreach (var file in eligibleFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(file.FullName);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.ErrorException("Error deleting file {0}", ex, file.FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the empty folders.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
private void DeleteEmptyFolders(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var d in Directory.EnumerateDirectories(path))
|
||||
{
|
||||
DeleteEmptyFolders(d);
|
||||
}
|
||||
|
||||
var entries = Directory.EnumerateFileSystemEntries(path);
|
||||
|
||||
if (!entries.Any())
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(path);
|
||||
}
|
||||
catch (UnauthorizedAccessException) { }
|
||||
catch (DirectoryNotFoundException) { }
|
||||
}
|
||||
}
|
||||
catch (UnauthorizedAccessException) { }
|
||||
}
|
||||
}
|
||||
}
|
@ -19,20 +19,23 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.TV
|
||||
protected override Episode Resolve(ItemResolveArgs args)
|
||||
{
|
||||
var parent = args.Parent;
|
||||
|
||||
if (parent == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var season = parent as Season;
|
||||
|
||||
// Just in case the user decided to nest episodes.
|
||||
// Not officially supported but in some cases we can handle it.
|
||||
if (season == null)
|
||||
{
|
||||
if (parent != null)
|
||||
{
|
||||
season = parent.Parents.OfType<Season>().FirstOrDefault();
|
||||
}
|
||||
season = parent.Parents.OfType<Season>().FirstOrDefault();
|
||||
}
|
||||
|
||||
// If the parent is a Season or Series, then this is an Episode if the VideoResolver returns something
|
||||
if (season != null || args.Parent is Series)
|
||||
if (season != null || parent.Parents.OfType<Series>().Any())
|
||||
{
|
||||
Episode episode = null;
|
||||
|
||||
|
@ -117,8 +117,10 @@
|
||||
<Compile Include="EntryPoints\Notifications\RemoteNotifications.cs" />
|
||||
<Compile Include="EntryPoints\Notifications\WebSocketNotifier.cs" />
|
||||
<Compile Include="EntryPoints\RefreshUsersMetadata.cs" />
|
||||
<Compile Include="FileOrganization\EpisodeFileOrganizer.cs" />
|
||||
<Compile Include="FileOrganization\FileOrganizationService.cs" />
|
||||
<Compile Include="FileOrganization\TvFileSorter.cs" />
|
||||
<Compile Include="FileOrganization\NameUtils.cs" />
|
||||
<Compile Include="FileOrganization\TvFolderOrganizer.cs" />
|
||||
<Compile Include="EntryPoints\UdpServerEntryPoint.cs" />
|
||||
<Compile Include="EntryPoints\ServerEventNotifier.cs" />
|
||||
<Compile Include="EntryPoints\UserDataChangeNotifier.cs" />
|
||||
|
@ -956,7 +956,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
||||
/// <param name="process">The process.</param>
|
||||
/// <param name="timeout">The timeout.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
private bool StartAndWaitForProcess(Process process, int timeout = 10000)
|
||||
private bool StartAndWaitForProcess(Process process, int timeout = 12000)
|
||||
{
|
||||
process.Start();
|
||||
|
||||
|
@ -27,6 +27,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
|
||||
|
||||
private IDbCommand _saveResultCommand;
|
||||
private IDbCommand _deleteResultCommand;
|
||||
private IDbCommand _deleteAllCommand;
|
||||
|
||||
public SqliteFileOrganizationRepository(ILogManager logManager, IServerApplicationPaths appPaths)
|
||||
{
|
||||
@ -85,6 +86,9 @@ namespace MediaBrowser.Server.Implementations.Persistence
|
||||
_deleteResultCommand.CommandText = "delete from organizationresults where ResultId = @ResultId";
|
||||
|
||||
_deleteResultCommand.Parameters.Add(_saveResultCommand, "@ResultId");
|
||||
|
||||
_deleteAllCommand = _connection.CreateCommand();
|
||||
_deleteAllCommand.CommandText = "delete from organizationresults";
|
||||
}
|
||||
|
||||
public async Task SaveResult(FileOrganizationResult result, CancellationToken cancellationToken)
|
||||
@ -188,7 +192,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.ErrorException("Failed to save FileOrganizationResult:", e);
|
||||
_logger.ErrorException("Failed to delete FileOrganizationResult:", e);
|
||||
|
||||
if (transaction != null)
|
||||
{
|
||||
@ -208,6 +212,53 @@ namespace MediaBrowser.Server.Implementations.Persistence
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteAll()
|
||||
{
|
||||
await _writeLock.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
IDbTransaction transaction = null;
|
||||
|
||||
try
|
||||
{
|
||||
transaction = _connection.BeginTransaction();
|
||||
|
||||
_deleteAllCommand.Transaction = transaction;
|
||||
|
||||
_deleteAllCommand.ExecuteNonQuery();
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
if (transaction != null)
|
||||
{
|
||||
transaction.Rollback();
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.ErrorException("Failed to delete results", e);
|
||||
|
||||
if (transaction != null)
|
||||
{
|
||||
transaction.Rollback();
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (transaction != null)
|
||||
{
|
||||
transaction.Dispose();
|
||||
}
|
||||
|
||||
_writeLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public QueryResult<FileOrganizationResult> GetResults(FileOrganizationResultQuery query)
|
||||
{
|
||||
if (query == null)
|
||||
|
@ -294,7 +294,7 @@ namespace MediaBrowser.ServerApplication
|
||||
var newsService = new Server.Implementations.News.NewsService(ApplicationPaths, JsonSerializer);
|
||||
RegisterSingleInstance<INewsService>(newsService);
|
||||
|
||||
var fileOrganizationService = new FileOrganizationService(TaskManager, FileOrganizationRepository, Logger, DirectoryWatchers, LibraryManager);
|
||||
var fileOrganizationService = new FileOrganizationService(TaskManager, FileOrganizationRepository, Logger, DirectoryWatchers, LibraryManager, ServerConfigurationManager, FileSystemManager);
|
||||
RegisterSingleInstance<IFileOrganizationService>(fileOrganizationService);
|
||||
|
||||
progress.Report(15);
|
||||
|
@ -134,12 +134,13 @@
|
||||
<Reference Include="ServiceStack.Interfaces">
|
||||
<HintPath>..\ThirdParty\ServiceStack\ServiceStack.Interfaces.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SimpleInjector, Version=2.4.0.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL">
|
||||
<Reference Include="SimpleInjector, Version=2.4.1.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\SimpleInjector.2.4.0\lib\net45\SimpleInjector.dll</HintPath>
|
||||
<HintPath>..\packages\SimpleInjector.2.4.1\lib\net45\SimpleInjector.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SimpleInjector.Diagnostics">
|
||||
<HintPath>..\packages\SimpleInjector.2.4.0\lib\net45\SimpleInjector.Diagnostics.dll</HintPath>
|
||||
<Reference Include="SimpleInjector.Diagnostics, Version=2.4.1.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\SimpleInjector.2.4.1\lib\net45\SimpleInjector.Diagnostics.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Configuration.Install" />
|
||||
|
@ -3,5 +3,5 @@
|
||||
<package id="Hardcodet.Wpf.TaskbarNotification" version="1.0.4.0" targetFramework="net45" />
|
||||
<package id="MediaBrowser.IsoMounting" version="3.0.65" targetFramework="net45" />
|
||||
<package id="NLog" version="2.1.0" targetFramework="net45" />
|
||||
<package id="SimpleInjector" version="2.4.0" targetFramework="net45" />
|
||||
<package id="SimpleInjector" version="2.4.1" targetFramework="net45" />
|
||||
</packages>
|
@ -687,6 +687,16 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi
|
||||
});
|
||||
};
|
||||
|
||||
self.clearOrganizationLog = function () {
|
||||
|
||||
var url = self.getUrl("Library/FileOrganizations");
|
||||
|
||||
return self.ajax({
|
||||
type: "DELETE",
|
||||
url: url
|
||||
});
|
||||
};
|
||||
|
||||
self.performOrganization = function (id) {
|
||||
|
||||
var url = self.getUrl("Library/FileOrganizations/" + id + "/Organize");
|
||||
@ -697,6 +707,16 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi
|
||||
});
|
||||
};
|
||||
|
||||
self.performEpisodeOrganization = function (id, options) {
|
||||
|
||||
var url = self.getUrl("Library/FileOrganizations/" + id + "/Episode/Organize", options || {});
|
||||
|
||||
return self.ajax({
|
||||
type: "POST",
|
||||
url: url
|
||||
});
|
||||
};
|
||||
|
||||
self.getLiveTvSeriesTimer = function (id) {
|
||||
|
||||
if (!id) {
|
||||
@ -2984,7 +3004,14 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi
|
||||
throw new Error("null userId");
|
||||
}
|
||||
|
||||
var url = self.getUrl("Users/" + userId + "/Items", options);
|
||||
var url;
|
||||
|
||||
if ((typeof userId).toString().toLowerCase() == 'string') {
|
||||
url = self.getUrl("Users/" + userId + "/Items", options);
|
||||
} else {
|
||||
options = userId;
|
||||
url = self.getUrl("Items", options || {});
|
||||
}
|
||||
|
||||
return self.ajax({
|
||||
type: "GET",
|
||||
|
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="MediaBrowser.ApiClient.Javascript" version="3.0.240" targetFramework="net45" />
|
||||
<package id="MediaBrowser.ApiClient.Javascript" version="3.0.243" targetFramework="net45" />
|
||||
</packages>
|
Loading…
Reference in New Issue
Block a user