Spaces:
Paused
Paused
File size: 13,319 Bytes
2037d0a a38005e f4003d6 a38005e 2c8ae4e a38005e 2c8ae4e a38005e 2c8ae4e a38005e 2c8ae4e a38005e fbc8659 a38005e 2c8ae4e f0e0245 094c13f f0e0245 f54670e f0e0245 7f36b83 8762688 6db0b76 5283e2c 4b9bf53 41b3b51 70cc48f 76b8a0b c165e10 f0e0245 2c8ae4e fbc8659 094c13f c63b398 5d84fe3 76b8a0b 1ef2319 ef14e02 2037d0a 9b2515d 13e3b99 2c8ae4e 6c38a8d f0e0245 6c38a8d 1ef2319 6c38a8d 2c8ae4e fbc8659 f0e0245 fbc8659 2c8ae4e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | import { useAuthStore } from '@/stores/auth'
const BASE_URL = import.meta.env.VITE_API_URL || ''
let _onAuthFailure: (() => void) | null = null
export function setOnAuthFailure(callback: () => void) {
_onAuthFailure = callback
}
async function request(path: string, options: RequestInit = {}): Promise<Response> {
const auth = useAuthStore()
const headers = new Headers(options.headers)
if (auth.token) {
headers.set('Authorization', `Bearer ${auth.token}`)
}
if (options.body && !headers.has('Content-Type')) {
headers.set('Content-Type', 'application/json')
}
const res = await fetch(`${BASE_URL}${path}`, { ...options, headers, credentials: 'include' })
if (res.status === 401 && auth.token) {
// Try refreshing the token
const refreshed = await auth.refresh()
if (refreshed) {
const retryHeaders = new Headers(options.headers)
retryHeaders.set('Authorization', `Bearer ${auth.token}`)
if (options.body && !retryHeaders.has('Content-Type')) {
retryHeaders.set('Content-Type', 'application/json')
}
return fetch(`${BASE_URL}${path}`, { ...options, headers: retryHeaders, credentials: 'include' })
}
// Refresh failed — clear credentials and redirect without retrying
auth.logout()
_onAuthFailure?.()
return res
}
return res
}
export async function get(path: string): Promise<Response> {
return request(path)
}
export async function post(path: string, body?: unknown): Promise<Response> {
return request(path, {
method: 'POST',
body: body ? JSON.stringify(body) : undefined,
})
}
export async function put(path: string, body?: unknown): Promise<Response> {
return request(path, {
method: 'PUT',
body: body ? JSON.stringify(body) : undefined,
})
}
export async function del(path: string): Promise<Response> {
return request(path, { method: 'DELETE' })
}
export interface SaveSettingsPayload {
image_source: string
lang: string
textless: boolean
ratings_limit: number
ratings_order: string
ratings_exclude: string
poster_position: string
logo_ratings_limit: number
backdrop_ratings_limit: number
poster_badge_style: string
logo_badge_style: string
backdrop_badge_style: string
poster_label_style: string
logo_label_style: string
backdrop_label_style: string
poster_badge_direction: string
poster_badge_split: boolean
poster_fit: string
poster_badge_size: string
logo_badge_size: string
backdrop_badge_size: string
backdrop_position: string
backdrop_badge_direction: string
backdrop_edge_inset_x: number
backdrop_edge_inset_y: number
episode_ratings_limit: number
episode_badge_style: string
episode_label_style: string
episode_badge_size: string
episode_position: string
episode_badge_direction: string
episode_blur: boolean
poster_badge_shape: string
logo_badge_shape: string
backdrop_badge_shape: string
episode_badge_shape: string
poster_badge_background: string
logo_badge_background: string
backdrop_badge_background: string
episode_badge_background: string
}
/** Build a URL path with query parameters, omitting entries with nullish values. */
function buildUrl(path: string, params: Record<string, string | number | undefined>): string {
const qs = new URLSearchParams()
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== '') {
qs.set(key, String(value))
}
}
const query = qs.toString()
return query ? `${path}?${query}` : path
}
// --- Typed API service layer ---
export const adminApi = {
getStats: (): Promise<Response> => get('/api/admin/stats'),
getPosters: (page: number, pageSize: number): Promise<Response> =>
get(`/api/admin/posters?page=${page}&page_size=${pageSize}`),
getPosterImage: (key: string): Promise<Response> =>
get(`/api/admin/posters/${key}/image`),
getSettings: (): Promise<Response> => get('/api/admin/settings'),
updateSettings: (settings: Partial<SaveSettingsPayload> & { image_source: string; free_api_key_enabled?: boolean }): Promise<Response> => put('/api/admin/settings', settings),
fetchPoster: (idType: string, idValue: string): Promise<Response> =>
post(`/api/admin/posters/${idType}/${idValue}/fetch`),
getLogos: (page: number, pageSize: number): Promise<Response> =>
get(`/api/admin/logos?page=${page}&page_size=${pageSize}`),
getLogoImage: (key: string): Promise<Response> =>
get(`/api/admin/logos/${key}`),
fetchLogo: (idType: string, idValue: string): Promise<Response> =>
post(`/api/admin/logos/${idType}/${idValue}/fetch`),
getBackdrops: (page: number, pageSize: number): Promise<Response> =>
get(`/api/admin/backdrops?page=${page}&page_size=${pageSize}`),
getBackdropImage: (key: string): Promise<Response> =>
get(`/api/admin/backdrops/${key}`),
fetchBackdrop: (idType: string, idValue: string): Promise<Response> =>
post(`/api/admin/backdrops/${idType}/${idValue}/fetch`),
getEpisodes: (page: number, pageSize: number): Promise<Response> =>
get(`/api/admin/episodes?page=${page}&page_size=${pageSize}`),
getEpisodeImage: (key: string): Promise<Response> =>
get(`/api/admin/episodes/${key}/image`),
fetchEpisode: (idType: string, idValue: string): Promise<Response> =>
post(`/api/admin/episodes/${idType}/${idValue}/fetch`),
previewPoster: (ratingsLimit: number, ratingsOrder: string, posterPosition?: string, badgeStyle?: string, labelStyle?: string, badgeDirection?: string, badgeSize?: string, ratingsExclude?: string, posterSplit?: boolean, badgeShape?: string, badgeBackground?: string, posterFit?: string, idType?: string, idValue?: string): Promise<Response> =>
get(buildUrl('/api/admin/preview/poster', { ratings_limit: ratingsLimit, ratings_order: ratingsOrder, ratings_exclude: ratingsExclude, position: posterPosition, badge_style: badgeStyle, label_style: labelStyle, badge_direction: badgeDirection, badge_size: badgeSize, split: posterSplit ? 'true' : undefined, badge_shape: badgeShape, badge_background: badgeBackground, fit: posterFit, id_type: idType, id_value: idValue })),
previewLogo: (ratingsLimit: number, ratingsOrder: string, badgeStyle?: string, labelStyle?: string, badgeSize?: string, ratingsExclude?: string, badgeShape?: string, badgeBackground?: string, idType?: string, idValue?: string): Promise<Response> =>
get(buildUrl('/api/admin/preview/logo', { ratings_limit: ratingsLimit, ratings_order: ratingsOrder, ratings_exclude: ratingsExclude, badge_style: badgeStyle, label_style: labelStyle, badge_size: badgeSize, badge_shape: badgeShape, badge_background: badgeBackground, id_type: idType, id_value: idValue })),
previewBackdrop: (ratingsLimit: number, ratingsOrder: string, badgeStyle?: string, labelStyle?: string, badgeSize?: string, position?: string, badgeDirection?: string, ratingsExclude?: string, badgeShape?: string, badgeBackground?: string, edgeInsetX?: number, edgeInsetY?: number, idType?: string, idValue?: string): Promise<Response> =>
get(buildUrl('/api/admin/preview/backdrop', { ratings_limit: ratingsLimit, ratings_order: ratingsOrder, ratings_exclude: ratingsExclude, badge_style: badgeStyle, label_style: labelStyle, badge_size: badgeSize, position, badge_direction: badgeDirection, badge_shape: badgeShape, badge_background: badgeBackground, edge_inset_x: edgeInsetX, edge_inset_y: edgeInsetY, id_type: idType, id_value: idValue })),
previewEpisode: (ratingsLimit: number, ratingsOrder: string, badgeStyle?: string, labelStyle?: string, badgeSize?: string, position?: string, badgeDirection?: string, blur?: boolean, ratingsExclude?: string, badgeShape?: string, badgeBackground?: string, idType?: string, idValue?: string): Promise<Response> =>
get(buildUrl('/api/admin/preview/episode', { ratings_limit: ratingsLimit, ratings_order: ratingsOrder, ratings_exclude: ratingsExclude, badge_style: badgeStyle, label_style: labelStyle, badge_size: badgeSize, position, badge_direction: badgeDirection, blur: blur ? 'true' : undefined, badge_shape: badgeShape, badge_background: badgeBackground, id_type: idType, id_value: idValue })),
getPurgeSources: (): Promise<Response> => get('/api/admin/purge/sources'),
runPurgeCatalog: (transportUrl: string, catalogId: string, catalogType: string): Promise<Response> =>
post('/api/admin/purge', { scope: 'catalog', transport_url: transportUrl, catalog_id: catalogId, catalog_type: catalogType }),
runPurgeLibrary: (accountLabel: string): Promise<Response> =>
post('/api/admin/purge', { scope: 'library', account_label: accountLabel }),
runPurgeLibraryItems: (items: { id: string; item_type: string }[]): Promise<Response> =>
post('/api/admin/purge', { scope: 'library_items', items }),
getLibraryAudit: (): Promise<Response> => get('/api/admin/library/audit'),
runLibraryRefresh: (accountLabel: string, ids: string[]): Promise<Response> =>
post('/api/admin/library/refresh', { account_label: accountLabel, ids }),
}
// --- Self-service API (API key session JWT auth) ---
const KEY_BASE_URL = import.meta.env.VITE_API_URL || ''
function keyRequest(path: string, options: RequestInit = {}): Promise<Response> {
const auth = useAuthStore()
const headers = new Headers(options.headers)
if (auth.apiKeyToken) {
headers.set('Authorization', `Bearer ${auth.apiKeyToken}`)
}
if (options.body && !headers.has('Content-Type')) {
headers.set('Content-Type', 'application/json')
}
return fetch(`${KEY_BASE_URL}${path}`, { ...options, headers })
}
export const selfApi = {
getInfo: (): Promise<Response> => keyRequest('/api/key/me'),
getSettings: (): Promise<Response> => keyRequest('/api/key/me/settings'),
updateSettings: (settings: SaveSettingsPayload): Promise<Response> =>
keyRequest('/api/key/me/settings', {
method: 'PUT',
body: JSON.stringify(settings),
}),
resetSettings: (): Promise<Response> =>
keyRequest('/api/key/me/settings', { method: 'DELETE' }),
previewPoster: (ratingsLimit: number, ratingsOrder: string, posterPosition?: string, badgeStyle?: string, labelStyle?: string, badgeDirection?: string, badgeSize?: string, ratingsExclude?: string, posterSplit?: boolean, badgeShape?: string, badgeBackground?: string, posterFit?: string, idType?: string, idValue?: string): Promise<Response> =>
keyRequest(buildUrl('/api/key/me/preview/poster', { ratings_limit: ratingsLimit, ratings_order: ratingsOrder, ratings_exclude: ratingsExclude, position: posterPosition, badge_style: badgeStyle, label_style: labelStyle, badge_direction: badgeDirection, badge_size: badgeSize, split: posterSplit ? 'true' : undefined, badge_shape: badgeShape, badge_background: badgeBackground, fit: posterFit, id_type: idType, id_value: idValue })),
previewLogo: (ratingsLimit: number, ratingsOrder: string, badgeStyle?: string, labelStyle?: string, badgeSize?: string, ratingsExclude?: string, badgeShape?: string, badgeBackground?: string, idType?: string, idValue?: string): Promise<Response> =>
keyRequest(buildUrl('/api/key/me/preview/logo', { ratings_limit: ratingsLimit, ratings_order: ratingsOrder, ratings_exclude: ratingsExclude, badge_style: badgeStyle, label_style: labelStyle, badge_size: badgeSize, badge_shape: badgeShape, badge_background: badgeBackground, id_type: idType, id_value: idValue })),
previewBackdrop: (ratingsLimit: number, ratingsOrder: string, badgeStyle?: string, labelStyle?: string, badgeSize?: string, position?: string, badgeDirection?: string, ratingsExclude?: string, badgeShape?: string, badgeBackground?: string, edgeInsetX?: number, edgeInsetY?: number, idType?: string, idValue?: string): Promise<Response> =>
keyRequest(buildUrl('/api/key/me/preview/backdrop', { ratings_limit: ratingsLimit, ratings_order: ratingsOrder, ratings_exclude: ratingsExclude, badge_style: badgeStyle, label_style: labelStyle, badge_size: badgeSize, position, badge_direction: badgeDirection, badge_shape: badgeShape, badge_background: badgeBackground, edge_inset_x: edgeInsetX, edge_inset_y: edgeInsetY, id_type: idType, id_value: idValue })),
previewEpisode: (ratingsLimit: number, ratingsOrder: string, badgeStyle?: string, labelStyle?: string, badgeSize?: string, position?: string, badgeDirection?: string, blur?: boolean, ratingsExclude?: string, badgeShape?: string, badgeBackground?: string, idType?: string, idValue?: string): Promise<Response> =>
keyRequest(buildUrl('/api/key/me/preview/episode', { ratings_limit: ratingsLimit, ratings_order: ratingsOrder, ratings_exclude: ratingsExclude, badge_style: badgeStyle, label_style: labelStyle, badge_size: badgeSize, position, badge_direction: badgeDirection, blur: blur ? 'true' : undefined, badge_shape: badgeShape, badge_background: badgeBackground, id_type: idType, id_value: idValue })),
}
export const keysApi = {
list: (): Promise<Response> => get('/api/keys'),
create: (name: string): Promise<Response> => post('/api/keys', { name }),
delete: (id: number): Promise<Response> => del(`/api/keys/${id}`),
getSettings: (id: number): Promise<Response> => get(`/api/keys/${id}/settings`),
updateSettings: (id: number, settings: SaveSettingsPayload): Promise<Response> => put(`/api/keys/${id}/settings`, settings),
deleteSettings: (id: number): Promise<Response> => del(`/api/keys/${id}/settings`),
}
|