New: Basic audiobook support

pull/1063/head
ta264 3 years ago
parent 62928b227b
commit f6a04f7890

@ -9,10 +9,17 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { inputTypes } from 'Helpers/Props'; import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
const writeAudioTagOptions = [
{ key: 'no', value: translate('WriteTagsNo') },
{ key: 'sync', value: translate('WriteTagsSync') },
{ key: 'allFiles', value: translate('WriteTagsAll') },
{ key: 'newFiles', value: translate('WriteTagsNew') }
];
const writeBookTagOptions = [ const writeBookTagOptions = [
{ key: 'sync', value: 'All files; keep in sync with Goodreads' }, { key: 'sync', value: translate('WriteTagsSync') },
{ key: 'allFiles', value: 'All files; initial import only' }, { key: 'allFiles', value: translate('WriteTagsAll') },
{ key: 'newFiles', value: 'For new downloads only' } { key: 'newFiles', value: translate('WriteTagsNew') }
]; ];
function MetadataProvider(props) { function MetadataProvider(props) {
@ -88,6 +95,35 @@ function MetadataProvider(props) {
</FormGroup> </FormGroup>
</FieldSet> </FieldSet>
<FieldSet legend={translate('AudioFileMetadata')}>
<FormGroup>
<FormLabel>{translate('WriteAudioTags')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="writeAudioTags"
helpTextWarning={translate('WriteBookTagsHelpTextWarning')}
helpLink="https://wiki.servarr.com/Lidarr_Settings#Write_Metadata_to_Audio_Files"
values={writeAudioTagOptions}
onChange={onInputChange}
{...settings.writeAudioTags}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('WriteAudioTagsScrub')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="scrubAudioTags"
helpTextWarning={translate('WriteAudioTagsScrubHelp')}
onChange={onInputChange}
{...settings.scrubAudioTags}
/>
</FormGroup>
</FieldSet>
</Form> </Form>
} }
</div> </div>

@ -130,6 +130,10 @@ namespace NzbDrone.Core.Test.MediaFiles.BookImport
.Setup(c => c.FilterUnchangedFiles(It.IsAny<List<IFileInfo>>(), It.IsAny<FilterFilesType>())) .Setup(c => c.FilterUnchangedFiles(It.IsAny<List<IFileInfo>>(), It.IsAny<FilterFilesType>()))
.Returns((List<IFileInfo> files, FilterFilesType filter) => files); .Returns((List<IFileInfo> files, FilterFilesType filter) => files);
Mocker.GetMock<IMetadataTagService>()
.Setup(s => s.ReadTags(It.IsAny<IFileInfo>()))
.Returns(new ParsedTrackInfo());
GivenSpecifications(_bookpass1); GivenSpecifications(_bookpass1);
} }

@ -54,6 +54,8 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
.Setup(c => c.GetConfig()).Returns(_namingConfig); .Setup(c => c.GetConfig()).Returns(_namingConfig);
_trackFile = Builder<BookFile>.CreateNew() _trackFile = Builder<BookFile>.CreateNew()
.With(e => e.Part = 1)
.With(e => e.PartCount = 1)
.With(e => e.Quality = new QualityModel(Quality.MP3_320)) .With(e => e.Quality = new QualityModel(Quality.MP3_320))
.With(e => e.ReleaseGroup = "ReadarrTest") .With(e => e.ReleaseGroup = "ReadarrTest")
.With(e => e.MediaInfo = new Parser.Model.MediaInfoModel .With(e => e.MediaInfo = new Parser.Model.MediaInfoModel

@ -83,11 +83,11 @@ namespace NzbDrone.Core.Test.ProviderTests.DiskScanProviderTests
} }
[Test] [Test]
public void should_return_audio_files_only() public void should_return_book_files_only()
{ {
GivenFiles(GetFiles(_path)); GivenFiles(GetFiles(_path));
Subject.GetBookFiles(_path).Should().HaveCount(3); Subject.GetBookFiles(_path).Should().HaveCount(5);
} }
[TestCase("Extras")] [TestCase("Extras")]

@ -0,0 +1,14 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(010)]
public class add_bookfile_part : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("BookFiles").AddColumn("Part").AsInt32().NotNullable().WithDefaultValue(1);
}
}
}

@ -150,6 +150,7 @@ namespace NzbDrone.Core.Datastore
b => b.Id > 0); b => b.Id > 0);
Mapper.Entity<BookFile>("BookFiles").RegisterModel() Mapper.Entity<BookFile>("BookFiles").RegisterModel()
.Ignore(x => x.PartCount)
.HasOne(f => f.Edition, f => f.EditionId) .HasOne(f => f.Edition, f => f.EditionId)
.LazyLoad(x => x.Author, .LazyLoad(x => x.Author,
(db, f) => AuthorRepository.Query(db, (db, f) => AuthorRepository.Query(db,

@ -10,7 +10,7 @@ namespace NzbDrone.Core.Indexers.Newznab
public static List<FieldSelectOption> GetFieldSelectOptions(List<NewznabCategory> categories) public static List<FieldSelectOption> GetFieldSelectOptions(List<NewznabCategory> categories)
{ {
// Ignore categories not relevant for Readarr // Ignore categories not relevant for Readarr
var ignoreCategories = new[] { 1000, 2000, 3000, 4000, 5000, 6000 }; var ignoreCategories = new[] { 1000, 2000, 4000, 5000, 6000 };
// And maybe relevant for specific users // And maybe relevant for specific users
var unimportantCategories = new[] { 0, 8000 }; var unimportantCategories = new[] { 0, 8000 };
@ -22,6 +22,15 @@ namespace NzbDrone.Core.Indexers.Newznab
// Fetching categories failed, use default Newznab categories // Fetching categories failed, use default Newznab categories
categories = new List<NewznabCategory>(); categories = new List<NewznabCategory>();
categories.Add(new NewznabCategory categories.Add(new NewznabCategory
{
Id = 3000,
Name = "Audio",
Subcategories = new List<NewznabCategory>
{
new NewznabCategory { Id = 3030, Name = "Audiobook" }
}
});
categories.Add(new NewznabCategory
{ {
Id = 7000, Id = 7000,
Name = "Books", Name = "Books",

@ -58,7 +58,7 @@ namespace NzbDrone.Core.Indexers.Newznab
public NewznabSettings() public NewznabSettings()
{ {
ApiPath = "/api"; ApiPath = "/api";
Categories = new[] { 7020, 8010 }; Categories = new[] { 3030, 7020, 8010 };
} }
[FieldDefinition(0, Label = "URL")] [FieldDefinition(0, Label = "URL")]

@ -38,6 +38,7 @@
"ApplyTagsHelpTexts2": "Add: Add the tags the existing list of tags", "ApplyTagsHelpTexts2": "Add: Add the tags the existing list of tags",
"ApplyTagsHelpTexts3": "Remove: Remove the entered tags", "ApplyTagsHelpTexts3": "Remove: Remove the entered tags",
"ApplyTagsHelpTexts4": "Replace: Replace the tags with the entered tags (enter no tags to clear all tags)", "ApplyTagsHelpTexts4": "Replace: Replace the tags with the entered tags (enter no tags to clear all tags)",
"AudioFileMetadata": "Write Metadata to Audio Files",
"Authentication": "Authentication", "Authentication": "Authentication",
"AuthenticationMethodHelpText": "Require Username and Password to access Readarr", "AuthenticationMethodHelpText": "Require Username and Password to access Readarr",
"Author": "Author", "Author": "Author",
@ -691,7 +692,14 @@
"WatchLibraryForChangesHelpText": "Rescan automatically when files change in a root folder", "WatchLibraryForChangesHelpText": "Rescan automatically when files change in a root folder",
"WatchRootFoldersForFileChanges": "Watch Root Folders for file changes", "WatchRootFoldersForFileChanges": "Watch Root Folders for file changes",
"WeekColumnHeader": "Week Column Header", "WeekColumnHeader": "Week Column Header",
"WriteAudioTags": "Tag Audio Files with Metadata",
"WriteAudioTagsScrub": "Scrub Existing Tags",
"WriteAudioTagsScrubHelp": "Remove existing tags from files, leaving only those added by Readarr.",
"WriteBookTagsHelpTextWarning": "Selecting 'All files' will alter existing files when they are imported.", "WriteBookTagsHelpTextWarning": "Selecting 'All files' will alter existing files when they are imported.",
"WriteTagsAll": "All files; initial import only",
"WriteTagsNew": "For new downloads only",
"WriteTagsNo": "Never",
"WriteTagsSync": "All files; keep in sync with Goodreads",
"Year": "Year", "Year": "Year",
"YesCancel": "Yes, Cancel" "YesCancel": "Yes, Cancel"
} }

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NLog.Fluent; using NLog.Fluent;
@ -24,12 +25,12 @@ namespace NzbDrone.Core.MediaFiles
void WriteTags(BookFile trackfile, bool newDownload, bool force = false); void WriteTags(BookFile trackfile, bool newDownload, bool force = false);
void SyncTags(List<Edition> tracks); void SyncTags(List<Edition> tracks);
List<RetagBookFilePreview> GetRetagPreviewsByAuthor(int authorId); List<RetagBookFilePreview> GetRetagPreviewsByAuthor(int authorId);
List<RetagBookFilePreview> GetRetagPreviewsByBook(int authorId); List<RetagBookFilePreview> GetRetagPreviewsByBook(int bookId);
void RetagFiles(RetagFilesCommand message);
void RetagAuthor(RetagAuthorCommand message);
} }
public class AudioTagService : IAudioTagService, public class AudioTagService : IAudioTagService
IExecute<RetagAuthorCommand>,
IExecute<RetagFilesCommand>
{ {
private readonly IConfigService _configService; private readonly IConfigService _configService;
private readonly IMediaFileService _mediaFileService; private readonly IMediaFileService _mediaFileService;
@ -71,7 +72,52 @@ namespace NzbDrone.Core.MediaFiles
public AudioTag GetTrackMetadata(BookFile trackfile) public AudioTag GetTrackMetadata(BookFile trackfile)
{ {
return new AudioTag(); var edition = trackfile.Edition.Value;
var book = edition.Book.Value;
var author = book.Author.Value;
var fileTags = ReadAudioTag(trackfile.Path);
var cover = edition.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Cover);
string imageFile = null;
long imageSize = 0;
if (cover != null)
{
imageFile = _mediaCoverService.GetCoverPath(book.Id, MediaCoverEntity.Book, cover.CoverType, cover.Extension, null);
_logger.Trace($"Embedding: {imageFile}");
var fileInfo = _diskProvider.GetFileInfo(imageFile);
if (fileInfo.Exists)
{
imageSize = fileInfo.Length;
}
else
{
imageFile = null;
}
}
return new AudioTag
{
Title = edition.Title,
Performers = new[] { author.Name },
BookAuthors = new[] { author.Name },
Track = fileTags.Track,
TrackCount = fileTags.TrackCount,
Book = book.Title,
Disc = fileTags.Disc,
DiscCount = fileTags.DiscCount,
// We may have omitted media so index in the list isn't the same as medium number
Media = fileTags.Media,
Date = edition.ReleaseDate,
Year = (uint)edition.ReleaseDate?.Year,
OriginalReleaseDate = book.ReleaseDate,
OriginalYear = (uint)book.ReleaseDate?.Year,
Publisher = edition.Publisher,
Genres = new string[0],
ImageFile = imageFile,
ImageSize = imageSize,
};
} }
private void UpdateTrackfileSizeAndModified(BookFile trackfile, string path) private void UpdateTrackfileSizeAndModified(BookFile trackfile, string path)
@ -187,7 +233,7 @@ namespace NzbDrone.Core.MediaFiles
private IEnumerable<RetagBookFilePreview> GetPreviews(List<BookFile> files) private IEnumerable<RetagBookFilePreview> GetPreviews(List<BookFile> files)
{ {
foreach (var f in files.OrderBy(x => x.Edition.Value.Title)) foreach (var f in files.Where(x => MediaFileExtensions.AudioExtensions.Contains(Path.GetExtension(x.Path))).OrderBy(x => x.Edition.Value.Title))
{ {
var file = f; var file = f;
@ -215,35 +261,38 @@ namespace NzbDrone.Core.MediaFiles
} }
} }
public void Execute(RetagFilesCommand message) public void RetagFiles(RetagFilesCommand message)
{ {
var author = _authorService.GetAuthor(message.AuthorId); var author = _authorService.GetAuthor(message.AuthorId);
var bookFiles = _mediaFileService.Get(message.Files); var bookFiles = _mediaFileService.Get(message.Files);
var audioFiles = bookFiles.Where(x => MediaFileExtensions.AudioExtensions.Contains(Path.GetExtension(x.Path))).ToList();
_logger.ProgressInfo("Re-tagging {0} files for {1}", bookFiles.Count, author.Name); _logger.ProgressInfo("Re-tagging {0} audio files for {1}", audioFiles.Count, author.Name);
foreach (var file in bookFiles) foreach (var file in audioFiles)
{ {
WriteTags(file, false, force: true); WriteTags(file, false, force: true);
} }
_logger.ProgressInfo("Selected track files re-tagged for {0}", author.Name); _logger.ProgressInfo("Selected audio files re-tagged for {0}", author.Name);
} }
public void Execute(RetagAuthorCommand message) public void RetagAuthor(RetagAuthorCommand message)
{ {
_logger.Debug("Re-tagging all files for selected authors"); _logger.Debug("Re-tagging all audio files for selected authors");
var authorToRename = _authorService.GetAuthors(message.AuthorIds); var authorToRename = _authorService.GetAuthors(message.AuthorIds);
foreach (var author in authorToRename) foreach (var author in authorToRename)
{ {
var bookFiles = _mediaFileService.GetFilesByAuthor(author.Id); var bookFiles = _mediaFileService.GetFilesByAuthor(author.Id);
_logger.ProgressInfo("Re-tagging all files for author: {0}", author.Name); var audioFiles = bookFiles.Where(x => MediaFileExtensions.AudioExtensions.Contains(Path.GetExtension(x.Path))).ToList();
foreach (var file in bookFiles)
_logger.ProgressInfo("Re-tagging all audio files for author: {0}", author.Name);
foreach (var file in audioFiles)
{ {
WriteTags(file, false, force: true); WriteTags(file, false, force: true);
} }
_logger.ProgressInfo("All track files re-tagged for {0}", author.Name); _logger.ProgressInfo("All audio files re-tagged for {0}", author.Name);
} }
} }
} }

@ -21,11 +21,15 @@ namespace NzbDrone.Core.MediaFiles
public MediaInfoModel MediaInfo { get; set; } public MediaInfoModel MediaInfo { get; set; }
public int EditionId { get; set; } public int EditionId { get; set; }
public int CalibreId { get; set; } public int CalibreId { get; set; }
public int Part { get; set; }
// These are queried from the database // These are queried from the database
public LazyLoaded<Author> Author { get; set; } public LazyLoaded<Author> Author { get; set; }
public LazyLoaded<Edition> Edition { get; set; } public LazyLoaded<Edition> Edition { get; set; }
// Calculated manually
public int PartCount { get; set; }
public override string ToString() public override string ToString()
{ {
return string.Format("[{0}] {1}", Id, Path); return string.Format("[{0}] {1}", Id, Path);

@ -153,7 +153,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport
try try
{ {
//check if already imported //check if already imported
if (importResults.Select(r => r.ImportDecision.Item.Book.Id).Contains(localTrack.Book.Id)) if (importResults.Where(r => r.ImportDecision.Item.Book.Id == localTrack.Book.Id).Any(r => r.ImportDecision.Item.Part == localTrack.Part))
{ {
importResults.Add(new ImportResult(importDecision, "Book has already been imported")); importResults.Add(new ImportResult(importDecision, "Book has already been imported"));
continue; continue;
@ -165,6 +165,8 @@ namespace NzbDrone.Core.MediaFiles.BookImport
{ {
Path = localTrack.Path.CleanFilePath(), Path = localTrack.Path.CleanFilePath(),
CalibreId = localTrack.CalibreId, CalibreId = localTrack.CalibreId,
Part = localTrack.Part,
PartCount = localTrack.PartCount,
Size = localTrack.Size, Size = localTrack.Size,
Modified = localTrack.Modified, Modified = localTrack.Modified,
DateAdded = DateTime.UtcNow, DateAdded = DateTime.UtcNow,

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions; using System.IO.Abstractions;
using System.Linq; using System.Linq;
using NLog; using NLog;
@ -48,8 +49,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport
private readonly IEnumerable<IImportDecisionEngineSpecification<LocalBook>> _trackSpecifications; private readonly IEnumerable<IImportDecisionEngineSpecification<LocalBook>> _trackSpecifications;
private readonly IEnumerable<IImportDecisionEngineSpecification<LocalEdition>> _bookSpecifications; private readonly IEnumerable<IImportDecisionEngineSpecification<LocalEdition>> _bookSpecifications;
private readonly IMediaFileService _mediaFileService; private readonly IMediaFileService _mediaFileService;
private readonly IEBookTagService _eBookTagService; private readonly IMetadataTagService _metadataTagService;
private readonly IAudioTagService _audioTagService;
private readonly IAugmentingService _augmentingService; private readonly IAugmentingService _augmentingService;
private readonly IIdentificationService _identificationService; private readonly IIdentificationService _identificationService;
private readonly IRootFolderService _rootFolderService; private readonly IRootFolderService _rootFolderService;
@ -59,8 +59,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport
public ImportDecisionMaker(IEnumerable<IImportDecisionEngineSpecification<LocalBook>> trackSpecifications, public ImportDecisionMaker(IEnumerable<IImportDecisionEngineSpecification<LocalBook>> trackSpecifications,
IEnumerable<IImportDecisionEngineSpecification<LocalEdition>> bookSpecifications, IEnumerable<IImportDecisionEngineSpecification<LocalEdition>> bookSpecifications,
IMediaFileService mediaFileService, IMediaFileService mediaFileService,
IEBookTagService eBookTagService, IMetadataTagService metadataTagService,
IAudioTagService audioTagService,
IAugmentingService augmentingService, IAugmentingService augmentingService,
IIdentificationService identificationService, IIdentificationService identificationService,
IRootFolderService rootFolderService, IRootFolderService rootFolderService,
@ -70,8 +69,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport
_trackSpecifications = trackSpecifications; _trackSpecifications = trackSpecifications;
_bookSpecifications = bookSpecifications; _bookSpecifications = bookSpecifications;
_mediaFileService = mediaFileService; _mediaFileService = mediaFileService;
_eBookTagService = eBookTagService; _metadataTagService = metadataTagService;
_audioTagService = audioTagService;
_augmentingService = augmentingService; _augmentingService = augmentingService;
_identificationService = identificationService; _identificationService = identificationService;
_rootFolderService = rootFolderService; _rootFolderService = rootFolderService;
@ -108,14 +106,17 @@ namespace NzbDrone.Core.MediaFiles.BookImport
{ {
_logger.ProgressInfo($"Reading file {i++}/{files.Count}"); _logger.ProgressInfo($"Reading file {i++}/{files.Count}");
var fileTrackInfo = _metadataTagService.ReadTags(file);
var localTrack = new LocalBook var localTrack = new LocalBook
{ {
DownloadClientBookInfo = downloadClientItemInfo, DownloadClientBookInfo = downloadClientItemInfo,
FolderTrackInfo = folderInfo, FolderTrackInfo = folderInfo,
Path = file.FullName, Path = file.FullName,
Part = fileTrackInfo.TrackNumbers.Any() ? fileTrackInfo.TrackNumbers.First() : 1,
Size = file.Length, Size = file.Length,
Modified = file.LastWriteTimeUtc, Modified = file.LastWriteTimeUtc,
FileTrackInfo = _eBookTagService.ReadTags(file), FileTrackInfo = fileTrackInfo,
AdditionalFile = false AdditionalFile = false
}; };

@ -40,7 +40,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Manual
private readonly IBookService _bookService; private readonly IBookService _bookService;
private readonly IEditionService _editionService; private readonly IEditionService _editionService;
private readonly IProvideBookInfo _bookInfo; private readonly IProvideBookInfo _bookInfo;
private readonly IAudioTagService _audioTagService; private readonly IMetadataTagService _metadataTagService;
private readonly IImportApprovedBooks _importApprovedBooks; private readonly IImportApprovedBooks _importApprovedBooks;
private readonly ITrackedDownloadService _trackedDownloadService; private readonly ITrackedDownloadService _trackedDownloadService;
private readonly IDownloadedBooksImportService _downloadedTracksImportService; private readonly IDownloadedBooksImportService _downloadedTracksImportService;
@ -57,7 +57,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Manual
IBookService bookService, IBookService bookService,
IEditionService editionService, IEditionService editionService,
IProvideBookInfo bookInfo, IProvideBookInfo bookInfo,
IAudioTagService audioTagService, IMetadataTagService metadataTagService,
IImportApprovedBooks importApprovedBooks, IImportApprovedBooks importApprovedBooks,
ITrackedDownloadService trackedDownloadService, ITrackedDownloadService trackedDownloadService,
IDownloadedBooksImportService downloadedTracksImportService, IDownloadedBooksImportService downloadedTracksImportService,
@ -74,7 +74,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Manual
_bookService = bookService; _bookService = bookService;
_editionService = editionService; _editionService = editionService;
_bookInfo = bookInfo; _bookInfo = bookInfo;
_audioTagService = audioTagService; _metadataTagService = metadataTagService;
_importApprovedBooks = importApprovedBooks; _importApprovedBooks = importApprovedBooks;
_trackedDownloadService = trackedDownloadService; _trackedDownloadService = trackedDownloadService;
_downloadedTracksImportService = downloadedTracksImportService; _downloadedTracksImportService = downloadedTracksImportService;
@ -312,16 +312,17 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Manual
edition = tuple.Item2.Editions.Value.SingleOrDefault(x => x.ForeignEditionId == file.ForeignEditionId); edition = tuple.Item2.Editions.Value.SingleOrDefault(x => x.ForeignEditionId == file.ForeignEditionId);
} }
var fileTrackInfo = _audioTagService.ReadTags(file.Path) ?? new ParsedTrackInfo();
var fileInfo = _diskProvider.GetFileInfo(file.Path);
var fileRootFolder = _rootFolderService.GetBestRootFolder(file.Path); var fileRootFolder = _rootFolderService.GetBestRootFolder(file.Path);
var fileInfo = _diskProvider.GetFileInfo(file.Path);
var fileTrackInfo = _metadataTagService.ReadTags(fileInfo) ?? new ParsedTrackInfo();
var localTrack = new LocalBook var localTrack = new LocalBook
{ {
ExistingFile = fileRootFolder != null, ExistingFile = fileRootFolder != null,
FileTrackInfo = fileTrackInfo, FileTrackInfo = fileTrackInfo,
Path = file.Path, Path = file.Path,
Part = fileTrackInfo.TrackNumbers.Any() ? fileTrackInfo.TrackNumbers.First() : 1,
PartCount = importBookId.Count(),
Size = fileInfo.Length, Size = fileInfo.Length,
Modified = fileInfo.LastWriteTimeUtc, Modified = fileInfo.LastWriteTimeUtc,
Quality = file.Quality, Quality = file.Quality,

@ -181,6 +181,8 @@ namespace NzbDrone.Core.MediaFiles
{ {
Path = decision.Item.Path, Path = decision.Item.Path,
CalibreId = decision.Item.CalibreId, CalibreId = decision.Item.CalibreId,
Part = decision.Item.Part,
PartCount = decision.Item.PartCount,
Size = decision.Item.Size, Size = decision.Item.Size,
Modified = decision.Item.Modified, Modified = decision.Item.Modified,
DateAdded = DateTime.UtcNow, DateAdded = DateTime.UtcNow,

@ -28,12 +28,12 @@ namespace NzbDrone.Core.MediaFiles
void WriteTags(BookFile trackfile, bool newDownload, bool force = false); void WriteTags(BookFile trackfile, bool newDownload, bool force = false);
void SyncTags(List<Edition> books); void SyncTags(List<Edition> books);
List<RetagBookFilePreview> GetRetagPreviewsByAuthor(int authorId); List<RetagBookFilePreview> GetRetagPreviewsByAuthor(int authorId);
List<RetagBookFilePreview> GetRetagPreviewsByBook(int authorId); List<RetagBookFilePreview> GetRetagPreviewsByBook(int bookId);
void RetagFiles(RetagFilesCommand message);
void RetagAuthor(RetagAuthorCommand message);
} }
public class EBookTagService : IEBookTagService, public class EBookTagService : IEBookTagService
IExecute<RetagFilesCommand>,
IExecute<RetagAuthorCommand>
{ {
private readonly IAuthorService _authorService; private readonly IAuthorService _authorService;
private readonly IMediaFileService _mediaFileService; private readonly IMediaFileService _mediaFileService;
@ -132,38 +132,38 @@ namespace NzbDrone.Core.MediaFiles
return GetPreviews(files).ToList(); return GetPreviews(files).ToList();
} }
public void Execute(RetagFilesCommand message) public void RetagFiles(RetagFilesCommand message)
{ {
var author = _authorService.GetAuthor(message.AuthorId); var author = _authorService.GetAuthor(message.AuthorId);
var files = _mediaFileService.Get(message.Files); var files = _mediaFileService.Get(message.Files);
_logger.ProgressInfo("Re-tagging {0} files for {1}", files.Count, author.Name); _logger.ProgressInfo("Re-tagging {0} ebook files for {1}", files.Count, author.Name);
foreach (var file in files.Where(x => x.CalibreId != 0)) foreach (var file in files.Where(x => x.CalibreId != 0))
{ {
WriteTagsInternal(file, message.UpdateCovers, message.EmbedMetadata); WriteTagsInternal(file, message.UpdateCovers, message.EmbedMetadata);
} }
_logger.ProgressInfo("Selected files re-tagged for {0}", author.Name); _logger.ProgressInfo("Selected ebook files re-tagged for {0}", author.Name);
} }
public void Execute(RetagAuthorCommand message) public void RetagAuthor(RetagAuthorCommand message)
{ {
_logger.Debug("Re-tagging all files for selected authors"); _logger.Debug("Re-tagging all ebook files for selected authors");
var authorsToRename = _authorService.GetAuthors(message.AuthorIds); var authorsToRename = _authorService.GetAuthors(message.AuthorIds);
foreach (var author in authorsToRename) foreach (var author in authorsToRename)
{ {
var files = _mediaFileService.GetFilesByAuthor(author.Id); var files = _mediaFileService.GetFilesByAuthor(author.Id);
_logger.ProgressInfo("Re-tagging all files for author: {0}", author.Name); _logger.ProgressInfo("Re-tagging all ebook files for author: {0}", author.Name);
foreach (var file in files.Where(x => x.CalibreId != 0)) foreach (var file in files.Where(x => x.CalibreId != 0))
{ {
WriteTagsInternal(file, message.UpdateCovers, message.EmbedMetadata); WriteTagsInternal(file, message.UpdateCovers, message.EmbedMetadata);
} }
_logger.ProgressInfo("All files re-tagged for {0}", author.Name); _logger.ProgressInfo("All ebook files re-tagged for {0}", author.Name);
} }
} }

@ -23,6 +23,22 @@ namespace NzbDrone.Core.MediaFiles
_audioExtensions = new Dictionary<string, Quality>(StringComparer.OrdinalIgnoreCase) _audioExtensions = new Dictionary<string, Quality>(StringComparer.OrdinalIgnoreCase)
{ {
{ ".flac", Quality.FLAC },
{ ".ape", Quality.FLAC },
{ ".wavpack", Quality.FLAC },
{ ".wav", Quality.FLAC },
{ ".alac", Quality.FLAC },
{ ".mp2", Quality.MP3_320 },
{ ".mp3", Quality.MP3_320 },
{ ".wma", Quality.MP3_320 },
{ ".m4a", Quality.MP3_320 },
{ ".m4p", Quality.MP3_320 },
{ ".m4b", Quality.MP3_320 },
{ ".aac", Quality.MP3_320 },
{ ".mp4a", Quality.MP3_320 },
{ ".ogg", Quality.MP3_320 },
{ ".oga", Quality.MP3_320 },
{ ".vorbis", Quality.MP3_320 },
}; };
} }

@ -0,0 +1,92 @@
using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using NLog;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles
{
public interface IMetadataTagService
{
ParsedTrackInfo ReadTags(IFileInfo file);
void WriteTags(BookFile trackfile, bool newDownload, bool force = false);
List<RetagBookFilePreview> GetRetagPreviewsByAuthor(int authorId);
List<RetagBookFilePreview> GetRetagPreviewsByBook(int authorId);
}
public class MetadataTagService : IMetadataTagService,
IExecute<RetagFilesCommand>,
IExecute<RetagAuthorCommand>
{
private readonly IAudioTagService _audioTagService;
private readonly IEBookTagService _eBookTagService;
private readonly Logger _logger;
public MetadataTagService(IAudioTagService audioTagService,
IEBookTagService eBookTagService,
Logger logger)
{
_audioTagService = audioTagService;
_eBookTagService = eBookTagService;
_logger = logger;
}
public ParsedTrackInfo ReadTags(IFileInfo file)
{
if (MediaFileExtensions.AudioExtensions.Contains(file.Extension))
{
return _audioTagService.ReadTags(file.FullName);
}
else
{
return _eBookTagService.ReadTags(file);
}
}
public void WriteTags(BookFile bookFile, bool newDownload, bool force = false)
{
var extension = Path.GetExtension(bookFile.Path);
if (MediaFileExtensions.AudioExtensions.Contains(extension))
{
_audioTagService.WriteTags(bookFile, newDownload, force);
}
else
{
_eBookTagService.WriteTags(bookFile, newDownload, force);
}
}
public List<RetagBookFilePreview> GetRetagPreviewsByAuthor(int authorId)
{
var previews = _audioTagService.GetRetagPreviewsByAuthor(authorId);
previews.AddRange(_eBookTagService.GetRetagPreviewsByAuthor(authorId));
return previews;
}
public List<RetagBookFilePreview> GetRetagPreviewsByBook(int bookId)
{
var previews = _audioTagService.GetRetagPreviewsByBook(bookId);
previews.AddRange(_eBookTagService.GetRetagPreviewsByBook(bookId));
return previews;
}
public void Execute(RetagFilesCommand message)
{
_eBookTagService.RetagFiles(message);
_audioTagService.RetagFiles(message);
}
public void Execute(RetagAuthorCommand message)
{
_eBookTagService.RetagAuthor(message);
_audioTagService.RetagAuthor(message);
}
}
}

@ -57,6 +57,7 @@ namespace NzbDrone.Core.MediaFiles
return GetPreviews(author, files) return GetPreviews(author, files)
.OrderByDescending(e => e.BookId) .OrderByDescending(e => e.BookId)
.ThenBy(e => e.ExistingPath)
.ToList(); .ToList();
} }
@ -66,15 +67,19 @@ namespace NzbDrone.Core.MediaFiles
var files = _mediaFileService.GetFilesByBook(bookId); var files = _mediaFileService.GetFilesByBook(bookId);
return GetPreviews(author, files) return GetPreviews(author, files)
.OrderByDescending(e => e.TrackNumbers.First()).ToList(); .OrderBy(e => e.ExistingPath).ToList();
} }
private IEnumerable<RenameBookFilePreview> GetPreviews(Author author, List<BookFile> files) private IEnumerable<RenameBookFilePreview> GetPreviews(Author author, List<BookFile> files)
{ {
var counts = files.GroupBy(x => x.EditionId).ToDictionary(g => g.Key, g => g.Count());
// Don't rename Calibre files // Don't rename Calibre files
foreach (var f in files.Where(x => x.CalibreId == 0)) foreach (var f in files.Where(x => x.CalibreId == 0))
{ {
var file = f; var file = f;
file.PartCount = counts[file.EditionId];
var book = file.Edition.Value; var book = file.Edition.Value;
var bookFilePath = file.Path; var bookFilePath = file.Path;

@ -113,6 +113,11 @@ namespace NzbDrone.Core.Organizer
fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString());
fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty); fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty);
if (bookFile.PartCount > 1)
{
fileName = fileName + " (" + bookFile.Part + ")";
}
return fileName; return fileName;
} }

@ -10,6 +10,8 @@ namespace NzbDrone.Core.Parser.Model
{ {
public string Path { get; set; } public string Path { get; set; }
public int CalibreId { get; set; } public int CalibreId { get; set; }
public int Part { get; set; }
public int PartCount { get; set; }
public long Size { get; set; } public long Size { get; set; }
public DateTime Modified { get; set; } public DateTime Modified { get; set; }
public ParsedTrackInfo FileTrackInfo { get; set; } public ParsedTrackInfo FileTrackInfo { get; set; }

@ -45,6 +45,7 @@ namespace NzbDrone.Core.Parser.Model
localTrack.Edition = Edition; localTrack.Edition = Edition;
localTrack.Book = Edition.Book.Value; localTrack.Book = Edition.Book.Value;
localTrack.Author = Edition.Book.Value.Author.Value; localTrack.Author = Edition.Book.Value.Author.Value;
localTrack.PartCount = LocalBooks.Count;
} }
} }
} }

@ -26,7 +26,7 @@ namespace Readarr.Api.V1.BookFiles
{ {
private readonly IMediaFileService _mediaFileService; private readonly IMediaFileService _mediaFileService;
private readonly IDeleteMediaFiles _mediaFileDeletionService; private readonly IDeleteMediaFiles _mediaFileDeletionService;
private readonly IEBookTagService _eBookTagService; private readonly IMetadataTagService _metadataTagService;
private readonly IAuthorService _authorService; private readonly IAuthorService _authorService;
private readonly IBookService _bookService; private readonly IBookService _bookService;
private readonly IUpgradableSpecification _upgradableSpecification; private readonly IUpgradableSpecification _upgradableSpecification;
@ -34,7 +34,7 @@ namespace Readarr.Api.V1.BookFiles
public BookFileController(IBroadcastSignalRMessage signalRBroadcaster, public BookFileController(IBroadcastSignalRMessage signalRBroadcaster,
IMediaFileService mediaFileService, IMediaFileService mediaFileService,
IDeleteMediaFiles mediaFileDeletionService, IDeleteMediaFiles mediaFileDeletionService,
IEBookTagService eBookTagService, IMetadataTagService metadataTagService,
IAuthorService authorService, IAuthorService authorService,
IBookService bookService, IBookService bookService,
IUpgradableSpecification upgradableSpecification) IUpgradableSpecification upgradableSpecification)
@ -42,7 +42,7 @@ namespace Readarr.Api.V1.BookFiles
{ {
_mediaFileService = mediaFileService; _mediaFileService = mediaFileService;
_mediaFileDeletionService = mediaFileDeletionService; _mediaFileDeletionService = mediaFileDeletionService;
_eBookTagService = eBookTagService; _metadataTagService = metadataTagService;
_authorService = authorService; _authorService = authorService;
_bookService = bookService; _bookService = bookService;
_upgradableSpecification = upgradableSpecification; _upgradableSpecification = upgradableSpecification;
@ -63,7 +63,7 @@ namespace Readarr.Api.V1.BookFiles
public override BookFileResource GetResourceById(int id) public override BookFileResource GetResourceById(int id)
{ {
var resource = MapToResource(_mediaFileService.Get(id)); var resource = MapToResource(_mediaFileService.Get(id));
resource.AudioTags = _eBookTagService.ReadTags((FileInfoBase)new FileInfo(resource.Path)); resource.AudioTags = _metadataTagService.ReadTags((FileInfoBase)new FileInfo(resource.Path));
return resource; return resource;
} }

@ -10,11 +10,11 @@ namespace Readarr.Api.V1.Books
[V1ApiController("retag")] [V1ApiController("retag")]
public class RetagBookController : Controller public class RetagBookController : Controller
{ {
private readonly IEBookTagService _eBookTagService; private readonly IMetadataTagService _metadataTagService;
public RetagBookController(IEBookTagService eBookTagService) public RetagBookController(IMetadataTagService metadataTagService)
{ {
_eBookTagService = eBookTagService; _metadataTagService = metadataTagService;
} }
[HttpGet] [HttpGet]
@ -22,11 +22,11 @@ namespace Readarr.Api.V1.Books
{ {
if (bookId.HasValue) if (bookId.HasValue)
{ {
return _eBookTagService.GetRetagPreviewsByBook(bookId.Value).Where(x => x.Changes.Any()).ToResource(); return _metadataTagService.GetRetagPreviewsByBook(bookId.Value).Where(x => x.Changes.Any()).ToResource();
} }
else if (authorId.HasValue) else if (authorId.HasValue)
{ {
return _eBookTagService.GetRetagPreviewsByAuthor(authorId.Value).Where(x => x.Changes.Any()).ToResource(); return _metadataTagService.GetRetagPreviewsByAuthor(authorId.Value).Where(x => x.Changes.Any()).ToResource();
} }
else else
{ {

@ -5,6 +5,8 @@ namespace Readarr.Api.V1.Config
{ {
public class MetadataProviderConfigResource : RestResource public class MetadataProviderConfigResource : RestResource
{ {
public WriteAudioTagsType WriteAudioTags { get; set; }
public bool ScrubAudioTags { get; set; }
public WriteBookTagsType WriteBookTags { get; set; } public WriteBookTagsType WriteBookTags { get; set; }
public bool UpdateCovers { get; set; } public bool UpdateCovers { get; set; }
public bool EmbedMetadata { get; set; } public bool EmbedMetadata { get; set; }
@ -16,6 +18,8 @@ namespace Readarr.Api.V1.Config
{ {
return new MetadataProviderConfigResource return new MetadataProviderConfigResource
{ {
WriteAudioTags = model.WriteAudioTags,
ScrubAudioTags = model.ScrubAudioTags,
WriteBookTags = model.WriteBookTags, WriteBookTags = model.WriteBookTags,
UpdateCovers = model.UpdateCovers, UpdateCovers = model.UpdateCovers,
EmbedMetadata = model.EmbedMetadata EmbedMetadata = model.EmbedMetadata

Loading…
Cancel
Save