711 lines
22 KiB
TypeScript
711 lines
22 KiB
TypeScript
import { exec } from "child_process";
|
|
import { Request, Response } from "express";
|
|
import fs from "fs-extra";
|
|
import multer from "multer";
|
|
import path from "path";
|
|
import { IMAGES_DIR, VIDEOS_DIR } from "../config/paths";
|
|
import downloadManager from "../services/downloadManager";
|
|
import * as downloadService from "../services/downloadService";
|
|
import { getVideoDuration } from "../services/metadataService";
|
|
import * as storageService from "../services/storageService";
|
|
import {
|
|
extractBilibiliVideoId,
|
|
extractUrlFromText,
|
|
isBilibiliUrl,
|
|
isValidUrl,
|
|
resolveShortUrl,
|
|
trimBilibiliUrl
|
|
} from "../utils/helpers";
|
|
|
|
// Configure Multer for file uploads
|
|
const storage = multer.diskStorage({
|
|
destination: (_req, _file, cb) => {
|
|
fs.ensureDirSync(VIDEOS_DIR);
|
|
cb(null, VIDEOS_DIR);
|
|
},
|
|
filename: (_req, file, cb) => {
|
|
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
|
cb(null, uniqueSuffix + path.extname(file.originalname));
|
|
}
|
|
});
|
|
|
|
export const upload = multer({ storage: storage });
|
|
|
|
// Search for videos
|
|
export const searchVideos = async (req: Request, res: Response): Promise<any> => {
|
|
try {
|
|
const { query } = req.query;
|
|
|
|
if (!query) {
|
|
return res.status(400).json({ error: "Search query is required" });
|
|
}
|
|
|
|
const results = await downloadService.searchYouTube(query as string);
|
|
res.status(200).json({ results });
|
|
} catch (error: any) {
|
|
console.error("Error searching for videos:", error);
|
|
res.status(500).json({
|
|
error: "Failed to search for videos",
|
|
details: error.message,
|
|
});
|
|
}
|
|
};
|
|
|
|
// Download video
|
|
export const downloadVideo = async (req: Request, res: Response): Promise<any> => {
|
|
try {
|
|
const { youtubeUrl, downloadAllParts, collectionName, downloadCollection, collectionInfo } = req.body;
|
|
let videoUrl = youtubeUrl;
|
|
|
|
if (!videoUrl) {
|
|
return res.status(400).json({ error: "Video URL is required" });
|
|
}
|
|
|
|
console.log("Processing download request for input:", videoUrl);
|
|
|
|
// Extract URL if the input contains text with a URL
|
|
videoUrl = extractUrlFromText(videoUrl);
|
|
console.log("Extracted URL:", videoUrl);
|
|
|
|
// Check if the input is a valid URL
|
|
if (!isValidUrl(videoUrl)) {
|
|
// If not a valid URL, treat it as a search term
|
|
return res.status(400).json({
|
|
error: "Not a valid URL",
|
|
isSearchTerm: true,
|
|
searchTerm: videoUrl,
|
|
});
|
|
}
|
|
|
|
// Determine initial title for the download task
|
|
let initialTitle = "Video";
|
|
try {
|
|
// Resolve shortened URLs (like b23.tv) first to get correct info
|
|
if (videoUrl.includes("b23.tv")) {
|
|
videoUrl = await resolveShortUrl(videoUrl);
|
|
console.log("Resolved shortened URL to:", videoUrl);
|
|
}
|
|
|
|
if (videoUrl.includes("youtube.com") || videoUrl.includes("youtu.be") || isBilibiliUrl(videoUrl) || videoUrl.includes("missav")) {
|
|
console.log("Fetching video info for title...");
|
|
const info = await downloadService.getVideoInfo(videoUrl);
|
|
if (info && info.title) {
|
|
initialTitle = info.title;
|
|
console.log("Fetched initial title:", initialTitle);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.warn("Failed to fetch video info for title, using default:", err);
|
|
if (videoUrl.includes("youtube.com") || videoUrl.includes("youtu.be")) {
|
|
initialTitle = "YouTube Video";
|
|
} else if (isBilibiliUrl(videoUrl)) {
|
|
initialTitle = "Bilibili Video";
|
|
}
|
|
}
|
|
|
|
// Generate a unique ID for this download task
|
|
const downloadId = Date.now().toString();
|
|
|
|
// Define the download task function
|
|
const downloadTask = async (registerCancel: (cancel: () => void) => void) => {
|
|
// Trim Bilibili URL if needed
|
|
if (isBilibiliUrl(videoUrl)) {
|
|
videoUrl = trimBilibiliUrl(videoUrl);
|
|
console.log("Using trimmed Bilibili URL:", videoUrl);
|
|
|
|
// If downloadCollection is true, handle collection/series download
|
|
if (downloadCollection && collectionInfo) {
|
|
console.log("Downloading Bilibili collection/series");
|
|
|
|
const result = await downloadService.downloadBilibiliCollection(
|
|
collectionInfo,
|
|
collectionName,
|
|
downloadId
|
|
);
|
|
|
|
if (result.success) {
|
|
return {
|
|
success: true,
|
|
collectionId: result.collectionId,
|
|
videosDownloaded: result.videosDownloaded,
|
|
isCollection: true
|
|
};
|
|
} else {
|
|
throw new Error(result.error || "Failed to download collection/series");
|
|
}
|
|
}
|
|
|
|
// If downloadAllParts is true, handle multi-part download
|
|
if (downloadAllParts) {
|
|
const videoId = extractBilibiliVideoId(videoUrl);
|
|
if (!videoId) {
|
|
throw new Error("Could not extract Bilibili video ID");
|
|
}
|
|
|
|
// Get video info to determine number of parts
|
|
const partsInfo = await downloadService.checkBilibiliVideoParts(videoId);
|
|
|
|
if (!partsInfo.success) {
|
|
throw new Error("Failed to get video parts information");
|
|
}
|
|
|
|
const { videosNumber, title } = partsInfo;
|
|
|
|
// Update title in storage
|
|
storageService.addActiveDownload(downloadId, title || "Bilibili Video");
|
|
|
|
// Create a collection for the multi-part video if collectionName is provided
|
|
let collectionId: string | null = null;
|
|
if (collectionName) {
|
|
const newCollection = {
|
|
id: Date.now().toString(),
|
|
name: collectionName,
|
|
videos: [],
|
|
createdAt: new Date().toISOString(),
|
|
title: collectionName,
|
|
};
|
|
storageService.saveCollection(newCollection);
|
|
collectionId = newCollection.id;
|
|
}
|
|
|
|
// Start downloading the first part
|
|
const baseUrl = videoUrl.split("?")[0];
|
|
const firstPartUrl = `${baseUrl}?p=1`;
|
|
|
|
// Download the first part
|
|
const firstPartResult = await downloadService.downloadSingleBilibiliPart(
|
|
firstPartUrl,
|
|
1,
|
|
videosNumber,
|
|
title || "Bilibili Video"
|
|
);
|
|
|
|
// Add to collection if needed
|
|
if (collectionId && firstPartResult.videoData) {
|
|
storageService.atomicUpdateCollection(collectionId, (collection) => {
|
|
collection.videos.push(firstPartResult.videoData!.id);
|
|
return collection;
|
|
});
|
|
}
|
|
|
|
// Set up background download for remaining parts
|
|
// Note: We don't await this, it runs in background
|
|
if (videosNumber > 1) {
|
|
downloadService.downloadRemainingBilibiliParts(
|
|
baseUrl,
|
|
2,
|
|
videosNumber,
|
|
title || "Bilibili Video",
|
|
collectionId!,
|
|
downloadId // Pass downloadId to track progress
|
|
);
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
video: firstPartResult.videoData,
|
|
isMultiPart: true,
|
|
totalParts: videosNumber,
|
|
collectionId,
|
|
};
|
|
} else {
|
|
// Regular single video download for Bilibili
|
|
console.log("Downloading single Bilibili video part");
|
|
|
|
const result = await downloadService.downloadSingleBilibiliPart(
|
|
videoUrl,
|
|
1,
|
|
1,
|
|
"" // seriesTitle not used when totalParts is 1
|
|
);
|
|
|
|
if (result.success) {
|
|
return { success: true, video: result.videoData };
|
|
} else {
|
|
throw new Error(result.error || "Failed to download Bilibili video");
|
|
}
|
|
}
|
|
} else if (videoUrl.includes("missav")) {
|
|
// MissAV download
|
|
const videoData = await downloadService.downloadMissAVVideo(videoUrl, downloadId, registerCancel);
|
|
return { success: true, video: videoData };
|
|
} else {
|
|
// YouTube download
|
|
const videoData = await downloadService.downloadYouTubeVideo(videoUrl, downloadId, registerCancel);
|
|
return { success: true, video: videoData };
|
|
}
|
|
};
|
|
|
|
// Determine type
|
|
let type = 'youtube';
|
|
if (videoUrl.includes("missav")) {
|
|
type = 'missav';
|
|
} else if (isBilibiliUrl(videoUrl)) {
|
|
type = 'bilibili';
|
|
}
|
|
|
|
// Add to download manager
|
|
downloadManager.addDownload(downloadTask, downloadId, initialTitle, videoUrl, type)
|
|
.then((result: any) => {
|
|
console.log("Download completed successfully:", result);
|
|
})
|
|
.catch((error: any) => {
|
|
console.error("Download failed:", error);
|
|
});
|
|
|
|
// Return success immediately indicating the download is queued/started
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "Download queued",
|
|
downloadId
|
|
});
|
|
|
|
} catch (error: any) {
|
|
console.error("Error queuing download:", error);
|
|
res
|
|
.status(500)
|
|
.json({ error: "Failed to queue download", details: error.message });
|
|
}
|
|
};
|
|
|
|
// Get all videos
|
|
export const getVideos = (_req: Request, res: Response): void => {
|
|
try {
|
|
const videos = storageService.getVideos();
|
|
res.status(200).json(videos);
|
|
} catch (error) {
|
|
console.error("Error fetching videos:", error);
|
|
res.status(500).json({ error: "Failed to fetch videos" });
|
|
}
|
|
};
|
|
|
|
// Get video by ID
|
|
export const getVideoById = (req: Request, res: Response): any => {
|
|
try {
|
|
const { id } = req.params;
|
|
const video = storageService.getVideoById(id);
|
|
|
|
if (!video) {
|
|
return res.status(404).json({ error: "Video not found" });
|
|
}
|
|
|
|
res.status(200).json(video);
|
|
} catch (error) {
|
|
console.error("Error fetching video:", error);
|
|
res.status(500).json({ error: "Failed to fetch video" });
|
|
}
|
|
};
|
|
|
|
// Delete video
|
|
export const deleteVideo = (req: Request, res: Response): any => {
|
|
try {
|
|
const { id } = req.params;
|
|
const success = storageService.deleteVideo(id);
|
|
|
|
if (!success) {
|
|
return res.status(404).json({ error: "Video not found" });
|
|
}
|
|
|
|
res
|
|
.status(200)
|
|
.json({ success: true, message: "Video deleted successfully" });
|
|
} catch (error) {
|
|
console.error("Error deleting video:", error);
|
|
res.status(500).json({ error: "Failed to delete video" });
|
|
}
|
|
};
|
|
|
|
// Get download status
|
|
export const getDownloadStatus = (_req: Request, res: Response): void => {
|
|
try {
|
|
const status = storageService.getDownloadStatus();
|
|
res.status(200).json(status);
|
|
} catch (error) {
|
|
console.error("Error fetching download status:", error);
|
|
res.status(500).json({ error: "Failed to fetch download status" });
|
|
}
|
|
};
|
|
|
|
// Check Bilibili parts
|
|
export const checkBilibiliParts = async (req: Request, res: Response): Promise<any> => {
|
|
try {
|
|
const { url } = req.query;
|
|
|
|
if (!url) {
|
|
return res.status(400).json({ error: "URL is required" });
|
|
}
|
|
|
|
if (!isBilibiliUrl(url as string)) {
|
|
return res.status(400).json({ error: "Not a valid Bilibili URL" });
|
|
}
|
|
|
|
// Resolve shortened URLs (like b23.tv)
|
|
let videoUrl = url as string;
|
|
if (videoUrl.includes("b23.tv")) {
|
|
videoUrl = await resolveShortUrl(videoUrl);
|
|
console.log("Resolved shortened URL to:", videoUrl);
|
|
}
|
|
|
|
// Trim Bilibili URL if needed
|
|
videoUrl = trimBilibiliUrl(videoUrl);
|
|
|
|
// Extract video ID
|
|
const videoId = extractBilibiliVideoId(videoUrl);
|
|
|
|
if (!videoId) {
|
|
return res
|
|
.status(400)
|
|
.json({ error: "Could not extract Bilibili video ID" });
|
|
}
|
|
|
|
const result = await downloadService.checkBilibiliVideoParts(videoId);
|
|
|
|
res.status(200).json(result);
|
|
} catch (error: any) {
|
|
console.error("Error checking Bilibili video parts:", error);
|
|
res.status(500).json({
|
|
error: "Failed to check Bilibili video parts",
|
|
details: error.message,
|
|
});
|
|
}
|
|
};
|
|
|
|
// Check if Bilibili URL is a collection or series
|
|
export const checkBilibiliCollection = async (req: Request, res: Response): Promise<any> => {
|
|
try {
|
|
const { url } = req.query;
|
|
|
|
if (!url) {
|
|
return res.status(400).json({ error: "URL is required" });
|
|
}
|
|
|
|
if (!isBilibiliUrl(url as string)) {
|
|
return res.status(400).json({ error: "Not a valid Bilibili URL" });
|
|
}
|
|
|
|
// Resolve shortened URLs (like b23.tv)
|
|
let videoUrl = url as string;
|
|
if (videoUrl.includes("b23.tv")) {
|
|
videoUrl = await resolveShortUrl(videoUrl);
|
|
console.log("Resolved shortened URL to:", videoUrl);
|
|
}
|
|
|
|
// Trim Bilibili URL if needed
|
|
videoUrl = trimBilibiliUrl(videoUrl);
|
|
|
|
// Extract video ID
|
|
const videoId = extractBilibiliVideoId(videoUrl);
|
|
|
|
if (!videoId) {
|
|
return res
|
|
.status(400)
|
|
.json({ error: "Could not extract Bilibili video ID" });
|
|
}
|
|
|
|
// Check if it's a collection or series
|
|
const result = await downloadService.checkBilibiliCollectionOrSeries(videoId);
|
|
|
|
res.status(200).json(result);
|
|
} catch (error: any) {
|
|
console.error("Error checking Bilibili collection/series:", error);
|
|
res.status(500).json({
|
|
error: "Failed to check Bilibili collection/series",
|
|
details: error.message,
|
|
});
|
|
}
|
|
};
|
|
|
|
// Get video comments
|
|
export const getVideoComments = async (req: Request, res: Response): Promise<any> => {
|
|
try {
|
|
const { id } = req.params;
|
|
const comments = await import("../services/commentService").then(m => m.getComments(id));
|
|
res.status(200).json(comments);
|
|
} catch (error) {
|
|
console.error("Error fetching video comments:", error);
|
|
res.status(500).json({ error: "Failed to fetch video comments" });
|
|
}
|
|
};
|
|
|
|
|
|
// Upload video
|
|
export const uploadVideo = async (req: Request, res: Response): Promise<any> => {
|
|
try {
|
|
if (!req.file) {
|
|
return res.status(400).json({ error: "No video file uploaded" });
|
|
}
|
|
|
|
const { title, author } = req.body;
|
|
const videoId = Date.now().toString();
|
|
const videoFilename = req.file.filename;
|
|
const thumbnailFilename = `${path.parse(videoFilename).name}.jpg`;
|
|
|
|
const videoPath = path.join(VIDEOS_DIR, videoFilename);
|
|
const thumbnailPath = path.join(IMAGES_DIR, thumbnailFilename);
|
|
|
|
// Generate thumbnail
|
|
await new Promise<void>((resolve, _reject) => {
|
|
exec(`ffmpeg -i "${videoPath}" -ss 00:00:00 -vframes 1 "${thumbnailPath}"`, (error) => {
|
|
if (error) {
|
|
console.error("Error generating thumbnail:", error);
|
|
// We resolve anyway to not block the upload, just without a custom thumbnail
|
|
resolve();
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Get video duration
|
|
const duration = await getVideoDuration(videoPath);
|
|
|
|
// Get file size
|
|
let fileSize: string | undefined;
|
|
try {
|
|
if (fs.existsSync(videoPath)) {
|
|
const stats = fs.statSync(videoPath);
|
|
fileSize = stats.size.toString();
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to get file size:", e);
|
|
}
|
|
|
|
const newVideo = {
|
|
id: videoId,
|
|
title: title || req.file.originalname,
|
|
author: author || "Admin",
|
|
source: "local",
|
|
sourceUrl: "", // No source URL for uploaded videos
|
|
videoFilename: videoFilename,
|
|
thumbnailFilename: fs.existsSync(thumbnailPath) ? thumbnailFilename : undefined,
|
|
videoPath: `/videos/${videoFilename}`,
|
|
thumbnailPath: fs.existsSync(thumbnailPath) ? `/images/${thumbnailFilename}` : undefined,
|
|
thumbnailUrl: fs.existsSync(thumbnailPath) ? `/images/${thumbnailFilename}` : undefined,
|
|
duration: duration ? duration.toString() : undefined,
|
|
fileSize: fileSize,
|
|
createdAt: new Date().toISOString(),
|
|
date: new Date().toISOString().split('T')[0].replace(/-/g, ''),
|
|
addedAt: new Date().toISOString(),
|
|
};
|
|
|
|
storageService.saveVideo(newVideo);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: "Video uploaded successfully",
|
|
video: newVideo
|
|
});
|
|
} catch (error: any) {
|
|
console.error("Error uploading video:", error);
|
|
res.status(500).json({
|
|
error: "Failed to upload video",
|
|
details: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
// Rate video
|
|
export const rateVideo = (req: Request, res: Response): any => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { rating } = req.body;
|
|
|
|
if (typeof rating !== 'number' || rating < 1 || rating > 5) {
|
|
return res.status(400).json({ error: "Rating must be a number between 1 and 5" });
|
|
}
|
|
|
|
const updatedVideo = storageService.updateVideo(id, { rating });
|
|
|
|
if (!updatedVideo) {
|
|
return res.status(404).json({ error: "Video not found" });
|
|
}
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "Video rated successfully",
|
|
video: updatedVideo
|
|
});
|
|
} catch (error) {
|
|
console.error("Error rating video:", error);
|
|
res.status(500).json({ error: "Failed to rate video" });
|
|
}
|
|
};
|
|
|
|
// Update video details
|
|
export const updateVideoDetails = (req: Request, res: Response): any => {
|
|
try {
|
|
const { id } = req.params;
|
|
const updates = req.body;
|
|
|
|
// Filter allowed updates
|
|
const allowedUpdates: any = {};
|
|
if (updates.title !== undefined) allowedUpdates.title = updates.title;
|
|
if (updates.tags !== undefined) allowedUpdates.tags = updates.tags;
|
|
// Add other allowed fields here if needed in the future
|
|
|
|
if (Object.keys(allowedUpdates).length === 0) {
|
|
return res.status(400).json({ error: "No valid updates provided" });
|
|
}
|
|
|
|
const updatedVideo = storageService.updateVideo(id, allowedUpdates);
|
|
|
|
if (!updatedVideo) {
|
|
return res.status(404).json({ error: "Video not found" });
|
|
}
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "Video updated successfully",
|
|
video: updatedVideo
|
|
});
|
|
} catch (error) {
|
|
console.error("Error updating video:", error);
|
|
res.status(500).json({ error: "Failed to update video" });
|
|
}
|
|
};
|
|
|
|
|
|
// Refresh video thumbnail
|
|
export const refreshThumbnail = async (req: Request, res: Response): Promise<any> => {
|
|
try {
|
|
const { id } = req.params;
|
|
const video = storageService.getVideoById(id);
|
|
|
|
if (!video) {
|
|
return res.status(404).json({ error: "Video not found" });
|
|
}
|
|
|
|
// Construct paths
|
|
let videoFilePath: string;
|
|
if (video.videoPath && video.videoPath.startsWith('/videos/')) {
|
|
const relativePath = video.videoPath.replace(/^\/videos\//, '');
|
|
// Split by / to handle the web path separators and join with system separator
|
|
videoFilePath = path.join(VIDEOS_DIR, ...relativePath.split('/'));
|
|
} else if (video.videoFilename) {
|
|
videoFilePath = path.join(VIDEOS_DIR, video.videoFilename);
|
|
} else {
|
|
return res.status(400).json({ error: "Video file path not found in record" });
|
|
}
|
|
|
|
if (!fs.existsSync(videoFilePath)) {
|
|
return res.status(404).json({ error: "Video file not found on disk" });
|
|
}
|
|
|
|
// Determine thumbnail path on disk
|
|
let thumbnailAbsolutePath: string;
|
|
let needsDbUpdate = false;
|
|
let newThumbnailFilename = video.thumbnailFilename;
|
|
let newThumbnailPath = video.thumbnailPath;
|
|
|
|
if (video.thumbnailPath && video.thumbnailPath.startsWith('/images/')) {
|
|
// Local file exists (or should exist) - preserve the existing path (e.g. inside a collection folder)
|
|
const relativePath = video.thumbnailPath.replace(/^\/images\//, '');
|
|
thumbnailAbsolutePath = path.join(IMAGES_DIR, ...relativePath.split('/'));
|
|
} else {
|
|
// Remote URL or missing - create a new local file in the root images directory
|
|
if (!newThumbnailFilename) {
|
|
const videoName = path.parse(path.basename(videoFilePath)).name;
|
|
newThumbnailFilename = `${videoName}.jpg`;
|
|
}
|
|
thumbnailAbsolutePath = path.join(IMAGES_DIR, newThumbnailFilename);
|
|
newThumbnailPath = `/images/${newThumbnailFilename}`;
|
|
needsDbUpdate = true;
|
|
}
|
|
|
|
// Ensure directory exists
|
|
fs.ensureDirSync(path.dirname(thumbnailAbsolutePath));
|
|
|
|
// Generate thumbnail
|
|
await new Promise<void>((resolve, reject) => {
|
|
// -y to overwrite existing file
|
|
exec(`ffmpeg -i "${videoFilePath}" -ss 00:00:00 -vframes 1 "${thumbnailAbsolutePath}" -y`, (error) => {
|
|
if (error) {
|
|
console.error("Error generating thumbnail:", error);
|
|
reject(error);
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Update video record if needed (switching from remote to local, or creating new)
|
|
if (needsDbUpdate) {
|
|
const updates: any = {
|
|
thumbnailFilename: newThumbnailFilename,
|
|
thumbnailPath: newThumbnailPath,
|
|
thumbnailUrl: newThumbnailPath
|
|
};
|
|
storageService.updateVideo(id, updates);
|
|
}
|
|
|
|
// Return success with timestamp to bust cache
|
|
const thumbnailUrl = `${newThumbnailPath}?t=${Date.now()}`;
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "Thumbnail refreshed successfully",
|
|
thumbnailUrl: thumbnailUrl
|
|
});
|
|
|
|
} catch (error: any) {
|
|
console.error("Error refreshing thumbnail:", error);
|
|
res.status(500).json({
|
|
});
|
|
}
|
|
};
|
|
|
|
// Increment view count
|
|
export const incrementViewCount = (req: Request, res: Response): any => {
|
|
try {
|
|
const { id } = req.params;
|
|
const video = storageService.getVideoById(id);
|
|
|
|
if (!video) {
|
|
return res.status(404).json({ error: "Video not found" });
|
|
}
|
|
|
|
const currentViews = video.viewCount || 0;
|
|
const updatedVideo = storageService.updateVideo(id, {
|
|
viewCount: currentViews + 1,
|
|
lastPlayedAt: Date.now()
|
|
});
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
viewCount: updatedVideo?.viewCount
|
|
});
|
|
} catch (error) {
|
|
console.error("Error incrementing view count:", error);
|
|
res.status(500).json({ error: "Failed to increment view count" });
|
|
}
|
|
};
|
|
|
|
// Update progress
|
|
export const updateProgress = (req: Request, res: Response): any => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { progress } = req.body;
|
|
|
|
if (typeof progress !== 'number') {
|
|
return res.status(400).json({ error: "Progress must be a number" });
|
|
}
|
|
|
|
const updatedVideo = storageService.updateVideo(id, {
|
|
progress,
|
|
lastPlayedAt: Date.now()
|
|
});
|
|
|
|
if (!updatedVideo) {
|
|
return res.status(404).json({ error: "Video not found" });
|
|
}
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
progress: updatedVideo.progress
|
|
});
|
|
} catch (error) {
|
|
console.error("Error updating progress:", error);
|
|
res.status(500).json({ error: "Failed to update progress" });
|
|
}
|
|
};
|