Fixed: Goodreads import lists

pull/43/head
ta264 5 years ago
parent 723beb9ca3
commit 5f2d57f33b

@ -16,7 +16,7 @@ import styles from './PlaylistInput.css';
const columns = [
{
name: 'name',
label: 'Playlist',
label: 'Bookshelf',
isSortable: false,
isVisible: true
}
@ -125,7 +125,7 @@ class PlaylistInput extends Component {
{
isPopulated && !isFetching && user && !!items.length &&
<div className={className}>
Select playlists to import from Goodreads user {user}.
Select bookshelves to import from Goodreads user {user}.
<Table
columns={columns}
selectAll={true}

@ -31,6 +31,7 @@ namespace NzbDrone.Common.OAuth
/// <seealso cref="http://oauth.net/core/1.0#request_urls"/>
public virtual string RequestUrl { get; set; }
public virtual Dictionary<string, string> Parameters { get; set; }
#if !WINRT
public string GetAuthorizationHeader(NameValueCollection parameters)

@ -43,7 +43,12 @@ namespace NzbDrone.Core.Test.ImportListTests
.Returns<int>(x => Builder<Book>
.CreateListOfSize(1)
.TheFirst(1)
.With(b => b.ForeignBookId = x.ToString())
.With(b => b.Editions = Builder<Edition>
.CreateListOfSize(1)
.TheFirst(1)
.With(e => e.ForeignEditionId = x.ToString())
.With(e => e.Monitored = true)
.BuildList())
.BuildList());
Mocker.GetMock<IImportListFactory>()
@ -74,26 +79,26 @@ namespace NzbDrone.Core.Test.ImportListTests
private void WithAuthorId()
{
_importListReports.First().ArtistMusicBrainzId = "f59c5520-5f46-4d2c-b2c4-822eabf53419";
_importListReports.First().AuthorGoodreadsId = "f59c5520-5f46-4d2c-b2c4-822eabf53419";
}
private void WithBookId()
{
_importListReports.First().AlbumMusicBrainzId = "101";
_importListReports.First().EditionGoodreadsId = "101";
}
private void WithExistingArtist()
{
Mocker.GetMock<IAuthorService>()
.Setup(v => v.FindById(_importListReports.First().ArtistMusicBrainzId))
.Returns(new Author { ForeignAuthorId = _importListReports.First().ArtistMusicBrainzId });
.Setup(v => v.FindById(_importListReports.First().AuthorGoodreadsId))
.Returns(new Author { ForeignAuthorId = _importListReports.First().AuthorGoodreadsId });
}
private void WithExistingAlbum()
{
Mocker.GetMock<IBookService>()
.Setup(v => v.FindById(_importListReports.First().AlbumMusicBrainzId))
.Returns(new Book { ForeignBookId = _importListReports.First().AlbumMusicBrainzId });
.Setup(v => v.FindById(_importListReports.First().EditionGoodreadsId))
.Returns(new Book { ForeignBookId = _importListReports.First().EditionGoodreadsId });
}
private void WithExcludedArtist()

@ -23,10 +23,6 @@ namespace NzbDrone.Core.Test.MusicTests
[SetUp]
public void Setup()
{
_fakeAlbum = Builder<Book>
.CreateNew()
.Build();
_fakeArtist = Builder<Author>
.CreateNew()
.With(s => s.Path = null)
@ -36,6 +32,16 @@ namespace NzbDrone.Core.Test.MusicTests
private void GivenValidAlbum(string readarrId)
{
_fakeAlbum = Builder<Book>
.CreateNew()
.With(x => x.Editions = Builder<Edition>
.CreateListOfSize(1)
.TheFirst(1)
.With(e => e.ForeignEditionId = readarrId)
.With(e => e.Monitored = true)
.BuildList())
.Build();
Mocker.GetMock<IProvideBookInfo>()
.Setup(s => s.GetBookInfo(readarrId))
.Returns(Tuple.Create(_fakeArtist.Metadata.Value.ForeignAuthorId,

@ -62,6 +62,7 @@ namespace NzbDrone.Core.Books
// Note it's a manual addition so it's not deleted on next refresh
book.AddOptions.AddType = BookAddType.Manual;
book.Editions.Value.Single(x => x.Monitored).ManualAdd = true;
// Add the author if necessary
var dbAuthor = _authorService.FindById(book.AuthorMetadata.Value.ForeignAuthorId);
@ -105,10 +106,11 @@ namespace NzbDrone.Core.Books
private Book AddSkyhookData(Book newBook)
{
var editionId = newBook.Editions.Value.Single(x => x.Monitored).ForeignEditionId;
Tuple<string, Book, List<AuthorMetadata>> tuple = null;
try
{
tuple = _bookInfo.GetBookInfo(newBook.Editions.Value.Single(x => x.Monitored).ForeignEditionId);
tuple = _bookInfo.GetBookInfo(editionId);
}
catch (BookNotFoundException)
{
@ -123,6 +125,10 @@ namespace NzbDrone.Core.Books
newBook.UseMetadataFrom(tuple.Item2);
newBook.Added = DateTime.UtcNow;
newBook.Editions = tuple.Item2.Editions.Value;
newBook.Editions.Value.ForEach(x => x.Monitored = false);
newBook.Editions.Value.Single(x => x.ForeignEditionId == editionId).Monitored = true;
var metadata = tuple.Item3.Single(x => x.ForeignAuthorId == tuple.Item1);
newBook.AuthorMetadata = metadata;

@ -255,6 +255,12 @@ namespace NzbDrone.Core.Books
monitored = children.Future;
}
if (monitored.Count == 0)
{
// there are no future children so nothing to do
return;
}
var toMonitor = monitored.OrderByDescending(x => _mediaFileService.GetFilesByEdition(x.Id).Count)
.ThenByDescending(x => x.Ratings.Popularity)
.First();

@ -51,7 +51,7 @@ namespace NzbDrone.Core.ImportLists.Goodreads
{
Author = x.Book.Authors.First().Name.CleanSpaces(),
Book = x.Book.TitleWithoutSeries.CleanSpaces(),
AlbumMusicBrainzId = x.Book.Uri.Replace("kca://book/", string.Empty)
EditionGoodreadsId = x.Book.Id.ToString()
}).ToList();
}

@ -10,6 +10,7 @@ using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.OAuth;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.MetadataSource.Goodreads;
@ -37,7 +38,6 @@ namespace NzbDrone.Core.ImportLists.Goodreads
public string AccessToken => Settings.AccessToken;
protected HttpRequestBuilder RequestBuilder() => new HttpRequestBuilder("https://www.goodreads.com/{route}")
.AddQueryParam("key", "xQh8LhdTztb9u3cL26RqVg", true)
.AddQueryParam("_nc", "1")
.KeepAlive();
@ -75,7 +75,7 @@ namespace NzbDrone.Core.ImportLists.Goodreads
throw new BadRequestException("QueryParam callbackUrl invalid.");
}
var oAuthRequest = OAuthRequest.ForRequestToken(Settings.ConsumerKey, Settings.ConsumerSecret, query["callbackUrl"]);
var oAuthRequest = OAuthRequest.ForRequestToken(null, null, query["callbackUrl"]);
oAuthRequest.RequestUrl = Settings.OAuthRequestTokenUrl;
var qscoll = OAuthQuery(oAuthRequest);
@ -99,7 +99,7 @@ namespace NzbDrone.Core.ImportLists.Goodreads
throw new BadRequestException("Missing requestTokenSecret.");
}
var oAuthRequest = OAuthRequest.ForAccessToken(Settings.ConsumerKey, Settings.ConsumerSecret, query["oauth_token"], query["requestTokenSecret"], "");
var oAuthRequest = OAuthRequest.ForAccessToken(null, null, query["oauth_token"], query["requestTokenSecret"], "");
oAuthRequest.RequestUrl = Settings.OAuthAccessTokenUrl;
var qscoll = OAuthQuery(oAuthRequest);
@ -123,22 +123,41 @@ namespace NzbDrone.Core.ImportLists.Goodreads
protected Common.Http.HttpResponse OAuthGet(HttpRequestBuilder builder)
{
var auth = OAuthRequest.ForProtectedResource(builder.Method.ToString(), Settings.ConsumerKey, Settings.ConsumerSecret, Settings.AccessToken, Settings.AccessTokenSecret);
var auth = OAuthRequest.ForProtectedResource(builder.Method.ToString(), null, null, Settings.AccessToken, Settings.AccessTokenSecret);
var request = builder.Build();
request.LogResponseContent = true;
// we need the url without the query to sign
auth.RequestUrl = request.Url.SetQuery(null).FullUri;
auth.Parameters = builder.QueryParams.ToDictionary(x => x.Key, x => x.Value);
var header = auth.GetAuthorizationHeader(builder.QueryParams.ToDictionary(x => x.Key, x => x.Value));
var header = GetAuthorizationHeader(auth);
request.Headers.Add("Authorization", header);
return _httpClient.Get(request);
}
private string GetAuthorizationHeader(OAuthRequest oAuthRequest)
{
var request = new Common.Http.HttpRequest(Settings.SigningUrl)
{
Method = HttpMethod.POST,
};
request.Headers.Set("Content-Type", "application/json");
var payload = oAuthRequest.ToJson();
_logger.Trace(payload);
request.SetContent(payload);
var response = _httpClient.Post<AuthorizationHeader>(request).Resource;
return response.Authorization;
}
private NameValueCollection OAuthQuery(OAuthRequest oAuthRequest)
{
var auth = oAuthRequest.GetAuthorizationHeader();
var auth = GetAuthorizationHeader(oAuthRequest);
var request = new Common.Http.HttpRequest(oAuthRequest.RequestUrl);
request.Headers.Add("Authorization", auth);
var response = _httpClient.Get(request);
@ -148,9 +167,7 @@ namespace NzbDrone.Core.ImportLists.Goodreads
private Tuple<string, string> GetUser()
{
var builder = RequestBuilder()
.SetSegment("route", $"api/auth_user")
.AddQueryParam("key", Settings.ConsumerKey, true);
var builder = RequestBuilder().SetSegment("route", $"api/auth_user");
var httpResponse = OAuthGet(builder);
@ -169,4 +186,9 @@ namespace NzbDrone.Core.ImportLists.Goodreads
return Tuple.Create(userId, userName);
}
}
public class AuthorizationHeader
{
public string Authorization { get; set; }
}
}

@ -49,9 +49,9 @@ namespace NzbDrone.Core.ImportLists.Goodreads
var result = reviews.Select(x => new ImportListItemInfo
{
Author = x.Book.Authors.First().Name.CleanSpaces(),
ArtistMusicBrainzId = x.Book.Authors.First().Id.ToString(),
AuthorGoodreadsId = x.Book.Authors.First().Id.ToString(),
Book = x.Book.TitleWithoutSeries.CleanSpaces(),
AlbumMusicBrainzId = x.Book.Id.ToString()
EditionGoodreadsId = x.Book.Id.ToString()
}).ToList();
return CleanupListItems(result);

@ -24,8 +24,7 @@ namespace NzbDrone.Core.ImportLists.Goodreads
public string BaseUrl { get; set; }
public string ConsumerKey => "xQh8LhdTztb9u3cL26RqVg";
public string ConsumerSecret => "96aDA1lJRcS8KofYbw2jjkRk3wTNKypHAL2GeOgbPZw";
public string SigningUrl => "https://auth.servarr.com/v1/goodreads/sign";
public string OAuthUrl => "https://www.goodreads.com/oauth/authorize";
public string OAuthRequestTokenUrl => "https://www.goodreads.com/oauth/request_token";
public string OAuthAccessTokenUrl => "https://www.goodreads.com/oauth/access_token";

@ -97,18 +97,18 @@ namespace NzbDrone.Core.ImportLists
var importList = _importListFactory.Get(report.ImportListId);
if (report.Book.IsNotNullOrWhiteSpace() || report.AlbumMusicBrainzId.IsNotNullOrWhiteSpace())
if (report.Book.IsNotNullOrWhiteSpace() || report.EditionGoodreadsId.IsNotNullOrWhiteSpace())
{
if (report.AlbumMusicBrainzId.IsNullOrWhiteSpace() || report.ArtistMusicBrainzId.IsNullOrWhiteSpace())
if (report.EditionGoodreadsId.IsNullOrWhiteSpace() || report.AuthorGoodreadsId.IsNullOrWhiteSpace())
{
MapAlbumReport(report);
}
ProcessAlbumReport(importList, report, listExclusions, albumsToAdd);
}
else if (report.Author.IsNotNullOrWhiteSpace() || report.ArtistMusicBrainzId.IsNotNullOrWhiteSpace())
else if (report.Author.IsNotNullOrWhiteSpace() || report.AuthorGoodreadsId.IsNotNullOrWhiteSpace())
{
if (report.ArtistMusicBrainzId.IsNullOrWhiteSpace())
if (report.AuthorGoodreadsId.IsNullOrWhiteSpace())
{
MapArtistReport(report);
}
@ -137,9 +137,10 @@ namespace NzbDrone.Core.ImportLists
{
Book mappedAlbum;
if (report.AlbumMusicBrainzId.IsNotNullOrWhiteSpace() && int.TryParse(report.AlbumMusicBrainzId, out var goodreadsId))
if (report.EditionGoodreadsId.IsNotNullOrWhiteSpace() && int.TryParse(report.EditionGoodreadsId, out var goodreadsId))
{
mappedAlbum = _bookSearchService.SearchByGoodreadsId(goodreadsId).FirstOrDefault(x => int.TryParse(x.ForeignBookId, out var bookId) && bookId == goodreadsId);
var search = _bookSearchService.SearchByGoodreadsId(goodreadsId);
mappedAlbum = search.FirstOrDefault(x => x.Editions.Value.Any(e => int.TryParse(e.ForeignEditionId, out var editionId) && editionId == goodreadsId));
}
else
{
@ -149,62 +150,71 @@ namespace NzbDrone.Core.ImportLists
// Break if we are looking for an book and cant find it. This will avoid us from adding the author and possibly getting it wrong.
if (mappedAlbum == null)
{
_logger.Trace($"Nothing found for {report.AlbumMusicBrainzId}");
report.AlbumMusicBrainzId = null;
_logger.Trace($"Nothing found for {report.EditionGoodreadsId}");
report.EditionGoodreadsId = null;
return;
}
_logger.Trace($"Mapped {report.AlbumMusicBrainzId} to {mappedAlbum}");
_logger.Trace($"Mapped {report.EditionGoodreadsId} to {mappedAlbum}");
report.AlbumMusicBrainzId = mappedAlbum.ForeignBookId;
report.EditionGoodreadsId = mappedAlbum.Editions.Value.Single(x => x.Monitored).ForeignEditionId;
report.BookGoodreadsId = mappedAlbum.ForeignBookId;
report.Book = mappedAlbum.Title;
report.Author = mappedAlbum.AuthorMetadata?.Value?.Name;
report.ArtistMusicBrainzId = mappedAlbum.AuthorMetadata?.Value?.ForeignAuthorId;
report.AuthorGoodreadsId = mappedAlbum.AuthorMetadata?.Value?.ForeignAuthorId;
}
private void ProcessAlbumReport(ImportListDefinition importList, ImportListItemInfo report, List<ImportListExclusion> listExclusions, List<Book> albumsToAdd)
{
if (report.AlbumMusicBrainzId == null)
if (report.EditionGoodreadsId == null)
{
return;
}
// Check to see if book in DB
var existingAlbum = _bookService.FindById(report.AlbumMusicBrainzId);
var existingAlbum = _bookService.FindById(report.EditionGoodreadsId);
if (existingAlbum != null)
{
_logger.Debug("{0} [{1}] Rejected, Book Exists in DB", report.AlbumMusicBrainzId, report.Book);
_logger.Debug("{0} [{1}] Rejected, Book Exists in DB", report.EditionGoodreadsId, report.Book);
return;
}
// Check to see if book excluded
var excludedAlbum = listExclusions.SingleOrDefault(s => s.ForeignId == report.AlbumMusicBrainzId);
var excludedAlbum = listExclusions.SingleOrDefault(s => s.ForeignId == report.EditionGoodreadsId);
if (excludedAlbum != null)
{
_logger.Debug("{0} [{1}] Rejected due to list exlcusion", report.AlbumMusicBrainzId, report.Book);
_logger.Debug("{0} [{1}] Rejected due to list exlcusion", report.EditionGoodreadsId, report.Book);
return;
}
// Check to see if author excluded
var excludedArtist = listExclusions.SingleOrDefault(s => s.ForeignId == report.ArtistMusicBrainzId);
var excludedArtist = listExclusions.SingleOrDefault(s => s.ForeignId == report.AuthorGoodreadsId);
if (excludedArtist != null)
{
_logger.Debug("{0} [{1}] Rejected due to list exlcusion for parent author", report.AlbumMusicBrainzId, report.Book);
_logger.Debug("{0} [{1}] Rejected due to list exlcusion for parent author", report.EditionGoodreadsId, report.Book);
return;
}
// Append Album if not already in DB or already on add list
if (albumsToAdd.All(s => s.ForeignBookId != report.AlbumMusicBrainzId))
if (albumsToAdd.All(s => s.ForeignBookId != report.EditionGoodreadsId))
{
var monitored = importList.ShouldMonitor != ImportListMonitorType.None;
var toAdd = new Book
{
ForeignBookId = report.AlbumMusicBrainzId,
ForeignBookId = report.BookGoodreadsId,
Monitored = monitored,
Editions = new List<Edition>
{
new Edition
{
ForeignEditionId = report.EditionGoodreadsId,
Monitored = true
}
},
Author = new Author
{
Monitored = monitored,
@ -234,37 +244,37 @@ namespace NzbDrone.Core.ImportLists
{
var mappedArtist = _authorSearchService.SearchForNewAuthor(report.Author)
.FirstOrDefault();
report.ArtistMusicBrainzId = mappedArtist?.Metadata.Value?.ForeignAuthorId;
report.AuthorGoodreadsId = mappedArtist?.Metadata.Value?.ForeignAuthorId;
report.Author = mappedArtist?.Metadata.Value?.Name;
}
private void ProcessArtistReport(ImportListDefinition importList, ImportListItemInfo report, List<ImportListExclusion> listExclusions, List<Author> artistsToAdd)
{
if (report.ArtistMusicBrainzId == null)
if (report.AuthorGoodreadsId == null)
{
return;
}
// Check to see if author in DB
var existingArtist = _authorService.FindById(report.ArtistMusicBrainzId);
var existingArtist = _authorService.FindById(report.AuthorGoodreadsId);
if (existingArtist != null)
{
_logger.Debug("{0} [{1}] Rejected, Author Exists in DB", report.ArtistMusicBrainzId, report.Author);
_logger.Debug("{0} [{1}] Rejected, Author Exists in DB", report.AuthorGoodreadsId, report.Author);
return;
}
// Check to see if author excluded
var excludedArtist = listExclusions.Where(s => s.ForeignId == report.ArtistMusicBrainzId).SingleOrDefault();
var excludedArtist = listExclusions.Where(s => s.ForeignId == report.AuthorGoodreadsId).SingleOrDefault();
if (excludedArtist != null)
{
_logger.Debug("{0} [{1}] Rejected due to list exlcusion", report.ArtistMusicBrainzId, report.Author);
_logger.Debug("{0} [{1}] Rejected due to list exlcusion", report.AuthorGoodreadsId, report.Author);
return;
}
// Append Author if not already in DB or already on add list
if (artistsToAdd.All(s => s.Metadata.Value.ForeignAuthorId != report.ArtistMusicBrainzId))
if (artistsToAdd.All(s => s.Metadata.Value.ForeignAuthorId != report.AuthorGoodreadsId))
{
var monitored = importList.ShouldMonitor != ImportListMonitorType.None;
@ -272,7 +282,7 @@ namespace NzbDrone.Core.ImportLists
{
Metadata = new AuthorMetadata
{
ForeignAuthorId = report.ArtistMusicBrainzId,
ForeignAuthorId = report.AuthorGoodreadsId,
Name = report.Author
},
Monitored = monitored,

@ -36,7 +36,7 @@ namespace NzbDrone.Core.ImportLists.LazyLibrarianImport
{
Author = item.AuthorName,
Book = item.BookName,
AlbumMusicBrainzId = item.BookId
EditionGoodreadsId = item.BookId
});
}

@ -7,9 +7,10 @@ namespace NzbDrone.Core.Parser.Model
public int ImportListId { get; set; }
public string ImportList { get; set; }
public string Author { get; set; }
public string ArtistMusicBrainzId { get; set; }
public string AuthorGoodreadsId { get; set; }
public string Book { get; set; }
public string AlbumMusicBrainzId { get; set; }
public string BookGoodreadsId { get; set; }
public string EditionGoodreadsId { get; set; }
public DateTime ReleaseDate { get; set; }
public override string ToString()

Loading…
Cancel
Save