mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-10-28 10:48:49 -04:00
Add support for qBittorrent API key (#832)
This commit is contained in:
1 parent
85e956dd92
commit
705957759e
19 files changed
+770
-9
No files matched your search
@@ -50,4 +50,5 @@ export const NEW_FEATURES: NewFeature[] = [
|
||||
{ id: 'change-username' },
|
||||
{ id: 'force-import' },
|
||||
{ id: 'usenet-queue-cleaner' },
|
||||
{ id: 'qbit-api-key' },
|
||||
];
|
||||
+2
-1
@@ -83,8 +83,9 @@
|
||||
}
|
||||
@if (showApiKeyField()) {
|
||||
<app-input label="API Key" placeholder="Enter API key" type="password" [revealable]="false" [formField]="clientForm.apiKey"
|
||||
[hint]="apiKeyHint"
|
||||
[hint]="apiKeyHint()"
|
||||
[error]="clientForm.apiKey().errors()[0]?.message"
|
||||
[featureId]="apiKeyFeatureId()"
|
||||
helpKey="download-client:apiKey" />
|
||||
}
|
||||
<app-input label="URL Base" placeholder="/api/v2" [formField]="clientForm.urlBase"
|
||||
|
||||
+109
-2
@@ -42,7 +42,9 @@ const TYPES: DownloadClientTypesResponse = {
|
||||
types: [
|
||||
{
|
||||
typeName: DownloadClientTypeName.qBittorrent,
|
||||
authFields: [DownloadClientAuthField.Username, DownloadClientAuthField.Password],
|
||||
authFields: [
|
||||
DownloadClientAuthField.Username, DownloadClientAuthField.Password, DownloadClientAuthField.ApiKey,
|
||||
],
|
||||
capabilities: [DownloadClientCapability.SeedingCleanup, DownloadClientCapability.TagFiltering],
|
||||
},
|
||||
{
|
||||
@@ -253,6 +255,10 @@ describe('DownloadClientsComponent', () => {
|
||||
expect(fieldLabels(fixture)).not.toContain('Username');
|
||||
expect(fieldLabels(fixture)).not.toContain('Password');
|
||||
expect(fieldLabels(fixture)).toContain('API Key');
|
||||
|
||||
component.clientForm.apiKey().value.set('sab-key');
|
||||
fixture.detectChanges();
|
||||
expect(hintFor(fixture, 'API Key')).toBe('API key from SABnzbd > Config > General');
|
||||
});
|
||||
|
||||
it('blocks saving a SABnzbd client with an empty API key and shows the error', () => {
|
||||
@@ -315,11 +321,112 @@ describe('DownloadClientsComponent', () => {
|
||||
component.openAddModal();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.clientModel().typeName).toBe(DownloadClientTypeName.qBittorrent);
|
||||
chooseClientType(fixture, 'Deluge');
|
||||
|
||||
expect(component.clientModel().typeName).toBe(DownloadClientTypeName.Deluge);
|
||||
expect(component.showApiKeyField()).toBe(false);
|
||||
expect(fieldLabels(fixture)).not.toContain('API Key');
|
||||
});
|
||||
|
||||
it('shows the API key field for qBittorrent alongside username and password', () => {
|
||||
const { fixture, component } = setup();
|
||||
|
||||
component.openAddModal();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.clientModel().typeName).toBe(DownloadClientTypeName.qBittorrent);
|
||||
expect(component.showUsernameField()).toBe(true);
|
||||
expect(component.showPasswordField()).toBe(true);
|
||||
expect(component.showApiKeyField()).toBe(true);
|
||||
expect(fieldLabels(fixture)).toContain('API Key');
|
||||
expect(hintFor(fixture, 'API Key'))
|
||||
.toBe('qBittorrent 5.2+: Options > WebUI > API key. Leave username and password empty.');
|
||||
});
|
||||
|
||||
it('blocks saving a qBittorrent client with both an API key and a password', () => {
|
||||
const { fixture, component, api } = setup();
|
||||
|
||||
component.openAddModal();
|
||||
component.clientForm.name().value.set('qbit box');
|
||||
component.clientForm.host().value.set('http://localhost:8080');
|
||||
component.clientForm.password().value.set('secret');
|
||||
component.clientForm.apiKey().value.set('qbit-key');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.hasModalErrors()).toBe(true);
|
||||
expect(component.clientForm.apiKey().errors()[0].message)
|
||||
.toBe('Use either an API key or a username and password, not both');
|
||||
|
||||
component.saveClient();
|
||||
expect(api.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('saves a qBittorrent client with only an API key', () => {
|
||||
const { fixture, component, api } = setup();
|
||||
|
||||
component.openAddModal();
|
||||
component.clientForm.name().value.set('qbit box');
|
||||
component.clientForm.host().value.set('http://localhost:8080');
|
||||
component.clientForm.apiKey().value.set('qbit-key');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.hasModalErrors()).toBe(false);
|
||||
|
||||
component.saveClient();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(api.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
typeName: DownloadClientTypeName.qBittorrent,
|
||||
apiKey: 'qbit-key',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('blocks saving a qBittorrent client with both an API key and a username', () => {
|
||||
const { fixture, component, api } = setup();
|
||||
|
||||
component.openAddModal();
|
||||
component.clientForm.name().value.set('qbit box');
|
||||
component.clientForm.host().value.set('http://localhost:8080');
|
||||
component.clientForm.username().value.set('admin');
|
||||
component.clientForm.apiKey().value.set('qbit-key');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.hasModalErrors()).toBe(true);
|
||||
expect(component.clientForm.apiKey().errors()[0].message)
|
||||
.toBe('Use either an API key or a username and password, not both');
|
||||
|
||||
component.saveClient();
|
||||
expect(api.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('edits a qBittorrent client with a null username and an API key placeholder without showing the exclusive error', () => {
|
||||
const qbitApiKeyOnly: ClientConfig = {
|
||||
...QBIT,
|
||||
username: null as unknown as string,
|
||||
password: undefined,
|
||||
apiKey: 'qbit-placeholder-key',
|
||||
};
|
||||
const { fixture, component, api } = setup(createApi({ clients: [qbitApiKeyOnly] }));
|
||||
|
||||
component.openEditModal(qbitApiKeyOnly);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.hasModalErrors()).toBe(false);
|
||||
|
||||
component.clientForm.name().value.set('qbit box renamed');
|
||||
fixture.detectChanges();
|
||||
|
||||
component.saveClient();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(api.update).toHaveBeenCalledWith(
|
||||
qbitApiKeyOnly.id,
|
||||
expect.objectContaining({ name: 'qbit box renamed', apiKey: 'qbit-placeholder-key' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('saves a SABnzbd client with the Usenet client type', () => {
|
||||
const { fixture, component, api } = setup();
|
||||
|
||||
|
||||
+20
-3
@@ -1,6 +1,6 @@
|
||||
import { Component, ChangeDetectionStrategy, inject, signal, computed } from '@angular/core';
|
||||
import { forkJoin } from 'rxjs';
|
||||
import { form, required, disabled, FormField } from '@angular/forms/signals';
|
||||
import { form, required, disabled, validate, FormField } from '@angular/forms/signals';
|
||||
import { PageHeaderComponent } from '@layout/page-header/page-header.component';
|
||||
import {
|
||||
CardComponent, ButtonComponent, InputComponent, ToggleComponent,
|
||||
@@ -114,6 +114,15 @@ export class DownloadClientsComponent implements HasPendingChanges {
|
||||
when: ({ valueOf }) => valueOf(p.typeName) === DownloadClientTypeName.Sabnzbd,
|
||||
message: 'API key is required',
|
||||
});
|
||||
validate(p.apiKey, () => {
|
||||
const m = this.clientModel();
|
||||
const fields = this.authFieldsByType()[m.typeName] ?? [];
|
||||
const supportsBoth = fields.includes('apiKey') && (fields.includes('username') || fields.includes('password'));
|
||||
const has = (v: string | null | undefined): boolean => !!v && v.trim() !== '';
|
||||
return supportsBoth && has(m.apiKey) && (has(m.username) || has(m.password))
|
||||
? { kind: 'exclusive', message: 'Use either an API key or a username and password, not both' }
|
||||
: undefined;
|
||||
});
|
||||
disabled(p.typeName, () => this.editingClient() !== null);
|
||||
});
|
||||
|
||||
@@ -133,7 +142,15 @@ export class DownloadClientsComponent implements HasPendingChanges {
|
||||
readonly showApiKeyField = computed(() =>
|
||||
(this.authFieldsByType()[this.clientModel().typeName] ?? []).includes('apiKey'));
|
||||
|
||||
readonly apiKeyHint = 'API key from SABnzbd > Config > General';
|
||||
readonly apiKeyHint = computed(() => {
|
||||
if (this.clientModel().typeName === DownloadClientTypeName.qBittorrent) {
|
||||
return 'qBittorrent 5.2+: Options > WebUI > API key. Leave username and password empty.';
|
||||
}
|
||||
return 'API key from SABnzbd > Config > General';
|
||||
});
|
||||
|
||||
readonly apiKeyFeatureId = computed(() =>
|
||||
this.clientModel().typeName === DownloadClientTypeName.qBittorrent ? 'qbit-api-key' : undefined);
|
||||
|
||||
readonly usernameHint = computed(() => {
|
||||
if (this.clientModel().typeName === DownloadClientTypeName.rTorrent) {
|
||||
@@ -204,7 +221,7 @@ export class DownloadClientsComponent implements HasPendingChanges {
|
||||
name: client.name,
|
||||
typeName: client.typeName,
|
||||
host: client.host,
|
||||
username: client.username,
|
||||
username: client.username ?? '',
|
||||
password: client.password ?? '',
|
||||
apiKey: client.apiKey ?? '',
|
||||
urlBase: client.urlBase,
|
||||
|
||||
Reference in new issue
Block a user