mirror of
https://github.com/fergalmoran/Readarr.git
synced 2025-12-22 09:29:59 +00:00
New: Refresh button on book page that bypasses cache
This commit is contained in:
@@ -117,6 +117,7 @@ class BookDetails extends Component {
|
|||||||
images,
|
images,
|
||||||
links,
|
links,
|
||||||
isSaving,
|
isSaving,
|
||||||
|
isRefreshing,
|
||||||
isFetching,
|
isFetching,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
bookFilesError,
|
bookFilesError,
|
||||||
@@ -127,6 +128,7 @@ class BookDetails extends Component {
|
|||||||
nextBook,
|
nextBook,
|
||||||
isSearching,
|
isSearching,
|
||||||
onMonitorTogglePress,
|
onMonitorTogglePress,
|
||||||
|
onRefreshPress,
|
||||||
onSearchPress
|
onSearchPress
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -142,6 +144,15 @@ class BookDetails extends Component {
|
|||||||
<PageContent title={title}>
|
<PageContent title={title}>
|
||||||
<PageToolbar>
|
<PageToolbar>
|
||||||
<PageToolbarSection>
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label="Refresh"
|
||||||
|
iconName={icons.REFRESH}
|
||||||
|
spinningName={icons.REFRESH}
|
||||||
|
title="Refresh information"
|
||||||
|
isSpinning={isRefreshing}
|
||||||
|
onPress={onRefreshPress}
|
||||||
|
/>
|
||||||
|
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label="Search Book"
|
label="Search Book"
|
||||||
iconName={icons.SEARCH}
|
iconName={icons.SEARCH}
|
||||||
@@ -473,6 +484,7 @@ BookDetails.propTypes = {
|
|||||||
monitored: PropTypes.bool.isRequired,
|
monitored: PropTypes.bool.isRequired,
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
isSaving: PropTypes.bool.isRequired,
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
isRefreshing: PropTypes.bool,
|
||||||
isSearching: PropTypes.bool,
|
isSearching: PropTypes.bool,
|
||||||
isFetching: PropTypes.bool,
|
isFetching: PropTypes.bool,
|
||||||
isPopulated: PropTypes.bool,
|
isPopulated: PropTypes.bool,
|
||||||
|
|||||||
@@ -70,6 +70,12 @@ function createMapStateToProps() {
|
|||||||
isSearchingCommand.body.bookIds.indexOf(book.id) > -1
|
isSearchingCommand.body.bookIds.indexOf(book.id) > -1
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isRefreshingCommand = findCommand(commands, { name: commandNames.REFRESH_BOOK });
|
||||||
|
const isRefreshing = (
|
||||||
|
isCommandExecuting(isRefreshingCommand) &&
|
||||||
|
isRefreshingCommand.body.bookId === book.id
|
||||||
|
);
|
||||||
|
|
||||||
const isFetching = isBookFilesFetching;
|
const isFetching = isBookFilesFetching;
|
||||||
const isPopulated = isBookFilesPopulated;
|
const isPopulated = isBookFilesPopulated;
|
||||||
|
|
||||||
@@ -77,6 +83,7 @@ function createMapStateToProps() {
|
|||||||
...book,
|
...book,
|
||||||
shortDateFormat: uiSettings.shortDateFormat,
|
shortDateFormat: uiSettings.shortDateFormat,
|
||||||
author,
|
author,
|
||||||
|
isRefreshing,
|
||||||
isSearching,
|
isSearching,
|
||||||
isFetching,
|
isFetching,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
@@ -147,6 +154,13 @@ class BookDetailsConnector extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onRefreshPress = () => {
|
||||||
|
this.props.executeCommand({
|
||||||
|
name: commandNames.REFRESH_BOOK,
|
||||||
|
bookId: this.props.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onSearchPress = () => {
|
onSearchPress = () => {
|
||||||
this.props.executeCommand({
|
this.props.executeCommand({
|
||||||
name: commandNames.BOOK_SEARCH,
|
name: commandNames.BOOK_SEARCH,
|
||||||
@@ -162,6 +176,7 @@ class BookDetailsConnector extends Component {
|
|||||||
<BookDetails
|
<BookDetails
|
||||||
{...this.props}
|
{...this.props}
|
||||||
onMonitorTogglePress={this.onMonitorTogglePress}
|
onMonitorTogglePress={this.onMonitorTogglePress}
|
||||||
|
onRefreshPress={this.onRefreshPress}
|
||||||
onSearchPress={this.onSearchPress}
|
onSearchPress={this.onSearchPress}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const INTERACTIVE_IMPORT = 'ManualImport';
|
|||||||
export const MISSING_BOOK_SEARCH = 'MissingBookSearch';
|
export const MISSING_BOOK_SEARCH = 'MissingBookSearch';
|
||||||
export const MOVE_AUTHOR = 'MoveAuthor';
|
export const MOVE_AUTHOR = 'MoveAuthor';
|
||||||
export const REFRESH_AUTHOR = 'RefreshAuthor';
|
export const REFRESH_AUTHOR = 'RefreshAuthor';
|
||||||
|
export const REFRESH_BOOK = 'RefreshBook';
|
||||||
export const RENAME_FILES = 'RenameFiles';
|
export const RENAME_FILES = 'RenameFiles';
|
||||||
export const RENAME_AUTHOR = 'RenameAuthor';
|
export const RENAME_AUTHOR = 'RenameAuthor';
|
||||||
export const RESCAN_FOLDERS = 'RescanFolders';
|
export const RESCAN_FOLDERS = 'RescanFolders';
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ namespace NzbDrone.Core.Test.Framework
|
|||||||
|
|
||||||
var httpClient = Mocker.Resolve<IHttpClient>();
|
var httpClient = Mocker.Resolve<IHttpClient>();
|
||||||
Mocker.GetMock<ICachedHttpResponseService>()
|
Mocker.GetMock<ICachedHttpResponseService>()
|
||||||
.Setup(x => x.Get(It.IsAny<HttpRequest>(), It.IsAny<TimeSpan>()))
|
.Setup(x => x.Get(It.IsAny<HttpRequest>(), It.IsAny<bool>(), It.IsAny<TimeSpan>()))
|
||||||
.Returns((HttpRequest request, TimeSpan ttl) => httpClient.Get(request));
|
.Returns((HttpRequest request, bool useCavhe, TimeSpan ttl) => httpClient.Get(request));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ namespace NzbDrone.Core.Test.MusicTests
|
|||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
Mocker.GetMock<IProvideBookInfo>()
|
Mocker.GetMock<IProvideBookInfo>()
|
||||||
.Setup(s => s.GetBookInfo(readarrId))
|
.Setup(s => s.GetBookInfo(readarrId, true))
|
||||||
.Returns(Tuple.Create(_fakeArtist.Metadata.Value.ForeignAuthorId,
|
.Returns(Tuple.Create(_fakeArtist.Metadata.Value.ForeignAuthorId,
|
||||||
_fakeAlbum,
|
_fakeAlbum,
|
||||||
new List<AuthorMetadata> { _fakeArtist.Metadata.Value }));
|
new List<AuthorMetadata> { _fakeArtist.Metadata.Value }));
|
||||||
@@ -99,7 +99,7 @@ namespace NzbDrone.Core.Test.MusicTests
|
|||||||
var newAlbum = AlbumToAdd("edition", "book", "author");
|
var newAlbum = AlbumToAdd("edition", "book", "author");
|
||||||
|
|
||||||
Mocker.GetMock<IProvideBookInfo>()
|
Mocker.GetMock<IProvideBookInfo>()
|
||||||
.Setup(s => s.GetBookInfo("edition"))
|
.Setup(s => s.GetBookInfo("edition", true))
|
||||||
.Throws(new BookNotFoundException("edition"));
|
.Throws(new BookNotFoundException("edition"));
|
||||||
|
|
||||||
Assert.Throws<ValidationException>(() => Subject.AddBook(newAlbum));
|
Assert.Throws<ValidationException>(() => Subject.AddBook(newAlbum));
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Test.MusicTests
|
|||||||
private void GivenValidArtist(string readarrId)
|
private void GivenValidArtist(string readarrId)
|
||||||
{
|
{
|
||||||
Mocker.GetMock<IProvideAuthorInfo>()
|
Mocker.GetMock<IProvideAuthorInfo>()
|
||||||
.Setup(s => s.GetAuthorInfo(readarrId))
|
.Setup(s => s.GetAuthorInfo(readarrId, true))
|
||||||
.Returns(_fakeArtist);
|
.Returns(_fakeArtist);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ namespace NzbDrone.Core.Test.MusicTests
|
|||||||
};
|
};
|
||||||
|
|
||||||
Mocker.GetMock<IProvideAuthorInfo>()
|
Mocker.GetMock<IProvideAuthorInfo>()
|
||||||
.Setup(s => s.GetAuthorInfo(newArtist.ForeignAuthorId))
|
.Setup(s => s.GetAuthorInfo(newArtist.ForeignAuthorId, true))
|
||||||
.Throws(new AuthorNotFoundException(newArtist.ForeignAuthorId));
|
.Throws(new AuthorNotFoundException(newArtist.ForeignAuthorId));
|
||||||
|
|
||||||
Mocker.GetMock<IAddAuthorValidator>()
|
Mocker.GetMock<IAddAuthorValidator>()
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ using System.Linq;
|
|||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Instrumentation.Extensions;
|
using NzbDrone.Common.Instrumentation.Extensions;
|
||||||
|
using NzbDrone.Core.Books.Commands;
|
||||||
using NzbDrone.Core.Books.Events;
|
using NzbDrone.Core.Books.Events;
|
||||||
|
using NzbDrone.Core.Exceptions;
|
||||||
using NzbDrone.Core.History;
|
using NzbDrone.Core.History;
|
||||||
using NzbDrone.Core.MediaCover;
|
using NzbDrone.Core.MediaCover;
|
||||||
using NzbDrone.Core.MediaFiles;
|
using NzbDrone.Core.MediaFiles;
|
||||||
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.Core.MetadataSource;
|
using NzbDrone.Core.MetadataSource;
|
||||||
|
|
||||||
@@ -20,12 +23,15 @@ namespace NzbDrone.Core.Books
|
|||||||
bool RefreshBookInfo(List<Book> books, List<Book> remoteBooks, Author remoteData, bool forceBookRefresh, bool forceUpdateFileTags, DateTime? lastUpdate);
|
bool RefreshBookInfo(List<Book> books, List<Book> remoteBooks, Author remoteData, bool forceBookRefresh, bool forceUpdateFileTags, DateTime? lastUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RefreshBookService : RefreshEntityServiceBase<Book, Edition>, IRefreshBookService
|
public class RefreshBookService : RefreshEntityServiceBase<Book, Edition>,
|
||||||
|
IRefreshBookService,
|
||||||
|
IExecute<RefreshBookCommand>
|
||||||
{
|
{
|
||||||
private readonly IBookService _bookService;
|
private readonly IBookService _bookService;
|
||||||
private readonly IAuthorService _authorService;
|
private readonly IAuthorService _authorService;
|
||||||
private readonly IAddAuthorService _addAuthorService;
|
private readonly IAddAuthorService _addAuthorService;
|
||||||
private readonly IEditionService _editionService;
|
private readonly IEditionService _editionService;
|
||||||
|
private readonly IProvideAuthorInfo _authorInfo;
|
||||||
private readonly IProvideBookInfo _bookInfo;
|
private readonly IProvideBookInfo _bookInfo;
|
||||||
private readonly IRefreshEditionService _refreshEditionService;
|
private readonly IRefreshEditionService _refreshEditionService;
|
||||||
private readonly IMediaFileService _mediaFileService;
|
private readonly IMediaFileService _mediaFileService;
|
||||||
@@ -40,6 +46,7 @@ namespace NzbDrone.Core.Books
|
|||||||
IAddAuthorService addAuthorService,
|
IAddAuthorService addAuthorService,
|
||||||
IEditionService editionService,
|
IEditionService editionService,
|
||||||
IAuthorMetadataService authorMetadataService,
|
IAuthorMetadataService authorMetadataService,
|
||||||
|
IProvideAuthorInfo authorInfo,
|
||||||
IProvideBookInfo bookInfo,
|
IProvideBookInfo bookInfo,
|
||||||
IRefreshEditionService refreshEditionService,
|
IRefreshEditionService refreshEditionService,
|
||||||
IMediaFileService mediaFileService,
|
IMediaFileService mediaFileService,
|
||||||
@@ -54,6 +61,7 @@ namespace NzbDrone.Core.Books
|
|||||||
_authorService = authorService;
|
_authorService = authorService;
|
||||||
_addAuthorService = addAuthorService;
|
_addAuthorService = addAuthorService;
|
||||||
_editionService = editionService;
|
_editionService = editionService;
|
||||||
|
_authorInfo = authorInfo;
|
||||||
_bookInfo = bookInfo;
|
_bookInfo = bookInfo;
|
||||||
_refreshEditionService = refreshEditionService;
|
_refreshEditionService = refreshEditionService;
|
||||||
_mediaFileService = mediaFileService;
|
_mediaFileService = mediaFileService;
|
||||||
@@ -64,6 +72,32 @@ namespace NzbDrone.Core.Books
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Author GetSkyhookData(Book book)
|
||||||
|
{
|
||||||
|
var foreignId = book.Editions.Value.First().ForeignEditionId;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tuple = _bookInfo.GetBookInfo(foreignId, false);
|
||||||
|
var author = _authorInfo.GetAuthorInfo(tuple.Item1, false);
|
||||||
|
var newbook = tuple.Item2;
|
||||||
|
|
||||||
|
newbook.Author = author;
|
||||||
|
newbook.AuthorMetadata = author.Metadata.Value;
|
||||||
|
newbook.AuthorMetadataId = book.AuthorMetadataId;
|
||||||
|
newbook.AuthorMetadata.Value.Id = book.AuthorMetadataId;
|
||||||
|
|
||||||
|
author.Books = new List<Book> { newbook };
|
||||||
|
return author;
|
||||||
|
}
|
||||||
|
catch (BookNotFoundException)
|
||||||
|
{
|
||||||
|
_logger.Error($"Could not find book with id {foreignId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
protected override RemoteData GetRemoteData(Book local, List<Book> remote, Author data)
|
protected override RemoteData GetRemoteData(Book local, List<Book> remote, Author data)
|
||||||
{
|
{
|
||||||
var result = new RemoteData();
|
var result = new RemoteData();
|
||||||
@@ -326,5 +360,22 @@ namespace NzbDrone.Core.Books
|
|||||||
{
|
{
|
||||||
return RefreshEntityInfo(book, remoteBooks, remoteData, true, forceUpdateFileTags, null);
|
return RefreshEntityInfo(book, remoteBooks, remoteData, true, forceUpdateFileTags, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool RefreshBookInfo(Book book)
|
||||||
|
{
|
||||||
|
var data = GetSkyhookData(book);
|
||||||
|
|
||||||
|
return RefreshBookInfo(book, data.Books, data, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute(RefreshBookCommand message)
|
||||||
|
{
|
||||||
|
if (message.BookId.HasValue)
|
||||||
|
{
|
||||||
|
var book = _bookService.GetBook(message.BookId.Value);
|
||||||
|
|
||||||
|
RefreshBookInfo(book);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace NzbDrone.Core.Http
|
|||||||
{
|
{
|
||||||
public interface ICachedHttpResponseService
|
public interface ICachedHttpResponseService
|
||||||
{
|
{
|
||||||
HttpResponse Get(HttpRequest request, TimeSpan ttl);
|
HttpResponse Get(HttpRequest request, bool useCache, TimeSpan ttl);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CachedHttpResponseService : ICachedHttpResponseService
|
public class CachedHttpResponseService : ICachedHttpResponseService
|
||||||
@@ -21,11 +21,11 @@ namespace NzbDrone.Core.Http
|
|||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpResponse Get(HttpRequest request, TimeSpan ttl)
|
public HttpResponse Get(HttpRequest request, bool useCache, TimeSpan ttl)
|
||||||
{
|
{
|
||||||
var cached = _repo.FindByUrl(request.Url.ToString());
|
var cached = _repo.FindByUrl(request.Url.ToString());
|
||||||
|
|
||||||
if (cached != null && cached.Expiry > DateTime.UtcNow)
|
if (useCache && cached != null && cached.Expiry > DateTime.UtcNow)
|
||||||
{
|
{
|
||||||
return new HttpResponse(request, new HttpHeader(), cached.Value, (HttpStatusCode)cached.StatusCode);
|
return new HttpResponse(request, new HttpHeader(), cached.Value, (HttpStatusCode)cached.StatusCode);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ namespace NzbDrone.Core.MetadataSource.Goodreads
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Author GetAuthorInfo(string foreignAuthorId)
|
public Author GetAuthorInfo(string foreignAuthorId, bool useCache = true)
|
||||||
{
|
{
|
||||||
_logger.Debug("Getting Author details GoodreadsId of {0}", foreignAuthorId);
|
_logger.Debug("Getting Author details GoodreadsId of {0}", foreignAuthorId);
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ namespace NzbDrone.Core.MetadataSource.Goodreads
|
|||||||
httpRequest.AllowAutoRedirect = true;
|
httpRequest.AllowAutoRedirect = true;
|
||||||
httpRequest.SuppressHttpError = true;
|
httpRequest.SuppressHttpError = true;
|
||||||
|
|
||||||
var httpResponse = _cachedHttpClient.Get(httpRequest, TimeSpan.FromDays(30));
|
var httpResponse = _cachedHttpClient.Get(httpRequest, useCache, TimeSpan.FromDays(30));
|
||||||
|
|
||||||
if (httpResponse.HasHttpError)
|
if (httpResponse.HasHttpError)
|
||||||
{
|
{
|
||||||
@@ -217,7 +217,7 @@ namespace NzbDrone.Core.MetadataSource.Goodreads
|
|||||||
httpRequest.AllowAutoRedirect = true;
|
httpRequest.AllowAutoRedirect = true;
|
||||||
httpRequest.SuppressHttpError = true;
|
httpRequest.SuppressHttpError = true;
|
||||||
|
|
||||||
var httpResponse = _cachedHttpClient.Get(httpRequest, TimeSpan.FromDays(7));
|
var httpResponse = _cachedHttpClient.Get(httpRequest, true, TimeSpan.FromDays(7));
|
||||||
|
|
||||||
if (httpResponse.HasHttpError)
|
if (httpResponse.HasHttpError)
|
||||||
{
|
{
|
||||||
@@ -249,7 +249,7 @@ namespace NzbDrone.Core.MetadataSource.Goodreads
|
|||||||
httpRequest.AllowAutoRedirect = true;
|
httpRequest.AllowAutoRedirect = true;
|
||||||
httpRequest.SuppressHttpError = true;
|
httpRequest.SuppressHttpError = true;
|
||||||
|
|
||||||
var httpResponse = _cachedHttpClient.Get(httpRequest, TimeSpan.FromDays(90));
|
var httpResponse = _cachedHttpClient.Get(httpRequest, true, TimeSpan.FromDays(90));
|
||||||
|
|
||||||
if (httpResponse.HasHttpError)
|
if (httpResponse.HasHttpError)
|
||||||
{
|
{
|
||||||
@@ -311,7 +311,7 @@ namespace NzbDrone.Core.MetadataSource.Goodreads
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Tuple<string, Book, List<AuthorMetadata>> GetBookInfo(string foreignEditionId)
|
public Tuple<string, Book, List<AuthorMetadata>> GetBookInfo(string foreignEditionId, bool useCache = true)
|
||||||
{
|
{
|
||||||
_logger.Debug("Getting Book with GoodreadsId of {0}", foreignEditionId);
|
_logger.Debug("Getting Book with GoodreadsId of {0}", foreignEditionId);
|
||||||
|
|
||||||
@@ -323,7 +323,7 @@ namespace NzbDrone.Core.MetadataSource.Goodreads
|
|||||||
httpRequest.AllowAutoRedirect = true;
|
httpRequest.AllowAutoRedirect = true;
|
||||||
httpRequest.SuppressHttpError = true;
|
httpRequest.SuppressHttpError = true;
|
||||||
|
|
||||||
var httpResponse = _cachedHttpClient.Get(httpRequest, TimeSpan.FromDays(90));
|
var httpResponse = _cachedHttpClient.Get(httpRequest, useCache, TimeSpan.FromDays(90));
|
||||||
|
|
||||||
if (httpResponse.HasHttpError)
|
if (httpResponse.HasHttpError)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace NzbDrone.Core.MetadataSource
|
|||||||
{
|
{
|
||||||
public interface IProvideAuthorInfo
|
public interface IProvideAuthorInfo
|
||||||
{
|
{
|
||||||
Author GetAuthorInfo(string readarrId);
|
Author GetAuthorInfo(string readarrId, bool useCache = true);
|
||||||
Author GetAuthorAndBooks(string readarrId, double minPopularity = 0);
|
Author GetAuthorAndBooks(string readarrId, double minPopularity = 0);
|
||||||
HashSet<string> GetChangedArtists(DateTime startTime);
|
HashSet<string> GetChangedArtists(DateTime startTime);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace NzbDrone.Core.MetadataSource
|
|||||||
{
|
{
|
||||||
public interface IProvideBookInfo
|
public interface IProvideBookInfo
|
||||||
{
|
{
|
||||||
Tuple<string, Book, List<AuthorMetadata>> GetBookInfo(string id);
|
Tuple<string, Book, List<AuthorMetadata>> GetBookInfo(string id, bool useCache = true);
|
||||||
HashSet<string> GetChangedBooks(DateTime startTime);
|
HashSet<string> GetChangedBooks(DateTime startTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user