mirror of
https://github.com/zimplexing/OrionTV.git
synced 2026-02-04 03:36:29 +08:00
chore: delete backend
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
# The port the backend server will run on
|
||||
PORT=3001
|
||||
|
||||
# Optional: The password for the login endpoint. If not provided, login is disabled.
|
||||
PASSWORD=
|
||||
@@ -1,5 +0,0 @@
|
||||
# The port the backend server will run on
|
||||
PORT=3001
|
||||
|
||||
# Optional: The password for the login endpoint. If not provided, login is disabled.
|
||||
PASSWORD=
|
||||
@@ -1,38 +0,0 @@
|
||||
# --- Build Stage ---
|
||||
FROM node:18-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package.json and yarn.lock first to leverage Docker cache
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
# Copy the rest of the source code
|
||||
COPY . .
|
||||
|
||||
# Compile TypeScript to JavaScript
|
||||
RUN yarn build
|
||||
|
||||
# Prune development dependencies
|
||||
RUN yarn install --production --ignore-scripts --prefer-offline
|
||||
|
||||
|
||||
# --- Production Stage ---
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy production dependencies and compiled code from the builder stage
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
# Copy config.json from the project root relative to the Docker build context
|
||||
# IMPORTANT: When building, run `docker build -f backend/Dockerfile .` from the project root.
|
||||
COPY src/config/config.json dist/config/
|
||||
|
||||
# Expose the port the app runs on
|
||||
EXPOSE 3001
|
||||
|
||||
# The command to run the application
|
||||
# You can override the port using -e PORT=... in `docker run`
|
||||
CMD [ "node", "dist/index.docker.js" ]
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"name": "OrionTV-proxy",
|
||||
"version": "1.0.1",
|
||||
"description": "Backend service for MyTV application",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"start": "node dist/index.js",
|
||||
"build": "tsc",
|
||||
"dev": "ts-node-dev --respawn --transpile-only src/index.docker.ts"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.19.2",
|
||||
"dotenv": "^16.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.14.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
{
|
||||
"cache_time": 7200,
|
||||
"api_site": {
|
||||
"dyttzy": {
|
||||
"api": "http://caiji.dyttzyapi.com/api.php/provide/vod",
|
||||
"name": "电影天堂资源"
|
||||
},
|
||||
"ruyi": {
|
||||
"api": "https://cj.rycjapi.com/api.php/provide/vod",
|
||||
"name": "如意资源"
|
||||
},
|
||||
"mozhua": {
|
||||
"api": "https://mozhuazy.com/api.php/provide/vod",
|
||||
"name": "魔爪资源"
|
||||
},
|
||||
"heimuer": {
|
||||
"api": "https://json.heimuer.xyz/api.php/provide/vod",
|
||||
"name": "黑木耳"
|
||||
},
|
||||
"bfzy": {
|
||||
"api": "https://bfzyapi.com/api.php/provide/vod",
|
||||
"name": "暴风资源"
|
||||
},
|
||||
"tyyszy": {
|
||||
"api": "https://tyyszy.com/api.php/provide/vod",
|
||||
"name": "天涯资源"
|
||||
},
|
||||
"ffzy": {
|
||||
"api": "http://ffzy5.tv/api.php/provide/vod",
|
||||
"name": "非凡影视"
|
||||
},
|
||||
"zy360": {
|
||||
"api": "https://360zy.com/api.php/provide/vod",
|
||||
"name": "360资源"
|
||||
},
|
||||
"iqiyi": {
|
||||
"api": "https://www.iqiyizyapi.com/api.php/provide/vod",
|
||||
"name": "iqiyi资源"
|
||||
},
|
||||
"wolong": {
|
||||
"api": "https://wolongzyw.com/api.php/provide/vod",
|
||||
"name": "卧龙资源"
|
||||
},
|
||||
"hwba": {
|
||||
"api": "https://cjhwba.com/api.php/provide/vod",
|
||||
"name": "华为吧资源"
|
||||
},
|
||||
"jisu": {
|
||||
"api": "https://jszyapi.com/api.php/provide/vod",
|
||||
"name": "极速资源"
|
||||
},
|
||||
"dbzy": {
|
||||
"api": "https://dbzy.tv/api.php/provide/vod",
|
||||
"name": "豆瓣资源"
|
||||
},
|
||||
"mdzy": {
|
||||
"api": "https://www.mdzyapi.com/api.php/provide/vod",
|
||||
"name": "魔都资源"
|
||||
},
|
||||
"zuid": {
|
||||
"api": "https://api.zuidapi.com/api.php/provide/vod",
|
||||
"name": "最大资源"
|
||||
},
|
||||
"yinghua": {
|
||||
"api": "https://m3u8.apiyhzy.com/api.php/provide/vod",
|
||||
"name": "樱花资源"
|
||||
},
|
||||
"wujin": {
|
||||
"api": "https://api.wujinapi.me/api.php/provide/vod",
|
||||
"name": "无尽资源"
|
||||
},
|
||||
"wwzy": {
|
||||
"api": "https://wwzy.tv/api.php/provide/vod",
|
||||
"name": "旺旺短剧"
|
||||
},
|
||||
"ikun": {
|
||||
"api": "https://ikunzyapi.com/api.php/provide/vod",
|
||||
"name": "iKun资源"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export interface ApiSite {
|
||||
key: string;
|
||||
api: string;
|
||||
name: string;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
export interface StorageConfig {
|
||||
type: "localstorage" | "database";
|
||||
database?: {
|
||||
host?: string;
|
||||
port?: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
database?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
cache_time?: number;
|
||||
api_site: {
|
||||
[key: string]: ApiSite;
|
||||
};
|
||||
storage?: StorageConfig;
|
||||
}
|
||||
|
||||
export const API_CONFIG = {
|
||||
search: {
|
||||
path: "?ac=videolist&wd=",
|
||||
pagePath: "?ac=videolist&wd={query}&pg={page}",
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
|
||||
Accept: "application/json",
|
||||
},
|
||||
},
|
||||
detail: {
|
||||
path: "?ac=videolist&ids=",
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
|
||||
Accept: "application/json",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Adjust path to read from project root, not from `backend/`
|
||||
const configPath = path.join(__dirname, "config.json");
|
||||
let cachedConfig: Config;
|
||||
|
||||
try {
|
||||
cachedConfig = JSON.parse(fs.readFileSync(configPath, "utf-8")) as Config;
|
||||
} catch (error) {
|
||||
console.info(`Error reading or parsing config.json at ${configPath}`, error);
|
||||
// Provide a default fallback config to prevent crashes
|
||||
cachedConfig = {
|
||||
api_site: {},
|
||||
cache_time: 300,
|
||||
};
|
||||
}
|
||||
|
||||
export function getConfig(): Config {
|
||||
return cachedConfig;
|
||||
}
|
||||
|
||||
export function getCacheTime(): number {
|
||||
const config = getConfig();
|
||||
return config.cache_time || 300; // 默认5分钟缓存
|
||||
}
|
||||
|
||||
export function getApiSites(): ApiSite[] {
|
||||
const config = getConfig();
|
||||
return Object.entries(config.api_site).map(([key, site]) => ({
|
||||
...site,
|
||||
key,
|
||||
}));
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -1,28 +0,0 @@
|
||||
import express, { Express, Request, Response } from "express";
|
||||
import cors from "cors";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app: Express = express();
|
||||
const port = process.env.PORT || 3001;
|
||||
|
||||
// Middlewares
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Health check route
|
||||
app.get("/", (req: Request, res: Response) => {
|
||||
res.send("MyTV Backend Service is running!");
|
||||
});
|
||||
|
||||
import apiRouter from "./routes";
|
||||
|
||||
// API routes
|
||||
app.use("/api", apiRouter);
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server is running on port ${port}`);
|
||||
});
|
||||
|
||||
export default app;
|
||||
@@ -1,24 +0,0 @@
|
||||
import express, { Express, Request, Response } from "express";
|
||||
import cors from "cors";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app: Express = express();
|
||||
const port = process.env.PORT || 3001;
|
||||
|
||||
// Middlewares
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Health check route
|
||||
app.get("/", (req: Request, res: Response) => {
|
||||
res.send("MyTV Backend Service is running!");
|
||||
});
|
||||
|
||||
import apiRouter from "./routes";
|
||||
|
||||
// API routes
|
||||
app.use("/api", apiRouter);
|
||||
|
||||
export default app;
|
||||
@@ -1,156 +0,0 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import { API_CONFIG, ApiSite, getApiSites, getCacheTime } from "../config";
|
||||
import { VideoDetail } from "../types";
|
||||
import { cleanHtmlTags } from "../utils";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Match m3u8 links
|
||||
const M3U8_PATTERN = /(https?:\/\/[^"'\s]+?\.m3u8)/g;
|
||||
|
||||
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);
|
||||
|
||||
const response = await fetch(detailUrl, {
|
||||
headers: API_CONFIG.detail.headers,
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`详情页请求失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
let matches: string[] = [];
|
||||
|
||||
if (apiSite.key === "ffzy") {
|
||||
const ffzyPattern = /\$(https?:\/\/[^"'\s]+?\/\d{8}\/\d+_[a-f0-9]+\/index\.m3u8)/g;
|
||||
matches = html.match(ffzyPattern) || [];
|
||||
}
|
||||
|
||||
if (matches.length === 0) {
|
||||
const generalPattern = /\$(https?:\/\/[^"'\s]+?\.m3u8)/g;
|
||||
matches = html.match(generalPattern) || [];
|
||||
}
|
||||
|
||||
matches = Array.from(new Set(matches)).map((link: string) => {
|
||||
link = link.substring(1);
|
||||
const parenIndex = link.indexOf("(");
|
||||
return parenIndex > 0 ? link.substring(0, parenIndex) : link;
|
||||
});
|
||||
|
||||
const titleMatch = html.match(/<h1[^>]*>([^<]+)<\/h1>/);
|
||||
const titleText = titleMatch ? titleMatch[1].trim() : "";
|
||||
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 {
|
||||
id,
|
||||
title: titleText,
|
||||
poster: coverUrl,
|
||||
desc: descText,
|
||||
source_name: apiSite.name,
|
||||
source: apiSite.key,
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const response = await fetch(detailUrl, {
|
||||
headers: API_CONFIG.detail.headers,
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`详情请求失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data || !data.list || !Array.isArray(data.list) || data.list.length === 0) {
|
||||
throw new Error("获取到的详情内容无效");
|
||||
}
|
||||
|
||||
const videoDetail = data.list[0];
|
||||
let episodes: string[] = [];
|
||||
|
||||
if (videoDetail.vod_play_url) {
|
||||
const playSources = videoDetail.vod_play_url.split("$$$");
|
||||
if (playSources.length > 0) {
|
||||
const mainSource = playSources[0];
|
||||
const episodeList = mainSource.split("#");
|
||||
episodes = episodeList
|
||||
.map((ep: string) => {
|
||||
const parts = ep.split("$");
|
||||
return parts.length > 1 ? parts[1] : "";
|
||||
})
|
||||
.filter((url: string) => url && (url.startsWith("http://") || url.startsWith("https://")));
|
||||
}
|
||||
}
|
||||
|
||||
if (episodes.length === 0 && videoDetail.vod_content) {
|
||||
const matches = videoDetail.vod_content.match(M3U8_PATTERN) || [];
|
||||
episodes = matches.map((link: string) => link.replace(/^\$/, ""));
|
||||
}
|
||||
|
||||
return {
|
||||
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> {
|
||||
if (!id) {
|
||||
throw new Error("缺少视频ID参数");
|
||||
}
|
||||
if (!/^[\w-]+$/.test(id)) {
|
||||
throw new Error("无效的视频ID格式");
|
||||
}
|
||||
const apiSites = getApiSites();
|
||||
const apiSite = apiSites.find((site) => site.key === sourceCode);
|
||||
if (!apiSite) {
|
||||
throw new Error("无效的API来源");
|
||||
}
|
||||
if (apiSite.detail) {
|
||||
return handleSpecialSourceDetail(id, apiSite);
|
||||
}
|
||||
return getDetailFromApi(apiSite, id);
|
||||
}
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const id = req.query.id as string;
|
||||
const sourceCode = req.query.source as string;
|
||||
|
||||
if (!id || !sourceCode) {
|
||||
return res.status(400).json({ error: "缺少必要参数" });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await getVideoDetail(id, sourceCode);
|
||||
const cacheTime = getCacheTime();
|
||||
res.setHeader("Cache-Control", `public, max-age=${cacheTime}`);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,161 +0,0 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import { getCacheTime } from "../config";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// --- Interfaces ---
|
||||
interface DoubanItem {
|
||||
title: string;
|
||||
poster: string;
|
||||
rate: string;
|
||||
}
|
||||
|
||||
interface DoubanResponse {
|
||||
code: number;
|
||||
message: string;
|
||||
list: DoubanItem[];
|
||||
}
|
||||
|
||||
interface DoubanApiResponse {
|
||||
subjects: Array<{
|
||||
title: string;
|
||||
cover: string;
|
||||
rate: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
// --- Helper Functions ---
|
||||
|
||||
async function fetchDoubanData(url: string): Promise<DoubanApiResponse> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
||||
|
||||
const fetchOptions = {
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
||||
Referer: "https://movie.douban.com/",
|
||||
Accept: "application/json, text/plain, */*",
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, fetchOptions);
|
||||
clearTimeout(timeoutId);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTop250(pageStart: number, res: Response) {
|
||||
const target = `https://movie.douban.com/top250?start=${pageStart}&filter=`;
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
||||
|
||||
const fetchOptions = {
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
||||
Referer: "https://movie.douban.com/",
|
||||
Accept:
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const fetchResponse = await fetch(target, fetchOptions);
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!fetchResponse.ok) {
|
||||
throw new Error(`HTTP error! Status: ${fetchResponse.status}`);
|
||||
}
|
||||
|
||||
const html = await fetchResponse.text();
|
||||
const moviePattern =
|
||||
/<div class="item">[\s\S]*?<img[^>]+alt="([^"]+)"[^>]*src="([^"]+)"[\s\S]*?<span class="rating_num"[^>]*>([^<]+)<\/span>[\s\S]*?<\/div>/g;
|
||||
const movies: DoubanItem[] = [];
|
||||
let match;
|
||||
|
||||
while ((match = moviePattern.exec(html)) !== null) {
|
||||
const title = match[1];
|
||||
const cover = match[2];
|
||||
const rate = match[3] || "";
|
||||
const processedCover = cover.replace(/^http:/, "https:");
|
||||
movies.push({ title, poster: processedCover, rate });
|
||||
}
|
||||
|
||||
const apiResponse: DoubanResponse = {
|
||||
code: 200,
|
||||
message: "获取成功",
|
||||
list: movies,
|
||||
};
|
||||
const cacheTime = getCacheTime();
|
||||
res.setHeader("Cache-Control", `public, max-age=${cacheTime}`);
|
||||
res.json(apiResponse);
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
res.status(500).json({
|
||||
error: "获取豆瓣 Top250 数据失败",
|
||||
details: (error as Error).message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --- Main Route Handler ---
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const { type, tag } = req.query;
|
||||
const pageSize = parseInt((req.query.pageSize as string) || "16");
|
||||
const pageStart = parseInt((req.query.pageStart as string) || "0");
|
||||
|
||||
if (!type || !tag) {
|
||||
return res.status(400).json({ error: "缺少必要参数: type 或 tag" });
|
||||
}
|
||||
if (typeof type !== "string" || !["tv", "movie"].includes(type)) {
|
||||
return res.status(400).json({ error: "type 参数必须是 tv 或 movie" });
|
||||
}
|
||||
if (pageSize < 1 || pageSize > 100) {
|
||||
return res.status(400).json({ error: "pageSize 必须在 1-100 之间" });
|
||||
}
|
||||
if (pageStart < 0) {
|
||||
return res.status(400).json({ error: "pageStart 不能小于 0" });
|
||||
}
|
||||
|
||||
if (tag === "top250") {
|
||||
return handleTop250(pageStart, res);
|
||||
}
|
||||
|
||||
const target = `https://movie.douban.com/j/search_subjects?type=${type}&tag=${tag}&sort=recommend&page_limit=${pageSize}&page_start=${pageStart}`;
|
||||
|
||||
try {
|
||||
const doubanData = await fetchDoubanData(target);
|
||||
const list: DoubanItem[] = doubanData.subjects.map((item) => ({
|
||||
title: item.title,
|
||||
poster: item.cover,
|
||||
rate: item.rate,
|
||||
}));
|
||||
|
||||
const response: DoubanResponse = {
|
||||
code: 200,
|
||||
message: "获取成功",
|
||||
list: list,
|
||||
};
|
||||
const cacheTime = getCacheTime();
|
||||
res.setHeader("Cache-Control", `public, max-age=${cacheTime}`);
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
error: "获取豆瓣数据失败",
|
||||
details: (error as Error).message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,67 +0,0 @@
|
||||
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;
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import { Readable } from "node:stream";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const imageUrl = req.query.url as string;
|
||||
|
||||
if (!imageUrl) {
|
||||
return res.status(400).send("Missing image URL");
|
||||
}
|
||||
|
||||
try {
|
||||
const imageResponse = await fetch(imageUrl, {
|
||||
headers: {
|
||||
Referer: "https://movie.douban.com/",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
||||
},
|
||||
});
|
||||
|
||||
if (!imageResponse.ok) {
|
||||
return res.status(imageResponse.status).send(imageResponse.statusText);
|
||||
}
|
||||
|
||||
const contentType = imageResponse.headers.get("content-type");
|
||||
if (contentType) {
|
||||
res.setHeader("Content-Type", contentType);
|
||||
}
|
||||
|
||||
if (imageResponse.body) {
|
||||
const nodeStream = Readable.fromWeb(imageResponse.body as any);
|
||||
nodeStream.pipe(res);
|
||||
} else {
|
||||
res.status(500).send("Image response has no body");
|
||||
}
|
||||
} catch (error) {
|
||||
console.info("Image proxy error:", error);
|
||||
res.status(500).send("Error fetching image");
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Router } from "express";
|
||||
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);
|
||||
router.use("/image-proxy", imageProxyRouter);
|
||||
|
||||
export default router;
|
||||
@@ -1,58 +0,0 @@
|
||||
import express, { Request, Response } from "express";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const router = express.Router();
|
||||
const username = process.env.USERNAME;
|
||||
const password = process.env.PASSWORD;
|
||||
|
||||
/**
|
||||
* @api {post} /api/login User Login
|
||||
* @apiName UserLogin
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiBody {String} username User's username.
|
||||
* @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) => {
|
||||
const { username: inputUsername, password: inputPassword } = req.body;
|
||||
|
||||
// Compatibility with old versions, if username is not set, only password is required
|
||||
if (!username || !password) {
|
||||
if (inputPassword === password) {
|
||||
res.cookie("auth", "true", { httpOnly: true, maxAge: 24 * 60 * 60 * 1000 });
|
||||
return res.json({ ok: true });
|
||||
} else if (!password) {
|
||||
// If no password is set, login is always successful.
|
||||
return res.json({ ok: true });
|
||||
} else {
|
||||
return res.status(400).json({ message: "Invalid password" });
|
||||
}
|
||||
}
|
||||
|
||||
if (inputUsername === username && inputPassword === password) {
|
||||
res.cookie("auth", "true", { httpOnly: true, maxAge: 24 * 60 * 60 * 1000 });
|
||||
res.json({ ok: true });
|
||||
} else {
|
||||
res.status(400).json({ message: "Invalid username or password" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,59 +0,0 @@
|
||||
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;
|
||||
@@ -1,270 +0,0 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import { API_CONFIG, ApiSite, getApiSites, getCacheTime } from "../config";
|
||||
import { cleanHtmlTags } from "../utils";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// 根据环境变量决定最大搜索页数,默认 5
|
||||
const MAX_SEARCH_PAGES: number = Number(process.env.SEARCH_MAX_PAGE) || 5;
|
||||
|
||||
export interface SearchResult {
|
||||
id: string;
|
||||
title: string;
|
||||
poster: string;
|
||||
episodes: string[];
|
||||
source: string;
|
||||
source_name: string;
|
||||
class?: string;
|
||||
year: string;
|
||||
desc?: string;
|
||||
type_name?: string;
|
||||
}
|
||||
|
||||
interface ApiSearchItem {
|
||||
vod_id: string;
|
||||
vod_name: string;
|
||||
vod_pic: string;
|
||||
vod_remarks?: string;
|
||||
vod_play_url?: string;
|
||||
vod_class?: string;
|
||||
vod_year?: string;
|
||||
vod_content?: string;
|
||||
type_name?: string;
|
||||
}
|
||||
|
||||
async function searchFromApi(
|
||||
apiSite: ApiSite,
|
||||
query: string
|
||||
): Promise<SearchResult[]> {
|
||||
try {
|
||||
const apiBaseUrl = apiSite.api;
|
||||
const apiUrl =
|
||||
apiBaseUrl + API_CONFIG.search.path + encodeURIComponent(query);
|
||||
const apiName = apiSite.name;
|
||||
|
||||
// 添加超时处理
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 8000);
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
headers: API_CONFIG.search.headers,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log(
|
||||
"apiUrl",
|
||||
apiSite.name,
|
||||
"response status",
|
||||
response.ok,
|
||||
"response data",
|
||||
data.list.length
|
||||
);
|
||||
|
||||
if (
|
||||
!data ||
|
||||
!data.list ||
|
||||
!Array.isArray(data.list) ||
|
||||
data.list.length === 0
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
// 处理第一页结果
|
||||
const results = data.list.map((item: ApiSearchItem) => {
|
||||
let episodes: string[] = [];
|
||||
|
||||
// 使用正则表达式从 vod_play_url 提取 m3u8 链接
|
||||
if (item.vod_play_url) {
|
||||
const m3u8Regex = /\$(https?:\/\/[^"'\s]+?\.m3u8)/g;
|
||||
// 先用 $$$ 分割
|
||||
const vod_play_url_array = item.vod_play_url.split("$$$");
|
||||
// 对每个分片做匹配,取匹配到最多的作为结果
|
||||
vod_play_url_array.forEach((url: string) => {
|
||||
const matches = url.match(m3u8Regex) || [];
|
||||
if (matches.length > episodes.length) {
|
||||
episodes = matches;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
episodes = Array.from(new Set(episodes)).map((link: string) => {
|
||||
link = link.substring(1); // 去掉开头的 $
|
||||
const parenIndex = link.indexOf("(");
|
||||
return parenIndex > 0 ? link.substring(0, parenIndex) : link;
|
||||
});
|
||||
|
||||
return {
|
||||
id: item.vod_id,
|
||||
title: item.vod_name,
|
||||
poster: item.vod_pic,
|
||||
episodes,
|
||||
source: apiSite.key,
|
||||
source_name: apiName,
|
||||
class: item.vod_class,
|
||||
year: item.vod_year ? item.vod_year.match(/\d{4}/)?.[0] || "" : "",
|
||||
desc: cleanHtmlTags(item.vod_content || ""),
|
||||
type_name: item.type_name,
|
||||
};
|
||||
});
|
||||
|
||||
// 获取总页数
|
||||
const pageCount = data.pagecount || 1;
|
||||
// 确定需要获取的额外页数
|
||||
const pagesToFetch = Math.min(pageCount - 1, MAX_SEARCH_PAGES - 1);
|
||||
|
||||
// 如果有额外页数,获取更多页的结果
|
||||
if (pagesToFetch > 0) {
|
||||
const additionalPagePromises = [];
|
||||
|
||||
for (let page = 2; page <= pagesToFetch + 1; page++) {
|
||||
const pageUrl =
|
||||
apiBaseUrl +
|
||||
API_CONFIG.search.pagePath
|
||||
.replace("{query}", encodeURIComponent(query))
|
||||
.replace("{page}", page.toString());
|
||||
|
||||
const pagePromise = (async () => {
|
||||
try {
|
||||
const pageController = new AbortController();
|
||||
const pageTimeoutId = setTimeout(
|
||||
() => pageController.abort(),
|
||||
8000
|
||||
);
|
||||
|
||||
const pageResponse = await fetch(pageUrl, {
|
||||
headers: API_CONFIG.search.headers,
|
||||
signal: pageController.signal,
|
||||
});
|
||||
|
||||
clearTimeout(pageTimeoutId);
|
||||
|
||||
if (!pageResponse.ok) return [];
|
||||
|
||||
const pageData = await pageResponse.json();
|
||||
|
||||
if (!pageData || !pageData.list || !Array.isArray(pageData.list))
|
||||
return [];
|
||||
|
||||
return pageData.list.map((item: ApiSearchItem) => {
|
||||
let episodes: string[] = [];
|
||||
|
||||
if (item.vod_play_url) {
|
||||
const m3u8Regex = /\$(https?:\/\/[^"'\s]+?\.m3u8)/g;
|
||||
episodes = item.vod_play_url.match(m3u8Regex) || [];
|
||||
}
|
||||
|
||||
episodes = Array.from(new Set(episodes)).map((link: string) => {
|
||||
link = link.substring(1); // 去掉开头的 $
|
||||
const parenIndex = link.indexOf("(");
|
||||
return parenIndex > 0 ? link.substring(0, parenIndex) : link;
|
||||
});
|
||||
|
||||
return {
|
||||
id: item.vod_id,
|
||||
title: item.vod_name,
|
||||
poster: item.vod_pic,
|
||||
episodes,
|
||||
source: apiSite.key,
|
||||
source_name: apiName,
|
||||
class: item.vod_class,
|
||||
year: item.vod_year
|
||||
? item.vod_year.match(/\d{4}/)?.[0] || ""
|
||||
: "",
|
||||
desc: cleanHtmlTags(item.vod_content || ""),
|
||||
type_name: item.type_name,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
})();
|
||||
|
||||
additionalPagePromises.push(pagePromise);
|
||||
}
|
||||
|
||||
const additionalResults = await Promise.all(additionalPagePromises);
|
||||
|
||||
additionalResults.forEach((pageResults) => {
|
||||
if (pageResults.length > 0) {
|
||||
results.push(...pageResults);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const query = req.query.q as string;
|
||||
|
||||
if (!query) {
|
||||
const cacheTime = getCacheTime();
|
||||
res.setHeader("Cache-Control", `public, max-age=${cacheTime}`);
|
||||
return res.json({ results: [] });
|
||||
}
|
||||
|
||||
const apiSites = getApiSites();
|
||||
const searchPromises = apiSites.map((site) => searchFromApi(site, query));
|
||||
|
||||
try {
|
||||
const results = await Promise.all(searchPromises);
|
||||
const flattenedResults = results.flat();
|
||||
const cacheTime = getCacheTime();
|
||||
|
||||
res.setHeader("Cache-Control", `public, max-age=${cacheTime}`);
|
||||
res.json({ results: flattenedResults });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: "搜索失败" });
|
||||
}
|
||||
});
|
||||
|
||||
// 按资源 url 单个获取数据
|
||||
router.get("/one", async (req: Request, res: Response) => {
|
||||
const { resourceId, q } = req.query;
|
||||
|
||||
if (!resourceId || !q) {
|
||||
return res.status(400).json({ error: "resourceId and q are required" });
|
||||
}
|
||||
|
||||
const apiSites = getApiSites();
|
||||
const apiSite = apiSites.find((site) => site.key === (resourceId as string));
|
||||
|
||||
if (!apiSite) {
|
||||
return res.status(404).json({ error: "Resource not found" });
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await searchFromApi(apiSite, q as string);
|
||||
const result = results.filter((r) => r.title === (q as string));
|
||||
|
||||
if (results) {
|
||||
const cacheTime = getCacheTime();
|
||||
res.setHeader("Cache-Control", `public, max-age=${cacheTime}`);
|
||||
res.json({results: result});
|
||||
} else {
|
||||
res.status(404).json({ error: "Resource not found with the given query" });
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: "Failed to fetch resource details" });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取所有可用的资源列表
|
||||
router.get("/resources", async (req: Request, res: Response) => {
|
||||
const apiSites = getApiSites();
|
||||
const cacheTime = getCacheTime();
|
||||
res.setHeader("Cache-Control", `public, max-age=${cacheTime}`);
|
||||
res.json(apiSites);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,66 +0,0 @@
|
||||
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;
|
||||
@@ -1,38 +0,0 @@
|
||||
import express, { Request, Response } from "express";
|
||||
import { getConfig } from "../config";
|
||||
|
||||
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" or "database").
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response (LocalStorage):
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "SiteName": "OrionTV-Local",
|
||||
* "StorageType": "localstorage"
|
||||
* }
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response (Database):
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "SiteName": "OrionTV-Cloud",
|
||||
* "StorageType": "database"
|
||||
* }
|
||||
*/
|
||||
router.get("/server-config", (req: Request, res: Response) => {
|
||||
const config = getConfig();
|
||||
const storageType = config.storage?.type || "database"; // Default to 'database' if not specified
|
||||
|
||||
res.json({
|
||||
SiteName: storageType === "localstorage" ? "OrionTV-Local" : "OrionTV-Cloud",
|
||||
StorageType: storageType,
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,28 +0,0 @@
|
||||
// Data structure for play records
|
||||
export interface PlayRecord {
|
||||
title: string;
|
||||
source_name: string;
|
||||
poster: string;
|
||||
index: number; // Episode number
|
||||
total_episodes: number; // Total number of episodes
|
||||
play_time: number; // Play progress in seconds
|
||||
total_time: number; // Total duration in seconds
|
||||
save_time: number; // Timestamp of when the record was saved
|
||||
user_id: number; // User ID, always 0 in this version
|
||||
}
|
||||
|
||||
// You can add other shared types here
|
||||
export interface VideoDetail {
|
||||
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;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export function cleanHtmlTags(text: string): string {
|
||||
if (!text) return "";
|
||||
return text
|
||||
.replace(/<[^>]+>/g, "\n") // 将 HTML 标签替换为换行
|
||||
.replace(/\n+/g, "\n") // 将多个连续换行合并为一个
|
||||
.replace(/[ \t]+/g, " ") // 将多个连续空格和制表符合并为一个空格,但保留换行符
|
||||
.replace(/^\n+|\n+$/g, "") // 去掉首尾换行
|
||||
.replace(/ /g, " ") // 将 替换为空格
|
||||
.trim(); // 去掉首尾空格
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "CommonJS",
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "**/*.spec.ts"]
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [
|
||||
{
|
||||
"src": "src/index.ts",
|
||||
"use": "@vercel/node",
|
||||
"config": {
|
||||
"includeFiles": ["./config.json"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"src": "/(.*)",
|
||||
"dest": "src/index.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
1025
backend/yarn.lock
1025
backend/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user