New: Allow keeping calibre in sync with goodreads

pull/965/head
ta264 4 years ago
parent 7072b913a6
commit f584d2d8d2

@ -59,6 +59,21 @@ class DevelopmentSettings extends Component {
id="developmentSettings" id="developmentSettings"
{...otherProps} {...otherProps}
> >
<FieldSet legend="Metadata Provider Source">
<FormGroup>
<FormLabel>Metadata Source</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="metadataSource"
helpText="Alternative Metadata Source (Leave blank for default)"
helpLink="https://wiki.servarr.com/Readarr_Settings#Metadata"
onChange={onInputChange}
{...settings.metadataSource}
/>
</FormGroup>
</FieldSet>
<FieldSet legend="Logging"> <FieldSet legend="Logging">
<FormGroup> <FormGroup>
<FormLabel>Log Rotation</FormLabel> <FormLabel>Log Rotation</FormLabel>

@ -8,16 +8,14 @@ import FormLabel from 'Components/Form/FormLabel';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { inputTypes } from 'Helpers/Props'; import { inputTypes } from 'Helpers/Props';
const writeAudioTagOptions = [ const writeBookTagOptions = [
{ key: 'sync', value: 'All files; keep in sync with MusicBrainz' }, { key: 'sync', value: 'All files; keep in sync with Goodreads' },
{ key: 'allFiles', value: 'All files; initial import only' }, { key: 'allFiles', value: 'All files; initial import only' },
{ key: 'newFiles', value: 'For new downloads only' }, { key: 'newFiles', value: 'For new downloads only' }
{ key: 'no', value: 'Never' }
]; ];
function MetadataProvider(props) { function MetadataProvider(props) {
const { const {
advancedSettings,
isFetching, isFetching,
error, error,
settings, settings,
@ -41,51 +39,42 @@ function MetadataProvider(props) {
{ {
hasSettings && !isFetching && !error && hasSettings && !isFetching && !error &&
<Form> <Form>
{ <FieldSet legend="Calibre Metadata">
advancedSettings && <FormGroup>
<FieldSet legend="Metadata Provider Source"> <FormLabel>Send Metadata to Calibre</FormLabel>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>Metadata Source</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.TEXT} type={inputTypes.SELECT}
name="metadataSource" name="writeBookTags"
helpText="Alternative Metadata Source (Leave blank for default)" helpTextWarning="Selecting 'All files' will alter existing files when they are imported."
helpLink="https://wiki.servarr.com/Readarr_Settings#Metadata" helpLink="https://wiki.servarr.com/Readarr_Settings#Write_Metadata_to_Book_Files"
values={writeBookTagOptions}
onChange={onInputChange} onChange={onInputChange}
{...settings.metadataSource} {...settings.writeBookTags}
/> />
</FormGroup> </FormGroup>
</FieldSet>
}
<FieldSet legend="Write Metadata to Audio Files">
<FormGroup> <FormGroup>
<FormLabel>Tag Audio Files with Metadata</FormLabel> <FormLabel>Update Covers</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.SELECT} type={inputTypes.CHECK}
name="writeAudioTags" name="updateCovers"
helpTextWarning="Selecting 'All files' will alter existing files when they are imported." helpText="Set book covers in Calibre to match those in Readarr"
helpLink="https://wiki.servarr.com/Readarr_Settings#Write_Metadata_to_Audio_Files"
values={writeAudioTagOptions}
onChange={onInputChange} onChange={onInputChange}
{...settings.writeAudioTags} {...settings.updateCovers}
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormLabel>Scrub Existing Tags</FormLabel> <FormLabel>Embed Metadata in Book Files</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="scrubAudioTags" name="embedMetadata"
helpText="Remove existing tags from files, leaving only those added by Readarr." helpText="Tell Calibre to write metadata into the actual book file"
onChange={onInputChange} onChange={onInputChange}
{...settings.scrubAudioTags} {...settings.embedMetadata}
/> />
</FormGroup> </FormGroup>
@ -98,7 +87,6 @@ function MetadataProvider(props) {
} }
MetadataProvider.propTypes = { MetadataProvider.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
settings: PropTypes.object.isRequired, settings: PropTypes.object.isRequired,

@ -2,7 +2,7 @@ import React, { Component } from 'react';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody'; import PageContentBody from 'Components/Page/PageContentBody';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import MetadatasConnector from './Metadata/MetadatasConnector'; // import MetadatasConnector from './Metadata/MetadatasConnector';
import MetadataProviderConnector from './MetadataProvider/MetadataProviderConnector'; import MetadataProviderConnector from './MetadataProvider/MetadataProviderConnector';
class MetadataSettings extends Component { class MetadataSettings extends Component {
@ -59,7 +59,7 @@ class MetadataSettings extends Component {
onChildMounted={this.onChildMounted} onChildMounted={this.onChildMounted}
onChildStateChange={this.onChildStateChange} onChildStateChange={this.onChildStateChange}
/> />
<MetadatasConnector /> {/* <MetadatasConnector /> */}
</PageContentBody> </PageContentBody>
</PageContent> </PageContent>
); );

@ -15,14 +15,17 @@ namespace NzbDrone.Core.Books
{ {
private readonly IEditionService _editionService; private readonly IEditionService _editionService;
private readonly IAudioTagService _audioTagService; private readonly IAudioTagService _audioTagService;
private readonly IEBookTagService _eBookTagService;
private readonly Logger _logger; private readonly Logger _logger;
public RefreshEditionService(IEditionService editionService, public RefreshEditionService(IEditionService editionService,
IAudioTagService audioTagService, IAudioTagService audioTagService,
IEBookTagService eBookTagService,
Logger logger) Logger logger)
{ {
_editionService = editionService; _editionService = editionService;
_audioTagService = audioTagService; _audioTagService = audioTagService;
_eBookTagService = eBookTagService;
_logger = logger; _logger = logger;
} }
@ -52,6 +55,7 @@ namespace NzbDrone.Core.Books
} }
_audioTagService.SyncTags(tagsToUpdate); _audioTagService.SyncTags(tagsToUpdate);
_eBookTagService.SyncTags(tagsToUpdate);
return add.Any() || delete.Any() || updateList.Any() || merge.Any(); return add.Any() || delete.Any() || updateList.Any() || merge.Any();
} }

@ -290,6 +290,27 @@ namespace NzbDrone.Core.Configuration
set { SetValue("ScrubAudioTags", value); } set { SetValue("ScrubAudioTags", value); }
} }
public WriteBookTagsType WriteBookTags
{
get { return GetValueEnum("WriteBookTags", WriteBookTagsType.NewFiles); }
set { SetValue("WriteBookTags", value); }
}
public bool UpdateCovers
{
get { return GetValueBoolean("UpdateCovers", true); }
set { SetValue("UpdateCovers", value); }
}
public bool EmbedMetadata
{
get { return GetValueBoolean("EmbedMetadata", false); }
set { SetValue("EmbedMetadata", value); }
}
public int FirstDayOfWeek public int FirstDayOfWeek
{ {
get { return GetValueInt("FirstDayOfWeek", (int)CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek); } get { return GetValueInt("FirstDayOfWeek", (int)CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek); }

@ -70,6 +70,9 @@ namespace NzbDrone.Core.Configuration
string MetadataSource { get; set; } string MetadataSource { get; set; }
WriteAudioTagsType WriteAudioTags { get; set; } WriteAudioTagsType WriteAudioTags { get; set; }
bool ScrubAudioTags { get; set; } bool ScrubAudioTags { get; set; }
WriteBookTagsType WriteBookTags { get; set; }
bool UpdateCovers { get; set; }
bool EmbedMetadata { get; set; }
//Forms Auth //Forms Auth
string RijndaelPassphrase { get; } string RijndaelPassphrase { get; }

@ -0,0 +1,9 @@
namespace NzbDrone.Core.Configuration
{
public enum WriteBookTagsType
{
NewFiles,
AllFiles,
Sync
}
}

@ -14,7 +14,6 @@ using NzbDrone.Core.Extras;
using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
using NzbDrone.Core.RootFolders; using NzbDrone.Core.RootFolders;
@ -31,9 +30,9 @@ namespace NzbDrone.Core.MediaFiles.BookImport
private readonly IUpgradeMediaFiles _bookFileUpgrader; private readonly IUpgradeMediaFiles _bookFileUpgrader;
private readonly IMediaFileService _mediaFileService; private readonly IMediaFileService _mediaFileService;
private readonly IAudioTagService _audioTagService; private readonly IAudioTagService _audioTagService;
private readonly IEBookTagService _eBookTagService;
private readonly IAuthorService _authorService; private readonly IAuthorService _authorService;
private readonly IAddAuthorService _addAuthorService; private readonly IAddAuthorService _addAuthorService;
private readonly IRefreshAuthorService _refreshAuthorService;
private readonly IBookService _bookService; private readonly IBookService _bookService;
private readonly IEditionService _editionService; private readonly IEditionService _editionService;
private readonly IRootFolderService _rootFolderService; private readonly IRootFolderService _rootFolderService;
@ -47,9 +46,9 @@ namespace NzbDrone.Core.MediaFiles.BookImport
public ImportApprovedBooks(IUpgradeMediaFiles bookFileUpgrader, public ImportApprovedBooks(IUpgradeMediaFiles bookFileUpgrader,
IMediaFileService mediaFileService, IMediaFileService mediaFileService,
IAudioTagService audioTagService, IAudioTagService audioTagService,
IEBookTagService eBookTagService,
IAuthorService authorService, IAuthorService authorService,
IAddAuthorService addAuthorService, IAddAuthorService addAuthorService,
IRefreshAuthorService refreshAuthorService,
IBookService bookService, IBookService bookService,
IEditionService editionService, IEditionService editionService,
IRootFolderService rootFolderService, IRootFolderService rootFolderService,
@ -63,9 +62,9 @@ namespace NzbDrone.Core.MediaFiles.BookImport
_bookFileUpgrader = bookFileUpgrader; _bookFileUpgrader = bookFileUpgrader;
_mediaFileService = mediaFileService; _mediaFileService = mediaFileService;
_audioTagService = audioTagService; _audioTagService = audioTagService;
_eBookTagService = eBookTagService;
_authorService = authorService; _authorService = authorService;
_addAuthorService = addAuthorService; _addAuthorService = addAuthorService;
_refreshAuthorService = refreshAuthorService;
_bookService = bookService; _bookService = bookService;
_editionService = editionService; _editionService = editionService;
_rootFolderService = rootFolderService; _rootFolderService = rootFolderService;
@ -207,6 +206,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport
} }
_audioTagService.WriteTags(bookFile, false); _audioTagService.WriteTags(bookFile, false);
_eBookTagService.WriteTags(bookFile, false);
} }
filesToAdd.Add(bookFile); filesToAdd.Add(bookFile);

@ -10,6 +10,7 @@ using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
using NzbDrone.Core.Books; using NzbDrone.Core.Books;
using NzbDrone.Core.Books.Calibre; using NzbDrone.Core.Books.Calibre;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.Azw; using NzbDrone.Core.MediaFiles.Azw;
using NzbDrone.Core.MediaFiles.Commands; using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
@ -25,6 +26,8 @@ namespace NzbDrone.Core.MediaFiles
public interface IEBookTagService public interface IEBookTagService
{ {
ParsedTrackInfo ReadTags(IFileInfo file); ParsedTrackInfo ReadTags(IFileInfo file);
void WriteTags(BookFile trackfile, bool newDownload, bool force = false);
void SyncTags(List<Edition> books);
List<RetagBookFilePreview> GetRetagPreviewsByAuthor(int authorId); List<RetagBookFilePreview> GetRetagPreviewsByAuthor(int authorId);
List<RetagBookFilePreview> GetRetagPreviewsByBook(int authorId); List<RetagBookFilePreview> GetRetagPreviewsByBook(int authorId);
} }
@ -36,18 +39,21 @@ namespace NzbDrone.Core.MediaFiles
private readonly IAuthorService _authorService; private readonly IAuthorService _authorService;
private readonly IMediaFileService _mediaFileService; private readonly IMediaFileService _mediaFileService;
private readonly IRootFolderService _rootFolderService; private readonly IRootFolderService _rootFolderService;
private readonly IConfigService _configService;
private readonly ICalibreProxy _calibre; private readonly ICalibreProxy _calibre;
private readonly Logger _logger; private readonly Logger _logger;
public EBookTagService(IAuthorService authorService, public EBookTagService(IAuthorService authorService,
IMediaFileService mediaFileService, IMediaFileService mediaFileService,
IRootFolderService rootFolderService, IRootFolderService rootFolderService,
IConfigService configService,
ICalibreProxy calibre, ICalibreProxy calibre,
Logger logger) Logger logger)
{ {
_authorService = authorService; _authorService = authorService;
_mediaFileService = mediaFileService; _mediaFileService = mediaFileService;
_rootFolderService = rootFolderService; _rootFolderService = rootFolderService;
_configService = configService;
_calibre = calibre; _calibre = calibre;
_logger = logger; _logger = logger;
@ -72,6 +78,48 @@ namespace NzbDrone.Core.MediaFiles
} }
} }
public void WriteTags(BookFile bookFile, bool newDownload, bool force = false)
{
if (!force)
{
if (_configService.WriteBookTags == WriteBookTagsType.NewFiles && !newDownload)
{
return;
}
}
_logger.Debug($"Writing tags for {bookFile}");
var rootFolder = _rootFolderService.GetBestRootFolder(bookFile.Path);
_calibre.SetFields(bookFile, rootFolder.CalibreSettings, _configService.UpdateCovers, _configService.EmbedMetadata);
}
public void SyncTags(List<Edition> editions)
{
if (_configService.WriteBookTags != WriteBookTagsType.Sync)
{
return;
}
// get the tracks to update
foreach (var edition in editions)
{
var bookFiles = edition.BookFiles.Value;
_logger.Debug($"Syncing ebook tags for {edition}");
foreach (var file in bookFiles)
{
// populate tracks (which should also have release/book/author set) because
// not all of the updates will have been committed to the database yet
file.Edition = edition;
var rootFolder = _rootFolderService.GetBestRootFolder(file.Path);
_calibre.SetFields(file, rootFolder.CalibreSettings, _configService.UpdateCovers, _configService.EmbedMetadata);
}
}
}
public List<RetagBookFilePreview> GetRetagPreviewsByAuthor(int authorId) public List<RetagBookFilePreview> GetRetagPreviewsByAuthor(int authorId)
{ {
var files = _mediaFileService.GetFilesByAuthor(authorId); var files = _mediaFileService.GetFilesByAuthor(authorId);

@ -5,6 +5,7 @@ using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Books.Calibre; using NzbDrone.Core.Books.Calibre;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.BookImport; using NzbDrone.Core.MediaFiles.BookImport;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RootFolders; using NzbDrone.Core.RootFolders;
@ -18,6 +19,7 @@ namespace NzbDrone.Core.MediaFiles
public class UpgradeMediaFileService : IUpgradeMediaFiles public class UpgradeMediaFileService : IUpgradeMediaFiles
{ {
private readonly IConfigService _configService;
private readonly IRecycleBinProvider _recycleBinProvider; private readonly IRecycleBinProvider _recycleBinProvider;
private readonly IMediaFileService _mediaFileService; private readonly IMediaFileService _mediaFileService;
private readonly IAudioTagService _audioTagService; private readonly IAudioTagService _audioTagService;
@ -28,7 +30,8 @@ namespace NzbDrone.Core.MediaFiles
private readonly ICalibreProxy _calibre; private readonly ICalibreProxy _calibre;
private readonly Logger _logger; private readonly Logger _logger;
public UpgradeMediaFileService(IRecycleBinProvider recycleBinProvider, public UpgradeMediaFileService(IConfigService configService,
IRecycleBinProvider recycleBinProvider,
IMediaFileService mediaFileService, IMediaFileService mediaFileService,
IAudioTagService audioTagService, IAudioTagService audioTagService,
IMoveBookFiles bookFileMover, IMoveBookFiles bookFileMover,
@ -38,6 +41,7 @@ namespace NzbDrone.Core.MediaFiles
ICalibreProxy calibre, ICalibreProxy calibre,
Logger logger) Logger logger)
{ {
_configService = configService;
_recycleBinProvider = recycleBinProvider; _recycleBinProvider = recycleBinProvider;
_mediaFileService = mediaFileService; _mediaFileService = mediaFileService;
_audioTagService = audioTagService; _audioTagService = audioTagService;
@ -136,15 +140,15 @@ namespace NzbDrone.Core.MediaFiles
_calibre.AddFormat(file, settings); _calibre.AddFormat(file, settings);
} }
_calibre.SetFields(file, settings); _rootFolderWatchingService.ReportFileSystemChangeBeginning(file.Path);
_calibre.SetFields(file, settings, true, _configService.EmbedMetadata);
var updated = _calibre.GetBook(file.CalibreId, settings); var updated = _calibre.GetBook(file.CalibreId, settings);
var path = updated.Formats.Values.OrderByDescending(x => x.LastModified).First().Path; var path = updated.Formats.Values.OrderByDescending(x => x.LastModified).First().Path;
file.Path = path; file.Path = path;
_rootFolderWatchingService.ReportFileSystemChangeBeginning(file.Path);
if (settings.OutputFormat.IsNotNullOrWhiteSpace()) if (settings.OutputFormat.IsNotNullOrWhiteSpace())
{ {
_logger.Trace($"Getting book data for {file.CalibreId}"); _logger.Trace($"Getting book data for {file.CalibreId}");

@ -1,7 +1,10 @@
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using FluentValidation;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Validation;
using NzbDrone.Http.REST.Attributes; using NzbDrone.Http.REST.Attributes;
using Readarr.Http; using Readarr.Http;
using Readarr.Http.REST; using Readarr.Http.REST;
@ -19,6 +22,8 @@ namespace Prowlarr.Api.V1.Config
{ {
_configFileProvider = configFileProvider; _configFileProvider = configFileProvider;
_configService = configService; _configService = configService;
SharedValidator.RuleFor(c => c.MetadataSource).IsValidUrl().When(c => !c.MetadataSource.IsNullOrWhiteSpace());
} }
public override DevelopmentConfigResource GetResourceById(int id) public override DevelopmentConfigResource GetResourceById(int id)

@ -5,6 +5,7 @@ namespace Prowlarr.Api.V1.Config
{ {
public class DevelopmentConfigResource : RestResource public class DevelopmentConfigResource : RestResource
{ {
public string MetadataSource { get; set; }
public string ConsoleLogLevel { get; set; } public string ConsoleLogLevel { get; set; }
public bool LogSql { get; set; } public bool LogSql { get; set; }
public int LogRotate { get; set; } public int LogRotate { get; set; }
@ -17,6 +18,7 @@ namespace Prowlarr.Api.V1.Config
{ {
return new DevelopmentConfigResource return new DevelopmentConfigResource
{ {
MetadataSource = configService.MetadataSource,
ConsoleLogLevel = model.ConsoleLogLevel, ConsoleLogLevel = model.ConsoleLogLevel,
LogSql = model.LogSql, LogSql = model.LogSql,
LogRotate = model.LogRotate, LogRotate = model.LogRotate,

@ -1,7 +1,4 @@
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Validation;
using Readarr.Http; using Readarr.Http;
namespace Readarr.Api.V1.Config namespace Readarr.Api.V1.Config
@ -12,7 +9,6 @@ namespace Readarr.Api.V1.Config
public MetadataProviderConfigController(IConfigService configService) public MetadataProviderConfigController(IConfigService configService)
: base(configService) : base(configService)
{ {
SharedValidator.RuleFor(c => c.MetadataSource).IsValidUrl().When(c => !c.MetadataSource.IsNullOrWhiteSpace());
} }
protected override MetadataProviderConfigResource ToResource(IConfigService model) protected override MetadataProviderConfigResource ToResource(IConfigService model)

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

Loading…
Cancel
Save