move auto-organize to plugin
This commit is contained in:
parent
6652d856f9
commit
d270b10db6
|
@ -17,7 +17,6 @@ using MediaBrowser.Controller.Dlna;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.FileOrganization;
|
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.LiveTv;
|
using MediaBrowser.Controller.LiveTv;
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
|
@ -94,7 +93,6 @@ using Emby.Server.Implementations.Channels;
|
||||||
using Emby.Server.Implementations.Collections;
|
using Emby.Server.Implementations.Collections;
|
||||||
using Emby.Server.Implementations.Dto;
|
using Emby.Server.Implementations.Dto;
|
||||||
using Emby.Server.Implementations.IO;
|
using Emby.Server.Implementations.IO;
|
||||||
using Emby.Server.Implementations.FileOrganization;
|
|
||||||
using Emby.Server.Implementations.HttpServer;
|
using Emby.Server.Implementations.HttpServer;
|
||||||
using Emby.Server.Implementations.HttpServer.Security;
|
using Emby.Server.Implementations.HttpServer.Security;
|
||||||
using Emby.Server.Implementations.Library;
|
using Emby.Server.Implementations.Library;
|
||||||
|
@ -216,7 +214,6 @@ namespace Emby.Server.Core
|
||||||
internal IDisplayPreferencesRepository DisplayPreferencesRepository { get; set; }
|
internal IDisplayPreferencesRepository DisplayPreferencesRepository { get; set; }
|
||||||
internal IItemRepository ItemRepository { get; set; }
|
internal IItemRepository ItemRepository { get; set; }
|
||||||
private INotificationsRepository NotificationsRepository { get; set; }
|
private INotificationsRepository NotificationsRepository { get; set; }
|
||||||
private IFileOrganizationRepository FileOrganizationRepository { get; set; }
|
|
||||||
|
|
||||||
private INotificationManager NotificationManager { get; set; }
|
private INotificationManager NotificationManager { get; set; }
|
||||||
private ISubtitleManager SubtitleManager { get; set; }
|
private ISubtitleManager SubtitleManager { get; set; }
|
||||||
|
@ -583,9 +580,6 @@ namespace Emby.Server.Core
|
||||||
ItemRepository = itemRepo;
|
ItemRepository = itemRepo;
|
||||||
RegisterSingleInstance(ItemRepository);
|
RegisterSingleInstance(ItemRepository);
|
||||||
|
|
||||||
FileOrganizationRepository = GetFileOrganizationRepository();
|
|
||||||
RegisterSingleInstance(FileOrganizationRepository);
|
|
||||||
|
|
||||||
AuthenticationRepository = GetAuthenticationRepository();
|
AuthenticationRepository = GetAuthenticationRepository();
|
||||||
RegisterSingleInstance(AuthenticationRepository);
|
RegisterSingleInstance(AuthenticationRepository);
|
||||||
|
|
||||||
|
@ -644,9 +638,6 @@ namespace Emby.Server.Core
|
||||||
var newsService = new Emby.Server.Implementations.News.NewsService(ApplicationPaths, JsonSerializer);
|
var newsService = new Emby.Server.Implementations.News.NewsService(ApplicationPaths, JsonSerializer);
|
||||||
RegisterSingleInstance<INewsService>(newsService);
|
RegisterSingleInstance<INewsService>(newsService);
|
||||||
|
|
||||||
var fileOrganizationService = new FileOrganizationService(TaskManager, FileOrganizationRepository, LogManager.GetLogger("FileOrganizationService"), LibraryMonitor, LibraryManager, ServerConfigurationManager, FileSystemManager, ProviderManager);
|
|
||||||
RegisterSingleInstance<IFileOrganizationService>(fileOrganizationService);
|
|
||||||
|
|
||||||
progress.Report(15);
|
progress.Report(15);
|
||||||
|
|
||||||
ChannelManager = new ChannelManager(UserManager, DtoService, LibraryManager, LogManager.GetLogger("ChannelManager"), ServerConfigurationManager, FileSystemManager, UserDataManager, JsonSerializer, LocalizationManager, HttpClient, ProviderManager);
|
ChannelManager = new ChannelManager(UserManager, DtoService, LibraryManager, LogManager.GetLogger("ChannelManager"), ServerConfigurationManager, FileSystemManager, UserDataManager, JsonSerializer, LocalizationManager, HttpClient, ProviderManager);
|
||||||
|
@ -932,19 +923,6 @@ namespace Emby.Server.Core
|
||||||
return repo;
|
return repo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the file organization repository.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Task{IUserRepository}.</returns>
|
|
||||||
private IFileOrganizationRepository GetFileOrganizationRepository()
|
|
||||||
{
|
|
||||||
var repo = new SqliteFileOrganizationRepository(LogManager.GetLogger("SqliteFileOrganizationRepository"), ServerConfigurationManager.ApplicationPaths);
|
|
||||||
|
|
||||||
repo.Initialize();
|
|
||||||
|
|
||||||
return repo;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IAuthenticationRepository GetAuthenticationRepository()
|
private IAuthenticationRepository GetAuthenticationRepository()
|
||||||
{
|
{
|
||||||
var repo = new AuthenticationRepository(LogManager.GetLogger("AuthenticationRepository"), ServerConfigurationManager.ApplicationPaths);
|
var repo = new AuthenticationRepository(LogManager.GetLogger("AuthenticationRepository"), ServerConfigurationManager.ApplicationPaths);
|
||||||
|
|
|
@ -1,284 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Controller;
|
|
||||||
using MediaBrowser.Controller.Persistence;
|
|
||||||
using MediaBrowser.Model.FileOrganization;
|
|
||||||
using MediaBrowser.Model.Logging;
|
|
||||||
using MediaBrowser.Model.Querying;
|
|
||||||
using SQLitePCL.pretty;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Data
|
|
||||||
{
|
|
||||||
public class SqliteFileOrganizationRepository : BaseSqliteRepository, IFileOrganizationRepository, IDisposable
|
|
||||||
{
|
|
||||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
|
||||||
|
|
||||||
public SqliteFileOrganizationRepository(ILogger logger, IServerApplicationPaths appPaths) : base(logger)
|
|
||||||
{
|
|
||||||
DbFilePath = Path.Combine(appPaths.DataPath, "fileorganization.db");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Opens the connection to the database
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
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)"
|
|
||||||
};
|
|
||||||
|
|
||||||
connection.RunQueries(queries);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SaveResult(FileOrganizationResult result, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (result == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("result");
|
|
||||||
}
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
using (WriteLock.Write())
|
|
||||||
{
|
|
||||||
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))
|
|
||||||
{
|
|
||||||
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 async Task Delete(string id)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(id))
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("id");
|
|
||||||
}
|
|
||||||
|
|
||||||
using (WriteLock.Write())
|
|
||||||
{
|
|
||||||
using (var connection = CreateConnection())
|
|
||||||
{
|
|
||||||
connection.RunInTransaction(db =>
|
|
||||||
{
|
|
||||||
using (var statement = db.PrepareStatement("delete from FileOrganizerResults where ResultId = @ResultId"))
|
|
||||||
{
|
|
||||||
statement.TryBind("@ResultId", id.ToGuidBlob());
|
|
||||||
statement.MoveNext();
|
|
||||||
}
|
|
||||||
}, TransactionMode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteAll()
|
|
||||||
{
|
|
||||||
using (WriteLock.Write())
|
|
||||||
{
|
|
||||||
using (var connection = CreateConnection())
|
|
||||||
{
|
|
||||||
connection.RunInTransaction(db =>
|
|
||||||
{
|
|
||||||
var commandText = "delete from FileOrganizerResults";
|
|
||||||
|
|
||||||
db.Execute(commandText);
|
|
||||||
}, TransactionMode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public QueryResult<FileOrganizationResult> GetResults(FileOrganizationResultQuery query)
|
|
||||||
{
|
|
||||||
if (query == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("query");
|
|
||||||
}
|
|
||||||
|
|
||||||
using (WriteLock.Read())
|
|
||||||
{
|
|
||||||
using (var connection = CreateConnection(true))
|
|
||||||
{
|
|
||||||
var commandText = "SELECT ResultId, OriginalPath, TargetPath, FileLength, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber, DuplicatePaths from FileOrganizerResults";
|
|
||||||
|
|
||||||
if (query.StartIndex.HasValue && query.StartIndex.Value > 0)
|
|
||||||
{
|
|
||||||
commandText += string.Format(" WHERE ResultId NOT IN (SELECT ResultId FROM FileOrganizerResults ORDER BY OrganizationDate desc LIMIT {0})",
|
|
||||||
query.StartIndex.Value.ToString(_usCulture));
|
|
||||||
}
|
|
||||||
|
|
||||||
commandText += " ORDER BY OrganizationDate desc";
|
|
||||||
|
|
||||||
if (query.Limit.HasValue)
|
|
||||||
{
|
|
||||||
commandText += " LIMIT " + query.Limit.Value.ToString(_usCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
var list = new List<FileOrganizationResult>();
|
|
||||||
|
|
||||||
using (var statement = connection.PrepareStatement(commandText))
|
|
||||||
{
|
|
||||||
foreach (var row in statement.ExecuteQuery())
|
|
||||||
{
|
|
||||||
list.Add(GetResult(row));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int count;
|
|
||||||
using (var statement = connection.PrepareStatement("select count (ResultId) from FileOrganizerResults"))
|
|
||||||
{
|
|
||||||
count = statement.ExecuteQuery().SelectScalarInt().First();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new QueryResult<FileOrganizationResult>()
|
|
||||||
{
|
|
||||||
Items = list.ToArray(),
|
|
||||||
TotalRecordCount = count
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public FileOrganizationResult GetResult(string id)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(id))
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("id");
|
|
||||||
}
|
|
||||||
|
|
||||||
using (WriteLock.Read())
|
|
||||||
{
|
|
||||||
using (var connection = CreateConnection(true))
|
|
||||||
{
|
|
||||||
using (var statement = connection.PrepareStatement("select ResultId, OriginalPath, TargetPath, FileLength, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber, DuplicatePaths from FileOrganizerResults where ResultId=@ResultId"))
|
|
||||||
{
|
|
||||||
statement.TryBind("@ResultId", id.ToGuidBlob());
|
|
||||||
|
|
||||||
foreach (var row in statement.ExecuteQuery())
|
|
||||||
{
|
|
||||||
return GetResult(row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public FileOrganizationResult GetResult(IReadOnlyList<IResultSetValue> reader)
|
|
||||||
{
|
|
||||||
var index = 0;
|
|
||||||
|
|
||||||
var result = new FileOrganizationResult
|
|
||||||
{
|
|
||||||
Id = reader[0].ReadGuidFromBlob().ToString("N")
|
|
||||||
};
|
|
||||||
|
|
||||||
index++;
|
|
||||||
if (reader[index].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
result.OriginalPath = reader[index].ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
index++;
|
|
||||||
if (reader[index].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
result.TargetPath = reader[index].ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
index++;
|
|
||||||
result.FileSize = reader[index].ToInt64();
|
|
||||||
|
|
||||||
index++;
|
|
||||||
result.Date = reader[index].ReadDateTime();
|
|
||||||
|
|
||||||
index++;
|
|
||||||
result.Status = (FileSortingStatus)Enum.Parse(typeof(FileSortingStatus), reader[index].ToString(), true);
|
|
||||||
|
|
||||||
index++;
|
|
||||||
result.Type = (FileOrganizerType)Enum.Parse(typeof(FileOrganizerType), reader[index].ToString(), true);
|
|
||||||
|
|
||||||
index++;
|
|
||||||
if (reader[index].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
result.StatusMessage = reader[index].ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
result.OriginalFileName = Path.GetFileName(result.OriginalPath);
|
|
||||||
|
|
||||||
index++;
|
|
||||||
if (reader[index].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
result.ExtractedName = reader[index].ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
index++;
|
|
||||||
if (reader[index].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
result.ExtractedYear = reader[index].ToInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
index++;
|
|
||||||
if (reader[index].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
result.ExtractedSeasonNumber = reader[index].ToInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
index++;
|
|
||||||
if (reader[index].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
result.ExtractedEpisodeNumber = reader[index].ToInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
index++;
|
|
||||||
if (reader[index].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
result.ExtractedEndingEpisodeNumber = reader[index].ToInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
index++;
|
|
||||||
if (reader[index].SQLiteType != SQLiteType.Null)
|
|
||||||
{
|
|
||||||
result.DuplicatePaths = reader[index].ToString().Split('|').Where(i => !string.IsNullOrEmpty(i)).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -54,7 +54,6 @@
|
||||||
<Compile Include="Configuration\ServerConfigurationManager.cs" />
|
<Compile Include="Configuration\ServerConfigurationManager.cs" />
|
||||||
<Compile Include="Data\ManagedConnection.cs" />
|
<Compile Include="Data\ManagedConnection.cs" />
|
||||||
<Compile Include="Data\SqliteDisplayPreferencesRepository.cs" />
|
<Compile Include="Data\SqliteDisplayPreferencesRepository.cs" />
|
||||||
<Compile Include="Data\SqliteFileOrganizationRepository.cs" />
|
|
||||||
<Compile Include="Data\SqliteItemRepository.cs" />
|
<Compile Include="Data\SqliteItemRepository.cs" />
|
||||||
<Compile Include="Data\SqliteUserDataRepository.cs" />
|
<Compile Include="Data\SqliteUserDataRepository.cs" />
|
||||||
<Compile Include="Data\SqliteUserRepository.cs" />
|
<Compile Include="Data\SqliteUserRepository.cs" />
|
||||||
|
@ -79,13 +78,6 @@
|
||||||
<Compile Include="FFMpeg\FFMpegInfo.cs" />
|
<Compile Include="FFMpeg\FFMpegInfo.cs" />
|
||||||
<Compile Include="FFMpeg\FFMpegInstallInfo.cs" />
|
<Compile Include="FFMpeg\FFMpegInstallInfo.cs" />
|
||||||
<Compile Include="FFMpeg\FFMpegLoader.cs" />
|
<Compile Include="FFMpeg\FFMpegLoader.cs" />
|
||||||
<Compile Include="FileOrganization\EpisodeFileOrganizer.cs" />
|
|
||||||
<Compile Include="FileOrganization\Extensions.cs" />
|
|
||||||
<Compile Include="FileOrganization\FileOrganizationNotifier.cs" />
|
|
||||||
<Compile Include="FileOrganization\FileOrganizationService.cs" />
|
|
||||||
<Compile Include="FileOrganization\NameUtils.cs" />
|
|
||||||
<Compile Include="FileOrganization\OrganizerScheduledTask.cs" />
|
|
||||||
<Compile Include="FileOrganization\TvFolderOrganizer.cs" />
|
|
||||||
<Compile Include="HttpServer\FileWriter.cs" />
|
<Compile Include="HttpServer\FileWriter.cs" />
|
||||||
<Compile Include="HttpServer\HttpListenerHost.cs" />
|
<Compile Include="HttpServer\HttpListenerHost.cs" />
|
||||||
<Compile Include="HttpServer\HttpResultFactory.cs" />
|
<Compile Include="HttpServer\HttpResultFactory.cs" />
|
||||||
|
|
|
@ -1,813 +0,0 @@
|
||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
|
||||||
using MediaBrowser.Controller.FileOrganization;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller.Providers;
|
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using MediaBrowser.Model.Extensions;
|
|
||||||
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.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Emby.Server.Implementations.Library;
|
|
||||||
using MediaBrowser.Controller.Dto;
|
|
||||||
using MediaBrowser.Controller.Entities;
|
|
||||||
using MediaBrowser.Controller.IO;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using MediaBrowser.Naming.TV;
|
|
||||||
using EpisodeInfo = MediaBrowser.Controller.Providers.EpisodeInfo;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.FileOrganization
|
|
||||||
{
|
|
||||||
public class EpisodeFileOrganizer
|
|
||||||
{
|
|
||||||
private readonly ILibraryMonitor _libraryMonitor;
|
|
||||||
private readonly ILibraryManager _libraryManager;
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly IFileSystem _fileSystem;
|
|
||||||
private readonly IFileOrganizationService _organizationService;
|
|
||||||
private readonly IServerConfigurationManager _config;
|
|
||||||
private readonly IProviderManager _providerManager;
|
|
||||||
|
|
||||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
|
||||||
|
|
||||||
public EpisodeFileOrganizer(IFileOrganizationService organizationService, IServerConfigurationManager config, IFileSystem fileSystem, ILogger logger, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager)
|
|
||||||
{
|
|
||||||
_organizationService = organizationService;
|
|
||||||
_config = config;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_logger = logger;
|
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_libraryMonitor = libraryMonitor;
|
|
||||||
_providerManager = providerManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<FileOrganizationResult> OrganizeEpisodeFile(string path, AutoOrganizeOptions options, bool overwriteExisting, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_logger.Info("Sorting file {0}", path);
|
|
||||||
|
|
||||||
var result = new FileOrganizationResult
|
|
||||||
{
|
|
||||||
Date = DateTime.UtcNow,
|
|
||||||
OriginalPath = path,
|
|
||||||
OriginalFileName = Path.GetFileName(path),
|
|
||||||
Type = FileOrganizerType.Episode,
|
|
||||||
FileSize = _fileSystem.GetFileInfo(path).Length
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_libraryMonitor.IsPathLocked(path))
|
|
||||||
{
|
|
||||||
result.Status = FileSortingStatus.Failure;
|
|
||||||
result.StatusMessage = "Path is locked by other processes. Please try again later.";
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
|
|
||||||
var resolver = new EpisodeResolver(namingOptions, new NullLogger());
|
|
||||||
|
|
||||||
var episodeInfo = resolver.Resolve(path, false) ??
|
|
||||||
new MediaBrowser.Naming.TV.EpisodeInfo();
|
|
||||||
|
|
||||||
var seriesName = episodeInfo.SeriesName;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(seriesName))
|
|
||||||
{
|
|
||||||
var seasonNumber = episodeInfo.SeasonNumber;
|
|
||||||
|
|
||||||
result.ExtractedSeasonNumber = seasonNumber;
|
|
||||||
|
|
||||||
// Passing in true will include a few extra regex's
|
|
||||||
var episodeNumber = episodeInfo.EpisodeNumber;
|
|
||||||
|
|
||||||
result.ExtractedEpisodeNumber = episodeNumber;
|
|
||||||
|
|
||||||
var premiereDate = episodeInfo.IsByDate ?
|
|
||||||
new DateTime(episodeInfo.Year.Value, episodeInfo.Month.Value, episodeInfo.Day.Value) :
|
|
||||||
(DateTime?)null;
|
|
||||||
|
|
||||||
if (episodeInfo.IsByDate || (seasonNumber.HasValue && episodeNumber.HasValue))
|
|
||||||
{
|
|
||||||
if (episodeInfo.IsByDate)
|
|
||||||
{
|
|
||||||
_logger.Debug("Extracted information from {0}. Series name {1}, Date {2}", path, seriesName, premiereDate.Value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.Debug("Extracted information from {0}. Series name {1}, Season {2}, Episode {3}", path, seriesName, seasonNumber, episodeNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
var endingEpisodeNumber = episodeInfo.EndingEpsiodeNumber;
|
|
||||||
|
|
||||||
result.ExtractedEndingEpisodeNumber = endingEpisodeNumber;
|
|
||||||
|
|
||||||
await OrganizeEpisode(path,
|
|
||||||
seriesName,
|
|
||||||
seasonNumber,
|
|
||||||
episodeNumber,
|
|
||||||
endingEpisodeNumber,
|
|
||||||
premiereDate,
|
|
||||||
options,
|
|
||||||
overwriteExisting,
|
|
||||||
false,
|
|
||||||
result,
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
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 series name from {0}", path);
|
|
||||||
result.Status = FileSortingStatus.Failure;
|
|
||||||
result.StatusMessage = msg;
|
|
||||||
_logger.Warn(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
var previousResult = _organizationService.GetResultBySourcePath(path);
|
|
||||||
|
|
||||||
if (previousResult != null)
|
|
||||||
{
|
|
||||||
// Don't keep saving the same result over and over if nothing has changed
|
|
||||||
if (previousResult.Status == result.Status && previousResult.StatusMessage == result.StatusMessage && result.Status != FileSortingStatus.Success)
|
|
||||||
{
|
|
||||||
return previousResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await _organizationService.SaveResult(result, CancellationToken.None).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
result.Status = FileSortingStatus.Failure;
|
|
||||||
result.StatusMessage = ex.Message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<FileOrganizationResult> OrganizeWithCorrection(EpisodeFileOrganizationRequest request, AutoOrganizeOptions options, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var result = _organizationService.GetResult(request.ResultId);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Series series = null;
|
|
||||||
|
|
||||||
if (request.NewSeriesProviderIds.Count > 0)
|
|
||||||
{
|
|
||||||
// We're having a new series here
|
|
||||||
SeriesInfo seriesRequest = new SeriesInfo();
|
|
||||||
seriesRequest.ProviderIds = request.NewSeriesProviderIds;
|
|
||||||
|
|
||||||
var refreshOptions = new MetadataRefreshOptions(_fileSystem);
|
|
||||||
series = new Series();
|
|
||||||
series.Id = Guid.NewGuid();
|
|
||||||
series.Name = request.NewSeriesName;
|
|
||||||
|
|
||||||
int year;
|
|
||||||
if (int.TryParse(request.NewSeriesYear, out year))
|
|
||||||
{
|
|
||||||
series.ProductionYear = year;
|
|
||||||
}
|
|
||||||
|
|
||||||
var seriesFolderName = series.Name;
|
|
||||||
if (series.ProductionYear.HasValue)
|
|
||||||
{
|
|
||||||
seriesFolderName = string.Format("{0} ({1})", seriesFolderName, series.ProductionYear);
|
|
||||||
}
|
|
||||||
|
|
||||||
seriesFolderName = _fileSystem.GetValidFilename(seriesFolderName);
|
|
||||||
|
|
||||||
series.Path = Path.Combine(request.TargetFolder, seriesFolderName);
|
|
||||||
|
|
||||||
series.ProviderIds = request.NewSeriesProviderIds;
|
|
||||||
|
|
||||||
await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (series == null)
|
|
||||||
{
|
|
||||||
// Existing Series
|
|
||||||
series = (Series)_libraryManager.GetItemById(new Guid(request.SeriesId));
|
|
||||||
}
|
|
||||||
|
|
||||||
await OrganizeEpisode(result.OriginalPath,
|
|
||||||
series,
|
|
||||||
request.SeasonNumber,
|
|
||||||
request.EpisodeNumber,
|
|
||||||
request.EndingEpisodeNumber,
|
|
||||||
null,
|
|
||||||
options,
|
|
||||||
true,
|
|
||||||
request.RememberCorrection,
|
|
||||||
result,
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
await _organizationService.SaveResult(result, CancellationToken.None).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
result.Status = FileSortingStatus.Failure;
|
|
||||||
result.StatusMessage = ex.Message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task OrganizeEpisode(string sourcePath,
|
|
||||||
string seriesName,
|
|
||||||
int? seasonNumber,
|
|
||||||
int? episodeNumber,
|
|
||||||
int? endingEpiosdeNumber,
|
|
||||||
DateTime? premiereDate,
|
|
||||||
AutoOrganizeOptions options,
|
|
||||||
bool overwriteExisting,
|
|
||||||
bool rememberCorrection,
|
|
||||||
FileOrganizationResult result,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var series = GetMatchingSeries(seriesName, result, options);
|
|
||||||
|
|
||||||
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 Task.FromResult(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return OrganizeEpisode(sourcePath,
|
|
||||||
series,
|
|
||||||
seasonNumber,
|
|
||||||
episodeNumber,
|
|
||||||
endingEpiosdeNumber,
|
|
||||||
premiereDate,
|
|
||||||
options,
|
|
||||||
overwriteExisting,
|
|
||||||
rememberCorrection,
|
|
||||||
result,
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OrganizeEpisode(string sourcePath,
|
|
||||||
Series series,
|
|
||||||
int? seasonNumber,
|
|
||||||
int? episodeNumber,
|
|
||||||
int? endingEpiosdeNumber,
|
|
||||||
DateTime? premiereDate,
|
|
||||||
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;
|
|
||||||
|
|
||||||
bool isNew = string.IsNullOrWhiteSpace(result.Id);
|
|
||||||
|
|
||||||
if (isNew)
|
|
||||||
{
|
|
||||||
await _organizationService.SaveResult(result, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_organizationService.AddToInProgressList(result, isNew))
|
|
||||||
{
|
|
||||||
throw new Exception("File is currently processed otherwise. Please try again later.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Proceed to sort the file
|
|
||||||
var newPath = await GetNewPath(sourcePath, series, seasonNumber, episodeNumber, endingEpiosdeNumber, premiereDate, options.TvOptions, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(newPath))
|
|
||||||
{
|
|
||||||
var msg = string.Format("Unable to sort {0} because target path could not be determined.", sourcePath);
|
|
||||||
throw new Exception(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.Info("Sorting file {0} to new path {1}", sourcePath, newPath);
|
|
||||||
result.TargetPath = newPath;
|
|
||||||
|
|
||||||
var fileExists = _fileSystem.FileExists(result.TargetPath);
|
|
||||||
var otherDuplicatePaths = GetOtherDuplicatePaths(result.TargetPath, series, seasonNumber, episodeNumber, endingEpiosdeNumber);
|
|
||||||
|
|
||||||
if (!overwriteExisting)
|
|
||||||
{
|
|
||||||
if (options.TvOptions.CopyOriginalFile && fileExists && IsSameEpisode(sourcePath, newPath))
|
|
||||||
{
|
|
||||||
var msg = string.Format("File '{0}' already copied to new path '{1}', stopping organization", sourcePath, newPath);
|
|
||||||
_logger.Info(msg);
|
|
||||||
result.Status = FileSortingStatus.SkippedExisting;
|
|
||||||
result.StatusMessage = msg;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileExists)
|
|
||||||
{
|
|
||||||
var msg = string.Format("File '{0}' already exists as '{1}', stopping organization", sourcePath, newPath);
|
|
||||||
_logger.Info(msg);
|
|
||||||
result.Status = FileSortingStatus.SkippedExisting;
|
|
||||||
result.StatusMessage = msg;
|
|
||||||
result.TargetPath = newPath;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (otherDuplicatePaths.Count > 0)
|
|
||||||
{
|
|
||||||
var msg = string.Format("File '{0}' already exists as these:'{1}'. Stopping organization", sourcePath, string.Join("', '", otherDuplicatePaths));
|
|
||||||
_logger.Info(msg);
|
|
||||||
result.Status = FileSortingStatus.SkippedExisting;
|
|
||||||
result.StatusMessage = msg;
|
|
||||||
result.DuplicatePaths = otherDuplicatePaths;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PerformFileSorting(options.TvOptions, result);
|
|
||||||
|
|
||||||
if (overwriteExisting)
|
|
||||||
{
|
|
||||||
var hasRenamedFiles = false;
|
|
||||||
|
|
||||||
foreach (var path in otherDuplicatePaths)
|
|
||||||
{
|
|
||||||
_logger.Debug("Removing duplicate episode {0}", path);
|
|
||||||
|
|
||||||
_libraryMonitor.ReportFileSystemChangeBeginning(path);
|
|
||||||
|
|
||||||
var renameRelatedFiles = !hasRenamedFiles &&
|
|
||||||
string.Equals(_fileSystem.GetDirectoryName(path), _fileSystem.GetDirectoryName(result.TargetPath), StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
if (renameRelatedFiles)
|
|
||||||
{
|
|
||||||
hasRenamedFiles = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
DeleteLibraryFile(path, renameRelatedFiles, result.TargetPath);
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
_logger.ErrorException("Error removing duplicate episode", ex, path);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_libraryMonitor.ReportFileSystemChangeComplete(path, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
result.Status = FileSortingStatus.Failure;
|
|
||||||
result.StatusMessage = ex.Message;
|
|
||||||
_logger.Warn(ex.Message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_organizationService.RemoveFromInprogressList(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rememberCorrection)
|
|
||||||
{
|
|
||||||
SaveSmartMatchString(originalExtractedSeriesString, series, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SaveSmartMatchString(string matchString, Series series, AutoOrganizeOptions options)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(matchString) || matchString.Length < 3)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SmartMatchInfo info = options.SmartMatchInfos.FirstOrDefault(i => string.Equals(i.ItemName, series.Name, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (info == null)
|
|
||||||
{
|
|
||||||
info = new SmartMatchInfo();
|
|
||||||
info.ItemName = series.Name;
|
|
||||||
info.OrganizerType = FileOrganizerType.Episode;
|
|
||||||
info.DisplayName = series.Name;
|
|
||||||
var list = options.SmartMatchInfos.ToList();
|
|
||||||
list.Add(info);
|
|
||||||
options.SmartMatchInfos = list.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!info.MatchStrings.Contains(matchString, StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var list = info.MatchStrings.ToList();
|
|
||||||
list.Add(matchString);
|
|
||||||
info.MatchStrings = list.ToArray();
|
|
||||||
_config.SaveAutoOrganizeOptions(options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DeleteLibraryFile(string path, bool renameRelatedFiles, string targetPath)
|
|
||||||
{
|
|
||||||
_fileSystem.DeleteFile(path);
|
|
||||||
|
|
||||||
if (!renameRelatedFiles)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now find other files
|
|
||||||
var originalFilenameWithoutExtension = Path.GetFileNameWithoutExtension(path);
|
|
||||||
var directory = _fileSystem.GetDirectoryName(path);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(originalFilenameWithoutExtension) && !string.IsNullOrWhiteSpace(directory))
|
|
||||||
{
|
|
||||||
// Get all related files, e.g. metadata, images, etc
|
|
||||||
var files = _fileSystem.GetFilePaths(directory)
|
|
||||||
.Where(i => (Path.GetFileNameWithoutExtension(i) ?? string.Empty).StartsWith(originalFilenameWithoutExtension, StringComparison.OrdinalIgnoreCase))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var targetFilenameWithoutExtension = Path.GetFileNameWithoutExtension(targetPath);
|
|
||||||
|
|
||||||
foreach (var file in files)
|
|
||||||
{
|
|
||||||
directory = _fileSystem.GetDirectoryName(file);
|
|
||||||
var filename = Path.GetFileName(file);
|
|
||||||
|
|
||||||
filename = filename.Replace(originalFilenameWithoutExtension, targetFilenameWithoutExtension,
|
|
||||||
StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
var destination = Path.Combine(directory, filename);
|
|
||||||
|
|
||||||
_fileSystem.MoveFile(file, destination);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<string> GetOtherDuplicatePaths(string targetPath,
|
|
||||||
Series series,
|
|
||||||
int? seasonNumber,
|
|
||||||
int? episodeNumber,
|
|
||||||
int? endingEpisodeNumber)
|
|
||||||
{
|
|
||||||
// TODO: Support date-naming?
|
|
||||||
if (!seasonNumber.HasValue || !episodeNumber.HasValue)
|
|
||||||
{
|
|
||||||
return new List<string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
var episodePaths = series.GetRecursiveChildren(i => i is Episode)
|
|
||||||
.OfType<Episode>()
|
|
||||||
.Where(i =>
|
|
||||||
{
|
|
||||||
var locationType = i.LocationType;
|
|
||||||
|
|
||||||
// Must be file system based and match exactly
|
|
||||||
if (locationType != LocationType.Remote &&
|
|
||||||
locationType != LocationType.Virtual &&
|
|
||||||
i.ParentIndexNumber.HasValue &&
|
|
||||||
i.ParentIndexNumber.Value == seasonNumber &&
|
|
||||||
i.IndexNumber.HasValue &&
|
|
||||||
i.IndexNumber.Value == episodeNumber)
|
|
||||||
{
|
|
||||||
|
|
||||||
if (endingEpisodeNumber.HasValue || i.IndexNumberEnd.HasValue)
|
|
||||||
{
|
|
||||||
return endingEpisodeNumber.HasValue && i.IndexNumberEnd.HasValue &&
|
|
||||||
endingEpisodeNumber.Value == i.IndexNumberEnd.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
.Select(i => i.Path)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var folder = _fileSystem.GetDirectoryName(targetPath);
|
|
||||||
var targetFileNameWithoutExtension = _fileSystem.GetFileNameWithoutExtension(targetPath);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var filesOfOtherExtensions = _fileSystem.GetFilePaths(folder)
|
|
||||||
.Where(i => _libraryManager.IsVideoFile(i) && string.Equals(_fileSystem.GetFileNameWithoutExtension(i), targetFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
episodePaths.AddRange(filesOfOtherExtensions);
|
|
||||||
}
|
|
||||||
catch (IOException)
|
|
||||||
{
|
|
||||||
// No big deal. Maybe the season folder doesn't already exist.
|
|
||||||
}
|
|
||||||
|
|
||||||
return episodePaths.Where(i => !string.Equals(i, targetPath, StringComparison.OrdinalIgnoreCase))
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PerformFileSorting(TvFileOrganizationOptions options, FileOrganizationResult result)
|
|
||||||
{
|
|
||||||
// We should probably handle this earlier so that we never even make it this far
|
|
||||||
if (string.Equals(result.OriginalPath, result.TargetPath, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_libraryMonitor.ReportFileSystemChangeBeginning(result.TargetPath);
|
|
||||||
|
|
||||||
_fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(result.TargetPath));
|
|
||||||
|
|
||||||
var targetAlreadyExists = _fileSystem.FileExists(result.TargetPath);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (targetAlreadyExists || options.CopyOriginalFile)
|
|
||||||
{
|
|
||||||
_fileSystem.CopyFile(result.OriginalPath, result.TargetPath, true);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_fileSystem.MoveFile(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}: {2}", result.OriginalPath, result.TargetPath, ex.Message);
|
|
||||||
|
|
||||||
result.Status = FileSortingStatus.Failure;
|
|
||||||
result.StatusMessage = errorMsg;
|
|
||||||
_logger.ErrorException(errorMsg, ex);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_libraryMonitor.ReportFileSystemChangeComplete(result.TargetPath, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetAlreadyExists && !options.CopyOriginalFile)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_fileSystem.DeleteFile(result.OriginalPath);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.ErrorException("Error deleting {0}", ex, result.OriginalPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Series GetMatchingSeries(string seriesName, FileOrganizationResult result, AutoOrganizeOptions options)
|
|
||||||
{
|
|
||||||
var parsedName = _libraryManager.ParseName(seriesName);
|
|
||||||
|
|
||||||
var yearInName = parsedName.Year;
|
|
||||||
var nameWithoutYear = parsedName.Name;
|
|
||||||
|
|
||||||
result.ExtractedName = nameWithoutYear;
|
|
||||||
result.ExtractedYear = yearInName;
|
|
||||||
|
|
||||||
var series = _libraryManager.GetItemList(new InternalItemsQuery
|
|
||||||
{
|
|
||||||
IncludeItemTypes = new[] { typeof(Series).Name },
|
|
||||||
Recursive = true,
|
|
||||||
DtoOptions = new DtoOptions(true)
|
|
||||||
})
|
|
||||||
.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.SmartMatchInfos.FirstOrDefault(e => e.MatchStrings.Contains(nameWithoutYear, StringComparer.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (info != null)
|
|
||||||
{
|
|
||||||
series = _libraryManager.GetItemList(new InternalItemsQuery
|
|
||||||
{
|
|
||||||
IncludeItemTypes = new[] { typeof(Series).Name },
|
|
||||||
Recursive = true,
|
|
||||||
Name = info.ItemName,
|
|
||||||
DtoOptions = new DtoOptions(true)
|
|
||||||
|
|
||||||
}).Cast<Series>().FirstOrDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return series;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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="premiereDate">The premiere date.</param>
|
|
||||||
/// <param name="options">The options.</param>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <returns>System.String.</returns>
|
|
||||||
private async Task<string> GetNewPath(string sourcePath,
|
|
||||||
Series series,
|
|
||||||
int? seasonNumber,
|
|
||||||
int? episodeNumber,
|
|
||||||
int? endingEpisodeNumber,
|
|
||||||
DateTime? premiereDate,
|
|
||||||
TvFileOrganizationOptions options,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var episodeInfo = new EpisodeInfo
|
|
||||||
{
|
|
||||||
IndexNumber = episodeNumber,
|
|
||||||
IndexNumberEnd = endingEpisodeNumber,
|
|
||||||
MetadataCountryCode = series.GetPreferredMetadataCountryCode(),
|
|
||||||
MetadataLanguage = series.GetPreferredMetadataLanguage(),
|
|
||||||
ParentIndexNumber = seasonNumber,
|
|
||||||
SeriesProviderIds = series.ProviderIds,
|
|
||||||
PremiereDate = premiereDate
|
|
||||||
};
|
|
||||||
|
|
||||||
var searchResults = await _providerManager.GetRemoteSearchResults<Episode, EpisodeInfo>(new RemoteSearchQuery<EpisodeInfo>
|
|
||||||
{
|
|
||||||
SearchInfo = episodeInfo
|
|
||||||
|
|
||||||
}, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var episode = searchResults.FirstOrDefault();
|
|
||||||
|
|
||||||
if (episode == null)
|
|
||||||
{
|
|
||||||
var msg = string.Format("No provider metadata found for {0} season {1} episode {2}", series.Name, seasonNumber, episodeNumber);
|
|
||||||
_logger.Warn(msg);
|
|
||||||
throw new Exception(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
var episodeName = episode.Name;
|
|
||||||
|
|
||||||
//if (string.IsNullOrWhiteSpace(episodeName))
|
|
||||||
//{
|
|
||||||
// var msg = string.Format("No provider metadata found for {0} season {1} episode {2}", series.Name, seasonNumber, episodeNumber);
|
|
||||||
// _logger.Warn(msg);
|
|
||||||
// return null;
|
|
||||||
//}
|
|
||||||
|
|
||||||
seasonNumber = seasonNumber ?? episode.ParentIndexNumber;
|
|
||||||
episodeNumber = episodeNumber ?? episode.IndexNumber;
|
|
||||||
|
|
||||||
var newPath = GetSeasonFolderPath(series, seasonNumber.Value, options);
|
|
||||||
|
|
||||||
var episodeFileName = GetEpisodeFileName(sourcePath, series.Name, seasonNumber.Value, episodeNumber.Value, endingEpisodeNumber, episodeName, options);
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(episodeFileName))
|
|
||||||
{
|
|
||||||
// cause failure
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
.GetRecursiveChildren(i => i is Season && i.LocationType == LocationType.FileSystem && i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber)
|
|
||||||
.FirstOrDefault();
|
|
||||||
|
|
||||||
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).Trim();
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(episodeTitle))
|
|
||||||
{
|
|
||||||
episodeTitle = string.Empty;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
episodeTitle = _fileSystem.GetValidFilename(episodeTitle).Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
var sourceExtension = (Path.GetExtension(sourcePath) ?? string.Empty).TrimStart('.');
|
|
||||||
|
|
||||||
var pattern = endingEpisodeNumber.HasValue ? options.MultiEpisodeNamePattern : options.EpisodeNamePattern;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(pattern))
|
|
||||||
{
|
|
||||||
throw new Exception("GetEpisodeFileName: Configured episode name pattern is empty!");
|
|
||||||
}
|
|
||||||
|
|
||||||
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", "%#1")
|
|
||||||
.Replace("%e.n", "%#2")
|
|
||||||
.Replace("%e_n", "%#3");
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
result = result.Replace("%e", episodeNumber.ToString(_usCulture))
|
|
||||||
.Replace("%0e", episodeNumber.ToString("00", _usCulture))
|
|
||||||
.Replace("%00e", episodeNumber.ToString("000", _usCulture));
|
|
||||||
|
|
||||||
if (result.Contains("%#"))
|
|
||||||
{
|
|
||||||
result = result.Replace("%#1", episodeTitle)
|
|
||||||
.Replace("%#2", episodeTitle.Replace(" ", "."))
|
|
||||||
.Replace("%#3", episodeTitle.Replace(" ", "_"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally, call GetValidFilename again in case user customized the episode expression with any invalid filename characters
|
|
||||||
return _fileSystem.GetValidFilename(result).Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsSameEpisode(string sourcePath, string newPath)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var sourceFileInfo = _fileSystem.GetFileInfo(sourcePath);
|
|
||||||
var destinationFileInfo = _fileSystem.GetFileInfo(newPath);
|
|
||||||
|
|
||||||
if (sourceFileInfo.Length == destinationFileInfo.Length)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (FileNotFoundException)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch (IOException)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
using MediaBrowser.Common.Configuration;
|
|
||||||
using MediaBrowser.Model.FileOrganization;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.FileOrganization
|
|
||||||
{
|
|
||||||
public static class ConfigurationExtension
|
|
||||||
{
|
|
||||||
public static AutoOrganizeOptions GetAutoOrganizeOptions(this IConfigurationManager manager)
|
|
||||||
{
|
|
||||||
return manager.GetConfiguration<AutoOrganizeOptions>("autoorganize");
|
|
||||||
}
|
|
||||||
public static void SaveAutoOrganizeOptions(this IConfigurationManager manager, AutoOrganizeOptions options)
|
|
||||||
{
|
|
||||||
manager.SaveConfiguration("autoorganize", options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AutoOrganizeOptionsFactory : IConfigurationFactory
|
|
||||||
{
|
|
||||||
public IEnumerable<ConfigurationStore> GetConfigurations()
|
|
||||||
{
|
|
||||||
return new List<ConfigurationStore>
|
|
||||||
{
|
|
||||||
new ConfigurationStore
|
|
||||||
{
|
|
||||||
Key = "autoorganize",
|
|
||||||
ConfigurationType = typeof (AutoOrganizeOptions)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
using MediaBrowser.Controller.FileOrganization;
|
|
||||||
using MediaBrowser.Controller.Plugins;
|
|
||||||
using MediaBrowser.Controller.Session;
|
|
||||||
using MediaBrowser.Model.Events;
|
|
||||||
using MediaBrowser.Model.FileOrganization;
|
|
||||||
using MediaBrowser.Model.Logging;
|
|
||||||
using System;
|
|
||||||
using System.Threading;
|
|
||||||
using MediaBrowser.Model.Tasks;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.FileOrganization
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Class SessionInfoWebSocketListener
|
|
||||||
/// </summary>
|
|
||||||
class FileOrganizationNotifier : IServerEntryPoint
|
|
||||||
{
|
|
||||||
private readonly IFileOrganizationService _organizationService;
|
|
||||||
private readonly ISessionManager _sessionManager;
|
|
||||||
private readonly ITaskManager _taskManager;
|
|
||||||
|
|
||||||
public FileOrganizationNotifier(ILogger logger, IFileOrganizationService organizationService, ISessionManager sessionManager, ITaskManager taskManager)
|
|
||||||
{
|
|
||||||
_organizationService = organizationService;
|
|
||||||
_sessionManager = sessionManager;
|
|
||||||
_taskManager = taskManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Run()
|
|
||||||
{
|
|
||||||
_organizationService.ItemAdded += _organizationService_ItemAdded;
|
|
||||||
_organizationService.ItemRemoved += _organizationService_ItemRemoved;
|
|
||||||
_organizationService.ItemUpdated += _organizationService_ItemUpdated;
|
|
||||||
_organizationService.LogReset += _organizationService_LogReset;
|
|
||||||
|
|
||||||
//_taskManager.TaskCompleted += _taskManager_TaskCompleted;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void _organizationService_LogReset(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
_sessionManager.SendMessageToAdminSessions("AutoOrganize_LogReset", (FileOrganizationResult)null, CancellationToken.None);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void _organizationService_ItemUpdated(object sender, GenericEventArgs<FileOrganizationResult> e)
|
|
||||||
{
|
|
||||||
_sessionManager.SendMessageToAdminSessions("AutoOrganize_ItemUpdated", e.Argument, CancellationToken.None);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void _organizationService_ItemRemoved(object sender, GenericEventArgs<FileOrganizationResult> e)
|
|
||||||
{
|
|
||||||
_sessionManager.SendMessageToAdminSessions("AutoOrganize_ItemRemoved", e.Argument, CancellationToken.None);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void _organizationService_ItemAdded(object sender, GenericEventArgs<FileOrganizationResult> e)
|
|
||||||
{
|
|
||||||
_sessionManager.SendMessageToAdminSessions("AutoOrganize_ItemAdded", e.Argument, CancellationToken.None);
|
|
||||||
}
|
|
||||||
|
|
||||||
//private void _taskManager_TaskCompleted(object sender, TaskCompletionEventArgs e)
|
|
||||||
//{
|
|
||||||
// var taskWithKey = e.Task.ScheduledTask as IHasKey;
|
|
||||||
// if (taskWithKey != null && taskWithKey.Key == "AutoOrganize")
|
|
||||||
// {
|
|
||||||
// _sessionManager.SendMessageToAdminSessions("AutoOrganize_TaskCompleted", (FileOrganizationResult)null, CancellationToken.None);
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_organizationService.ItemAdded -= _organizationService_ItemAdded;
|
|
||||||
_organizationService.ItemRemoved -= _organizationService_ItemRemoved;
|
|
||||||
_organizationService.ItemUpdated -= _organizationService_ItemUpdated;
|
|
||||||
_organizationService.LogReset -= _organizationService_LogReset;
|
|
||||||
|
|
||||||
//_taskManager.TaskCompleted -= _taskManager_TaskCompleted;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,283 +0,0 @@
|
||||||
using MediaBrowser.Common.Extensions;
|
|
||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
using MediaBrowser.Controller.FileOrganization;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller.Net;
|
|
||||||
using MediaBrowser.Controller.Persistence;
|
|
||||||
using MediaBrowser.Controller.Providers;
|
|
||||||
using MediaBrowser.Model.FileOrganization;
|
|
||||||
using MediaBrowser.Model.Logging;
|
|
||||||
using MediaBrowser.Model.Querying;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using MediaBrowser.Controller.Session;
|
|
||||||
using MediaBrowser.Model.Events;
|
|
||||||
using MediaBrowser.Common.Events;
|
|
||||||
|
|
||||||
using MediaBrowser.Controller.IO;
|
|
||||||
using MediaBrowser.Model.Tasks;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.FileOrganization
|
|
||||||
{
|
|
||||||
public class FileOrganizationService : IFileOrganizationService
|
|
||||||
{
|
|
||||||
private readonly ITaskManager _taskManager;
|
|
||||||
private readonly IFileOrganizationRepository _repo;
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly ILibraryMonitor _libraryMonitor;
|
|
||||||
private readonly ILibraryManager _libraryManager;
|
|
||||||
private readonly IServerConfigurationManager _config;
|
|
||||||
private readonly IFileSystem _fileSystem;
|
|
||||||
private readonly IProviderManager _providerManager;
|
|
||||||
private readonly ConcurrentDictionary<string, bool> _inProgressItemIds = new ConcurrentDictionary<string, bool>();
|
|
||||||
|
|
||||||
public event EventHandler<GenericEventArgs<FileOrganizationResult>> ItemAdded;
|
|
||||||
public event EventHandler<GenericEventArgs<FileOrganizationResult>> ItemUpdated;
|
|
||||||
public event EventHandler<GenericEventArgs<FileOrganizationResult>> ItemRemoved;
|
|
||||||
public event EventHandler LogReset;
|
|
||||||
|
|
||||||
public FileOrganizationService(ITaskManager taskManager, IFileOrganizationRepository repo, ILogger logger, ILibraryMonitor libraryMonitor, ILibraryManager libraryManager, IServerConfigurationManager config, IFileSystem fileSystem, IProviderManager providerManager)
|
|
||||||
{
|
|
||||||
_taskManager = taskManager;
|
|
||||||
_repo = repo;
|
|
||||||
_logger = logger;
|
|
||||||
_libraryMonitor = libraryMonitor;
|
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_config = config;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_providerManager = providerManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void BeginProcessNewFiles()
|
|
||||||
{
|
|
||||||
_taskManager.CancelIfRunningAndQueue<OrganizerScheduledTask>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task SaveResult(FileOrganizationResult result, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (result == null || string.IsNullOrEmpty(result.OriginalPath))
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("result");
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Id = result.OriginalPath.GetMD5().ToString("N");
|
|
||||||
|
|
||||||
return _repo.SaveResult(result, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public QueryResult<FileOrganizationResult> GetResults(FileOrganizationResultQuery query)
|
|
||||||
{
|
|
||||||
var results = _repo.GetResults(query);
|
|
||||||
|
|
||||||
foreach (var result in results.Items)
|
|
||||||
{
|
|
||||||
result.IsInProgress = _inProgressItemIds.ContainsKey(result.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
public FileOrganizationResult GetResult(string id)
|
|
||||||
{
|
|
||||||
var result = _repo.GetResult(id);
|
|
||||||
|
|
||||||
if (result != null)
|
|
||||||
{
|
|
||||||
result.IsInProgress = _inProgressItemIds.ContainsKey(result.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public FileOrganizationResult GetResultBySourcePath(string path)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(path))
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("path");
|
|
||||||
}
|
|
||||||
|
|
||||||
var id = path.GetMD5().ToString("N");
|
|
||||||
|
|
||||||
return GetResult(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteOriginalFile(string resultId)
|
|
||||||
{
|
|
||||||
var result = _repo.GetResult(resultId);
|
|
||||||
|
|
||||||
_logger.Info("Requested to delete {0}", result.OriginalPath);
|
|
||||||
|
|
||||||
if (!AddToInProgressList(result, false))
|
|
||||||
{
|
|
||||||
throw new Exception("Path is currently processed otherwise. Please try again later.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_fileSystem.DeleteFile(result.OriginalPath);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.ErrorException("Error deleting {0}", ex, result.OriginalPath);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
RemoveFromInprogressList(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _repo.Delete(resultId);
|
|
||||||
|
|
||||||
EventHelper.FireEventIfNotNull(ItemRemoved, this, new GenericEventArgs<FileOrganizationResult>(result), _logger);
|
|
||||||
}
|
|
||||||
|
|
||||||
private AutoOrganizeOptions GetAutoOrganizeOptions()
|
|
||||||
{
|
|
||||||
return _config.GetAutoOrganizeOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PerformOrganization(string resultId)
|
|
||||||
{
|
|
||||||
var result = _repo.GetResult(resultId);
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(result.TargetPath))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("No target path available.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var organizer = new EpisodeFileOrganizer(this, _config, _fileSystem, _logger, _libraryManager,
|
|
||||||
_libraryMonitor, _providerManager);
|
|
||||||
|
|
||||||
var organizeResult = await organizer.OrganizeEpisodeFile(result.OriginalPath, GetAutoOrganizeOptions(), true, CancellationToken.None)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (organizeResult.Status != FileSortingStatus.Success)
|
|
||||||
{
|
|
||||||
throw new Exception(result.StatusMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ClearLog()
|
|
||||||
{
|
|
||||||
await _repo.DeleteAll();
|
|
||||||
EventHelper.FireEventIfNotNull(LogReset, this, EventArgs.Empty, _logger);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PerformEpisodeOrganization(EpisodeFileOrganizationRequest request)
|
|
||||||
{
|
|
||||||
var organizer = new EpisodeFileOrganizer(this, _config, _fileSystem, _logger, _libraryManager,
|
|
||||||
_libraryMonitor, _providerManager);
|
|
||||||
|
|
||||||
var result = await organizer.OrganizeWithCorrection(request, GetAutoOrganizeOptions(), CancellationToken.None).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (result.Status != FileSortingStatus.Success)
|
|
||||||
{
|
|
||||||
throw new Exception(result.StatusMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public QueryResult<SmartMatchInfo> GetSmartMatchInfos(FileOrganizationResultQuery query)
|
|
||||||
{
|
|
||||||
if (query == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("query");
|
|
||||||
}
|
|
||||||
|
|
||||||
var options = GetAutoOrganizeOptions();
|
|
||||||
|
|
||||||
var items = options.SmartMatchInfos.Skip(query.StartIndex ?? 0).Take(query.Limit ?? Int32.MaxValue).ToArray();
|
|
||||||
|
|
||||||
return new QueryResult<SmartMatchInfo>()
|
|
||||||
{
|
|
||||||
Items = items,
|
|
||||||
TotalRecordCount = options.SmartMatchInfos.Length
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DeleteSmartMatchEntry(string itemName, string matchString)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(itemName))
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("itemName");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(matchString))
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("matchString");
|
|
||||||
}
|
|
||||||
|
|
||||||
var options = GetAutoOrganizeOptions();
|
|
||||||
|
|
||||||
SmartMatchInfo info = options.SmartMatchInfos.FirstOrDefault(i => string.Equals(i.ItemName, itemName));
|
|
||||||
|
|
||||||
if (info != null && info.MatchStrings.Contains(matchString))
|
|
||||||
{
|
|
||||||
var list = info.MatchStrings.ToList();
|
|
||||||
list.Remove(matchString);
|
|
||||||
info.MatchStrings = list.ToArray();
|
|
||||||
|
|
||||||
if (info.MatchStrings.Length == 0)
|
|
||||||
{
|
|
||||||
var infos = options.SmartMatchInfos.ToList();
|
|
||||||
infos.Remove(info);
|
|
||||||
options.SmartMatchInfos = infos.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
_config.SaveAutoOrganizeOptions(options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attempts to add a an item to the list of currently processed items.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="result">The result item.</param>
|
|
||||||
/// <param name="isNewItem">Passing true will notify the client to reload all items, otherwise only a single item will be refreshed.</param>
|
|
||||||
/// <returns>True if the item was added, False if the item is already contained in the list.</returns>
|
|
||||||
public bool AddToInProgressList(FileOrganizationResult result, bool isNewItem)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(result.Id))
|
|
||||||
{
|
|
||||||
result.Id = result.OriginalPath.GetMD5().ToString("N");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_inProgressItemIds.TryAdd(result.Id, false))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.IsInProgress = true;
|
|
||||||
|
|
||||||
if (isNewItem)
|
|
||||||
{
|
|
||||||
EventHelper.FireEventIfNotNull(ItemAdded, this, new GenericEventArgs<FileOrganizationResult>(result), _logger);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
EventHelper.FireEventIfNotNull(ItemUpdated, this, new GenericEventArgs<FileOrganizationResult>(result), _logger);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes an item from the list of currently processed items.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="result">The result item.</param>
|
|
||||||
/// <returns>True if the item was removed, False if the item was not contained in the list.</returns>
|
|
||||||
public bool RemoveFromInprogressList(FileOrganizationResult result)
|
|
||||||
{
|
|
||||||
bool itemValue;
|
|
||||||
var retval = _inProgressItemIds.TryRemove(result.Id, out itemValue);
|
|
||||||
|
|
||||||
result.IsInProgress = false;
|
|
||||||
|
|
||||||
EventHelper.FireEventIfNotNull(ItemUpdated, this, new GenericEventArgs<FileOrganizationResult>(result), _logger);
|
|
||||||
|
|
||||||
return retval;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
using MediaBrowser.Model.Extensions;
|
|
||||||
using MediaBrowser.Controller.Entities;
|
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using MediaBrowser.Controller.Extensions;
|
|
||||||
|
|
||||||
namespace Emby.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)
|
|
||||||
{
|
|
||||||
name = name.RemoveDiacritics();
|
|
||||||
|
|
||||||
name = " " + name + " ";
|
|
||||||
|
|
||||||
name = name.Replace(".", " ")
|
|
||||||
.Replace("_", " ")
|
|
||||||
.Replace(" and ", " ")
|
|
||||||
.Replace(".and.", " ")
|
|
||||||
.Replace("&", " ")
|
|
||||||
.Replace("!", " ")
|
|
||||||
.Replace("(", " ")
|
|
||||||
.Replace(")", " ")
|
|
||||||
.Replace(":", " ")
|
|
||||||
.Replace(",", " ")
|
|
||||||
.Replace("-", " ")
|
|
||||||
.Replace("'", " ")
|
|
||||||
.Replace("[", " ")
|
|
||||||
.Replace("]", " ")
|
|
||||||
.Replace(" a ", String.Empty, StringComparison.OrdinalIgnoreCase)
|
|
||||||
.Replace(" the ", String.Empty, StringComparison.OrdinalIgnoreCase)
|
|
||||||
.Replace(" ", String.Empty);
|
|
||||||
|
|
||||||
return name.Trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
using MediaBrowser.Controller.FileOrganization;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller.Providers;
|
|
||||||
using MediaBrowser.Model.FileOrganization;
|
|
||||||
using MediaBrowser.Model.Logging;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using MediaBrowser.Controller.IO;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using MediaBrowser.Model.Tasks;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.FileOrganization
|
|
||||||
{
|
|
||||||
public class OrganizerScheduledTask : IScheduledTask, IConfigurableScheduledTask
|
|
||||||
{
|
|
||||||
private readonly ILibraryMonitor _libraryMonitor;
|
|
||||||
private readonly ILibraryManager _libraryManager;
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly IFileSystem _fileSystem;
|
|
||||||
private readonly IServerConfigurationManager _config;
|
|
||||||
private readonly IFileOrganizationService _organizationService;
|
|
||||||
private readonly IProviderManager _providerManager;
|
|
||||||
|
|
||||||
public OrganizerScheduledTask(ILibraryMonitor libraryMonitor, ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem, IServerConfigurationManager config, IFileOrganizationService organizationService, IProviderManager providerManager)
|
|
||||||
{
|
|
||||||
_libraryMonitor = libraryMonitor;
|
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_logger = logger;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_config = config;
|
|
||||||
_organizationService = organizationService;
|
|
||||||
_providerManager = providerManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Name
|
|
||||||
{
|
|
||||||
get { return "Organize new media files"; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Description
|
|
||||||
{
|
|
||||||
get { return "Processes new files available in the configured watch folder."; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Category
|
|
||||||
{
|
|
||||||
get { return "Library"; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private AutoOrganizeOptions GetAutoOrganizeOptions()
|
|
||||||
{
|
|
||||||
return _config.GetAutoOrganizeOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
|
|
||||||
{
|
|
||||||
if (GetAutoOrganizeOptions().TvOptions.IsEnabled)
|
|
||||||
{
|
|
||||||
await new TvFolderOrganizer(_libraryManager, _logger, _fileSystem, _libraryMonitor, _organizationService, _config, _providerManager)
|
|
||||||
.Organize(GetAutoOrganizeOptions(), cancellationToken, progress).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates the triggers that define when the task will run
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>IEnumerable{BaseTaskTrigger}.</returns>
|
|
||||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
|
||||||
{
|
|
||||||
return new[] {
|
|
||||||
|
|
||||||
// Every so often
|
|
||||||
new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromMinutes(5).Ticks}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsHidden
|
|
||||||
{
|
|
||||||
get { return !GetAutoOrganizeOptions().TvOptions.IsEnabled; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsEnabled
|
|
||||||
{
|
|
||||||
get { return GetAutoOrganizeOptions().TvOptions.IsEnabled; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsLogged
|
|
||||||
{
|
|
||||||
get { return false; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Key
|
|
||||||
{
|
|
||||||
get { return "AutoOrganize"; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,236 +0,0 @@
|
||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
using MediaBrowser.Controller.FileOrganization;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller.Providers;
|
|
||||||
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;
|
|
||||||
|
|
||||||
using MediaBrowser.Controller.IO;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.FileOrganization
|
|
||||||
{
|
|
||||||
public class TvFolderOrganizer
|
|
||||||
{
|
|
||||||
private readonly ILibraryMonitor _libraryMonitor;
|
|
||||||
private readonly ILibraryManager _libraryManager;
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly IFileSystem _fileSystem;
|
|
||||||
private readonly IFileOrganizationService _organizationService;
|
|
||||||
private readonly IServerConfigurationManager _config;
|
|
||||||
private readonly IProviderManager _providerManager;
|
|
||||||
|
|
||||||
public TvFolderOrganizer(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem, ILibraryMonitor libraryMonitor, IFileOrganizationService organizationService, IServerConfigurationManager config, IProviderManager providerManager)
|
|
||||||
{
|
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_logger = logger;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_libraryMonitor = libraryMonitor;
|
|
||||||
_organizationService = organizationService;
|
|
||||||
_config = config;
|
|
||||||
_providerManager = providerManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool EnableOrganization(FileSystemMetadata fileInfo, TvFileOrganizationOptions options)
|
|
||||||
{
|
|
||||||
var minFileBytes = options.MinFileSizeMb * 1024 * 1024;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return _libraryManager.IsVideoFile(fileInfo.FullName) && fileInfo.Length >= minFileBytes;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.ErrorException("Error organizing file {0}", ex, fileInfo.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsValidWatchLocation(string path, List<string> libraryFolderPaths)
|
|
||||||
{
|
|
||||||
if (IsPathAlreadyInMediaLibrary(path, libraryFolderPaths))
|
|
||||||
{
|
|
||||||
_logger.Info("Folder {0} is not eligible for auto-organize because it is also part of an Emby library", path);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsPathAlreadyInMediaLibrary(string path, List<string> libraryFolderPaths)
|
|
||||||
{
|
|
||||||
return libraryFolderPaths.Any(i => string.Equals(i, path, StringComparison.Ordinal) || _fileSystem.ContainsSubPath(i, path));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Organize(AutoOrganizeOptions options, CancellationToken cancellationToken, IProgress<double> progress)
|
|
||||||
{
|
|
||||||
var libraryFolderPaths = _libraryManager.GetVirtualFolders().SelectMany(i => i.Locations).ToList();
|
|
||||||
|
|
||||||
var watchLocations = options.TvOptions.WatchLocations
|
|
||||||
.Where(i => IsValidWatchLocation(i, libraryFolderPaths))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var eligibleFiles = watchLocations.SelectMany(GetFilesToOrganize)
|
|
||||||
.OrderBy(_fileSystem.GetCreationTimeUtc)
|
|
||||||
.Where(i => EnableOrganization(i, options.TvOptions))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var processedFolders = new HashSet<string>();
|
|
||||||
|
|
||||||
progress.Report(10);
|
|
||||||
|
|
||||||
if (eligibleFiles.Count > 0)
|
|
||||||
{
|
|
||||||
var numComplete = 0;
|
|
||||||
|
|
||||||
foreach (var file in eligibleFiles)
|
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
var organizer = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager,
|
|
||||||
_libraryMonitor, _providerManager);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = await organizer.OrganizeEpisodeFile(file.FullName, options, options.TvOptions.OverwriteExistingEpisodes, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (result.Status == FileSortingStatus.Success && !processedFolders.Contains(file.DirectoryName, StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
processedFolders.Add(file.DirectoryName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.ErrorException("Error organizing episode {0}", ex, file.FullName);
|
|
||||||
}
|
|
||||||
|
|
||||||
numComplete++;
|
|
||||||
double percent = numComplete;
|
|
||||||
percent /= eligibleFiles.Count;
|
|
||||||
|
|
||||||
progress.Report(10 + 89 * percent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
progress.Report(99);
|
|
||||||
|
|
||||||
foreach (var path in processedFolders)
|
|
||||||
{
|
|
||||||
var deleteExtensions = options.TvOptions.LeftOverFileExtensionsToDelete
|
|
||||||
.Select(i => i.Trim().TrimStart('.'))
|
|
||||||
.Where(i => !string.IsNullOrEmpty(i))
|
|
||||||
.Select(i => "." + i)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (deleteExtensions.Count > 0)
|
|
||||||
{
|
|
||||||
DeleteLeftOverFiles(path, deleteExtensions);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.TvOptions.DeleteEmptyFolders)
|
|
||||||
{
|
|
||||||
if (!IsWatchFolder(path, watchLocations))
|
|
||||||
{
|
|
||||||
DeleteEmptyFolders(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
progress.Report(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the files to organize.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">The path.</param>
|
|
||||||
/// <returns>IEnumerable{FileInfo}.</returns>
|
|
||||||
private List<FileSystemMetadata> GetFilesToOrganize(string path)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return _fileSystem.GetFiles(path, true)
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
_logger.ErrorException("Error getting files from {0}", ex, path);
|
|
||||||
|
|
||||||
return new List<FileSystemMetadata>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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 = _fileSystem.GetFilePaths(path, extensions.ToArray(), false, true)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
foreach (var file in eligibleFiles)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_fileSystem.DeleteFile(file);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.ErrorException("Error deleting file {0}", ex, file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes the empty folders.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">The path.</param>
|
|
||||||
private void DeleteEmptyFolders(string path)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
foreach (var d in _fileSystem.GetDirectoryPaths(path))
|
|
||||||
{
|
|
||||||
DeleteEmptyFolders(d);
|
|
||||||
}
|
|
||||||
|
|
||||||
var entries = _fileSystem.GetFileSystemEntryPaths(path);
|
|
||||||
|
|
||||||
if (!entries.Any())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_logger.Debug("Deleting empty directory {0}", path);
|
|
||||||
_fileSystem.DeleteDirectory(path, false);
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException) { }
|
|
||||||
catch (IOException) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines if a given folder path is contained in a folder list
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">The folder path to check.</param>
|
|
||||||
/// <param name="watchLocations">A list of folders.</param>
|
|
||||||
private bool IsWatchFolder(string path, IEnumerable<string> watchLocations)
|
|
||||||
{
|
|
||||||
return watchLocations.Contains(path, StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,7 +4,6 @@ using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Common.Security;
|
using MediaBrowser.Common.Security;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.FileOrganization;
|
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.LiveTv;
|
using MediaBrowser.Controller.LiveTv;
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
|
@ -36,7 +35,6 @@ using MediaBrowser.Controller.Entities.TV;
|
||||||
using MediaBrowser.Controller.IO;
|
using MediaBrowser.Controller.IO;
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
using MediaBrowser.Model.Diagnostics;
|
using MediaBrowser.Model.Diagnostics;
|
||||||
using MediaBrowser.Model.FileOrganization;
|
|
||||||
using MediaBrowser.Model.System;
|
using MediaBrowser.Model.System;
|
||||||
using MediaBrowser.Model.Threading;
|
using MediaBrowser.Model.Threading;
|
||||||
using MediaBrowser.Model.Extensions;
|
using MediaBrowser.Model.Extensions;
|
||||||
|
@ -61,7 +59,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||||
private readonly ILibraryMonitor _libraryMonitor;
|
private readonly ILibraryMonitor _libraryMonitor;
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly IProviderManager _providerManager;
|
private readonly IProviderManager _providerManager;
|
||||||
private readonly IFileOrganizationService _organizationService;
|
|
||||||
private readonly IMediaEncoder _mediaEncoder;
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
private readonly IProcessFactory _processFactory;
|
private readonly IProcessFactory _processFactory;
|
||||||
private readonly ISystemEvents _systemEvents;
|
private readonly ISystemEvents _systemEvents;
|
||||||
|
@ -74,7 +71,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||||
private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings =
|
private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings =
|
||||||
new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase);
|
new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
public EmbyTV(IServerApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IServerConfigurationManager config, ILiveTvManager liveTvManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, IFileOrganizationService organizationService, IMediaEncoder mediaEncoder, ITimerFactory timerFactory, IProcessFactory processFactory, ISystemEvents systemEvents)
|
public EmbyTV(IServerApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IServerConfigurationManager config, ILiveTvManager liveTvManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, IMediaEncoder mediaEncoder, ITimerFactory timerFactory, IProcessFactory processFactory, ISystemEvents systemEvents)
|
||||||
{
|
{
|
||||||
Current = this;
|
Current = this;
|
||||||
|
|
||||||
|
@ -86,7 +83,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
_libraryMonitor = libraryMonitor;
|
_libraryMonitor = libraryMonitor;
|
||||||
_providerManager = providerManager;
|
_providerManager = providerManager;
|
||||||
_organizationService = organizationService;
|
|
||||||
_mediaEncoder = mediaEncoder;
|
_mediaEncoder = mediaEncoder;
|
||||||
_processFactory = processFactory;
|
_processFactory = processFactory;
|
||||||
_systemEvents = systemEvents;
|
_systemEvents = systemEvents;
|
||||||
|
|
|
@ -1,213 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using MediaBrowser.Controller.FileOrganization;
|
|
||||||
using MediaBrowser.Controller.Net;
|
|
||||||
using MediaBrowser.Model.FileOrganization;
|
|
||||||
using MediaBrowser.Model.Querying;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Model.Dto;
|
|
||||||
using MediaBrowser.Model.Serialization;
|
|
||||||
using MediaBrowser.Model.Services;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Library
|
|
||||||
{
|
|
||||||
[Route("/Library/FileOrganization", "GET", Summary = "Gets file organization results")]
|
|
||||||
public class GetFileOrganizationActivity : IReturn<QueryResult<FileOrganizationResult>>
|
|
||||||
{
|
|
||||||
/// <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/FileOrganizations", "DELETE", Summary = "Clears the activity log")]
|
|
||||||
public class ClearOrganizationLog : IReturnVoid
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[Route("/Library/FileOrganizations/{Id}/File", "DELETE", Summary = "Deletes the original file of a organizer result")]
|
|
||||||
public class DeleteOriginalFile : IReturnVoid
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the id.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The id.</value>
|
|
||||||
[ApiMember(Name = "Id", Description = "Result Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
|
|
||||||
public string Id { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[Route("/Library/FileOrganizations/{Id}/Organize", "POST", Summary = "Performs an organization")]
|
|
||||||
public class PerformOrganization : IReturn<QueryResult<FileOrganizationResult>>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the id.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The id.</value>
|
|
||||||
[ApiMember(Name = "Id", Description = "Result Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
|
||||||
public string Id { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[Route("/Library/FileOrganizations/{Id}/Episode/Organize", "POST", Summary = "Performs organization of a tv episode")]
|
|
||||||
public class OrganizeEpisode
|
|
||||||
{
|
|
||||||
[ApiMember(Name = "Id", Description = "Result Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
|
||||||
public string Id { get; set; }
|
|
||||||
|
|
||||||
[ApiMember(Name = "SeriesId", Description = "Series Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
|
|
||||||
public string SeriesId { get; set; }
|
|
||||||
|
|
||||||
[ApiMember(Name = "SeasonNumber", IsRequired = true, DataType = "int", ParameterType = "query", Verb = "POST")]
|
|
||||||
public int SeasonNumber { get; set; }
|
|
||||||
|
|
||||||
[ApiMember(Name = "EpisodeNumber", IsRequired = true, DataType = "int", ParameterType = "query", Verb = "POST")]
|
|
||||||
public int EpisodeNumber { get; set; }
|
|
||||||
|
|
||||||
[ApiMember(Name = "EndingEpisodeNumber", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
|
|
||||||
public int? EndingEpisodeNumber { get; set; }
|
|
||||||
|
|
||||||
[ApiMember(Name = "RememberCorrection", Description = "Whether or not to apply the same correction to future episodes of the same series.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
|
|
||||||
public bool RememberCorrection { get; set; }
|
|
||||||
|
|
||||||
[ApiMember(Name = "NewSeriesProviderIds", Description = "A list of provider IDs identifying a new series.", IsRequired = false, DataType = "Dictionary<string, string>", ParameterType = "query", Verb = "POST")]
|
|
||||||
public Dictionary<string, string> NewSeriesProviderIds { get; set; }
|
|
||||||
|
|
||||||
[ApiMember(Name = "NewSeriesName", Description = "Name of a series to add.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
|
||||||
public string NewSeriesName { get; set; }
|
|
||||||
|
|
||||||
[ApiMember(Name = "NewSeriesYear", Description = "Year of a series to add.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
|
||||||
public string NewSeriesYear { get; set; }
|
|
||||||
|
|
||||||
[ApiMember(Name = "TargetFolder", Description = "Target Folder", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
|
||||||
public string TargetFolder { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[Route("/Library/FileOrganizations/SmartMatches", "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/FileOrganizations/SmartMatches/Delete", "POST", Summary = "Deletes a smart match entry")]
|
|
||||||
public class DeleteSmartMatchEntry
|
|
||||||
{
|
|
||||||
[ApiMember(Name = "Entries", Description = "SmartMatch Entry", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
|
|
||||||
public List<NameValuePair> Entries { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[Authenticated(Roles = "Admin")]
|
|
||||||
public class FileOrganizationService : BaseApiService
|
|
||||||
{
|
|
||||||
private readonly IFileOrganizationService _iFileOrganizationService;
|
|
||||||
|
|
||||||
private readonly IJsonSerializer _jsonSerializer;
|
|
||||||
|
|
||||||
public FileOrganizationService(IFileOrganizationService iFileOrganizationService, IJsonSerializer jsonSerializer)
|
|
||||||
{
|
|
||||||
_iFileOrganizationService = iFileOrganizationService;
|
|
||||||
_jsonSerializer = jsonSerializer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public object Get(GetFileOrganizationActivity request)
|
|
||||||
{
|
|
||||||
var result = _iFileOrganizationService.GetResults(new FileOrganizationResultQuery
|
|
||||||
{
|
|
||||||
Limit = request.Limit,
|
|
||||||
StartIndex = request.StartIndex
|
|
||||||
});
|
|
||||||
|
|
||||||
return ToOptimizedSerializedResultUsingCache(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Delete(DeleteOriginalFile request)
|
|
||||||
{
|
|
||||||
var task = _iFileOrganizationService.DeleteOriginalFile(request.Id);
|
|
||||||
|
|
||||||
Task.WaitAll(task);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Delete(ClearOrganizationLog request)
|
|
||||||
{
|
|
||||||
var task = _iFileOrganizationService.ClearLog();
|
|
||||||
|
|
||||||
Task.WaitAll(task);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Post(PerformOrganization request)
|
|
||||||
{
|
|
||||||
// Don't await this
|
|
||||||
var task = _iFileOrganizationService.PerformOrganization(request.Id);
|
|
||||||
|
|
||||||
// Async processing (close dialog early instead of waiting until the file has been copied)
|
|
||||||
// Wait 2s for exceptions that may occur to have them forwarded to the client for immediate error display
|
|
||||||
task.Wait(2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Post(OrganizeEpisode request)
|
|
||||||
{
|
|
||||||
var dicNewProviderIds = new Dictionary<string, string>();
|
|
||||||
|
|
||||||
if (request.NewSeriesProviderIds != null)
|
|
||||||
{
|
|
||||||
dicNewProviderIds = request.NewSeriesProviderIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't await this
|
|
||||||
var task = _iFileOrganizationService.PerformEpisodeOrganization(new EpisodeFileOrganizationRequest
|
|
||||||
{
|
|
||||||
EndingEpisodeNumber = request.EndingEpisodeNumber,
|
|
||||||
EpisodeNumber = request.EpisodeNumber,
|
|
||||||
RememberCorrection = request.RememberCorrection,
|
|
||||||
ResultId = request.Id,
|
|
||||||
SeasonNumber = request.SeasonNumber,
|
|
||||||
SeriesId = request.SeriesId,
|
|
||||||
NewSeriesName = request.NewSeriesName,
|
|
||||||
NewSeriesYear = request.NewSeriesYear,
|
|
||||||
NewSeriesProviderIds = dicNewProviderIds,
|
|
||||||
TargetFolder = request.TargetFolder
|
|
||||||
});
|
|
||||||
|
|
||||||
// Async processing (close dialog early instead of waiting until the file has been copied)
|
|
||||||
// Wait 2s for exceptions that may occur to have them forwarded to the client for immediate error display
|
|
||||||
task.Wait(2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
foreach (var entry in request.Entries)
|
|
||||||
{
|
|
||||||
_iFileOrganizationService.DeleteSmartMatchEntry(entry.Name, entry.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -93,7 +93,6 @@
|
||||||
<Compile Include="ItemRefreshService.cs" />
|
<Compile Include="ItemRefreshService.cs" />
|
||||||
<Compile Include="ItemUpdateService.cs" />
|
<Compile Include="ItemUpdateService.cs" />
|
||||||
<Compile Include="Library\LibraryService.cs" />
|
<Compile Include="Library\LibraryService.cs" />
|
||||||
<Compile Include="Library\FileOrganizationService.cs" />
|
|
||||||
<Compile Include="Library\LibraryStructureService.cs" />
|
<Compile Include="Library\LibraryStructureService.cs" />
|
||||||
<Compile Include="LiveTv\LiveTvService.cs" />
|
<Compile Include="LiveTv\LiveTvService.cs" />
|
||||||
<Compile Include="LocalizationService.cs" />
|
<Compile Include="LocalizationService.cs" />
|
||||||
|
|
|
@ -1,107 +0,0 @@
|
||||||
using MediaBrowser.Model.Events;
|
|
||||||
using MediaBrowser.Model.FileOrganization;
|
|
||||||
using MediaBrowser.Model.Querying;
|
|
||||||
using System;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.FileOrganization
|
|
||||||
{
|
|
||||||
public interface IFileOrganizationService
|
|
||||||
{
|
|
||||||
event EventHandler<GenericEventArgs<FileOrganizationResult>> ItemAdded;
|
|
||||||
event EventHandler<GenericEventArgs<FileOrganizationResult>> ItemUpdated;
|
|
||||||
event EventHandler<GenericEventArgs<FileOrganizationResult>> ItemRemoved;
|
|
||||||
event EventHandler LogReset;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Processes the new files.
|
|
||||||
/// </summary>
|
|
||||||
void BeginProcessNewFiles();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes the original file.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="resultId">The result identifier.</param>
|
|
||||||
/// <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.
|
|
||||||
/// </summary>
|
|
||||||
/// <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>
|
|
||||||
/// Gets the result by source path.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">The path.</param>
|
|
||||||
/// <returns>FileOrganizationResult.</returns>
|
|
||||||
FileOrganizationResult GetResultBySourcePath(string path);
|
|
||||||
|
|
||||||
/// <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>
|
|
||||||
/// 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="ItemName">Item name.</param>
|
|
||||||
/// <param name="matchString">The match string to delete.</param>
|
|
||||||
void DeleteSmartMatchEntry(string ItemName, string matchString);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attempts to add a an item to the list of currently processed items.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="result">The result item.</param>
|
|
||||||
/// <param name="fullClientRefresh">Passing true will notify the client to reload all items, otherwise only a single item will be refreshed.</param>
|
|
||||||
/// <returns>True if the item was added, False if the item is already contained in the list.</returns>
|
|
||||||
bool AddToInProgressList(FileOrganizationResult result, bool fullClientRefresh);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes an item from the list of currently processed items.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="result">The result item.</param>
|
|
||||||
/// <returns>True if the item was removed, False if the item was not contained in the list.</returns>
|
|
||||||
bool RemoveFromInprogressList(FileOrganizationResult result);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -128,7 +128,6 @@
|
||||||
<Compile Include="Entities\UserView.cs" />
|
<Compile Include="Entities\UserView.cs" />
|
||||||
<Compile Include="Entities\UserViewBuilder.cs" />
|
<Compile Include="Entities\UserViewBuilder.cs" />
|
||||||
<Compile Include="Extensions\StringExtensions.cs" />
|
<Compile Include="Extensions\StringExtensions.cs" />
|
||||||
<Compile Include="FileOrganization\IFileOrganizationService.cs" />
|
|
||||||
<Compile Include="IO\StreamHelper.cs" />
|
<Compile Include="IO\StreamHelper.cs" />
|
||||||
<Compile Include="Library\DeleteOptions.cs" />
|
<Compile Include="Library\DeleteOptions.cs" />
|
||||||
<Compile Include="Library\ILibraryPostScanTask.cs" />
|
<Compile Include="Library\ILibraryPostScanTask.cs" />
|
||||||
|
@ -205,7 +204,6 @@
|
||||||
<Compile Include="Notifications\INotificationTypeFactory.cs" />
|
<Compile Include="Notifications\INotificationTypeFactory.cs" />
|
||||||
<Compile Include="Notifications\NotificationUpdateEventArgs.cs" />
|
<Compile Include="Notifications\NotificationUpdateEventArgs.cs" />
|
||||||
<Compile Include="Notifications\UserNotification.cs" />
|
<Compile Include="Notifications\UserNotification.cs" />
|
||||||
<Compile Include="Persistence\IFileOrganizationRepository.cs" />
|
|
||||||
<Compile Include="Persistence\MediaStreamQuery.cs" />
|
<Compile Include="Persistence\MediaStreamQuery.cs" />
|
||||||
<Compile Include="Playlists\IPlaylistManager.cs" />
|
<Compile Include="Playlists\IPlaylistManager.cs" />
|
||||||
<Compile Include="Playlists\Playlist.cs" />
|
<Compile Include="Playlists\Playlist.cs" />
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
using MediaBrowser.Model.FileOrganization;
|
|
||||||
using MediaBrowser.Model.Querying;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.Persistence
|
|
||||||
{
|
|
||||||
public interface IFileOrganizationRepository
|
|
||||||
{
|
|
||||||
/// <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 specified identifier.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">The identifier.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
Task Delete(string id);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the result.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">The identifier.</param>
|
|
||||||
/// <returns>FileOrganizationResult.</returns>
|
|
||||||
FileOrganizationResult GetResult(string id);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the results.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="query">The query.</param>
|
|
||||||
/// <returns>IEnumerable{FileOrganizationResult}.</returns>
|
|
||||||
QueryResult<FileOrganizationResult> GetResults(FileOrganizationResultQuery query);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes all.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
Task DeleteAll();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -597,21 +597,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var mediaInfo = new ProbeResultNormalizer(_logger, FileSystem, _memoryStreamProvider).GetMediaInfo(result, videoType, isAudio, primaryPath, protocol);
|
return new ProbeResultNormalizer(_logger, FileSystem, _memoryStreamProvider).GetMediaInfo(result, videoType, isAudio, primaryPath, protocol);
|
||||||
|
|
||||||
var videoStream = mediaInfo.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
|
|
||||||
|
|
||||||
if (videoStream != null && !videoStream.IsInterlaced)
|
|
||||||
{
|
|
||||||
var isInterlaced = DetectInterlaced(mediaInfo, videoStream);
|
|
||||||
|
|
||||||
if (isInterlaced)
|
|
||||||
{
|
|
||||||
videoStream.IsInterlaced = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mediaInfo;
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
@ -622,23 +608,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool DetectInterlaced(MediaSourceInfo video, MediaStream videoStream)
|
|
||||||
{
|
|
||||||
// If it's mpeg based, assume true
|
|
||||||
if ((videoStream.Codec ?? string.Empty).IndexOf("mpeg", StringComparison.OrdinalIgnoreCase) != -1)
|
|
||||||
{
|
|
||||||
var formats = (video.Container ?? string.Empty).Split(',').ToList();
|
|
||||||
return formats.Contains("vob", StringComparer.OrdinalIgnoreCase) ||
|
|
||||||
formats.Contains("m2ts", StringComparer.OrdinalIgnoreCase) ||
|
|
||||||
formats.Contains("ts", StringComparer.OrdinalIgnoreCase) ||
|
|
||||||
formats.Contains("mpegts", StringComparer.OrdinalIgnoreCase) ||
|
|
||||||
formats.Contains("wtv", StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The us culture
|
/// The us culture
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
|
|
||||||
namespace MediaBrowser.Model.FileOrganization
|
|
||||||
{
|
|
||||||
public class AutoOrganizeOptions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the tv options.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The tv options.</value>
|
|
||||||
public TvFileOrganizationOptions TvOptions { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a list of smart match entries.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The smart match entries.</value>
|
|
||||||
public SmartMatchInfo[] SmartMatchInfos { get; set; }
|
|
||||||
|
|
||||||
public AutoOrganizeOptions()
|
|
||||||
{
|
|
||||||
TvOptions = new TvFileOrganizationOptions();
|
|
||||||
SmartMatchInfos = new SmartMatchInfo[]{};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Model.FileOrganization
|
|
||||||
{
|
|
||||||
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; }
|
|
||||||
public string NewSeriesName { get; set; }
|
|
||||||
|
|
||||||
public string NewSeriesYear { get; set; }
|
|
||||||
|
|
||||||
public string TargetFolder { get; set; }
|
|
||||||
|
|
||||||
public Dictionary<string, string> NewSeriesProviderIds { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,109 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Model.FileOrganization
|
|
||||||
{
|
|
||||||
public class FileOrganizationResult
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the result identifier.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The result identifier.</value>
|
|
||||||
public string Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the original path.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The original path.</value>
|
|
||||||
public string OriginalPath { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the name of the original file.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The name of the original file.</value>
|
|
||||||
public string OriginalFileName { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the name of the extracted.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The name of the extracted.</value>
|
|
||||||
public string ExtractedName { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the extracted year.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The extracted year.</value>
|
|
||||||
public int? ExtractedYear { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the extracted season number.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The extracted season number.</value>
|
|
||||||
public int? ExtractedSeasonNumber { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the extracted episode number.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The extracted episode number.</value>
|
|
||||||
public int? ExtractedEpisodeNumber { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the extracted ending episode number.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The extracted ending episode number.</value>
|
|
||||||
public int? ExtractedEndingEpisodeNumber { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the target path.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The target path.</value>
|
|
||||||
public string TargetPath { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the date.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The date.</value>
|
|
||||||
public DateTime Date { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the error message.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The error message.</value>
|
|
||||||
public string StatusMessage { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the status.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The status.</value>
|
|
||||||
public FileSortingStatus Status { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the type.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The type.</value>
|
|
||||||
public FileOrganizerType Type { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the duplicate paths.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The duplicate paths.</value>
|
|
||||||
public List<string> DuplicatePaths { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the size of the file.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The size of the file.</value>
|
|
||||||
public long FileSize { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates if the item is currently being processed.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>Runtime property not persisted to the store.</remarks>
|
|
||||||
public bool IsInProgress { get; set; }
|
|
||||||
|
|
||||||
public FileOrganizationResult()
|
|
||||||
{
|
|
||||||
DuplicatePaths = new List<string>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
|
|
||||||
namespace MediaBrowser.Model.FileOrganization
|
|
||||||
{
|
|
||||||
public class FileOrganizationResultQuery
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Skips over a given number of items within the results. Use for paging.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The start index.</value>
|
|
||||||
public int? StartIndex { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The maximum number of items to return
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The limit.</value>
|
|
||||||
public int? Limit { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
namespace MediaBrowser.Model.FileOrganization
|
|
||||||
{
|
|
||||||
public enum FileOrganizerType
|
|
||||||
{
|
|
||||||
Movie,
|
|
||||||
Episode,
|
|
||||||
Song
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
namespace MediaBrowser.Model.FileOrganization
|
|
||||||
{
|
|
||||||
public enum FileSortingStatus
|
|
||||||
{
|
|
||||||
Success,
|
|
||||||
Failure,
|
|
||||||
SkippedExisting
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
|
|
||||||
namespace MediaBrowser.Model.FileOrganization
|
|
||||||
{
|
|
||||||
public class SmartMatchInfo
|
|
||||||
{
|
|
||||||
public string ItemName { get; set; }
|
|
||||||
public string DisplayName { get; set; }
|
|
||||||
public FileOrganizerType OrganizerType { get; set; }
|
|
||||||
public string[] MatchStrings { get; set; }
|
|
||||||
|
|
||||||
public SmartMatchInfo()
|
|
||||||
{
|
|
||||||
MatchStrings = new string[] { };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
|
|
||||||
namespace MediaBrowser.Model.FileOrganization
|
|
||||||
{
|
|
||||||
public class TvFileOrganizationOptions
|
|
||||||
{
|
|
||||||
public bool IsEnabled { get; set; }
|
|
||||||
public int MinFileSizeMb { get; set; }
|
|
||||||
public string[] LeftOverFileExtensionsToDelete { get; set; }
|
|
||||||
public string[] WatchLocations { get; set; }
|
|
||||||
|
|
||||||
public string SeasonFolderPattern { get; set; }
|
|
||||||
|
|
||||||
public string SeasonZeroFolderName { get; set; }
|
|
||||||
|
|
||||||
public string EpisodeNamePattern { get; set; }
|
|
||||||
public string MultiEpisodeNamePattern { get; set; }
|
|
||||||
|
|
||||||
public bool OverwriteExistingEpisodes { get; set; }
|
|
||||||
|
|
||||||
public bool DeleteEmptyFolders { get; set; }
|
|
||||||
|
|
||||||
public bool CopyOriginalFile { get; set; }
|
|
||||||
|
|
||||||
public TvFileOrganizationOptions()
|
|
||||||
{
|
|
||||||
MinFileSizeMb = 50;
|
|
||||||
|
|
||||||
LeftOverFileExtensionsToDelete = new string[] { };
|
|
||||||
|
|
||||||
WatchLocations = new string[] { };
|
|
||||||
|
|
||||||
EpisodeNamePattern = "%sn - %sx%0e - %en.%ext";
|
|
||||||
MultiEpisodeNamePattern = "%sn - %sx%0e-x%0ed - %en.%ext";
|
|
||||||
SeasonFolderPattern = "Season %s";
|
|
||||||
SeasonZeroFolderName = "Season 0";
|
|
||||||
|
|
||||||
CopyOriginalFile = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -143,7 +143,6 @@
|
||||||
<Compile Include="System\IPowerManagement.cs" />
|
<Compile Include="System\IPowerManagement.cs" />
|
||||||
<Compile Include="Text\ITextEncoding.cs" />
|
<Compile Include="Text\ITextEncoding.cs" />
|
||||||
<Compile Include="Extensions\LinqExtensions.cs" />
|
<Compile Include="Extensions\LinqExtensions.cs" />
|
||||||
<Compile Include="FileOrganization\SmartMatchInfo.cs" />
|
|
||||||
<Compile Include="Health\IHealthMonitor.cs" />
|
<Compile Include="Health\IHealthMonitor.cs" />
|
||||||
<Compile Include="IO\FileSystemMetadata.cs" />
|
<Compile Include="IO\FileSystemMetadata.cs" />
|
||||||
<Compile Include="IO\IFileSystem.cs" />
|
<Compile Include="IO\IFileSystem.cs" />
|
||||||
|
@ -160,8 +159,6 @@
|
||||||
<Compile Include="Configuration\DynamicDayOfWeek.cs" />
|
<Compile Include="Configuration\DynamicDayOfWeek.cs" />
|
||||||
<Compile Include="Entities\ExtraType.cs" />
|
<Compile Include="Entities\ExtraType.cs" />
|
||||||
<Compile Include="Entities\TrailerType.cs" />
|
<Compile Include="Entities\TrailerType.cs" />
|
||||||
<Compile Include="FileOrganization\AutoOrganizeOptions.cs" />
|
|
||||||
<Compile Include="FileOrganization\TvFileOrganizationOptions.cs" />
|
|
||||||
<Compile Include="Configuration\BaseApplicationConfiguration.cs" />
|
<Compile Include="Configuration\BaseApplicationConfiguration.cs" />
|
||||||
<Compile Include="Configuration\DlnaOptions.cs" />
|
<Compile Include="Configuration\DlnaOptions.cs" />
|
||||||
<Compile Include="Configuration\ImageOption.cs" />
|
<Compile Include="Configuration\ImageOption.cs" />
|
||||||
|
@ -248,11 +245,6 @@
|
||||||
<Compile Include="Events\GenericEventArgs.cs" />
|
<Compile Include="Events\GenericEventArgs.cs" />
|
||||||
<Compile Include="Extensions\ListHelper.cs" />
|
<Compile Include="Extensions\ListHelper.cs" />
|
||||||
<Compile Include="Extensions\StringHelper.cs" />
|
<Compile Include="Extensions\StringHelper.cs" />
|
||||||
<Compile Include="FileOrganization\EpisodeFileOrganizationRequest.cs" />
|
|
||||||
<Compile Include="FileOrganization\FileOrganizationResult.cs" />
|
|
||||||
<Compile Include="FileOrganization\FileOrganizationResultQuery.cs" />
|
|
||||||
<Compile Include="FileOrganization\FileOrganizerType.cs" />
|
|
||||||
<Compile Include="FileOrganization\FileSortingStatus.cs" />
|
|
||||||
<Compile Include="Globalization\LocalizatonOption.cs" />
|
<Compile Include="Globalization\LocalizatonOption.cs" />
|
||||||
<Compile Include="IO\FileSystemEntryType.cs" />
|
<Compile Include="IO\FileSystemEntryType.cs" />
|
||||||
<Compile Include="Library\PlayAccess.cs" />
|
<Compile Include="Library\PlayAccess.cs" />
|
||||||
|
|
|
@ -174,6 +174,9 @@ namespace MediaBrowser.WebDashboard.Api
|
||||||
IPlugin plugin = null;
|
IPlugin plugin = null;
|
||||||
Stream stream = null;
|
Stream stream = null;
|
||||||
|
|
||||||
|
var isJs = false;
|
||||||
|
var isTemplate = false;
|
||||||
|
|
||||||
var page = ServerEntryPoint.Instance.PluginConfigurationPages.FirstOrDefault(p => string.Equals(p.Name, request.Name, StringComparison.OrdinalIgnoreCase));
|
var page = ServerEntryPoint.Instance.PluginConfigurationPages.FirstOrDefault(p => string.Equals(p.Name, request.Name, StringComparison.OrdinalIgnoreCase));
|
||||||
if (page != null)
|
if (page != null)
|
||||||
{
|
{
|
||||||
|
@ -188,11 +191,23 @@ namespace MediaBrowser.WebDashboard.Api
|
||||||
{
|
{
|
||||||
plugin = altPage.Item2;
|
plugin = altPage.Item2;
|
||||||
stream = _assemblyInfo.GetManifestResourceStream(plugin.GetType(), altPage.Item1.EmbeddedResourcePath);
|
stream = _assemblyInfo.GetManifestResourceStream(plugin.GetType(), altPage.Item1.EmbeddedResourcePath);
|
||||||
|
|
||||||
|
isJs = string.Equals(Path.GetExtension(altPage.Item1.EmbeddedResourcePath), ".js", StringComparison.OrdinalIgnoreCase);
|
||||||
|
isTemplate = altPage.Item1.EmbeddedResourcePath.EndsWith(".template.html");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plugin != null && stream != null)
|
if (plugin != null && stream != null)
|
||||||
{
|
{
|
||||||
|
if (isJs)
|
||||||
|
{
|
||||||
|
return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.js"), () => Task.FromResult(stream));
|
||||||
|
}
|
||||||
|
if (isTemplate)
|
||||||
|
{
|
||||||
|
return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => Task.FromResult(stream));
|
||||||
|
}
|
||||||
|
|
||||||
return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => GetPackageCreator(DashboardUIPath).ModifyHtml("dummy.html", stream, null, _appHost.ApplicationVersion.ToString(), null));
|
return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => GetPackageCreator(DashboardUIPath).ModifyHtml("dummy.html", stream, null, _appHost.ApplicationVersion.ToString(), null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
|
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
|
||||||
<metadata>
|
<metadata>
|
||||||
<id>MediaBrowser.Common</id>
|
<id>MediaBrowser.Common</id>
|
||||||
<version>3.0.708</version>
|
<version>3.0.709</version>
|
||||||
<title>Emby.Common</title>
|
<title>Emby.Common</title>
|
||||||
<authors>Emby Team</authors>
|
<authors>Emby Team</authors>
|
||||||
<owners>ebr,Luke,scottisafool</owners>
|
<owners>ebr,Luke,scottisafool</owners>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
|
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
|
||||||
<metadata>
|
<metadata>
|
||||||
<id>MediaBrowser.Server.Core</id>
|
<id>MediaBrowser.Server.Core</id>
|
||||||
<version>3.0.708</version>
|
<version>3.0.709</version>
|
||||||
<title>Emby.Server.Core</title>
|
<title>Emby.Server.Core</title>
|
||||||
<authors>Emby Team</authors>
|
<authors>Emby Team</authors>
|
||||||
<owners>ebr,Luke,scottisafool</owners>
|
<owners>ebr,Luke,scottisafool</owners>
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
<description>Contains core components required to build plugins for Emby Server.</description>
|
<description>Contains core components required to build plugins for Emby Server.</description>
|
||||||
<copyright>Copyright © Emby 2013</copyright>
|
<copyright>Copyright © Emby 2013</copyright>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency id="MediaBrowser.Common" version="3.0.708" />
|
<dependency id="MediaBrowser.Common" version="3.0.709" />
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</metadata>
|
</metadata>
|
||||||
<files>
|
<files>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user