Skip to content
Open
Show file tree
Hide file tree
Changes from all 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