New: Goodreads Shelves + Owned Books notifications

pull/45/head
ta264 4 years ago
parent 3504cbe9cd
commit 821aa90b14

@ -1,4 +1,4 @@
.playlistInputWrapper {
.bookshelfInputWrapper {
display: flex;
flex-direction: column;
}

@ -11,7 +11,7 @@ import TableBody from 'Components/Table/TableBody';
import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import styles from './PlaylistInput.css';
import styles from './BookshelfInput.css';
const columns = [
{
@ -22,7 +22,7 @@ const columns = [
}
];
class PlaylistInput extends Component {
class BookshelfInput extends Component {
//
// Lifecycle
@ -82,6 +82,7 @@ class PlaylistInput extends Component {
render() {
const {
className,
helptext,
items,
user,
isFetching,
@ -104,7 +105,7 @@ class PlaylistInput extends Component {
{
!isPopulated && !isFetching &&
<div>
Authenticate with Goodreads to retrieve bookshelves to import.
Authenticate with Goodreads to retrieve bookshelves.
</div>
}
@ -125,7 +126,7 @@ class PlaylistInput extends Component {
{
isPopulated && !isFetching && user && !!items.length &&
<div className={className}>
Select bookshelves to import from Goodreads user {user}.
{helptext}
<Table
columns={columns}
selectAll={true}
@ -165,10 +166,11 @@ class PlaylistInput extends Component {
}
}
PlaylistInput.propTypes = {
BookshelfInput.propTypes = {
className: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired,
helptext: PropTypes.string.isRequired,
user: PropTypes.string.isRequired,
items: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
hasError: PropTypes.bool,
@ -178,9 +180,9 @@ PlaylistInput.propTypes = {
onChange: PropTypes.func.isRequired
};
PlaylistInput.defaultProps = {
className: styles.playlistInputWrapper,
BookshelfInput.defaultProps = {
className: styles.bookshelfInputWrapper,
inputClassName: styles.input
};
export default PlaylistInput;
export default BookshelfInput;

@ -4,19 +4,21 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchOptions, clearOptions } from 'Store/Actions/providerOptionActions';
import PlaylistInput from './PlaylistInput';
import BookshelfInput from './BookshelfInput';
function createMapStateToProps() {
return createSelector(
(state) => state.providerOptions,
(state) => {
(state, props) => props.name,
(state, name) => {
const {
items,
...otherState
} = state;
return ({
helptext: items.helptext && items.helptext[name] ? items.helptext[name] : '',
user: items.user ? items.user : '',
items: items.playlists ? items.playlists : [],
items: items.shelves ? items.shelves : [],
...otherState
});
}
@ -28,7 +30,7 @@ const mapDispatchToProps = {
dispatchClearOptions: clearOptions
};
class PlaylistInputConnector extends Component {
class BookshelfInputConnector extends Component {
//
// Lifecycle
@ -58,11 +60,13 @@ class PlaylistInputConnector extends Component {
const {
provider,
providerData,
dispatchFetchOptions
dispatchFetchOptions,
name
} = this.props;
dispatchFetchOptions({
action: 'getPlaylists',
action: 'getBookshelves',
queryParams: { name },
provider,
providerData
});
@ -77,7 +81,7 @@ class PlaylistInputConnector extends Component {
render() {
return (
<PlaylistInput
<BookshelfInput
{...this.props}
onRefreshPress={this.onRefreshPress}
/>
@ -85,7 +89,7 @@ class PlaylistInputConnector extends Component {
}
}
PlaylistInputConnector.propTypes = {
BookshelfInputConnector.propTypes = {
provider: PropTypes.string.isRequired,
providerData: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
@ -94,4 +98,4 @@ PlaylistInputConnector.propTypes = {
dispatchClearOptions: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(PlaylistInputConnector);
export default connect(createMapStateToProps, mapDispatchToProps)(BookshelfInputConnector);

@ -6,7 +6,7 @@ import AutoCompleteInput from './AutoCompleteInput';
import CaptchaInputConnector from './CaptchaInputConnector';
import CheckInput from './CheckInput';
import DeviceInputConnector from './DeviceInputConnector';
import PlaylistInputConnector from './PlaylistInputConnector';
import BookshelfInputConnector from './BookshelfInputConnector';
import KeyValueListInput from './KeyValueListInput';
import MonitorBooksSelectInput from './MonitorBooksSelectInput';
import NumberInput from './NumberInput';
@ -39,8 +39,8 @@ function getComponent(type) {
case inputTypes.DEVICE:
return DeviceInputConnector;
case inputTypes.PLAYLIST:
return PlaylistInputConnector;
case inputTypes.BOOKSHELF:
return BookshelfInputConnector;
case inputTypes.KEY_VALUE_LIST:
return KeyValueListInput;

@ -14,8 +14,8 @@ function getType(type) {
return inputTypes.CHECK;
case 'device':
return inputTypes.DEVICE;
case 'playlist':
return inputTypes.PLAYLIST;
case 'bookshelf':
return inputTypes.BOOKSHELF;
case 'password':
return inputTypes.PASSWORD;
case 'number':

@ -2,7 +2,7 @@ export const AUTO_COMPLETE = 'autoComplete';
export const CAPTCHA = 'captcha';
export const CHECK = 'check';
export const DEVICE = 'device';
export const PLAYLIST = 'playlist';
export const BOOKSHELF = 'bookshelf';
export const KEY_VALUE_LIST = 'keyValueList';
export const MONITOR_BOOKS_SELECT = 'monitorBooksSelect';
export const NUMBER = 'number';
@ -24,7 +24,7 @@ export const all = [
CAPTCHA,
CHECK,
DEVICE,
PLAYLIST,
BOOKSHELF,
KEY_VALUE_LIST,
MONITOR_BOOKS_SELECT,
NUMBER,

@ -37,7 +37,7 @@ namespace NzbDrone.Core.Annotations
Captcha,
OAuth,
Device,
Playlist
Bookshelf
}
public enum HiddenType

@ -12,7 +12,7 @@ using NzbDrone.Core.Validation;
namespace NzbDrone.Core.ImportLists.Goodreads
{
public class GoodreadsBookshelf : GoodreadsImportListBase<GoodreadsBookshelfSettings>
public class GoodreadsBookshelf : GoodreadsImportListBase<GoodreadsBookshelfImportListSettings>
{
public GoodreadsBookshelf(IImportListStatusService importListStatusService,
IConfigService configService,
@ -27,7 +27,7 @@ namespace NzbDrone.Core.ImportLists.Goodreads
public override IList<ImportListItemInfo> Fetch()
{
return CleanupListItems(Settings.PlaylistIds.SelectMany(x => Fetch(x)).ToList());
return CleanupListItems(Settings.BookshelfIds.SelectMany(x => Fetch(x)).ToList());
}
public IList<ImportListItemInfo> Fetch(string shelf)
@ -57,13 +57,13 @@ namespace NzbDrone.Core.ImportLists.Goodreads
public override object RequestAction(string action, IDictionary<string, string> query)
{
if (action == "getPlaylists")
if (action == "getBookshelves")
{
if (Settings.AccessToken.IsNullOrWhiteSpace())
{
return new
{
playlists = new List<object>()
shelves = new List<object>()
};
}
@ -83,12 +83,18 @@ namespace NzbDrone.Core.ImportLists.Goodreads
shelves.AddRange(curr);
}
var helptext = new
{
shelfIds = $"Import books from {Settings.UserName}'s shelves:"
};
return new
{
options = new
{
helptext,
user = Settings.UserName,
playlists = shelves.OrderBy(p => p.Name)
shelves = shelves.OrderBy(p => p.Name)
.Select(p => new
{
id = p.Name,

@ -0,0 +1,28 @@
using System.Collections.Generic;
using FluentValidation;
using NzbDrone.Core.Annotations;
namespace NzbDrone.Core.ImportLists.Goodreads
{
public class GoodreadsBookshelfImportListSettingsValidator : GoodreadsSettingsBaseValidator<GoodreadsBookshelfImportListSettings>
{
public GoodreadsBookshelfImportListSettingsValidator()
: base()
{
RuleFor(c => c.BookshelfIds).NotEmpty();
}
}
public class GoodreadsBookshelfImportListSettings : GoodreadsSettingsBase<GoodreadsBookshelfImportListSettings>
{
public GoodreadsBookshelfImportListSettings()
{
BookshelfIds = new string[] { };
}
[FieldDefinition(1, Label = "Bookshelves", Type = FieldType.Bookshelf)]
public IEnumerable<string> BookshelfIds { get; set; }
protected override AbstractValidator<GoodreadsBookshelfImportListSettings> Validator => new GoodreadsBookshelfImportListSettingsValidator();
}
}

@ -1,28 +0,0 @@
using System.Collections.Generic;
using FluentValidation;
using NzbDrone.Core.Annotations;
namespace NzbDrone.Core.ImportLists.Goodreads
{
public class GoodreadsBookshelfSettingsValidator : GoodreadsSettingsBaseValidator<GoodreadsBookshelfSettings>
{
public GoodreadsBookshelfSettingsValidator()
: base()
{
RuleFor(c => c.PlaylistIds).NotEmpty();
}
}
public class GoodreadsBookshelfSettings : GoodreadsSettingsBase<GoodreadsBookshelfSettings>
{
public GoodreadsBookshelfSettings()
{
PlaylistIds = new string[] { };
}
[FieldDefinition(1, Label = "Bookshelves", Type = FieldType.Playlist)]
public IEnumerable<string> PlaylistIds { get; set; }
protected override AbstractValidator<GoodreadsBookshelfSettings> Validator => new GoodreadsBookshelfSettingsValidator();
}
}

@ -5,18 +5,17 @@ using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.MetadataSource.Goodreads;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.ImportLists.Goodreads
{
public class GoodreadsOwnedBooksSettings : GoodreadsSettingsBase<GoodreadsOwnedBooksSettings>
public class GoodreadsOwnedBooksImportListSettings : GoodreadsSettingsBase<GoodreadsOwnedBooksImportListSettings>
{
}
public class GoodreadsOwnedBooks : GoodreadsImportListBase<GoodreadsOwnedBooksSettings>
public class GoodreadsOwnedBooks : GoodreadsImportListBase<GoodreadsOwnedBooksImportListSettings>
{
public GoodreadsOwnedBooks(IImportListStatusService importListStatusService,
IConfigService configService,

@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.MetadataSource.Goodreads;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Notifications.Goodreads
{
public class GoodreadsBookshelf : GoodreadsNotificationBase<GoodreadsBookshelfNotificationSettings>
{
public GoodreadsBookshelf(IHttpClient httpClient,
Logger logger)
: base(httpClient, logger)
{
}
public override string Name => "Goodreads Bookshelves";
public override string Link => "https://goodreads.com/";
public override void OnReleaseImport(BookDownloadMessage message)
{
var bookId = message.Book.Editions.Value.Single(x => x.Monitored).ForeignEditionId;
RemoveBookFromShelves(bookId, Settings.RemoveIds);
AddToShelves(bookId, Settings.AddIds);
}
public override object RequestAction(string action, IDictionary<string, string> query)
{
if (action == "getBookshelves")
{
if (Settings.AccessToken.IsNullOrWhiteSpace())
{
return new
{
shelves = new List<object>()
};
}
Settings.Validate().Filter("AccessToken").ThrowOnError();
var shelves = new List<UserShelfResource>();
var page = 0;
while (true)
{
var curr = GetShelfList(++page);
if (curr == null || curr.Count == 0)
{
break;
}
shelves.AddRange(curr);
}
_logger.Trace($"Name: {query["name"]} {query["name"] == "removeIds"}");
var helptext = new
{
addIds = $"Add imported book to {Settings.UserName}'s shelves:",
removeIds = $"Remove imported book from {Settings.UserName}'s shelves:"
};
return new
{
options = new
{
helptext,
user = Settings.UserName,
shelves = shelves.OrderBy(p => p.Name)
.Select(p => new
{
id = p.Name,
name = p.Name
})
}
};
}
else
{
return base.RequestAction(action, query);
}
}
private IReadOnlyList<UserShelfResource> GetShelfList(int page)
{
try
{
var builder = RequestBuilder()
.SetSegment("route", $"shelf/list.xml")
.AddQueryParam("user_id", Settings.UserId)
.AddQueryParam("page", page);
var httpResponse = OAuthExecute(builder);
return httpResponse.Deserialize<PaginatedList<UserShelfResource>>("shelves").List;
}
catch (Exception ex)
{
_logger.Warn(ex, "Error fetching bookshelves from Goodreads");
return new List<UserShelfResource>();
}
}
private void RemoveBookFromShelves(string bookId, IEnumerable<string> shelves)
{
foreach (var shelf in shelves)
{
var req = RequestBuilder()
.Post()
.SetSegment("route", "shelf/add_to_shelf.xml")
.AddFormParameter("name", shelf)
.AddFormParameter("book_id", bookId)
.AddFormParameter("a", "remove");
// in case not found in shelf
req.SuppressHttpError = true;
OAuthExecute(req);
}
}
private void AddToShelves(string bookId, IEnumerable<string> shelves)
{
var req = RequestBuilder()
.Post()
.SetSegment("route", "shelf/add_books_to_shelves.xml")
.AddFormParameter("bookids", bookId)
.AddFormParameter("shelves", shelves.ConcatToString());
OAuthExecute(req);
}
}
}

@ -0,0 +1,40 @@
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Notifications.Goodreads
{
public class GoodreadsBookshelfNotificationSettingsValidator : GoodreadsSettingsBaseValidator<GoodreadsBookshelfNotificationSettings>
{
public GoodreadsBookshelfNotificationSettingsValidator()
: base()
{
RuleFor(c => c.RemoveIds).NotEmpty().When(c => !c.AddIds.Any());
RuleFor(c => c.AddIds).NotEmpty().When(c => !c.RemoveIds.Any());
}
}
public class GoodreadsBookshelfNotificationSettings : GoodreadsSettingsBase<GoodreadsBookshelfNotificationSettings>
{
private static readonly GoodreadsBookshelfNotificationSettingsValidator Validator = new GoodreadsBookshelfNotificationSettingsValidator();
public GoodreadsBookshelfNotificationSettings()
{
RemoveIds = new string[] { };
AddIds = new string[] { };
}
[FieldDefinition(1, Label = "Remove from Bookshelves", Type = FieldType.Bookshelf)]
public IEnumerable<string> RemoveIds { get; set; }
[FieldDefinition(1, Label = "Add to Bookshelves", Type = FieldType.Bookshelf)]
public IEnumerable<string> AddIds { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

@ -0,0 +1,198 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Web;
using System.Xml.Linq;
using System.Xml.XPath;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.OAuth;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.ImportLists.Goodreads;
using NzbDrone.Core.MetadataSource.Goodreads;
namespace NzbDrone.Core.Notifications.Goodreads
{
public abstract class GoodreadsNotificationBase<TSettings> : NotificationBase<TSettings>
where TSettings : GoodreadsSettingsBase<TSettings>, new()
{
protected readonly IHttpClient _httpClient;
protected readonly Logger _logger;
protected GoodreadsNotificationBase(IHttpClient httpClient,
Logger logger)
{
_httpClient = httpClient;
_logger = logger;
}
public override string Link => "https://goodreads.com/";
public override ValidationResult Test()
{
var failures = new List<ValidationFailure>();
failures.AddIfNotNull(TestConnection());
return new ValidationResult(failures);
}
public override object RequestAction(string action, IDictionary<string, string> query)
{
if (action == "startOAuth")
{
if (query["callbackUrl"].IsNullOrWhiteSpace())
{
throw new BadRequestException("QueryParam callbackUrl invalid.");
}
var oAuthRequest = OAuthRequest.ForRequestToken(null, null, query["callbackUrl"]);
oAuthRequest.RequestUrl = Settings.OAuthRequestTokenUrl;
var qscoll = OAuthQuery(oAuthRequest);
var url = string.Format("{0}?oauth_token={1}&oauth_callback={2}", Settings.OAuthUrl, qscoll["oauth_token"], query["callbackUrl"]);
return new
{
OauthUrl = url,
RequestTokenSecret = qscoll["oauth_token_secret"]
};
}
else if (action == "getOAuthToken")
{
if (query["oauth_token"].IsNullOrWhiteSpace())
{
throw new BadRequestException("QueryParam oauth_token invalid.");
}
if (query["requestTokenSecret"].IsNullOrWhiteSpace())
{
throw new BadRequestException("Missing requestTokenSecret.");
}
var oAuthRequest = OAuthRequest.ForAccessToken(null, null, query["oauth_token"], query["requestTokenSecret"], "");
oAuthRequest.RequestUrl = Settings.OAuthAccessTokenUrl;
var qscoll = OAuthQuery(oAuthRequest);
Settings.AccessToken = qscoll["oauth_token"];
Settings.AccessTokenSecret = qscoll["oauth_token_secret"];
var user = GetUser();
return new
{
Settings.AccessToken,
Settings.AccessTokenSecret,
RequestTokenSecret = "",
UserId = user.Item1,
UserName = user.Item2
};
}
return new { };
}
protected HttpRequestBuilder RequestBuilder()
{
return new HttpRequestBuilder("https://www.goodreads.com/{route}").KeepAlive();
}
protected Common.Http.HttpResponse OAuthExecute(HttpRequestBuilder builder)
{
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;
if (builder.Method == HttpMethod.GET)
{
auth.Parameters = builder.QueryParams.ToDictionary(x => x.Key, x => x.Value);
}
else if (builder.Method == HttpMethod.POST)
{
auth.Parameters = builder.FormData.ToDictionary(x => x.Name, x => Encoding.UTF8.GetString(x.ContentData));
}
var header = GetAuthorizationHeader(auth);
request.Headers.Add("Authorization", header);
return _httpClient.Execute(request);
}
private ValidationFailure TestConnection()
{
try
{
GetUser();
return null;
}
catch (Common.Http.HttpException ex)
{
_logger.Warn(ex, "Goodreads Authentication Error");
return new ValidationFailure(string.Empty, $"Goodreads authentication error: {ex.Message}");
}
catch (Exception ex)
{
_logger.Warn(ex, "Unable to connect to Goodreads");
return new ValidationFailure(string.Empty, "Unable to connect to Goodreads, check the log for more details");
}
}
private Tuple<string, string> GetUser()
{
var builder = RequestBuilder().SetSegment("route", "api/auth_user");
var httpResponse = OAuthExecute(builder);
string userId = null;
string userName = null;
var content = httpResponse.Content;
if (!string.IsNullOrWhiteSpace(content))
{
var user = XDocument.Parse(content).XPathSelectElement("GoodreadsResponse/user");
userId = user.AttributeAsString("id");
userName = user.ElementAsString("name");
}
return Tuple.Create(userId, userName);
}
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 = GetAuthorizationHeader(oAuthRequest);
var request = new Common.Http.HttpRequest(oAuthRequest.RequestUrl);
request.Headers.Add("Authorization", auth);
var response = _httpClient.Get(request);
return HttpUtility.ParseQueryString(response.Content);
}
}
}

@ -0,0 +1,53 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Notifications.Goodreads
{
public class GoodreadsSettingsBaseValidator<TSettings> : AbstractValidator<TSettings>
where TSettings : GoodreadsSettingsBase<TSettings>
{
public GoodreadsSettingsBaseValidator()
{
RuleFor(c => c.AccessToken).NotEmpty();
RuleFor(c => c.AccessTokenSecret).NotEmpty();
}
}
public abstract class GoodreadsSettingsBase<TSettings> : IProviderConfig
where TSettings : GoodreadsSettingsBase<TSettings>
{
public GoodreadsSettingsBase()
{
SignIn = "startOAuth";
}
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";
[FieldDefinition(0, Label = "Access Token", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
public string AccessToken { get; set; }
[FieldDefinition(0, Label = "Access Token Secret", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
public string AccessTokenSecret { get; set; }
[FieldDefinition(0, Label = "Request Token Secret", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
public string RequestTokenSecret { get; set; }
[FieldDefinition(0, Label = "User Id", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
public string UserId { get; set; }
[FieldDefinition(0, Label = "User Name", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
public string UserName { get; set; }
[FieldDefinition(99, Label = "Authenticate with Goodreads", Type = FieldType.OAuth)]
public string SignIn { get; set; }
public bool IsValid => !string.IsNullOrWhiteSpace(AccessTokenSecret);
public abstract NzbDroneValidationResult Validate();
}
}

@ -0,0 +1,48 @@
using System;
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.Notifications.Goodreads
{
public class GoodreadsOwnedBooks : GoodreadsNotificationBase<GoodreadsOwnedBooksNotificationSettings>
{
public GoodreadsOwnedBooks(IHttpClient httpClient,
Logger logger)
: base(httpClient, logger)
{
}
public override string Name => "Goodreads Owned Books";
public override string Link => "https://goodreads.com/";
public override void OnReleaseImport(BookDownloadMessage message)
{
var bookId = message.Book.Editions.Value.Single(x => x.Monitored).ForeignEditionId;
AddOwnedBook(bookId);
}
private void AddOwnedBook(string bookId)
{
var req = RequestBuilder()
.Post()
.SetSegment("route", "owned_books.xml")
.AddFormParameter("owned_book[book_id]", bookId)
.AddFormParameter("owned_book[condition_code]", Settings.Condition)
.AddFormParameter("owned_book[original_purchase_date]", DateTime.Now.ToString("O"));
if (Settings.Description.IsNotNullOrWhiteSpace())
{
req.AddFormParameter("owned_book[condition_description]", Settings.Description);
}
if (Settings.Location.IsNotNullOrWhiteSpace())
{
req.AddFormParameter("owned_book[original_purchase_location]", Settings.Location);
}
OAuthExecute(req);
}
}
}

@ -0,0 +1,38 @@
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Notifications.Goodreads
{
public enum OwnedBookCondition
{
BrandNew = 10,
LikeNew = 20,
VeryGood = 30,
Good = 40,
Acceptable = 50,
Poor = 60
}
public class GoodreadsOwnedBooksNotificationSettings : GoodreadsSettingsBase<GoodreadsOwnedBooksNotificationSettings>
{
private static readonly GoodreadsSettingsBaseValidator<GoodreadsOwnedBooksNotificationSettings> Validator = new GoodreadsSettingsBaseValidator<GoodreadsOwnedBooksNotificationSettings>();
public GoodreadsOwnedBooksNotificationSettings()
{
}
[FieldDefinition(1, Label = "Condition", Type = FieldType.Select, SelectOptions = typeof(OwnedBookCondition))]
public int Condition { get; set; } = (int)OwnedBookCondition.BrandNew;
[FieldDefinition(1, Label = "Condition Description", Type = FieldType.Textbox)]
public string Description { get; set; }
[FieldDefinition(1, Label = "Purchase Location", HelpText = "Will be displayed on Goodreads website", Type = FieldType.Textbox)]
public string Location { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}
Loading…
Cancel
Save