265 lines
8.5 KiB
TypeScript
265 lines
8.5 KiB
TypeScript
// Load environment variables from .env file
|
|
import dotenv from "dotenv";
|
|
dotenv.config();
|
|
|
|
import axios from "axios";
|
|
import cors from "cors";
|
|
import express from "express";
|
|
import path from "path";
|
|
import {
|
|
CLOUD_THUMBNAIL_CACHE_DIR,
|
|
IMAGES_DIR,
|
|
SUBTITLES_DIR,
|
|
VIDEOS_DIR,
|
|
} from "./config/paths";
|
|
import { runMigrations } from "./db/migrate";
|
|
import { visitorModeMiddleware } from "./middleware/visitorModeMiddleware";
|
|
import { visitorModeSettingsMiddleware } from "./middleware/visitorModeSettingsMiddleware";
|
|
import apiRoutes from "./routes/api";
|
|
import settingsRoutes from "./routes/settingsRoutes";
|
|
import { cloudflaredService } from "./services/cloudflaredService";
|
|
import downloadManager from "./services/downloadManager";
|
|
import * as storageService from "./services/storageService";
|
|
import { logger } from "./utils/logger";
|
|
import { VERSION } from "./version";
|
|
|
|
// Display version information
|
|
VERSION.displayVersion();
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 5551;
|
|
|
|
// Security: Disable X-Powered-By header to prevent information disclosure
|
|
app.disable("x-powered-by");
|
|
|
|
// Middleware
|
|
app.use(cors());
|
|
// Increase body size limits for large file uploads (10GB)
|
|
app.use(express.json({ limit: "100gb" }));
|
|
app.use(express.urlencoded({ extended: true, limit: "100gb" }));
|
|
|
|
// Initialize storage (create directories, etc.)
|
|
// storageService.initializeStorage(); // Moved inside startServer
|
|
|
|
// Start the server
|
|
const startServer = async () => {
|
|
try {
|
|
// Run database migrations
|
|
await runMigrations();
|
|
|
|
// Initialize storage (create directories, etc.)
|
|
storageService.initializeStorage();
|
|
|
|
// Initialize download manager (restore queued tasks)
|
|
downloadManager.initialize();
|
|
|
|
// Serve static files with proper MIME types
|
|
app.use(
|
|
"/videos",
|
|
express.static(VIDEOS_DIR, {
|
|
setHeaders: (res, path) => {
|
|
if (path.endsWith(".mp4")) {
|
|
res.setHeader("Content-Type", "video/mp4");
|
|
} else if (path.endsWith(".webm")) {
|
|
res.setHeader("Content-Type", "video/webm");
|
|
}
|
|
},
|
|
})
|
|
);
|
|
app.use("/images", express.static(IMAGES_DIR));
|
|
app.use(
|
|
"/api/cloud/thumbnail-cache",
|
|
express.static(CLOUD_THUMBNAIL_CACHE_DIR)
|
|
);
|
|
app.use(
|
|
"/subtitles",
|
|
express.static(SUBTITLES_DIR, {
|
|
setHeaders: (res, path) => {
|
|
if (path.endsWith(".vtt")) {
|
|
res.setHeader("Content-Type", "text/vtt");
|
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
}
|
|
},
|
|
})
|
|
);
|
|
|
|
// Serve Frontend Static Files
|
|
const frontendDist = path.join(__dirname, "../../frontend/dist");
|
|
app.use(express.static(frontendDist));
|
|
|
|
// Cloud storage proxy endpoints
|
|
// Proxy /cloud/videos/* and /cloud/images/* to Alist API
|
|
const proxyCloudFile = async (
|
|
req: express.Request,
|
|
res: express.Response,
|
|
fileType: "video" | "image"
|
|
) => {
|
|
try {
|
|
const { filename } = req.params;
|
|
const settings = storageService.getSettings();
|
|
|
|
if (
|
|
!settings.cloudDriveEnabled ||
|
|
!settings.openListApiUrl ||
|
|
!settings.openListToken
|
|
) {
|
|
return res.status(404).send("Cloud storage not configured");
|
|
}
|
|
|
|
// Construct Alist API URL for file download
|
|
const apiBaseUrl = settings.openListApiUrl.replace("/api/fs/put", "");
|
|
const uploadPath = (settings.cloudDrivePath || "/").replace(/\\/g, "/");
|
|
const normalizedPath = uploadPath.endsWith("/")
|
|
? `${uploadPath}${filename}`
|
|
: `${uploadPath}/${filename}`;
|
|
const filePath = normalizedPath.startsWith("/")
|
|
? normalizedPath
|
|
: `/${normalizedPath}`;
|
|
|
|
// Alist API endpoint for getting file: /api/fs/get (POST with JSON body)
|
|
const alistUrl = `${apiBaseUrl}/api/fs/get`;
|
|
|
|
// Handle range requests for video streaming
|
|
const range = req.headers.range;
|
|
const headers: any = {
|
|
Authorization: settings.openListToken,
|
|
};
|
|
|
|
if (range) {
|
|
headers.Range = range;
|
|
}
|
|
|
|
// Make request to Alist API (POST method with path in body)
|
|
const response = await axios.post(
|
|
alistUrl,
|
|
{ path: filePath },
|
|
{
|
|
headers: headers,
|
|
responseType: "stream",
|
|
maxRedirects: 0,
|
|
validateStatus: (status) => status >= 200 && status < 400,
|
|
}
|
|
);
|
|
|
|
// Set appropriate content type
|
|
const ext = filename.split(".").pop()?.toLowerCase();
|
|
if (fileType === "video") {
|
|
if (ext === "mp4") {
|
|
res.setHeader("Content-Type", "video/mp4");
|
|
} else if (ext === "webm") {
|
|
res.setHeader("Content-Type", "video/webm");
|
|
} else if (ext === "mkv") {
|
|
res.setHeader("Content-Type", "video/x-matroska");
|
|
} else {
|
|
res.setHeader("Content-Type", "application/octet-stream");
|
|
}
|
|
// Support range requests for video streaming
|
|
if (range && response.headers["content-range"]) {
|
|
res.setHeader("Content-Range", response.headers["content-range"]);
|
|
res.status(206); // Partial Content
|
|
}
|
|
if (response.headers["accept-ranges"]) {
|
|
res.setHeader("Accept-Ranges", response.headers["accept-ranges"]);
|
|
}
|
|
} else {
|
|
// Image
|
|
if (ext === "jpg" || ext === "jpeg") {
|
|
res.setHeader("Content-Type", "image/jpeg");
|
|
} else if (ext === "png") {
|
|
res.setHeader("Content-Type", "image/png");
|
|
} else if (ext === "gif") {
|
|
res.setHeader("Content-Type", "image/gif");
|
|
} else {
|
|
res.setHeader("Content-Type", "image/jpeg");
|
|
}
|
|
}
|
|
|
|
// Set content length if available
|
|
if (response.headers["content-length"]) {
|
|
res.setHeader("Content-Length", response.headers["content-length"]);
|
|
}
|
|
|
|
// Stream the file to client
|
|
response.data.pipe(res);
|
|
|
|
response.data.on("error", (err: Error) => {
|
|
logger.error("Error streaming cloud file:", err);
|
|
if (!res.headersSent) {
|
|
res.status(500).send("Error streaming file from cloud storage");
|
|
}
|
|
});
|
|
} catch (error: any) {
|
|
logger.error(
|
|
`Error proxying cloud ${fileType}:`,
|
|
error instanceof Error ? error : new Error(String(error))
|
|
);
|
|
if (!res.headersSent) {
|
|
res.status(500).send(`Error fetching ${fileType} from cloud storage`);
|
|
}
|
|
}
|
|
};
|
|
|
|
app.get("/cloud/videos/:filename", (req, res) =>
|
|
proxyCloudFile(req, res, "video")
|
|
);
|
|
app.get("/cloud/images/:filename", (req, res) =>
|
|
proxyCloudFile(req, res, "image")
|
|
);
|
|
|
|
// API Routes
|
|
// Apply visitor mode middleware to all API routes
|
|
app.use("/api", visitorModeMiddleware, apiRoutes);
|
|
// Use separate middleware for settings that allows disabling visitor mode
|
|
app.use("/api/settings", visitorModeSettingsMiddleware, settingsRoutes);
|
|
|
|
// SPA Fallback for Frontend
|
|
app.get("*", (req, res) => {
|
|
// Don't serve index.html for API calls that 404
|
|
if (req.path.startsWith("/api") || req.path.startsWith("/cloud")) {
|
|
res.status(404).send("Not Found");
|
|
return;
|
|
}
|
|
res.sendFile(path.join(frontendDist, "index.html"));
|
|
});
|
|
|
|
app.listen(PORT, () => {
|
|
console.log(`Server running on port ${PORT}`);
|
|
|
|
// Start subscription scheduler
|
|
import("./services/subscriptionService")
|
|
.then(({ subscriptionService }) => {
|
|
subscriptionService.startScheduler();
|
|
})
|
|
.catch((err) =>
|
|
console.error("Failed to start subscription service:", err)
|
|
);
|
|
|
|
// Run duration backfill in background
|
|
import("./services/metadataService")
|
|
.then((service) => {
|
|
service.backfillDurations();
|
|
})
|
|
.catch((err) =>
|
|
console.error("Failed to start metadata service:", err)
|
|
);
|
|
|
|
// Start Cloudflared tunnel if enabled
|
|
const settings = storageService.getSettings();
|
|
if (settings.cloudflaredTunnelEnabled) {
|
|
if (settings.cloudflaredToken) {
|
|
cloudflaredService.start(settings.cloudflaredToken);
|
|
} else {
|
|
// Quick Tunnel
|
|
const port = typeof PORT === "string" ? parseInt(PORT) : PORT;
|
|
cloudflaredService.start(undefined, port);
|
|
}
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error("Failed to start server:", error);
|
|
process.exit(1);
|
|
}
|
|
};
|
|
|
|
startServer();
|