mirror of
https://github.com/Readarr/Readarr
synced 2025-12-14 20:36:18 +01:00
New: Basic audiobook support
This commit is contained in:
parent
62928b227b
commit
f6a04f7890
25 changed files with 316 additions and 58 deletions
|
|
@ -9,10 +9,17 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|||
import { inputTypes } from 'Helpers/Props';
|
||||
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 = [
|
||||
{ key: 'sync', value: 'All files; keep in sync with Goodreads' },
|
||||
{ key: 'allFiles', value: 'All files; initial import only' },
|
||||
{ key: 'newFiles', value: 'For new downloads only' }
|
||||
{ key: 'sync', value: translate('WriteTagsSync') },
|
||||
{ key: 'allFiles', value: translate('WriteTagsAll') },
|
||||
{ key: 'newFiles', value: translate('WriteTagsNew') }
|
||||
];
|
||||
|
||||
function MetadataProvider(props) {
|
||||
|
|
@ -88,6 +95,35 @@ function MetadataProvider(props) {
|
|||
</FormGroup>
|
||||
|
||||
</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>
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -130,6 +130,10 @@ public void Setup()
|
|||
.Setup(c => c.FilterUnchangedFiles(It.IsAny<List<IFileInfo>>(), It.IsAny<FilterFilesType>()))
|
||||
.Returns((List<IFileInfo> files, FilterFilesType filter) => files);
|
||||
|
||||
Mocker.GetMock<IMetadataTagService>()
|
||||
.Setup(s => s.ReadTags(It.IsAny<IFileInfo>()))
|
||||
.Returns(new ParsedTrackInfo());
|
||||
|
||||
GivenSpecifications(_bookpass1);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ public void Setup()
|
|||
.Setup(c => c.GetConfig()).Returns(_namingConfig);
|
||||
|
||||
_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.ReleaseGroup = "ReadarrTest")
|
||||
.With(e => e.MediaInfo = new Parser.Model.MediaInfoModel
|
||||
|
|
|
|||
|
|
@ -83,11 +83,11 @@ public void should_check_top_level_directory_only_when_allDirectories_is_false()
|
|||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_audio_files_only()
|
||||
public void should_return_book_files_only()
|
||||
{
|
||||
GivenFiles(GetFiles(_path));
|
||||
|
||||
Subject.GetBookFiles(_path).Should().HaveCount(3);
|
||||
Subject.GetBookFiles(_path).Should().HaveCount(5);
|
||||
}
|
||||
|
||||
[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 @@ public static void Map()
|
|||
b => b.Id > 0);
|
||||
|
||||
Mapper.Entity<BookFile>("BookFiles").RegisterModel()
|
||||
.Ignore(x => x.PartCount)
|
||||
.HasOne(f => f.Edition, f => f.EditionId)
|
||||
.LazyLoad(x => x.Author,
|
||||
(db, f) => AuthorRepository.Query(db,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ public static class NewznabCategoryFieldOptionsConverter
|
|||
public static List<FieldSelectOption> GetFieldSelectOptions(List<NewznabCategory> categories)
|
||||
{
|
||||
// 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
|
||||
var unimportantCategories = new[] { 0, 8000 };
|
||||
|
|
@ -22,6 +22,15 @@ public static List<FieldSelectOption> GetFieldSelectOptions(List<NewznabCategory
|
|||
// Fetching categories failed, use default Newznab categories
|
||||
categories = new List<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,
|
||||
Name = "Books",
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ public class NewznabSettings : IIndexerSettings
|
|||
public NewznabSettings()
|
||||
{
|
||||
ApiPath = "/api";
|
||||
Categories = new[] { 7020, 8010 };
|
||||
Categories = new[] { 3030, 7020, 8010 };
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "URL")]
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@
|
|||
"ApplyTagsHelpTexts2": "Add: Add the tags the existing list of tags",
|
||||
"ApplyTagsHelpTexts3": "Remove: Remove the entered 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",
|
||||
"AuthenticationMethodHelpText": "Require Username and Password to access Readarr",
|
||||
"Author": "Author",
|
||||
|
|
@ -351,7 +352,7 @@
|
|||
"MinimumFreeSpaceWhenImportingHelpText": "Prevent import if it would leave less than this amount of disk space available",
|
||||
"MinimumLimits": "Minimum Limits",
|
||||
"MinimumPages": "Minimum Pages",
|
||||
"MinPagesHelpText": "Ignore books with fewer pages than this",
|
||||
"MinPagesHelpText": "Ignore books with fewer pages than this",
|
||||
"MinimumPopularity": "Minimum Popularity",
|
||||
"Missing": "Missing",
|
||||
"MissingBooks": "Missing Books",
|
||||
|
|
@ -691,7 +692,14 @@
|
|||
"WatchLibraryForChangesHelpText": "Rescan automatically when files change in a root folder",
|
||||
"WatchRootFoldersForFileChanges": "Watch Root Folders for file changes",
|
||||
"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.",
|
||||
"WriteTagsAll": "All files; initial import only",
|
||||
"WriteTagsNew": "For new downloads only",
|
||||
"WriteTagsNo": "Never",
|
||||
"WriteTagsSync": "All files; keep in sync with Goodreads",
|
||||
"Year": "Year",
|
||||
"YesCancel": "Yes, Cancel"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NLog.Fluent;
|
||||
|
|
@ -24,12 +25,12 @@ public interface IAudioTagService
|
|||
void WriteTags(BookFile trackfile, bool newDownload, bool force = false);
|
||||
void SyncTags(List<Edition> tracks);
|
||||
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,
|
||||
IExecute<RetagAuthorCommand>,
|
||||
IExecute<RetagFilesCommand>
|
||||
public class AudioTagService : IAudioTagService
|
||||
{
|
||||
private readonly IConfigService _configService;
|
||||
private readonly IMediaFileService _mediaFileService;
|
||||
|
|
@ -71,7 +72,52 @@ public ParsedTrackInfo ReadTags(string path)
|
|||
|
||||
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)
|
||||
|
|
@ -187,7 +233,7 @@ public List<RetagBookFilePreview> GetRetagPreviewsByBook(int bookId)
|
|||
|
||||
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;
|
||||
|
||||
|
|
@ -215,35 +261,38 @@ private IEnumerable<RetagBookFilePreview> GetPreviews(List<BookFile> files)
|
|||
}
|
||||
}
|
||||
|
||||
public void Execute(RetagFilesCommand message)
|
||||
public void RetagFiles(RetagFilesCommand message)
|
||||
{
|
||||
var author = _authorService.GetAuthor(message.AuthorId);
|
||||
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);
|
||||
foreach (var file in bookFiles)
|
||||
_logger.ProgressInfo("Re-tagging {0} audio files for {1}", audioFiles.Count, author.Name);
|
||||
foreach (var file in audioFiles)
|
||||
{
|
||||
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);
|
||||
|
||||
foreach (var author in authorToRename)
|
||||
{
|
||||
var bookFiles = _mediaFileService.GetFilesByAuthor(author.Id);
|
||||
_logger.ProgressInfo("Re-tagging all files for author: {0}", author.Name);
|
||||
foreach (var file in bookFiles)
|
||||
var audioFiles = bookFiles.Where(x => MediaFileExtensions.AudioExtensions.Contains(Path.GetExtension(x.Path))).ToList();
|
||||
|
||||
_logger.ProgressInfo("Re-tagging all audio files for author: {0}", author.Name);
|
||||
foreach (var file in audioFiles)
|
||||
{
|
||||
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 @@ public class BookFile : ModelBase
|
|||
public MediaInfoModel MediaInfo { get; set; }
|
||||
public int EditionId { get; set; }
|
||||
public int CalibreId { get; set; }
|
||||
public int Part { get; set; }
|
||||
|
||||
// These are queried from the database
|
||||
public LazyLoaded<Author> Author { get; set; }
|
||||
public LazyLoaded<Edition> Edition { get; set; }
|
||||
|
||||
// Calculated manually
|
||||
public int PartCount { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[{0}] {1}", Id, Path);
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ public List<ImportResult> Import(List<ImportDecision<LocalBook>> decisions, bool
|
|||
try
|
||||
{
|
||||
//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"));
|
||||
continue;
|
||||
|
|
@ -165,6 +165,8 @@ public List<ImportResult> Import(List<ImportDecision<LocalBook>> decisions, bool
|
|||
{
|
||||
Path = localTrack.Path.CleanFilePath(),
|
||||
CalibreId = localTrack.CalibreId,
|
||||
Part = localTrack.Part,
|
||||
PartCount = localTrack.PartCount,
|
||||
Size = localTrack.Size,
|
||||
Modified = localTrack.Modified,
|
||||
DateAdded = DateTime.UtcNow,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
|
|
@ -48,8 +49,7 @@ public class ImportDecisionMaker : IMakeImportDecision
|
|||
private readonly IEnumerable<IImportDecisionEngineSpecification<LocalBook>> _trackSpecifications;
|
||||
private readonly IEnumerable<IImportDecisionEngineSpecification<LocalEdition>> _bookSpecifications;
|
||||
private readonly IMediaFileService _mediaFileService;
|
||||
private readonly IEBookTagService _eBookTagService;
|
||||
private readonly IAudioTagService _audioTagService;
|
||||
private readonly IMetadataTagService _metadataTagService;
|
||||
private readonly IAugmentingService _augmentingService;
|
||||
private readonly IIdentificationService _identificationService;
|
||||
private readonly IRootFolderService _rootFolderService;
|
||||
|
|
@ -59,8 +59,7 @@ public class ImportDecisionMaker : IMakeImportDecision
|
|||
public ImportDecisionMaker(IEnumerable<IImportDecisionEngineSpecification<LocalBook>> trackSpecifications,
|
||||
IEnumerable<IImportDecisionEngineSpecification<LocalEdition>> bookSpecifications,
|
||||
IMediaFileService mediaFileService,
|
||||
IEBookTagService eBookTagService,
|
||||
IAudioTagService audioTagService,
|
||||
IMetadataTagService metadataTagService,
|
||||
IAugmentingService augmentingService,
|
||||
IIdentificationService identificationService,
|
||||
IRootFolderService rootFolderService,
|
||||
|
|
@ -70,8 +69,7 @@ public ImportDecisionMaker(IEnumerable<IImportDecisionEngineSpecification<LocalB
|
|||
_trackSpecifications = trackSpecifications;
|
||||
_bookSpecifications = bookSpecifications;
|
||||
_mediaFileService = mediaFileService;
|
||||
_eBookTagService = eBookTagService;
|
||||
_audioTagService = audioTagService;
|
||||
_metadataTagService = metadataTagService;
|
||||
_augmentingService = augmentingService;
|
||||
_identificationService = identificationService;
|
||||
_rootFolderService = rootFolderService;
|
||||
|
|
@ -108,14 +106,17 @@ public Tuple<List<LocalBook>, List<ImportDecision<LocalBook>>> GetLocalTracks(Li
|
|||
{
|
||||
_logger.ProgressInfo($"Reading file {i++}/{files.Count}");
|
||||
|
||||
var fileTrackInfo = _metadataTagService.ReadTags(file);
|
||||
|
||||
var localTrack = new LocalBook
|
||||
{
|
||||
DownloadClientBookInfo = downloadClientItemInfo,
|
||||
FolderTrackInfo = folderInfo,
|
||||
Path = file.FullName,
|
||||
Part = fileTrackInfo.TrackNumbers.Any() ? fileTrackInfo.TrackNumbers.First() : 1,
|
||||
Size = file.Length,
|
||||
Modified = file.LastWriteTimeUtc,
|
||||
FileTrackInfo = _eBookTagService.ReadTags(file),
|
||||
FileTrackInfo = fileTrackInfo,
|
||||
AdditionalFile = false
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ public class ManualImportService : IExecute<ManualImportCommand>, IManualImportS
|
|||
private readonly IBookService _bookService;
|
||||
private readonly IEditionService _editionService;
|
||||
private readonly IProvideBookInfo _bookInfo;
|
||||
private readonly IAudioTagService _audioTagService;
|
||||
private readonly IMetadataTagService _metadataTagService;
|
||||
private readonly IImportApprovedBooks _importApprovedBooks;
|
||||
private readonly ITrackedDownloadService _trackedDownloadService;
|
||||
private readonly IDownloadedBooksImportService _downloadedTracksImportService;
|
||||
|
|
@ -57,7 +57,7 @@ public ManualImportService(IDiskProvider diskProvider,
|
|||
IBookService bookService,
|
||||
IEditionService editionService,
|
||||
IProvideBookInfo bookInfo,
|
||||
IAudioTagService audioTagService,
|
||||
IMetadataTagService metadataTagService,
|
||||
IImportApprovedBooks importApprovedBooks,
|
||||
ITrackedDownloadService trackedDownloadService,
|
||||
IDownloadedBooksImportService downloadedTracksImportService,
|
||||
|
|
@ -74,7 +74,7 @@ public ManualImportService(IDiskProvider diskProvider,
|
|||
_bookService = bookService;
|
||||
_editionService = editionService;
|
||||
_bookInfo = bookInfo;
|
||||
_audioTagService = audioTagService;
|
||||
_metadataTagService = metadataTagService;
|
||||
_importApprovedBooks = importApprovedBooks;
|
||||
_trackedDownloadService = trackedDownloadService;
|
||||
_downloadedTracksImportService = downloadedTracksImportService;
|
||||
|
|
@ -312,16 +312,17 @@ public void Execute(ManualImportCommand message)
|
|||
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 fileInfo = _diskProvider.GetFileInfo(file.Path);
|
||||
var fileTrackInfo = _metadataTagService.ReadTags(fileInfo) ?? new ParsedTrackInfo();
|
||||
|
||||
var localTrack = new LocalBook
|
||||
{
|
||||
ExistingFile = fileRootFolder != null,
|
||||
FileTrackInfo = fileTrackInfo,
|
||||
Path = file.Path,
|
||||
Part = fileTrackInfo.TrackNumbers.Any() ? fileTrackInfo.TrackNumbers.First() : 1,
|
||||
PartCount = importBookId.Count(),
|
||||
Size = fileInfo.Length,
|
||||
Modified = fileInfo.LastWriteTimeUtc,
|
||||
Quality = file.Quality,
|
||||
|
|
|
|||
|
|
@ -181,6 +181,8 @@ public void Scan(List<string> folders = null, FilterFilesType filter = FilterFil
|
|||
{
|
||||
Path = decision.Item.Path,
|
||||
CalibreId = decision.Item.CalibreId,
|
||||
Part = decision.Item.Part,
|
||||
PartCount = decision.Item.PartCount,
|
||||
Size = decision.Item.Size,
|
||||
Modified = decision.Item.Modified,
|
||||
DateAdded = DateTime.UtcNow,
|
||||
|
|
|
|||
|
|
@ -28,12 +28,12 @@ public interface IEBookTagService
|
|||
void WriteTags(BookFile trackfile, bool newDownload, bool force = false);
|
||||
void SyncTags(List<Edition> books);
|
||||
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,
|
||||
IExecute<RetagFilesCommand>,
|
||||
IExecute<RetagAuthorCommand>
|
||||
public class EBookTagService : IEBookTagService
|
||||
{
|
||||
private readonly IAuthorService _authorService;
|
||||
private readonly IMediaFileService _mediaFileService;
|
||||
|
|
@ -132,38 +132,38 @@ public List<RetagBookFilePreview> GetRetagPreviewsByBook(int bookId)
|
|||
return GetPreviews(files).ToList();
|
||||
}
|
||||
|
||||
public void Execute(RetagFilesCommand message)
|
||||
public void RetagFiles(RetagFilesCommand message)
|
||||
{
|
||||
var author = _authorService.GetAuthor(message.AuthorId);
|
||||
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))
|
||||
{
|
||||
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);
|
||||
|
||||
foreach (var author in authorsToRename)
|
||||
{
|
||||
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))
|
||||
{
|
||||
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 @@ static MediaFileExtensions()
|
|||
|
||||
_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 },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
92
src/NzbDrone.Core/MediaFiles/MetadataTagService.cs
Normal file
92
src/NzbDrone.Core/MediaFiles/MetadataTagService.cs
Normal file
|
|
@ -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 @@ public List<RenameBookFilePreview> GetRenamePreviews(int authorId)
|
|||
|
||||
return GetPreviews(author, files)
|
||||
.OrderByDescending(e => e.BookId)
|
||||
.ThenBy(e => e.ExistingPath)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
|
@ -66,15 +67,19 @@ public List<RenameBookFilePreview> GetRenamePreviews(int authorId, int bookId)
|
|||
var files = _mediaFileService.GetFilesByBook(bookId);
|
||||
|
||||
return GetPreviews(author, files)
|
||||
.OrderByDescending(e => e.TrackNumbers.First()).ToList();
|
||||
.OrderBy(e => e.ExistingPath).ToList();
|
||||
}
|
||||
|
||||
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
|
||||
foreach (var f in files.Where(x => x.CalibreId == 0))
|
||||
{
|
||||
var file = f;
|
||||
file.PartCount = counts[file.EditionId];
|
||||
|
||||
var book = file.Edition.Value;
|
||||
var bookFilePath = file.Path;
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,11 @@ public string BuildBookFileName(Author author, Edition edition, BookFile bookFil
|
|||
fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString());
|
||||
fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty);
|
||||
|
||||
if (bookFile.PartCount > 1)
|
||||
{
|
||||
fileName = fileName + " (" + bookFile.Part + ")";
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ public class LocalBook
|
|||
{
|
||||
public string Path { get; set; }
|
||||
public int CalibreId { get; set; }
|
||||
public int Part { get; set; }
|
||||
public int PartCount { get; set; }
|
||||
public long Size { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
public ParsedTrackInfo FileTrackInfo { get; set; }
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ public void PopulateMatch()
|
|||
localTrack.Edition = Edition;
|
||||
localTrack.Book = Edition.Book.Value;
|
||||
localTrack.Author = Edition.Book.Value.Author.Value;
|
||||
localTrack.PartCount = LocalBooks.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ public class BookFileController : RestControllerWithSignalR<BookFileResource, Bo
|
|||
{
|
||||
private readonly IMediaFileService _mediaFileService;
|
||||
private readonly IDeleteMediaFiles _mediaFileDeletionService;
|
||||
private readonly IEBookTagService _eBookTagService;
|
||||
private readonly IMetadataTagService _metadataTagService;
|
||||
private readonly IAuthorService _authorService;
|
||||
private readonly IBookService _bookService;
|
||||
private readonly IUpgradableSpecification _upgradableSpecification;
|
||||
|
|
@ -34,7 +34,7 @@ public class BookFileController : RestControllerWithSignalR<BookFileResource, Bo
|
|||
public BookFileController(IBroadcastSignalRMessage signalRBroadcaster,
|
||||
IMediaFileService mediaFileService,
|
||||
IDeleteMediaFiles mediaFileDeletionService,
|
||||
IEBookTagService eBookTagService,
|
||||
IMetadataTagService metadataTagService,
|
||||
IAuthorService authorService,
|
||||
IBookService bookService,
|
||||
IUpgradableSpecification upgradableSpecification)
|
||||
|
|
@ -42,7 +42,7 @@ public BookFileController(IBroadcastSignalRMessage signalRBroadcaster,
|
|||
{
|
||||
_mediaFileService = mediaFileService;
|
||||
_mediaFileDeletionService = mediaFileDeletionService;
|
||||
_eBookTagService = eBookTagService;
|
||||
_metadataTagService = metadataTagService;
|
||||
_authorService = authorService;
|
||||
_bookService = bookService;
|
||||
_upgradableSpecification = upgradableSpecification;
|
||||
|
|
@ -63,7 +63,7 @@ private BookFileResource MapToResource(BookFile bookFile)
|
|||
public override BookFileResource GetResourceById(int 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ namespace Readarr.Api.V1.Books
|
|||
[V1ApiController("retag")]
|
||||
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]
|
||||
|
|
@ -22,11 +22,11 @@ public List<RetagBookResource> GetBooks(int? authorId, int? bookId)
|
|||
{
|
||||
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)
|
||||
{
|
||||
return _eBookTagService.GetRetagPreviewsByAuthor(authorId.Value).Where(x => x.Changes.Any()).ToResource();
|
||||
return _metadataTagService.GetRetagPreviewsByAuthor(authorId.Value).Where(x => x.Changes.Any()).ToResource();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ namespace Readarr.Api.V1.Config
|
|||
{
|
||||
public class MetadataProviderConfigResource : RestResource
|
||||
{
|
||||
public WriteAudioTagsType WriteAudioTags { get; set; }
|
||||
public bool ScrubAudioTags { get; set; }
|
||||
public WriteBookTagsType WriteBookTags { get; set; }
|
||||
public bool UpdateCovers { get; set; }
|
||||
public bool EmbedMetadata { get; set; }
|
||||
|
|
@ -16,6 +18,8 @@ public static MetadataProviderConfigResource ToResource(IConfigService model)
|
|||
{
|
||||
return new MetadataProviderConfigResource
|
||||
{
|
||||
WriteAudioTags = model.WriteAudioTags,
|
||||
ScrubAudioTags = model.ScrubAudioTags,
|
||||
WriteBookTags = model.WriteBookTags,
|
||||
UpdateCovers = model.UpdateCovers,
|
||||
EmbedMetadata = model.EmbedMetadata
|
||||
|
|
|
|||
Loading…
Reference in a new issue