mirror of
https://github.com/zimplexing/OrionTV.git
synced 2026-02-17 14:14:46 +08:00
feat: Implement user authentication and data management features
- Added LoginModal component for user login functionality. - Introduced API routes for user login, favorites, play records, and search history management. - Created JSON files for storing favorites, play records, and search history. - Updated API service to handle new endpoints and refactored data management to use API calls instead of local storage. - Adjusted data structures in types and services to align with new API responses.
This commit is contained in:
1
backend/src/data/favorites.json
Normal file
1
backend/src/data/favorites.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
backend/src/data/playrecords.json
Normal file
1
backend/src/data/playrecords.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
backend/src/data/searchhistory.json
Normal file
1
backend/src/data/searchhistory.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -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<VideoDetail> {
|
||||
async function handleSpecialSourceDetail(id: string, apiSite: ApiSite): Promise<VideoDetail> {
|
||||
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[^>]*>([^<]+)<\/h1>/);
|
||||
const titleText = titleMatch ? titleMatch[1].trim() : "";
|
||||
const descMatch = html.match(
|
||||
/<div[^>]*class=["']sketch["'][^>]*>([\s\S]*?)<\/div>/
|
||||
);
|
||||
const descMatch = html.match(/<div[^>]*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<VideoDetail> {
|
||||
async function getDetailFromApi(apiSite: ApiSite, id: string): Promise<VideoDetail> {
|
||||
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<VideoDetail> {
|
||||
async function getVideoDetail(id: string, sourceCode: string): Promise<VideoDetail> {
|
||||
if (!id) {
|
||||
throw new Error("缺少视频ID参数");
|
||||
}
|
||||
|
||||
67
backend/src/routes/favorites.ts
Normal file
67
backend/src/routes/favorites.ts
Normal file
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
47
backend/src/routes/login.ts
Normal file
47
backend/src/routes/login.ts
Normal file
@@ -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;
|
||||
59
backend/src/routes/playrecords.ts
Normal file
59
backend/src/routes/playrecords.ts
Normal file
@@ -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;
|
||||
66
backend/src/routes/searchhistory.ts
Normal file
66
backend/src/routes/searchhistory.ts
Normal file
@@ -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<string[]> => {
|
||||
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;
|
||||
27
backend/src/routes/server-config.ts
Normal file
27
backend/src/routes/server-config.ts
Normal file
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user