mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-30 03:36:37 +08:00
初始化
初始化项目
This commit is contained in:
@@ -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),
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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 }
|
||||
}),
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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() },
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
Reference in New Issue
Block a user