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.Books; using NzbDrone.Core.Configuration; using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Extras.Others; using NzbDrone.Core.MediaFiles; namespace NzbDrone.Core.Extras.Metadata { public class MetadataService : ExtraFileManager { private readonly IMetadataFactory _metadataFactory; private readonly ICleanMetadataService _cleanMetadataService; private readonly IRecycleBinProvider _recycleBinProvider; private readonly IOtherExtraFileRenamer _otherExtraFileRenamer; private readonly IDiskTransferService _diskTransferService; private readonly IDiskProvider _diskProvider; private readonly IHttpClient _httpClient; private readonly IMediaFileAttributeService _mediaFileAttributeService; private readonly IMetadataFileService _metadataFileService; private readonly IBookService _bookService; private readonly Logger _logger; public MetadataService(IConfigService configService, IDiskProvider diskProvider, IDiskTransferService diskTransferService, IRecycleBinProvider recycleBinProvider, IOtherExtraFileRenamer otherExtraFileRenamer, IMetadataFactory metadataFactory, ICleanMetadataService cleanMetadataService, IHttpClient httpClient, IMediaFileAttributeService mediaFileAttributeService, IMetadataFileService metadataFileService, IBookService bookService, Logger logger) : base(configService, diskProvider, diskTransferService, logger) { _metadataFactory = metadataFactory; _cleanMetadataService = cleanMetadataService; _otherExtraFileRenamer = otherExtraFileRenamer; _recycleBinProvider = recycleBinProvider; _diskTransferService = diskTransferService; _diskProvider = diskProvider; _httpClient = httpClient; _mediaFileAttributeService = mediaFileAttributeService; _metadataFileService = metadataFileService; _bookService = bookService; _logger = logger; } public override int Order => 0; public override IEnumerable CreateAfterAuthorScan(Author author, List bookFiles) { var metadataFiles = _metadataFileService.GetFilesByAuthor(author.Id); _cleanMetadataService.Clean(author); if (!_diskProvider.FolderExists(author.Path)) { _logger.Info("Author folder does not exist, skipping metadata creation"); return Enumerable.Empty(); } var files = new List(); foreach (var consumer in _metadataFactory.Enabled()) { var consumerFiles = GetMetadataFilesForConsumer(consumer, metadataFiles); files.AddIfNotNull(ProcessAuthorMetadata(consumer, author, consumerFiles)); files.AddRange(ProcessAuthorImages(consumer, author, consumerFiles)); foreach (var bookFile in bookFiles) { files.AddIfNotNull(ProcessBookMetadata(consumer, author, bookFile, consumerFiles)); } } _metadataFileService.Upsert(files); return files; } public override IEnumerable CreateAfterBookImport(Author author, BookFile bookFile) { var files = new List(); foreach (var consumer in _metadataFactory.Enabled()) { files.AddIfNotNull(ProcessBookMetadata(consumer, author, bookFile, new List())); } _metadataFileService.Upsert(files); return files; } public override IEnumerable CreateAfterBookImport(Author author, Book book, string authorFolder, string bookFolder) { var metadataFiles = _metadataFileService.GetFilesByAuthor(author.Id); if (authorFolder.IsNullOrWhiteSpace() && bookFolder.IsNullOrWhiteSpace()) { return new List(); } var files = new List(); foreach (var consumer in _metadataFactory.Enabled()) { var consumerFiles = GetMetadataFilesForConsumer(consumer, metadataFiles); if (authorFolder.IsNotNullOrWhiteSpace()) { files.AddIfNotNull(ProcessAuthorMetadata(consumer, author, consumerFiles)); files.AddRange(ProcessAuthorImages(consumer, author, consumerFiles)); } } _metadataFileService.Upsert(files); return files; } public override IEnumerable MoveFilesAfterRename(Author author, List bookFiles) { var metadataFiles = _metadataFileService.GetFilesByAuthor(author.Id); var movedFiles = new List(); var distinctTrackFilePaths = bookFiles.DistinctBy(s => Path.GetDirectoryName(s.Path)).ToList(); // 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 filePath in distinctTrackFilePaths) { var metadataFilesForConsumer = GetMetadataFilesForConsumer(consumer, metadataFiles) .Where(m => m.BookId == filePath.Edition.Value.BookId) .Where(m => m.Type == MetadataType.BookImage || m.Type == MetadataType.BookMetadata) .ToList(); foreach (var metadataFile in metadataFilesForConsumer) { var newFileName = consumer.GetFilenameAfterMove(author, Path.GetDirectoryName(filePath.Path), metadataFile); var existingFileName = Path.Combine(author.Path, metadataFile.RelativePath); if (newFileName.PathNotEquals(existingFileName)) { try { _diskProvider.MoveFile(existingFileName, newFileName); metadataFile.RelativePath = author.Path.GetRelativePath(newFileName); movedFiles.Add(metadataFile); } catch (Exception ex) { _logger.Warn(ex, "Unable to move metadata file after rename: {0}", existingFileName); } } } } foreach (var bookFile in bookFiles) { var metadataFilesForConsumer = GetMetadataFilesForConsumer(consumer, metadataFiles).Where(m => m.BookFileId == bookFile.Id).ToList(); foreach (var metadataFile in metadataFilesForConsumer) { var newFileName = consumer.GetFilenameAfterMove(author, bookFile, metadataFile); var existingFileName = Path.Combine(author.Path, metadataFile.RelativePath); if (newFileName.PathNotEquals(existingFileName)) { try { _diskProvider.MoveFile(existingFileName, newFileName); metadataFile.RelativePath = author.Path.GetRelativePath(newFileName); movedFiles.Add(metadataFile); } catch (Exception ex) { _logger.Warn(ex, "Unable to move metadata file after rename: {0}", existingFileName); } } } } } _metadataFileService.Upsert(movedFiles); return movedFiles; } public override ExtraFile Import(Author author, BookFile bookFile, string path, string extension, bool readOnly) { return null; } private List GetMetadataFilesForConsumer(IMetadata consumer, List authorMetadata) { return authorMetadata.Where(c => c.Consumer == consumer.GetType().Name).ToList(); } private MetadataFile ProcessAuthorMetadata(IMetadata consumer, Author author, List existingMetadataFiles) { var authorMetadata = consumer.AuthorMetadata(author); if (authorMetadata == null) { return null; } var hash = authorMetadata.Contents.SHA256Hash(); var metadata = GetMetadataFile(author, existingMetadataFiles, e => e.Type == MetadataType.AuthorMetadata) ?? new MetadataFile { AuthorId = author.Id, Consumer = consumer.GetType().Name, Type = MetadataType.AuthorMetadata }; if (hash == metadata.Hash) { if (authorMetadata.RelativePath != metadata.RelativePath) { metadata.RelativePath = authorMetadata.RelativePath; return metadata; } return null; } var fullPath = Path.Combine(author.Path, authorMetadata.RelativePath); _otherExtraFileRenamer.RenameOtherExtraFile(author, fullPath); _logger.Debug("Writing Author Metadata to: {0}", fullPath); SaveMetadataFile(fullPath, authorMetadata.Contents); metadata.Hash = hash; metadata.RelativePath = authorMetadata.RelativePath; metadata.Extension = Path.GetExtension(fullPath); return metadata; } private MetadataFile ProcessBookMetadata(IMetadata consumer, Author author, BookFile bookFile, List existingMetadataFiles) { var trackMetadata = consumer.BookMetadata(author, bookFile); if (trackMetadata == null) { return null; } var fullPath = Path.Combine(author.Path, trackMetadata.RelativePath); _otherExtraFileRenamer.RenameOtherExtraFile(author, fullPath); var existingMetadata = GetMetadataFile(author, existingMetadataFiles, c => c.Type == MetadataType.BookMetadata && c.BookFileId == bookFile.Id); if (existingMetadata != null) { var existingFullPath = Path.Combine(author.Path, existingMetadata.RelativePath); if (fullPath.PathNotEquals(existingFullPath)) { _diskTransferService.TransferFile(existingFullPath, fullPath, TransferMode.Move); existingMetadata.RelativePath = trackMetadata.RelativePath; } } var hash = trackMetadata.Contents.SHA256Hash(); var metadata = existingMetadata ?? new MetadataFile { AuthorId = author.Id, BookId = bookFile.Edition.Value.BookId, BookFileId = bookFile.Id, Consumer = consumer.GetType().Name, Type = MetadataType.BookMetadata, RelativePath = trackMetadata.RelativePath, Extension = Path.GetExtension(fullPath) }; if (hash == metadata.Hash) { return null; } _logger.Debug("Writing Track Metadata to: {0}", fullPath); SaveMetadataFile(fullPath, trackMetadata.Contents); metadata.Hash = hash; return metadata; } private List ProcessAuthorImages(IMetadata consumer, Author author, List existingMetadataFiles) { var result = new List(); foreach (var image in consumer.AuthorImages(author)) { var fullPath = Path.Combine(author.Path, image.RelativePath); if (_diskProvider.FileExists(fullPath)) { _logger.Debug("Author image already exists: {0}", fullPath); continue; } _otherExtraFileRenamer.RenameOtherExtraFile(author, fullPath); var metadata = GetMetadataFile(author, existingMetadataFiles, c => c.Type == MetadataType.AuthorImage && c.RelativePath == image.RelativePath) ?? new MetadataFile { AuthorId = author.Id, Consumer = consumer.GetType().Name, Type = MetadataType.AuthorImage, RelativePath = image.RelativePath, Extension = Path.GetExtension(fullPath) }; DownloadImage(author, image); result.Add(metadata); } return result; } private void DownloadImage(Author author, ImageFileResult image) { var fullPath = Path.Combine(author.Path, image.RelativePath); var downloaded = true; try { if (image.Url.StartsWith("http")) { _httpClient.DownloadFile(image.Url, fullPath); } else if (_diskProvider.FileExists(image.Url)) { _diskProvider.CopyFile(image.Url, fullPath); } else { downloaded = false; } if (downloaded) { _mediaFileAttributeService.SetFilePermissions(fullPath); } } catch (WebException ex) { _logger.Warn(ex, "Couldn't download image {0} for {1}. {2}", image.Url, author, ex.Message); } catch (Exception ex) { _logger.Error(ex, "Couldn't download image {0} for {1}", image.Url, author); } } private void SaveMetadataFile(string path, string contents) { _diskProvider.WriteAllText(path, contents); _mediaFileAttributeService.SetFilePermissions(path); } private MetadataFile GetMetadataFile(Author author, List existingMetadataFiles, Func 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(author.Path, file.RelativePath); _logger.Debug("Removing duplicate Metadata file: {0}", path); var subfolder = _diskProvider.GetParentFolder(author.Path).GetRelativePath(_diskProvider.GetParentFolder(path)); _recycleBinProvider.DeleteFile(path, subfolder); _metadataFileService.Delete(file.Id); } return matchingMetadataFiles.First(); } } }