mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
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:
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user