mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-02 12:26:36 +08:00
添加新功能,完善项目
This commit is contained in:
@@ -8,6 +8,10 @@ import { getActivePlayProfileId, getActivePlayProfilePinToken } from '../stores/
|
||||
export const api = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 30000,
|
||||
// Serialize arrays as repeated params without brackets: genre=a&genre=b
|
||||
// instead of axios's default genre[]=a&genre[]=b which Gin's QueryArray
|
||||
// does not recognise.
|
||||
paramsSerializer: { indexes: null },
|
||||
})
|
||||
|
||||
export const LONG_REQUEST_TIMEOUT = 120_000
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { api } from './client'
|
||||
|
||||
// 设备管理客户端。
|
||||
//
|
||||
// 普通用户走 /me/devices(服务端按会话身份过滤,前端无法越权指定他人);
|
||||
// 管理员走 /admin/users/:id/devices 代管。
|
||||
|
||||
export interface DeviceInfo {
|
||||
id: string
|
||||
device_id: string
|
||||
device_name?: string
|
||||
client?: string
|
||||
last_ip?: string
|
||||
last_seen_at?: string
|
||||
last_play_at?: string
|
||||
kicked: boolean
|
||||
online: boolean
|
||||
playing: boolean
|
||||
warnings: number
|
||||
}
|
||||
|
||||
interface DeviceListResponse {
|
||||
devices: DeviceInfo[] | null
|
||||
}
|
||||
|
||||
export const devicesAPI = {
|
||||
listMine: () =>
|
||||
api.get<DeviceListResponse>('/me/devices').then((r) => r.data.devices ?? []),
|
||||
|
||||
kickMine: (deviceId: string) =>
|
||||
api.post(`/me/devices/${encodeURIComponent(deviceId)}/kick`).then((r) => r.data),
|
||||
|
||||
kickAllMine: () => api.post('/me/devices/kick-all').then((r) => r.data),
|
||||
|
||||
listForUser: (userId: string) =>
|
||||
api
|
||||
.get<DeviceListResponse>(`/admin/users/${encodeURIComponent(userId)}/devices`)
|
||||
.then((r) => r.data.devices ?? []),
|
||||
|
||||
kickForUser: (userId: string, deviceId: string) =>
|
||||
api
|
||||
.post(
|
||||
`/admin/users/${encodeURIComponent(userId)}/devices/${encodeURIComponent(deviceId)}/kick`,
|
||||
)
|
||||
.then((r) => r.data),
|
||||
|
||||
kickAllForUser: (userId: string) =>
|
||||
api.post(`/admin/users/${encodeURIComponent(userId)}/devices/kick-all`).then((r) => r.data),
|
||||
}
|
||||
+46
-3
@@ -1,6 +1,18 @@
|
||||
import { api, BATCH_REQUEST_TIMEOUT, LONG_REQUEST_TIMEOUT } from './client'
|
||||
import type { Library, LibraryRoot, Media, PlaybackInfo, ScanResult } from '../types'
|
||||
import type { SeriesCard } from '../utils/groupSeries'
|
||||
import {
|
||||
EMPTY_LIBRARY_FILTERS,
|
||||
toFilterQuery,
|
||||
type LibraryFilterParams,
|
||||
} from '../utils/libraryFilters'
|
||||
|
||||
/** 媒体库筛选面板的可选项。 */
|
||||
export interface LibraryFacets {
|
||||
genres: Array<{ name: string; count: number }>
|
||||
year_min: number
|
||||
year_max: number
|
||||
}
|
||||
|
||||
export interface MediaPage {
|
||||
items: Media[]
|
||||
@@ -165,7 +177,12 @@ export const libraryAPI = {
|
||||
id: string,
|
||||
page = 1,
|
||||
pageSize = 50,
|
||||
options?: { groupVersions?: boolean; sort?: string; order?: 'asc' | 'desc' },
|
||||
options?: {
|
||||
groupVersions?: boolean
|
||||
sort?: string
|
||||
order?: 'asc' | 'desc'
|
||||
filters?: LibraryFilterParams
|
||||
},
|
||||
) =>
|
||||
api
|
||||
.get<MediaPage>(`/libraries/${id}/media`, {
|
||||
@@ -175,20 +192,46 @@ export const libraryAPI = {
|
||||
group_versions: options?.groupVersions === false ? 0 : undefined,
|
||||
sort: options?.sort,
|
||||
order: options?.order,
|
||||
...toFilterQuery(options?.filters ?? EMPTY_LIBRARY_FILTERS),
|
||||
},
|
||||
timeout: LONG_REQUEST_TIMEOUT,
|
||||
})
|
||||
.then((r) => r.data),
|
||||
|
||||
/** 媒体库筛选面板的可选项:类型清单与年份区间。 */
|
||||
facets: (id: string) =>
|
||||
api
|
||||
.get<LibraryFacets>(`/libraries/${id}/facets`, { timeout: LONG_REQUEST_TIMEOUT })
|
||||
.then((r) => r.data),
|
||||
|
||||
/** 「随便看看」:按同一套筛选条件随机取一条,无命中时抛 404。 */
|
||||
random: (id: string, filters?: LibraryFilterParams) =>
|
||||
api
|
||||
.get<Media>(`/libraries/${id}/random`, {
|
||||
params: toFilterQuery(filters ?? EMPTY_LIBRARY_FILTERS),
|
||||
timeout: LONG_REQUEST_TIMEOUT,
|
||||
})
|
||||
.then((r) => r.data),
|
||||
|
||||
listSeries: (
|
||||
id: string,
|
||||
page = 1,
|
||||
pageSize = 500,
|
||||
options?: { sort?: string; order?: 'asc' | 'desc' },
|
||||
options?: {
|
||||
sort?: string
|
||||
order?: 'asc' | 'desc'
|
||||
filters?: LibraryFilterParams
|
||||
},
|
||||
) =>
|
||||
api
|
||||
.get<SeriesPage>(`/libraries/${id}/series`, {
|
||||
params: { page, page_size: pageSize, sort: options?.sort, order: options?.order },
|
||||
params: {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
sort: options?.sort,
|
||||
order: options?.order,
|
||||
...toFilterQuery(options?.filters ?? EMPTY_LIBRARY_FILTERS),
|
||||
},
|
||||
timeout: LONG_REQUEST_TIMEOUT,
|
||||
})
|
||||
.then((r) => r.data),
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { api } from './client'
|
||||
|
||||
// 统计相关客户端。
|
||||
//
|
||||
// 个人统计走 /watch-history/stats(服务端按会话身份取数);全站排行走
|
||||
// /stats/top-users,服务端仅允许管理员访问。
|
||||
|
||||
export interface TopUserEntry {
|
||||
user_id: string
|
||||
username: string
|
||||
plays: number
|
||||
}
|
||||
|
||||
interface TopUsersResponse {
|
||||
items: TopUserEntry[] | null
|
||||
}
|
||||
|
||||
export const statsAPI = {
|
||||
topUsers: (limit = 5) =>
|
||||
api
|
||||
.get<TopUsersResponse>(`/stats/top-users?limit=${limit}`)
|
||||
.then((r) => r.data.items ?? []),
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { api } from './client'
|
||||
|
||||
// Telegram 通知绑定客户端。
|
||||
//
|
||||
// 绑定流程:startBind() 拿一次性码 → 用户在 Bot 里发 /bind <码> → 轮询 status()
|
||||
// 直到 bound 变 true。服务端不接收用户手填的 chat id。
|
||||
|
||||
export interface TelegramStatus {
|
||||
bound: boolean
|
||||
chat_id_masked?: string
|
||||
}
|
||||
|
||||
export interface TelegramBindCode {
|
||||
code: string
|
||||
expires_in_seconds: number
|
||||
}
|
||||
|
||||
export const telegramAPI = {
|
||||
status: () => api.get<TelegramStatus>('/me/telegram').then((r) => r.data),
|
||||
|
||||
startBind: () =>
|
||||
api.post<TelegramBindCode>('/me/telegram/bind-code').then((r) => r.data),
|
||||
|
||||
unbind: () => api.delete('/me/telegram').then((r) => r.data),
|
||||
|
||||
test: () => api.post<{ success: boolean; error?: string }>('/admin/telegram/test').then((r) => r.data),
|
||||
}
|
||||
Reference in New Issue
Block a user