Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/ListenArr.Api/bin/Debug/net7.0/ListenArr.Api.dll",
"program": "${workspaceFolder}/listenarr.api/bin/Debug/net8.0/Listenarr.Api.dll",
"args": [],
"cwd": "${workspaceFolder}/listenarr.api",
"stopAtEntry": false,
Expand Down
2 changes: 1 addition & 1 deletion .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"${workspaceFolder}/listenarr.slnx",
"/property:GenerateFullPaths=true",
"\"/consoleloggerparameters:NoSummary;ForceNoAlign\""
]
],
"problemMatcher": "$msCompile"
},
{
Expand Down
1 change: 0 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ Note: there is also a `watch` task available in the workspace tasks that runs `d
- If adding something not already requested, please create an issue first to discuss it
- Reach out on [Discussions](https://github.com/Listenarrs/Listenarr/discussions) if you have questions

- Run frontend tests: `cd fe && npm test` (the frontend uses Vitest/Vite; check `fe/package.json` for exact scripts)
- Rebase from Listenarr's `develop` branch, don't merge
- Make meaningful commits, or squash them before submitting PR
- Feel free to make a pull request before work is complete (mark as draft) - this lets us see progress and provide feedback
Expand Down
15 changes: 8 additions & 7 deletions fe/src/__tests__/ManualSearchModal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { nextTick } from 'vue'
import { describe, it, expect, vi, afterEach } from 'vitest'
import ManualSearchModal from '@/components/domain/search/ManualSearchModal.vue'
import * as apiModule from '@/services/api'
import { DownloadProtocol } from '@/types/DownloadProtocolConfig'

const { apiService } = apiModule

Expand Down Expand Up @@ -32,7 +33,7 @@ if (!(apiService as unknown as Record<string, unknown>).scoreSearchResults) {
type ManualSearchResult = {
id: string
title?: string
downloadType?: string
protocol: DownloadProtocol
resultUrl?: string
source?: string
nzbUrl?: string
Expand Down Expand Up @@ -104,8 +105,8 @@ describe('ManualSearchModal.vue', () => {
{
id: 'https://indexer/info/123',
title: 'Test Usenet',
downloadType: 'Usenet',
resultUrl: '',
protocol: DownloadProtocol.Usenet,
resultUrl: 'https://indexer/info/123',
sourceLink: 'https://indexer/info/123',
nzbUrl: 'https://indexer/download/123.nzb',
source: 'altHUB',
Expand Down Expand Up @@ -138,7 +139,7 @@ describe('ManualSearchModal.vue', () => {
id: 'u2',
title: 'Lang Test',
language: 'Unknown',
downloadType: 'Usenet',
protocol: DownloadProtocol.Usenet,
resultUrl: 'https://indexer/info/2',
source: 'alt',
size: 0,
Expand Down Expand Up @@ -167,7 +168,7 @@ describe('ManualSearchModal.vue', () => {
title: 'Format Fallback Test',
quality: 'FLAC',
format: 'FLAC',
downloadType: 'Torrent',
protocol: DownloadProtocol.Torrent,
resultUrl: 'https://indexer/info/4',
source: 'test',
size: 0,
Expand Down Expand Up @@ -196,7 +197,7 @@ describe('ManualSearchModal.vue', () => {
const fake = {
id: 'r3',
title: 'Rejected Test',
downloadType: 'Torrent',
protocol: DownloadProtocol.Torrent,
resultUrl: 'https://indexer/info/3',
source: 'test',
size: 0,
Expand Down Expand Up @@ -278,7 +279,7 @@ describe('ManualSearchModal.vue', () => {
{
id: 'r1',
title: 'Smart Score Test',
downloadType: 'Torrent',
protocol: DownloadProtocol.Torrent,
resultUrl: 'https://indexer/info/1',
source: 'test',
size: 0,
Expand Down
19 changes: 19 additions & 0 deletions fe/src/assets/icons/clients/slskd.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions fe/src/assets/icons/indexers/soulseek.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 changes: 21 additions & 40 deletions fe/src/components/domain/download/DownloadClientFormModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,7 @@
<div class="form-group">
<label for="type">Type *</label>
<select id="type" v-model="formData.type" required @change="onTypeChange">
<option value="qbittorrent">qBittorrent</option>
<option value="transmission">Transmission</option>
<option value="sabnzbd">SABnzbd</option>
<option value="nzbget">NZBGet</option>
<option v-for="(config, client) in DownloadClientConfigs" :value="client">{{ config.label }}</option>
</select>
</div>

Expand Down Expand Up @@ -101,7 +98,7 @@
</FormSection>

<!-- Authentication -->
<FormSection title="Authentication" :icon="PhLock" v-if="requiresAuth">
<FormSection title="Authentication" :icon="PhLock">
<div class="form-group" v-if="requiresApiKey">
<label for="apiKey">API Key *</label>
<PasswordInput
Expand All @@ -122,9 +119,9 @@
v-model="formData.username"
type="text"
placeholder="admin"
:required="formData.type === 'nzbget'"
:required="formData.type === DownloadClient.nzbget"
/>
<small v-if="formData.type === 'nzbget'"
<small v-if="formData.type === DownloadClient.nzbget"
>Required when NZBGet authentication is enabled.</small
>
</div>
Expand All @@ -135,10 +132,10 @@
id="password"
v-model="formData.password"
:placeholder="props.editingClient && !formData.password ? '(Saved password)' : '********'"
:required="formData.type === 'nzbget'"
:required="formData.type === DownloadClient.nzbget"
class="admin-input"
/>
<small v-if="formData.type === 'nzbget'"
<small v-if="formData.type === DownloadClient.nzbget"
>Use the NZBGet RPC password (default: nzbget).</small
>
</div>
Expand Down Expand Up @@ -230,7 +227,7 @@
</div>
</FormSection>

<FormSection title="Advanced Settings" :icon="PhWrench" v-if="formData.type === 'qbittorrent'">
<FormSection title="Advanced Settings" :icon="PhWrench" v-if="formData.type === DownloadClient.qbittorrent">
<div class="form-group">
<label for="initialState">Initial State</label>
<select id="initialState" v-model="formData.initialState">
Expand Down Expand Up @@ -328,6 +325,8 @@ import { useConfigurationStore } from '@/stores/configuration'
import { getRemotePathMappings, testDownloadClient } from '@/services/api'
import { logger } from '@/utils/logger'
import type { RemotePathMapping } from '@/types'
import { DownloadClient, DownloadClientConfigs } from '@/types/DownloadClientConfig'
import { DownloadProtocol } from '@/types/DownloadProtocolConfig'


interface Props {
Expand All @@ -351,7 +350,7 @@ const testing = ref(false)

const defaultFormData = {
name: '',
type: 'qbittorrent' as 'qbittorrent' | 'transmission' | 'sabnzbd' | 'nzbget',
type: DownloadClient.qbittorrent,
host: '',
port: 8080,
username: '',
Expand Down Expand Up @@ -401,35 +400,23 @@ const normalizeHost = (value: string): string => {
}

const isUsenet = computed(() => {
return formData.value.type === 'sabnzbd' || formData.value.type === 'nzbget'
return DownloadClientConfigs[formData.value.type].protocols.includes(DownloadProtocol.Usenet)
})

const requiresAuth = computed(() => {
return true // All clients require some form of auth
return !DownloadClientConfigs[formData.value.type].requiresApiKey
})

const requiresApiKey = computed(() => {
return formData.value.type === 'sabnzbd'
return DownloadClientConfigs[formData.value.type].requiresApiKey
})

const getHostPlaceholder = () => {
const placeholders: Record<string, string> = {
qbittorrent: 'qbittorrent.tld.com',
transmission: 'transmission.tld.com',
sabnzbd: 'sabnzbd.tld.com',
nzbget: 'nzbget.tld.com',
}
return placeholders[formData.value.type] || 'localhost'
return DownloadClientConfigs[formData.value.type].url || 'localhost'
}

const getPortPlaceholder = () => {
const ports: Record<string, number> = {
qbittorrent: 8080,
transmission: 9091,
sabnzbd: 8080,
nzbget: 6789,
}
return ports[formData.value.type]?.toString() || '8080'
return DownloadClientConfigs[formData.value.type].port?.toString() || '8080'
}

const getPortHelpText = () => {
Expand All @@ -451,15 +438,9 @@ const getCategoryHelp = () => {

const onTypeChange = () => {
// Update default port when type changes
const defaultPorts: Record<string, number> = {
qbittorrent: 8080,
transmission: 9091,
sabnzbd: 8080,
nzbget: 6789,
}
formData.value.port = defaultPorts[formData.value.type] || 8080
formData.value.port = DownloadClientConfigs[formData.value.type].port || 8080

if (formData.value.type === 'sabnzbd') {
if (DownloadClientConfigs[formData.value.type].requiresApiKey) {
formData.value.username = ''
formData.value.password = ''
} else {
Expand Down Expand Up @@ -536,10 +517,10 @@ const testConnection = async () => {
isEnabled: formData.value.isEnabled,
removeCompletedDownloads: formData.value.removeCompletedDownloads,
settings: {
...(formData.value.type === 'sabnzbd' && formData.value.apiKey
...(DownloadClientConfigs[formData.value.type].requiresApiKey
? { apiKey: formData.value.apiKey }
: {}),
...(formData.value.type === 'transmission' && formData.value.urlBase
...(formData.value.type === DownloadClient.transmission && formData.value.urlBase
? { urlBase: formData.value.urlBase }
: {}),
...(formData.value.category && { category: formData.value.category }),
Expand Down Expand Up @@ -596,10 +577,10 @@ const handleSubmit = async () => {
isEnabled: formData.value.isEnabled,
removeCompletedDownloads: formData.value.removeCompletedDownloads,
settings: {
...(formData.value.type === 'sabnzbd' && formData.value.apiKey
...(DownloadClientConfigs[formData.value.type].requiresApiKey
? { apiKey: formData.value.apiKey }
: {}),
...(formData.value.type === 'transmission' && formData.value.urlBase
...(formData.value.type === DownloadClient.transmission && formData.value.urlBase
? { urlBase: formData.value.urlBase }
: {}),
...(formData.value.category && { category: formData.value.category }),
Expand Down
Loading
Loading