Compare commits

..

No commits in common. "89bb05d3fcad5be7083ad05d31b3c958d17a1ae7" and "95a49380da093e25d9a1e19d51fbe090fc627574" have entirely different histories.

13 changed files with 1 additions and 574 deletions

View File

@ -12,8 +12,6 @@ Feature requirement:
7. there should be a database(sqlite) to save the media informations 7. there should be a database(sqlite) to save the media informations
8. there should be at least two tables in the database, one to keep the video informations, such as path, size, thumbnails, title, etc(not limit to these); one to keep the media libraries. videos should be linked to the library 8. there should be at least two tables in the database, one to keep the video informations, such as path, size, thumbnails, title, etc(not limit to these); one to keep the media libraries. videos should be linked to the library
9. thumbnail generate feature 9. thumbnail generate feature
10. bookmark feature: the video can be bookmarked
11. star feature: the video can be stared as one to five stars
UI: UI:
@ -26,6 +24,5 @@ UI:
6. photo section: TBD 6. photo section: TBD
7. folder viewer: list libraries in the side bar folder viewer section. once one of the folder is selected, display the folder sturcture in the main area. the vdieo or photo display thunbnail and information as the video card would do. for the folder, display the folder icon, which can be entered in 7. folder viewer: list libraries in the side bar folder viewer section. once one of the folder is selected, display the folder sturcture in the main area. the vdieo or photo display thunbnail and information as the video card would do. for the folder, display the folder icon, which can be entered in
8. the video card can be clicked, once clicked, a poped up video player will be displayed. it can be closed, fast forward, expand to full screen, etc. 8. the video card can be clicked, once clicked, a poped up video player will be displayed. it can be closed, fast forward, expand to full screen, etc.
9. can bookmark/un-bookmark the video, can star the video

View File

@ -12,8 +12,6 @@ Feature requirement:
7. there should be a database(sqlite) to save the media informations 7. there should be a database(sqlite) to save the media informations
8. there should be at least two tables in the database, one to keep the video informations, such as path, size, thumbnails, title, etc(not limit to these); one to keep the media libraries. videos should be linked to the library 8. there should be at least two tables in the database, one to keep the video informations, such as path, size, thumbnails, title, etc(not limit to these); one to keep the media libraries. videos should be linked to the library
9. thumbnail generate feature 9. thumbnail generate feature
10. bookmark feature: the video can be bookmarked
11. star feature: the video can be stared as one to five stars
UI: UI:
@ -26,6 +24,5 @@ UI:
6. photo section: TBD 6. photo section: TBD
7. folder viewer: list libraries in the side bar folder viewer section. once one of the folder is selected, display the folder sturcture in the main area. the vdieo or photo display thunbnail and information as the video card would do. for the folder, display the folder icon, which can be entered in 7. folder viewer: list libraries in the side bar folder viewer section. once one of the folder is selected, display the folder sturcture in the main area. the vdieo or photo display thunbnail and information as the video card would do. for the folder, display the folder icon, which can be entered in
8. the video card can be clicked, once clicked, a poped up video player will be displayed. it can be closed, fast forward, expand to full screen, etc. 8. the video card can be clicked, once clicked, a poped up video player will be displayed. it can be closed, fast forward, expand to full screen, etc.
9. can bookmark/un-bookmark the video, can star the video

3
PRD.md
View File

@ -12,8 +12,6 @@ Feature requirement:
7. there should be a database(sqlite) to save the media informations 7. there should be a database(sqlite) to save the media informations
8. there should be at least two tables in the database, one to keep the video informations, such as path, size, thumbnails, title, etc(not limit to these); one to keep the media libraries. videos should be linked to the library 8. there should be at least two tables in the database, one to keep the video informations, such as path, size, thumbnails, title, etc(not limit to these); one to keep the media libraries. videos should be linked to the library
9. thumbnail generate feature 9. thumbnail generate feature
10. bookmark feature: the video can be bookmarked
11. star feature: the video can be stared as one to five stars
UI: UI:
@ -26,6 +24,5 @@ UI:
6. photo section: TBD 6. photo section: TBD
7. folder viewer: list libraries in the side bar folder viewer section. once one of the folder is selected, display the folder sturcture in the main area. the vdieo or photo display thunbnail and information as the video card would do. for the folder, display the folder icon, which can be entered in 7. folder viewer: list libraries in the side bar folder viewer section. once one of the folder is selected, display the folder sturcture in the main area. the vdieo or photo display thunbnail and information as the video card would do. for the folder, display the folder icon, which can be entered in
8. the video card can be clicked, once clicked, a poped up video player will be displayed. it can be closed, fast forward, expand to full screen, etc. 8. the video card can be clicked, once clicked, a poped up video player will be displayed. it can be closed, fast forward, expand to full screen, etc.
9. can bookmark/un-bookmark the video, can star the video

BIN
media.db

Binary file not shown.

View File

@ -1,36 +0,0 @@
import { NextResponse } from 'next/server';
import db from '@/db';
export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
try {
const parsedId = parseInt(id);
if (isNaN(parsedId)) {
return NextResponse.json({ error: 'Invalid bookmark ID' }, { status: 400 });
}
// Get media_id before deleting
const bookmark = db.prepare(`
SELECT media_id FROM bookmarks WHERE id = ?
`).get(parsedId) as { media_id: number } | undefined;
if (!bookmark) {
return NextResponse.json({ error: 'Bookmark not found' }, { status: 404 });
}
// Delete bookmark
db.prepare(`DELETE FROM bookmarks WHERE id = ?`).run(parsedId);
// Update media bookmark count
db.prepare(`
UPDATE media
SET bookmark_count = (SELECT COUNT(*) FROM bookmarks WHERE media_id = ?)
WHERE id = ?
`).run(bookmark.media_id, bookmark.media_id);
return NextResponse.json({ success: true });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@ -1,53 +0,0 @@
import { NextResponse } from 'next/server';
import db from '@/db';
export async function GET(request: Request) {
try {
const bookmarks = db.prepare(`
SELECT m.*, l.path as library_path
FROM bookmarks b
JOIN media m ON b.media_id = m.id
JOIN libraries l ON m.library_id = l.id
ORDER BY b.updated_at DESC
`).all();
return NextResponse.json(bookmarks);
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
export async function POST(request: Request) {
try {
const { mediaId } = await request.json();
if (!mediaId) {
return NextResponse.json({ error: 'mediaId is required' }, { status: 400 });
}
// Check if already bookmarked
const existing = db.prepare(`
SELECT id FROM bookmarks WHERE media_id = ?
`).get(mediaId);
if (existing) {
return NextResponse.json({ error: 'Already bookmarked' }, { status: 400 });
}
// Insert bookmark
const result = db.prepare(`
INSERT INTO bookmarks (media_id) VALUES (?)
`).run(mediaId);
// Update media bookmark count
db.prepare(`
UPDATE media
SET bookmark_count = (SELECT COUNT(*) FROM bookmarks WHERE media_id = ?)
WHERE id = ?
`).run(mediaId, mediaId);
return NextResponse.json({ id: result.lastInsertRowid });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@ -1,38 +0,0 @@
import { NextResponse } from 'next/server';
import db from '@/db';
export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
try {
const parsedId = parseInt(id);
if (isNaN(parsedId)) {
return NextResponse.json({ error: 'Invalid star ID' }, { status: 400 });
}
// Get media_id before deleting
const star = db.prepare(`
SELECT media_id FROM stars WHERE id = ?
`).get(parsedId) as { media_id: number } | undefined;
if (!star) {
return NextResponse.json({ error: 'Star rating not found' }, { status: 404 });
}
// Delete star rating
db.prepare(`DELETE FROM stars WHERE id = ?`).run(parsedId);
// Update media star count and average rating
db.prepare(`
UPDATE media
SET
star_count = (SELECT COUNT(*) FROM stars WHERE media_id = ?),
avg_rating = COALESCE((SELECT AVG(rating) FROM stars WHERE media_id = ?), 0.0)
WHERE id = ?
`).run(star.media_id, star.media_id, star.media_id);
return NextResponse.json({ success: true });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@ -1,78 +0,0 @@
import { NextResponse } from 'next/server';
import db from '@/db';
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const mediaId = searchParams.get('mediaId');
if (mediaId) {
// Get stars for specific media
const stars = db.prepare(`
SELECT * FROM stars WHERE media_id = ?
ORDER BY created_at DESC
`).all(parseInt(mediaId));
return NextResponse.json(stars);
} else {
// Get all stars
const stars = db.prepare(`
SELECT m.*, l.path as library_path, s.rating
FROM stars s
JOIN media m ON s.media_id = m.id
JOIN libraries l ON m.library_id = l.id
ORDER BY s.created_at DESC
`).all();
return NextResponse.json(stars);
}
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
export async function POST(request: Request) {
try {
const { mediaId, rating } = await request.json();
if (!mediaId || !rating) {
return NextResponse.json({ error: 'mediaId and rating are required' }, { status: 400 });
}
if (rating < 1 || rating > 5) {
return NextResponse.json({ error: 'Rating must be between 1 and 5' }, { status: 400 });
}
// Check if already starred
const existing = db.prepare(`
SELECT id FROM stars WHERE media_id = ?
`).get(mediaId);
if (existing) {
// Update existing star rating
db.prepare(`
UPDATE stars
SET rating = ?, updated_at = CURRENT_TIMESTAMP
WHERE media_id = ?
`).run(rating, mediaId);
} else {
// Insert new star rating
db.prepare(`
INSERT INTO stars (media_id, rating) VALUES (?, ?)
`).run(mediaId, rating);
}
// Update media star count and average rating
db.prepare(`
UPDATE media
SET
star_count = (SELECT COUNT(*) FROM stars WHERE media_id = ?),
avg_rating = (SELECT AVG(rating) FROM stars WHERE media_id = ?)
WHERE id = ?
`).run(mediaId, mediaId, mediaId);
return NextResponse.json({ success: true });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@ -1,28 +0,0 @@
import { NextResponse } from 'next/server';
import db from '@/db';
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
try {
const parsedId = parseInt(id);
if (isNaN(parsedId)) {
return NextResponse.json({ error: 'Invalid video ID' }, { status: 400 });
}
const video = db.prepare(`
SELECT m.*, l.path as library_path
FROM media m
JOIN libraries l ON m.library_id = l.id
WHERE m.id = ?
`).get(parsedId);
if (!video) {
return NextResponse.json({ error: 'Video not found' }, { status: 404 });
}
return NextResponse.json(video);
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@ -1,179 +0,0 @@
"use client";
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import InlineVideoPlayer from "@/components/inline-video-player";
import { Bookmark, Heart, Star, Film } from 'lucide-react';
interface Video {
id: number;
title: string;
path: string;
size: number;
thumbnail: string;
type: string;
bookmark_count: number;
star_count: number;
avg_rating: number;
library_path: string;
}
export default function BookmarksPage() {
const [videos, setVideos] = useState<Video[]>([]);
const [loading, setLoading] = useState(true);
const [selectedVideo, setSelectedVideo] = useState<Video | null>(null);
const [isPlayerOpen, setIsPlayerOpen] = useState(false);
const [scrollPosition, setScrollPosition] = useState(0);
useEffect(() => {
fetchBookmarkedVideos();
}, []);
const fetchBookmarkedVideos = async () => {
try {
const response = await fetch('/api/bookmarks');
const data = await response.json();
setVideos(data);
} catch (error) {
console.error('Error fetching bookmarked videos:', error);
} finally {
setLoading(false);
}
};
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const handleVideoClick = (video: Video) => {
setScrollPosition(window.scrollY);
setSelectedVideo(video);
setIsPlayerOpen(true);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const handleClosePlayer = () => {
setIsPlayerOpen(false);
setSelectedVideo(null);
// Restore scroll position
setTimeout(() => {
window.scrollTo({ top: scrollPosition, behavior: 'smooth' });
}, 100);
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-4"></div>
<p className="text-muted-foreground">Loading bookmarked videos...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-zinc-950">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 bg-blue-500 rounded-lg flex items-center justify-center">
<Bookmark className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-3xl font-bold text-white">Bookmarked Videos</h1>
<p className="text-zinc-400 mt-1">
{videos.length} {videos.length === 1 ? 'video' : 'videos'} bookmarked
</p>
</div>
</div>
</div>
{videos.length === 0 ? (
<div className="flex flex-col items-center justify-center min-h-[60vh]">
<div className="text-center max-w-md">
<div className="w-20 h-20 bg-zinc-900 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg">
<Bookmark className="h-10 w-10 text-zinc-400" />
</div>
<h2 className="text-2xl font-bold text-white mb-2">No Bookmarks Yet</h2>
<p className="text-zinc-400">
Start bookmarking videos by clicking the bookmark icon in the video player.
</p>
</div>
</div>
) : (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 gap-4">
{videos.map((video) => (
<Card
key={video.id}
className="group bg-white dark:bg-slate-800 border-0 shadow-sm hover:shadow-lg transition-all duration-300 hover:-translate-y-1 overflow-hidden cursor-pointer"
onClick={() => handleVideoClick(video)}
>
<CardHeader className="p-0">
<div className="aspect-video bg-slate-200 dark:bg-slate-700 relative overflow-hidden">
{video.thumbnail ? (
<img
src={video.thumbnail}
alt={video.title}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-slate-200 to-slate-300 dark:from-slate-700 dark:to-slate-800">
<Film className="w-12 h-12 text-slate-400" />
</div>
)}
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
</div>
</CardHeader>
<CardContent className="p-3">
<CardTitle className="text-sm font-semibold text-slate-900 dark:text-slate-100 truncate mb-1">
{video.title || video.path.split('/').pop()}
</CardTitle>
<CardDescription className="text-xs text-slate-600 dark:text-slate-400">
{formatFileSize(video.size)}
</CardDescription>
{/* Stats */}
<div className="flex items-center gap-3 mt-2 text-xs text-slate-500 dark:text-slate-400">
<div className="flex items-center gap-1">
<Heart className="h-3 w-3" />
<span>{video.bookmark_count || 0}</span>
</div>
<div className="flex items-center gap-1">
<Star className="h-3 w-3" />
<span>{video.avg_rating?.toFixed(1) || '0.0'}</span>
</div>
</div>
<div className="text-xs text-slate-500 dark:text-slate-400 mt-1 truncate">
{video.library_path?.split('/').pop()}
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
{/* Inline Video Player */}
{selectedVideo && (
<InlineVideoPlayer
video={{
id: selectedVideo.id,
title: selectedVideo.title || selectedVideo.path.split('/').pop() || 'Untitled',
path: selectedVideo.path,
size: selectedVideo.size,
thumbnail: selectedVideo.thumbnail || '',
}}
isOpen={isPlayerOpen}
onClose={handleClosePlayer}
scrollPosition={scrollPosition}
/>
)}
</div>
);
}

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { X, Play, Pause, Maximize, Minimize, Volume2, VolumeX, Bookmark, Star, Heart } from 'lucide-react'; import { X, Play, Pause, Maximize, Minimize, Volume2, VolumeX } from 'lucide-react';
interface InlineVideoPlayerProps { interface InlineVideoPlayerProps {
video: { video: {
@ -25,19 +25,11 @@ export default function InlineVideoPlayer({ video, isOpen, onClose, scrollPositi
const [duration, setDuration] = useState(0); const [duration, setDuration] = useState(0);
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const [showControls, setShowControls] = useState(true); const [showControls, setShowControls] = useState(true);
const [isBookmarked, setIsBookmarked] = useState(false);
const [userRating, setUserRating] = useState(0);
const [avgRating, setAvgRating] = useState(0);
const [bookmarkCount, setBookmarkCount] = useState(0);
const [starCount, setStarCount] = useState(0);
const [showRating, setShowRating] = useState(false);
const videoRef = useRef<HTMLVideoElement>(null); const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {
setIsVisible(true); setIsVisible(true);
loadBookmarkStatus();
loadStarRating();
} else { } else {
setIsVisible(false); setIsVisible(false);
} }
@ -117,82 +109,6 @@ export default function InlineVideoPlayer({ video, isOpen, onClose, scrollPositi
return `${minutes}:${seconds.toString().padStart(2, '0')}`; return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}; };
const loadBookmarkStatus = async () => {
try {
const response = await fetch(`/api/bookmarks?mediaId=${video.id}`);
const data = await response.json();
setIsBookmarked(data.length > 0);
} catch (error) {
console.error('Error loading bookmark status:', error);
}
};
const loadStarRating = async () => {
try {
const response = await fetch(`/api/stars?mediaId=${video.id}`);
const stars = await response.json();
// Get media info for counts and avg rating
const mediaResponse = await fetch(`/api/videos/${video.id}`);
const mediaData = await mediaResponse.json();
setBookmarkCount(mediaData.bookmark_count || 0);
setStarCount(mediaData.star_count || 0);
setAvgRating(mediaData.avg_rating || 0);
// Set user's rating if exists
if (stars.length > 0) {
setUserRating(stars[0].rating);
}
} catch (error) {
console.error('Error loading star rating:', error);
}
};
const toggleBookmark = async () => {
try {
if (isBookmarked) {
// Remove bookmark
const response = await fetch(`/api/bookmarks/${video.id}`, { method: 'DELETE' });
if (response.ok) {
setIsBookmarked(false);
setBookmarkCount(prev => Math.max(0, prev - 1));
}
} else {
// Add bookmark
const response = await fetch('/api/bookmarks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mediaId: video.id })
});
if (response.ok) {
setIsBookmarked(true);
setBookmarkCount(prev => prev + 1);
}
}
} catch (error) {
console.error('Error toggling bookmark:', error);
}
};
const handleStarClick = async (rating: number) => {
try {
const response = await fetch('/api/stars', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mediaId: video.id, rating })
});
if (response.ok) {
setUserRating(rating);
// Reload star data
await loadStarRating();
}
} catch (error) {
console.error('Error setting star rating:', error);
}
};
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') { if (e.key === 'Escape') {
@ -237,42 +153,6 @@ export default function InlineVideoPlayer({ video, isOpen, onClose, scrollPositi
<h1 className="text-xl font-semibold text-foreground truncate max-w-md"> <h1 className="text-xl font-semibold text-foreground truncate max-w-md">
{video.title} {video.title}
</h1> </h1>
<div className="flex items-center gap-3">
{/* Bookmark Button */}
<button
onClick={toggleBookmark}
className={`flex items-center gap-2 px-3 py-1.5 rounded-full transition-colors ${
isBookmarked
? 'bg-blue-500 text-white hover:bg-blue-600'
: 'bg-muted hover:bg-muted/80'
}`}
>
<Bookmark className={`h-4 w-4 ${isBookmarked ? 'fill-current' : ''}`} />
<span className="text-sm">{bookmarkCount}</span>
</button>
{/* Star Rating */}
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
{[1, 2, 3, 4, 5].map((star) => (
<button
key={star}
onClick={() => handleStarClick(star)}
className="text-yellow-500 hover:text-yellow-400 transition-colors"
>
<Star
className={`h-4 w-4 ${
star <= userRating ? 'fill-current' : ''
}`}
/>
</button>
))}
</div>
<span className="text-sm text-muted-foreground">
{avgRating.toFixed(1)} ({starCount})
</span>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -15,7 +15,6 @@ import {
Image as ImageIcon, Image as ImageIcon,
Play, Play,
FolderOpen, FolderOpen,
Bookmark,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { usePathname, useSearchParams } from "next/navigation"; import { usePathname, useSearchParams } from "next/navigation";
@ -50,7 +49,6 @@ const SidebarContent = () => {
{ href: "/", label: "Home", icon: Home }, { href: "/", label: "Home", icon: Home },
{ href: "/videos", label: "Videos", icon: Film }, { href: "/videos", label: "Videos", icon: Film },
{ href: "/photos", label: "Photos", icon: ImageIcon }, { href: "/photos", label: "Photos", icon: ImageIcon },
{ href: "/bookmarks", label: "Bookmarks", icon: Bookmark },
{ href: "/settings", label: "Settings", icon: Settings }, { href: "/settings", label: "Settings", icon: Settings },
]; ];

View File

@ -21,39 +21,9 @@ db.exec(`
title TEXT, title TEXT,
size INTEGER, size INTEGER,
thumbnail TEXT, thumbnail TEXT,
bookmark_count INTEGER DEFAULT 0,
star_count INTEGER DEFAULT 0,
avg_rating REAL DEFAULT 0.0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (library_id) REFERENCES libraries (id) FOREIGN KEY (library_id) REFERENCES libraries (id)
); );
`); `);
db.exec(`
CREATE TABLE IF NOT EXISTS bookmarks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
media_id INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (media_id) REFERENCES media(id) ON DELETE CASCADE
);
`);
db.exec(`
CREATE TABLE IF NOT EXISTS stars (
id INTEGER PRIMARY KEY AUTOINCREMENT,
media_id INTEGER NOT NULL,
rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (media_id) REFERENCES media(id) ON DELETE CASCADE
);
`);
// Create indexes for performance
db.exec(`CREATE INDEX IF NOT EXISTS idx_bookmarks_media_id ON bookmarks(media_id);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_stars_media_id ON stars(media_id);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_media_bookmark_count ON media(bookmark_count);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_media_star_count ON media(star_count);`);
export default db; export default db;