add deleteTorrent method
This commit is contained in:
parent
2e39bad718
commit
35a15fcf63
@ -53,7 +53,7 @@ export async function getB2FileInfo(job: Job, s3Key: string): Promise<FileInfo>
|
|||||||
const args = ["file", "info", `b2://${env.AWS_BUCKET}/${s3Key}`];
|
const args = ["file", "info", `b2://${env.AWS_BUCKET}/${s3Key}`];
|
||||||
|
|
||||||
let stdout: string;
|
let stdout: string;
|
||||||
|
await job.log(`Running ${cmd}, ${args.join(' ')}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await spawn(cmd, args);
|
const result = await spawn(cmd, args);
|
||||||
|
|||||||
@ -150,7 +150,9 @@ async function createQBittorrentTorrent(
|
|||||||
async function uploadTorrentToSeedbox(job: Job, videoFilePath: string, torrentFilePath: string) {
|
async function uploadTorrentToSeedbox(job: Job, videoFilePath: string, torrentFilePath: string) {
|
||||||
|
|
||||||
job.log(`Uploading ${videoFilePath} to seedbox...`);
|
job.log(`Uploading ${videoFilePath} to seedbox...`);
|
||||||
await sshClient.uploadFile(videoFilePath, './data');
|
await sshClient.uploadFile(videoFilePath, './data', async ({ percent }) => {
|
||||||
|
await job.log(`Video upload progress: ${percent.toFixed(1)}%`);
|
||||||
|
});
|
||||||
|
|
||||||
job.log(`Uploading ${torrentFilePath} to seedbox...`);
|
job.log(`Uploading ${torrentFilePath} to seedbox...`);
|
||||||
await sshClient.uploadFile(torrentFilePath, './watch');
|
await sshClient.uploadFile(torrentFilePath, './watch');
|
||||||
@ -179,7 +181,7 @@ export async function createTorrent(job: Job) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
job.log('Creating torrent.');
|
job.log('Creating torrent. ~');
|
||||||
|
|
||||||
// we gotta put the download in a place that qbittorrent docker container can access it
|
// we gotta put the download in a place that qbittorrent docker container can access it
|
||||||
|
|
||||||
|
|||||||
@ -73,21 +73,28 @@ export async function __download(job: Job, s3Key: string, cachePath: string) {
|
|||||||
if (!job) throw new Error('Job arg0 missing');
|
if (!job) throw new Error('Job arg0 missing');
|
||||||
if (!s3Key) throw new Error('s3Key arg1 missing');
|
if (!s3Key) throw new Error('s3Key arg1 missing');
|
||||||
if (!cachePath) throw new Error('cachePath arg2 missing');
|
if (!cachePath) throw new Error('cachePath arg2 missing');
|
||||||
|
|
||||||
job.log(`downloading ${s3Key} to ${cachePath}...`);
|
job.log(`downloading ${s3Key} to ${cachePath}...`);
|
||||||
|
|
||||||
|
const { size } = await getB2FileInfo(job, s3Key);
|
||||||
const { size } = (await getB2FileInfo(job, s3Key));
|
|
||||||
|
|
||||||
const stopMonitor = await monitorProgress(cachePath, size, job);
|
const stopMonitor = await monitorProgress(cachePath, size, job);
|
||||||
const { stdout } = await spawn('b2', ['file', 'download', `b2://${env.AWS_BUCKET}/${s3Key}`, cachePath])
|
|
||||||
job.log(stdout);
|
|
||||||
stopMonitor();
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { stdout } = await spawn(
|
||||||
|
'b2',
|
||||||
|
['file', 'download', `b2://${env.AWS_BUCKET}/${s3Key}`, cachePath]
|
||||||
|
);
|
||||||
|
job.log(stdout);
|
||||||
|
} finally {
|
||||||
|
stopMonitor(); // always stop monitor, even on error
|
||||||
|
}
|
||||||
|
|
||||||
job.log('Download complete.');
|
job.log('Download complete.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default async function download(job: Job) {
|
export default async function download(job: Job) {
|
||||||
assertPayload(job.data);
|
assertPayload(job.data);
|
||||||
const vodId = job.data.vodId;
|
const vodId = job.data.vodId;
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { QBittorrentClient } from "./qbittorrent";
|
import { QBittorrentClient } from "./qbittorrent";
|
||||||
import { test, expect, describe, beforeAll, expectTypeOf } from 'vitest';
|
import { test, expect, describe, beforeAll, expectTypeOf } from 'vitest';
|
||||||
import { join } from "node:path";
|
import { join, basename } from "node:path";
|
||||||
|
|
||||||
|
|
||||||
const fixturesDir = join(import.meta.dirname, '..', 'fixtures');
|
const fixturesDir = join(import.meta.dirname, '..', 'fixtures');
|
||||||
const torrentFixture = join(fixturesDir, 'ubuntu-24.04.3-desktop-amd64.iso.torrent');
|
const ubuntuTorrentName = 'ubuntu-24.04.3-desktop-amd64.iso.torrent';
|
||||||
|
const torrentFixture = join(fixturesDir, ubuntuTorrentName);
|
||||||
const fileFixture = join(fixturesDir, 'pizza.avif');
|
const fileFixture = join(fixturesDir, 'pizza.avif');
|
||||||
|
|
||||||
describe('qbittorrent integration', () => {
|
describe('qbittorrent integration', () => {
|
||||||
@ -14,8 +15,6 @@ describe('qbittorrent integration', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("QBittorrentClient methods", async () => {
|
test("QBittorrentClient methods", async () => {
|
||||||
|
|
||||||
|
|
||||||
expect(client).toHaveProperty('addTorrent');
|
expect(client).toHaveProperty('addTorrent');
|
||||||
expect(client).toHaveProperty('getInfoHashV2');
|
expect(client).toHaveProperty('getInfoHashV2');
|
||||||
expect(client).toHaveProperty('connect');
|
expect(client).toHaveProperty('connect');
|
||||||
@ -54,4 +53,10 @@ describe('qbittorrent integration', () => {
|
|||||||
expect(torrent).toHaveProperty('info');
|
expect(torrent).toHaveProperty('info');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("deleteTorrent", async () => {
|
||||||
|
await expect(
|
||||||
|
client.deleteTorrent(basename(ubuntuTorrentName, '.torrent'))
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@ -153,9 +153,16 @@ export class QBittorrentClient {
|
|||||||
this.baseUrl = `http://${this.host}:${this.port}`;
|
this.baseUrl = `http://${this.host}:${this.port}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* idempotently login to qBittorrent.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
async connect(): Promise<void> {
|
async connect(): Promise<void> {
|
||||||
|
if (!this.sidCookie) {
|
||||||
console.log(`Connecting to qBittorrent at ${this.baseUrl}`);
|
console.log(`Connecting to qBittorrent at ${this.baseUrl}`);
|
||||||
await this.login();
|
await this.__login();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -191,8 +198,8 @@ export class QBittorrentClient {
|
|||||||
*
|
*
|
||||||
* Then use the returned SID cookie for subsequent requests.
|
* Then use the returned SID cookie for subsequent requests.
|
||||||
*/
|
*/
|
||||||
private async login(): Promise<void> {
|
private async __login(): Promise<void> {
|
||||||
console.log(`login() begin. using username=${this.username}, password=${this.password} env=${env.NODE_ENV}`)
|
console.log(`login() begin. using username=${this.username}, password=${this.password} env=${env.NODE_ENV}`);
|
||||||
const response = await fetch(`${this.baseUrl}/api/v2/auth/login`, {
|
const response = await fetch(`${this.baseUrl}/api/v2/auth/login`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@ -225,6 +232,8 @@ export class QBittorrentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.sidCookie = setCookie;
|
this.sidCookie = setCookie;
|
||||||
|
console.log(`sidCookie=${this.sidCookie}`);
|
||||||
|
|
||||||
console.log("Successfully logged into qBittorrent.");
|
console.log("Successfully logged into qBittorrent.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -395,7 +404,7 @@ export class QBittorrentClient {
|
|||||||
const torrent = torrents.find((t) => t.name === torrentName) as QBTorrentInfo;
|
const torrent = torrents.find((t) => t.name === torrentName) as QBTorrentInfo;
|
||||||
|
|
||||||
if (!torrent) {
|
if (!torrent) {
|
||||||
throw new Error(`__getTorrentInfos failure. Torrent ${torrentName} not found in qBittorrent after adding`);
|
throw new Error(`__getTorrentInfos failure. Torrent ${torrentName} not found in qBittorrent`);
|
||||||
}
|
}
|
||||||
return torrent;
|
return torrent;
|
||||||
}
|
}
|
||||||
@ -439,7 +448,7 @@ export class QBittorrentClient {
|
|||||||
const fileBuffer = await readFile(localFilePath);
|
const fileBuffer = await readFile(localFilePath);
|
||||||
|
|
||||||
|
|
||||||
const blob = new Blob([fileBuffer]); // wrap Buffer in Blob (necessary for MDN FormData)
|
const blob = new Blob([fileBuffer as any]); // wrap Buffer in Blob (necessary for MDN FormData)
|
||||||
form.append("torrents", blob, path.basename(localFilePath)); // this is for MDN FormData
|
form.append("torrents", blob, path.basename(localFilePath)); // this is for MDN FormData
|
||||||
// form.append("torrents", fileBuffer); // this is for npm:form-data
|
// form.append("torrents", fileBuffer); // this is for npm:form-data
|
||||||
|
|
||||||
@ -463,6 +472,69 @@ export class QBittorrentClient {
|
|||||||
console.log('__addTorrent success.');
|
console.log('__addTorrent success.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @see https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#delete-torrents
|
||||||
|
* @gripe (eww wtf qBittorrent, this isn't RESTful. If we're deleting a resource, we should be using DELETE. Oh well.)
|
||||||
|
*
|
||||||
|
* @param id {String} - a hash or a torrent name
|
||||||
|
*/
|
||||||
|
async deleteTorrent(id: string): Promise<void> {
|
||||||
|
await this.connect();
|
||||||
|
|
||||||
|
if (!this.sidCookie) {
|
||||||
|
throw new Error('Not logged in. sidCookie missing.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect if `id` is already a 40-character hex hash
|
||||||
|
const isHexHash = /^[a-f0-9]{40}$/.test(id);
|
||||||
|
|
||||||
|
let hashToDelete: string;
|
||||||
|
|
||||||
|
if (isHexHash) {
|
||||||
|
// Already a hash → safe to delete
|
||||||
|
hashToDelete = id;
|
||||||
|
} else {
|
||||||
|
// Not a hash → treat as name → look up hash
|
||||||
|
const info = await this.__getTorrentInfos(id);
|
||||||
|
console.log('info', info);
|
||||||
|
hashToDelete = info.hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`deleting ${id} (${hashToDelete})`);
|
||||||
|
await this.__deleteTorrent(hashToDelete);
|
||||||
|
|
||||||
|
console.log(`deleteTorrent success for: ${hashToDelete}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param hashes {String} - Example: 8c212779b4abde7c6bc608063a0d008b7e40ce32|54eddd830a5b58480a6143d616a97e3a6c23c439
|
||||||
|
*/
|
||||||
|
private async __deleteTorrent(hashes: string): Promise<void> {
|
||||||
|
if (!hashes) throw new Error('__deleteTorrent hashes arg missing');
|
||||||
|
if (!this.sidCookie) throw new Error('__deleteTorrent missing sidCookie');
|
||||||
|
|
||||||
|
console.log(`deleting hashes`, hashes)
|
||||||
|
|
||||||
|
const body = new URLSearchParams({
|
||||||
|
hashes,
|
||||||
|
deleteFiles: "false",
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await fetch(`${this.baseUrl}/api/v2/torrents/delete?hashes=${hashes}&deleteFiles=false`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Cookie: this.sidCookie,
|
||||||
|
},
|
||||||
|
body
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text();
|
||||||
|
throw new Error(`__deleteTorrent failed: ${res.status} ${res.statusText} ${body}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @deprecated use __getTorrentInfos instead
|
* @deprecated use __getTorrentInfos instead
|
||||||
|
|||||||
@ -86,7 +86,11 @@ export class SSHClient {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @throws {Error} If the upload fails.
|
* @throws {Error} If the upload fails.
|
||||||
*/
|
*/
|
||||||
async uploadFile(localFilePath: string, remoteDir: string): Promise<void> {
|
async uploadFile(
|
||||||
|
localFilePath: string,
|
||||||
|
remoteDir: string,
|
||||||
|
onProgress?: (info: { transferred: number; total: number; percent: number }) => void
|
||||||
|
): Promise<void> {
|
||||||
console.log(`Uploading localFilePath=${localFilePath} to remoteDir=${remoteDir}...`);
|
console.log(`Uploading localFilePath=${localFilePath} to remoteDir=${remoteDir}...`);
|
||||||
|
|
||||||
console.log('awaiting connect')
|
console.log('awaiting connect')
|
||||||
@ -104,7 +108,21 @@ export class SSHClient {
|
|||||||
console.log(`remoteFilePath=${remoteFilePath}`)
|
console.log(`remoteFilePath=${remoteFilePath}`)
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
sftp.fastPut(localFilePath, remoteFilePath, (err) => (err ? reject(err) : resolve()));
|
sftp.fastPut(
|
||||||
|
localFilePath,
|
||||||
|
remoteFilePath,
|
||||||
|
{
|
||||||
|
step: (transferred, chunk, total) => {
|
||||||
|
const percent = (transferred / total) * 100;
|
||||||
|
|
||||||
|
// Call user-supplied callback if provided
|
||||||
|
if (onProgress) {
|
||||||
|
onProgress({ transferred, total, percent });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(err) => (err ? reject(err) : resolve())
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user