Add hasharr service validation from MeTube settings UI.
Expose a test endpoint and UI button that verifies hasharr reachability and service ID validity, then returns and displays profile details from hasharr for quick pairing validation. Made-with: Cursor
This commit is contained in:
parent
c6b86690ae
commit
36740b00d9
4 changed files with 136 additions and 1 deletions
62
app/main.py
62
app/main.py
|
|
@ -14,6 +14,8 @@ import logging
|
||||||
import json
|
import json
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
|
from urllib import request as urlrequest
|
||||||
|
from urllib import error as urlerror
|
||||||
from watchfiles import DefaultFilter, Change, awatch
|
from watchfiles import DefaultFilter, Change, awatch
|
||||||
|
|
||||||
from ytdl import DownloadQueueNotifier, DownloadQueue, Download
|
from ytdl import DownloadQueueNotifier, DownloadQueue, Download
|
||||||
|
|
@ -586,6 +588,66 @@ async def set_hasharr_settings(request):
|
||||||
config.HASHARR_TIMEOUT_SEC = timeout_sec
|
config.HASHARR_TIMEOUT_SEC = timeout_sec
|
||||||
return web.Response(text=serializer.encode({'status': 'ok'}), content_type='application/json')
|
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
|
@sio.event
|
||||||
async def connect(sid, environ):
|
async def connect(sid, environ):
|
||||||
log.info(f"Client connected: {sid}")
|
log.info(f"Client connected: {sid}")
|
||||||
|
|
|
||||||
|
|
@ -477,10 +477,30 @@
|
||||||
<button type="button" class="btn btn-secondary" (click)="saveHasharrSettings()">
|
<button type="button" class="btn btn-secondary" (click)="saveHasharrSettings()">
|
||||||
Save Hasharr Settings
|
Save Hasharr Settings
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary ms-2" (click)="testHasharrService()">
|
||||||
|
Test Service
|
||||||
|
</button>
|
||||||
@if (hasharrSettingsStatus) {
|
@if (hasharrSettingsStatus) {
|
||||||
<span class="ms-3 text-muted">{{ hasharrSettingsStatus }}</span>
|
<span class="ms-3 text-muted">{{ hasharrSettingsStatus }}</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@if (hasharrTestStatus) {
|
||||||
|
<div class="col-12">
|
||||||
|
<small class="text-muted">{{ hasharrTestStatus }}</small>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (hasharrTestProfile) {
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="small text-muted">
|
||||||
|
<div><strong>Profile Name:</strong> {{ hasharrTestProfile['name'] }}</div>
|
||||||
|
<div><strong>Enabled:</strong> {{ hasharrTestProfile['enabled'] }}</div>
|
||||||
|
<div><strong>Apply Actions:</strong> {{ hasharrTestProfile['applyActions'] }}</div>
|
||||||
|
<div><strong>Stash Index:</strong> {{ hasharrTestProfile['stashIndex'] }}</div>
|
||||||
|
<div><strong>Max Time Delta:</strong> {{ hasharrTestProfile['maxTimeDelta'] }}</div>
|
||||||
|
<div><strong>Max Distance:</strong> {{ hasharrTestProfile['maxDistance'] }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
|
|
|
||||||
|
|
@ -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 { 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 { faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||||
import { CookieService } from 'ngx-cookie-service';
|
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 { Themes } from './theme';
|
||||||
import {
|
import {
|
||||||
Download,
|
Download,
|
||||||
|
|
@ -101,6 +101,8 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
|
||||||
hasharrServiceID = 1;
|
hasharrServiceID = 1;
|
||||||
hasharrTimeoutSec = 20;
|
hasharrTimeoutSec = 20;
|
||||||
hasharrSettingsStatus = '';
|
hasharrSettingsStatus = '';
|
||||||
|
hasharrTestStatus = '';
|
||||||
|
hasharrTestProfile: Record<string, unknown> | null = null;
|
||||||
sortAscending = false;
|
sortAscending = false;
|
||||||
expandedErrors: Set<string> = new Set<string>();
|
expandedErrors: Set<string> = new Set<string>();
|
||||||
cachedSortedDone: [string, Download][] = [];
|
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() {
|
ngAfterViewInit() {
|
||||||
this.downloads.queueChanged.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
this.downloads.queueChanged.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
||||||
this.queueMasterCheckbox()?.selectionChanged();
|
this.queueMasterCheckbox()?.selectionChanged();
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,14 @@ export interface HasharrSettings {
|
||||||
timeout_sec: number;
|
timeout_sec: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HasharrServiceTestResponse {
|
||||||
|
status: string;
|
||||||
|
reachable: boolean;
|
||||||
|
valid_service_id: boolean;
|
||||||
|
message?: string;
|
||||||
|
profile?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AddDownloadPayload {
|
export interface AddDownloadPayload {
|
||||||
url: string;
|
url: string;
|
||||||
downloadType: string;
|
downloadType: string;
|
||||||
|
|
@ -243,4 +251,10 @@ export class DownloadsService {
|
||||||
catchError(this.handleHTTPError)
|
catchError(this.handleHTTPError)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testHasharrSettings(settings: Pick<HasharrSettings, 'url' | 'service_id' | 'timeout_sec'>) {
|
||||||
|
return this.http.post<HasharrServiceTestResponse>('hasharr-settings/test', settings).pipe(
|
||||||
|
catchError(this.handleHTTPError)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue