mirror of
https://github.com/Radarr/Radarr
synced 2026-04-26 01:30:50 +02:00
feat: Phase 2 Multi-Media Infrastructure - Books/Audiobooks Backend & Frontend (#125)
* feat(api): add Book and Audiobook lookup and editor controllers Adds search and bulk edit functionality for Books and Audiobooks: - BookLookupController: search by ISBN, ISBN13, ASIN, ForeignId, title - AudiobookLookupController: search by ISBN, ASIN, narrator, title - BookEditorController: bulk update/delete books - AudiobookEditorController: bulk update/delete audiobooks - Editor validators and resources for both entity types * feat(core): add AddBookService and AddAudiobookService Adds services for adding new books and audiobooks with validation: - AddBookService: handles book creation with path generation - AddAudiobookService: handles audiobook creation with narrator-aware paths - AddBookValidator: validates book additions - AddAudiobookValidator: validates audiobook additions * feat(core): add BookFile and AudiobookFile entities and repositories Adds file tracking infrastructure for books and audiobooks: - BookFile entity with format tracking - AudiobookFile entity with audio metadata (duration, bitrate, etc.) - BookFileRepository for book file queries - AudiobookFileRepository for audiobook file queries - Database migration for new tables - Table mappings for new entities * feat(metadata): add Book and Audiobook metadata provider interfaces Adds metadata provider infrastructure for books and audiobooks: - IProvideBookInfo: interface for book metadata lookups - IProvideAudiobookInfo: interface for audiobook metadata lookups - BookMetadata: model for book metadata from external sources - AudiobookMetadata: model for audiobook metadata with narrator info - BookInfoProxy: stub implementation (to be replaced with Goodreads, etc.) - AudiobookInfoProxy: stub implementation (to be replaced with Audible, etc.) * feat(api): add Author and Series repositories, services, and API controllers * feat(ui): add Book and Audiobook Redux store and index pages --------- Co-authored-by: admin <admin@ardentleatherworks.com>
This commit is contained in:
parent
cc9d8cd4d0
commit
4b7b273683
42 changed files with 2416 additions and 12 deletions
|
|
@ -1,5 +1,7 @@
|
|||
import { Error } from './AppSectionState';
|
||||
import AudiobooksAppState from './AudiobooksAppState';
|
||||
import BlocklistAppState from './BlocklistAppState';
|
||||
import BooksAppState from './BooksAppState';
|
||||
import CalendarAppState from './CalendarAppState';
|
||||
import CaptchaAppState from './CaptchaAppState';
|
||||
import CommandAppState from './CommandAppState';
|
||||
|
|
@ -81,7 +83,9 @@ export interface AppSectionState {
|
|||
|
||||
interface AppState {
|
||||
app: AppSectionState;
|
||||
audiobooks: AudiobooksAppState;
|
||||
blocklist: BlocklistAppState;
|
||||
books: BooksAppState;
|
||||
calendar: CalendarAppState;
|
||||
captcha: CaptchaAppState;
|
||||
commands: CommandAppState;
|
||||
|
|
|
|||
15
frontend/src/App/State/AudiobooksAppState.ts
Normal file
15
frontend/src/App/State/AudiobooksAppState.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Audiobook from 'Audiobook/Audiobook';
|
||||
|
||||
interface AudiobooksAppState
|
||||
extends
|
||||
AppSectionState<Audiobook>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {
|
||||
pendingChanges: Partial<Audiobook>;
|
||||
}
|
||||
|
||||
export default AudiobooksAppState;
|
||||
12
frontend/src/App/State/BooksAppState.ts
Normal file
12
frontend/src/App/State/BooksAppState.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Book from 'Book/Book';
|
||||
|
||||
interface BooksAppState
|
||||
extends AppSectionState<Book>, AppSectionDeleteState, AppSectionSaveState {
|
||||
pendingChanges: Partial<Book>;
|
||||
}
|
||||
|
||||
export default BooksAppState;
|
||||
28
frontend/src/Audiobook/Audiobook.ts
Normal file
28
frontend/src/Audiobook/Audiobook.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import ModelBase from 'App/ModelBase';
|
||||
|
||||
interface Audiobook extends ModelBase {
|
||||
title: string;
|
||||
sortTitle: string;
|
||||
description: string;
|
||||
foreignAudiobookId: string;
|
||||
isbn: string;
|
||||
asin: string;
|
||||
narrator: string;
|
||||
durationMinutes: number;
|
||||
releaseDate: string;
|
||||
publisher: string;
|
||||
language: string;
|
||||
monitored: boolean;
|
||||
qualityProfileId: number;
|
||||
path: string;
|
||||
rootFolderPath: string;
|
||||
added: string;
|
||||
tags: number[];
|
||||
lastSearchTime?: string;
|
||||
authorId?: number;
|
||||
seriesId?: number;
|
||||
seriesPosition?: number;
|
||||
isSaving?: boolean;
|
||||
}
|
||||
|
||||
export default Audiobook;
|
||||
|
|
@ -1,17 +1,102 @@
|
|||
import React from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { fetchAudiobooks } from 'Store/Actions/audiobookActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AudiobookIndexRow from './AudiobookIndexRow';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'title',
|
||||
label: () => translate('Title'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'narrator',
|
||||
label: () => translate('Narrator'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'duration',
|
||||
label: () => translate('Duration'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'releaseDate',
|
||||
label: () => translate('ReleaseDate'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'monitored',
|
||||
label: () => translate('Monitored'),
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
function AudiobookIndex() {
|
||||
const dispatch = useDispatch();
|
||||
const { isFetching, isPopulated, error, items } = useSelector(
|
||||
(state: AppState) => state.audiobooks
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchAudiobooks());
|
||||
}, [dispatch]);
|
||||
|
||||
const onRefreshPress = useCallback(() => {
|
||||
dispatch(fetchAudiobooks());
|
||||
}, [dispatch]);
|
||||
|
||||
const hasNoAudiobooks = isPopulated && !items.length;
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Audiobooks')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('RefreshAll')}
|
||||
iconName={icons.REFRESH}
|
||||
isSpinning={isFetching}
|
||||
onPress={onRefreshPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
<div style={{ padding: '20px', textAlign: 'center' }}>
|
||||
<h1>{translate('Audiobooks')}</h1>
|
||||
<p>Audiobook management coming soon.</p>
|
||||
<p>This feature is part of Phase 3 development.</p>
|
||||
</div>
|
||||
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && !!error ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadAudiobooks')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !error && items.length > 0 ? (
|
||||
<Table columns={columns}>
|
||||
<TableBody>
|
||||
{items.map((audiobook) => (
|
||||
<AudiobookIndexRow key={audiobook.id} {...audiobook} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : null}
|
||||
|
||||
{hasNoAudiobooks ? (
|
||||
<div style={{ padding: '20px', textAlign: 'center' }}>
|
||||
<p>{translate('NoAudiobooks')}</p>
|
||||
<p>Add audiobooks to start tracking your library.</p>
|
||||
</div>
|
||||
) : null}
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
|
|
|
|||
43
frontend/src/Audiobook/Index/AudiobookIndexRow.tsx
Normal file
43
frontend/src/Audiobook/Index/AudiobookIndexRow.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import React from 'react';
|
||||
import Audiobook from 'Audiobook/Audiobook';
|
||||
import Icon from 'Components/Icon';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import formatDate from 'Utilities/Date/formatDate';
|
||||
|
||||
function formatDuration(minutes: number | undefined): string {
|
||||
if (!minutes) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${mins}m`;
|
||||
}
|
||||
|
||||
return `${mins}m`;
|
||||
}
|
||||
|
||||
function AudiobookIndexRow(props: Audiobook) {
|
||||
const { title, narrator, durationMinutes, releaseDate, monitored } = props;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell>{title}</TableRowCell>
|
||||
<TableRowCell>{narrator || '-'}</TableRowCell>
|
||||
<TableRowCell>{formatDuration(durationMinutes)}</TableRowCell>
|
||||
<TableRowCell>{releaseDate ? formatDate(releaseDate) : '-'}</TableRowCell>
|
||||
<TableRowCell>
|
||||
<Icon
|
||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
||||
title={monitored ? 'Monitored' : 'Unmonitored'}
|
||||
/>
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default AudiobookIndexRow;
|
||||
28
frontend/src/Book/Book.ts
Normal file
28
frontend/src/Book/Book.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import ModelBase from 'App/ModelBase';
|
||||
|
||||
interface Book extends ModelBase {
|
||||
title: string;
|
||||
sortTitle: string;
|
||||
description: string;
|
||||
foreignBookId: string;
|
||||
isbn: string;
|
||||
isbn13: string;
|
||||
asin: string;
|
||||
pageCount: number;
|
||||
releaseDate: string;
|
||||
publisher: string;
|
||||
language: string;
|
||||
monitored: boolean;
|
||||
qualityProfileId: number;
|
||||
path: string;
|
||||
rootFolderPath: string;
|
||||
added: string;
|
||||
tags: number[];
|
||||
lastSearchTime?: string;
|
||||
authorId?: number;
|
||||
seriesId?: number;
|
||||
seriesPosition?: number;
|
||||
isSaving?: boolean;
|
||||
}
|
||||
|
||||
export default Book;
|
||||
|
|
@ -1,17 +1,100 @@
|
|||
import React from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { fetchBooks } from 'Store/Actions/bookActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import BookIndexRow from './BookIndexRow';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'title',
|
||||
label: () => translate('Title'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'author',
|
||||
label: () => translate('Author'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'publisher',
|
||||
label: () => translate('Publisher'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'releaseDate',
|
||||
label: () => translate('ReleaseDate'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'monitored',
|
||||
label: () => translate('Monitored'),
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
function BookIndex() {
|
||||
const dispatch = useDispatch();
|
||||
const { isFetching, isPopulated, error, items } = useSelector(
|
||||
(state: AppState) => state.books
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchBooks());
|
||||
}, [dispatch]);
|
||||
|
||||
const onRefreshPress = useCallback(() => {
|
||||
dispatch(fetchBooks());
|
||||
}, [dispatch]);
|
||||
|
||||
const hasNoBooks = isPopulated && !items.length;
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Books')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('RefreshAll')}
|
||||
iconName={icons.REFRESH}
|
||||
isSpinning={isFetching}
|
||||
onPress={onRefreshPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
<div style={{ padding: '20px', textAlign: 'center' }}>
|
||||
<h1>{translate('Books')}</h1>
|
||||
<p>Book management coming soon.</p>
|
||||
<p>This feature is part of Phase 3 development.</p>
|
||||
</div>
|
||||
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && !!error ? (
|
||||
<Alert kind={kinds.DANGER}>{translate('UnableToLoadBooks')}</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !error && items.length > 0 ? (
|
||||
<Table columns={columns}>
|
||||
<TableBody>
|
||||
{items.map((book) => (
|
||||
<BookIndexRow key={book.id} {...book} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : null}
|
||||
|
||||
{hasNoBooks ? (
|
||||
<div style={{ padding: '20px', textAlign: 'center' }}>
|
||||
<p>{translate('NoBooks')}</p>
|
||||
<p>Add books to start tracking your library.</p>
|
||||
</div>
|
||||
) : null}
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
|
|
|
|||
28
frontend/src/Book/Index/BookIndexRow.tsx
Normal file
28
frontend/src/Book/Index/BookIndexRow.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import React from 'react';
|
||||
import Book from 'Book/Book';
|
||||
import Icon from 'Components/Icon';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import formatDate from 'Utilities/Date/formatDate';
|
||||
|
||||
function BookIndexRow(props: Book) {
|
||||
const { title, publisher, releaseDate, monitored } = props;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell>{title}</TableRowCell>
|
||||
<TableRowCell>-</TableRowCell>
|
||||
<TableRowCell>{publisher || '-'}</TableRowCell>
|
||||
<TableRowCell>{releaseDate ? formatDate(releaseDate) : '-'}</TableRowCell>
|
||||
<TableRowCell>
|
||||
<Icon
|
||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
||||
title={monitored ? 'Monitored' : 'Unmonitored'}
|
||||
/>
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default BookIndexRow;
|
||||
57
frontend/src/Store/Actions/audiobookActions.js
Normal file
57
frontend/src/Store/Actions/audiobookActions.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { createAction } from 'redux-actions';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createFetchHandler from './Creators/createFetchHandler';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
|
||||
import createSaveProviderHandler from './Creators/createSaveProviderHandler';
|
||||
import createSetSettingValueReducer from './Creators/Reducers/createSetSettingValueReducer';
|
||||
|
||||
export const section = 'audiobooks';
|
||||
|
||||
export const defaultState = {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
isDeleting: false,
|
||||
deleteError: null,
|
||||
items: [],
|
||||
sortKey: 'sortTitle',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
pendingChanges: {}
|
||||
};
|
||||
|
||||
export const FETCH_AUDIOBOOKS = 'audiobooks/fetchAudiobooks';
|
||||
export const SET_AUDIOBOOK_VALUE = 'audiobooks/setAudiobookValue';
|
||||
export const SAVE_AUDIOBOOK = 'audiobooks/saveAudiobook';
|
||||
export const DELETE_AUDIOBOOK = 'audiobooks/deleteAudiobook';
|
||||
|
||||
export const fetchAudiobooks = createThunk(FETCH_AUDIOBOOKS);
|
||||
export const saveAudiobook = createThunk(SAVE_AUDIOBOOK);
|
||||
export const deleteAudiobook = createThunk(DELETE_AUDIOBOOK, (payload) => {
|
||||
return {
|
||||
...payload,
|
||||
queryParams: {
|
||||
deleteFiles: payload.deleteFiles
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export const setAudiobookValue = createAction(SET_AUDIOBOOK_VALUE, (payload) => {
|
||||
return {
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
[FETCH_AUDIOBOOKS]: createFetchHandler(section, '/audiobook'),
|
||||
[SAVE_AUDIOBOOK]: createSaveProviderHandler(section, '/audiobook'),
|
||||
[DELETE_AUDIOBOOK]: createRemoveItemHandler(section, '/audiobook')
|
||||
});
|
||||
|
||||
export const reducers = createHandleActions({
|
||||
[SET_AUDIOBOOK_VALUE]: createSetSettingValueReducer(section)
|
||||
}, defaultState, section);
|
||||
57
frontend/src/Store/Actions/bookActions.js
Normal file
57
frontend/src/Store/Actions/bookActions.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { createAction } from 'redux-actions';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createFetchHandler from './Creators/createFetchHandler';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
|
||||
import createSaveProviderHandler from './Creators/createSaveProviderHandler';
|
||||
import createSetSettingValueReducer from './Creators/Reducers/createSetSettingValueReducer';
|
||||
|
||||
export const section = 'books';
|
||||
|
||||
export const defaultState = {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
isDeleting: false,
|
||||
deleteError: null,
|
||||
items: [],
|
||||
sortKey: 'sortTitle',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
pendingChanges: {}
|
||||
};
|
||||
|
||||
export const FETCH_BOOKS = 'books/fetchBooks';
|
||||
export const SET_BOOK_VALUE = 'books/setBookValue';
|
||||
export const SAVE_BOOK = 'books/saveBook';
|
||||
export const DELETE_BOOK = 'books/deleteBook';
|
||||
|
||||
export const fetchBooks = createThunk(FETCH_BOOKS);
|
||||
export const saveBook = createThunk(SAVE_BOOK);
|
||||
export const deleteBook = createThunk(DELETE_BOOK, (payload) => {
|
||||
return {
|
||||
...payload,
|
||||
queryParams: {
|
||||
deleteFiles: payload.deleteFiles
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export const setBookValue = createAction(SET_BOOK_VALUE, (payload) => {
|
||||
return {
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
[FETCH_BOOKS]: createFetchHandler(section, '/book'),
|
||||
[SAVE_BOOK]: createSaveProviderHandler(section, '/book'),
|
||||
[DELETE_BOOK]: createRemoveItemHandler(section, '/book')
|
||||
});
|
||||
|
||||
export const reducers = createHandleActions({
|
||||
[SET_BOOK_VALUE]: createSetSettingValueReducer(section)
|
||||
}, defaultState, section);
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import * as addMovie from './addMovieActions';
|
||||
import * as app from './appActions';
|
||||
import * as audiobooks from './audiobookActions';
|
||||
import * as blocklist from './blocklistActions';
|
||||
import * as books from './bookActions';
|
||||
import * as calendar from './calendarActions';
|
||||
import * as captcha from './captchaActions';
|
||||
import * as commands from './commandActions';
|
||||
|
|
@ -33,7 +35,9 @@ import * as wanted from './wantedActions';
|
|||
export default [
|
||||
addMovie,
|
||||
app,
|
||||
audiobooks,
|
||||
blocklist,
|
||||
books,
|
||||
calendar,
|
||||
captcha,
|
||||
commands,
|
||||
|
|
|
|||
124
src/NzbDrone.Core/Audiobooks/AddAudiobookService.cs
Normal file
124
src/NzbDrone.Core/Audiobooks/AddAudiobookService.cs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using FluentValidation;
|
||||
using NLog;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Organizer;
|
||||
|
||||
namespace NzbDrone.Core.Audiobooks
|
||||
{
|
||||
public interface IAddAudiobookService
|
||||
{
|
||||
Audiobook AddAudiobook(Audiobook newAudiobook);
|
||||
List<Audiobook> AddAudiobooks(List<Audiobook> newAudiobooks, bool ignoreErrors = false);
|
||||
}
|
||||
|
||||
public class AddAudiobookService : IAddAudiobookService
|
||||
{
|
||||
private readonly IAudiobookService _audiobookService;
|
||||
private readonly IBuildFileNames _fileNameBuilder;
|
||||
private readonly IAddAudiobookValidator _addAudiobookValidator;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public AddAudiobookService(IAudiobookService audiobookService,
|
||||
IBuildFileNames fileNameBuilder,
|
||||
IAddAudiobookValidator addAudiobookValidator,
|
||||
Logger logger)
|
||||
{
|
||||
_audiobookService = audiobookService;
|
||||
_fileNameBuilder = fileNameBuilder;
|
||||
_addAudiobookValidator = addAudiobookValidator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Audiobook AddAudiobook(Audiobook newAudiobook)
|
||||
{
|
||||
Ensure.That(newAudiobook, () => newAudiobook).IsNotNull();
|
||||
|
||||
newAudiobook = SetPropertiesAndValidate(newAudiobook);
|
||||
|
||||
_logger.Info("Adding Audiobook {0} Path: [{1}]", newAudiobook, newAudiobook.Path.SanitizeForLog());
|
||||
|
||||
_audiobookService.AddAudiobook(newAudiobook);
|
||||
|
||||
return newAudiobook;
|
||||
}
|
||||
|
||||
public List<Audiobook> AddAudiobooks(List<Audiobook> newAudiobooks, bool ignoreErrors = false)
|
||||
{
|
||||
var added = DateTime.UtcNow;
|
||||
var audiobooksToAdd = new List<Audiobook>();
|
||||
|
||||
foreach (var a in newAudiobooks)
|
||||
{
|
||||
if (a.Path.IsNullOrWhiteSpace())
|
||||
{
|
||||
_logger.Info("Adding Audiobook {0} Root Folder Path: [{1}]", a, a.RootFolderPath.SanitizeForLog());
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Info("Adding Audiobook {0} Path: [{1}]", a, a.Path.SanitizeForLog());
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var audiobook = SetPropertiesAndValidate(a);
|
||||
audiobook.Added = added;
|
||||
audiobooksToAdd.Add(audiobook);
|
||||
}
|
||||
catch (ValidationException ex)
|
||||
{
|
||||
if (!ignoreErrors)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
_logger.Debug("Audiobook {0} was not added due to validation failures. {1}", a.Title, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return _audiobookService.AddAudiobooks(audiobooksToAdd);
|
||||
}
|
||||
|
||||
private Audiobook SetPropertiesAndValidate(Audiobook newAudiobook)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(newAudiobook.Path))
|
||||
{
|
||||
var folderName = GetAudiobookFolder(newAudiobook);
|
||||
newAudiobook.Path = Path.Combine(newAudiobook.RootFolderPath, folderName);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(newAudiobook.SortTitle))
|
||||
{
|
||||
newAudiobook.SortTitle = newAudiobook.Title?.ToLowerInvariant();
|
||||
}
|
||||
|
||||
newAudiobook.Added = DateTime.UtcNow;
|
||||
|
||||
var validationResult = _addAudiobookValidator.Validate(newAudiobook);
|
||||
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
return newAudiobook;
|
||||
}
|
||||
|
||||
private string GetAudiobookFolder(Audiobook audiobook)
|
||||
{
|
||||
var title = audiobook.Title ?? "Unknown";
|
||||
var year = audiobook.ReleaseDate?.Year.ToString() ?? "Unknown";
|
||||
var narrator = audiobook.Narrator;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(narrator))
|
||||
{
|
||||
return FileNameBuilder.CleanFileName($"{title} ({year}) [{narrator}]");
|
||||
}
|
||||
|
||||
return FileNameBuilder.CleanFileName($"{title} ({year})");
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/NzbDrone.Core/Audiobooks/AddAudiobookValidator.cs
Normal file
25
src/NzbDrone.Core/Audiobooks/AddAudiobookValidator.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
|
||||
namespace NzbDrone.Core.Audiobooks
|
||||
{
|
||||
public interface IAddAudiobookValidator
|
||||
{
|
||||
ValidationResult Validate(Audiobook instance);
|
||||
}
|
||||
|
||||
public class AddAudiobookValidator : AbstractValidator<Audiobook>, IAddAudiobookValidator
|
||||
{
|
||||
public AddAudiobookValidator(RootFolderValidator rootFolderValidator,
|
||||
RecycleBinValidator recycleBinValidator)
|
||||
{
|
||||
RuleFor(c => c.Path).Cascade(CascadeMode.Stop)
|
||||
.IsValidPath()
|
||||
.SetValidator(rootFolderValidator)
|
||||
.SetValidator(recycleBinValidator);
|
||||
|
||||
RuleFor(c => c.Title).NotEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/NzbDrone.Core/Authors/AuthorRepository.cs
Normal file
43
src/NzbDrone.Core/Authors/AuthorRepository.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Authors
|
||||
{
|
||||
public interface IAuthorRepository : IBasicRepository<Author>
|
||||
{
|
||||
Author FindByName(string name);
|
||||
Author FindByForeignId(string foreignAuthorId);
|
||||
List<Author> GetMonitored();
|
||||
bool AuthorPathExists(string path);
|
||||
}
|
||||
|
||||
public class AuthorRepository : BasicRepository<Author>, IAuthorRepository
|
||||
{
|
||||
public AuthorRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
|
||||
public Author FindByName(string name)
|
||||
{
|
||||
return Query(a => a.Name == name).FirstOrDefault();
|
||||
}
|
||||
|
||||
public Author FindByForeignId(string foreignAuthorId)
|
||||
{
|
||||
return Query(a => a.ForeignAuthorId == foreignAuthorId).FirstOrDefault();
|
||||
}
|
||||
|
||||
public List<Author> GetMonitored()
|
||||
{
|
||||
return Query(a => a.Monitored);
|
||||
}
|
||||
|
||||
public bool AuthorPathExists(string path)
|
||||
{
|
||||
return Query(a => a.Path == path).Any();
|
||||
}
|
||||
}
|
||||
}
|
||||
115
src/NzbDrone.Core/Authors/AuthorService.cs
Normal file
115
src/NzbDrone.Core/Authors/AuthorService.cs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Authors
|
||||
{
|
||||
public interface IAuthorService
|
||||
{
|
||||
Author GetAuthor(int authorId);
|
||||
List<Author> GetAuthors(IEnumerable<int> authorIds);
|
||||
Author AddAuthor(Author newAuthor);
|
||||
List<Author> AddAuthors(List<Author> newAuthors);
|
||||
Author FindByName(string name);
|
||||
Author FindByForeignId(string foreignAuthorId);
|
||||
void DeleteAuthor(int authorId, bool deleteFiles);
|
||||
void DeleteAuthors(List<int> authorIds, bool deleteFiles);
|
||||
List<Author> GetAllAuthors();
|
||||
List<Author> GetMonitoredAuthors();
|
||||
Author UpdateAuthor(Author author);
|
||||
List<Author> UpdateAuthors(List<Author> authors);
|
||||
bool AuthorPathExists(string path);
|
||||
}
|
||||
|
||||
public class AuthorService : IAuthorService
|
||||
{
|
||||
private readonly IAuthorRepository _authorRepository;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public AuthorService(IAuthorRepository authorRepository,
|
||||
IEventAggregator eventAggregator,
|
||||
Logger logger)
|
||||
{
|
||||
_authorRepository = authorRepository;
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Author GetAuthor(int authorId)
|
||||
{
|
||||
return _authorRepository.Get(authorId);
|
||||
}
|
||||
|
||||
public List<Author> GetAuthors(IEnumerable<int> authorIds)
|
||||
{
|
||||
return _authorRepository.Get(authorIds).ToList();
|
||||
}
|
||||
|
||||
public Author AddAuthor(Author newAuthor)
|
||||
{
|
||||
newAuthor.Added = DateTime.UtcNow;
|
||||
return _authorRepository.Insert(newAuthor);
|
||||
}
|
||||
|
||||
public List<Author> AddAuthors(List<Author> newAuthors)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
foreach (var author in newAuthors)
|
||||
{
|
||||
author.Added = now;
|
||||
}
|
||||
|
||||
_authorRepository.InsertMany(newAuthors);
|
||||
return newAuthors;
|
||||
}
|
||||
|
||||
public Author FindByName(string name)
|
||||
{
|
||||
return _authorRepository.FindByName(name);
|
||||
}
|
||||
|
||||
public Author FindByForeignId(string foreignAuthorId)
|
||||
{
|
||||
return _authorRepository.FindByForeignId(foreignAuthorId);
|
||||
}
|
||||
|
||||
public void DeleteAuthor(int authorId, bool deleteFiles)
|
||||
{
|
||||
_authorRepository.Delete(authorId);
|
||||
}
|
||||
|
||||
public void DeleteAuthors(List<int> authorIds, bool deleteFiles)
|
||||
{
|
||||
_authorRepository.DeleteMany(authorIds);
|
||||
}
|
||||
|
||||
public List<Author> GetAllAuthors()
|
||||
{
|
||||
return _authorRepository.All().ToList();
|
||||
}
|
||||
|
||||
public List<Author> GetMonitoredAuthors()
|
||||
{
|
||||
return _authorRepository.GetMonitored();
|
||||
}
|
||||
|
||||
public Author UpdateAuthor(Author author)
|
||||
{
|
||||
return _authorRepository.Update(author);
|
||||
}
|
||||
|
||||
public List<Author> UpdateAuthors(List<Author> authors)
|
||||
{
|
||||
_authorRepository.UpdateMany(authors);
|
||||
return authors;
|
||||
}
|
||||
|
||||
public bool AuthorPathExists(string path)
|
||||
{
|
||||
return _authorRepository.AuthorPathExists(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
117
src/NzbDrone.Core/Books/AddBookService.cs
Normal file
117
src/NzbDrone.Core/Books/AddBookService.cs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using FluentValidation;
|
||||
using NLog;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Organizer;
|
||||
|
||||
namespace NzbDrone.Core.Books
|
||||
{
|
||||
public interface IAddBookService
|
||||
{
|
||||
Book AddBook(Book newBook);
|
||||
List<Book> AddBooks(List<Book> newBooks, bool ignoreErrors = false);
|
||||
}
|
||||
|
||||
public class AddBookService : IAddBookService
|
||||
{
|
||||
private readonly IBookService _bookService;
|
||||
private readonly IBuildFileNames _fileNameBuilder;
|
||||
private readonly IAddBookValidator _addBookValidator;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public AddBookService(IBookService bookService,
|
||||
IBuildFileNames fileNameBuilder,
|
||||
IAddBookValidator addBookValidator,
|
||||
Logger logger)
|
||||
{
|
||||
_bookService = bookService;
|
||||
_fileNameBuilder = fileNameBuilder;
|
||||
_addBookValidator = addBookValidator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Book AddBook(Book newBook)
|
||||
{
|
||||
Ensure.That(newBook, () => newBook).IsNotNull();
|
||||
|
||||
newBook = SetPropertiesAndValidate(newBook);
|
||||
|
||||
_logger.Info("Adding Book {0} Path: [{1}]", newBook, newBook.Path.SanitizeForLog());
|
||||
|
||||
_bookService.AddBook(newBook);
|
||||
|
||||
return newBook;
|
||||
}
|
||||
|
||||
public List<Book> AddBooks(List<Book> newBooks, bool ignoreErrors = false)
|
||||
{
|
||||
var added = DateTime.UtcNow;
|
||||
var booksToAdd = new List<Book>();
|
||||
|
||||
foreach (var b in newBooks)
|
||||
{
|
||||
if (b.Path.IsNullOrWhiteSpace())
|
||||
{
|
||||
_logger.Info("Adding Book {0} Root Folder Path: [{1}]", b, b.RootFolderPath.SanitizeForLog());
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Info("Adding Book {0} Path: [{1}]", b, b.Path.SanitizeForLog());
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var book = SetPropertiesAndValidate(b);
|
||||
book.Added = added;
|
||||
booksToAdd.Add(book);
|
||||
}
|
||||
catch (ValidationException ex)
|
||||
{
|
||||
if (!ignoreErrors)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
_logger.Debug("Book {0} was not added due to validation failures. {1}", b.Title, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return _bookService.AddBooks(booksToAdd);
|
||||
}
|
||||
|
||||
private Book SetPropertiesAndValidate(Book newBook)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(newBook.Path))
|
||||
{
|
||||
var folderName = GetBookFolder(newBook);
|
||||
newBook.Path = Path.Combine(newBook.RootFolderPath, folderName);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(newBook.SortTitle))
|
||||
{
|
||||
newBook.SortTitle = newBook.Title?.ToLowerInvariant();
|
||||
}
|
||||
|
||||
newBook.Added = DateTime.UtcNow;
|
||||
|
||||
var validationResult = _addBookValidator.Validate(newBook);
|
||||
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
|
||||
return newBook;
|
||||
}
|
||||
|
||||
private string GetBookFolder(Book book)
|
||||
{
|
||||
var title = book.Title ?? "Unknown";
|
||||
var year = book.ReleaseDate?.Year.ToString() ?? "Unknown";
|
||||
return FileNameBuilder.CleanFileName($"{title} ({year})");
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/NzbDrone.Core/Books/AddBookValidator.cs
Normal file
25
src/NzbDrone.Core/Books/AddBookValidator.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
|
||||
namespace NzbDrone.Core.Books
|
||||
{
|
||||
public interface IAddBookValidator
|
||||
{
|
||||
ValidationResult Validate(Book instance);
|
||||
}
|
||||
|
||||
public class AddBookValidator : AbstractValidator<Book>, IAddBookValidator
|
||||
{
|
||||
public AddBookValidator(RootFolderValidator rootFolderValidator,
|
||||
RecycleBinValidator recycleBinValidator)
|
||||
{
|
||||
RuleFor(c => c.Path).Cascade(CascadeMode.Stop)
|
||||
.IsValidPath()
|
||||
.SetValidator(rootFolderValidator)
|
||||
.SetValidator(recycleBinValidator);
|
||||
|
||||
RuleFor(c => c.Title).NotEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(247)]
|
||||
public class add_book_audiobook_files_tables : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Create.TableForModel("BookFiles")
|
||||
.WithColumn("BookId").AsInt32().NotNullable()
|
||||
.WithColumn("RelativePath").AsString().Nullable()
|
||||
.WithColumn("Size").AsInt64().WithDefaultValue(0)
|
||||
.WithColumn("DateAdded").AsDateTime().WithDefaultValue(System.DateTime.UtcNow)
|
||||
.WithColumn("OriginalFilePath").AsString().Nullable()
|
||||
.WithColumn("SceneName").AsString().Nullable()
|
||||
.WithColumn("ReleaseGroup").AsString().Nullable()
|
||||
.WithColumn("Quality").AsString().WithDefaultValue("{}")
|
||||
.WithColumn("Format").AsString().Nullable();
|
||||
|
||||
Create.TableForModel("AudiobookFiles")
|
||||
.WithColumn("AudiobookId").AsInt32().NotNullable()
|
||||
.WithColumn("RelativePath").AsString().Nullable()
|
||||
.WithColumn("Size").AsInt64().WithDefaultValue(0)
|
||||
.WithColumn("DateAdded").AsDateTime().WithDefaultValue(System.DateTime.UtcNow)
|
||||
.WithColumn("OriginalFilePath").AsString().Nullable()
|
||||
.WithColumn("SceneName").AsString().Nullable()
|
||||
.WithColumn("ReleaseGroup").AsString().Nullable()
|
||||
.WithColumn("Quality").AsString().WithDefaultValue("{}")
|
||||
.WithColumn("Format").AsString().Nullable()
|
||||
.WithColumn("DurationSeconds").AsInt32().Nullable()
|
||||
.WithColumn("Bitrate").AsInt32().Nullable()
|
||||
.WithColumn("SampleRate").AsInt32().Nullable()
|
||||
.WithColumn("Channels").AsInt32().Nullable();
|
||||
|
||||
Create.Index("IX_BookFiles_BookId").OnTable("BookFiles").OnColumn("BookId");
|
||||
Create.Index("IX_AudiobookFiles_AudiobookId").OnTable("AudiobookFiles").OnColumn("AudiobookId");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -148,6 +148,12 @@ public static void Map()
|
|||
|
||||
Mapper.Entity<Audiobook>("Audiobooks").RegisterModel();
|
||||
|
||||
Mapper.Entity<BookFile>("BookFiles").RegisterModel()
|
||||
.Ignore(f => f.Path);
|
||||
|
||||
Mapper.Entity<AudiobookFile>("AudiobookFiles").RegisterModel()
|
||||
.Ignore(f => f.Path);
|
||||
|
||||
Mapper.Entity<QualityDefinition>("QualityDefinitions").RegisterModel()
|
||||
.Ignore(d => d.GroupName)
|
||||
.Ignore(d => d.Weight);
|
||||
|
|
|
|||
52
src/NzbDrone.Core/MediaFiles/AudiobookFile.cs
Normal file
52
src/NzbDrone.Core/MediaFiles/AudiobookFile.cs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
using System;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Audiobooks;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
namespace NzbDrone.Core.MediaFiles
|
||||
{
|
||||
public class AudiobookFile : ModelBase
|
||||
{
|
||||
public int AudiobookId { get; set; }
|
||||
public string RelativePath { get; set; }
|
||||
public string Path { get; set; }
|
||||
public long Size { get; set; }
|
||||
public DateTime DateAdded { get; set; }
|
||||
public string OriginalFilePath { get; set; }
|
||||
public string SceneName { get; set; }
|
||||
public string ReleaseGroup { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public string Format { get; set; }
|
||||
public int? DurationSeconds { get; set; }
|
||||
public int? Bitrate { get; set; }
|
||||
public int? SampleRate { get; set; }
|
||||
public int? Channels { get; set; }
|
||||
public Audiobook Audiobook { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[{0}] {1}", Id, RelativePath);
|
||||
}
|
||||
|
||||
public string GetSceneOrFileName()
|
||||
{
|
||||
if (SceneName.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return SceneName;
|
||||
}
|
||||
|
||||
if (RelativePath.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return System.IO.Path.GetFileNameWithoutExtension(RelativePath);
|
||||
}
|
||||
|
||||
if (Path.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return System.IO.Path.GetFileNameWithoutExtension(Path);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/NzbDrone.Core/MediaFiles/AudiobookFileRepository.cs
Normal file
43
src/NzbDrone.Core/MediaFiles/AudiobookFileRepository.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.MediaFiles
|
||||
{
|
||||
public interface IAudiobookFileRepository : IBasicRepository<AudiobookFile>
|
||||
{
|
||||
List<AudiobookFile> GetFilesByAudiobook(int audiobookId);
|
||||
List<AudiobookFile> GetFilesByAudiobooks(IEnumerable<int> audiobookIds);
|
||||
void DeleteForAudiobooks(List<int> audiobookIds);
|
||||
List<AudiobookFile> GetFilesWithRelativePath(int audiobookId, string relativePath);
|
||||
}
|
||||
|
||||
public class AudiobookFileRepository : BasicRepository<AudiobookFile>, IAudiobookFileRepository
|
||||
{
|
||||
public AudiobookFileRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
|
||||
public List<AudiobookFile> GetFilesByAudiobook(int audiobookId)
|
||||
{
|
||||
return Query(x => x.AudiobookId == audiobookId);
|
||||
}
|
||||
|
||||
public List<AudiobookFile> GetFilesByAudiobooks(IEnumerable<int> audiobookIds)
|
||||
{
|
||||
return Query(x => audiobookIds.Contains(x.AudiobookId));
|
||||
}
|
||||
|
||||
public void DeleteForAudiobooks(List<int> audiobookIds)
|
||||
{
|
||||
Delete(x => audiobookIds.Contains(x.AudiobookId));
|
||||
}
|
||||
|
||||
public List<AudiobookFile> GetFilesWithRelativePath(int audiobookId, string relativePath)
|
||||
{
|
||||
return Query(c => c.AudiobookId == audiobookId && c.RelativePath == relativePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/NzbDrone.Core/MediaFiles/BookFile.cs
Normal file
48
src/NzbDrone.Core/MediaFiles/BookFile.cs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
using System;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
namespace NzbDrone.Core.MediaFiles
|
||||
{
|
||||
public class BookFile : ModelBase
|
||||
{
|
||||
public int BookId { get; set; }
|
||||
public string RelativePath { get; set; }
|
||||
public string Path { get; set; }
|
||||
public long Size { get; set; }
|
||||
public DateTime DateAdded { get; set; }
|
||||
public string OriginalFilePath { get; set; }
|
||||
public string SceneName { get; set; }
|
||||
public string ReleaseGroup { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public string Format { get; set; }
|
||||
public Book Book { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[{0}] {1}", Id, RelativePath);
|
||||
}
|
||||
|
||||
public string GetSceneOrFileName()
|
||||
{
|
||||
if (SceneName.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return SceneName;
|
||||
}
|
||||
|
||||
if (RelativePath.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return System.IO.Path.GetFileNameWithoutExtension(RelativePath);
|
||||
}
|
||||
|
||||
if (Path.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return System.IO.Path.GetFileNameWithoutExtension(Path);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/NzbDrone.Core/MediaFiles/BookFileRepository.cs
Normal file
43
src/NzbDrone.Core/MediaFiles/BookFileRepository.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.MediaFiles
|
||||
{
|
||||
public interface IBookFileRepository : IBasicRepository<BookFile>
|
||||
{
|
||||
List<BookFile> GetFilesByBook(int bookId);
|
||||
List<BookFile> GetFilesByBooks(IEnumerable<int> bookIds);
|
||||
void DeleteForBooks(List<int> bookIds);
|
||||
List<BookFile> GetFilesWithRelativePath(int bookId, string relativePath);
|
||||
}
|
||||
|
||||
public class BookFileRepository : BasicRepository<BookFile>, IBookFileRepository
|
||||
{
|
||||
public BookFileRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
|
||||
public List<BookFile> GetFilesByBook(int bookId)
|
||||
{
|
||||
return Query(x => x.BookId == bookId);
|
||||
}
|
||||
|
||||
public List<BookFile> GetFilesByBooks(IEnumerable<int> bookIds)
|
||||
{
|
||||
return Query(x => bookIds.Contains(x.BookId));
|
||||
}
|
||||
|
||||
public void DeleteForBooks(List<int> bookIds)
|
||||
{
|
||||
Delete(x => bookIds.Contains(x.BookId));
|
||||
}
|
||||
|
||||
public List<BookFile> GetFilesWithRelativePath(int bookId, string relativePath)
|
||||
{
|
||||
return Query(c => c.BookId == bookId && c.RelativePath == relativePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NLog;
|
||||
|
||||
namespace NzbDrone.Core.MetadataSource.Audiobook
|
||||
{
|
||||
public class AudiobookInfoProxy : IProvideAudiobookInfo
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
|
||||
public AudiobookInfoProxy(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public AudiobookMetadata GetByExternalId(string externalId)
|
||||
{
|
||||
_logger.Debug("GetByExternalId called for: {0} (stub implementation)", externalId);
|
||||
return null;
|
||||
}
|
||||
|
||||
public AudiobookMetadata GetById(int providerId)
|
||||
{
|
||||
_logger.Debug("GetById called for: {0} (stub implementation)", providerId);
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<AudiobookMetadata> GetBulkInfo(List<int> providerIds)
|
||||
{
|
||||
_logger.Debug("GetBulkInfo called for {0} IDs (stub implementation)", providerIds.Count);
|
||||
return new List<AudiobookMetadata>();
|
||||
}
|
||||
|
||||
public List<AudiobookMetadata> GetTrending()
|
||||
{
|
||||
_logger.Debug("GetTrending called (stub implementation)");
|
||||
return new List<AudiobookMetadata>();
|
||||
}
|
||||
|
||||
public List<AudiobookMetadata> GetPopular()
|
||||
{
|
||||
_logger.Debug("GetPopular called (stub implementation)");
|
||||
return new List<AudiobookMetadata>();
|
||||
}
|
||||
|
||||
public HashSet<int> GetChangedItems(DateTime startTime)
|
||||
{
|
||||
_logger.Debug("GetChangedItems called since {0} (stub implementation)", startTime);
|
||||
return new HashSet<int>();
|
||||
}
|
||||
|
||||
public List<AudiobookMetadata> SearchByTitle(string title)
|
||||
{
|
||||
_logger.Debug("SearchByTitle called for: {0} (stub implementation)", title);
|
||||
return new List<AudiobookMetadata>();
|
||||
}
|
||||
|
||||
public List<AudiobookMetadata> SearchByTitle(string title, int year)
|
||||
{
|
||||
_logger.Debug("SearchByTitle called for: {0} ({1}) (stub implementation)", title, year);
|
||||
return new List<AudiobookMetadata>();
|
||||
}
|
||||
|
||||
public AudiobookMetadata GetByIsbn(string isbn)
|
||||
{
|
||||
_logger.Debug("GetByIsbn called for: {0} (stub implementation)", isbn);
|
||||
return null;
|
||||
}
|
||||
|
||||
public AudiobookMetadata GetByAsin(string asin)
|
||||
{
|
||||
_logger.Debug("GetByAsin called for: {0} (stub implementation)", asin);
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<AudiobookMetadata> GetByNarrator(string narratorName)
|
||||
{
|
||||
_logger.Debug("GetByNarrator called for: {0} (stub implementation)", narratorName);
|
||||
return new List<AudiobookMetadata>();
|
||||
}
|
||||
|
||||
public List<AudiobookMetadata> GetByAuthor(string authorName)
|
||||
{
|
||||
_logger.Debug("GetByAuthor called for: {0} (stub implementation)", authorName);
|
||||
return new List<AudiobookMetadata>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.MetadataSource.Audiobook
|
||||
{
|
||||
public interface IProvideAudiobookInfo : IProvideMediaInfo<AudiobookMetadata>, ISearchableMediaProvider<AudiobookMetadata>
|
||||
{
|
||||
AudiobookMetadata GetByIsbn(string isbn);
|
||||
AudiobookMetadata GetByAsin(string asin);
|
||||
List<AudiobookMetadata> GetByNarrator(string narratorName);
|
||||
List<AudiobookMetadata> GetByAuthor(string authorName);
|
||||
}
|
||||
|
||||
public class AudiobookMetadata
|
||||
{
|
||||
public string ForeignAudiobookId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string SortTitle { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Isbn { get; set; }
|
||||
public string Isbn13 { get; set; }
|
||||
public string Asin { get; set; }
|
||||
public DateTime? ReleaseDate { get; set; }
|
||||
public string Publisher { get; set; }
|
||||
public string Language { get; set; }
|
||||
public string Narrator { get; set; }
|
||||
public List<string> Narrators { get; set; }
|
||||
public int? DurationMinutes { get; set; }
|
||||
public bool IsAbridged { get; set; }
|
||||
public List<string> Authors { get; set; }
|
||||
public List<string> Genres { get; set; }
|
||||
public string CoverUrl { get; set; }
|
||||
public double? Rating { get; set; }
|
||||
public int? RatingsCount { get; set; }
|
||||
public int? BookId { get; set; }
|
||||
}
|
||||
}
|
||||
88
src/NzbDrone.Core/MetadataSource/Book/BookInfoProxy.cs
Normal file
88
src/NzbDrone.Core/MetadataSource/Book/BookInfoProxy.cs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NLog;
|
||||
|
||||
namespace NzbDrone.Core.MetadataSource.Book
|
||||
{
|
||||
public class BookInfoProxy : IProvideBookInfo
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
|
||||
public BookInfoProxy(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public BookMetadata GetByExternalId(string externalId)
|
||||
{
|
||||
_logger.Debug("GetByExternalId called for: {0} (stub implementation)", externalId);
|
||||
return null;
|
||||
}
|
||||
|
||||
public BookMetadata GetById(int providerId)
|
||||
{
|
||||
_logger.Debug("GetById called for: {0} (stub implementation)", providerId);
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<BookMetadata> GetBulkInfo(List<int> providerIds)
|
||||
{
|
||||
_logger.Debug("GetBulkInfo called for {0} IDs (stub implementation)", providerIds.Count);
|
||||
return new List<BookMetadata>();
|
||||
}
|
||||
|
||||
public List<BookMetadata> GetTrending()
|
||||
{
|
||||
_logger.Debug("GetTrending called (stub implementation)");
|
||||
return new List<BookMetadata>();
|
||||
}
|
||||
|
||||
public List<BookMetadata> GetPopular()
|
||||
{
|
||||
_logger.Debug("GetPopular called (stub implementation)");
|
||||
return new List<BookMetadata>();
|
||||
}
|
||||
|
||||
public HashSet<int> GetChangedItems(DateTime startTime)
|
||||
{
|
||||
_logger.Debug("GetChangedItems called since {0} (stub implementation)", startTime);
|
||||
return new HashSet<int>();
|
||||
}
|
||||
|
||||
public List<BookMetadata> SearchByTitle(string title)
|
||||
{
|
||||
_logger.Debug("SearchByTitle called for: {0} (stub implementation)", title);
|
||||
return new List<BookMetadata>();
|
||||
}
|
||||
|
||||
public List<BookMetadata> SearchByTitle(string title, int year)
|
||||
{
|
||||
_logger.Debug("SearchByTitle called for: {0} ({1}) (stub implementation)", title, year);
|
||||
return new List<BookMetadata>();
|
||||
}
|
||||
|
||||
public BookMetadata GetByIsbn(string isbn)
|
||||
{
|
||||
_logger.Debug("GetByIsbn called for: {0} (stub implementation)", isbn);
|
||||
return null;
|
||||
}
|
||||
|
||||
public BookMetadata GetByIsbn13(string isbn13)
|
||||
{
|
||||
_logger.Debug("GetByIsbn13 called for: {0} (stub implementation)", isbn13);
|
||||
return null;
|
||||
}
|
||||
|
||||
public BookMetadata GetByAsin(string asin)
|
||||
{
|
||||
_logger.Debug("GetByAsin called for: {0} (stub implementation)", asin);
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<BookMetadata> GetByAuthor(string authorName)
|
||||
{
|
||||
_logger.Debug("GetByAuthor called for: {0} (stub implementation)", authorName);
|
||||
return new List<BookMetadata>();
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/NzbDrone.Core/MetadataSource/Book/IProvideBookInfo.cs
Normal file
33
src/NzbDrone.Core/MetadataSource/Book/IProvideBookInfo.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.MetadataSource.Book
|
||||
{
|
||||
public interface IProvideBookInfo : IProvideMediaInfo<BookMetadata>, ISearchableMediaProvider<BookMetadata>
|
||||
{
|
||||
BookMetadata GetByIsbn(string isbn);
|
||||
BookMetadata GetByIsbn13(string isbn13);
|
||||
BookMetadata GetByAsin(string asin);
|
||||
List<BookMetadata> GetByAuthor(string authorName);
|
||||
}
|
||||
|
||||
public class BookMetadata
|
||||
{
|
||||
public string ForeignBookId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string SortTitle { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Isbn { get; set; }
|
||||
public string Isbn13 { get; set; }
|
||||
public string Asin { get; set; }
|
||||
public int? PageCount { get; set; }
|
||||
public DateTime? ReleaseDate { get; set; }
|
||||
public string Publisher { get; set; }
|
||||
public string Language { get; set; }
|
||||
public List<string> Authors { get; set; }
|
||||
public List<string> Genres { get; set; }
|
||||
public string CoverUrl { get; set; }
|
||||
public double? Rating { get; set; }
|
||||
public int? RatingsCount { get; set; }
|
||||
}
|
||||
}
|
||||
43
src/NzbDrone.Core/Series/SeriesRepository.cs
Normal file
43
src/NzbDrone.Core/Series/SeriesRepository.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Series
|
||||
{
|
||||
public interface ISeriesRepository : IBasicRepository<Series>
|
||||
{
|
||||
Series FindByTitle(string title);
|
||||
Series FindByForeignId(string foreignSeriesId);
|
||||
List<Series> FindByAuthorId(int authorId);
|
||||
List<Series> GetMonitored();
|
||||
}
|
||||
|
||||
public class SeriesRepository : BasicRepository<Series>, ISeriesRepository
|
||||
{
|
||||
public SeriesRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
|
||||
public Series FindByTitle(string title)
|
||||
{
|
||||
return Query(s => s.Title == title).FirstOrDefault();
|
||||
}
|
||||
|
||||
public Series FindByForeignId(string foreignSeriesId)
|
||||
{
|
||||
return Query(s => s.ForeignSeriesId == foreignSeriesId).FirstOrDefault();
|
||||
}
|
||||
|
||||
public List<Series> FindByAuthorId(int authorId)
|
||||
{
|
||||
return Query(s => s.AuthorId == authorId);
|
||||
}
|
||||
|
||||
public List<Series> GetMonitored()
|
||||
{
|
||||
return Query(s => s.Monitored);
|
||||
}
|
||||
}
|
||||
}
|
||||
107
src/NzbDrone.Core/Series/SeriesService.cs
Normal file
107
src/NzbDrone.Core/Series/SeriesService.cs
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Series
|
||||
{
|
||||
public interface ISeriesService
|
||||
{
|
||||
Series GetSeries(int seriesId);
|
||||
List<Series> GetSeriesItems(IEnumerable<int> seriesIds);
|
||||
Series AddSeries(Series newSeries);
|
||||
List<Series> AddMultipleSeries(List<Series> newSeries);
|
||||
Series FindByTitle(string title);
|
||||
Series FindByForeignId(string foreignSeriesId);
|
||||
List<Series> FindByAuthorId(int authorId);
|
||||
void DeleteSeries(int seriesId);
|
||||
void DeleteMultipleSeries(List<int> seriesIds);
|
||||
List<Series> GetAllSeries();
|
||||
List<Series> GetMonitoredSeries();
|
||||
Series UpdateSeries(Series series);
|
||||
List<Series> UpdateMultipleSeries(List<Series> series);
|
||||
}
|
||||
|
||||
public class SeriesService : ISeriesService
|
||||
{
|
||||
private readonly ISeriesRepository _seriesRepository;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public SeriesService(ISeriesRepository seriesRepository,
|
||||
IEventAggregator eventAggregator,
|
||||
Logger logger)
|
||||
{
|
||||
_seriesRepository = seriesRepository;
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Series GetSeries(int seriesId)
|
||||
{
|
||||
return _seriesRepository.Get(seriesId);
|
||||
}
|
||||
|
||||
public List<Series> GetSeriesItems(IEnumerable<int> seriesIds)
|
||||
{
|
||||
return _seriesRepository.Get(seriesIds).ToList();
|
||||
}
|
||||
|
||||
public Series AddSeries(Series newSeries)
|
||||
{
|
||||
return _seriesRepository.Insert(newSeries);
|
||||
}
|
||||
|
||||
public List<Series> AddMultipleSeries(List<Series> newSeries)
|
||||
{
|
||||
_seriesRepository.InsertMany(newSeries);
|
||||
return newSeries;
|
||||
}
|
||||
|
||||
public Series FindByTitle(string title)
|
||||
{
|
||||
return _seriesRepository.FindByTitle(title);
|
||||
}
|
||||
|
||||
public Series FindByForeignId(string foreignSeriesId)
|
||||
{
|
||||
return _seriesRepository.FindByForeignId(foreignSeriesId);
|
||||
}
|
||||
|
||||
public List<Series> FindByAuthorId(int authorId)
|
||||
{
|
||||
return _seriesRepository.FindByAuthorId(authorId);
|
||||
}
|
||||
|
||||
public void DeleteSeries(int seriesId)
|
||||
{
|
||||
_seriesRepository.Delete(seriesId);
|
||||
}
|
||||
|
||||
public void DeleteMultipleSeries(List<int> seriesIds)
|
||||
{
|
||||
_seriesRepository.DeleteMany(seriesIds);
|
||||
}
|
||||
|
||||
public List<Series> GetAllSeries()
|
||||
{
|
||||
return _seriesRepository.All().ToList();
|
||||
}
|
||||
|
||||
public List<Series> GetMonitoredSeries()
|
||||
{
|
||||
return _seriesRepository.GetMonitored();
|
||||
}
|
||||
|
||||
public Series UpdateSeries(Series series)
|
||||
{
|
||||
return _seriesRepository.Update(series);
|
||||
}
|
||||
|
||||
public List<Series> UpdateMultipleSeries(List<Series> series)
|
||||
{
|
||||
_seriesRepository.UpdateMany(series);
|
||||
return series;
|
||||
}
|
||||
}
|
||||
}
|
||||
92
src/Radarr.Api.V3/Audiobooks/AudiobookEditorController.cs
Normal file
92
src/Radarr.Api.V3/Audiobooks/AudiobookEditorController.cs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Audiobooks;
|
||||
using Radarr.Http;
|
||||
|
||||
namespace Radarr.Api.V3.Audiobooks
|
||||
{
|
||||
[V3ApiController("audiobook/editor")]
|
||||
public class AudiobookEditorController : Controller
|
||||
{
|
||||
private readonly IAudiobookService _audiobookService;
|
||||
private readonly AudiobookEditorValidator _audiobookEditorValidator;
|
||||
|
||||
public AudiobookEditorController(IAudiobookService audiobookService,
|
||||
AudiobookEditorValidator audiobookEditorValidator)
|
||||
{
|
||||
_audiobookService = audiobookService;
|
||||
_audiobookEditorValidator = audiobookEditorValidator;
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
public IActionResult SaveAll([FromBody] AudiobookEditorResource resource)
|
||||
{
|
||||
var audiobooksToUpdate = _audiobookService.GetAudiobooks(resource.AudiobookIds);
|
||||
|
||||
foreach (var audiobook in audiobooksToUpdate)
|
||||
{
|
||||
if (resource.Monitored.HasValue)
|
||||
{
|
||||
audiobook.Monitored = resource.Monitored.Value;
|
||||
}
|
||||
|
||||
if (resource.QualityProfileId.HasValue)
|
||||
{
|
||||
audiobook.QualityProfileId = resource.QualityProfileId.Value;
|
||||
}
|
||||
|
||||
if (resource.RootFolderPath.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
audiobook.RootFolderPath = resource.RootFolderPath;
|
||||
}
|
||||
|
||||
if (resource.Tags != null)
|
||||
{
|
||||
var newTags = resource.Tags;
|
||||
var applyTags = resource.ApplyTags;
|
||||
|
||||
switch (applyTags)
|
||||
{
|
||||
case ApplyTags.Add:
|
||||
newTags.ForEach(t => audiobook.Tags.Add(t));
|
||||
break;
|
||||
case ApplyTags.Remove:
|
||||
newTags.ForEach(t => audiobook.Tags.Remove(t));
|
||||
break;
|
||||
case ApplyTags.Replace:
|
||||
audiobook.Tags = new HashSet<int>(newTags);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var validationResult = _audiobookEditorValidator.Validate(audiobook);
|
||||
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
}
|
||||
|
||||
var updatedAudiobooks = _audiobookService.UpdateAudiobooks(audiobooksToUpdate);
|
||||
|
||||
var audiobooksResources = new List<AudiobookResource>(updatedAudiobooks.Count);
|
||||
|
||||
foreach (var audiobook in updatedAudiobooks)
|
||||
{
|
||||
audiobooksResources.Add(audiobook.ToResource());
|
||||
}
|
||||
|
||||
return Ok(audiobooksResources);
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
public object DeleteAudiobooks([FromBody] AudiobookEditorResource resource)
|
||||
{
|
||||
_audiobookService.DeleteAudiobooks(resource.AudiobookIds, resource.DeleteFiles);
|
||||
|
||||
return new { };
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/Radarr.Api.V3/Audiobooks/AudiobookEditorResource.cs
Normal file
16
src/Radarr.Api.V3/Audiobooks/AudiobookEditorResource.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Radarr.Api.V3.Audiobooks
|
||||
{
|
||||
public class AudiobookEditorResource
|
||||
{
|
||||
public List<int> AudiobookIds { get; set; }
|
||||
public bool? Monitored { get; set; }
|
||||
public int? QualityProfileId { get; set; }
|
||||
public string RootFolderPath { get; set; }
|
||||
public List<int> Tags { get; set; }
|
||||
public ApplyTags ApplyTags { get; set; }
|
||||
public bool MoveFiles { get; set; }
|
||||
public bool DeleteFiles { get; set; }
|
||||
}
|
||||
}
|
||||
23
src/Radarr.Api.V3/Audiobooks/AudiobookEditorValidator.cs
Normal file
23
src/Radarr.Api.V3/Audiobooks/AudiobookEditorValidator.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
using FluentValidation;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Audiobooks;
|
||||
using NzbDrone.Core.Validation;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
|
||||
namespace Radarr.Api.V3.Audiobooks
|
||||
{
|
||||
public class AudiobookEditorValidator : AbstractValidator<Audiobook>
|
||||
{
|
||||
public AudiobookEditorValidator(RootFolderExistsValidator rootFolderExistsValidator, QualityProfileExistsValidator qualityProfileExistsValidator)
|
||||
{
|
||||
RuleFor(s => s.RootFolderPath).Cascade(CascadeMode.Stop)
|
||||
.IsValidPath()
|
||||
.SetValidator(rootFolderExistsValidator)
|
||||
.When(s => s.RootFolderPath.IsNotNullOrWhiteSpace());
|
||||
|
||||
RuleFor(c => c.QualityProfileId).Cascade(CascadeMode.Stop)
|
||||
.ValidId()
|
||||
.SetValidator(qualityProfileExistsValidator);
|
||||
}
|
||||
}
|
||||
}
|
||||
116
src/Radarr.Api.V3/Audiobooks/AudiobookLookupController.cs
Normal file
116
src/Radarr.Api.V3/Audiobooks/AudiobookLookupController.cs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Audiobooks;
|
||||
using Radarr.Http;
|
||||
using Radarr.Http.REST;
|
||||
|
||||
namespace Radarr.Api.V3.Audiobooks
|
||||
{
|
||||
[V3ApiController("audiobook/lookup")]
|
||||
public class AudiobookLookupController : RestController<AudiobookResource>
|
||||
{
|
||||
private readonly IAudiobookService _audiobookService;
|
||||
|
||||
public AudiobookLookupController(IAudiobookService audiobookService)
|
||||
{
|
||||
_audiobookService = audiobookService;
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
public override ActionResult<AudiobookResource> GetResourceByIdWithErrorHandler(int id)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override AudiobookResource GetResourceById(int id)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
[HttpGet("isbn")]
|
||||
[Produces("application/json")]
|
||||
public ActionResult<AudiobookResource> SearchByIsbn(string isbn)
|
||||
{
|
||||
var audiobook = _audiobookService.FindByIsbn(isbn);
|
||||
if (audiobook == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return audiobook.ToResource();
|
||||
}
|
||||
|
||||
[HttpGet("isbn13")]
|
||||
[Produces("application/json")]
|
||||
public ActionResult<AudiobookResource> SearchByIsbn13(string isbn13)
|
||||
{
|
||||
var audiobook = _audiobookService.FindByIsbn13(isbn13);
|
||||
if (audiobook == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return audiobook.ToResource();
|
||||
}
|
||||
|
||||
[HttpGet("asin")]
|
||||
[Produces("application/json")]
|
||||
public ActionResult<AudiobookResource> SearchByAsin(string asin)
|
||||
{
|
||||
var audiobook = _audiobookService.FindByAsin(asin);
|
||||
if (audiobook == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return audiobook.ToResource();
|
||||
}
|
||||
|
||||
[HttpGet("foreignid")]
|
||||
[Produces("application/json")]
|
||||
public ActionResult<AudiobookResource> SearchByForeignId(string foreignId)
|
||||
{
|
||||
var audiobook = _audiobookService.FindByForeignId(foreignId);
|
||||
if (audiobook == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return audiobook.ToResource();
|
||||
}
|
||||
|
||||
[HttpGet("narrator")]
|
||||
[Produces("application/json")]
|
||||
public IEnumerable<AudiobookResource> SearchByNarrator(string narrator)
|
||||
{
|
||||
var audiobooks = _audiobookService.FindByNarrator(narrator);
|
||||
return audiobooks.ToResource();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Produces("application/json")]
|
||||
public IEnumerable<AudiobookResource> Search([FromQuery] string term)
|
||||
{
|
||||
// For now, search in local database by title or narrator
|
||||
// Future: integrate with metadata provider
|
||||
var allAudiobooks = _audiobookService.GetAllAudiobooks();
|
||||
var results = new List<AudiobookResource>();
|
||||
|
||||
foreach (var audiobook in allAudiobooks)
|
||||
{
|
||||
var matchesTitle = audiobook.Title != null &&
|
||||
audiobook.Title.Contains(term, StringComparison.OrdinalIgnoreCase);
|
||||
var matchesNarrator = audiobook.Narrator != null &&
|
||||
audiobook.Narrator.Contains(term, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (matchesTitle || matchesNarrator)
|
||||
{
|
||||
results.Add(audiobook.ToResource());
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
||||
130
src/Radarr.Api.V3/Authors/AuthorController.cs
Normal file
130
src/Radarr.Api.V3/Authors/AuthorController.cs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Authors;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.RootFolders;
|
||||
using NzbDrone.Core.Validation;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
using NzbDrone.SignalR;
|
||||
using Radarr.Http;
|
||||
using Radarr.Http.REST;
|
||||
using Radarr.Http.REST.Attributes;
|
||||
|
||||
namespace Radarr.Api.V3.Authors
|
||||
{
|
||||
[V3ApiController]
|
||||
public class AuthorController : RestControllerWithSignalR<AuthorResource, Author>
|
||||
{
|
||||
private readonly IAuthorService _authorService;
|
||||
private readonly IRootFolderService _rootFolderService;
|
||||
|
||||
public AuthorController(IBroadcastSignalRMessage signalRBroadcaster,
|
||||
IAuthorService authorService,
|
||||
IRootFolderService rootFolderService,
|
||||
RootFolderValidator rootFolderValidator,
|
||||
MappedNetworkDriveValidator mappedNetworkDriveValidator,
|
||||
RecycleBinValidator recycleBinValidator,
|
||||
SystemFolderValidator systemFolderValidator,
|
||||
QualityProfileExistsValidator qualityProfileExistsValidator,
|
||||
RootFolderExistsValidator rootFolderExistsValidator)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
_authorService = authorService;
|
||||
_rootFolderService = rootFolderService;
|
||||
|
||||
SharedValidator.RuleFor(s => s.Path).Cascade(CascadeMode.Stop)
|
||||
.IsValidPath()
|
||||
.SetValidator(rootFolderValidator)
|
||||
.SetValidator(mappedNetworkDriveValidator)
|
||||
.SetValidator(recycleBinValidator)
|
||||
.SetValidator(systemFolderValidator)
|
||||
.When(s => s.Path.IsNotNullOrWhiteSpace());
|
||||
|
||||
PostValidator.RuleFor(s => s.Path).Cascade(CascadeMode.Stop)
|
||||
.NotEmpty()
|
||||
.IsValidPath()
|
||||
.When(s => s.RootFolderPath.IsNullOrWhiteSpace());
|
||||
PostValidator.RuleFor(s => s.RootFolderPath).Cascade(CascadeMode.Stop)
|
||||
.NotEmpty()
|
||||
.IsValidPath()
|
||||
.SetValidator(rootFolderExistsValidator)
|
||||
.When(s => s.Path.IsNullOrWhiteSpace());
|
||||
|
||||
PutValidator.RuleFor(s => s.Path).Cascade(CascadeMode.Stop)
|
||||
.NotEmpty()
|
||||
.IsValidPath();
|
||||
|
||||
SharedValidator.RuleFor(s => s.QualityProfileId).Cascade(CascadeMode.Stop)
|
||||
.ValidId()
|
||||
.SetValidator(qualityProfileExistsValidator);
|
||||
|
||||
PostValidator.RuleFor(s => s.Name).NotEmpty();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public List<AuthorResource> GetAuthors()
|
||||
{
|
||||
var authors = _authorService.GetAllAuthors();
|
||||
var resources = authors.ToResource();
|
||||
var rootFolders = _rootFolderService.All();
|
||||
|
||||
foreach (var resource in resources)
|
||||
{
|
||||
resource.RootFolderPath = _rootFolderService.GetBestRootFolderPath(resource.Path, rootFolders);
|
||||
}
|
||||
|
||||
return resources;
|
||||
}
|
||||
|
||||
protected override AuthorResource GetResourceById(int id)
|
||||
{
|
||||
var author = _authorService.GetAuthor(id);
|
||||
return MapToResource(author);
|
||||
}
|
||||
|
||||
private AuthorResource MapToResource(Author author)
|
||||
{
|
||||
if (author == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var resource = author.ToResource();
|
||||
resource.RootFolderPath = _rootFolderService.GetBestRootFolderPath(resource.Path);
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
[RestPostById]
|
||||
[Consumes("application/json")]
|
||||
[Produces("application/json")]
|
||||
public ActionResult<AuthorResource> AddAuthor([FromBody] AuthorResource authorResource)
|
||||
{
|
||||
var author = _authorService.AddAuthor(authorResource.ToModel());
|
||||
return Created(author.Id);
|
||||
}
|
||||
|
||||
[RestPutById]
|
||||
[Consumes("application/json")]
|
||||
[Produces("application/json")]
|
||||
public ActionResult<AuthorResource> UpdateAuthor([FromBody] AuthorResource authorResource)
|
||||
{
|
||||
var author = _authorService.GetAuthor(authorResource.Id);
|
||||
var updatedAuthor = _authorService.UpdateAuthor(authorResource.ToModel(author));
|
||||
var resource = MapToResource(updatedAuthor);
|
||||
|
||||
BroadcastResourceChange(ModelAction.Updated, resource);
|
||||
|
||||
return Ok(resource);
|
||||
}
|
||||
|
||||
[RestDeleteById]
|
||||
public ActionResult DeleteAuthor(int id, bool deleteFiles = false)
|
||||
{
|
||||
_authorService.DeleteAuthor(id, deleteFiles);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
102
src/Radarr.Api.V3/Authors/AuthorResource.cs
Normal file
102
src/Radarr.Api.V3/Authors/AuthorResource.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Authors;
|
||||
using Radarr.Http.REST;
|
||||
|
||||
namespace Radarr.Api.V3.Authors
|
||||
{
|
||||
public class AuthorResource : RestResource
|
||||
{
|
||||
public AuthorResource()
|
||||
{
|
||||
Monitored = true;
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
public string SortName { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string ForeignAuthorId { get; set; }
|
||||
public bool Monitored { get; set; }
|
||||
public string Path { get; set; }
|
||||
public string RootFolderPath { get; set; }
|
||||
public int QualityProfileId { get; set; }
|
||||
public DateTime Added { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
}
|
||||
|
||||
public static class AuthorResourceMapper
|
||||
{
|
||||
public static AuthorResource ToResource(this Author model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AuthorResource
|
||||
{
|
||||
Id = model.Id,
|
||||
Name = model.Name,
|
||||
SortName = model.SortName,
|
||||
Description = model.Description,
|
||||
ForeignAuthorId = model.ForeignAuthorId,
|
||||
Monitored = model.Monitored,
|
||||
Path = model.Path,
|
||||
RootFolderPath = model.RootFolderPath,
|
||||
QualityProfileId = model.QualityProfileId,
|
||||
Added = model.Added,
|
||||
Tags = model.Tags
|
||||
};
|
||||
}
|
||||
|
||||
public static Author ToModel(this AuthorResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Author
|
||||
{
|
||||
Id = resource.Id,
|
||||
Name = resource.Name,
|
||||
SortName = resource.SortName,
|
||||
Description = resource.Description,
|
||||
ForeignAuthorId = resource.ForeignAuthorId,
|
||||
Monitored = resource.Monitored,
|
||||
Path = resource.Path,
|
||||
RootFolderPath = resource.RootFolderPath,
|
||||
QualityProfileId = resource.QualityProfileId,
|
||||
Tags = resource.Tags ?? new HashSet<int>()
|
||||
};
|
||||
}
|
||||
|
||||
public static Author ToModel(this AuthorResource resource, Author author)
|
||||
{
|
||||
var updatedAuthor = resource.ToModel();
|
||||
|
||||
author.Name = updatedAuthor.Name;
|
||||
author.SortName = updatedAuthor.SortName;
|
||||
author.Description = updatedAuthor.Description;
|
||||
author.ForeignAuthorId = updatedAuthor.ForeignAuthorId;
|
||||
author.Monitored = updatedAuthor.Monitored;
|
||||
author.Path = updatedAuthor.Path;
|
||||
author.RootFolderPath = updatedAuthor.RootFolderPath;
|
||||
author.QualityProfileId = updatedAuthor.QualityProfileId;
|
||||
author.Tags = updatedAuthor.Tags;
|
||||
|
||||
return author;
|
||||
}
|
||||
|
||||
public static List<AuthorResource> ToResource(this IEnumerable<Author> authors)
|
||||
{
|
||||
return authors.Select(ToResource).ToList();
|
||||
}
|
||||
|
||||
public static List<Author> ToModel(this IEnumerable<AuthorResource> resources)
|
||||
{
|
||||
return resources.Select(ToModel).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
92
src/Radarr.Api.V3/Books/BookEditorController.cs
Normal file
92
src/Radarr.Api.V3/Books/BookEditorController.cs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Books;
|
||||
using Radarr.Http;
|
||||
|
||||
namespace Radarr.Api.V3.Books
|
||||
{
|
||||
[V3ApiController("book/editor")]
|
||||
public class BookEditorController : Controller
|
||||
{
|
||||
private readonly IBookService _bookService;
|
||||
private readonly BookEditorValidator _bookEditorValidator;
|
||||
|
||||
public BookEditorController(IBookService bookService,
|
||||
BookEditorValidator bookEditorValidator)
|
||||
{
|
||||
_bookService = bookService;
|
||||
_bookEditorValidator = bookEditorValidator;
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
public IActionResult SaveAll([FromBody] BookEditorResource resource)
|
||||
{
|
||||
var booksToUpdate = _bookService.GetBooks(resource.BookIds);
|
||||
|
||||
foreach (var book in booksToUpdate)
|
||||
{
|
||||
if (resource.Monitored.HasValue)
|
||||
{
|
||||
book.Monitored = resource.Monitored.Value;
|
||||
}
|
||||
|
||||
if (resource.QualityProfileId.HasValue)
|
||||
{
|
||||
book.QualityProfileId = resource.QualityProfileId.Value;
|
||||
}
|
||||
|
||||
if (resource.RootFolderPath.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
book.RootFolderPath = resource.RootFolderPath;
|
||||
}
|
||||
|
||||
if (resource.Tags != null)
|
||||
{
|
||||
var newTags = resource.Tags;
|
||||
var applyTags = resource.ApplyTags;
|
||||
|
||||
switch (applyTags)
|
||||
{
|
||||
case ApplyTags.Add:
|
||||
newTags.ForEach(t => book.Tags.Add(t));
|
||||
break;
|
||||
case ApplyTags.Remove:
|
||||
newTags.ForEach(t => book.Tags.Remove(t));
|
||||
break;
|
||||
case ApplyTags.Replace:
|
||||
book.Tags = new HashSet<int>(newTags);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var validationResult = _bookEditorValidator.Validate(book);
|
||||
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
throw new ValidationException(validationResult.Errors);
|
||||
}
|
||||
}
|
||||
|
||||
var updatedBooks = _bookService.UpdateBooks(booksToUpdate);
|
||||
|
||||
var booksResources = new List<BookResource>(updatedBooks.Count);
|
||||
|
||||
foreach (var book in updatedBooks)
|
||||
{
|
||||
booksResources.Add(book.ToResource());
|
||||
}
|
||||
|
||||
return Ok(booksResources);
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
public object DeleteBooks([FromBody] BookEditorResource resource)
|
||||
{
|
||||
_bookService.DeleteBooks(resource.BookIds, resource.DeleteFiles);
|
||||
|
||||
return new { };
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/Radarr.Api.V3/Books/BookEditorResource.cs
Normal file
16
src/Radarr.Api.V3/Books/BookEditorResource.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Radarr.Api.V3.Books
|
||||
{
|
||||
public class BookEditorResource
|
||||
{
|
||||
public List<int> BookIds { get; set; }
|
||||
public bool? Monitored { get; set; }
|
||||
public int? QualityProfileId { get; set; }
|
||||
public string RootFolderPath { get; set; }
|
||||
public List<int> Tags { get; set; }
|
||||
public ApplyTags ApplyTags { get; set; }
|
||||
public bool MoveFiles { get; set; }
|
||||
public bool DeleteFiles { get; set; }
|
||||
}
|
||||
}
|
||||
23
src/Radarr.Api.V3/Books/BookEditorValidator.cs
Normal file
23
src/Radarr.Api.V3/Books/BookEditorValidator.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
using FluentValidation;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.Validation;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
|
||||
namespace Radarr.Api.V3.Books
|
||||
{
|
||||
public class BookEditorValidator : AbstractValidator<Book>
|
||||
{
|
||||
public BookEditorValidator(RootFolderExistsValidator rootFolderExistsValidator, QualityProfileExistsValidator qualityProfileExistsValidator)
|
||||
{
|
||||
RuleFor(s => s.RootFolderPath).Cascade(CascadeMode.Stop)
|
||||
.IsValidPath()
|
||||
.SetValidator(rootFolderExistsValidator)
|
||||
.When(s => s.RootFolderPath.IsNotNullOrWhiteSpace());
|
||||
|
||||
RuleFor(c => c.QualityProfileId).Cascade(CascadeMode.Stop)
|
||||
.ValidId()
|
||||
.SetValidator(qualityProfileExistsValidator);
|
||||
}
|
||||
}
|
||||
}
|
||||
104
src/Radarr.Api.V3/Books/BookLookupController.cs
Normal file
104
src/Radarr.Api.V3/Books/BookLookupController.cs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Books;
|
||||
using Radarr.Http;
|
||||
using Radarr.Http.REST;
|
||||
|
||||
namespace Radarr.Api.V3.Books
|
||||
{
|
||||
[V3ApiController("book/lookup")]
|
||||
public class BookLookupController : RestController<BookResource>
|
||||
{
|
||||
private readonly IBookService _bookService;
|
||||
|
||||
public BookLookupController(IBookService bookService)
|
||||
{
|
||||
_bookService = bookService;
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
public override ActionResult<BookResource> GetResourceByIdWithErrorHandler(int id)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override BookResource GetResourceById(int id)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
[HttpGet("isbn")]
|
||||
[Produces("application/json")]
|
||||
public ActionResult<BookResource> SearchByIsbn(string isbn)
|
||||
{
|
||||
var book = _bookService.FindByIsbn(isbn);
|
||||
if (book == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return book.ToResource();
|
||||
}
|
||||
|
||||
[HttpGet("isbn13")]
|
||||
[Produces("application/json")]
|
||||
public ActionResult<BookResource> SearchByIsbn13(string isbn13)
|
||||
{
|
||||
var book = _bookService.FindByIsbn13(isbn13);
|
||||
if (book == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return book.ToResource();
|
||||
}
|
||||
|
||||
[HttpGet("asin")]
|
||||
[Produces("application/json")]
|
||||
public ActionResult<BookResource> SearchByAsin(string asin)
|
||||
{
|
||||
var book = _bookService.FindByAsin(asin);
|
||||
if (book == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return book.ToResource();
|
||||
}
|
||||
|
||||
[HttpGet("foreignid")]
|
||||
[Produces("application/json")]
|
||||
public ActionResult<BookResource> SearchByForeignId(string foreignId)
|
||||
{
|
||||
var book = _bookService.FindByForeignId(foreignId);
|
||||
if (book == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return book.ToResource();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Produces("application/json")]
|
||||
public IEnumerable<BookResource> Search([FromQuery] string term)
|
||||
{
|
||||
// For now, search in local database by title
|
||||
// Future: integrate with metadata provider
|
||||
var allBooks = _bookService.GetAllBooks();
|
||||
var results = new List<BookResource>();
|
||||
|
||||
foreach (var book in allBooks)
|
||||
{
|
||||
if (book.Title != null &&
|
||||
book.Title.Contains(term, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
results.Add(book.ToResource());
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
||||
81
src/Radarr.Api.V3/Series/SeriesController.cs
Normal file
81
src/Radarr.Api.V3/Series/SeriesController.cs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.Series;
|
||||
using NzbDrone.SignalR;
|
||||
using Radarr.Http;
|
||||
using Radarr.Http.REST;
|
||||
using Radarr.Http.REST.Attributes;
|
||||
using SeriesModel = NzbDrone.Core.Series.Series;
|
||||
|
||||
namespace Radarr.Api.V3.Series
|
||||
{
|
||||
[V3ApiController]
|
||||
public class SeriesController : RestControllerWithSignalR<SeriesResource, SeriesModel>
|
||||
{
|
||||
private readonly ISeriesService _seriesService;
|
||||
|
||||
public SeriesController(IBroadcastSignalRMessage signalRBroadcaster,
|
||||
ISeriesService seriesService)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
_seriesService = seriesService;
|
||||
|
||||
PostValidator.RuleFor(s => s.Title).NotEmpty();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public List<SeriesResource> GetSeries(int? authorId = null)
|
||||
{
|
||||
List<SeriesModel> seriesList;
|
||||
|
||||
if (authorId.HasValue)
|
||||
{
|
||||
seriesList = _seriesService.FindByAuthorId(authorId.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
seriesList = _seriesService.GetAllSeries();
|
||||
}
|
||||
|
||||
return seriesList.ToResource();
|
||||
}
|
||||
|
||||
protected override SeriesResource GetResourceById(int id)
|
||||
{
|
||||
var series = _seriesService.GetSeries(id);
|
||||
return series?.ToResource();
|
||||
}
|
||||
|
||||
[RestPostById]
|
||||
[Consumes("application/json")]
|
||||
[Produces("application/json")]
|
||||
public ActionResult<SeriesResource> AddSeries([FromBody] SeriesResource seriesResource)
|
||||
{
|
||||
var series = _seriesService.AddSeries(seriesResource.ToModel());
|
||||
return Created(series.Id);
|
||||
}
|
||||
|
||||
[RestPutById]
|
||||
[Consumes("application/json")]
|
||||
[Produces("application/json")]
|
||||
public ActionResult<SeriesResource> UpdateSeries([FromBody] SeriesResource seriesResource)
|
||||
{
|
||||
var series = _seriesService.GetSeries(seriesResource.Id);
|
||||
var updatedSeries = _seriesService.UpdateSeries(seriesResource.ToModel(series));
|
||||
var resource = updatedSeries.ToResource();
|
||||
|
||||
BroadcastResourceChange(ModelAction.Updated, resource);
|
||||
|
||||
return Ok(resource);
|
||||
}
|
||||
|
||||
[RestDeleteById]
|
||||
public ActionResult DeleteSeries(int id)
|
||||
{
|
||||
_seriesService.DeleteSeries(id);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
87
src/Radarr.Api.V3/Series/SeriesResource.cs
Normal file
87
src/Radarr.Api.V3/Series/SeriesResource.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Radarr.Http.REST;
|
||||
using SeriesModel = NzbDrone.Core.Series.Series;
|
||||
|
||||
namespace Radarr.Api.V3.Series
|
||||
{
|
||||
public class SeriesResource : RestResource
|
||||
{
|
||||
public SeriesResource()
|
||||
{
|
||||
Monitored = true;
|
||||
}
|
||||
|
||||
public string Title { get; set; }
|
||||
public string SortTitle { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string ForeignSeriesId { get; set; }
|
||||
public int? AuthorId { get; set; }
|
||||
public bool Monitored { get; set; }
|
||||
}
|
||||
|
||||
public static class SeriesResourceMapper
|
||||
{
|
||||
public static SeriesResource ToResource(this SeriesModel model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SeriesResource
|
||||
{
|
||||
Id = model.Id,
|
||||
Title = model.Title,
|
||||
SortTitle = model.SortTitle,
|
||||
Description = model.Description,
|
||||
ForeignSeriesId = model.ForeignSeriesId,
|
||||
AuthorId = model.AuthorId,
|
||||
Monitored = model.Monitored
|
||||
};
|
||||
}
|
||||
|
||||
public static SeriesModel ToModel(this SeriesResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SeriesModel
|
||||
{
|
||||
Id = resource.Id,
|
||||
Title = resource.Title,
|
||||
SortTitle = resource.SortTitle,
|
||||
Description = resource.Description,
|
||||
ForeignSeriesId = resource.ForeignSeriesId,
|
||||
AuthorId = resource.AuthorId,
|
||||
Monitored = resource.Monitored
|
||||
};
|
||||
}
|
||||
|
||||
public static SeriesModel ToModel(this SeriesResource resource, SeriesModel series)
|
||||
{
|
||||
var updatedSeries = resource.ToModel();
|
||||
|
||||
series.Title = updatedSeries.Title;
|
||||
series.SortTitle = updatedSeries.SortTitle;
|
||||
series.Description = updatedSeries.Description;
|
||||
series.ForeignSeriesId = updatedSeries.ForeignSeriesId;
|
||||
series.AuthorId = updatedSeries.AuthorId;
|
||||
series.Monitored = updatedSeries.Monitored;
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
public static List<SeriesResource> ToResource(this IEnumerable<SeriesModel> seriesList)
|
||||
{
|
||||
return seriesList.Select(ToResource).ToList();
|
||||
}
|
||||
|
||||
public static List<SeriesModel> ToModel(this IEnumerable<SeriesResource> resources)
|
||||
{
|
||||
return resources.Select(ToModel).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue