diff --git a/app/_layout.tsx b/app/_layout.tsx index 072a576..08721d4 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -8,7 +8,7 @@ import Toast from "react-native-toast-message"; import { useSettingsStore } from "@/stores/settingsStore"; import { useRemoteControlStore } from "@/stores/remoteControlStore"; -import { remoteControlService } from "@/services/remoteControlService"; +import LoginModal from "@/components/LoginModal"; // Prevent the splash screen from auto-hiding before asset loading is complete. SplashScreen.preventAutoHideAsync(); @@ -58,6 +58,7 @@ export default function RootLayout() { + ); } diff --git a/app/play.tsx b/app/play.tsx index 553eb18..6104f01 100644 --- a/app/play.tsx +++ b/app/play.tsx @@ -98,7 +98,7 @@ export default function PlayScreen() { style={styles.videoPlayer} source={{ uri: currentEpisode?.url }} usePoster - posterSource={{ uri: detail?.videoInfo.cover ?? "" }} + posterSource={{ uri: detail?.videoInfo.poster ?? "" }} resizeMode={ResizeMode.CONTAIN} onPlaybackStatusUpdate={handlePlaybackStatusUpdate} onLoad={() => { diff --git a/backend/src/data/favorites.json b/backend/src/data/favorites.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/backend/src/data/favorites.json @@ -0,0 +1 @@ +{} diff --git a/backend/src/data/playrecords.json b/backend/src/data/playrecords.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/backend/src/data/playrecords.json @@ -0,0 +1 @@ +{} diff --git a/backend/src/data/searchhistory.json b/backend/src/data/searchhistory.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/backend/src/data/searchhistory.json @@ -0,0 +1 @@ +[] diff --git a/backend/src/routes/detail.ts b/backend/src/routes/detail.ts index 70e54f9..cb9b47a 100644 --- a/backend/src/routes/detail.ts +++ b/backend/src/routes/detail.ts @@ -8,10 +8,7 @@ const router = Router(); // Match m3u8 links const M3U8_PATTERN = /(https?:\/\/[^"'\s]+?\.m3u8)/g; -async function handleSpecialSourceDetail( - id: string, - apiSite: ApiSite -): Promise { +async function handleSpecialSourceDetail(id: string, apiSite: ApiSite): Promise { const detailUrl = `${apiSite.detail}/index.php/vod/detail/id/${id}.html`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); @@ -30,8 +27,7 @@ async function handleSpecialSourceDetail( let matches: string[] = []; if (apiSite.key === "ffzy") { - const ffzyPattern = - /\$(https?:\/\/[^"'\s]+?\/\d{8}\/\d+_[a-f0-9]+\/index\.m3u8)/g; + const ffzyPattern = /\$(https?:\/\/[^"'\s]+?\/\d{8}\/\d+_[a-f0-9]+\/index\.m3u8)/g; matches = html.match(ffzyPattern) || []; } @@ -48,32 +44,22 @@ async function handleSpecialSourceDetail( const titleMatch = html.match(/]*>([^<]+)<\/h1>/); const titleText = titleMatch ? titleMatch[1].trim() : ""; - const descMatch = html.match( - /]*class=["']sketch["'][^>]*>([\s\S]*?)<\/div>/ - ); + const descMatch = html.match(/]*class=["']sketch["'][^>]*>([\s\S]*?)<\/div>/); const descText = descMatch ? cleanHtmlTags(descMatch[1]) : ""; const coverMatch = html.match(/(https?:\/\/[^"'\s]+?\.jpg)/g); const coverUrl = coverMatch ? coverMatch[0].trim() : ""; return { - code: 200, - episodes: matches, - detailUrl, - videoInfo: { - title: titleText, - cover: coverUrl, - desc: descText, - source_name: apiSite.name, - source: apiSite.key, - id, - }, + id, + title: titleText, + poster: coverUrl, + desc: descText, + source_name: apiSite.name, + source: apiSite.key, }; } -async function getDetailFromApi( - apiSite: ApiSite, - id: string -): Promise { +async function getDetailFromApi(apiSite: ApiSite, id: string): Promise { const detailUrl = `${apiSite.api}${API_CONFIG.detail.path}${id}`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); @@ -89,12 +75,7 @@ async function getDetailFromApi( } const data = await response.json(); - if ( - !data || - !data.list || - !Array.isArray(data.list) || - data.list.length === 0 - ) { + if (!data || !data.list || !Array.isArray(data.list) || data.list.length === 0) { throw new Error("获取到的详情内容无效"); } @@ -111,10 +92,7 @@ async function getDetailFromApi( const parts = ep.split("$"); return parts.length > 1 ? parts[1] : ""; }) - .filter( - (url: string) => - url && (url.startsWith("http://") || url.startsWith("https://")) - ); + .filter((url: string) => url && (url.startsWith("http://") || url.startsWith("https://"))); } } @@ -124,30 +102,22 @@ async function getDetailFromApi( } return { - code: 200, - episodes, - detailUrl, - videoInfo: { - title: videoDetail.vod_name, - cover: videoDetail.vod_pic, - desc: cleanHtmlTags(videoDetail.vod_content), - type: videoDetail.type_name, - year: videoDetail.vod_year?.match(/\d{4}/)?.[0] || "", - area: videoDetail.vod_area, - director: videoDetail.vod_director, - actor: videoDetail.vod_actor, - remarks: videoDetail.vod_remarks, - source_name: apiSite.name, - source: apiSite.key, - id, - }, + id, + title: videoDetail.vod_name, + poster: videoDetail.vod_pic, + desc: cleanHtmlTags(videoDetail.vod_content), + type: videoDetail.type_name, + year: videoDetail.vod_year?.match(/\d{4}/)?.[0] || "", + area: videoDetail.vod_area, + director: videoDetail.vod_director, + actor: videoDetail.vod_actor, + remarks: videoDetail.vod_remarks, + source_name: apiSite.name, + source: apiSite.key, }; } -async function getVideoDetail( - id: string, - sourceCode: string -): Promise { +async function getVideoDetail(id: string, sourceCode: string): Promise { if (!id) { throw new Error("缺少视频ID参数"); } diff --git a/backend/src/routes/favorites.ts b/backend/src/routes/favorites.ts new file mode 100644 index 0000000..c7fa2fe --- /dev/null +++ b/backend/src/routes/favorites.ts @@ -0,0 +1,67 @@ +import express, { Request, Response } from "express"; +import fs from "fs/promises"; +import path from "path"; + +const router = express.Router(); +const dataPath = path.join(__dirname, "..", "data", "favorites.json"); + +// Helper function to read data +const readFavorites = async () => { + try { + const data = await fs.readFile(dataPath, "utf-8"); + return JSON.parse(data); + } catch (error) { + // If file doesn't exist or is invalid json, return empty object + return {}; + } +}; + +// Helper function to write data +const writeFavorites = async (data: any) => { + await fs.writeFile(dataPath, JSON.stringify(data, null, 2), "utf-8"); +}; + +// GET /api/favorites +router.get("/favorites", async (req: Request, res: Response) => { + const { key } = req.query; + const favorites = await readFavorites(); + + if (key) { + res.json(favorites[key as string] || null); + } else { + res.json(favorites); + } +}); + +// POST /api/favorites +router.post("/favorites", async (req: Request, res: Response) => { + const { key, favorite } = req.body; + + if (!key || !favorite) { + return res.status(400).json({ message: "Missing key or favorite data" }); + } + + const favorites = await readFavorites(); + favorites[key] = { ...favorite, save_time: Math.floor(Date.now() / 1000) }; + await writeFavorites(favorites); + + res.json({ success: true }); +}); + +// DELETE /api/favorites +router.delete("/favorites", async (req: Request, res: Response) => { + const { key } = req.query; + let favorites = await readFavorites(); + + if (key) { + delete favorites[key as string]; + } else { + // Clear all favorites if no key is provided + favorites = {}; + } + + await writeFavorites(favorites); + res.json({ success: true }); +}); + +export default router; diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index 2ffb2f9..b0b455f 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -3,9 +3,19 @@ import searchRouter from "./search"; import detailRouter from "./detail"; import doubanRouter from "./douban"; import imageProxyRouter from "./image-proxy"; +import serverConfigRouter from "./server-config"; +import loginRouter from "./login"; +import favoritesRouter from "./favorites"; +import playRecordsRouter from "./playrecords"; +import searchHistoryRouter from "./searchhistory"; const router = Router(); +router.use(serverConfigRouter); +router.use(loginRouter); +router.use(favoritesRouter); +router.use(playRecordsRouter); +router.use(searchHistoryRouter); router.use("/search", searchRouter); router.use("/detail", detailRouter); router.use("/douban", doubanRouter); diff --git a/backend/src/routes/login.ts b/backend/src/routes/login.ts new file mode 100644 index 0000000..21f10f3 --- /dev/null +++ b/backend/src/routes/login.ts @@ -0,0 +1,47 @@ +import express, { Request, Response } from "express"; +import dotenv from "dotenv"; + +dotenv.config(); + +const router = express.Router(); +const password = process.env.PASSWORD; + +/** + * @api {post} /api/login User Login + * @apiName UserLogin + * @apiGroup User + * + * @apiBody {String} password User's password. + * + * @apiSuccess {Boolean} ok Indicates if the login was successful. + * + * @apiSuccessExample {json} Success-Response: + * HTTP/1.1 200 OK + * { + * "ok": true + * } + * + * @apiError {String} message Error message. + * + * @apiErrorExample {json} Error-Response: + * HTTP/1.1 400 Bad Request + * { + * "message": "Invalid password" + * } + */ +router.post("/login", (req: Request, res: Response) => { + if (!password) { + // If no password is set, login is always successful. + return res.json({ ok: true }); + } + + const { password: inputPassword } = req.body; + + if (inputPassword === password) { + res.json({ ok: true }); + } else { + res.status(400).json({ message: "Invalid password" }); + } +}); + +export default router; diff --git a/backend/src/routes/playrecords.ts b/backend/src/routes/playrecords.ts new file mode 100644 index 0000000..43f6255 --- /dev/null +++ b/backend/src/routes/playrecords.ts @@ -0,0 +1,59 @@ +import express, { Request, Response } from "express"; +import fs from "fs/promises"; +import path from "path"; + +const router = express.Router(); +const dataPath = path.join(__dirname, "..", "data", "playrecords.json"); + +// Helper function to read data +const readPlayRecords = async () => { + try { + const data = await fs.readFile(dataPath, "utf-8"); + return JSON.parse(data); + } catch (error) { + return {}; + } +}; + +// Helper function to write data +const writePlayRecords = async (data: any) => { + await fs.writeFile(dataPath, JSON.stringify(data, null, 2), "utf-8"); +}; + +// GET /api/playrecords +router.get("/playrecords", async (req: Request, res: Response) => { + const records = await readPlayRecords(); + res.json(records); +}); + +// POST /api/playrecords +router.post("/playrecords", async (req: Request, res: Response) => { + const { key, record } = req.body; + + if (!key || !record) { + return res.status(400).json({ message: "Missing key or record data" }); + } + + const records = await readPlayRecords(); + records[key] = { ...record, time: Math.floor(Date.now() / 1000) }; + await writePlayRecords(records); + + res.json({ success: true }); +}); + +// DELETE /api/playrecords +router.delete("/playrecords", async (req: Request, res: Response) => { + const { key } = req.query; + let records = await readPlayRecords(); + + if (key) { + delete records[key as string]; + } else { + records = {}; + } + + await writePlayRecords(records); + res.json({ success: true }); +}); + +export default router; diff --git a/backend/src/routes/searchhistory.ts b/backend/src/routes/searchhistory.ts new file mode 100644 index 0000000..f814018 --- /dev/null +++ b/backend/src/routes/searchhistory.ts @@ -0,0 +1,66 @@ +import express, { Request, Response } from "express"; +import fs from "fs/promises"; +import path from "path"; + +const router = express.Router(); +const dataPath = path.join(__dirname, "..", "data", "searchhistory.json"); + +// Helper function to read data +const readSearchHistory = async (): Promise => { + try { + const data = await fs.readFile(dataPath, "utf-8"); + return JSON.parse(data); + } catch (error) { + return []; + } +}; + +// Helper function to write data +const writeSearchHistory = async (data: string[]) => { + await fs.writeFile(dataPath, JSON.stringify(data, null, 2), "utf-8"); +}; + +// GET /api/searchhistory +router.get("/searchhistory", async (req: Request, res: Response) => { + const history = await readSearchHistory(); + res.json(history); +}); + +// POST /api/searchhistory +router.post("/searchhistory", async (req: Request, res: Response) => { + const { keyword } = req.body; + + if (!keyword) { + return res.status(400).json({ message: "Missing keyword" }); + } + + let history = await readSearchHistory(); + // Remove keyword if it already exists to move it to the front + history = history.filter((item) => item !== keyword); + // Add to the beginning of the array + history.unshift(keyword); + // Optional: Limit history size + if (history.length > 100) { + history = history.slice(0, 100); + } + + await writeSearchHistory(history); + res.json(history); +}); + +// DELETE /api/searchhistory +router.delete("/searchhistory", async (req: Request, res: Response) => { + const { keyword } = req.query; + let history = await readSearchHistory(); + + if (keyword) { + history = history.filter((item) => item !== keyword); + } else { + history = []; + } + + await writeSearchHistory(history); + res.json({ success: true }); +}); + +export default router; diff --git a/backend/src/routes/server-config.ts b/backend/src/routes/server-config.ts new file mode 100644 index 0000000..ac04294 --- /dev/null +++ b/backend/src/routes/server-config.ts @@ -0,0 +1,27 @@ +import express, { Request, Response } from "express"; + +const router = express.Router(); + +/** + * @api {get} /api/server-config Get Server Configuration + * @apiName GetServerConfig + * @apiGroup Server + * + * @apiSuccess {String} SiteName The name of the site. + * @apiSuccess {String} StorageType The storage type used by the server ("localstorage"). + * + * @apiSuccessExample {json} Success-Response: + * HTTP/1.1 200 OK + * { + * "SiteName": "OrionTV-Local", + * "StorageType": "localstorage" + * } + */ +router.get("/server-config", (req: Request, res: Response) => { + res.json({ + SiteName: "OrionTV-Local", + StorageType: "localstorage", + }); +}); + +export default router; diff --git a/backend/src/types/index.ts b/backend/src/types/index.ts index 984e431..4d67937 100644 --- a/backend/src/types/index.ts +++ b/backend/src/types/index.ts @@ -2,7 +2,7 @@ export interface PlayRecord { title: string; source_name: string; - cover: string; + poster: string; index: number; // Episode number total_episodes: number; // Total number of episodes play_time: number; // Play progress in seconds @@ -13,21 +13,16 @@ export interface PlayRecord { // You can add other shared types here export interface VideoDetail { - code: number; - episodes: string[]; - detailUrl: string; - videoInfo: { - title: string; - cover: string; - desc: string; - source_name: string; - source: string; - id: string; - type?: string; - year?: string; - area?: string; - director?: string; - actor?: string; - remarks?: string; - }; + id: string; + title: string; + poster: string; + source: string; + source_name: string; + desc?: string; + type?: string; + year?: string; + area?: string; + director?: string; + actor?: string; + remarks?: string; } diff --git a/components/LoginModal.tsx b/components/LoginModal.tsx new file mode 100644 index 0000000..e03fdeb --- /dev/null +++ b/components/LoginModal.tsx @@ -0,0 +1,100 @@ +import React, { useState } from "react"; +import { Modal, View, Text, TextInput, StyleSheet, ActivityIndicator } from "react-native"; +import Toast from "react-native-toast-message"; +import useAuthStore from "@/stores/authStore"; +import { api } from "@/services/api"; +import { ThemedView } from "./ThemedView"; +import { ThemedText } from "./ThemedText"; +import { StyledButton } from "./StyledButton"; + +const LoginModal = () => { + const { isLoginModalVisible, hideLoginModal } = useAuthStore(); + const [password, setPassword] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + const handleLogin = async () => { + if (!password) { + Toast.show({ type: "error", text1: "请输入密码" }); + return; + } + setIsLoading(true); + try { + await api.login(password); + Toast.show({ type: "success", text1: "登录成功" }); + hideLoginModal(); + setPassword(""); + } catch (error) { + Toast.show({ type: "error", text1: "登录失败", text2: "密码错误或服务器无法连接" }); + } finally { + setIsLoading(false); + } + }; + + return ( + + + + 需要登录 + 服务器需要验证您的身份 + + + {isLoading && } + + + + + ); +}; + +const styles = StyleSheet.create({ + overlay: { + flex: 1, + backgroundColor: "rgba(0, 0, 0, 0.6)", + justifyContent: "center", + alignItems: "center", + }, + container: { + width: "80%", + maxWidth: 400, + padding: 24, + borderRadius: 12, + alignItems: "center", + }, + title: { + fontSize: 22, + fontWeight: "bold", + marginBottom: 8, + }, + subtitle: { + fontSize: 16, + color: "#ccc", + marginBottom: 20, + textAlign: "center", + }, + input: { + width: "100%", + height: 50, + backgroundColor: "#333", + borderRadius: 8, + paddingHorizontal: 16, + color: "#fff", + fontSize: 16, + marginBottom: 20, + borderWidth: 1, + borderColor: "#555", + }, + button: { + width: "100%", + height: 50, + }, +}); + +export default LoginModal; diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..c3c1994 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,313 @@ +### 服务器配置 + +- **接口地址**: `/api/server-config` +- **请求方法**: `GET` +- **功能说明**: 获取服务器配置信息 +- **请求参数**: 无 +- **返回格式**: + ```json + { + "SiteName": "string", + "StorageType": "string" + } + ``` + StorageType 可选值: +- "localstorage" +- "redis" + +localstorage 方式部署的实例,收藏、播放记录和搜索历史无服务器同步,客户端自行处理即可 + +localstorage 方式部署的实例,登录时只需输入密码,无用户名 + +### 登录校验 + +- **接口地址**: `/api/login` +- **请求方法**: `POST` +- **功能说明**: 用户登录认证 +- **请求参数**: + ```json + { + "password": "string", // 必填,用户密码 + "username": "string" // 选填,用户名(非 localStorage 模式时必填) + } + ``` +- **返回格式**: + ```json + { + "ok": true + } + ``` +- **错误码**: + - `400`: 参数错误或密码错误 + - `500`: 服务器内部错误 + +response 会设置 set-cookie 的 auth 字段,用于后续请求的鉴权 + +后续的所有接口请求时都需要携带 auth 字段,否则会返回 401 错误 + +建议客户端保存用户输入的用户名和密码,在每次 app 启动时请求登录接口获取 cookie + +### 视频搜索接口 + +- **接口地址**: `/api/search` +- **请求方法**: `GET` +- **功能说明**: 搜索视频内容 +- **请求参数**: + - `q`: 搜索关键词(可选,不传返回空结果) +- **返回格式**: + ```json + { + "results": [ + { + "id": "string", // 视频在源站中的 id + "title": "string", // 视频标题 + "poster": "string", // 视频封面 + "source": "string", // 视频源站 key + "source_name": "string", // 视频源站名称 + "class": "string", // 视频分类 + "year": "string", // 视频年份 + "desc": "string", // 视频描述 + "type_name": "string", // 视频类型 + "douban_id": "string" // 视频豆瓣 id + } + ] + } + ``` +- **错误码**: + - `500`: 搜索失败 + +### 视频详情接口 + +- **接口地址**: `/api/detail` +- **请求方法**: `GET` +- **功能说明**: 获取视频详细信息 +- **请求参数**: + - `id`: 视频 ID(必填) + - `source`: 视频来源代码(必填) +- **返回格式**: + ```json + { + "id": "string", // 视频在源站中的 id + "title": "string", // 视频标题 + "poster": "string", // 视频封面 + "source": "string", // 视频源站 key + "source_name": "string", // 视频源站名称 + "class": "string", // 视频分类 + "year": "string", // 视频年份 + "desc": "string", // 视频描述 + "type_name": "string", // 视频类型 + "douban_id": "string" // 视频豆瓣 id + } + ``` +- **错误码**: + - `400`: 缺少必要参数或无效参数 + - `500`: 获取详情失败 + +### 豆瓣数据接口 + +- **接口地址**: `/api/douban` +- **请求方法**: `GET` +- **功能说明**: 获取豆瓣电影/电视剧数据 +- **请求参数**: + - `type`: 类型,必须是 `tv` 或 `movie`(必填) + - `tag`: 标签,如 `热门`、`最新` 等(必填) + - `pageSize`: 每页数量,1-100 之间(可选,默认 16) + - `pageStart`: 起始位置,不能小于 0(可选,默认 0) +- **返回格式**: + ```json + { + "code": 200, + "message": "获取成功", + "list": [ + { + "id": "string", + "title": "string", + "poster": "string", + "rate": "string" + } + ] + } + ``` +- **错误码**: + - `400`: 参数错误 + - `500`: 获取豆瓣数据失败 + +### 用户数据接口 + +#### 收藏管理 + +- **接口地址**: `/api/favorites` +- **请求方法**: `GET` / `POST` / `DELETE` +- **功能说明**: 管理用户收藏 +- **认证**: 需要认证 + +##### GET 请求 - 获取收藏 + +- **请求参数**: + - `key`: 收藏项 key(可选,格式为 `source+id`) +- **返回格式**: + + ```json + // 不带key参数时返回所有收藏 + { + "source+id": { + "title": "string", + "poster": "string", + "source_name": "string", + "save_time": 1234567890 + } + } + + // 带key参数时返回单个收藏或null + { + "title": "string", + "poster": "string", + "source_name": "string", + "save_time": 1234567890 + } + ``` + +##### POST 请求 - 添加收藏 + +- **请求参数**: + ```json + { + "key": "string", // 必填,格式为 source+id + "favorite": { + "title": "string", + "poster": "string", + "source_name": "string", + "save_time": 1234567890 + } + } + ``` +- **返回格式**: + ```json + { + "success": true + } + ``` + +##### DELETE 请求 - 删除收藏 + +- **请求参数**: + - `key`: 收藏项 key(可选,不传则清空所有收藏) +- **返回格式**: + + ```json + { + "success": true + } + ``` + +- **错误码**: + - `400`: 参数错误 + - `401`: 未认证 + - `500`: 服务器内部错误 + +#### 播放记录管理 + +- **接口地址**: `/api/playrecords` +- **请求方法**: `GET` / `POST` / `DELETE` +- **功能说明**: 管理用户播放记录 +- **认证**: 需要认证 + +##### GET 请求 - 获取播放记录 + +- **请求参数**: 无 +- **返回格式**: + ```json + { + "source+id": { + "title": "string", + "poster": "string", + "source_name": "string", + "index": 1, + "time": 1234567890 + } + } + ``` + +##### POST 请求 - 保存播放记录 + +- **请求参数**: + ```json + { + "key": "string", // 必填,格式为 source+id + "record": { + "title": "string", + "poster": "string", + "source_name": "string", + "index": 1, + "time": 1234567890 + } + } + ``` +- **返回格式**: + ```json + { + "success": true + } + ``` + +##### DELETE 请求 - 删除播放记录 + +- **请求参数**: + - `key`: 播放记录 key(可选,不传则清空所有记录) +- **返回格式**: + + ```json + { + "success": true + } + ``` + +- **错误码**: + - `400`: 参数错误 + - `401`: 未认证 + - `500`: 服务器内部错误 + +#### 搜索历史管理 + +- **接口地址**: `/api/searchhistory` +- **请求方法**: `GET` / `POST` / `DELETE` +- **功能说明**: 管理用户搜索历史 +- **认证**: 需要认证 + +##### GET 请求 - 获取搜索历史 + +- **请求参数**: 无 +- **返回格式**: + ```json + ["搜索关键词1", "搜索关键词2"] + ``` + +##### POST 请求 - 添加搜索历史 + +- **请求参数**: + ```json + { + "keyword": "string" // 必填,搜索关键词 + } + ``` +- **返回格式**: + ```json + ["搜索关键词1", "搜索关键词2"] + ``` + +##### DELETE 请求 - 删除搜索历史 + +- **请求参数**: + - `keyword`: 要删除的关键词(可选,不传则清空所有历史) +- **返回格式**: + + ```json + { + "success": true + } + ``` + +- **错误码**: + - `400`: 参数错误 + - `401`: 未认证 + - `500`: 服务器内部错误 diff --git a/hooks/usePlaybackManager.ts b/hooks/usePlaybackManager.ts deleted file mode 100644 index 1b2cfba..0000000 --- a/hooks/usePlaybackManager.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { useState, useRef, useEffect } from "react"; -import { useLocalSearchParams } from "expo-router"; -import { Video, AVPlaybackStatus } from "expo-av"; -import { api, VideoDetail } from "@/services/api"; -import { PlayRecordManager } from "@/services/storage"; -import { getResolutionFromM3U8 } from "@/services/m3u8"; - -interface Episode { - title?: string; - url: string; -} - -interface Source { - name?: string; - url: string; -} - -export const usePlaybackManager = (videoRef: React.RefObject