chore: update bgutil-ytdlp-pot-provider submodule

This commit is contained in:
Peifan Li
2025-12-22 18:08:18 -05:00
parent bf95ea32fb
commit 9d94b6ef96
2 changed files with 262 additions and 174 deletions

View File

@@ -1,5 +1,11 @@
# Change Log
## v1.6.32 (2025-12-22)
### Chore
- chore: update bgutil-ytdlp-pot-provider submodule (d59617c)
## v1.6.31 (2025-12-22)
### Feat

View File

@@ -1,25 +1,25 @@
import { Request, Response } from 'express';
import fs from 'fs-extra';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { Request, Response } from "express";
import fs from "fs-extra";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
deleteVideo,
getVideoById,
getVideos,
updateVideoDetails,
} from '../../controllers/videoController';
} from "../../controllers/videoController";
import {
checkBilibiliCollection,
checkBilibiliParts,
downloadVideo,
getDownloadStatus,
searchVideos,
} from '../../controllers/videoDownloadController';
import { rateVideo } from '../../controllers/videoMetadataController';
import downloadManager from '../../services/downloadManager';
import * as downloadService from '../../services/downloadService';
import * as storageService from '../../services/storageService';
} from "../../controllers/videoDownloadController";
import { rateVideo } from "../../controllers/videoMetadataController";
import downloadManager from "../../services/downloadManager";
import * as downloadService from "../../services/downloadService";
import * as storageService from "../../services/storageService";
vi.mock('../../db', () => ({
vi.mock("../../db", () => ({
db: {
insert: vi.fn(),
update: vi.fn(),
@@ -32,12 +32,14 @@ vi.mock('../../db', () => ({
},
}));
vi.mock('../../services/downloadService');
vi.mock('../../services/storageService');
vi.mock('../../services/downloadManager');
vi.mock('fs-extra');
vi.mock('child_process');
vi.mock('multer', () => {
vi.mock("../../services/downloadService");
vi.mock("../../services/storageService");
vi.mock("../../services/downloadManager");
vi.mock("../../services/metadataService");
vi.mock("../../utils/security");
vi.mock("fs-extra");
vi.mock("child_process");
vi.mock("multer", () => {
const multer = vi.fn(() => ({
single: vi.fn(),
array: vi.fn(),
@@ -46,7 +48,7 @@ vi.mock('multer', () => {
return { default: multer };
});
describe('VideoController', () => {
describe("VideoController", () => {
let req: Partial<Request>;
let res: Partial<Response>;
let json: any;
@@ -63,20 +65,20 @@ describe('VideoController', () => {
};
});
describe('searchVideos', () => {
it('should return search results', async () => {
req.query = { query: 'test' };
const mockResults = [{ id: '1', title: 'Test' }];
describe("searchVideos", () => {
it("should return search results", async () => {
req.query = { query: "test" };
const mockResults = [{ id: "1", title: "Test" }];
(downloadService.searchYouTube as any).mockResolvedValue(mockResults);
await searchVideos(req as Request, res as Response);
expect(downloadService.searchYouTube).toHaveBeenCalledWith('test', 8, 1);
expect(downloadService.searchYouTube).toHaveBeenCalledWith("test", 8, 1);
expect(status).toHaveBeenCalledWith(200);
expect(json).toHaveBeenCalledWith({ results: mockResults });
});
it('should return 400 if query is missing', async () => {
it("should return 400 if query is missing", async () => {
req.query = {};
req.query = {};
@@ -89,101 +91,143 @@ describe('VideoController', () => {
// So calling searchVideos will THROW.
try {
await searchVideos(req as Request, res as Response);
expect.fail('Should have thrown');
expect.fail("Should have thrown");
} catch (error: any) {
expect(error.name).toBe('ValidationError');
expect(error.name).toBe("ValidationError");
}
});
});
describe('downloadVideo', () => {
it('should queue download for valid URL', async () => {
req.body = { youtubeUrl: 'https://youtube.com/watch?v=123' };
(downloadManager.addDownload as any).mockResolvedValue('success');
describe("downloadVideo", () => {
it("should queue download for valid URL", async () => {
req.body = { youtubeUrl: "https://youtube.com/watch?v=123" };
(downloadManager.addDownload as any).mockResolvedValue("success");
await downloadVideo(req as Request, res as Response);
expect(downloadManager.addDownload).toHaveBeenCalled();
expect(status).toHaveBeenCalledWith(200);
expect(json).toHaveBeenCalledWith(expect.objectContaining({ success: true, message: 'Download queued' }));
expect(json).toHaveBeenCalledWith(
expect.objectContaining({ success: true, message: "Download queued" })
);
});
it('should return 400 for invalid URL', async () => {
req.body = { youtubeUrl: 'not-a-url' };
it("should return 400 for invalid URL", async () => {
req.body = { youtubeUrl: "not-a-url" };
await downloadVideo(req as Request, res as Response);
expect(status).toHaveBeenCalledWith(400);
expect(json).toHaveBeenCalledWith(expect.objectContaining({ error: 'Not a valid URL' }));
expect(json).toHaveBeenCalledWith(
expect.objectContaining({ error: "Not a valid URL" })
);
});
it('should return 400 if url is missing', async () => {
it("should return 400 if url is missing", async () => {
req.body = {};
await downloadVideo(req as Request, res as Response);
expect(status).toHaveBeenCalledWith(400);
});
it('should handle Bilibili collection download', async () => {
it("should handle Bilibili collection download", async () => {
req.body = {
youtubeUrl: 'https://www.bilibili.com/video/BV1xx',
youtubeUrl: "https://www.bilibili.com/video/BV1xx",
downloadCollection: true,
collectionName: 'Col',
collectionInfo: {}
collectionName: "Col",
collectionInfo: {},
};
(downloadService.downloadBilibiliCollection as any).mockResolvedValue({ success: true, collectionId: '1' });
(downloadService.downloadBilibiliCollection as any).mockResolvedValue({
success: true,
collectionId: "1",
});
await downloadVideo(req as Request, res as Response);
// The actual download task runs async, we just check it queued successfully
expect(json).toHaveBeenCalledWith(expect.objectContaining({ success: true, message: 'Download queued' }));
expect(json).toHaveBeenCalledWith(
expect.objectContaining({ success: true, message: "Download queued" })
);
});
it('should handle Bilibili multi-part download', async () => {
it("should handle Bilibili multi-part download", async () => {
req.body = {
youtubeUrl: 'https://www.bilibili.com/video/BV1xx',
youtubeUrl: "https://www.bilibili.com/video/BV1xx",
downloadAllParts: true,
collectionName: 'Col'
collectionName: "Col",
};
(downloadService.checkBilibiliVideoParts as any).mockResolvedValue({ success: true, videosNumber: 2, title: 'Title' });
(downloadService.downloadSingleBilibiliPart as any).mockResolvedValue({ success: true, videoData: { id: 'v1' } });
(downloadService.downloadRemainingBilibiliParts as any).mockImplementation(() => {});
(downloadService.checkBilibiliVideoParts as any).mockResolvedValue({
success: true,
videosNumber: 2,
title: "Title",
});
(downloadService.downloadSingleBilibiliPart as any).mockResolvedValue({
success: true,
videoData: { id: "v1" },
});
(
downloadService.downloadRemainingBilibiliParts as any
).mockImplementation(() => {});
(storageService.saveCollection as any).mockImplementation(() => {});
(storageService.atomicUpdateCollection as any).mockImplementation((_id: string, fn: Function) => fn({ videos: [] }));
(storageService.atomicUpdateCollection as any).mockImplementation(
(_id: string, fn: Function) => fn({ videos: [] })
);
await downloadVideo(req as Request, res as Response);
// The actual download task runs async, we just check it queued successfully
expect(json).toHaveBeenCalledWith(expect.objectContaining({ success: true, message: 'Download queued' }));
expect(json).toHaveBeenCalledWith(
expect.objectContaining({ success: true, message: "Download queued" })
);
});
it('should handle MissAV download', async () => {
req.body = { youtubeUrl: 'https://missav.com/v1' };
(downloadService.downloadMissAVVideo as any).mockResolvedValue({ id: 'v1' });
(storageService.checkVideoDownloadBySourceId as any).mockReturnValue({ found: false });
it("should handle MissAV download", async () => {
req.body = { youtubeUrl: "https://missav.com/v1" };
(downloadService.downloadMissAVVideo as any).mockResolvedValue({
id: "v1",
});
(storageService.checkVideoDownloadBySourceId as any).mockReturnValue({
found: false,
});
await downloadVideo(req as Request, res as Response);
// The actual download task runs async, we just check it queued successfully
expect(json).toHaveBeenCalledWith(expect.objectContaining({ success: true, message: 'Download queued' }));
expect(json).toHaveBeenCalledWith(
expect.objectContaining({ success: true, message: "Download queued" })
);
});
it('should handle Bilibili single part download when checkParts returns 1 video', async () => {
it("should handle Bilibili single part download when checkParts returns 1 video", async () => {
req.body = {
youtubeUrl: 'https://www.bilibili.com/video/BV1xx',
youtubeUrl: "https://www.bilibili.com/video/BV1xx",
downloadAllParts: true,
};
(downloadService.checkBilibiliVideoParts as any).mockResolvedValue({ success: true, videosNumber: 1, title: 'Title' });
(downloadService.downloadSingleBilibiliPart as any).mockResolvedValue({ success: true, videoData: { id: 'v1' } });
(downloadService.checkBilibiliVideoParts as any).mockResolvedValue({
success: true,
videosNumber: 1,
title: "Title",
});
(downloadService.downloadSingleBilibiliPart as any).mockResolvedValue({
success: true,
videoData: { id: "v1" },
});
await downloadVideo(req as Request, res as Response);
expect(json).toHaveBeenCalledWith(expect.objectContaining({ success: true, message: 'Download queued' }));
expect(json).toHaveBeenCalledWith(
expect.objectContaining({ success: true, message: "Download queued" })
);
});
it('should handle Bilibili single part download failure', async () => {
req.body = { youtubeUrl: 'https://www.bilibili.com/video/BV1xx' };
(downloadService.downloadSingleBilibiliPart as any).mockResolvedValue({ success: false, error: 'Failed' });
(storageService.checkVideoDownloadBySourceId as any).mockReturnValue({ found: false });
it("should handle Bilibili single part download failure", async () => {
req.body = { youtubeUrl: "https://www.bilibili.com/video/BV1xx" };
(downloadService.downloadSingleBilibiliPart as any).mockResolvedValue({
success: false,
error: "Failed",
});
(storageService.checkVideoDownloadBySourceId as any).mockReturnValue({
found: false,
});
(downloadManager.addDownload as any).mockReturnValue(Promise.resolve());
await downloadVideo(req as Request, res as Response);
@@ -192,32 +236,38 @@ describe('VideoController', () => {
expect(status).toHaveBeenCalledWith(200);
});
it('should handle download task errors', async () => {
req.body = { youtubeUrl: 'https://youtube.com/watch?v=123' };
it("should handle download task errors", async () => {
req.body = { youtubeUrl: "https://youtube.com/watch?v=123" };
(downloadManager.addDownload as any).mockImplementation(() => {
throw new Error('Queue error');
throw new Error("Queue error");
});
await downloadVideo(req as Request, res as Response);
expect(status).toHaveBeenCalledWith(500);
expect(json).toHaveBeenCalledWith(expect.objectContaining({ error: 'Failed to queue download' }));
expect(json).toHaveBeenCalledWith(
expect.objectContaining({ error: "Failed to queue download" })
);
});
it('should handle YouTube download', async () => {
req.body = { youtubeUrl: 'https://www.youtube.com/watch?v=abc123' };
(downloadService.downloadYouTubeVideo as any).mockResolvedValue({ id: 'v1' });
(downloadManager.addDownload as any).mockResolvedValue('success');
it("should handle YouTube download", async () => {
req.body = { youtubeUrl: "https://www.youtube.com/watch?v=abc123" };
(downloadService.downloadYouTubeVideo as any).mockResolvedValue({
id: "v1",
});
(downloadManager.addDownload as any).mockResolvedValue("success");
await downloadVideo(req as Request, res as Response);
expect(json).toHaveBeenCalledWith(expect.objectContaining({ success: true, message: 'Download queued' }));
expect(json).toHaveBeenCalledWith(
expect.objectContaining({ success: true, message: "Download queued" })
);
});
});
describe('getVideos', () => {
it('should return all videos', () => {
const mockVideos = [{ id: '1' }];
describe("getVideos", () => {
it("should return all videos", () => {
const mockVideos = [{ id: "1" }];
(storageService.getVideos as any).mockReturnValue(mockVideos);
getVideos(req as Request, res as Response);
@@ -228,113 +278,117 @@ describe('VideoController', () => {
});
});
describe('getVideoById', () => {
it('should return video if found', () => {
req.params = { id: '1' };
const mockVideo = { id: '1' };
describe("getVideoById", () => {
it("should return video if found", () => {
req.params = { id: "1" };
const mockVideo = { id: "1" };
(storageService.getVideoById as any).mockReturnValue(mockVideo);
getVideoById(req as Request, res as Response);
expect(storageService.getVideoById).toHaveBeenCalledWith('1');
expect(storageService.getVideoById).toHaveBeenCalledWith("1");
expect(status).toHaveBeenCalledWith(200);
expect(json).toHaveBeenCalledWith(mockVideo);
});
it('should throw NotFoundError if not found', async () => {
req.params = { id: '1' };
it("should throw NotFoundError if not found", async () => {
req.params = { id: "1" };
(storageService.getVideoById as any).mockReturnValue(undefined);
try {
await getVideoById(req as Request, res as Response);
expect.fail('Should have thrown');
expect.fail("Should have thrown");
} catch (error: any) {
expect(error.name).toBe('NotFoundError');
expect(error.name).toBe("NotFoundError");
}
});
});
describe('deleteVideo', () => {
it('should delete video', () => {
req.params = { id: '1' };
describe("deleteVideo", () => {
it("should delete video", () => {
req.params = { id: "1" };
(storageService.deleteVideo as any).mockReturnValue(true);
deleteVideo(req as Request, res as Response);
expect(storageService.deleteVideo).toHaveBeenCalledWith('1');
expect(storageService.deleteVideo).toHaveBeenCalledWith("1");
expect(status).toHaveBeenCalledWith(200);
});
it('should throw NotFoundError if delete fails', async () => {
req.params = { id: '1' };
it("should throw NotFoundError if delete fails", async () => {
req.params = { id: "1" };
(storageService.deleteVideo as any).mockReturnValue(false);
try {
await deleteVideo(req as Request, res as Response);
expect.fail('Should have thrown');
expect.fail("Should have thrown");
} catch (error: any) {
expect(error.name).toBe('NotFoundError');
expect(error.name).toBe("NotFoundError");
}
});
});
describe('rateVideo', () => {
it('should rate video', () => {
req.params = { id: '1' };
describe("rateVideo", () => {
it("should rate video", () => {
req.params = { id: "1" };
req.body = { rating: 5 };
const mockVideo = { id: '1', rating: 5 };
const mockVideo = { id: "1", rating: 5 };
(storageService.updateVideo as any).mockReturnValue(mockVideo);
rateVideo(req as Request, res as Response);
expect(storageService.updateVideo).toHaveBeenCalledWith('1', { rating: 5 });
expect(storageService.updateVideo).toHaveBeenCalledWith("1", {
rating: 5,
});
expect(status).toHaveBeenCalledWith(200);
expect(json).toHaveBeenCalledWith({ success: true, video: mockVideo });
});
it('should throw ValidationError for invalid rating', async () => {
req.params = { id: '1' };
it("should throw ValidationError for invalid rating", async () => {
req.params = { id: "1" };
req.body = { rating: 6 };
try {
await rateVideo(req as Request, res as Response);
expect.fail('Should have thrown');
expect.fail("Should have thrown");
} catch (error: any) {
expect(error.name).toBe('ValidationError');
expect(error.name).toBe("ValidationError");
}
});
it('should throw NotFoundError if video not found', async () => {
req.params = { id: '1' };
it("should throw NotFoundError if video not found", async () => {
req.params = { id: "1" };
req.body = { rating: 5 };
(storageService.updateVideo as any).mockReturnValue(null);
try {
await rateVideo(req as Request, res as Response);
expect.fail('Should have thrown');
expect.fail("Should have thrown");
} catch (error: any) {
expect(error.name).toBe('NotFoundError');
expect(error.name).toBe("NotFoundError");
}
});
});
describe('updateVideoDetails', () => {
it('should update video details', () => {
req.params = { id: '1' };
req.body = { title: 'New Title' };
const mockVideo = { id: '1', title: 'New Title' };
describe("updateVideoDetails", () => {
it("should update video details", () => {
req.params = { id: "1" };
req.body = { title: "New Title" };
const mockVideo = { id: "1", title: "New Title" };
(storageService.updateVideo as any).mockReturnValue(mockVideo);
updateVideoDetails(req as Request, res as Response);
expect(storageService.updateVideo).toHaveBeenCalledWith('1', { title: 'New Title' });
expect(storageService.updateVideo).toHaveBeenCalledWith("1", {
title: "New Title",
});
expect(status).toHaveBeenCalledWith(200);
});
it('should update tags field', () => {
req.params = { id: '1' };
req.body = { tags: ['tag1', 'tag2'] };
const mockVideo = { id: '1', tags: ['tag1', 'tag2'] };
it("should update tags field", () => {
req.params = { id: "1" };
req.body = { tags: ["tag1", "tag2"] };
const mockVideo = { id: "1", tags: ["tag1", "tag2"] };
(storageService.updateVideo as any).mockReturnValue(mockVideo);
updateVideoDetails(req as Request, res as Response);
@@ -342,36 +396,38 @@ describe('VideoController', () => {
expect(status).toHaveBeenCalledWith(200);
});
it('should throw NotFoundError if video not found', async () => {
req.params = { id: '1' };
req.body = { title: 'New Title' };
it("should throw NotFoundError if video not found", async () => {
req.params = { id: "1" };
req.body = { title: "New Title" };
(storageService.updateVideo as any).mockReturnValue(null);
try {
await updateVideoDetails(req as Request, res as Response);
expect.fail('Should have thrown');
expect.fail("Should have thrown");
} catch (error: any) {
expect(error.name).toBe('NotFoundError');
expect(error.name).toBe("NotFoundError");
}
});
it('should throw ValidationError if no valid updates', async () => {
req.params = { id: '1' };
req.body = { invalid: 'field' };
it("should throw ValidationError if no valid updates", async () => {
req.params = { id: "1" };
req.body = { invalid: "field" };
try {
await updateVideoDetails(req as Request, res as Response);
expect.fail('Should have thrown');
expect.fail("Should have thrown");
} catch (error: any) {
expect(error.name).toBe('ValidationError');
expect(error.name).toBe("ValidationError");
}
});
});
describe('checkBilibiliParts', () => {
it('should check bilibili parts', async () => {
req.query = { url: 'https://www.bilibili.com/video/BV1xx' };
(downloadService.checkBilibiliVideoParts as any).mockResolvedValue({ success: true });
describe("checkBilibiliParts", () => {
it("should check bilibili parts", async () => {
req.query = { url: "https://www.bilibili.com/video/BV1xx" };
(downloadService.checkBilibiliVideoParts as any).mockResolvedValue({
success: true,
});
await checkBilibiliParts(req as Request, res as Response);
@@ -379,83 +435,109 @@ describe('VideoController', () => {
expect(status).toHaveBeenCalledWith(200);
});
it('should throw ValidationError if url is missing', async () => {
it("should throw ValidationError if url is missing", async () => {
req.query = {};
try {
await checkBilibiliParts(req as Request, res as Response);
expect.fail('Should have thrown');
expect.fail("Should have thrown");
} catch (error: any) {
expect(error.name).toBe('ValidationError');
expect(error.name).toBe("ValidationError");
}
});
it('should throw ValidationError if url is invalid', async () => {
req.query = { url: 'invalid' };
it("should throw ValidationError if url is invalid", async () => {
req.query = { url: "invalid" };
try {
await checkBilibiliParts(req as Request, res as Response);
expect.fail('Should have thrown');
expect.fail("Should have thrown");
} catch (error: any) {
expect(error.name).toBe('ValidationError');
expect(error.name).toBe("ValidationError");
}
});
});
describe('checkBilibiliCollection', () => {
it('should check bilibili collection', async () => {
req.query = { url: 'https://www.bilibili.com/video/BV1xx' };
(downloadService.checkBilibiliCollectionOrSeries as any).mockResolvedValue({ success: true });
describe("checkBilibiliCollection", () => {
it("should check bilibili collection", async () => {
req.query = { url: "https://www.bilibili.com/video/BV1xx" };
(
downloadService.checkBilibiliCollectionOrSeries as any
).mockResolvedValue({ success: true });
await checkBilibiliCollection(req as Request, res as Response);
expect(downloadService.checkBilibiliCollectionOrSeries).toHaveBeenCalled();
expect(
downloadService.checkBilibiliCollectionOrSeries
).toHaveBeenCalled();
expect(status).toHaveBeenCalledWith(200);
});
it('should throw ValidationError if url is missing', async () => {
it("should throw ValidationError if url is missing", async () => {
req.query = {};
try {
await checkBilibiliCollection(req as Request, res as Response);
expect.fail('Should have thrown');
expect.fail("Should have thrown");
} catch (error: any) {
expect(error.name).toBe('ValidationError');
expect(error.name).toBe("ValidationError");
}
});
});
describe('getVideoComments', () => {
it('should get video comments', async () => {
req.params = { id: '1' };
describe("getVideoComments", () => {
it("should get video comments", async () => {
req.params = { id: "1" };
// Mock commentService dynamically since it's imported dynamically in controller
vi.mock('../../services/commentService', () => ({
vi.mock("../../services/commentService", () => ({
getComments: vi.fn().mockResolvedValue([]),
}));
await import('../../controllers/videoController').then(m => m.getVideoComments(req as Request, res as Response));
await import("../../controllers/videoController").then((m) =>
m.getVideoComments(req as Request, res as Response)
);
expect(status).toHaveBeenCalledWith(200);
expect(json).toHaveBeenCalledWith([]);
});
});
describe('uploadVideo', () => {
it('should upload video', async () => {
req.file = { filename: 'vid.mp4', originalname: 'vid.mp4' } as any;
req.body = { title: 'Title' };
describe("uploadVideo", () => {
it("should upload video", async () => {
req.file = { filename: "vid.mp4", originalname: "vid.mp4" } as any;
req.body = { title: "Title" };
(fs.existsSync as any).mockReturnValue(true);
(fs.statSync as any).mockReturnValue({ size: 1024 });
(fs.ensureDirSync as any).mockImplementation(() => {});
const { exec } = await import('child_process');
(exec as any).mockImplementation((_cmd: any, cb: any) => cb(null));
// Set up mocks before importing the controller
const securityUtils = await import("../../utils/security");
vi.mocked(securityUtils.execFileSafe).mockResolvedValue({
stdout: "",
stderr: "",
});
vi.mocked(securityUtils.validateVideoPath).mockImplementation(
(path: string) => path
);
vi.mocked(securityUtils.validateImagePath).mockImplementation(
(path: string) => path
);
await import('../../controllers/videoController').then(m => m.uploadVideo(req as Request, res as Response));
const metadataService = await import("../../services/metadataService");
vi.mocked(metadataService.getVideoDuration).mockResolvedValue(120);
await import("../../controllers/videoController").then((m) =>
m.uploadVideo(req as Request, res as Response)
);
expect(storageService.saveVideo).toHaveBeenCalled();
expect(status).toHaveBeenCalledWith(201);
});
});
describe('getDownloadStatus', () => {
it('should return download status', async () => {
(storageService.getDownloadStatus as any).mockReturnValue({ activeDownloads: [], queuedDownloads: [] });
describe("getDownloadStatus", () => {
it("should return download status", async () => {
(storageService.getDownloadStatus as any).mockReturnValue({
activeDownloads: [],
queuedDownloads: [],
});
await getDownloadStatus(req as Request, res as Response);