Added download ability

Added /bookfile/download/{id} to API
pull/3344/head
adechant 8 months ago
parent d248747635
commit 0922982d3d

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "C#: <project-name> Debug",
"type": "dotnet",
"request": "launch",
"projectPath": "${workspaceFolder}/src/NzbDrone/Readarr.csproj"
}
]
}

Binary file not shown.

@ -11,6 +11,11 @@ export interface AppSectionDeleteState {
deleteError: Error;
}
export interface AppSectionDownloadingState {
isDownloading: boolean;
downloadError: Error;
}
export interface AppSectionSaveState {
isSaving: boolean;
saveError: Error;

@ -284,6 +284,7 @@ class BookDetails extends Component {
<BookFileEditorTable
authorId={author.id}
bookId={id}
title={title}
/>
</TabPanel>
</Tabs>

@ -37,6 +37,10 @@ class BookFileActionsCell extends Component {
this.setState({ isConfirmDeleteModalOpen: true });
};
onDownloadFilePress = () => {
this.props.downloadBookFile({ id: this.props.id });
};
onConfirmDelete = () => {
this.setState({ isConfirmDeleteModalOpen: false });
this.props.deleteBookFile({ id: this.props.id });
@ -77,6 +81,13 @@ class BookFileActionsCell extends Component {
onPress={this.onDeleteFilePress}
/>
}
{
path &&
<IconButton
name={icons.SAVE}
onPress={this.onDownloadFilePress}
/>
}
<FileDetailsModal
isOpen={isDetailsModalOpen}
@ -102,7 +113,8 @@ class BookFileActionsCell extends Component {
BookFileActionsCell.propTypes = {
id: PropTypes.number.isRequired,
path: PropTypes.string,
deleteBookFile: PropTypes.func.isRequired
deleteBookFile: PropTypes.func.isRequired,
downloadBookFile: PropTypes.func.isRequired
};
export default BookFileActionsCell;

@ -19,7 +19,8 @@ function BookFileEditorRow(props) {
qualityCutoffNotMet,
isSelected,
onSelectedChange,
deleteBookFile
deleteBookFile,
downloadBookFile
} = props;
return (
@ -59,6 +60,7 @@ function BookFileEditorRow(props) {
id={id}
path={path}
deleteBookFile={deleteBookFile}
downloadBookFile={downloadBookFile}
/>
</TableRow>
);
@ -73,7 +75,8 @@ BookFileEditorRow.propTypes = {
dateAdded: PropTypes.string.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired,
deleteBookFile: PropTypes.func.isRequired
deleteBookFile: PropTypes.func.isRequired,
downloadBookFile: PropTypes.func.isRequired
};
export default BookFileEditorRow;

@ -14,6 +14,7 @@
.actions {
display: flex;
margin-right: auto;
gap: 10px;
}
.selectInput {

@ -98,6 +98,7 @@ class BookFileEditorTableContent extends Component {
items,
qualities,
dispatchDeleteBookFile,
dispatchDownloadBookFile,
...otherProps
} = this.props;
@ -163,6 +164,7 @@ class BookFileEditorTableContent extends Component {
{...item}
onSelectedChange={this.onSelectedChange}
deleteBookFile={dispatchDeleteBookFile}
downloadBookFile={dispatchDownloadBookFile}
/>
);
})
@ -174,16 +176,8 @@ class BookFileEditorTableContent extends Component {
}
<div className={styles.actions}>
<SpinnerButton
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!hasSelectedFiles}
onPress={this.onDeletePress}
>
Delete
</SpinnerButton>
<div className={styles.selectInput}>
<div className={styles.actions}>
<SelectInput
name="quality"
value="selectQuality"
@ -192,6 +186,16 @@ class BookFileEditorTableContent extends Component {
onChange={this.onQualityChange}
/>
</div>
<SpinnerButton
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!hasSelectedFiles}
onPress={this.onDeletePress}
>
Delete
</SpinnerButton>
</div>
<ConfirmModal
@ -217,7 +221,8 @@ BookFileEditorTableContent.propTypes = {
qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
onDeletePress: PropTypes.func.isRequired,
onQualityChange: PropTypes.func.isRequired,
dispatchDeleteBookFile: PropTypes.func.isRequired
dispatchDeleteBookFile: PropTypes.func.isRequired,
dispatchDownloadBookFile: PropTypes.func.isRequired
};
export default BookFileEditorTableContent;

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { deleteBookFile, deleteBookFiles, setBookFilesSort, updateBookFiles } from 'Store/Actions/bookFileActions';
import { deleteBookFile, deleteBookFiles, downloadBookFile, setBookFilesSort, updateBookFiles } from 'Store/Actions/bookFileActions';
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
@ -80,6 +80,10 @@ function createMapDispatchToProps(dispatch, props) {
dispatchDeleteBookFile(id) {
dispatch(deleteBookFile(id));
},
dispatchDownloadBookFile(id, path, title) {
dispatch(downloadBookFile(id, path, title));
}
};
}
@ -117,7 +121,6 @@ class BookFileEditorTableContentConnector extends Component {
dispatchUpdateBookFiles,
...otherProps
} = this.props;
return (
<BookFileEditorTableContent
{...otherProps}
@ -130,6 +133,7 @@ class BookFileEditorTableContentConnector extends Component {
BookFileEditorTableContentConnector.propTypes = {
authorId: PropTypes.number.isRequired,
bookId: PropTypes.number,
title: PropTypes.string,
qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
dispatchFetchQualityProfileSchema: PropTypes.func.isRequired,
dispatchUpdateBookFiles: PropTypes.func.isRequired,

@ -1,4 +1,4 @@
import _ from 'lodash';
import _, { lastIndexOf, stubString } from 'lodash';
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import bookEntities from 'Book/bookEntities';
@ -29,6 +29,7 @@ export const defaultState = {
error: null,
isDeleting: false,
isDownloading: false,
deleteError: null,
isSaving: false,
saveError: null,
@ -86,6 +87,7 @@ export const persistState = [
export const FETCH_BOOK_FILES = 'bookFiles/fetchBookFiles';
export const DELETE_BOOK_FILE = 'bookFiles/deleteBookFile';
export const DELETE_BOOK_FILES = 'bookFiles/deleteBookFiles';
export const DOWNLOAD_BOOK_FILE = 'bookFiles/downloadBookFile';
export const UPDATE_BOOK_FILES = 'bookFiles/updateBookFiles';
export const SET_BOOK_FILES_SORT = 'bookFiles/setBookFilesSort';
export const SET_BOOK_FILES_TABLE_OPTION = 'bookFiles/setBookFilesTableOption';
@ -97,6 +99,7 @@ export const CLEAR_BOOK_FILES = 'bookFiles/clearBookFiles';
export const fetchBookFiles = createThunk(FETCH_BOOK_FILES);
export const deleteBookFile = createThunk(DELETE_BOOK_FILE);
export const deleteBookFiles = createThunk(DELETE_BOOK_FILES);
export const downloadBookFile = createThunk(DOWNLOAD_BOOK_FILE);
export const updateBookFiles = createThunk(UPDATE_BOOK_FILES);
export const setBookFilesSort = createAction(SET_BOOK_FILES_SORT);
export const setBookFilesTableOption = createAction(SET_BOOK_FILES_TABLE_OPTION);
@ -192,6 +195,44 @@ export const actionHandlers = handleThunks({
});
},
[DOWNLOAD_BOOK_FILE]: function(getState, payload, dispatch) {
const {
id: bookFileId
} = payload;
const downloadPromise = createAjaxRequest({
url: `/bookFile/download/${bookFileId}`,
method: 'GET'
}).request;
downloadPromise.done((data, textStatus, jqXHR) => {
if( textStatus === "success"){
var fileName = "download"
var ext = ".unknown"
var contentType = jqXHR.getResponseHeader("content-type")
ext = `.${contentType.substring(contentType.indexOf("/")+1)}`
if(jqXHR.getResponseHeader("content-disposition")){
var contentDisposition = jqXHR.getResponseHeader("content-disposition");
if(contentDisposition.indexOf("=")>=0){
fileName = contentDisposition.substring(contentDisposition.indexOf("=")+1);
ext="";
}
}
const blob = new Blob([data],{contentType});
const URL = window.URL.createObjectURL(blob);
const el = document.createElement("a");
el.href = URL;
el.download = `${fileName}${ext}`;
document.body.appendChild(el);
el.click();
}
});
},
[UPDATE_BOOK_FILES]: function(getState, payload, dispatch) {
const {
bookFileIds,

18733
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -50,7 +50,9 @@
"mobile-detect": "1.4.5",
"moment": "2.29.4",
"mousetrap": "1.6.5",
"nodejs": "^0.0.0",
"normalize.css": "8.0.1",
"path-browserify": "1.0.1",
"prop-types": "15.8.1",
"qs": "6.11.1",
"react": "17.0.2",
@ -84,7 +86,8 @@
"redux-thunk": "2.3.0",
"reselect": "4.1.8",
"stacktrace-js": "2.0.2",
"typescript": "4.9.5"
"typescript": "4.9.5",
"yarn": "^1.22.19"
},
"devDependencies": {
"@babel/core": "7.22.11",

@ -28,6 +28,14 @@ namespace NzbDrone.Common.Extensions
private static readonly Regex PARENT_PATH_END_SLASH_REGEX = new Regex(@"(?<!:)\\$", RegexOptions.Compiled);
public static string BaseName(this string path)
{
Ensure.That(path, () => path).IsNotNullOrWhiteSpace();
Ensure.That(path, () => path).IsValidPath(PathValidationType.AnyOs);
return Path.GetFileName(path);
}
public static string CleanFilePath(this string path)
{
Ensure.That(path, () => path).IsNotNullOrWhiteSpace();

@ -3,6 +3,8 @@ using System.IO;
using System.IO.Abstractions;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Books;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.DecisionEngine.Specifications;
@ -30,6 +32,7 @@ namespace Readarr.Api.V1.BookFiles
private readonly IAuthorService _authorService;
private readonly IBookService _bookService;
private readonly IUpgradableSpecification _upgradableSpecification;
private readonly IContentTypeProvider _mimeTypeProvider;
public BookFileController(IBroadcastSignalRMessage signalRBroadcaster,
IMediaFileService mediaFileService,
@ -46,6 +49,7 @@ namespace Readarr.Api.V1.BookFiles
_authorService = authorService;
_bookService = bookService;
_upgradableSpecification = upgradableSpecification;
_mimeTypeProvider = new FileExtensionContentTypeProvider();
}
private BookFileResource MapToResource(BookFile bookFile)
@ -108,6 +112,15 @@ namespace Readarr.Api.V1.BookFiles
}
}
[HttpGet("download/{id:int}")]
public IActionResult GetBookFile(int id)
{
var bookFile = _mediaFileService.Get(id);
var filePath = bookFile.Path;
Response.Headers.Add("Content-Disposition", string.Format("attachment;filename={0}", PathExtensions.BaseName(filePath)));
return new PhysicalFileResult(filePath, GetContentType(filePath));
}
[RestPutById]
public ActionResult<BookFileResource> SetQuality(BookFileResource bookFileResource)
{
@ -180,5 +193,15 @@ namespace Readarr.Api.V1.BookFiles
{
BroadcastResourceChange(ModelAction.Deleted, MapToResource(message.BookFile));
}
private string GetContentType(string filePath)
{
if (!_mimeTypeProvider.TryGetContentType(filePath, out var contentType))
{
contentType = string.Format("application/{0}", PathExtensions.GetPathExtension(filePath));
}
return contentType;
}
}
}

@ -4694,6 +4694,11 @@ node-releases@^2.0.13:
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d"
integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==
nodejs@^0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/nodejs/-/nodejs-0.0.0.tgz#4722fa2e18ac4eb73a42ae16d01e3584a12b7531"
integrity sha512-1V+0HwaB/dhxzidEFc4uJ3k52gLI4B6YBZgJIofjwYCSAkD6CI0me6TDBT2QM2nbGWNxCHcq9/wVynzQYZOhUg==
normalize-package-data@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e"
@ -4910,6 +4915,11 @@ pascal-case@^3.1.2:
no-case "^3.0.4"
tslib "^2.0.3"
path-browserify@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@ -6967,6 +6977,11 @@ yargs-parser@^20.2.9:
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
yarn@^1.22.19:
version "1.22.19"
resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.19.tgz#4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447"
integrity sha512-/0V5q0WbslqnwP91tirOvldvYISzaqhClxzyUKXYxs07yUILIs5jx/k6CFe8bvKSkds5w+eiOqta39Wk3WxdcQ==
yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"

Loading…
Cancel
Save