mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-25 22:37:27 -04:00
@@ -0,0 +1,61 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Extras
|
||||
{
|
||||
public class ExistingExtraFileService : IHandle<SeriesScannedEvent>
|
||||
{
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly List<IImportExistingExtraFiles> _existingExtraFileImporters;
|
||||
private readonly List<IManageExtraFiles> _extraFileManagers;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ExistingExtraFileService(IDiskProvider diskProvider,
|
||||
List<IImportExistingExtraFiles> existingExtraFileImporters,
|
||||
List<IManageExtraFiles> extraFileManagers,
|
||||
Logger logger)
|
||||
{
|
||||
_diskProvider = diskProvider;
|
||||
_existingExtraFileImporters = existingExtraFileImporters.OrderBy(e => e.Order).ToList();
|
||||
_extraFileManagers = extraFileManagers.OrderBy(e => e.Order).ToList();
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void Handle(SeriesScannedEvent message)
|
||||
{
|
||||
var series = message.Series;
|
||||
var extraFiles = new List<ExtraFile>();
|
||||
|
||||
if (!_diskProvider.FolderExists(series.Path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Debug("Looking for existing extra files in {0}", series.Path);
|
||||
|
||||
var filesOnDisk = _diskProvider.GetFiles(series.Path, SearchOption.AllDirectories);
|
||||
var possibleExtraFiles = filesOnDisk.Where(c => !MediaFileExtensions.Extensions.Contains(Path.GetExtension(c).ToLower()) &&
|
||||
!c.StartsWith(Path.Combine(series.Path, "EXTRAS"))).ToList();
|
||||
|
||||
var filteredFiles = possibleExtraFiles;
|
||||
var importedFiles = new List<string>();
|
||||
|
||||
foreach (var existingExtraFileImporter in _existingExtraFileImporters)
|
||||
{
|
||||
var imported = existingExtraFileImporter.ProcessFiles(series, filteredFiles, importedFiles);
|
||||
|
||||
importedFiles.AddRange(imported.Select(f => Path.Combine(series.Path, f.RelativePath)));
|
||||
}
|
||||
|
||||
_logger.Info("Found {0} extra files", extraFiles);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Extras
|
||||
{
|
||||
public interface IExtraService
|
||||
{
|
||||
void ImportExtraFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly);
|
||||
}
|
||||
|
||||
public class ExtraService : IExtraService,
|
||||
IHandle<MediaCoversUpdatedEvent>,
|
||||
IHandle<EpisodeFolderCreatedEvent>,
|
||||
IHandle<SeriesRenamedEvent>
|
||||
{
|
||||
private readonly IMediaFileService _mediaFileService;
|
||||
private readonly IEpisodeService _episodeService;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly List<IManageExtraFiles> _extraFileManagers;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ExtraService(IMediaFileService mediaFileService,
|
||||
IEpisodeService episodeService,
|
||||
IDiskProvider diskProvider,
|
||||
IConfigService configService,
|
||||
List<IManageExtraFiles> extraFileManagers,
|
||||
Logger logger)
|
||||
{
|
||||
_mediaFileService = mediaFileService;
|
||||
_episodeService = episodeService;
|
||||
_diskProvider = diskProvider;
|
||||
_configService = configService;
|
||||
_extraFileManagers = extraFileManagers.OrderBy(e => e.Order).ToList();
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void ImportExtraFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly)
|
||||
{
|
||||
// TODO: Remove
|
||||
// Not importing files yet, testing that parsing is working properly first
|
||||
return;
|
||||
|
||||
var series = localEpisode.Series;
|
||||
|
||||
foreach (var extraFileManager in _extraFileManagers)
|
||||
{
|
||||
extraFileManager.CreateAfterEpisodeImport(series, episodeFile);
|
||||
}
|
||||
|
||||
var sourcePath = localEpisode.Path;
|
||||
var sourceFolder = _diskProvider.GetParentFolder(sourcePath);
|
||||
var sourceFileName = Path.GetFileNameWithoutExtension(sourcePath);
|
||||
var files = _diskProvider.GetFiles(sourceFolder, SearchOption.TopDirectoryOnly);
|
||||
|
||||
var wantedExtensions = _configService.ExtraFileExtensions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(e => e.Trim(' ', '.'))
|
||||
.ToList();
|
||||
|
||||
var matchingFilenames = files.Where(f => Path.GetFileNameWithoutExtension(f).StartsWith(sourceFileName));
|
||||
|
||||
foreach (var matchingFilename in matchingFilenames)
|
||||
{
|
||||
var matchingExtension = wantedExtensions.FirstOrDefault(e => matchingFilename.EndsWith(e));
|
||||
|
||||
if (matchingExtension == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var extraFileManager in _extraFileManagers)
|
||||
{
|
||||
var extraFile = extraFileManager.Import(series, episodeFile, matchingFilename, matchingExtension, isReadOnly);
|
||||
|
||||
if (extraFile != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Failed to import extra file: {0}", matchingFilename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Handle(MediaCoversUpdatedEvent message)
|
||||
{
|
||||
var series = message.Series;
|
||||
var episodeFiles = GetEpisodeFiles(series.Id);
|
||||
|
||||
foreach (var extraFileManager in _extraFileManagers)
|
||||
{
|
||||
extraFileManager.CreateAfterSeriesScan(series, episodeFiles);
|
||||
}
|
||||
}
|
||||
|
||||
public void Handle(EpisodeFolderCreatedEvent message)
|
||||
{
|
||||
var series = message.Series;
|
||||
|
||||
foreach (var extraFileManager in _extraFileManagers)
|
||||
{
|
||||
extraFileManager.CreateAfterEpisodeImport(series, message.SeriesFolder, message.SeasonFolder);
|
||||
}
|
||||
}
|
||||
|
||||
public void Handle(SeriesRenamedEvent message)
|
||||
{
|
||||
var series = message.Series;
|
||||
var episodeFiles = GetEpisodeFiles(series.Id);
|
||||
|
||||
foreach (var extraFileManager in _extraFileManagers)
|
||||
{
|
||||
extraFileManager.MoveFilesAfterRename(series, episodeFiles);
|
||||
}
|
||||
}
|
||||
|
||||
private List<EpisodeFile> GetEpisodeFiles(int seriesId)
|
||||
{
|
||||
var episodeFiles = _mediaFileService.GetFilesBySeries(seriesId);
|
||||
var episodes = _episodeService.GetEpisodeBySeries(seriesId);
|
||||
|
||||
foreach (var episodeFile in episodeFiles)
|
||||
{
|
||||
var localEpisodeFile = episodeFile;
|
||||
episodeFile.Episodes = new LazyList<Episode>(episodes.Where(e => e.EpisodeFileId == localEpisodeFile.Id));
|
||||
}
|
||||
|
||||
return episodeFiles;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Files
|
||||
{
|
||||
public abstract class ExtraFile : ModelBase
|
||||
{
|
||||
public int SeriesId { get; set; }
|
||||
public int? EpisodeFileId { get; set; }
|
||||
public int? SeasonNumber { get; set; }
|
||||
public string RelativePath { get; set; }
|
||||
public DateTime Added { get; set; }
|
||||
public DateTime LastUpdated { get; set; }
|
||||
public string Extension { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Files
|
||||
{
|
||||
public interface IManageExtraFiles
|
||||
{
|
||||
int Order { get; }
|
||||
IEnumerable<ExtraFile> CreateAfterSeriesScan(Series series, List<EpisodeFile> episodeFiles);
|
||||
IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile);
|
||||
IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder);
|
||||
IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> episodeFiles);
|
||||
ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly);
|
||||
}
|
||||
|
||||
public abstract class ExtraFileManager<TExtraFile> : IManageExtraFiles
|
||||
where TExtraFile : ExtraFile, new()
|
||||
|
||||
{
|
||||
private readonly IConfigService _configService;
|
||||
private readonly IDiskTransferService _diskTransferService;
|
||||
private readonly IExtraFileService<TExtraFile> _extraFileService;
|
||||
|
||||
public ExtraFileManager(IConfigService configService,
|
||||
IDiskTransferService diskTransferService,
|
||||
IExtraFileService<TExtraFile> extraFileService)
|
||||
{
|
||||
_configService = configService;
|
||||
_diskTransferService = diskTransferService;
|
||||
_extraFileService = extraFileService;
|
||||
}
|
||||
|
||||
public abstract int Order { get; }
|
||||
public abstract IEnumerable<ExtraFile> CreateAfterSeriesScan(Series series, List<EpisodeFile> episodeFiles);
|
||||
public abstract IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile);
|
||||
public abstract IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder);
|
||||
public abstract IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> episodeFiles);
|
||||
public abstract ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly);
|
||||
|
||||
protected TExtraFile ImportFile(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly)
|
||||
{
|
||||
var newFileName = Path.Combine(series.Path, Path.ChangeExtension(episodeFile.RelativePath, extension));
|
||||
|
||||
var transferMode = TransferMode.Move;
|
||||
|
||||
if (readOnly)
|
||||
{
|
||||
transferMode = _configService.CopyUsingHardlinks ? TransferMode.HardLinkOrCopy : TransferMode.Copy;
|
||||
}
|
||||
|
||||
_diskTransferService.TransferFile(path, newFileName, transferMode, true, false);
|
||||
|
||||
return new TExtraFile
|
||||
{
|
||||
SeriesId = series.Id,
|
||||
SeasonNumber = episodeFile.SeasonNumber,
|
||||
EpisodeFileId = episodeFile.Id,
|
||||
RelativePath = series.Path.GetRelativePath(newFileName),
|
||||
Extension = Path.GetExtension(path)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Files
|
||||
{
|
||||
public interface IExtraFileRepository<TExtraFile> : IBasicRepository<TExtraFile> where TExtraFile : ExtraFile, new()
|
||||
{
|
||||
void DeleteForSeries(int seriesId);
|
||||
void DeleteForSeason(int seriesId, int seasonNumber);
|
||||
void DeleteForEpisodeFile(int episodeFileId);
|
||||
List<TExtraFile> GetFilesBySeries(int seriesId);
|
||||
List<TExtraFile> GetFilesBySeason(int seriesId, int seasonNumber);
|
||||
List<TExtraFile> GetFilesByEpisodeFile(int episodeFileId);
|
||||
TExtraFile FindByPath(string path);
|
||||
}
|
||||
|
||||
public class ExtraFileRepository<TExtraFile> : BasicRepository<TExtraFile>, IExtraFileRepository<TExtraFile>
|
||||
where TExtraFile : ExtraFile, new()
|
||||
{
|
||||
public ExtraFileRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
|
||||
public void DeleteForSeries(int seriesId)
|
||||
{
|
||||
Delete(c => c.SeriesId == seriesId);
|
||||
}
|
||||
|
||||
public void DeleteForSeason(int seriesId, int seasonNumber)
|
||||
{
|
||||
Delete(c => c.SeriesId == seriesId && c.SeasonNumber == seasonNumber);
|
||||
}
|
||||
|
||||
public void DeleteForEpisodeFile(int episodeFileId)
|
||||
{
|
||||
Delete(c => c.EpisodeFileId == episodeFileId);
|
||||
}
|
||||
|
||||
public List<TExtraFile> GetFilesBySeries(int seriesId)
|
||||
{
|
||||
return Query.Where(c => c.SeriesId == seriesId);
|
||||
}
|
||||
|
||||
public List<TExtraFile> GetFilesBySeason(int seriesId, int seasonNumber)
|
||||
{
|
||||
return Query.Where(c => c.SeriesId == seriesId && c.SeasonNumber == seasonNumber);
|
||||
}
|
||||
|
||||
public List<TExtraFile> GetFilesByEpisodeFile(int episodeFileId)
|
||||
{
|
||||
return Query.Where(c => c.EpisodeFileId == episodeFileId);
|
||||
}
|
||||
|
||||
public TExtraFile FindByPath(string path)
|
||||
{
|
||||
return Query.Where(c => c.RelativePath == path).SingleOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Tv;
|
||||
using NzbDrone.Core.Tv.Events;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Files
|
||||
{
|
||||
public interface IExtraFileService<TExtraFile>
|
||||
where TExtraFile : ExtraFile, new()
|
||||
{
|
||||
List<TExtraFile> GetFilesBySeries(int seriesId);
|
||||
List<TExtraFile> GetFilesByEpisodeFile(int episodeFileId);
|
||||
TExtraFile FindByPath(string path);
|
||||
void Upsert(TExtraFile extraFile);
|
||||
void Upsert(List<TExtraFile> extraFiles);
|
||||
void Delete(int id);
|
||||
void DeleteMany(IEnumerable<int> ids);
|
||||
}
|
||||
|
||||
public abstract class ExtraFileService<TExtraFile> : IExtraFileService<TExtraFile>,
|
||||
IHandleAsync<SeriesDeletedEvent>,
|
||||
IHandleAsync<EpisodeFileDeletedEvent>
|
||||
where TExtraFile : ExtraFile, new()
|
||||
{
|
||||
private readonly IExtraFileRepository<TExtraFile> _repository;
|
||||
private readonly ISeriesService _seriesService;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ExtraFileService(IExtraFileRepository<TExtraFile> repository,
|
||||
ISeriesService seriesService,
|
||||
IDiskProvider diskProvider,
|
||||
Logger logger)
|
||||
{
|
||||
_repository = repository;
|
||||
_seriesService = seriesService;
|
||||
_diskProvider = diskProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public List<TExtraFile> GetFilesBySeries(int seriesId)
|
||||
{
|
||||
return _repository.GetFilesBySeries(seriesId);
|
||||
}
|
||||
|
||||
public List<TExtraFile> GetFilesByEpisodeFile(int episodeFileId)
|
||||
{
|
||||
return _repository.GetFilesByEpisodeFile(episodeFileId);
|
||||
}
|
||||
|
||||
public TExtraFile FindByPath(string path)
|
||||
{
|
||||
return _repository.FindByPath(path);
|
||||
}
|
||||
|
||||
public void Upsert(TExtraFile extraFile)
|
||||
{
|
||||
Upsert(new List<TExtraFile> { extraFile });
|
||||
}
|
||||
|
||||
public void Upsert(List<TExtraFile> extraFiles)
|
||||
{
|
||||
extraFiles.ForEach(m =>
|
||||
{
|
||||
m.LastUpdated = DateTime.UtcNow;
|
||||
|
||||
if (m.Id == 0)
|
||||
{
|
||||
m.Added = m.LastUpdated;
|
||||
}
|
||||
});
|
||||
|
||||
_repository.InsertMany(extraFiles.Where(m => m.Id == 0).ToList());
|
||||
_repository.UpdateMany(extraFiles.Where(m => m.Id > 0).ToList());
|
||||
}
|
||||
|
||||
public void Delete(int id)
|
||||
{
|
||||
_repository.Delete(id);
|
||||
}
|
||||
|
||||
public void DeleteMany(IEnumerable<int> ids)
|
||||
{
|
||||
_repository.DeleteMany(ids);
|
||||
}
|
||||
|
||||
public void HandleAsync(SeriesDeletedEvent message)
|
||||
{
|
||||
_logger.Debug("Deleting Extra from database for series: {0}", message.Series);
|
||||
_repository.DeleteForSeries(message.Series.Id);
|
||||
}
|
||||
|
||||
public void HandleAsync(EpisodeFileDeletedEvent message)
|
||||
{
|
||||
var episodeFile = message.EpisodeFile;
|
||||
var series = _seriesService.GetSeries(message.EpisodeFile.SeriesId);
|
||||
|
||||
foreach (var extra in _repository.GetFilesByEpisodeFile(episodeFile.Id))
|
||||
{
|
||||
var path = Path.Combine(series.Path, extra.RelativePath);
|
||||
|
||||
if (_diskProvider.FileExists(path))
|
||||
{
|
||||
_diskProvider.DeleteFile(path);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Debug("Deleting Extra from database for episode file: {0}", episodeFile);
|
||||
_repository.DeleteForEpisodeFile(episodeFile.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Extras
|
||||
{
|
||||
public interface IImportExistingExtraFiles
|
||||
{
|
||||
int Order { get; }
|
||||
IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Extras
|
||||
{
|
||||
public abstract class ImportExistingExtraFilesBase<TExtraFile> : IImportExistingExtraFiles
|
||||
where TExtraFile : ExtraFile, new()
|
||||
{
|
||||
private readonly IExtraFileService<TExtraFile> _extraFileService;
|
||||
|
||||
public ImportExistingExtraFilesBase(IExtraFileService<TExtraFile> extraFileService)
|
||||
{
|
||||
_extraFileService = extraFileService;
|
||||
}
|
||||
|
||||
public abstract int Order { get; }
|
||||
public abstract IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles);
|
||||
|
||||
public virtual List<string> FilterAndClean(Series series, List<string> filesOnDisk, List<string> importedFiles)
|
||||
{
|
||||
var seriesFiles = _extraFileService.GetFilesBySeries(series.Id);
|
||||
|
||||
Clean(series, filesOnDisk, importedFiles, seriesFiles);
|
||||
|
||||
return Filter(series, filesOnDisk, importedFiles, seriesFiles);
|
||||
}
|
||||
|
||||
private List<string> Filter(Series series, List<string> filesOnDisk, List<string> importedFiles, List<TExtraFile> seriesFiles)
|
||||
{
|
||||
var filteredFiles = filesOnDisk;
|
||||
|
||||
filteredFiles = filteredFiles.Except(seriesFiles.Select(f => Path.Combine(series.Path, f.RelativePath)).ToList(), PathEqualityComparer.Instance).ToList();
|
||||
return filteredFiles.Except(importedFiles, PathEqualityComparer.Instance).ToList();
|
||||
}
|
||||
|
||||
private void Clean(Series series, List<string> filesOnDisk, List<string> importedFiles, List<TExtraFile> seriesFiles)
|
||||
{
|
||||
var alreadyImportedFileIds = seriesFiles.IntersectBy(f => Path.Combine(series.Path, f.RelativePath), importedFiles, i => i, PathEqualityComparer.Instance)
|
||||
.Select(f => f.Id);
|
||||
|
||||
var deletedFiles = seriesFiles.ExceptBy(f => Path.Combine(series.Path, f.RelativePath), filesOnDisk, i => i, PathEqualityComparer.Instance)
|
||||
.Select(f => f.Id);
|
||||
|
||||
_extraFileService.DeleteMany(alreadyImportedFileIds);
|
||||
_extraFileService.DeleteMany(deletedFiles);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Extras.Metadata.Files;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser
|
||||
{
|
||||
public class MediaBrowserMetadata : MetadataBase<MediaBrowserMetadataSettings>
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
|
||||
public MediaBrowserMetadata(
|
||||
Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Emby (Legacy)";
|
||||
}
|
||||
}
|
||||
|
||||
public override MetadataFile FindMetadataFile(Series series, string path)
|
||||
{
|
||||
var filename = Path.GetFileName(path);
|
||||
|
||||
if (filename == null) return null;
|
||||
|
||||
var metadata = new MetadataFile
|
||||
{
|
||||
SeriesId = series.Id,
|
||||
Consumer = GetType().Name,
|
||||
RelativePath = series.Path.GetRelativePath(path)
|
||||
};
|
||||
|
||||
if (filename.Equals("series.xml", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
metadata.Type = MetadataType.SeriesMetadata;
|
||||
return metadata;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override MetadataFileResult SeriesMetadata(Series series)
|
||||
{
|
||||
if (!Settings.SeriesMetadata)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.Debug("Generating series.xml for: {0}", series.Title);
|
||||
var sb = new StringBuilder();
|
||||
var xws = new XmlWriterSettings();
|
||||
xws.OmitXmlDeclaration = true;
|
||||
xws.Indent = false;
|
||||
|
||||
using (var xw = XmlWriter.Create(sb, xws))
|
||||
{
|
||||
var tvShow = new XElement("Series");
|
||||
|
||||
tvShow.Add(new XElement("id", series.TvdbId));
|
||||
tvShow.Add(new XElement("Status", series.Status));
|
||||
tvShow.Add(new XElement("Network", series.Network));
|
||||
tvShow.Add(new XElement("Airs_Time", series.AirTime));
|
||||
|
||||
if (series.FirstAired.HasValue)
|
||||
{
|
||||
tvShow.Add(new XElement("FirstAired", series.FirstAired.Value.ToString("yyyy-MM-dd")));
|
||||
}
|
||||
|
||||
tvShow.Add(new XElement("ContentRating", series.Certification));
|
||||
tvShow.Add(new XElement("Added", series.Added.ToString("MM/dd/yyyy HH:mm:ss tt")));
|
||||
tvShow.Add(new XElement("LockData", "false"));
|
||||
tvShow.Add(new XElement("Overview", series.Overview));
|
||||
tvShow.Add(new XElement("LocalTitle", series.Title));
|
||||
|
||||
if (series.FirstAired.HasValue)
|
||||
{
|
||||
tvShow.Add(new XElement("PremiereDate", series.FirstAired.Value.ToString("yyyy-MM-dd")));
|
||||
}
|
||||
|
||||
tvShow.Add(new XElement("Rating", series.Ratings.Value));
|
||||
tvShow.Add(new XElement("ProductionYear", series.Year));
|
||||
tvShow.Add(new XElement("RunningTime", series.Runtime));
|
||||
tvShow.Add(new XElement("IMDB", series.ImdbId));
|
||||
tvShow.Add(new XElement("TVRageId", series.TvRageId));
|
||||
tvShow.Add(new XElement("Genres", series.Genres.Select(genre => new XElement("Genre", genre))));
|
||||
|
||||
var persons = new XElement("Persons");
|
||||
|
||||
foreach (var person in series.Actors)
|
||||
{
|
||||
persons.Add(new XElement("Person",
|
||||
new XElement("Name", person.Name),
|
||||
new XElement("Type", "Actor"),
|
||||
new XElement("Role", person.Character)
|
||||
));
|
||||
}
|
||||
|
||||
tvShow.Add(persons);
|
||||
|
||||
|
||||
var doc = new XDocument(tvShow);
|
||||
doc.Save(xw);
|
||||
|
||||
_logger.Debug("Saving series.xml for {0}", series.Title);
|
||||
|
||||
return new MetadataFileResult("series.xml", doc.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public override List<ImageFileResult> SeriesImages(Series series)
|
||||
{
|
||||
return new List<ImageFileResult>();
|
||||
}
|
||||
|
||||
public override List<ImageFileResult> SeasonImages(Series series, Season season)
|
||||
{
|
||||
return new List<ImageFileResult>();
|
||||
}
|
||||
|
||||
public override List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile)
|
||||
{
|
||||
return new List<ImageFileResult>();
|
||||
}
|
||||
|
||||
private IEnumerable<ImageFileResult> ProcessSeriesImages(Series series)
|
||||
{
|
||||
return new List<ImageFileResult>();
|
||||
}
|
||||
|
||||
private IEnumerable<ImageFileResult> ProcessSeasonImages(Series series, Season season)
|
||||
{
|
||||
return new List<ImageFileResult>();
|
||||
}
|
||||
|
||||
private string GetEpisodeNfoFilename(string episodeFilePath)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
private string GetEpisodeImageFilename(string episodeFilePath)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser
|
||||
{
|
||||
public class MediaBrowserSettingsValidator : AbstractValidator<MediaBrowserMetadataSettings>
|
||||
{
|
||||
public MediaBrowserSettingsValidator()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class MediaBrowserMetadataSettings : IProviderConfig
|
||||
{
|
||||
private static readonly MediaBrowserSettingsValidator Validator = new MediaBrowserSettingsValidator();
|
||||
|
||||
public MediaBrowserMetadataSettings()
|
||||
{
|
||||
SeriesMetadata = true;
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Series Metadata", Type = FieldType.Checkbox)]
|
||||
public bool SeriesMetadata { get; set; }
|
||||
|
||||
public bool IsValid
|
||||
{
|
||||
get
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.Extras.Metadata.Files;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
|
||||
{
|
||||
public class RoksboxMetadata : MetadataBase<RoksboxMetadataSettings>
|
||||
{
|
||||
private readonly IMapCoversToLocal _mediaCoverService;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public RoksboxMetadata(IMapCoversToLocal mediaCoverService,
|
||||
IDiskProvider diskProvider,
|
||||
Logger logger)
|
||||
{
|
||||
_mediaCoverService = mediaCoverService;
|
||||
_diskProvider = diskProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private static List<string> ValidCertification = new List<string> { "G", "NC-17", "PG", "PG-13", "R", "UR", "UNRATED", "NR", "TV-Y", "TV-Y7", "TV-Y7-FV", "TV-G", "TV-PG", "TV-14", "TV-MA" };
|
||||
private static readonly Regex SeasonImagesRegex = new Regex(@"^(season (?<season>\d+))|(?<specials>specials)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
public override string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Roksbox";
|
||||
}
|
||||
}
|
||||
|
||||
public override string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile)
|
||||
{
|
||||
var episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath);
|
||||
|
||||
if (metadataFile.Type == MetadataType.EpisodeImage)
|
||||
{
|
||||
return GetEpisodeImageFilename(episodeFilePath);
|
||||
}
|
||||
|
||||
if (metadataFile.Type == MetadataType.EpisodeMetadata)
|
||||
{
|
||||
return GetEpisodeMetadataFilename(episodeFilePath);
|
||||
}
|
||||
|
||||
_logger.Debug("Unknown episode file metadata: {0}", metadataFile.RelativePath);
|
||||
return Path.Combine(series.Path, metadataFile.RelativePath);
|
||||
}
|
||||
|
||||
public override MetadataFile FindMetadataFile(Series series, string path)
|
||||
{
|
||||
var filename = Path.GetFileName(path);
|
||||
|
||||
if (filename == null) return null;
|
||||
var parentdir = Directory.GetParent(path);
|
||||
|
||||
var metadata = new MetadataFile
|
||||
{
|
||||
SeriesId = series.Id,
|
||||
Consumer = GetType().Name,
|
||||
RelativePath = series.Path.GetRelativePath(path)
|
||||
};
|
||||
|
||||
//Series and season images are both named folder.jpg, only season ones sit in season folders
|
||||
if (Path.GetFileNameWithoutExtension(filename).Equals(parentdir.Name, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
var seasonMatch = SeasonImagesRegex.Match(parentdir.Name);
|
||||
|
||||
if (seasonMatch.Success)
|
||||
{
|
||||
metadata.Type = MetadataType.SeasonImage;
|
||||
|
||||
if (seasonMatch.Groups["specials"].Success)
|
||||
{
|
||||
metadata.SeasonNumber = 0;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
metadata.SeasonNumber = Convert.ToInt32(seasonMatch.Groups["season"].Value);
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
metadata.Type = MetadataType.SeriesImage;
|
||||
return metadata;
|
||||
}
|
||||
|
||||
var parseResult = Parser.Parser.ParseTitle(filename);
|
||||
|
||||
if (parseResult != null &&
|
||||
!parseResult.FullSeason)
|
||||
{
|
||||
var extension = Path.GetExtension(filename).ToLowerInvariant();
|
||||
|
||||
if (extension == ".xml")
|
||||
{
|
||||
metadata.Type = MetadataType.EpisodeMetadata;
|
||||
return metadata;
|
||||
}
|
||||
|
||||
if (extension == ".jpg")
|
||||
{
|
||||
if (!Path.GetFileNameWithoutExtension(filename).EndsWith("-thumb"))
|
||||
{
|
||||
metadata.Type = MetadataType.EpisodeImage;
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override MetadataFileResult SeriesMetadata(Series series)
|
||||
{
|
||||
//Series metadata is not supported
|
||||
return null;
|
||||
}
|
||||
|
||||
public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile)
|
||||
{
|
||||
if (!Settings.EpisodeMetadata)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.Debug("Generating Episode Metadata for: {0}", episodeFile.RelativePath);
|
||||
|
||||
var xmlResult = string.Empty;
|
||||
foreach (var episode in episodeFile.Episodes.Value)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var xws = new XmlWriterSettings();
|
||||
xws.OmitXmlDeclaration = true;
|
||||
xws.Indent = false;
|
||||
|
||||
using (var xw = XmlWriter.Create(sb, xws))
|
||||
{
|
||||
var doc = new XDocument();
|
||||
|
||||
var details = new XElement("video");
|
||||
details.Add(new XElement("title", string.Format("{0} - {1}x{2} - {3}", series.Title, episode.SeasonNumber, episode.EpisodeNumber, episode.Title)));
|
||||
details.Add(new XElement("year", episode.AirDate));
|
||||
details.Add(new XElement("genre", string.Join(" / ", series.Genres)));
|
||||
var actors = string.Join(" , ", series.Actors.ConvertAll(c => c.Name + " - " + c.Character).GetRange(0, Math.Min(3, series.Actors.Count)));
|
||||
details.Add(new XElement("actors", actors));
|
||||
details.Add(new XElement("description", episode.Overview));
|
||||
details.Add(new XElement("length", series.Runtime));
|
||||
|
||||
if (series.Certification.IsNotNullOrWhiteSpace() &&
|
||||
ValidCertification.Contains(series.Certification.ToUpperInvariant()))
|
||||
{
|
||||
details.Add(new XElement("mpaa", series.Certification.ToUpperInvariant()));
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
details.Add(new XElement("mpaa", "UNRATED"));
|
||||
}
|
||||
|
||||
doc.Add(details);
|
||||
doc.Save(xw);
|
||||
|
||||
xmlResult += doc.ToString();
|
||||
xmlResult += Environment.NewLine;
|
||||
}
|
||||
}
|
||||
|
||||
return new MetadataFileResult(GetEpisodeMetadataFilename(episodeFile.RelativePath), xmlResult.Trim(Environment.NewLine.ToCharArray()));
|
||||
}
|
||||
|
||||
public override List<ImageFileResult> SeriesImages(Series series)
|
||||
{
|
||||
var image = series.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? series.Images.FirstOrDefault();
|
||||
if (image == null)
|
||||
{
|
||||
_logger.Trace("Failed to find suitable Series image for series {0}.", series.Title);
|
||||
return null;
|
||||
}
|
||||
|
||||
var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType);
|
||||
var destination = Path.Combine(series.Path, Path.GetFileName(series.Path) + Path.GetExtension(source));
|
||||
|
||||
return new List<ImageFileResult>{ new ImageFileResult(destination, source) };
|
||||
}
|
||||
|
||||
public override List<ImageFileResult> SeasonImages(Series series, Season season)
|
||||
{
|
||||
var seasonFolders = GetSeasonFolders(series);
|
||||
|
||||
string seasonFolder;
|
||||
if (!seasonFolders.TryGetValue(season.SeasonNumber, out seasonFolder))
|
||||
{
|
||||
_logger.Trace("Failed to find season folder for series {0}, season {1}.", series.Title, season.SeasonNumber);
|
||||
return new List<ImageFileResult>();
|
||||
}
|
||||
|
||||
//Roksbox only supports one season image, so first of all try for poster otherwise just use whatever is first in the collection
|
||||
var image = season.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? season.Images.FirstOrDefault();
|
||||
if (image == null)
|
||||
{
|
||||
_logger.Trace("Failed to find suitable season image for series {0}, season {1}.", series.Title, season.SeasonNumber);
|
||||
return new List<ImageFileResult>();
|
||||
}
|
||||
|
||||
var filename = Path.GetFileName(seasonFolder) + ".jpg";
|
||||
var path = Path.Combine(series.Path, seasonFolder, filename);
|
||||
|
||||
return new List<ImageFileResult> { new ImageFileResult(path, image.Url) };
|
||||
}
|
||||
|
||||
public override List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile)
|
||||
{
|
||||
var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot);
|
||||
|
||||
if (screenshot == null)
|
||||
{
|
||||
_logger.Trace("Episode screenshot not available");
|
||||
return new List<ImageFileResult>();
|
||||
}
|
||||
|
||||
return new List<ImageFileResult> {new ImageFileResult(GetEpisodeImageFilename(episodeFile.RelativePath), screenshot.Url)};
|
||||
}
|
||||
|
||||
private string GetEpisodeMetadataFilename(string episodeFilePath)
|
||||
{
|
||||
return Path.ChangeExtension(episodeFilePath, "xml");
|
||||
}
|
||||
|
||||
private string GetEpisodeImageFilename(string episodeFilePath)
|
||||
{
|
||||
return Path.ChangeExtension(episodeFilePath, "jpg");
|
||||
}
|
||||
|
||||
private Dictionary<int, string> GetSeasonFolders(Series series)
|
||||
{
|
||||
var seasonFolderMap = new Dictionary<int, string>();
|
||||
|
||||
foreach (var folder in _diskProvider.GetDirectories(series.Path))
|
||||
{
|
||||
var directoryinfo = new DirectoryInfo(folder);
|
||||
var seasonMatch = SeasonImagesRegex.Match(directoryinfo.Name);
|
||||
|
||||
if (seasonMatch.Success)
|
||||
{
|
||||
var seasonNumber = seasonMatch.Groups["season"].Value;
|
||||
|
||||
if (seasonNumber.Contains("specials"))
|
||||
{
|
||||
seasonFolderMap[0] = folder;
|
||||
}
|
||||
else
|
||||
{
|
||||
int matchedSeason;
|
||||
if (int.TryParse(seasonNumber, out matchedSeason))
|
||||
{
|
||||
seasonFolderMap[matchedSeason] = folder;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug("Failed to parse season number from {0} for series {1}.", folder, series.Title);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug("Rejecting folder {0} for series {1}.", Path.GetDirectoryName(folder), series.Title);
|
||||
}
|
||||
}
|
||||
|
||||
return seasonFolderMap;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
|
||||
{
|
||||
public class RoksboxSettingsValidator : AbstractValidator<RoksboxMetadataSettings>
|
||||
{
|
||||
public RoksboxSettingsValidator()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class RoksboxMetadataSettings : IProviderConfig
|
||||
{
|
||||
private static readonly RoksboxSettingsValidator Validator = new RoksboxSettingsValidator();
|
||||
|
||||
public RoksboxMetadataSettings()
|
||||
{
|
||||
EpisodeMetadata = true;
|
||||
SeriesImages = true;
|
||||
SeasonImages = true;
|
||||
EpisodeImages = true;
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox)]
|
||||
public bool EpisodeMetadata { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Series Images", Type = FieldType.Checkbox)]
|
||||
public bool SeriesImages { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Season Images", Type = FieldType.Checkbox)]
|
||||
public bool SeasonImages { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox)]
|
||||
public bool EpisodeImages { get; set; }
|
||||
|
||||
public bool IsValid
|
||||
{
|
||||
get
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.Extras.Metadata.Files;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
|
||||
{
|
||||
public class WdtvMetadata : MetadataBase<WdtvMetadataSettings>
|
||||
{
|
||||
private readonly IMapCoversToLocal _mediaCoverService;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public WdtvMetadata(IMapCoversToLocal mediaCoverService,
|
||||
IDiskProvider diskProvider,
|
||||
Logger logger)
|
||||
{
|
||||
_mediaCoverService = mediaCoverService;
|
||||
_diskProvider = diskProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private static readonly Regex SeasonImagesRegex = new Regex(@"^(season (?<season>\d+))|(?<specials>specials)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
public override string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return "WDTV";
|
||||
}
|
||||
}
|
||||
|
||||
public override string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile)
|
||||
{
|
||||
var episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath);
|
||||
|
||||
if (metadataFile.Type == MetadataType.EpisodeImage)
|
||||
{
|
||||
return GetEpisodeImageFilename(episodeFilePath);
|
||||
}
|
||||
|
||||
if (metadataFile.Type == MetadataType.EpisodeMetadata)
|
||||
{
|
||||
return GetEpisodeMetadataFilename(episodeFilePath);
|
||||
}
|
||||
|
||||
_logger.Debug("Unknown episode file metadata: {0}", metadataFile.RelativePath);
|
||||
return Path.Combine(series.Path, metadataFile.RelativePath);
|
||||
|
||||
}
|
||||
|
||||
public override MetadataFile FindMetadataFile(Series series, string path)
|
||||
{
|
||||
var filename = Path.GetFileName(path);
|
||||
|
||||
if (filename == null) return null;
|
||||
|
||||
var metadata = new MetadataFile
|
||||
{
|
||||
SeriesId = series.Id,
|
||||
Consumer = GetType().Name,
|
||||
RelativePath = series.Path.GetRelativePath(path)
|
||||
};
|
||||
|
||||
//Series and season images are both named folder.jpg, only season ones sit in season folders
|
||||
if (Path.GetFileName(filename).Equals("folder.jpg", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
var parentdir = Directory.GetParent(path);
|
||||
var seasonMatch = SeasonImagesRegex.Match(parentdir.Name);
|
||||
if (seasonMatch.Success)
|
||||
{
|
||||
metadata.Type = MetadataType.SeasonImage;
|
||||
|
||||
if (seasonMatch.Groups["specials"].Success)
|
||||
{
|
||||
metadata.SeasonNumber = 0;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
metadata.SeasonNumber = Convert.ToInt32(seasonMatch.Groups["season"].Value);
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
metadata.Type = MetadataType.SeriesImage;
|
||||
return metadata;
|
||||
}
|
||||
|
||||
var parseResult = Parser.Parser.ParseTitle(filename);
|
||||
|
||||
if (parseResult != null &&
|
||||
!parseResult.FullSeason)
|
||||
{
|
||||
switch (Path.GetExtension(filename).ToLowerInvariant())
|
||||
{
|
||||
case ".xml":
|
||||
metadata.Type = MetadataType.EpisodeMetadata;
|
||||
return metadata;
|
||||
case ".metathumb":
|
||||
metadata.Type = MetadataType.EpisodeImage;
|
||||
return metadata;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override MetadataFileResult SeriesMetadata(Series series)
|
||||
{
|
||||
//Series metadata is not supported
|
||||
return null;
|
||||
}
|
||||
|
||||
public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile)
|
||||
{
|
||||
if (!Settings.EpisodeMetadata)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.Debug("Generating Episode Metadata for: {0}", Path.Combine(series.Path, episodeFile.RelativePath));
|
||||
|
||||
var xmlResult = string.Empty;
|
||||
foreach (var episode in episodeFile.Episodes.Value)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var xws = new XmlWriterSettings();
|
||||
xws.OmitXmlDeclaration = true;
|
||||
xws.Indent = false;
|
||||
|
||||
using (var xw = XmlWriter.Create(sb, xws))
|
||||
{
|
||||
var doc = new XDocument();
|
||||
|
||||
var details = new XElement("details");
|
||||
details.Add(new XElement("id", series.Id));
|
||||
details.Add(new XElement("title", string.Format("{0} - {1}x{2:00} - {3}", series.Title, episode.SeasonNumber, episode.EpisodeNumber, episode.Title)));
|
||||
details.Add(new XElement("series_name", series.Title));
|
||||
details.Add(new XElement("episode_name", episode.Title));
|
||||
details.Add(new XElement("season_number", episode.SeasonNumber.ToString("00")));
|
||||
details.Add(new XElement("episode_number", episode.EpisodeNumber.ToString("00")));
|
||||
details.Add(new XElement("firstaired", episode.AirDate));
|
||||
details.Add(new XElement("genre", string.Join(" / ", series.Genres)));
|
||||
details.Add(new XElement("actor", string.Join(" / ", series.Actors.ConvertAll(c => c.Name + " - " + c.Character))));
|
||||
details.Add(new XElement("overview", episode.Overview));
|
||||
|
||||
|
||||
//Todo: get guest stars, writer and director
|
||||
//details.Add(new XElement("credits", tvdbEpisode.Writer.FirstOrDefault()));
|
||||
//details.Add(new XElement("director", tvdbEpisode.Directors.FirstOrDefault()));
|
||||
|
||||
doc.Add(details);
|
||||
doc.Save(xw);
|
||||
|
||||
xmlResult += doc.ToString();
|
||||
xmlResult += Environment.NewLine;
|
||||
}
|
||||
}
|
||||
|
||||
var filename = GetEpisodeMetadataFilename(episodeFile.RelativePath);
|
||||
|
||||
return new MetadataFileResult(filename, xmlResult.Trim(Environment.NewLine.ToCharArray()));
|
||||
}
|
||||
|
||||
public override List<ImageFileResult> SeriesImages(Series series)
|
||||
{
|
||||
if (!Settings.SeriesImages)
|
||||
{
|
||||
return new List<ImageFileResult>();
|
||||
}
|
||||
|
||||
//Because we only support one image, attempt to get the Poster type, then if that fails grab the first
|
||||
var image = series.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? series.Images.FirstOrDefault();
|
||||
if (image == null)
|
||||
{
|
||||
_logger.Trace("Failed to find suitable Series image for series {0}.", series.Title);
|
||||
return new List<ImageFileResult>();
|
||||
}
|
||||
|
||||
var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType);
|
||||
var destination = "folder" + Path.GetExtension(source);
|
||||
|
||||
return new List<ImageFileResult>
|
||||
{
|
||||
new ImageFileResult(destination, source)
|
||||
};
|
||||
}
|
||||
|
||||
public override List<ImageFileResult> SeasonImages(Series series, Season season)
|
||||
{
|
||||
if (!Settings.SeasonImages)
|
||||
{
|
||||
return new List<ImageFileResult>();
|
||||
}
|
||||
|
||||
var seasonFolders = GetSeasonFolders(series);
|
||||
|
||||
//Work out the path to this season - if we don't have a matching path then skip this season.
|
||||
string seasonFolder;
|
||||
if (!seasonFolders.TryGetValue(season.SeasonNumber, out seasonFolder))
|
||||
{
|
||||
_logger.Trace("Failed to find season folder for series {0}, season {1}.", series.Title, season.SeasonNumber);
|
||||
return new List<ImageFileResult>();
|
||||
}
|
||||
|
||||
//WDTV only supports one season image, so first of all try for poster otherwise just use whatever is first in the collection
|
||||
var image = season.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? season.Images.FirstOrDefault();
|
||||
if (image == null)
|
||||
{
|
||||
_logger.Trace("Failed to find suitable season image for series {0}, season {1}.", series.Title, season.SeasonNumber);
|
||||
return new List<ImageFileResult>();
|
||||
}
|
||||
|
||||
var path = Path.Combine(seasonFolder, "folder.jpg");
|
||||
|
||||
return new List<ImageFileResult>{ new ImageFileResult(path, image.Url) };
|
||||
}
|
||||
|
||||
public override List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile)
|
||||
{
|
||||
if (!Settings.EpisodeImages)
|
||||
{
|
||||
return new List<ImageFileResult>();
|
||||
}
|
||||
|
||||
var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot);
|
||||
|
||||
if (screenshot == null)
|
||||
{
|
||||
_logger.Trace("Episode screenshot not available");
|
||||
return new List<ImageFileResult>();
|
||||
}
|
||||
|
||||
return new List<ImageFileResult>{ new ImageFileResult(GetEpisodeImageFilename(episodeFile.RelativePath), screenshot.Url) };
|
||||
}
|
||||
|
||||
private string GetEpisodeMetadataFilename(string episodeFilePath)
|
||||
{
|
||||
return Path.ChangeExtension(episodeFilePath, "xml");
|
||||
}
|
||||
|
||||
private string GetEpisodeImageFilename(string episodeFilePath)
|
||||
{
|
||||
return Path.ChangeExtension(episodeFilePath, "metathumb");
|
||||
}
|
||||
|
||||
private Dictionary<int, string> GetSeasonFolders(Series series)
|
||||
{
|
||||
var seasonFolderMap = new Dictionary<int, string>();
|
||||
|
||||
foreach (var folder in _diskProvider.GetDirectories(series.Path))
|
||||
{
|
||||
var directoryinfo = new DirectoryInfo(folder);
|
||||
var seasonMatch = SeasonImagesRegex.Match(directoryinfo.Name);
|
||||
|
||||
if (seasonMatch.Success)
|
||||
{
|
||||
var seasonNumber = seasonMatch.Groups["season"].Value;
|
||||
|
||||
if (seasonNumber.Contains("specials"))
|
||||
{
|
||||
seasonFolderMap[0] = folder;
|
||||
}
|
||||
else
|
||||
{
|
||||
int matchedSeason;
|
||||
if (int.TryParse(seasonNumber, out matchedSeason))
|
||||
{
|
||||
seasonFolderMap[matchedSeason] = folder;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug("Failed to parse season number from {0} for series {1}.", folder, series.Title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
_logger.Debug("Rejecting folder {0} for series {1}.", Path.GetDirectoryName(folder), series.Title);
|
||||
}
|
||||
}
|
||||
|
||||
return seasonFolderMap;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
|
||||
{
|
||||
public class WdtvSettingsValidator : AbstractValidator<WdtvMetadataSettings>
|
||||
{
|
||||
public WdtvSettingsValidator()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class WdtvMetadataSettings : IProviderConfig
|
||||
{
|
||||
private static readonly WdtvSettingsValidator Validator = new WdtvSettingsValidator();
|
||||
|
||||
public WdtvMetadataSettings()
|
||||
{
|
||||
EpisodeMetadata = true;
|
||||
SeriesImages = true;
|
||||
SeasonImages = true;
|
||||
EpisodeImages = true;
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox)]
|
||||
public bool EpisodeMetadata { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Series Images", Type = FieldType.Checkbox)]
|
||||
public bool SeriesImages { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Season Images", Type = FieldType.Checkbox)]
|
||||
public bool SeasonImages { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox)]
|
||||
public bool EpisodeImages { get; set; }
|
||||
|
||||
public bool IsValid
|
||||
{
|
||||
get
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.Extras.Metadata.Files;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
|
||||
{
|
||||
public class XbmcMetadata : MetadataBase<XbmcMetadataSettings>
|
||||
{
|
||||
private readonly IMapCoversToLocal _mediaCoverService;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public XbmcMetadata(IMapCoversToLocal mediaCoverService,
|
||||
IDiskProvider diskProvider,
|
||||
Logger logger)
|
||||
{
|
||||
_mediaCoverService = mediaCoverService;
|
||||
_diskProvider = diskProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private static readonly Regex SeriesImagesRegex = new Regex(@"^(?<type>poster|banner|fanart)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Regex SeasonImagesRegex = new Regex(@"^season(?<season>\d{2,}|-all|-specials)-(?<type>poster|banner|fanart)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Regex EpisodeImageRegex = new Regex(@"-thumb\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
public override string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Kodi (XBMC) / Emby";
|
||||
}
|
||||
}
|
||||
|
||||
public override string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile)
|
||||
{
|
||||
var episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath);
|
||||
|
||||
if (metadataFile.Type == MetadataType.EpisodeImage)
|
||||
{
|
||||
return GetEpisodeImageFilename(episodeFilePath);
|
||||
}
|
||||
|
||||
if (metadataFile.Type == MetadataType.EpisodeMetadata)
|
||||
{
|
||||
return GetEpisodeMetadataFilename(episodeFilePath);
|
||||
}
|
||||
|
||||
_logger.Debug("Unknown episode file metadata: {0}", metadataFile.RelativePath);
|
||||
return Path.Combine(series.Path, metadataFile.RelativePath);
|
||||
}
|
||||
|
||||
public override MetadataFile FindMetadataFile(Series series, string path)
|
||||
{
|
||||
var filename = Path.GetFileName(path);
|
||||
|
||||
if (filename == null) return null;
|
||||
|
||||
var metadata = new MetadataFile
|
||||
{
|
||||
SeriesId = series.Id,
|
||||
Consumer = GetType().Name,
|
||||
RelativePath = series.Path.GetRelativePath(path)
|
||||
};
|
||||
|
||||
if (SeriesImagesRegex.IsMatch(filename))
|
||||
{
|
||||
metadata.Type = MetadataType.SeriesImage;
|
||||
return metadata;
|
||||
}
|
||||
|
||||
var seasonMatch = SeasonImagesRegex.Match(filename);
|
||||
|
||||
if (seasonMatch.Success)
|
||||
{
|
||||
metadata.Type = MetadataType.SeasonImage;
|
||||
|
||||
var seasonNumberMatch = seasonMatch.Groups["season"].Value;
|
||||
int seasonNumber;
|
||||
|
||||
if (seasonNumberMatch.Contains("specials"))
|
||||
{
|
||||
metadata.SeasonNumber = 0;
|
||||
}
|
||||
|
||||
else if (int.TryParse(seasonNumberMatch, out seasonNumber))
|
||||
{
|
||||
metadata.SeasonNumber = seasonNumber;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
if (EpisodeImageRegex.IsMatch(filename))
|
||||
{
|
||||
metadata.Type = MetadataType.EpisodeImage;
|
||||
return metadata;
|
||||
}
|
||||
|
||||
if (filename.Equals("tvshow.nfo", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
metadata.Type = MetadataType.SeriesMetadata;
|
||||
return metadata;
|
||||
}
|
||||
|
||||
var parseResult = Parser.Parser.ParseTitle(filename);
|
||||
|
||||
if (parseResult != null &&
|
||||
!parseResult.FullSeason &&
|
||||
Path.GetExtension(filename) == ".nfo")
|
||||
{
|
||||
metadata.Type = MetadataType.EpisodeMetadata;
|
||||
return metadata;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override MetadataFileResult SeriesMetadata(Series series)
|
||||
{
|
||||
if (!Settings.SeriesMetadata)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.Debug("Generating tvshow.nfo for: {0}", series.Title);
|
||||
var sb = new StringBuilder();
|
||||
var xws = new XmlWriterSettings();
|
||||
xws.OmitXmlDeclaration = true;
|
||||
xws.Indent = false;
|
||||
|
||||
var episodeGuideUrl = string.Format("http://www.thetvdb.com/api/1D62F2F90030C444/series/{0}/all/en.zip", series.TvdbId);
|
||||
|
||||
using (var xw = XmlWriter.Create(sb, xws))
|
||||
{
|
||||
var tvShow = new XElement("tvshow");
|
||||
|
||||
tvShow.Add(new XElement("title", series.Title));
|
||||
|
||||
if (series.Ratings != null && series.Ratings.Votes > 0)
|
||||
{
|
||||
tvShow.Add(new XElement("rating", series.Ratings.Value));
|
||||
}
|
||||
|
||||
tvShow.Add(new XElement("plot", series.Overview));
|
||||
tvShow.Add(new XElement("episodeguide", new XElement("url", episodeGuideUrl)));
|
||||
tvShow.Add(new XElement("episodeguideurl", episodeGuideUrl));
|
||||
tvShow.Add(new XElement("mpaa", series.Certification));
|
||||
tvShow.Add(new XElement("id", series.TvdbId));
|
||||
|
||||
foreach (var genre in series.Genres)
|
||||
{
|
||||
tvShow.Add(new XElement("genre", genre));
|
||||
}
|
||||
|
||||
if (series.FirstAired.HasValue)
|
||||
{
|
||||
tvShow.Add(new XElement("premiered", series.FirstAired.Value.ToString("yyyy-MM-dd")));
|
||||
}
|
||||
|
||||
tvShow.Add(new XElement("studio", series.Network));
|
||||
|
||||
foreach (var actor in series.Actors)
|
||||
{
|
||||
var xmlActor = new XElement("actor",
|
||||
new XElement("name", actor.Name),
|
||||
new XElement("role", actor.Character));
|
||||
|
||||
if (actor.Images.Any())
|
||||
{
|
||||
xmlActor.Add(new XElement("thumb", actor.Images.First().Url));
|
||||
}
|
||||
|
||||
tvShow.Add(xmlActor);
|
||||
}
|
||||
|
||||
var doc = new XDocument(tvShow);
|
||||
doc.Save(xw);
|
||||
|
||||
_logger.Debug("Saving tvshow.nfo for {0}", series.Title);
|
||||
|
||||
return new MetadataFileResult("tvshow.nfo", doc.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile)
|
||||
{
|
||||
if (!Settings.EpisodeMetadata)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.Debug("Generating Episode Metadata for: {0}", Path.Combine(series.Path, episodeFile.RelativePath));
|
||||
|
||||
var xmlResult = string.Empty;
|
||||
foreach (var episode in episodeFile.Episodes.Value)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var xws = new XmlWriterSettings();
|
||||
xws.OmitXmlDeclaration = true;
|
||||
xws.Indent = false;
|
||||
|
||||
using (var xw = XmlWriter.Create(sb, xws))
|
||||
{
|
||||
var doc = new XDocument();
|
||||
var image = episode.Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot);
|
||||
|
||||
var details = new XElement("episodedetails");
|
||||
details.Add(new XElement("title", episode.Title));
|
||||
details.Add(new XElement("season", episode.SeasonNumber));
|
||||
details.Add(new XElement("episode", episode.EpisodeNumber));
|
||||
details.Add(new XElement("aired", episode.AirDate));
|
||||
details.Add(new XElement("plot", episode.Overview));
|
||||
|
||||
//If trakt ever gets airs before information for specials we should add set it
|
||||
details.Add(new XElement("displayseason"));
|
||||
details.Add(new XElement("displayepisode"));
|
||||
|
||||
if (image == null)
|
||||
{
|
||||
details.Add(new XElement("thumb"));
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
details.Add(new XElement("thumb", image.Url));
|
||||
}
|
||||
|
||||
details.Add(new XElement("watched", "false"));
|
||||
|
||||
if (episode.Ratings != null && episode.Ratings.Votes > 0)
|
||||
{
|
||||
details.Add(new XElement("rating", episode.Ratings.Value));
|
||||
}
|
||||
|
||||
if (episodeFile.MediaInfo != null)
|
||||
{
|
||||
var fileInfo = new XElement("fileinfo");
|
||||
var streamDetails = new XElement("streamdetails");
|
||||
|
||||
var video = new XElement("video");
|
||||
video.Add(new XElement("aspect", (float) episodeFile.MediaInfo.Width / (float) episodeFile.MediaInfo.Height));
|
||||
video.Add(new XElement("bitrate", episodeFile.MediaInfo.VideoBitrate));
|
||||
video.Add(new XElement("codec", episodeFile.MediaInfo.VideoCodec));
|
||||
video.Add(new XElement("framerate", episodeFile.MediaInfo.VideoFps));
|
||||
video.Add(new XElement("height", episodeFile.MediaInfo.Height));
|
||||
video.Add(new XElement("scantype", episodeFile.MediaInfo.ScanType));
|
||||
video.Add(new XElement("width", episodeFile.MediaInfo.Height));
|
||||
|
||||
if (episodeFile.MediaInfo.RunTime != null)
|
||||
{
|
||||
video.Add(new XElement("duration", episodeFile.MediaInfo.RunTime.TotalMinutes));
|
||||
video.Add(new XElement("durationinseconds", episodeFile.MediaInfo.RunTime.TotalSeconds));
|
||||
}
|
||||
|
||||
streamDetails.Add(video);
|
||||
|
||||
var audio = new XElement("audio");
|
||||
audio.Add(new XElement("bitrate", episodeFile.MediaInfo.AudioBitrate));
|
||||
audio.Add(new XElement("channels", episodeFile.MediaInfo.AudioChannels));
|
||||
audio.Add(new XElement("codec", GetAudioCodec(episodeFile.MediaInfo.AudioFormat)));
|
||||
audio.Add(new XElement("language", episodeFile.MediaInfo.AudioLanguages));
|
||||
streamDetails.Add(audio);
|
||||
|
||||
if (episodeFile.MediaInfo.Subtitles != null && episodeFile.MediaInfo.Subtitles.Length > 0)
|
||||
{
|
||||
var subtitle = new XElement("subtitle");
|
||||
subtitle.Add(new XElement("language", episodeFile.MediaInfo.Subtitles));
|
||||
streamDetails.Add(subtitle);
|
||||
}
|
||||
|
||||
fileInfo.Add(streamDetails);
|
||||
details.Add(fileInfo);
|
||||
}
|
||||
|
||||
//Todo: get guest stars, writer and director
|
||||
//details.Add(new XElement("credits", tvdbEpisode.Writer.FirstOrDefault()));
|
||||
//details.Add(new XElement("director", tvdbEpisode.Directors.FirstOrDefault()));
|
||||
|
||||
doc.Add(details);
|
||||
doc.Save(xw);
|
||||
|
||||
xmlResult += doc.ToString();
|
||||
xmlResult += Environment.NewLine;
|
||||
}
|
||||
}
|
||||
|
||||
return new MetadataFileResult(GetEpisodeMetadataFilename(episodeFile.RelativePath), xmlResult.Trim(Environment.NewLine.ToCharArray()));
|
||||
}
|
||||
|
||||
public override List<ImageFileResult> SeriesImages(Series series)
|
||||
{
|
||||
if (!Settings.SeriesImages)
|
||||
{
|
||||
return new List<ImageFileResult>();
|
||||
}
|
||||
|
||||
return ProcessSeriesImages(series).ToList();
|
||||
}
|
||||
|
||||
public override List<ImageFileResult> SeasonImages(Series series, Season season)
|
||||
{
|
||||
if (!Settings.SeasonImages)
|
||||
{
|
||||
return new List<ImageFileResult>();
|
||||
}
|
||||
|
||||
return ProcessSeasonImages(series, season).ToList();
|
||||
}
|
||||
|
||||
public override List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile)
|
||||
{
|
||||
if (!Settings.EpisodeImages)
|
||||
{
|
||||
return new List<ImageFileResult>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot);
|
||||
|
||||
if (screenshot == null)
|
||||
{
|
||||
_logger.Debug("Episode screenshot not available");
|
||||
return new List<ImageFileResult>();
|
||||
}
|
||||
|
||||
return new List<ImageFileResult>
|
||||
{
|
||||
new ImageFileResult(GetEpisodeImageFilename(episodeFile.RelativePath), screenshot.Url)
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to process episode image for file: " + Path.Combine(series.Path, episodeFile.RelativePath));
|
||||
|
||||
return new List<ImageFileResult>();
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<ImageFileResult> ProcessSeriesImages(Series series)
|
||||
{
|
||||
foreach (var image in series.Images)
|
||||
{
|
||||
var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType);
|
||||
var destination = image.CoverType.ToString().ToLowerInvariant() + Path.GetExtension(source);
|
||||
|
||||
yield return new ImageFileResult(destination, source);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<ImageFileResult> ProcessSeasonImages(Series series, Season season)
|
||||
{
|
||||
foreach (var image in season.Images)
|
||||
{
|
||||
var filename = string.Format("season{0:00}-{1}.jpg", season.SeasonNumber, image.CoverType.ToString().ToLower());
|
||||
|
||||
if (season.SeasonNumber == 0)
|
||||
{
|
||||
filename = string.Format("season-specials-{0}.jpg", image.CoverType.ToString().ToLower());
|
||||
}
|
||||
|
||||
yield return new ImageFileResult(filename, image.Url);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetEpisodeMetadataFilename(string episodeFilePath)
|
||||
{
|
||||
return Path.ChangeExtension(episodeFilePath, "nfo");
|
||||
}
|
||||
|
||||
private string GetEpisodeImageFilename(string episodeFilePath)
|
||||
{
|
||||
return Path.ChangeExtension(episodeFilePath, "").Trim('.') + "-thumb.jpg";
|
||||
}
|
||||
|
||||
private string GetAudioCodec(string audioCodec)
|
||||
{
|
||||
if (audioCodec == "AC-3")
|
||||
{
|
||||
return "AC3";
|
||||
}
|
||||
|
||||
return audioCodec;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
|
||||
{
|
||||
public class XbmcSettingsValidator : AbstractValidator<XbmcMetadataSettings>
|
||||
{
|
||||
public XbmcSettingsValidator()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class XbmcMetadataSettings : IProviderConfig
|
||||
{
|
||||
private static readonly XbmcSettingsValidator Validator = new XbmcSettingsValidator();
|
||||
|
||||
public XbmcMetadataSettings()
|
||||
{
|
||||
SeriesMetadata = true;
|
||||
EpisodeMetadata = true;
|
||||
SeriesImages = true;
|
||||
SeasonImages = true;
|
||||
EpisodeImages = true;
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Series Metadata", Type = FieldType.Checkbox)]
|
||||
public bool SeriesMetadata { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Episode Metadata", Type = FieldType.Checkbox)]
|
||||
public bool EpisodeMetadata { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Series Images", Type = FieldType.Checkbox)]
|
||||
public bool SeriesImages { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Season Images", Type = FieldType.Checkbox)]
|
||||
public bool SeasonImages { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "Episode Images", Type = FieldType.Checkbox)]
|
||||
public bool EpisodeImages { get; set; }
|
||||
|
||||
public bool IsValid
|
||||
{
|
||||
get
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.Extras.Metadata.Files;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Metadata
|
||||
{
|
||||
public class ExistingMetadataImporter : ImportExistingExtraFilesBase<MetadataFile>
|
||||
{
|
||||
private readonly IExtraFileService<MetadataFile> _metadataFileService;
|
||||
private readonly IParsingService _parsingService;
|
||||
private readonly Logger _logger;
|
||||
private readonly List<IMetadata> _consumers;
|
||||
|
||||
public ExistingMetadataImporter(IExtraFileService<MetadataFile> metadataFileService,
|
||||
IEnumerable<IMetadata> consumers,
|
||||
IParsingService parsingService,
|
||||
Logger logger)
|
||||
: base(metadataFileService)
|
||||
{
|
||||
_metadataFileService = metadataFileService;
|
||||
_parsingService = parsingService;
|
||||
_logger = logger;
|
||||
_consumers = consumers.ToList();
|
||||
}
|
||||
|
||||
public override int Order
|
||||
{
|
||||
get
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles)
|
||||
{
|
||||
_logger.Debug("Looking for existing metadata in {0}", series.Path);
|
||||
|
||||
var metadataFiles = new List<MetadataFile>();
|
||||
var filteredFiles = FilterAndClean(series, filesOnDisk, importedFiles);
|
||||
|
||||
foreach (var possibleMetadataFile in filteredFiles)
|
||||
{
|
||||
foreach (var consumer in _consumers)
|
||||
{
|
||||
var metadata = consumer.FindMetadataFile(series, possibleMetadataFile);
|
||||
|
||||
if (metadata == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (metadata.Type == MetadataType.EpisodeImage ||
|
||||
metadata.Type == MetadataType.EpisodeMetadata)
|
||||
{
|
||||
var localEpisode = _parsingService.GetLocalEpisode(possibleMetadataFile, series);
|
||||
|
||||
if (localEpisode == null)
|
||||
{
|
||||
_logger.Debug("Unable to parse extra file: {0}", possibleMetadataFile);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (localEpisode.Episodes.Empty())
|
||||
{
|
||||
_logger.Debug("Cannot find related episodes for: {0}", possibleMetadataFile);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (localEpisode.Episodes.DistinctBy(e => e.EpisodeFileId).Count() > 1)
|
||||
{
|
||||
_logger.Debug("Extra file: {0} does not match existing files.", possibleMetadataFile);
|
||||
continue;
|
||||
}
|
||||
|
||||
metadata.SeasonNumber = localEpisode.SeasonNumber;
|
||||
metadata.EpisodeFileId = localEpisode.Episodes.First().EpisodeFileId;
|
||||
metadata.Extension = Path.GetExtension(possibleMetadataFile);
|
||||
}
|
||||
|
||||
metadataFiles.Add(metadata);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Info("Found {0} existing metadata files", metadataFiles.Count);
|
||||
_metadataFileService.Upsert(metadataFiles);
|
||||
|
||||
return metadataFiles;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.IO;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Metadata.Files
|
||||
{
|
||||
public interface ICleanMetadataService
|
||||
{
|
||||
void Clean(Series series);
|
||||
}
|
||||
|
||||
public class CleanExtraFileService : ICleanMetadataService
|
||||
{
|
||||
private readonly IMetadataFileService _metadataFileService;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public CleanExtraFileService(IMetadataFileService metadataFileService,
|
||||
IDiskProvider diskProvider,
|
||||
Logger logger)
|
||||
{
|
||||
_metadataFileService = metadataFileService;
|
||||
_diskProvider = diskProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void Clean(Series series)
|
||||
{
|
||||
_logger.Debug("Cleaning missing metadata files for series: {0}", series.Title);
|
||||
|
||||
var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id);
|
||||
|
||||
foreach (var metadataFile in metadataFiles)
|
||||
{
|
||||
if (!_diskProvider.FileExists(Path.Combine(series.Path, metadataFile.RelativePath)))
|
||||
{
|
||||
_logger.Debug("Deleting metadata file from database: {0}", metadataFile.RelativePath);
|
||||
_metadataFileService.Delete(metadataFile.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace NzbDrone.Core.Extras.Metadata.Files
|
||||
{
|
||||
public class ImageFileResult
|
||||
{
|
||||
public string RelativePath { get; set; }
|
||||
public string Url { get; set; }
|
||||
|
||||
public ImageFileResult(string relativePath, string url)
|
||||
{
|
||||
RelativePath = relativePath;
|
||||
Url = url;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Metadata.Files
|
||||
{
|
||||
public class MetadataFile : ExtraFile
|
||||
{
|
||||
public string Hash { get; set; }
|
||||
public string Consumer { get; set; }
|
||||
public MetadataType Type { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Metadata.Files
|
||||
{
|
||||
public interface IMetadataFileRepository : IExtraFileRepository<MetadataFile>
|
||||
{
|
||||
}
|
||||
|
||||
public class MetadataFileRepository : ExtraFileRepository<MetadataFile>, IMetadataFileRepository
|
||||
{
|
||||
public MetadataFileRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace NzbDrone.Core.Extras.Metadata.Files
|
||||
{
|
||||
public class MetadataFileResult
|
||||
{
|
||||
public string RelativePath { get; set; }
|
||||
public string Contents { get; set; }
|
||||
|
||||
public MetadataFileResult(string relativePath, string contents)
|
||||
{
|
||||
RelativePath = relativePath;
|
||||
Contents = contents;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Metadata.Files
|
||||
{
|
||||
public interface IMetadataFileService : IExtraFileService<MetadataFile>
|
||||
{
|
||||
}
|
||||
|
||||
public class MetadataFileService : ExtraFileService<MetadataFile>, IMetadataFileService
|
||||
{
|
||||
public MetadataFileService(IExtraFileRepository<MetadataFile> repository, ISeriesService seriesService, IDiskProvider diskProvider, Logger logger)
|
||||
: base(repository, seriesService, diskProvider, logger)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Extras.Metadata.Files;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Metadata
|
||||
{
|
||||
public interface IMetadata : IProvider
|
||||
{
|
||||
string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile);
|
||||
MetadataFile FindMetadataFile(Series series, string path);
|
||||
MetadataFileResult SeriesMetadata(Series series);
|
||||
MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile);
|
||||
List<ImageFileResult> SeriesImages(Series series);
|
||||
List<ImageFileResult> SeasonImages(Series series, Season season);
|
||||
List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using FluentValidation.Results;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.Extras.Metadata.Files;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Metadata
|
||||
{
|
||||
public abstract class MetadataBase<TSettings> : IMetadata where TSettings : IProviderConfig, new()
|
||||
{
|
||||
public abstract string Name { get; }
|
||||
|
||||
public Type ConfigContract
|
||||
{
|
||||
get
|
||||
{
|
||||
return typeof(TSettings);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual ProviderMessage Message
|
||||
{
|
||||
get
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<ProviderDefinition> DefaultDefinitions
|
||||
{
|
||||
get
|
||||
{
|
||||
return new List<ProviderDefinition>();
|
||||
}
|
||||
}
|
||||
|
||||
public ProviderDefinition Definition { get; set; }
|
||||
|
||||
public ValidationResult Test()
|
||||
{
|
||||
return new ValidationResult();
|
||||
}
|
||||
|
||||
public virtual string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile)
|
||||
{
|
||||
var existingFilename = Path.Combine(series.Path, metadataFile.RelativePath);
|
||||
var extension = Path.GetExtension(existingFilename).TrimStart('.');
|
||||
var newFileName = Path.ChangeExtension(Path.Combine(series.Path, episodeFile.RelativePath), extension);
|
||||
|
||||
return newFileName;
|
||||
}
|
||||
|
||||
public abstract MetadataFile FindMetadataFile(Series series, string path);
|
||||
|
||||
public abstract MetadataFileResult SeriesMetadata(Series series);
|
||||
public abstract MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile);
|
||||
public abstract List<ImageFileResult> SeriesImages(Series series);
|
||||
public abstract List<ImageFileResult> SeasonImages(Series series, Season season);
|
||||
public abstract List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile);
|
||||
|
||||
public virtual object RequestAction(string action, IDictionary<string, string> query) { return null; }
|
||||
|
||||
protected TSettings Settings
|
||||
{
|
||||
get
|
||||
{
|
||||
return (TSettings)Definition.Settings;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return GetType().Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Metadata
|
||||
{
|
||||
public class MetadataDefinition : ProviderDefinition
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Composition;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Metadata
|
||||
{
|
||||
public interface IMetadataFactory : IProviderFactory<IMetadata, MetadataDefinition>
|
||||
{
|
||||
List<IMetadata> Enabled();
|
||||
}
|
||||
|
||||
public class MetadataFactory : ProviderFactory<IMetadata, MetadataDefinition>, IMetadataFactory
|
||||
{
|
||||
private readonly IMetadataRepository _providerRepository;
|
||||
|
||||
public MetadataFactory(IMetadataRepository providerRepository, IEnumerable<IMetadata> providers, IContainer container, IEventAggregator eventAggregator, Logger logger)
|
||||
: base(providerRepository, providers, container, eventAggregator, logger)
|
||||
{
|
||||
_providerRepository = providerRepository;
|
||||
}
|
||||
|
||||
protected override void InitializeProviders()
|
||||
{
|
||||
var definitions = new List<MetadataDefinition>();
|
||||
|
||||
foreach (var provider in _providers)
|
||||
{
|
||||
definitions.Add(new MetadataDefinition
|
||||
{
|
||||
Enable = false,
|
||||
Name = provider.Name,
|
||||
Implementation = provider.GetType().Name,
|
||||
Settings = (IProviderConfig)Activator.CreateInstance(provider.ConfigContract)
|
||||
});
|
||||
}
|
||||
|
||||
var currentProviders = All();
|
||||
|
||||
var newProviders = definitions.Where(def => currentProviders.All(c => c.Implementation != def.Implementation)).ToList();
|
||||
|
||||
if (newProviders.Any())
|
||||
{
|
||||
_providerRepository.InsertMany(newProviders.Cast<MetadataDefinition>().ToList());
|
||||
}
|
||||
}
|
||||
|
||||
public List<IMetadata> Enabled()
|
||||
{
|
||||
return GetAvailableProviders().Where(n => ((MetadataDefinition)n.Definition).Enable).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Metadata
|
||||
{
|
||||
public interface IMetadataRepository : IProviderRepository<MetadataDefinition>
|
||||
{
|
||||
}
|
||||
|
||||
public class MetadataRepository : ProviderRepository<MetadataDefinition>, IMetadataRepository
|
||||
{
|
||||
public MetadataRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.Extras.Metadata.Files;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Metadata
|
||||
{
|
||||
public class MetadataService : ExtraFileManager<MetadataFile>
|
||||
{
|
||||
private readonly IMetadataFactory _metadataFactory;
|
||||
private readonly ICleanMetadataService _cleanMetadataService;
|
||||
private readonly IDiskTransferService _diskTransferService;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IMediaFileAttributeService _mediaFileAttributeService;
|
||||
private readonly IMetadataFileService _metadataFileService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public MetadataService(IConfigService configService,
|
||||
IDiskTransferService diskTransferService,
|
||||
IMetadataFactory metadataFactory,
|
||||
ICleanMetadataService cleanMetadataService,
|
||||
IDiskProvider diskProvider,
|
||||
IHttpClient httpClient,
|
||||
IMediaFileAttributeService mediaFileAttributeService,
|
||||
IMetadataFileService metadataFileService,
|
||||
Logger logger)
|
||||
: base(configService, diskTransferService, metadataFileService)
|
||||
{
|
||||
_metadataFactory = metadataFactory;
|
||||
_cleanMetadataService = cleanMetadataService;
|
||||
_diskTransferService = diskTransferService;
|
||||
_diskProvider = diskProvider;
|
||||
_httpClient = httpClient;
|
||||
_mediaFileAttributeService = mediaFileAttributeService;
|
||||
_metadataFileService = metadataFileService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override int Order
|
||||
{
|
||||
get
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<ExtraFile> CreateAfterSeriesScan(Series series, List<EpisodeFile> episodeFiles)
|
||||
{
|
||||
var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id);
|
||||
_cleanMetadataService.Clean(series);
|
||||
|
||||
if (!_diskProvider.FolderExists(series.Path))
|
||||
{
|
||||
_logger.Info("Series folder does not exist, skipping metadata creation");
|
||||
return Enumerable.Empty<MetadataFile>();
|
||||
}
|
||||
|
||||
var files = new List<MetadataFile>();
|
||||
|
||||
foreach (var consumer in _metadataFactory.Enabled())
|
||||
{
|
||||
var consumerFiles = GetMetadataFilesForConsumer(consumer, metadataFiles);
|
||||
|
||||
files.AddIfNotNull(ProcessSeriesMetadata(consumer, series, consumerFiles));
|
||||
files.AddRange(ProcessSeriesImages(consumer, series, consumerFiles));
|
||||
files.AddRange(ProcessSeasonImages(consumer, series, consumerFiles));
|
||||
|
||||
foreach (var episodeFile in episodeFiles)
|
||||
{
|
||||
files.AddIfNotNull(ProcessEpisodeMetadata(consumer, series, episodeFile, consumerFiles));
|
||||
files.AddRange(ProcessEpisodeImages(consumer, series, episodeFile, consumerFiles));
|
||||
}
|
||||
}
|
||||
|
||||
_metadataFileService.Upsert(files);
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile)
|
||||
{
|
||||
var files = new List<MetadataFile>();
|
||||
|
||||
foreach (var consumer in _metadataFactory.Enabled())
|
||||
{
|
||||
|
||||
files.AddIfNotNull(ProcessEpisodeMetadata(consumer, series, episodeFile, new List<MetadataFile>()));
|
||||
files.AddRange(ProcessEpisodeImages(consumer, series, episodeFile, new List<MetadataFile>()));
|
||||
}
|
||||
|
||||
_metadataFileService.Upsert(files);
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder)
|
||||
{
|
||||
var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id);
|
||||
|
||||
if (seriesFolder.IsNullOrWhiteSpace() && seasonFolder.IsNullOrWhiteSpace())
|
||||
{
|
||||
return new List<MetadataFile>();
|
||||
}
|
||||
|
||||
var files = new List<MetadataFile>();
|
||||
|
||||
foreach (var consumer in _metadataFactory.Enabled())
|
||||
{
|
||||
var consumerFiles = GetMetadataFilesForConsumer(consumer, metadataFiles);
|
||||
|
||||
if (seriesFolder.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
files.AddIfNotNull(ProcessSeriesMetadata(consumer, series, consumerFiles));
|
||||
files.AddRange(ProcessSeriesImages(consumer, series, consumerFiles));
|
||||
}
|
||||
|
||||
if (seasonFolder.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
files.AddRange(ProcessSeasonImages(consumer, series, consumerFiles));
|
||||
}
|
||||
}
|
||||
|
||||
_metadataFileService.Upsert(files);
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
public override IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> episodeFiles)
|
||||
{
|
||||
var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id);
|
||||
var movedFiles = new List<MetadataFile>();
|
||||
|
||||
// TODO: Move EpisodeImage and EpisodeMetadata metadata files, instead of relying on consumers to do it
|
||||
// (Xbmc's EpisodeImage is more than just the extension)
|
||||
|
||||
foreach (var consumer in _metadataFactory.GetAvailableProviders())
|
||||
{
|
||||
foreach (var episodeFile in episodeFiles)
|
||||
{
|
||||
var metadataFilesForConsumer = GetMetadataFilesForConsumer(consumer, metadataFiles).Where(m => m.EpisodeFileId == episodeFile.Id).ToList();
|
||||
|
||||
foreach (var metadataFile in metadataFilesForConsumer)
|
||||
{
|
||||
var newFileName = consumer.GetFilenameAfterMove(series, episodeFile, metadataFile);
|
||||
var existingFileName = Path.Combine(series.Path, metadataFile.RelativePath);
|
||||
|
||||
if (newFileName.PathNotEquals(existingFileName))
|
||||
{
|
||||
try
|
||||
{
|
||||
_diskProvider.MoveFile(existingFileName, newFileName);
|
||||
metadataFile.RelativePath = series.Path.GetRelativePath(newFileName);
|
||||
movedFiles.Add(metadataFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Unable to move metadata file: {0}", existingFileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_metadataFileService.Upsert(movedFiles);
|
||||
|
||||
return movedFiles;
|
||||
}
|
||||
|
||||
public override ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<MetadataFile> GetMetadataFilesForConsumer(IMetadata consumer, List<MetadataFile> seriesMetadata)
|
||||
{
|
||||
return seriesMetadata.Where(c => c.Consumer == consumer.GetType().Name).ToList();
|
||||
}
|
||||
|
||||
private MetadataFile ProcessSeriesMetadata(IMetadata consumer, Series series, List<MetadataFile> existingMetadataFiles)
|
||||
{
|
||||
var seriesMetadata = consumer.SeriesMetadata(series);
|
||||
|
||||
if (seriesMetadata == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var hash = seriesMetadata.Contents.SHA256Hash();
|
||||
|
||||
var metadata = GetMetadataFile(series, existingMetadataFiles, e => e.Type == MetadataType.SeriesMetadata) ??
|
||||
new MetadataFile
|
||||
{
|
||||
SeriesId = series.Id,
|
||||
Consumer = consumer.GetType().Name,
|
||||
Type = MetadataType.SeriesMetadata
|
||||
};
|
||||
|
||||
if (hash == metadata.Hash)
|
||||
{
|
||||
if (seriesMetadata.RelativePath != metadata.RelativePath)
|
||||
{
|
||||
metadata.RelativePath = seriesMetadata.RelativePath;
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
var fullPath = Path.Combine(series.Path, seriesMetadata.RelativePath);
|
||||
|
||||
_logger.Debug("Writing Series Metadata to: {0}", fullPath);
|
||||
SaveMetadataFile(fullPath, seriesMetadata.Contents);
|
||||
|
||||
metadata.Hash = hash;
|
||||
metadata.RelativePath = seriesMetadata.RelativePath;
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private MetadataFile ProcessEpisodeMetadata(IMetadata consumer, Series series, EpisodeFile episodeFile, List<MetadataFile> existingMetadataFiles)
|
||||
{
|
||||
var episodeMetadata = consumer.EpisodeMetadata(series, episodeFile);
|
||||
|
||||
if (episodeMetadata == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var fullPath = Path.Combine(series.Path, episodeMetadata.RelativePath);
|
||||
|
||||
var existingMetadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.EpisodeMetadata &&
|
||||
c.EpisodeFileId == episodeFile.Id);
|
||||
|
||||
if (existingMetadata != null)
|
||||
{
|
||||
var existingFullPath = Path.Combine(series.Path, existingMetadata.RelativePath);
|
||||
if (fullPath.PathNotEquals(existingFullPath))
|
||||
{
|
||||
_diskTransferService.TransferFile(existingFullPath, fullPath, TransferMode.Move);
|
||||
existingMetadata.RelativePath = episodeMetadata.RelativePath;
|
||||
}
|
||||
}
|
||||
|
||||
var hash = episodeMetadata.Contents.SHA256Hash();
|
||||
|
||||
var metadata = existingMetadata ??
|
||||
new MetadataFile
|
||||
{
|
||||
SeriesId = series.Id,
|
||||
SeasonNumber = episodeFile.SeasonNumber,
|
||||
EpisodeFileId = episodeFile.Id,
|
||||
Consumer = consumer.GetType().Name,
|
||||
Type = MetadataType.EpisodeMetadata,
|
||||
RelativePath = episodeMetadata.RelativePath
|
||||
};
|
||||
|
||||
if (hash == metadata.Hash)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.Debug("Writing Episode Metadata to: {0}", fullPath);
|
||||
SaveMetadataFile(fullPath, episodeMetadata.Contents);
|
||||
|
||||
metadata.Hash = hash;
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private List<MetadataFile> ProcessSeriesImages(IMetadata consumer, Series series, List<MetadataFile> existingMetadataFiles)
|
||||
{
|
||||
var result = new List<MetadataFile>();
|
||||
|
||||
foreach (var image in consumer.SeriesImages(series))
|
||||
{
|
||||
var fullPath = Path.Combine(series.Path, image.RelativePath);
|
||||
|
||||
if (_diskProvider.FileExists(fullPath))
|
||||
{
|
||||
_logger.Debug("Series image already exists: {0}", fullPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
var metadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.SeriesImage &&
|
||||
c.RelativePath == image.RelativePath) ??
|
||||
new MetadataFile
|
||||
{
|
||||
SeriesId = series.Id,
|
||||
Consumer = consumer.GetType().Name,
|
||||
Type = MetadataType.SeriesImage,
|
||||
RelativePath = image.RelativePath
|
||||
};
|
||||
|
||||
DownloadImage(series, image);
|
||||
|
||||
result.Add(metadata);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<MetadataFile> ProcessSeasonImages(IMetadata consumer, Series series, List<MetadataFile> existingMetadataFiles)
|
||||
{
|
||||
var result = new List<MetadataFile>();
|
||||
|
||||
foreach (var season in series.Seasons)
|
||||
{
|
||||
foreach (var image in consumer.SeasonImages(series, season))
|
||||
{
|
||||
var fullPath = Path.Combine(series.Path, image.RelativePath);
|
||||
|
||||
if (_diskProvider.FileExists(fullPath))
|
||||
{
|
||||
_logger.Debug("Season image already exists: {0}", fullPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
var metadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.SeasonImage &&
|
||||
c.SeasonNumber == season.SeasonNumber &&
|
||||
c.RelativePath == image.RelativePath) ??
|
||||
new MetadataFile
|
||||
{
|
||||
SeriesId = series.Id,
|
||||
SeasonNumber = season.SeasonNumber,
|
||||
Consumer = consumer.GetType().Name,
|
||||
Type = MetadataType.SeasonImage,
|
||||
RelativePath = image.RelativePath
|
||||
};
|
||||
|
||||
DownloadImage(series, image);
|
||||
|
||||
result.Add(metadata);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<MetadataFile> ProcessEpisodeImages(IMetadata consumer, Series series, EpisodeFile episodeFile, List<MetadataFile> existingMetadataFiles)
|
||||
{
|
||||
var result = new List<MetadataFile>();
|
||||
|
||||
foreach (var image in consumer.EpisodeImages(series, episodeFile))
|
||||
{
|
||||
var fullPath = Path.Combine(series.Path, image.RelativePath);
|
||||
|
||||
if (_diskProvider.FileExists(fullPath))
|
||||
{
|
||||
_logger.Debug("Episode image already exists: {0}", fullPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
var existingMetadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.EpisodeImage &&
|
||||
c.EpisodeFileId == episodeFile.Id);
|
||||
|
||||
if (existingMetadata != null)
|
||||
{
|
||||
var existingFullPath = Path.Combine(series.Path, existingMetadata.RelativePath);
|
||||
if (fullPath.PathNotEquals(existingFullPath))
|
||||
{
|
||||
_diskTransferService.TransferFile(existingFullPath, fullPath, TransferMode.Move);
|
||||
existingMetadata.RelativePath = image.RelativePath;
|
||||
|
||||
return new List<MetadataFile>{ existingMetadata };
|
||||
}
|
||||
}
|
||||
|
||||
var metadata = existingMetadata ??
|
||||
new MetadataFile
|
||||
{
|
||||
SeriesId = series.Id,
|
||||
SeasonNumber = episodeFile.SeasonNumber,
|
||||
EpisodeFileId = episodeFile.Id,
|
||||
Consumer = consumer.GetType().Name,
|
||||
Type = MetadataType.EpisodeImage,
|
||||
RelativePath = image.RelativePath
|
||||
};
|
||||
|
||||
DownloadImage(series, image);
|
||||
|
||||
result.Add(metadata);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void DownloadImage(Series series, ImageFileResult image)
|
||||
{
|
||||
var fullPath = Path.Combine(series.Path, image.RelativePath);
|
||||
|
||||
try
|
||||
{
|
||||
if (image.Url.StartsWith("http"))
|
||||
{
|
||||
_httpClient.DownloadFile(image.Url, fullPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
_diskProvider.CopyFile(image.Url, fullPath);
|
||||
}
|
||||
_mediaFileAttributeService.SetFilePermissions(fullPath);
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
_logger.Warn(ex, "Couldn't download image {0} for {1}. {2}", image.Url, series, ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Couldn't download image {0} for {1}. {2}", image.Url, series, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveMetadataFile(string path, string contents)
|
||||
{
|
||||
_diskProvider.WriteAllText(path, contents);
|
||||
_mediaFileAttributeService.SetFilePermissions(path);
|
||||
}
|
||||
|
||||
private MetadataFile GetMetadataFile(Series series, List<MetadataFile> existingMetadataFiles, Func<MetadataFile, bool> predicate)
|
||||
{
|
||||
var matchingMetadataFiles = existingMetadataFiles.Where(predicate).ToList();
|
||||
|
||||
if (matchingMetadataFiles.Empty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
//Remove duplicate metadata files from DB and disk
|
||||
foreach (var file in matchingMetadataFiles.Skip(1))
|
||||
{
|
||||
var path = Path.Combine(series.Path, file.RelativePath);
|
||||
|
||||
_logger.Debug("Removing duplicate Metadata file: {0}", path);
|
||||
|
||||
_diskProvider.DeleteFile(path);
|
||||
_metadataFileService.Delete(file.Id);
|
||||
}
|
||||
|
||||
|
||||
return matchingMetadataFiles.First();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace NzbDrone.Core.Extras.Metadata
|
||||
{
|
||||
public enum MetadataType
|
||||
{
|
||||
Unknown = 0,
|
||||
SeriesMetadata = 1,
|
||||
EpisodeMetadata = 2,
|
||||
SeriesImage = 3,
|
||||
SeasonImage = 4,
|
||||
EpisodeImage = 5
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Others
|
||||
{
|
||||
public class ExistingOtherExtraImporter : ImportExistingExtraFilesBase<OtherExtraFile>
|
||||
{
|
||||
private readonly IExtraFileService<OtherExtraFile> _otherExtraFileService;
|
||||
private readonly IParsingService _parsingService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ExistingOtherExtraImporter(IExtraFileService<OtherExtraFile> otherExtraFileService,
|
||||
IParsingService parsingService,
|
||||
Logger logger)
|
||||
: base(otherExtraFileService)
|
||||
{
|
||||
_otherExtraFileService = otherExtraFileService;
|
||||
_parsingService = parsingService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override int Order
|
||||
{
|
||||
get
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles)
|
||||
{
|
||||
_logger.Debug("Looking for existing extra files in {0}", series.Path);
|
||||
|
||||
var extraFiles = new List<OtherExtraFile>();
|
||||
var filteredFiles = FilterAndClean(series, filesOnDisk, importedFiles);
|
||||
|
||||
foreach (var possibleExtraFile in filteredFiles)
|
||||
{
|
||||
var localEpisode = _parsingService.GetLocalEpisode(possibleExtraFile, series);
|
||||
|
||||
if (localEpisode == null)
|
||||
{
|
||||
_logger.Debug("Unable to parse extra file: {0}", possibleExtraFile);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (localEpisode.Episodes.Empty())
|
||||
{
|
||||
_logger.Debug("Cannot find related episodes for: {0}", possibleExtraFile);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (localEpisode.Episodes.DistinctBy(e => e.EpisodeFileId).Count() > 1)
|
||||
{
|
||||
_logger.Debug("Extra file: {0} does not match existing files.", possibleExtraFile);
|
||||
continue;
|
||||
}
|
||||
|
||||
var extraFile = new OtherExtraFile
|
||||
{
|
||||
SeriesId = series.Id,
|
||||
SeasonNumber = localEpisode.SeasonNumber,
|
||||
EpisodeFileId = localEpisode.Episodes.First().EpisodeFileId,
|
||||
RelativePath = series.Path.GetRelativePath(possibleExtraFile),
|
||||
Extension = Path.GetExtension(possibleExtraFile)
|
||||
};
|
||||
|
||||
extraFiles.Add(extraFile);
|
||||
}
|
||||
|
||||
_logger.Info("Found {0} existing other extra files", extraFiles.Count);
|
||||
_otherExtraFileService.Upsert(extraFiles);
|
||||
|
||||
return extraFiles;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Others
|
||||
{
|
||||
public class OtherExtraFile : ExtraFile
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Others
|
||||
{
|
||||
public interface IOtherExtraFileRepository : IExtraFileRepository<OtherExtraFile>
|
||||
{
|
||||
}
|
||||
|
||||
public class OtherExtraFileRepository : ExtraFileRepository<OtherExtraFile>, IOtherExtraFileRepository
|
||||
{
|
||||
public OtherExtraFileRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Others
|
||||
{
|
||||
public interface IOtherExtraFileService : IExtraFileService<OtherExtraFile>
|
||||
{
|
||||
}
|
||||
|
||||
public class OtherExtraFileService : ExtraFileService<OtherExtraFile>, IOtherExtraFileService
|
||||
{
|
||||
public OtherExtraFileService(IExtraFileRepository<OtherExtraFile> repository, ISeriesService seriesService, IDiskProvider diskProvider, Logger logger)
|
||||
: base(repository, seriesService, diskProvider, logger)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Others
|
||||
{
|
||||
public class OtherExtraService : ExtraFileManager<OtherExtraFile>
|
||||
{
|
||||
private readonly IOtherExtraFileService _otherExtraFileService;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public OtherExtraService(IConfigService configService,
|
||||
IDiskTransferService diskTransferService,
|
||||
IOtherExtraFileService otherExtraFileService,
|
||||
IDiskProvider diskProvider,
|
||||
Logger logger)
|
||||
: base(configService, diskTransferService, otherExtraFileService)
|
||||
{
|
||||
_otherExtraFileService = otherExtraFileService;
|
||||
_diskProvider = diskProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override int Order
|
||||
{
|
||||
get
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<ExtraFile> CreateAfterSeriesScan(Series series, List<EpisodeFile> episodeFiles)
|
||||
{
|
||||
return Enumerable.Empty<ExtraFile>();
|
||||
}
|
||||
|
||||
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile)
|
||||
{
|
||||
return Enumerable.Empty<ExtraFile>();
|
||||
}
|
||||
|
||||
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder)
|
||||
{
|
||||
return Enumerable.Empty<ExtraFile>();
|
||||
}
|
||||
|
||||
public override IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> episodeFiles)
|
||||
{
|
||||
// TODO: Remove
|
||||
// We don't want to move files after rename yet.
|
||||
|
||||
return Enumerable.Empty<ExtraFile>();
|
||||
|
||||
var extraFiles = _otherExtraFileService.GetFilesBySeries(series.Id);
|
||||
var movedFiles = new List<OtherExtraFile>();
|
||||
|
||||
foreach (var episodeFile in episodeFiles)
|
||||
{
|
||||
var extraFilesForEpisodeFile = extraFiles.Where(m => m.EpisodeFileId == episodeFile.Id).ToList();
|
||||
|
||||
foreach (var extraFile in extraFilesForEpisodeFile)
|
||||
{
|
||||
var existingFileName = Path.Combine(series.Path, extraFile.RelativePath);
|
||||
var extension = Path.GetExtension(existingFileName).TrimStart('.');
|
||||
var newFileName = Path.ChangeExtension(Path.Combine(series.Path, episodeFile.RelativePath), extension);
|
||||
|
||||
if (newFileName.PathNotEquals(existingFileName))
|
||||
{
|
||||
try
|
||||
{
|
||||
_diskProvider.MoveFile(existingFileName, newFileName);
|
||||
extraFile.RelativePath = series.Path.GetRelativePath(newFileName);
|
||||
movedFiles.Add(extraFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Unable to move extra file: {0}", existingFileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_otherExtraFileService.Upsert(movedFiles);
|
||||
|
||||
return movedFiles;
|
||||
}
|
||||
|
||||
public override ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly)
|
||||
{
|
||||
// If the extension is .nfo we need to change it to .nfo-orig
|
||||
if (Path.GetExtension(path).Equals(".nfo"))
|
||||
{
|
||||
extension += "-orig";
|
||||
}
|
||||
|
||||
var extraFile = ImportFile(series, episodeFile, path, extension, readOnly);
|
||||
|
||||
_otherExtraFileService.Upsert(extraFile);
|
||||
|
||||
return extraFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Subtitles
|
||||
{
|
||||
public class ExistingSubtitleImporter : ImportExistingExtraFilesBase<SubtitleFile>
|
||||
{
|
||||
private readonly IExtraFileService<SubtitleFile> _subtitleFileService;
|
||||
private readonly IParsingService _parsingService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ExistingSubtitleImporter(IExtraFileService<SubtitleFile> subtitleFileService,
|
||||
IParsingService parsingService,
|
||||
Logger logger)
|
||||
: base (subtitleFileService)
|
||||
{
|
||||
_subtitleFileService = subtitleFileService;
|
||||
_parsingService = parsingService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override int Order
|
||||
{
|
||||
get
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles)
|
||||
{
|
||||
_logger.Debug("Looking for existing subtitle files in {0}", series.Path);
|
||||
|
||||
var subtitleFiles = new List<SubtitleFile>();
|
||||
var filteredFiles = FilterAndClean(series, filesOnDisk, importedFiles);
|
||||
|
||||
foreach (var possibleSubtitleFile in filteredFiles)
|
||||
{
|
||||
var extension = Path.GetExtension(possibleSubtitleFile);
|
||||
|
||||
if (SubtitleFileExtensions.Extensions.Contains(extension))
|
||||
{
|
||||
var localEpisode = _parsingService.GetLocalEpisode(possibleSubtitleFile, series);
|
||||
|
||||
if (localEpisode == null)
|
||||
{
|
||||
_logger.Debug("Unable to parse subtitle file: {0}", possibleSubtitleFile);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (localEpisode.Episodes.Empty())
|
||||
{
|
||||
_logger.Debug("Cannot find related episodes for: {0}", possibleSubtitleFile);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (localEpisode.Episodes.DistinctBy(e => e.EpisodeFileId).Count() > 1)
|
||||
{
|
||||
_logger.Debug("Subtitle file: {0} does not match existing files.", possibleSubtitleFile);
|
||||
continue;
|
||||
}
|
||||
|
||||
var subtitleFile = new SubtitleFile
|
||||
{
|
||||
SeriesId = series.Id,
|
||||
SeasonNumber = localEpisode.SeasonNumber,
|
||||
EpisodeFileId = localEpisode.Episodes.First().EpisodeFileId,
|
||||
RelativePath = series.Path.GetRelativePath(possibleSubtitleFile),
|
||||
Language = LanguageParser.ParseSubtitleLanguage(possibleSubtitleFile),
|
||||
Extension = extension
|
||||
};
|
||||
|
||||
subtitleFiles.Add(subtitleFile);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Info("Found {0} existing subtitle files", subtitleFiles.Count);
|
||||
_subtitleFileService.Upsert(subtitleFiles);
|
||||
|
||||
return subtitleFiles;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Subtitles
|
||||
{
|
||||
public class ImportedSubtitleFiles
|
||||
{
|
||||
public List<string> SourceFiles { get; set; }
|
||||
public List<ExtraFile> SubtitleFiles { get; set; }
|
||||
|
||||
public ImportedSubtitleFiles()
|
||||
{
|
||||
SourceFiles = new List<string>();
|
||||
SubtitleFiles = new List<ExtraFile>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.Parser;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Subtitles
|
||||
{
|
||||
public class SubtitleFile : ExtraFile
|
||||
{
|
||||
public Language Language { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Subtitles
|
||||
{
|
||||
public static class SubtitleFileExtensions
|
||||
{
|
||||
private static HashSet<string> _fileExtensions;
|
||||
|
||||
static SubtitleFileExtensions()
|
||||
{
|
||||
_fileExtensions = new HashSet<string>
|
||||
{
|
||||
".aqt",
|
||||
".ass",
|
||||
".idx",
|
||||
".jss",
|
||||
".psb",
|
||||
".rt",
|
||||
".smi",
|
||||
".srt",
|
||||
".ssa",
|
||||
".sub",
|
||||
".txt",
|
||||
".utf",
|
||||
".utf8",
|
||||
".utf-8"
|
||||
};
|
||||
}
|
||||
|
||||
public static HashSet<string> Extensions
|
||||
{
|
||||
get { return _fileExtensions; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Subtitles
|
||||
{
|
||||
public interface ISubtitleFileRepository : IExtraFileRepository<SubtitleFile>
|
||||
{
|
||||
}
|
||||
|
||||
public class SubtitleFileRepository : ExtraFileRepository<SubtitleFile>, ISubtitleFileRepository
|
||||
{
|
||||
public SubtitleFileRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Subtitles
|
||||
{
|
||||
public interface ISubtitleFileService : IExtraFileService<SubtitleFile>
|
||||
{
|
||||
}
|
||||
|
||||
public class SubtitleFileService : ExtraFileService<SubtitleFile>, ISubtitleFileService
|
||||
{
|
||||
public SubtitleFileService(IExtraFileRepository<SubtitleFile> repository, ISeriesService seriesService, IDiskProvider diskProvider, Logger logger)
|
||||
: base(repository, seriesService, diskProvider, logger)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Extras.Files;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Extras.Subtitles
|
||||
{
|
||||
public class SubtitleService : ExtraFileManager<SubtitleFile>
|
||||
{
|
||||
private readonly ISubtitleFileService _subtitleFileService;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public SubtitleService(IConfigService configService,
|
||||
IDiskTransferService diskTransferService,
|
||||
ISubtitleFileService subtitleFileService,
|
||||
IDiskProvider diskProvider,
|
||||
Logger logger)
|
||||
: base(configService, diskTransferService, subtitleFileService)
|
||||
{
|
||||
_subtitleFileService = subtitleFileService;
|
||||
_diskProvider = diskProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override int Order
|
||||
{
|
||||
get
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<ExtraFile> CreateAfterSeriesScan(Series series, List<EpisodeFile> episodeFiles)
|
||||
{
|
||||
return Enumerable.Empty<SubtitleFile>();
|
||||
}
|
||||
|
||||
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile)
|
||||
{
|
||||
return Enumerable.Empty<SubtitleFile>();
|
||||
}
|
||||
|
||||
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder)
|
||||
{
|
||||
return Enumerable.Empty<SubtitleFile>();
|
||||
}
|
||||
|
||||
public override IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> episodeFiles)
|
||||
{
|
||||
// TODO: Remove
|
||||
// We don't want to move files after rename yet.
|
||||
|
||||
return Enumerable.Empty<ExtraFile>();
|
||||
|
||||
var subtitleFiles = _subtitleFileService.GetFilesBySeries(series.Id);
|
||||
|
||||
var movedFiles = new List<SubtitleFile>();
|
||||
|
||||
foreach (var episodeFile in episodeFiles)
|
||||
{
|
||||
var groupedExtraFilesForEpisodeFile = subtitleFiles.Where(m => m.EpisodeFileId == episodeFile.Id)
|
||||
.GroupBy(s => s.Language + s.Extension).ToList();
|
||||
|
||||
foreach (var group in groupedExtraFilesForEpisodeFile)
|
||||
{
|
||||
var groupCount = group.Count();
|
||||
var copy = 1;
|
||||
|
||||
if (groupCount > 1)
|
||||
{
|
||||
_logger.Warn("Multiple subtitle files found with the same language and extension for {0}", Path.Combine(series.Path, episodeFile.RelativePath));
|
||||
}
|
||||
|
||||
foreach (var extraFile in group)
|
||||
{
|
||||
var existingFileName = Path.Combine(series.Path, extraFile.RelativePath);
|
||||
var extension = GetExtension(extraFile, existingFileName, copy, groupCount > 1);
|
||||
var newFileName = Path.ChangeExtension(Path.Combine(series.Path, episodeFile.RelativePath), extension);
|
||||
|
||||
if (newFileName.PathNotEquals(existingFileName))
|
||||
{
|
||||
try
|
||||
{
|
||||
_diskProvider.MoveFile(existingFileName, newFileName);
|
||||
extraFile.RelativePath = series.Path.GetRelativePath(newFileName);
|
||||
movedFiles.Add(extraFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Unable to move subtitle file: {0}", existingFileName);
|
||||
}
|
||||
}
|
||||
|
||||
copy++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_subtitleFileService.Upsert(movedFiles);
|
||||
|
||||
return movedFiles;
|
||||
}
|
||||
|
||||
public override ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly)
|
||||
{
|
||||
if (SubtitleFileExtensions.Extensions.Contains(Path.GetExtension(path)))
|
||||
{
|
||||
var subtitleFile = ImportFile(series, episodeFile, path, extension, readOnly);
|
||||
subtitleFile.Language = LanguageParser.ParseSubtitleLanguage(path);
|
||||
|
||||
_subtitleFileService.Upsert(subtitleFile);
|
||||
|
||||
return subtitleFile;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private string GetExtension(SubtitleFile extraFile, string existingFileName, int copy, bool multipleCopies = false)
|
||||
{
|
||||
var fileExtension = Path.GetExtension(existingFileName);
|
||||
var extensionBuilder = new StringBuilder();
|
||||
|
||||
if (multipleCopies)
|
||||
{
|
||||
extensionBuilder.Append(copy);
|
||||
extensionBuilder.Append(".");
|
||||
}
|
||||
|
||||
if (extraFile.Language != Language.Unknown)
|
||||
{
|
||||
extensionBuilder.Append(IsoLanguages.Get(extraFile.Language).TwoLetterCode);
|
||||
extensionBuilder.Append(".");
|
||||
}
|
||||
|
||||
extensionBuilder.Append(fileExtension.TrimStart('.'));
|
||||
|
||||
return extensionBuilder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user