From 41f5f0f2d47e94d3b90d68a145e9ac5a09677a41 Mon Sep 17 00:00:00 2001 From: ta264 Date: Wed, 31 Mar 2021 21:20:32 +0100 Subject: [PATCH] New: Search for new editions from goodreads when identifying --- .../SelectAuthorModalContentConnector.js | 2 +- .../Book/SelectBookModalContentConnector.js | 2 +- .../InteractiveImport/Book/SelectBookRow.js | 16 +----- .../SelectEditionModalContentConnector.js | 5 +- .../Edition/SelectEditionRow.js | 11 ++-- .../InteractiveImportModalContent.js | 10 ++-- .../InteractiveImportModalContentConnector.js | 4 +- .../Identification/CandidateServiceFixture.cs | 2 +- src/NzbDrone.Core/Books/Model/Book.cs | 2 +- .../Identification/CandidateService.cs | 52 ++++++++++++++----- .../Identification/IdentificationService.cs | 28 ++++++++-- .../BookImport/Manual/ManualImportFile.cs | 2 +- .../BookImport/Manual/ManualImportService.cs | 27 +++++++--- .../DownloadedBooksImportService.cs | 6 +-- .../Goodreads/GoodreadsProxy.cs | 49 +++++++++-------- .../ApiTests/WantedFixture.cs | 8 +-- .../IntegrationTestBase.cs | 12 ++--- .../ManualImport/ManualImportController.cs | 2 +- .../ManualImport/ManualImportResource.cs | 4 +- 19 files changed, 149 insertions(+), 95 deletions(-) diff --git a/frontend/src/InteractiveImport/Author/SelectAuthorModalContentConnector.js b/frontend/src/InteractiveImport/Author/SelectAuthorModalContentConnector.js index 80513856c..ab17b59e9 100644 --- a/frontend/src/InteractiveImport/Author/SelectAuthorModalContentConnector.js +++ b/frontend/src/InteractiveImport/Author/SelectAuthorModalContentConnector.js @@ -48,7 +48,7 @@ class SelectAuthorModalContentConnector extends Component { id, author, book: undefined, - bookReleaseId: undefined, + foreignEditionId: undefined, rejections: [] }); }); diff --git a/frontend/src/InteractiveImport/Book/SelectBookModalContentConnector.js b/frontend/src/InteractiveImport/Book/SelectBookModalContentConnector.js index de3eed5ec..2d94eab95 100644 --- a/frontend/src/InteractiveImport/Book/SelectBookModalContentConnector.js +++ b/frontend/src/InteractiveImport/Book/SelectBookModalContentConnector.js @@ -64,7 +64,7 @@ class SelectBookModalContentConnector extends Component { this.props.updateInteractiveImportItem({ id, book, - editionId: undefined, + foreignEditionId: undefined, rejections: [] }); }); diff --git a/frontend/src/InteractiveImport/Book/SelectBookRow.js b/frontend/src/InteractiveImport/Book/SelectBookRow.js index 32543762f..5a3dd76c6 100644 --- a/frontend/src/InteractiveImport/Book/SelectBookRow.js +++ b/frontend/src/InteractiveImport/Book/SelectBookRow.js @@ -34,8 +34,6 @@ class SelectBookRow extends Component { render() { const { title, - disambiguation, - bookType, releaseDate, statistics, monitored, @@ -48,8 +46,6 @@ class SelectBookRow extends Component { totalBookCount } = statistics; - const extendedTitle = disambiguation ? `${title} (${disambiguation})` : title; - return ( - {extendedTitle} - - ); - } - - if (name === 'bookType') { - return ( - - {bookType} + {title} ); } @@ -121,8 +109,6 @@ class SelectBookRow extends Component { SelectBookRow.propTypes = { id: PropTypes.number.isRequired, title: PropTypes.string.isRequired, - disambiguation: PropTypes.string.isRequired, - bookType: PropTypes.string.isRequired, releaseDate: PropTypes.string.isRequired, onBookSelect: PropTypes.func.isRequired, statistics: PropTypes.object.isRequired, diff --git a/frontend/src/InteractiveImport/Edition/SelectEditionModalContentConnector.js b/frontend/src/InteractiveImport/Edition/SelectEditionModalContentConnector.js index 7d4eaf8c4..564896141 100644 --- a/frontend/src/InteractiveImport/Edition/SelectEditionModalContentConnector.js +++ b/frontend/src/InteractiveImport/Edition/SelectEditionModalContentConnector.js @@ -20,13 +20,14 @@ class SelectEditionModalContentConnector extends Component { // // Listeners - onEditionSelect = (bookId, editionId) => { + onEditionSelect = (bookId, foreignEditionId) => { + console.log(`book: ${bookId} id: ${foreignEditionId} ${typeof foreignEditionId}`); const ids = this.props.importIdsByBook[bookId]; ids.forEach((id) => { this.props.updateInteractiveImportItem({ id, - editionId, + foreignEditionId, disableReleaseSwitching: true, tracks: [], rejections: [] diff --git a/frontend/src/InteractiveImport/Edition/SelectEditionRow.js b/frontend/src/InteractiveImport/Edition/SelectEditionRow.js index 92814976c..821040f9a 100644 --- a/frontend/src/InteractiveImport/Edition/SelectEditionRow.js +++ b/frontend/src/InteractiveImport/Edition/SelectEditionRow.js @@ -13,7 +13,7 @@ class SelectEditionRow extends Component { // Listeners onInputChange = ({ name, value }) => { - this.props.onEditionSelect(parseInt(name), parseInt(value)); + this.props.onEditionSelect(parseInt(name), value); } // @@ -49,6 +49,9 @@ class SelectEditionRow extends Component { if (bookEdition.isbn13) { extras.push(bookEdition.isbn13); } + if (bookEdition.asin) { + extras.push(bookEdition.asin); + } if (bookEdition.format) { extras.push(bookEdition.format); } @@ -61,7 +64,7 @@ class SelectEditionRow extends Component { } return { - key: bookEdition.id, + key: bookEdition.foreignEditionId, value }; }); @@ -114,9 +117,9 @@ class SelectEditionRow extends Component { SelectEditionRow.propTypes = { id: PropTypes.number.isRequired, - matchedEditionId: PropTypes.number.isRequired, + matchedEditionId: PropTypes.string.isRequired, title: PropTypes.string.isRequired, - disambiguation: PropTypes.string.isRequired, + disambiguation: PropTypes.string, editions: PropTypes.arrayOf(PropTypes.object).isRequired, onEditionSelect: PropTypes.func.isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js index 6210df54e..0c95aad4f 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js @@ -115,9 +115,9 @@ class InteractiveImportModalContent extends Component { const selectedItems = _.filter(this.props.items, (x) => _.includes(selectedIds, x.id)); const inconsistent = _(selectedItems) - .map((x) => ({ bookId: x.book ? x.book.id : 0, releaseId: x.EditionId })) + .map((x) => ({ bookId: x.book ? x.book.id : 0, foreignEditionId: x.ForeignEditionId })) .groupBy('bookId') - .mapValues((book) => _(book).groupBy((x) => x.releaseId).values().value().length) + .mapValues((book) => _(book).groupBy((x) => x.foreignEditionId).values().value().length) .values() .some((x) => x !== undefined && x > 1); @@ -271,6 +271,8 @@ class InteractiveImportModalContent extends Component { const selectedIds = this.getSelectedIds(); const selectedItem = selectedIds.length ? _.find(items, { id: selectedIds[0] }) : null; + const importIdsByBook = _.chain(items).filter((x) => x.book).groupBy((x) => x.book.id).mapValues((x) => x.map((y) => y.id)).value(); + const editions = _.chain(items).filter((x) => x.book).keyBy((x) => x.book.id).mapValues((x) => ({ matchedEditionId: x.foreignEditionId, book: x.book })).values().value(); const errorMessage = getErrorMessage(error, 'Unable to load manual import items'); const bulkSelectOptions = [ @@ -475,8 +477,8 @@ class InteractiveImportModalContent extends Component { x.book).groupBy((x) => x.book.id).mapValues((x) => x.map((y) => y.id)).value()} - books={_.chain(items).filter((x) => x.book).keyBy((x) => x.book.id).mapValues((x) => ({ matchedEditionId: x.editionId, book: x.book })).values().value()} + importIdsByBook={importIdsByBook} + books={editions} onModalClose={this.onSelectModalClose} /> diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js index 776a15107..640c33943 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js @@ -127,7 +127,7 @@ class InteractiveImportModalContentConnector extends Component { const { author, book, - editionId, + foreignEditionId, quality, disableReleaseSwitching } = item; @@ -151,7 +151,7 @@ class InteractiveImportModalContentConnector extends Component { path: item.path, authorId: author.id, bookId: book.id, - editionId, + foreignEditionId, quality, downloadId: this.props.downloadId, disableReleaseSwitching diff --git a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Identification/CandidateServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Identification/CandidateServiceFixture.cs index a64601c6c..e8b24cbea 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Identification/CandidateServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Identification/CandidateServiceFixture.cs @@ -35,7 +35,7 @@ public void should_not_throw_on_goodreads_exception() } }; - Subject.GetRemoteCandidates(edition).Should().BeEmpty(); + Subject.GetRemoteCandidates(edition, null).Should().BeEmpty(); } } } diff --git a/src/NzbDrone.Core/Books/Model/Book.cs b/src/NzbDrone.Core/Books/Model/Book.cs index 960ace724..2208978a6 100644 --- a/src/NzbDrone.Core/Books/Model/Book.cs +++ b/src/NzbDrone.Core/Books/Model/Book.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; using Equ; -using Newtonsoft.Json; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles; diff --git a/src/NzbDrone.Core/MediaFiles/BookImport/Identification/CandidateService.cs b/src/NzbDrone.Core/MediaFiles/BookImport/Identification/CandidateService.cs index 99a980d7d..f5d3248db 100644 --- a/src/NzbDrone.Core/MediaFiles/BookImport/Identification/CandidateService.cs +++ b/src/NzbDrone.Core/MediaFiles/BookImport/Identification/CandidateService.cs @@ -13,7 +13,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Identification public interface ICandidateService { List GetDbCandidatesFromTags(LocalEdition localEdition, IdentificationOverrides idOverrides, bool includeExisting); - IEnumerable GetRemoteCandidates(LocalEdition localEdition); + IEnumerable GetRemoteCandidates(LocalEdition localEdition, IdentificationOverrides idOverrides); } public class CandidateService : ICandidateService @@ -184,8 +184,10 @@ private List GetDbCandidates(LocalEdition localEdition, bool i return candidateReleases; } - public IEnumerable GetRemoteCandidates(LocalEdition localEdition) + public IEnumerable GetRemoteCandidates(LocalEdition localEdition, IdentificationOverrides idOverrides) { + // TODO handle edition override + // Gets candidate book releases from the metadata server. // Will eventually need adding locally if we find a match List remoteBooks; @@ -210,7 +212,7 @@ public IEnumerable GetRemoteCandidates(LocalEdition localEditi remoteBooks = new List(); } - foreach (var candidate in ToCandidates(remoteBooks, seenCandidates)) + foreach (var candidate in ToCandidates(remoteBooks, seenCandidates, idOverrides)) { yield return candidate; } @@ -232,7 +234,7 @@ public IEnumerable GetRemoteCandidates(LocalEdition localEditi remoteBooks = new List(); } - foreach (var candidate in ToCandidates(remoteBooks, seenCandidates)) + foreach (var candidate in ToCandidates(remoteBooks, seenCandidates, idOverrides)) { yield return candidate; } @@ -255,15 +257,18 @@ public IEnumerable GetRemoteCandidates(LocalEdition localEditi remoteBooks = new List(); } - foreach (var candidate in ToCandidates(remoteBooks, seenCandidates)) + foreach (var candidate in ToCandidates(remoteBooks, seenCandidates, idOverrides)) { yield return candidate; } } } - // If we got an id result, stop - if (seenCandidates.Any()) + // If we got an id result, or any overrides are set, stop + if (seenCandidates.Any() || + idOverrides?.Edition != null || + idOverrides?.Book != null || + idOverrides?.Author != null) { yield break; } @@ -301,7 +306,7 @@ public IEnumerable GetRemoteCandidates(LocalEdition localEditi remoteBooks = new List(); } - foreach (var candidate in ToCandidates(remoteBooks, seenCandidates)) + foreach (var candidate in ToCandidates(remoteBooks, seenCandidates, idOverrides)) { yield return candidate; } @@ -324,7 +329,7 @@ public IEnumerable GetRemoteCandidates(LocalEdition localEditi remoteBooks = new List(); } - foreach (var candidate in ToCandidates(remoteBooks, seenCandidates)) + foreach (var candidate in ToCandidates(remoteBooks, seenCandidates, idOverrides)) { yield return candidate; } @@ -342,14 +347,14 @@ public IEnumerable GetRemoteCandidates(LocalEdition localEditi remoteBooks = new List(); } - foreach (var candidate in ToCandidates(remoteBooks, seenCandidates)) + foreach (var candidate in ToCandidates(remoteBooks, seenCandidates, idOverrides)) { yield return candidate; } } } - private List ToCandidates(IEnumerable books, HashSet seenCandidates) + private List ToCandidates(IEnumerable books, HashSet seenCandidates, IdentificationOverrides idOverrides) { var candidates = new List(); @@ -359,10 +364,11 @@ private List ToCandidates(IEnumerable books, HashSet ToCandidates(IEnumerable books, HashSet ToLocalTrack(IEnumerable trackfiles, LocalEdit private void IdentifyRelease(LocalEdition localBookRelease, IdentificationOverrides idOverrides, ImportDecisionMakerConfig config) { var watch = System.Diagnostics.Stopwatch.StartNew(); + bool usedRemote = false; IEnumerable candidateReleases = _candidateService.GetDbCandidatesFromTags(localBookRelease, idOverrides, config.IncludeExisting); @@ -126,14 +127,20 @@ private void IdentifyRelease(LocalEdition localBookRelease, IdentificationOverri _logger.Debug($"Retrieved {allLocalTracks.Count} possible tracks in {watch.ElapsedMilliseconds}ms"); - if (!candidateReleases.Any() && config.AddNewAuthors) + if (!candidateReleases.Any()) { - candidateReleases = _candidateService.GetRemoteCandidates(localBookRelease); + candidateReleases = _candidateService.GetRemoteCandidates(localBookRelease, idOverrides); + if (!config.AddNewAuthors) + { + candidateReleases = candidateReleases.Where(x => x.Edition.Book.Value.Id > 0); + } + + usedRemote = true; } if (!candidateReleases.Any()) { - // can't find any candidates even after fingerprinting + // can't find any candidates even after using remote search // populate the overrides and return foreach (var localTrack in localBookRelease.LocalBooks) { @@ -147,6 +154,21 @@ private void IdentifyRelease(LocalEdition localBookRelease, IdentificationOverri GetBestRelease(localBookRelease, candidateReleases, allLocalTracks); + // If the result isn't great and we haven't tried remote candidates, try looking for remote candidates + // Goodreads may have a better edition of a local book + if (localBookRelease.Distance.NormalizedDistance() > 0.15 && !usedRemote) + { + _logger.Debug("Match not good enough, trying remote candidates"); + candidateReleases = _candidateService.GetRemoteCandidates(localBookRelease, idOverrides); + + if (!config.AddNewAuthors) + { + candidateReleases = candidateReleases.Where(x => x.Edition.Book.Value.Id > 0); + } + + GetBestRelease(localBookRelease, candidateReleases, allLocalTracks); + } + _logger.Debug($"Best release found in {watch.ElapsedMilliseconds}ms"); localBookRelease.PopulateMatch(); diff --git a/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportFile.cs b/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportFile.cs index 0709b2d8e..777f3b3f9 100644 --- a/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportFile.cs +++ b/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportFile.cs @@ -9,7 +9,7 @@ public class ManualImportFile : IEquatable public string Path { get; set; } public int AuthorId { get; set; } public int BookId { get; set; } - public int EditionId { get; set; } + public string ForeignEditionId { get; set; } public QualityModel Quality { get; set; } public string DownloadId { get; set; } public bool DisableReleaseSwitching { get; set; } diff --git a/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportService.cs index bc2d15aae..3807dbd7f 100644 --- a/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.IO; using System.IO.Abstractions; using System.Linq; @@ -15,6 +16,7 @@ using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RootFolders; @@ -37,6 +39,7 @@ public class ManualImportService : IExecute, IManualImportS private readonly IAuthorService _authorService; private readonly IBookService _bookService; private readonly IEditionService _editionService; + private readonly IProvideBookInfo _bookInfo; private readonly IAudioTagService _audioTagService; private readonly IImportApprovedBooks _importApprovedBooks; private readonly ITrackedDownloadService _trackedDownloadService; @@ -53,6 +56,7 @@ public ManualImportService(IDiskProvider diskProvider, IAuthorService authorService, IBookService bookService, IEditionService editionService, + IProvideBookInfo bookInfo, IAudioTagService audioTagService, IImportApprovedBooks importApprovedBooks, ITrackedDownloadService trackedDownloadService, @@ -69,6 +73,7 @@ public ManualImportService(IDiskProvider diskProvider, _authorService = authorService; _bookService = bookService; _editionService = editionService; + _bookInfo = bookInfo; _audioTagService = audioTagService; _importApprovedBooks = importApprovedBooks; _trackedDownloadService = trackedDownloadService; @@ -299,7 +304,14 @@ public void Execute(ManualImportCommand message) var author = _authorService.GetAuthor(file.AuthorId); var book = _bookService.GetBook(file.BookId); - var edition = _editionService.GetEdition(file.EditionId); + + var edition = _editionService.GetEditionByForeignEditionId(file.ForeignEditionId); + if (edition == null) + { + var tuple = _bookInfo.GetBookInfo(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); @@ -355,20 +367,23 @@ public void Execute(ManualImportCommand message) foreach (var groupedTrackedDownload in importedTrackedDownload.GroupBy(i => i.TrackedDownload.DownloadItem.DownloadId).ToList()) { var trackedDownload = groupedTrackedDownload.First().TrackedDownload; - var outputPath = trackedDownload.ImportItem.OutputPath.FullPath; if (_diskProvider.FolderExists(outputPath)) { - if (_downloadedTracksImportService.ShouldDeleteFolder( - _diskProvider.GetDirectoryInfo(outputPath), - trackedDownload.RemoteBook.Author) && trackedDownload.DownloadItem.CanMoveFiles) + if (_downloadedTracksImportService.ShouldDeleteFolder(_diskProvider.GetDirectoryInfo(outputPath)) && + trackedDownload.DownloadItem.CanMoveFiles) { _diskProvider.DeleteFolder(outputPath, true); } } - if (groupedTrackedDownload.Select(c => c.ImportResult).Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteBook.Books.Count)) + var importedCount = groupedTrackedDownload.Select(c => c.ImportResult) + .Count(c => c.Result == ImportResultType.Imported); + var downloadItemCount = Math.Max(1, trackedDownload.RemoteBook?.Books.Count ?? 1); + var allItemsImported = importedCount >= downloadItemCount; + + if (allItemsImported) { trackedDownload.State = TrackedDownloadState.Imported; _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload)); diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedBooksImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedBooksImportService.cs index 4ebca821a..fb8b4e1e2 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedBooksImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedBooksImportService.cs @@ -21,7 +21,7 @@ public interface IDownloadedBooksImportService { List ProcessRootFolder(IDirectoryInfo directoryInfo); List ProcessPath(string path, ImportMode importMode = ImportMode.Auto, Author author = null, DownloadClientItem downloadClientItem = null); - bool ShouldDeleteFolder(IDirectoryInfo directoryInfo, Author author); + bool ShouldDeleteFolder(IDirectoryInfo directoryInfo); } public class DownloadedBooksImportService : IDownloadedBooksImportService @@ -110,7 +110,7 @@ public List ProcessPath(string path, ImportMode importMode = Impor return new List(); } - public bool ShouldDeleteFolder(IDirectoryInfo directoryInfo, Author author) + public bool ShouldDeleteFolder(IDirectoryInfo directoryInfo) { try { @@ -238,7 +238,7 @@ private List ProcessFolder(IDirectoryInfo directoryInfo, ImportMod if (importMode == ImportMode.Move && importResults.Any(i => i.Result == ImportResultType.Imported) && - ShouldDeleteFolder(directoryInfo, author)) + ShouldDeleteFolder(directoryInfo)) { _logger.Debug("Deleting folder after importing valid files"); _diskProvider.DeleteFolder(directoryInfo.FullName, true); diff --git a/src/NzbDrone.Core/MetadataSource/Goodreads/GoodreadsProxy.cs b/src/NzbDrone.Core/MetadataSource/Goodreads/GoodreadsProxy.cs index 92c6e65ad..78a360e0d 100644 --- a/src/NzbDrone.Core/MetadataSource/Goodreads/GoodreadsProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/Goodreads/GoodreadsProxy.cs @@ -421,30 +421,12 @@ public List SearchForNewBook(string title, string author) public List SearchByIsbn(string isbn) { - var result = SearchByField("isbn", isbn); - - // we don't get isbn back in search result, but if only one result assume the query was correct - // and add in the searched isbn - if (result.Count == 1 && result[0].Editions.Value.Count == 1) - { - result[0].Editions.Value[0].Isbn13 = isbn; - } - - return result; + return SearchByField("isbn", isbn, e => e.Isbn13 = isbn); } public List SearchByAsin(string asin) { - var result = SearchByField("asin", asin); - - // we don't get isbn back in search result, but if only one result assume the query was correct - // and add in the searched isbn - if (result.Count == 1 && result[0].Editions.Value.Count == 1) - { - result[0].Editions.Value[0].Asin = asin; - } - - return result; + return SearchByField("asin", asin, e => e.Asin = asin); } public List SearchByGoodreadsId(int id) @@ -492,7 +474,7 @@ public List SearchByGoodreadsId(int id) } } - public List SearchByField(string field, string query) + public List SearchByField(string field, string query, Action applyData = null) { try { @@ -500,9 +482,9 @@ public List SearchByField(string field, string query) .AddQueryParam("q", query) .Build(); - var result = _cachedHttpClient.Get>(httpRequest, true, TimeSpan.FromDays(5)); + var response = _cachedHttpClient.Get>(httpRequest, true, TimeSpan.FromDays(5)); - return result.Resource.SelectList(MapJsonSearchResult); + return response.Resource.SelectList(x => MapJsonSearchResult(x, response.Resource.Count == 1 ? applyData : null)); } catch (HttpException) { @@ -735,7 +717,7 @@ private Book MapSearchResult(WorkResource resource) return book; } - private Book MapJsonSearchResult(SearchJsonResource resource) + private Book MapJsonSearchResult(SearchJsonResource resource, Action applyData = null) { var book = _bookService.FindById(resource.WorkId.ToString()); var edition = _editionService.GetEditionByForeignEditionId(resource.BookId.ToString()); @@ -751,6 +733,11 @@ private Book MapJsonSearchResult(SearchJsonResource resource) PageCount = resource.PageCount, Overview = resource.Description?.Html ?? string.Empty }; + + if (applyData != null) + { + applyData(edition); + } } edition.Monitored = true; @@ -777,7 +764,19 @@ private Book MapJsonSearchResult(SearchJsonResource resource) }; } - book.Editions = new List { edition }; + if (book.Editions != null) + { + if (book.Editions.Value.Any()) + { + edition.Monitored = false; + } + + book.Editions.Value.Add(edition); + } + else + { + book.Editions = new List { edition }; + } var authorId = resource.Author.Id.ToString(); var author = _authorService.FindById(authorId); diff --git a/src/NzbDrone.Integration.Test/ApiTests/WantedFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/WantedFixture.cs index 4da24fc6a..5b57a5397 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/WantedFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/WantedFixture.cs @@ -64,7 +64,7 @@ public void cutoff_should_have_monitored_items() { EnsureProfileCutoff(1, Quality.AZW3); var author = EnsureAuthor("14586394", "43765115", "Andrew Hunter Murray", true); - EnsureBookFile(author, 1, 1, Quality.MOBI); + EnsureBookFile(author, 1, "43765115", Quality.MOBI); var result = WantedCutoffUnmet.GetPaged(0, 15, "releaseDate", "desc"); @@ -88,7 +88,7 @@ public void cutoff_should_not_have_unmonitored_items() { EnsureProfileCutoff(1, Quality.AZW3); var author = EnsureAuthor("14586394", "43765115", "Andrew Hunter Murray", false); - EnsureBookFile(author, 1, 1, Quality.MOBI); + EnsureBookFile(author, 1, "43765115", Quality.MOBI); var result = WantedCutoffUnmet.GetPaged(0, 15, "releaseDate", "desc"); @@ -101,7 +101,7 @@ public void cutoff_should_have_author() { EnsureProfileCutoff(1, Quality.AZW3); var author = EnsureAuthor("14586394", "43765115", "Andrew Hunter Murray", true); - EnsureBookFile(author, 1, 1, Quality.MOBI); + EnsureBookFile(author, 1, "43765115", Quality.MOBI); var result = WantedCutoffUnmet.GetPaged(0, 15, "releaseDate", "desc"); @@ -126,7 +126,7 @@ public void cutoff_should_have_unmonitored_items() { EnsureProfileCutoff(1, Quality.AZW3); var author = EnsureAuthor("14586394", "43765115", "Andrew Hunter Murray", false); - EnsureBookFile(author, 1, 1, Quality.MOBI); + EnsureBookFile(author, 1, "43765115", Quality.MOBI); var result = WantedCutoffUnmet.GetPaged(0, 15, "releaseDate", "desc", "monitored", "false"); diff --git a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs index 66a701312..e1275566a 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs @@ -234,13 +234,13 @@ public static void WaitForCompletion(Func predicate, int timeout = 10000, Assert.Fail("Timed on wait"); } - public AuthorResource EnsureAuthor(string authorId, string goodreadsBookId, string authorName, bool? monitored = null) + public AuthorResource EnsureAuthor(string authorId, string goodreadsEditionId, string authorName, bool? monitored = null) { var result = Author.All().FirstOrDefault(v => v.ForeignAuthorId == authorId); if (result == null) { - var lookup = Author.Lookup("readarr:" + goodreadsBookId); + var lookup = Author.Lookup("readarr:" + goodreadsEditionId); var author = lookup.First(); author.QualityProfileId = 1; author.MetadataProfileId = 1; @@ -291,9 +291,9 @@ public void EnsureNoAuthor(string readarrId, string authorTitle) } } - public void EnsureBookFile(AuthorResource author, int bookId, int editionId, Quality quality) + public void EnsureBookFile(AuthorResource author, int bookId, string foreignEditionId, Quality quality) { - var result = Books.GetBooksInAuthor(author.Id).Single(v => v.Id == editionId); + var result = Books.GetBooksInAuthor(author.Id).Single(v => v.Id == bookId); // if (result.BookFile == null) if (true) @@ -312,14 +312,14 @@ public void EnsureBookFile(AuthorResource author, int bookId, int editionId, Qua Path = path, AuthorId = author.Id, BookId = bookId, - EditionId = editionId, + ForeignEditionId = foreignEditionId, Quality = new QualityModel(quality) } } }); Commands.WaitAll(); - var track = Books.GetBooksInAuthor(author.Id).Single(x => x.Id == editionId); + var track = Books.GetBooksInAuthor(author.Id).Single(x => x.Id == bookId); // track.BookFileId.Should().NotBe(0); } diff --git a/src/Readarr.Api.V1/ManualImport/ManualImportController.cs b/src/Readarr.Api.V1/ManualImport/ManualImportController.cs index 39b41f678..053beb0c8 100644 --- a/src/Readarr.Api.V1/ManualImport/ManualImportController.cs +++ b/src/Readarr.Api.V1/ManualImport/ManualImportController.cs @@ -78,7 +78,7 @@ private List UpdateImportItems(List Size = resource.Size, Author = resource.Author == null ? null : _authorService.GetAuthor(resource.Author.Id), Book = resource.Book == null ? null : _bookService.GetBook(resource.Book.Id), - Edition = resource.EditionId == 0 ? null : _editionService.GetEdition(resource.EditionId), + Edition = resource.ForeignEditionId == null ? null : _editionService.GetEditionByForeignEditionId(resource.ForeignEditionId), Quality = resource.Quality, DownloadId = resource.DownloadId, AdditionalFile = resource.AdditionalFile, diff --git a/src/Readarr.Api.V1/ManualImport/ManualImportResource.cs b/src/Readarr.Api.V1/ManualImport/ManualImportResource.cs index ccaea6eb0..baa945623 100644 --- a/src/Readarr.Api.V1/ManualImport/ManualImportResource.cs +++ b/src/Readarr.Api.V1/ManualImport/ManualImportResource.cs @@ -17,7 +17,7 @@ public class ManualImportResource : RestResource public long Size { get; set; } public AuthorResource Author { get; set; } public BookResource Book { get; set; } - public int EditionId { get; set; } + public string ForeignEditionId { get; set; } public QualityModel Quality { get; set; } public int QualityWeight { get; set; } public string DownloadId { get; set; } @@ -45,7 +45,7 @@ public static ManualImportResource ToResource(this ManualImportItem model) Size = model.Size, Author = model.Author.ToResource(), Book = model.Book.ToResource(), - EditionId = model.Edition?.Id ?? 0, + ForeignEditionId = model.Edition?.ForeignEditionId, Quality = model.Quality, //QualityWeight