Apply reverse path mapping in ensureCategory

Ensure ensureCategory applies reverse path mapping (local → remote) before creating or editing qBittorrent categories. Uses PathMapper.reverseTransform to compute the remote save path and updates logging and error details to reference the transformed path. Adds integration tests covering category creation, updating, and no-op when the remote path already matches.
This commit is contained in:
kikootwo
2026-02-03 10:22:03 -05:00
parent 0d64b90fd0
commit 0bab806484
2 changed files with 96 additions and 7 deletions
+11 -7
View File
@@ -459,12 +459,16 @@ export class QBittorrentService {
/** /**
* Ensure category exists in qBittorrent with correct save path * Ensure category exists in qBittorrent with correct save path
* Checks existing categories first, then creates or updates as needed * Checks existing categories first, then creates or updates as needed
* Applies reverse path mapping (local → remote) for remote seedbox scenarios
*/ */
private async ensureCategory(category: string): Promise<void> { private async ensureCategory(category: string): Promise<void> {
if (!this.cookie) { if (!this.cookie) {
await this.login(); await this.login();
} }
// Apply reverse path mapping (local → remote) to get the path qBittorrent expects
const remoteSavePath = PathMapper.reverseTransform(this.defaultSavePath, this.pathMappingConfig);
try { try {
// First, get all categories to check if it exists and what save path it has // First, get all categories to check if it exists and what save path it has
const categoriesResponse = await this.client.get('/torrents/categories', { const categoriesResponse = await this.client.get('/torrents/categories', {
@@ -476,13 +480,13 @@ export class QBittorrentService {
if (!existingCategory) { if (!existingCategory) {
// Category doesn't exist - create it // Category doesn't exist - create it
logger.info(` Creating category "${category}" with save path: ${this.defaultSavePath}`); logger.info(` Creating category "${category}" with save path: ${remoteSavePath}`);
await this.client.post( await this.client.post(
'/torrents/createCategory', '/torrents/createCategory',
new URLSearchParams({ new URLSearchParams({
category, category,
savePath: this.defaultSavePath, savePath: remoteSavePath,
}), }),
{ {
headers: { headers: {
@@ -497,14 +501,14 @@ export class QBittorrentService {
// Category exists - check if save path needs updating // Category exists - check if save path needs updating
const currentSavePath = existingCategory.savePath || existingCategory.save_path; const currentSavePath = existingCategory.savePath || existingCategory.save_path;
if (currentSavePath !== this.defaultSavePath) { if (currentSavePath !== remoteSavePath) {
logger.info(` Updating category "${category}" save path from "${currentSavePath}" to "${this.defaultSavePath}"`); logger.info(` Updating category "${category}" save path from "${currentSavePath}" to "${remoteSavePath}"`);
await this.client.post( await this.client.post(
'/torrents/editCategory', '/torrents/editCategory',
new URLSearchParams({ new URLSearchParams({
category, category,
savePath: this.defaultSavePath, savePath: remoteSavePath,
}), }),
{ {
headers: { headers: {
@@ -516,7 +520,7 @@ export class QBittorrentService {
logger.info(` Category "${category}" save path updated successfully`); logger.info(` Category "${category}" save path updated successfully`);
} else { } else {
logger.info(` Category "${category}" already has correct save path: ${this.defaultSavePath}`); logger.info(` Category "${category}" already has correct save path: ${remoteSavePath}`);
} }
} }
} catch (error) { } catch (error) {
@@ -527,7 +531,7 @@ export class QBittorrentService {
status: error.response?.status, status: error.response?.status,
statusText: error.response?.statusText, statusText: error.response?.statusText,
data: error.response?.data, data: error.response?.data,
requestedPath: this.defaultSavePath, requestedPath: remoteSavePath,
}); });
} else { } else {
logger.error('Failed to ensure category', { error: error instanceof Error ? error.message : String(error) }); logger.error('Failed to ensure category', { error: error instanceof Error ? error.message : String(error) });
@@ -395,6 +395,91 @@ describe('QBittorrentService', () => {
expect(clientMock.post).not.toHaveBeenCalled(); expect(clientMock.post).not.toHaveBeenCalled();
}); });
it('applies reverse path mapping when creating category', async () => {
const service = new QBittorrentService(
'http://qb',
'user',
'pass',
'/downloads',
'readmeabook',
false,
{ enabled: true, remotePath: 'F:\\Docker\\downloads', localPath: '/downloads' }
);
(service as any).cookie = 'SID=pathmap';
clientMock.get.mockResolvedValue({ data: {} }); // No existing categories
clientMock.post.mockResolvedValue({ data: 'Ok.' });
await (service as any).ensureCategory('readmeabook');
expect(clientMock.post).toHaveBeenCalledWith(
'/torrents/createCategory',
expect.any(URLSearchParams),
expect.any(Object)
);
// Verify the savePath was reverse transformed (local → remote)
const postCall = clientMock.post.mock.calls[0];
const params = postCall[1] as URLSearchParams;
expect(params.get('savePath')).toBe('F:\\Docker\\downloads');
});
it('applies reverse path mapping when updating category', async () => {
const service = new QBittorrentService(
'http://qb',
'user',
'pass',
'/downloads',
'readmeabook',
false,
{ enabled: true, remotePath: 'F:\\Docker\\downloads', localPath: '/downloads' }
);
(service as any).cookie = 'SID=pathmap-update';
// Category exists with old path
clientMock.get.mockResolvedValue({
data: {
readmeabook: { savePath: 'F:\\OldPath' },
},
});
clientMock.post.mockResolvedValue({ data: 'Ok.' });
await (service as any).ensureCategory('readmeabook');
expect(clientMock.post).toHaveBeenCalledWith(
'/torrents/editCategory',
expect.any(URLSearchParams),
expect.any(Object)
);
// Verify the savePath was reverse transformed (local → remote)
const postCall = clientMock.post.mock.calls[0];
const params = postCall[1] as URLSearchParams;
expect(params.get('savePath')).toBe('F:\\Docker\\downloads');
});
it('does not update category when remote path already matches', async () => {
const service = new QBittorrentService(
'http://qb',
'user',
'pass',
'/downloads',
'readmeabook',
false,
{ enabled: true, remotePath: 'F:\\Docker\\downloads', localPath: '/downloads' }
);
(service as any).cookie = 'SID=pathmap-match';
// Category already has the correct remote path
clientMock.get.mockResolvedValue({
data: {
readmeabook: { savePath: 'F:\\Docker\\downloads' },
},
});
await (service as any).ensureCategory('readmeabook');
// Should not call post since path already matches
expect(clientMock.post).not.toHaveBeenCalled();
});
it('pauses and resumes torrents', async () => { it('pauses and resumes torrents', async () => {
const service = new QBittorrentService('http://qb', 'user', 'pass'); const service = new QBittorrentService('http://qb', 'user', 'pass');
(service as any).cookie = 'SID=pause'; (service as any).cookie = 'SID=pause';