From f0568e89349d510daa186c1b19e0e90d69b03d36 Mon Sep 17 00:00:00 2001 From: Peifan Li Date: Tue, 25 Nov 2025 17:29:36 -0500 Subject: [PATCH] feat: Add tags support to videos and implement tag management --- backend/src/controllers/videoController.ts | 1 + backend/src/db/index.ts | 2 +- backend/src/db/schema.ts | 1 + backend/src/services/storageService.ts | 56 ++++++++++++++-- frontend/src/App.tsx | 2 +- frontend/src/components/TagsList.tsx | 76 ++++++++++++++++++++++ frontend/src/pages/Home.tsx | 47 ++++++++++++- frontend/src/pages/SettingsPage.tsx | 58 ++++++++++++++++- frontend/src/pages/VideoPlayer.tsx | 59 +++++++++++++++++ frontend/src/types.ts | 1 + frontend/src/utils/translations.ts | 6 ++ 11 files changed, 296 insertions(+), 13 deletions(-) create mode 100644 frontend/src/components/TagsList.tsx diff --git a/backend/src/controllers/videoController.ts b/backend/src/controllers/videoController.ts index 61cc9f3..2f74a86 100644 --- a/backend/src/controllers/videoController.ts +++ b/backend/src/controllers/videoController.ts @@ -505,6 +505,7 @@ export const updateVideoDetails = (req: Request, res: Response): any => { // 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) { diff --git a/backend/src/db/index.ts b/backend/src/db/index.ts index c363cb3..bf40b46 100644 --- a/backend/src/db/index.ts +++ b/backend/src/db/index.ts @@ -9,6 +9,6 @@ import * as schema from './schema'; fs.ensureDirSync(DATA_DIR); const dbPath = path.join(DATA_DIR, 'mytube.db'); -const sqlite = new Database(dbPath); +export const sqlite = new Database(dbPath); export const db = drizzle(sqlite, { schema }); diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index e0fd318..d02df6c 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -24,6 +24,7 @@ export const videos = sqliteTable('videos', { description: text('description'), viewCount: integer('view_count'), duration: text('duration'), + tags: text('tags'), // JSON stringified array of strings }); export const collections = sqliteTable('collections', { diff --git a/backend/src/services/storageService.ts b/backend/src/services/storageService.ts index c0aa3e2..6114474 100644 --- a/backend/src/services/storageService.ts +++ b/backend/src/services/storageService.ts @@ -8,7 +8,7 @@ import { UPLOADS_DIR, VIDEOS_DIR, } from "../config/paths"; -import { db } from "../db"; +import { db, sqlite } from "../db"; import { collections, collectionVideos, downloads, settings, videos } from "../db/schema"; export interface Video { @@ -18,6 +18,7 @@ export interface Video { videoFilename?: string; thumbnailFilename?: string; createdAt: string; + tags?: string[]; [key: string]: any; } @@ -74,6 +75,20 @@ export function initializeStorage(): void { ); } } + + // Check and migrate tags column if needed + try { + const tableInfo = sqlite.prepare("PRAGMA table_info(videos)").all(); + const hasTags = (tableInfo as any[]).some((col: any) => col.name === 'tags'); + + if (!hasTags) { + console.log("Migrating database: Adding tags column to videos table..."); + sqlite.prepare("ALTER TABLE videos ADD COLUMN tags TEXT").run(); + console.log("Migration successful."); + } + } catch (error) { + console.error("Error checking/migrating tags column:", error); + } } @@ -240,7 +255,10 @@ export function saveSettings(newSettings: Record): void { export function getVideos(): Video[] { try { const allVideos = db.select().from(videos).orderBy(desc(videos.createdAt)).all(); - return allVideos as Video[]; + return allVideos.map(v => ({ + ...v, + tags: v.tags ? JSON.parse(v.tags) : [], + })) as Video[]; } catch (error) { console.error("Error getting videos:", error); return []; @@ -250,7 +268,13 @@ export function getVideos(): Video[] { export function getVideoById(id: string): Video | undefined { try { const video = db.select().from(videos).where(eq(videos.id, id)).get(); - return video as Video | undefined; + if (video) { + return { + ...video, + tags: video.tags ? JSON.parse(video.tags) : [], + } as Video; + } + return undefined; } catch (error) { console.error("Error getting video by id:", error); return undefined; @@ -259,9 +283,13 @@ export function getVideoById(id: string): Video | undefined { export function saveVideo(videoData: Video): Video { try { - db.insert(videos).values(videoData as any).onConflictDoUpdate({ + const videoToSave = { + ...videoData, + tags: videoData.tags ? JSON.stringify(videoData.tags) : undefined, + }; + db.insert(videos).values(videoToSave as any).onConflictDoUpdate({ target: videos.id, - set: videoData, + set: videoToSave, }).run(); return videoData; } catch (error) { @@ -272,8 +300,22 @@ export function saveVideo(videoData: Video): Video { export function updateVideo(id: string, updates: Partial