diff --git a/app/main.py b/app/main.py
index 1684301..d235cb3 100644
--- a/app/main.py
+++ b/app/main.py
@@ -14,6 +14,8 @@ import logging
import json
import pathlib
import re
+from urllib import request as urlrequest
+from urllib import error as urlerror
from watchfiles import DefaultFilter, Change, awatch
from ytdl import DownloadQueueNotifier, DownloadQueue, Download
@@ -586,6 +588,66 @@ async def set_hasharr_settings(request):
config.HASHARR_TIMEOUT_SEC = timeout_sec
return web.Response(text=serializer.encode({'status': 'ok'}), content_type='application/json')
+@routes.post(config.URL_PREFIX + 'hasharr-settings/test')
+async def test_hasharr_settings(request):
+ post = await _read_json_request(request)
+ url = str(post.get('url', config.HASHARR_URL)).strip()
+ service_id = int(post.get('service_id', config.HASHARR_SERVICE_ID))
+ timeout_sec = int(post.get('timeout_sec', config.HASHARR_TIMEOUT_SEC))
+ if not url:
+ raise web.HTTPBadRequest(reason='url is required')
+ if service_id <= 0:
+ raise web.HTTPBadRequest(reason='service_id must be > 0')
+ if timeout_sec <= 0:
+ raise web.HTTPBadRequest(reason='timeout_sec must be > 0')
+
+ base_url = url.rstrip('/')
+ profile_url = f"{base_url}/v1/hash-service-profiles/{service_id}"
+
+ def _fetch_profile():
+ req = urlrequest.Request(profile_url, method='GET')
+ with urlrequest.urlopen(req, timeout=timeout_sec) as resp:
+ body = resp.read().decode('utf-8', errors='replace')
+ return resp.status, body
+
+ try:
+ status, body = await asyncio.get_running_loop().run_in_executor(None, _fetch_profile)
+ if status != 200:
+ return web.json_response({
+ 'status': 'error',
+ 'reachable': True,
+ 'valid_service_id': False,
+ 'message': f'hasharr responded with status {status}',
+ }, status=502)
+ profile = json.loads(body)
+ return web.json_response({
+ 'status': 'ok',
+ 'reachable': True,
+ 'valid_service_id': True,
+ 'profile': profile,
+ })
+ except urlerror.HTTPError as exc:
+ if exc.code == 404:
+ return web.json_response({
+ 'status': 'ok',
+ 'reachable': True,
+ 'valid_service_id': False,
+ 'message': f'Service ID {service_id} was not found in hasharr.',
+ })
+ return web.json_response({
+ 'status': 'error',
+ 'reachable': True,
+ 'valid_service_id': False,
+ 'message': f'hasharr returned HTTP {exc.code}',
+ }, status=502)
+ except Exception as exc:
+ return web.json_response({
+ 'status': 'error',
+ 'reachable': False,
+ 'valid_service_id': False,
+ 'message': f'Could not reach hasharr: {exc}',
+ }, status=502)
+
@sio.event
async def connect(sid, environ):
log.info(f"Client connected: {sid}")
diff --git a/ui/src/app/app.html b/ui/src/app/app.html
index 22be60d..87c110e 100644
--- a/ui/src/app/app.html
+++ b/ui/src/app/app.html
@@ -477,10 +477,30 @@
+
@if (hasharrSettingsStatus) {
{{ hasharrSettingsStatus }}
}
+ @if (hasharrTestStatus) {
+
+ {{ hasharrTestStatus }}
+
+ }
+ @if (hasharrTestProfile) {
+
+
+
Profile Name: {{ hasharrTestProfile['name'] }}
+
Enabled: {{ hasharrTestProfile['enabled'] }}
+
Apply Actions: {{ hasharrTestProfile['applyActions'] }}
+
Stash Index: {{ hasharrTestProfile['stashIndex'] }}
+
Max Time Delta: {{ hasharrTestProfile['maxTimeDelta'] }}
+
Max Distance: {{ hasharrTestProfile['maxDistance'] }}
+
+
+ }
diff --git a/ui/src/app/app.ts b/ui/src/app/app.ts
index f909c4e..aac0ea6 100644
--- a/ui/src/app/app.ts
+++ b/ui/src/app/app.ts
@@ -10,7 +10,7 @@ import { NgSelectModule } from '@ng-select/ng-select';
import { faTrashAlt, faCheckCircle, faTimesCircle, faRedoAlt, faSun, faMoon, faCheck, faCircleHalfStroke, faDownload, faExternalLinkAlt, faFileImport, faFileExport, faCopy, faClock, faTachometerAlt, faSortAmountDown, faSortAmountUp, faChevronRight, faChevronDown, faUpload } from '@fortawesome/free-solid-svg-icons';
import { faGithub } from '@fortawesome/free-brands-svg-icons';
import { CookieService } from 'ngx-cookie-service';
-import { AddDownloadPayload, DownloadsService, HasharrSettings } from './services/downloads.service';
+import { AddDownloadPayload, DownloadsService, HasharrSettings, HasharrServiceTestResponse } from './services/downloads.service';
import { Themes } from './theme';
import {
Download,
@@ -101,6 +101,8 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
hasharrServiceID = 1;
hasharrTimeoutSec = 20;
hasharrSettingsStatus = '';
+ hasharrTestStatus = '';
+ hasharrTestProfile: Record | null = null;
sortAscending = false;
expandedErrors: Set = new Set();
cachedSortedDone: [string, Download][] = [];
@@ -332,6 +334,43 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
});
}
+ testHasharrService() {
+ this.hasharrTestStatus = 'Testing hasharr service...';
+ this.hasharrTestProfile = null;
+ this.cdr.markForCheck();
+ this.downloads.testHasharrSettings({
+ url: String(this.hasharrUrl || '').trim(),
+ service_id: Math.max(1, Number(this.hasharrServiceID || 1)),
+ timeout_sec: Math.max(1, Number(this.hasharrTimeoutSec || 20)),
+ }).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
+ next: (resp) => {
+ if (!resp || typeof resp !== 'object' || ('status' in resp && resp.status === 'error')) {
+ this.hasharrTestStatus = 'Hasharr test failed.';
+ this.hasharrTestProfile = null;
+ this.cdr.markForCheck();
+ return;
+ }
+ const result = resp as HasharrServiceTestResponse;
+ if (result.valid_service_id) {
+ this.hasharrTestStatus = 'Hasharr reachable. Service ID is valid.';
+ this.hasharrTestProfile = result.profile || null;
+ } else if (result.reachable) {
+ this.hasharrTestStatus = result.message || 'Hasharr reachable, but service ID is not valid.';
+ this.hasharrTestProfile = null;
+ } else {
+ this.hasharrTestStatus = result.message || 'Could not reach hasharr.';
+ this.hasharrTestProfile = null;
+ }
+ this.cdr.markForCheck();
+ },
+ error: () => {
+ this.hasharrTestStatus = 'Hasharr test failed.';
+ this.hasharrTestProfile = null;
+ this.cdr.markForCheck();
+ },
+ });
+ }
+
ngAfterViewInit() {
this.downloads.queueChanged.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
this.queueMasterCheckbox()?.selectionChanged();
diff --git a/ui/src/app/services/downloads.service.ts b/ui/src/app/services/downloads.service.ts
index a2ff8e9..e414921 100644
--- a/ui/src/app/services/downloads.service.ts
+++ b/ui/src/app/services/downloads.service.ts
@@ -13,6 +13,14 @@ export interface HasharrSettings {
timeout_sec: number;
}
+export interface HasharrServiceTestResponse {
+ status: string;
+ reachable: boolean;
+ valid_service_id: boolean;
+ message?: string;
+ profile?: Record;
+}
+
export interface AddDownloadPayload {
url: string;
downloadType: string;
@@ -243,4 +251,10 @@ export class DownloadsService {
catchError(this.handleHTTPError)
);
}
+
+ testHasharrSettings(settings: Pick) {
+ return this.http.post('hasharr-settings/test', settings).pipe(
+ catchError(this.handleHTTPError)
+ );
+ }
}