mirror of
https://github.com/zoriya/flood.git
synced 2026-06-01 18:47:44 +00:00
API: torrents: return hashes of added torrents when possible
This commit is contained in:
Generated
+501
-132
File diff suppressed because it is too large
Load Diff
@@ -101,6 +101,7 @@
|
||||
"@types/nedb": "^1.8.11",
|
||||
"@types/node": "^12.19.16",
|
||||
"@types/overlayscrollbars": "^1.12.0",
|
||||
"@types/parse-torrent": "^5.8.3",
|
||||
"@types/passport": "^1.0.6",
|
||||
"@types/passport-jwt": "^3.0.4",
|
||||
"@types/react": "^17.0.1",
|
||||
@@ -175,6 +176,7 @@
|
||||
"nedb-promises": "^4.1.1",
|
||||
"overlayscrollbars": "^1.13.1",
|
||||
"overlayscrollbars-react": "^0.2.2",
|
||||
"parse-torrent": "^9.1.3",
|
||||
"passport": "^0.4.1",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"postcss": "^8.2.6",
|
||||
|
||||
@@ -101,7 +101,7 @@ describe('POST /api/torrents/add-urls', () => {
|
||||
})
|
||||
.set('Cookie', [authToken])
|
||||
.set('Accept', 'application/json')
|
||||
.expect(500)
|
||||
.expect(403)
|
||||
.expect('Content-Type', /json/)
|
||||
.end((err, res) => {
|
||||
if (err) done(err);
|
||||
@@ -208,7 +208,7 @@ describe('POST /api/torrents/add-files', () => {
|
||||
})
|
||||
.set('Cookie', [authToken])
|
||||
.set('Accept', 'application/json')
|
||||
.expect(500)
|
||||
.expect(403)
|
||||
.expect('Content-Type', /json/)
|
||||
.end((err, res) => {
|
||||
if (err) done(err);
|
||||
|
||||
@@ -111,12 +111,13 @@ router.get('/', (req, res) => {
|
||||
* @tags Torrents
|
||||
* @security User
|
||||
* @param {AddTorrentByURLOptions} request.body.required - options - application/json
|
||||
* @return {object} 200 - success response - application/json
|
||||
* @return {object} 200 - all torrents added - application/json
|
||||
* @return {object} 202 - requests sent to torrent client - application/json
|
||||
* @return {object} 207 - some succeed, some failed - application/json
|
||||
* @return {Error} 403 - illegal destination - application/json
|
||||
* @return {Error} 500 - failure response - application/json
|
||||
*/
|
||||
router.post<unknown, unknown, AddTorrentByURLOptions>('/add-urls', async (req, res) => {
|
||||
const callback = getResponseFn(res);
|
||||
|
||||
const parsedResult = addTorrentByURLSchema.safeParse(req.body);
|
||||
|
||||
if (!parsedResult.success) {
|
||||
@@ -142,7 +143,8 @@ router.post<unknown, unknown, AddTorrentByURLOptions>('/add-urls', async (req, r
|
||||
});
|
||||
|
||||
if (finalDestination == null) {
|
||||
callback(null, accessDeniedError());
|
||||
const {code, message} = accessDeniedError();
|
||||
res.status(403).json({code, message});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -158,14 +160,21 @@ router.post<unknown, unknown, AddTorrentByURLOptions>('/add-urls', async (req, r
|
||||
isInitialSeeding: isInitialSeeding ?? false,
|
||||
start: start ?? false,
|
||||
})
|
||||
.then((response) => {
|
||||
req.services?.torrentService.fetchTorrentList();
|
||||
return response;
|
||||
})
|
||||
.then(callback)
|
||||
.catch((err) => {
|
||||
callback(null, err);
|
||||
});
|
||||
.then(
|
||||
(response) => {
|
||||
req.services?.torrentService.fetchTorrentList();
|
||||
if (response.length === 0) {
|
||||
res.status(202).json(response);
|
||||
} else if (response.length < urls.length) {
|
||||
res.status(207).json(response);
|
||||
} else {
|
||||
res.status(200).json(response);
|
||||
}
|
||||
},
|
||||
({code, message}) => {
|
||||
res.status(500).json({code, message});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -174,12 +183,13 @@ router.post<unknown, unknown, AddTorrentByURLOptions>('/add-urls', async (req, r
|
||||
* @tags Torrents
|
||||
* @security User
|
||||
* @param {AddTorrentByFileOptions} request.body.required - options - application/json
|
||||
* @return {object} 200 - success response - application/json
|
||||
* @return {object} 200 - all torrents added - application/json
|
||||
* @return {object} 202 - requests sent to torrent client - application/json
|
||||
* @return {object} 207 - some succeed, some failed - application/json
|
||||
* @return {Error} 403 - illegal destination - application/json
|
||||
* @return {Error} 500 - failure response - application/json
|
||||
*/
|
||||
router.post<unknown, unknown, AddTorrentByFileOptions>('/add-files', async (req, res) => {
|
||||
const callback = getResponseFn(res);
|
||||
|
||||
const parsedResult = addTorrentByFileSchema.safeParse(req.body);
|
||||
|
||||
if (!parsedResult.success) {
|
||||
@@ -195,7 +205,8 @@ router.post<unknown, unknown, AddTorrentByFileOptions>('/add-files', async (req,
|
||||
});
|
||||
|
||||
if (finalDestination == null) {
|
||||
callback(null, accessDeniedError());
|
||||
const {code, message} = accessDeniedError();
|
||||
res.status(403).json({code, message});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -210,14 +221,21 @@ router.post<unknown, unknown, AddTorrentByFileOptions>('/add-files', async (req,
|
||||
isInitialSeeding: isInitialSeeding ?? false,
|
||||
start: start ?? false,
|
||||
})
|
||||
.then((response) => {
|
||||
req.services?.torrentService.fetchTorrentList();
|
||||
return response;
|
||||
})
|
||||
.then(callback)
|
||||
.catch((err) => {
|
||||
callback(null, err);
|
||||
});
|
||||
.then(
|
||||
(response) => {
|
||||
req.services?.torrentService.fetchTorrentList();
|
||||
if (response.length === 0) {
|
||||
res.status(202).json(response);
|
||||
} else if (response.length < files.length) {
|
||||
res.status(207).json(response);
|
||||
} else {
|
||||
res.status(200).json(response);
|
||||
}
|
||||
},
|
||||
({code, message}) => {
|
||||
res.status(500).json({code, message});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -43,8 +43,8 @@ class TransmissionClientGatewayService extends ClientGatewayService {
|
||||
tags,
|
||||
isCompleted,
|
||||
start,
|
||||
}: Required<AddTorrentByFileOptions>): Promise<void> {
|
||||
const addedTorrents: [string, ...string[]] = await Promise.all(
|
||||
}: Required<AddTorrentByFileOptions>): Promise<string[]> {
|
||||
const addedTorrents = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const {hashString} =
|
||||
(await this.clientRequestManager
|
||||
@@ -57,23 +57,22 @@ class TransmissionClientGatewayService extends ClientGatewayService {
|
||||
.catch(() => undefined)) || {};
|
||||
return hashString;
|
||||
}),
|
||||
)
|
||||
.then((results) => results.filter((hash) => hash != null) as string[])
|
||||
.then((hashes) => {
|
||||
if (hashes.length < 1) {
|
||||
throw new Error();
|
||||
}
|
||||
return hashes as [string, ...string[]];
|
||||
});
|
||||
).then((results) => results.filter((hash) => hash) as string[]);
|
||||
|
||||
if (addedTorrents[0] == null) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
if (tags.length > 0) {
|
||||
await this.setTorrentsTags({hashes: addedTorrents, tags});
|
||||
await this.setTorrentsTags({hashes: addedTorrents as [string, ...string[]], tags});
|
||||
}
|
||||
|
||||
if (isCompleted) {
|
||||
// Transmission doesn't support skipping verification
|
||||
this.checkTorrents({hashes: addedTorrents}).catch(() => undefined);
|
||||
}
|
||||
|
||||
return addedTorrents;
|
||||
}
|
||||
|
||||
async addTorrentsByURL({
|
||||
@@ -81,10 +80,9 @@ class TransmissionClientGatewayService extends ClientGatewayService {
|
||||
cookies,
|
||||
destination,
|
||||
tags,
|
||||
isCompleted,
|
||||
start,
|
||||
}: Required<AddTorrentByURLOptions>): Promise<void> {
|
||||
const addedTorrents: [string, ...string[]] = await Promise.all(
|
||||
}: Required<AddTorrentByURLOptions>): Promise<string[]> {
|
||||
const addedTorrents = await Promise.all(
|
||||
urls.map(async (url) => {
|
||||
const domain = url.split('/')[2];
|
||||
const {hashString} =
|
||||
@@ -99,23 +97,17 @@ class TransmissionClientGatewayService extends ClientGatewayService {
|
||||
.catch(() => undefined)) || {};
|
||||
return hashString;
|
||||
}),
|
||||
)
|
||||
.then((results) => results.filter((hash) => hash != null) as string[])
|
||||
.then((hashes) => {
|
||||
if (hashes.length < 1) {
|
||||
throw new Error();
|
||||
}
|
||||
return hashes as [string, ...string[]];
|
||||
});
|
||||
).then((results) => results.filter((hash) => hash) as string[]);
|
||||
|
||||
if (addedTorrents[0] == null) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
if (tags.length > 0) {
|
||||
await this.setTorrentsTags({hashes: addedTorrents, tags});
|
||||
await this.setTorrentsTags({hashes: addedTorrents as [string, ...string[]], tags});
|
||||
}
|
||||
|
||||
if (isCompleted) {
|
||||
// Transmission doesn't support skipping verification
|
||||
this.checkTorrents({hashes: addedTorrents}).catch(() => undefined);
|
||||
}
|
||||
return addedTorrents;
|
||||
}
|
||||
|
||||
async checkTorrents({hashes}: CheckTorrentsOptions): Promise<void> {
|
||||
|
||||
@@ -41,17 +41,17 @@ abstract class ClientGatewayService extends BaseService<ClientGatewayServiceEven
|
||||
* Adds torrents by file
|
||||
*
|
||||
* @param {Required<AddTorrentByFileOptions>} options - An object of options...
|
||||
* @return {Promise<void>} - Rejects with error.
|
||||
* @return {Promise<string[]>} - Resolves with an array of hashes of added torrents or rejects with error.
|
||||
*/
|
||||
abstract addTorrentsByFile(options: Required<AddTorrentByFileOptions>): Promise<void>;
|
||||
abstract addTorrentsByFile(options: Required<AddTorrentByFileOptions>): Promise<string[]>;
|
||||
|
||||
/**
|
||||
* Adds torrents by URL
|
||||
*
|
||||
* @param {Required<AddTorrentByURLOptions>} options - An object of options...
|
||||
* @return {Promise<void>} - Rejects with error.
|
||||
* @return {Promise<string[]>} - Resolves with an array of hashes of added torrents or rejects with error.
|
||||
*/
|
||||
abstract addTorrentsByURL(options: Required<AddTorrentByURLOptions>): Promise<void>;
|
||||
abstract addTorrentsByURL(options: Required<AddTorrentByURLOptions>): Promise<string[]>;
|
||||
|
||||
/**
|
||||
* Checks torrents
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {homedir} from 'os';
|
||||
import parseTorrent from 'parse-torrent';
|
||||
import path from 'path';
|
||||
|
||||
import type {
|
||||
@@ -50,16 +51,34 @@ class QBittorrentClientGatewayService extends ClientGatewayService {
|
||||
tags,
|
||||
isBasePath,
|
||||
isCompleted,
|
||||
isInitialSeeding,
|
||||
isSequential,
|
||||
start,
|
||||
}: Required<AddTorrentByFileOptions>): Promise<void> {
|
||||
// TODO: isInitialSeeding not implemented
|
||||
}: Required<AddTorrentByFileOptions>): Promise<string[]> {
|
||||
const fileBuffers: Buffer[] = [];
|
||||
|
||||
const fileBuffers = files.map((file) => {
|
||||
return Buffer.from(file, 'base64');
|
||||
});
|
||||
const torrentHashes: string[] = (
|
||||
await Promise.all(
|
||||
files.map(async (file) => {
|
||||
try {
|
||||
const fileBuffer = Buffer.from(file, 'base64');
|
||||
|
||||
return this.clientRequestManager
|
||||
const {infoHash} = parseTorrent(fileBuffer);
|
||||
fileBuffers.push(fileBuffer);
|
||||
|
||||
return infoHash;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}),
|
||||
)
|
||||
).filter((hash) => hash) as string[];
|
||||
|
||||
if (torrentHashes[0] == null) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
await this.clientRequestManager
|
||||
.torrentsAddFiles(fileBuffers, {
|
||||
savepath: destination,
|
||||
tags: tags.join(','),
|
||||
@@ -70,6 +89,10 @@ class QBittorrentClientGatewayService extends ClientGatewayService {
|
||||
skip_checking: isCompleted,
|
||||
})
|
||||
.then(this.processClientRequestSuccess, this.processClientRequestError);
|
||||
|
||||
await this.setTorrentsInitialSeeding({hashes: torrentHashes, isInitialSeeding});
|
||||
|
||||
return torrentHashes;
|
||||
}
|
||||
|
||||
async addTorrentsByURL({
|
||||
@@ -81,10 +104,10 @@ class QBittorrentClientGatewayService extends ClientGatewayService {
|
||||
isCompleted,
|
||||
isSequential,
|
||||
start,
|
||||
}: Required<AddTorrentByURLOptions>): Promise<void> {
|
||||
}: Required<AddTorrentByURLOptions>): Promise<string[]> {
|
||||
// TODO: isInitialSeeding not implemented
|
||||
|
||||
return this.clientRequestManager
|
||||
await this.clientRequestManager
|
||||
.torrentsAddURLs(urls, {
|
||||
cookie: cookies != null ? Object.values(cookies)[0]?.[0] : undefined,
|
||||
savepath: destination,
|
||||
@@ -96,6 +119,8 @@ class QBittorrentClientGatewayService extends ClientGatewayService {
|
||||
skip_checking: isCompleted,
|
||||
})
|
||||
.then(this.processClientRequestSuccess, this.processClientRequestError);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async checkTorrents({hashes}: CheckTorrentsOptions): Promise<void> {
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from 'fs';
|
||||
import geoip from 'geoip-country';
|
||||
import {moveSync} from 'fs-extra';
|
||||
import path from 'path';
|
||||
import parseTorrent from 'parse-torrent';
|
||||
import sanitize from 'sanitize-filename';
|
||||
|
||||
import type {
|
||||
@@ -66,7 +67,7 @@ class RTorrentClientGatewayService extends ClientGatewayService {
|
||||
isSequential,
|
||||
isInitialSeeding,
|
||||
start,
|
||||
}: Required<AddTorrentByFileOptions>): Promise<void> {
|
||||
}: Required<AddTorrentByFileOptions>): Promise<string[]> {
|
||||
const torrentPaths = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
return saveBufferToTempFile(Buffer.from(file, 'base64'), 'torrent', {
|
||||
@@ -75,21 +76,17 @@ class RTorrentClientGatewayService extends ClientGatewayService {
|
||||
}),
|
||||
);
|
||||
|
||||
if (torrentPaths[0] != null) {
|
||||
return this.addTorrentsByURL({
|
||||
urls: torrentPaths as [string, ...string[]],
|
||||
cookies: {},
|
||||
destination,
|
||||
tags,
|
||||
isBasePath,
|
||||
isCompleted,
|
||||
isSequential,
|
||||
isInitialSeeding,
|
||||
start,
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject();
|
||||
return this.addTorrentsByURL({
|
||||
urls: torrentPaths as [string, ...string[]],
|
||||
cookies: {},
|
||||
destination,
|
||||
tags,
|
||||
isBasePath,
|
||||
isCompleted,
|
||||
isSequential,
|
||||
isInitialSeeding,
|
||||
start,
|
||||
});
|
||||
}
|
||||
|
||||
async addTorrentsByURL({
|
||||
@@ -102,7 +99,7 @@ class RTorrentClientGatewayService extends ClientGatewayService {
|
||||
isSequential,
|
||||
isInitialSeeding,
|
||||
start,
|
||||
}: Required<AddTorrentByURLOptions>): Promise<void> {
|
||||
}: Required<AddTorrentByURLOptions>): Promise<string[]> {
|
||||
await fs.promises.mkdir(destination, {recursive: true});
|
||||
|
||||
const torrentPaths: Array<string> = (
|
||||
@@ -123,23 +120,39 @@ class RTorrentClientGatewayService extends ClientGatewayService {
|
||||
return '';
|
||||
}
|
||||
|
||||
// TODO: handle potential other types of downloads
|
||||
|
||||
return url;
|
||||
}),
|
||||
)
|
||||
).filter((torrentPath) => torrentPath !== '');
|
||||
|
||||
if (isCompleted) {
|
||||
const torrentHashes: string[] = (
|
||||
await Promise.all(
|
||||
torrentPaths.map((torrentPath) => {
|
||||
if (!fs.existsSync(torrentPath)) {
|
||||
return false;
|
||||
}
|
||||
torrentPaths.map(
|
||||
async (torrentPath): Promise<string | undefined> => {
|
||||
try {
|
||||
if (torrentPath.startsWith('magnet:')) {
|
||||
return parseTorrent(torrentPath).infoHash;
|
||||
}
|
||||
|
||||
return setCompleted(torrentPath, destination, isBasePath);
|
||||
}),
|
||||
);
|
||||
if (!fs.existsSync(torrentPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCompleted) {
|
||||
await setCompleted(torrentPath, destination, isBasePath);
|
||||
}
|
||||
|
||||
return parseTorrent(fs.readFileSync(torrentPath)).infoHash;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
).filter((torrentHash) => torrentHash) as string[];
|
||||
|
||||
if (torrentHashes[0] == null) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const methodCalls: MultiMethodCalls = torrentPaths.map((torrentPath) => {
|
||||
@@ -167,12 +180,11 @@ class RTorrentClientGatewayService extends ClientGatewayService {
|
||||
};
|
||||
}, []);
|
||||
|
||||
return this.clientRequestManager
|
||||
await this.clientRequestManager
|
||||
.methodCall('system.multicall', [methodCalls])
|
||||
.then(this.processClientRequestSuccess, this.processClientRequestError)
|
||||
.then(() => {
|
||||
// returns nothing.
|
||||
});
|
||||
.then(this.processClientRequestSuccess, this.processClientRequestError);
|
||||
|
||||
return torrentHashes;
|
||||
}
|
||||
|
||||
async checkTorrents({hashes}: CheckTorrentsOptions): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user