初始化

初始化项目
This commit is contained in:
truewhile
2026-08-23 22:12:32 +08:00
parent 0bcb1fec87
commit 71bf60c69c
631 changed files with 2121 additions and 76050 deletions
-50
View File
@@ -1,50 +0,0 @@
import { api } from './client'
import type { Media } from '../types'
export interface SearchIntent {
query: string
year?: number
genre?: string
type?: string
sort?: string
language?: string
}
export interface ExternalMediaResult {
source: string
media_type?: string
title: string
original_name?: string
overview?: string
poster_url?: string
backdrop_url?: string
year?: number
rating?: number
tmdb_id?: number
bangumi_id?: number
douban_id?: string
subscribe_keyword: string
subscribe_aliases?: string[]
total_episodes?: number
downloaded_episodes?: number
local_media_count?: number
missing_episodes?: number[]
in_library?: boolean
}
export const aiAPI = {
status: () =>
api
.get<{ enabled: boolean; provider: string; model: string }>('/ai/status')
.then((r) => r.data),
smartSearch: (query: string) =>
api
.post<{ intent: SearchIntent; items: Media[]; external_items: ExternalMediaResult[] }>(
'/ai/search',
{ query },
)
.then((r) => r.data),
recommend: () => api.get<{ titles: string[] }>('/ai/recommend').then((r) => r.data.titles),
}
-65
View File
@@ -1,65 +0,0 @@
import { api } from './client'
export interface AssistantSession {
id: string
user_id: string
title: string
created_at: string
updated_at: string
}
export interface AssistantMessage {
id: string
session_id: string
role: 'user' | 'assistant' | 'system'
content: string
operation_id?: string
created_at: string
}
export interface SessionView {
session: AssistantSession
messages: AssistantMessage[]
}
export const assistantAPI = {
listSessions: () =>
api.get<AssistantSession[]>('/admin/assistant/sessions').then((r) => r.data),
createSession: (title?: string) =>
api
.post<AssistantSession>('/admin/assistant/sessions', { title })
.then((r) => r.data),
getSession: (id: string) =>
api.get<SessionView>(`/admin/assistant/session/${id}`).then((r) => r.data),
deleteSession: (id: string) =>
api.delete(`/admin/assistant/session/${id}`).then((r) => r.data),
chat: (sessionID: string, message: string) =>
api
.post<SessionView>('/admin/assistant/chat', {
session_id: sessionID,
message,
})
.then((r) => r.data),
execute: (sessionID: string, action: Record<string, unknown>) =>
api
.post<{ op_id: string }>('/admin/assistant/execute', {
session_id: sessionID,
action,
})
.then((r) => r.data),
undo: (opID: string) =>
api.post(`/admin/assistant/undo/${opID}`).then((r) => r.data),
history: () =>
api
.get<{ items: { op_id: string; session: string; created_at: string; content: string }[] }>(
'/admin/assistant/history',
)
.then((r) => r.data.items),
}
-84
View File
@@ -1,84 +0,0 @@
import { api } from './client'
import type { Media } from '../types'
// TMDb-derived "Match" rows used by trending/popular rails. We re-use the
// Media interface — only TMDb id / poster / overview are populated.
export interface DiscoverItem extends Partial<Media> {
source?: string
media_type?: string
tmdb_id?: number
douban_id?: string
bangumi_id?: number
title: string
poster_url?: string
backdrop_url?: string
overview?: string
year?: number
rating?: number
subscribe_keyword?: string
subscribe_aliases?: string[]
total_episodes?: number
downloaded_episodes?: number
local_media_count?: number
missing_episodes?: number[]
in_library?: boolean
}
export interface DiscoverSection {
key: string
label: string
provider?: string
}
export interface DiscoverFeedMeta {
page: number
has_next: boolean
duration_ms?: number
error?: string
warning?: string
stale?: boolean
disabled?: boolean
}
export interface DiscoverFeedResult {
items: Record<string, DiscoverItem[]>
meta: Record<string, DiscoverFeedMeta>
}
// 后端在 TMDb 不可达 / API key 缺失时统一返回 { items: [], error: "..." }
// 200 状态码——前端必须能区分这两种情况,不能简单用 items.length === 0
// 推断"未配置 API key"。
export interface DiscoverResp {
items: DiscoverItem[]
error?: string
}
export const discoverAPI = {
trending: () =>
api.get<DiscoverResp>('/discover/trending').then((r) => ({
items: r.data.items ?? [],
error: r.data.error,
})),
popular: () =>
api.get<DiscoverResp>('/discover/popular').then((r) => ({
items: r.data.items ?? [],
error: r.data.error,
})),
sections: () =>
api.get<{ sections: DiscoverSection[] }>('/discover/sections').then((r) => r.data.sections),
feed: (sectionKeys: string[], page = 1): Promise<DiscoverFeedResult> =>
api
.get<Record<string, DiscoverItem[] | DiscoverFeedMeta | Record<string, DiscoverFeedMeta> | null>>('/discover/feed', {
params: { sections: sectionKeys.join(','), page },
})
.then((r) => {
const raw = r.data
const meta = ((raw._meta as Record<string, DiscoverFeedMeta> | undefined) ?? {})
const items: Record<string, DiscoverItem[]> = {}
for (const key of sectionKeys) {
const row = raw[key]
items[key] = Array.isArray(row) ? row : []
}
return { items, meta }
}),
}
-53
View File
@@ -1,53 +0,0 @@
import { api } from './client'
export type DownloadClientType = 'qbittorrent' | 'aria2' | 'transmission'
// 与后端 model.DownloadClient 字段对齐(json:"host")。
// 之前前端用的 `url` / `save_path` 字段会被后端忽略并触发 400 binding 错误。
export interface DownloadClient {
id: string
name: string
type: DownloadClientType
host: string
username?: string
is_default: boolean
enabled: boolean
created_at: string
updated_at: string
}
export interface DownloadClientInput {
name: string
type: DownloadClientType
host: string
username?: string
password?: string
is_default: boolean
enabled: boolean
}
export const downloadClientsAPI = {
list: () =>
api.get<DownloadClient[]>('/admin/download/clients').then((r) => r.data ?? []),
create: (input: DownloadClientInput) =>
api.post<DownloadClient>('/admin/download/clients', input).then((r) => r.data),
update: (id: string, input: DownloadClientInput) =>
api
.put<DownloadClient>(`/admin/download/clients/${id}`, input)
.then((r) => r.data),
remove: (id: string) =>
api.delete(`/admin/download/clients/${id}`).then((r) => r.data),
test: (id: string) =>
api
.post<{ ok: boolean; error?: string }>(`/admin/download/clients/${id}/test`)
.then((r) => r.data),
aria2Stats: (clientID: string) =>
api
.get('/admin/download/aria2/stats', { params: { client_id: clientID } })
.then((r) => r.data),
}
-35
View File
@@ -1,35 +0,0 @@
import { api } from './client'
import type { DownloadTask, QBitTorrent } from '../types'
export interface DownloadsState {
tasks: DownloadTask[]
torrents: QBitTorrent[] | null
}
export interface AddDownloadInput {
url: string
save_path?: string
title?: string
poster_url?: string
backdrop_url?: string
overview?: string
media_type?: string
media_category?: string
source_category?: string
}
export const downloadsAPI = {
list: () => api.get<DownloadsState>('/downloads').then((r) => r.data),
add: (url: string, savePath = '', meta: Omit<AddDownloadInput, 'url' | 'save_path'> = {}) =>
api
.post<DownloadTask>('/downloads', { url, save_path: savePath, ...meta })
.then((r) => r.data),
remove: (hash: string, clientID: string, deleteFiles = false) =>
api
.delete(`/downloads/${hash}`, { params: { client_id: clientID, delete_files: deleteFiles } })
.then((r) => r.data),
reload: () => api.post('/downloads/reload').then((r) => r.data),
}
-37
View File
@@ -1,37 +0,0 @@
import { api } from './client'
import type { Media } from '../types'
export interface DuplicateGroup {
hash: string
primary: Media
duplicates: Media[]
}
export interface DuplicateReport {
total_scanned: number
groups_found: number
items_marked: number
missing_removed?: number
groups: DuplicateGroup[]
}
export const duplicatesAPI = {
list: (libraryID = '') =>
api
.get<DuplicateReport>('/duplicates', {
params: libraryID ? { library_id: libraryID } : undefined,
})
.then((r) => ({ ...r.data, groups: r.data.groups ?? [] })),
scan: (libraryID = '') =>
api
.post<DuplicateReport>('/duplicates/scan', null, {
params: libraryID ? { library_id: libraryID } : undefined,
})
.then((r) => ({ ...r.data, groups: r.data.groups ?? [] })),
unmark: (libraryID = '') =>
api
.post<{ unmarked: number }>('/duplicates/unmark', null, {
params: libraryID ? { library_id: libraryID } : undefined,
})
.then((r) => r.data),
}
-54
View File
@@ -1,54 +0,0 @@
import { api } from './client'
// ── License Server types (generic, ready for MediaStationGo license integration) ──
/** Response after binding/activating a license. */
export interface LicenseActivation {
id: string
key_id: string
/** The license key string (e.g. MS-XXXX-XXXX-XXXX) */
key?: string
device_id: string
device_name?: string
plan?: string
max_activations?: number
max_users?: number | null
unlimited_users?: boolean
/** ISO8601 — null means perpetual */
expires_at?: string | null
valid: boolean
ip?: string
heartbeat_at?: string | null
created_at: string
}
/** Status of the currently bound license on this device. */
export interface LicenseStatus {
/** Whether a license is currently active */
active: boolean
activation?: LicenseActivation
max_users?: number | null
unlimited_users?: boolean
/** Error or status message */
message?: string
}
// ── API methods ──
export const licenseAPI = {
/** Bind / activate a license key for this device. */
bind: (key: string) =>
api
.post<LicenseActivation>('/license/activate', {
key: key.trim(),
})
.then((r) => r.data),
/** Get the status of the currently active license. */
status: () =>
api.get<LicenseStatus>('/license/status').then((r) => r.data),
/** Refresh the heartbeat for the active license. */
heartbeat: () =>
api.post('/license/heartbeat').then((r) => r.data),
}
-41
View File
@@ -1,41 +0,0 @@
import { api } from './client'
import type { NotifyChannel } from '../types'
// Payload accepted by create / update. `events` and `enabled` are optional.
export interface NotifyChannelInput {
name: string
type: NotifyChannel['type']
config: Record<string, string>
events?: string[]
enabled?: boolean
}
// notifyChannelsAPI wraps the admin /admin/notify/channels surface.
export const notifyChannelsAPI = {
list: () =>
api.get<NotifyChannel[]>('/admin/notify/channels').then((r) => r.data ?? []),
create: (input: NotifyChannelInput) =>
api.post<NotifyChannel>('/admin/notify/channels', input).then((r) => r.data),
update: (id: string, input: NotifyChannelInput) =>
api.put<NotifyChannel>(`/admin/notify/channels/${id}`, input).then((r) => r.data),
remove: (id: string) =>
api.delete(`/admin/notify/channels/${id}`).then((r) => r.data),
test: (id: string) =>
api.post<{ message: string }>(`/admin/notify/channels/${id}/test`).then((r) => r.data),
startTelegramPolling: () =>
api.post<{
message: string
started: number
already_running: number
skipped: number
errors?: string[]
}>('/admin/telegram/polling/start').then((r) => r.data),
stopTelegramPolling: () =>
api.post<{ message: string; stopped: number }>('/admin/telegram/polling/stop').then((r) => r.data),
}
-9
View File
@@ -1,21 +1,12 @@
import { api } from './client'
import type { Media } from '../types'
export const recycleAPI = {
list: () => api.get<{ items: Media[] }>('/recycle').then((r) => r.data.items),
softDelete: (id: string) => api.delete(`/media/${id}`).then((r) => r.data),
restore: (id: string) => api.post(`/media/${id}/restore`).then((r) => r.data),
restoreMany: (ids: string[]) =>
api.post<{ applied: number; errors?: string[] }>('/recycle/restore', { media_ids: ids }).then((r) => r.data),
purge: (id: string) => api.delete(`/media/${id}/purge`).then((r) => r.data),
purgeMany: (ids: string[]) =>
api.post<{ applied: number; errors?: string[] }>('/recycle/purge', { media_ids: ids }).then((r) => r.data),
exportNFO: (id: string) =>
api.post<{ path: string }>(`/media/${id}/nfo`).then((r) => r.data),
-15
View File
@@ -1,15 +0,0 @@
import { api } from './client'
export interface JobStatus {
name: string
interval: string
last_run?: string
last_err?: string
running?: boolean
started_at?: string
}
export const schedulerAPI = {
status: () => api.get<{ jobs: JobStatus[] }>('/admin/scheduler').then((r) => r.data.jobs),
run: (name: string) => api.post(`/admin/scheduler/${name}/run`).then((r) => r.data),
}
-70
View File
@@ -1,70 +0,0 @@
import { api } from './client'
// ─── TypeScript interfaces ──────────────────────────────────────────────
// Note: the canonical Site type lives in ../types/index.ts
// The backend uses json:"url" (not base_url) and json:"type" (not site_type).
export interface SiteSearchResult {
site_name: string
site_id: string
title: string
subtitle?: string
torrent_url: string
download_url: string
category?: string
size: number
seeders: number
leechers: number
free: boolean
}
export interface CreateSiteInput {
name: string
url: string
type?: string
auth_type?: string
cookie?: string
api_key?: string
auth_header?: string
enabled?: boolean
is_default?: boolean
extra?: string
}
// ─── API client ─────────────────────────────────────────────────────────
export const sitesAPI = {
// List all sites
list: () => api.get('/sites').then((r) => r.data),
// Get single site with decrypted fields
get: (id: string | number) => api.get(`/sites/${id}`).then((r) => r.data),
// Create a new site
create: (data: Record<string, unknown>) =>
api.post('/sites', data).then((r) => r.data),
// Update existing site
update: (id: string | number, data: Record<string, unknown>) =>
api.put(`/sites/${id}`, data).then((r) => r.data),
// Delete a site
remove: (id: string | number) =>
api.delete(`/sites/${id}`).then((r) => r.data),
// Test site connectivity
test: (id: string | number) =>
api.post(`/sites/${id}/test`).then((r) => r.data),
// Get supported site types
types: () => api.get('/sites/types').then((r) => r.data),
// Get supported auth types
authTypes: () => api.get('/sites/auth-types').then((r) => r.data),
// Search across all sites
search: (keyword: string) =>
api
.get('/sites/search', { params: { keyword } })
.then((r) => r.data),
}
-7
View File
@@ -1,7 +0,0 @@
import { api } from './client'
import type { Hardware, StatsSnapshot } from '../types'
export const statsAPI = {
snapshot: () => api.get<StatsSnapshot>('/stats').then((r) => r.data),
monitor: () => api.get<Hardware>('/stats/monitor').then((r) => r.data),
}
-175
View File
@@ -1,175 +0,0 @@
import { api, BATCH_REQUEST_TIMEOUT, LONG_REQUEST_TIMEOUT } from './client'
export type StorageType = 'alist' | 'openlist' | 'webdav' | 'cloud115' | 'clouddrive2'
export interface CloudEntry {
id: string
name: string
is_dir: boolean
size: number
pick_code?: string
}
export interface QRSession {
uid: string
time: number
sign: string
qr_image_url: string
}
export interface QRStatus {
state: 'waiting' | 'scanned' | 'confirmed' | 'expired'
cookie?: string
}
export interface StorageConfig {
id: string
type: StorageType
config: Record<string, string>
enabled: boolean
last_error?: string
created_at: string
updated_at: string
}
export interface CloudUploadResult {
source_path: string
dest_path: string
uploaded: number
moved?: number
skipped: number
bytes: number
errors?: string[]
items?: Array<{
source: string
target: string
action: 'upload' | 'move' | 'skip' | 'error'
size?: number
reason?: string
}>
}
export interface CloudScanStatus {
library_id: string
provider: string
stage: string
state: string
dirs: number
discovered: number
visited: number
added: number
updated: number
skipped: number
removed: number
error?: string
resume_hint?: string
estimate_message?: string
files_per_second?: number
}
export const storageAPI = {
status: () =>
api
.get<{ items: StorageConfig[] }>('/admin/storage/status')
.then((r) => r.data.items),
get: (type: StorageType) =>
api.get<StorageConfig>(`/admin/storage/${type}`).then((r) => r.data),
save: (type: StorageType, config: Record<string, string>, enabled = true) =>
api
.put<StorageConfig>(`/admin/storage/${type}`, { type, config, enabled })
.then((r) => r.data),
logout: (type: StorageType) =>
api
.post<StorageConfig>(`/admin/storage/${type}/logout`)
.then((r) => r.data),
test: (type: StorageType, config: Record<string, string>) =>
api
.post<{ ok: boolean; error?: string }>(`/admin/storage/${type}/test`, {
type,
config,
}, {
timeout: LONG_REQUEST_TIMEOUT,
})
.then((r) => r.data),
uploadLocal: (
type: StorageType,
input: {
source_path: string
dest_path: string
recursive: boolean
include_sidecars: boolean
overwrite: boolean
transfer_mode?: 'copy' | 'move'
},
) =>
api
.post<{ result: CloudUploadResult; error?: string }>(`/admin/storage/${type}/upload-local`, input, {
timeout: BATCH_REQUEST_TIMEOUT,
})
.then((r) => r.data),
scanAllCloud: () =>
api
.post<{ items: CloudScanStatus[]; message?: string; estimate_message?: string }>('/admin/cloud/scan-all')
.then((r) => r.data),
cancelCloudScan: (libraryID = '', provider = '') =>
api
.post<{ cancelled: number; message?: string }>('/admin/cloud/scan/cancel', null, {
params: libraryID ? { library_id: libraryID } : provider ? { provider } : undefined,
})
.then((r) => r.data),
cloudScanStatus: () =>
api
.get<{ items: CloudScanStatus[] }>('/admin/cloud/scan/status')
.then((r) => r.data),
}
// cloudAPI drives 网盘 browsing, QR login and 302 import.
export const cloudAPI = {
list: (type: StorageType, dir = '') =>
api
.get<{ items: CloudEntry[]; error?: string }>(`/admin/cloud/${type}/list`, {
params: { dir },
timeout: LONG_REQUEST_TIMEOUT,
})
.then((r) => r.data),
mkdir: (type: StorageType, dir: string, name: string) =>
api
.post<{ entry: CloudEntry }>(`/admin/cloud/${type}/mkdir`, { dir, name }, {
timeout: LONG_REQUEST_TIMEOUT,
})
.then((r) => r.data),
rename: (type: StorageType, ref: string, name: string) =>
api
.put<{ entry: CloudEntry }>(`/admin/cloud/${type}/rename`, { ref, name }, {
timeout: LONG_REQUEST_TIMEOUT,
})
.then((r) => r.data),
import: (type: StorageType, ref: string, name: string, size: number) =>
api
.post(`/admin/cloud/${type}/import`, { ref, name, size })
.then((r) => r.data),
mount: (type: StorageType, dir = '', name = '', media_type = 'movie', dir_path = '') =>
api
.post(`/admin/cloud/${type}/mount`, { dir, dir_path, name, media_type }, {
timeout: LONG_REQUEST_TIMEOUT,
})
.then((r) => r.data),
qrStart: (type: StorageType) =>
api.post<QRSession>(`/admin/cloud/${type}/qr/start`).then((r) => r.data),
qrPoll: (type: StorageType, sess: QRSession) =>
api.post<QRStatus>(`/admin/cloud/${type}/qr/poll`, sess).then((r) => r.data),
}
-124
View File
@@ -1,124 +0,0 @@
import { api, BATCH_REQUEST_TIMEOUT } from './client'
export type GenerateSTRMInput = {
library_id: string
output_dir?: string
base_url?: string
enabled?: boolean
overwrite?: boolean
include_local?: boolean
preserve_tree?: boolean
refresh_library?: boolean
scrape_after?: boolean
}
export type GenerateSTRMResult = {
library_id: string
output_dir: string
generated: number
updated: number
skipped: number
cleaned: number
total?: number
remaining?: number
batch_limited?: boolean
ignored?: number
previewed?: number
errors?: string[]
ignored_items?: string[]
refresh?: STRMRefreshResult
items?: Array<{
media_id: string
title: string
file_path: string
url?: string
action: string
reason?: string
}>
}
export type GenerateSTRMTreeInput = {
provider: string
tree_text?: string
paths?: string[]
source_root?: string
output_prefix?: string
output_dir: string
base_url?: string
overwrite?: boolean
cleanup?: boolean
dry_run?: boolean
batch_limit?: number
recognize_rename?: boolean
transfer_subtitles?: boolean
missing_only?: boolean
refresh_library?: boolean
scrape_after?: boolean
}
export type RepairSTRMInput = {
output_dir: string
base_url?: string
dry_run?: boolean
refresh_library?: boolean
scrape_after?: boolean
}
export type RepairSTRMResult = {
output_dir: string
repaired: number
previewed?: number
skipped: number
errors?: string[]
refresh?: STRMRefreshResult
items?: Array<{
file_path: string
before?: string
after?: string
action: string
reason?: string
}>
}
export type STRMRefreshResult = {
requested: boolean
queued: boolean
reason?: string
scrape_requested?: boolean
scrape_queued?: boolean
scrape_reason?: string
targets?: Array<{
library_id: string
root_id?: string
name: string
path: string
}>
}
export type STRMOutputPreset = {
label: string
path: string
kind: 'default' | 'library' | string
}
export const strmAPI = {
set: (mediaID: string, url: string) =>
api.put(`/media/${mediaID}/strm`, { url }).then((r) => r.data),
clear: (mediaID: string) => api.delete(`/media/${mediaID}/strm`).then((r) => r.data),
outputPresets: () =>
api.get<{ items: STRMOutputPreset[] }>('/strm/output-presets').then((r) => r.data.items),
importURL: (libraryID: string, title: string, url: string) =>
api.post('/strm/import', { library_id: libraryID, title, url }).then((r) => r.data),
generate: (input: GenerateSTRMInput) =>
api
.post<GenerateSTRMResult>('/strm/generate', input, { timeout: BATCH_REQUEST_TIMEOUT })
.then((r) => r.data),
generateFromTree: (input: GenerateSTRMTreeInput) =>
api
.post<GenerateSTRMResult>('/strm/generate-from-tree', input, { timeout: BATCH_REQUEST_TIMEOUT })
.then((r) => r.data),
repair: (input: RepairSTRMInput) =>
api
.post<RepairSTRMResult>('/strm/repair', input, { timeout: BATCH_REQUEST_TIMEOUT })
.then((r) => r.data),
}
-104
View File
@@ -1,104 +0,0 @@
import { api } from './client'
import type { Subscription } from '../types'
export function buildSiteSearchFeedURL(keyword: string, source?: string, aliases: string[] = []) {
const params = new URLSearchParams()
params.set('keyword', keyword)
if (source) params.set('source', source)
const seen = new Set([keyword.trim().toLowerCase()])
aliases
.map((alias) => alias.trim())
.filter(Boolean)
.forEach((alias) => {
const key = alias.toLowerCase()
if (seen.has(key)) return
seen.add(key)
params.append('alias', alias)
})
return `site-search://search?${params.toString()}`
}
export function buildSubscriptionAliases(item: {
title?: string
original_name?: string
subscribe_keyword?: string
subscribe_aliases?: string[]
year?: number
}) {
const withYear = (value?: string) => {
const title = (value || '').trim()
if (!title) return ''
return item.year && item.year > 0 ? `${title} ${item.year}` : title
}
return [
...(item.subscribe_aliases || []),
item.title || '',
item.original_name || '',
withYear(item.title),
withYear(item.original_name),
item.subscribe_keyword || '',
]
}
export const subscriptionsAPI = {
list: () =>
api
.get<{ items: Subscription[] }>('/subscriptions', subscriptionListRequestConfig())
.then((r) => r.data.items),
history: () =>
api
.get<{ items: Subscription[] }>('/subscriptions/history', subscriptionListRequestConfig())
.then((r) => r.data.items),
create: (input: {
name: string
feed_url: string
filter?: string
media_type?: string
media_category?: string
save_path?: string
search_mode?: string
imdb_id?: string
source?: string
poster_url?: string
backdrop_url?: string
overview?: string
original_name?: string
year?: number
resolution?: string
quality?: string
effects?: string
release_groups?: string
exclude_words?: string
min_seeders?: number
max_seeders?: number
min_size_gb?: number
max_size_gb?: number
free_only?: boolean
wash_enabled?: boolean
wash_priority?: string
total_episodes?: number
priority?: number
enabled?: boolean
}) =>
api.post<Subscription>('/subscriptions', input).then((r) => r.data),
update: (id: string, input: Partial<Subscription>) =>
api.put(`/subscriptions/${id}`, input).then((r) => r.data),
remove: (id: string) => api.delete(`/subscriptions/${id}`).then((r) => r.data),
restore: (id: string) =>
api.post<Subscription>(`/subscriptions/${id}/restore`).then((r) => r.data),
runNow: (id: string) =>
api.post<{ queued: number }>(`/subscriptions/${id}/run`).then((r) => r.data),
}
function subscriptionListRequestConfig() {
return {
headers: { 'Cache-Control': 'no-cache' },
params: { _ts: Date.now() },
}
}
-55
View File
@@ -1,55 +0,0 @@
import { api } from './client'
import type { QBitTorrent } from '../types'
export interface ActiveTranscode {
media_id: string
encoder: string
started_at: string
playlist_ok: boolean
}
export type TaskItemStatus = 'pending' | 'running' | 'succeeded' | 'failed'
export interface TaskItem {
id: string
kind: string // organize | scan | scrape
status: TaskItemStatus
name: string
source?: string
dest_path?: string
library_id?: string
error?: string
}
export interface BackgroundTask {
id: string
kind: string
name: string
status: 'running' | 'completed' | 'failed'
stage?: string
source_path?: string
dest_path?: string
message?: string
error?: string
details?: string[]
metrics?: Record<string, number>
items?: TaskItem[]
started_at: string
updated_at: string
finished_at?: string
}
export interface BackgroundTaskSnapshot {
active: BackgroundTask[]
recent: BackgroundTask[]
}
export interface TasksSnapshot {
transcodes: ActiveTranscode[]
torrents: QBitTorrent[] | null
background_tasks?: BackgroundTaskSnapshot
}
export const tasksAPI = {
snapshot: () => api.get<TasksSnapshot>('/tasks').then((r) => r.data),
}