diff --git a/app/api/playlists/[id]/route.ts b/app/api/playlists/[id]/route.ts index 553c597..4c2cba7 100644 --- a/app/api/playlists/[id]/route.ts +++ b/app/api/playlists/[id]/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { getDb } from '@/lib/db' +import { db } from '@/lib/db' export async function DELETE( request: NextRequest, @@ -8,8 +8,7 @@ export async function DELETE( try { const { id } = await params - const db = getDb() - db.prepare('DELETE FROM playlists WHERE id = ?').run(id) + db.deletePlaylist(id) console.log('[API] Playlist deleted:', id) return NextResponse.json({ success: true }) @@ -29,8 +28,7 @@ export async function GET( try { const { id } = await params - const db = getDb() - const playlist = db.prepare('SELECT * FROM playlists WHERE id = ?').get(id) as any + const playlist = db.getPlaylistById(id) if (!playlist) { return NextResponse.json( @@ -40,16 +38,13 @@ export async function GET( } // Get songs for this playlist - const songs = db.prepare(` - SELECT s.*, ps.addedAt FROM songs s - INNER JOIN playlistSongs ps ON s.id = ps.songId - WHERE ps.playlistId = ? - ORDER BY ps.addedAt ASC - `).all(id) + const playlistSongs = db.getPlaylistSongs(id) + const playlistWithSongs = { + ...playlist, + playlistSongs + } - playlist.playlistSongs = songs.map((song: any) => ({ song })) - - return NextResponse.json(playlist) + return NextResponse.json(playlistWithSongs) } catch (error) { console.error('[API] Error fetching playlist:', error) return NextResponse.json( diff --git a/app/api/playlists/[id]/songs/[songId]/route.ts b/app/api/playlists/[id]/songs/[songId]/route.ts index 70cfc72..eddb8e8 100644 --- a/app/api/playlists/[id]/songs/[songId]/route.ts +++ b/app/api/playlists/[id]/songs/[songId]/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { getDb } from '@/lib/db' +import { db } from '@/lib/db' export async function DELETE( request: NextRequest, @@ -8,8 +8,7 @@ export async function DELETE( try { const { id, songId } = await params - const db = getDb() - db.prepare('DELETE FROM playlistSongs WHERE playlistId = ? AND songId = ?').run(id, songId) + db.removeSongFromPlaylist(id, songId) console.log('[API] Song removed from playlist') return NextResponse.json({ success: true }) diff --git a/app/api/playlists/[id]/songs/route.ts b/app/api/playlists/[id]/songs/route.ts index afe3d4b..1dcd0c2 100644 --- a/app/api/playlists/[id]/songs/route.ts +++ b/app/api/playlists/[id]/songs/route.ts @@ -1,6 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { getDb } from '@/lib/db' -import { randomBytes } from 'crypto' +import { db } from '@/lib/db' export async function POST( request: NextRequest, @@ -17,30 +16,16 @@ export async function POST( ) } - // Check if already in playlist - const db = getDb() - const existing = db.prepare('SELECT * FROM playlistSongs WHERE playlistId = ? AND songId = ?').get(id, songId) + const playlistSong = db.addSongToPlaylist(id, songId) - if (existing) { + if (!playlistSong) { return NextResponse.json( { error: 'Song already in playlist' }, { status: 400 } ) } - // Get next position - const maxPosition = db.prepare('SELECT MAX(position) as max FROM playlistSongs WHERE playlistId = ?').get(id) as any - const position = (maxPosition?.max ?? -1) + 1 - - const playlistSongId = randomBytes(16).toString('hex') - db.prepare(` - INSERT INTO playlistSongs (id, playlistId, songId, position) - VALUES (?, ?, ?, ?) - `).run(playlistSongId, id, songId, position) - - const playlistSong = db.prepare('SELECT * FROM playlistSongs WHERE id = ?').get(playlistSongId) - - console.log('[API] Song added to playlist:', playlistSongId) + console.log('[API] Song added to playlist:', playlistSong.id) return NextResponse.json(playlistSong) } catch (error) { console.error('[API] Error adding song to playlist:', error) diff --git a/app/api/playlists/route.ts b/app/api/playlists/route.ts index 1fcc559..a7d6649 100644 --- a/app/api/playlists/route.ts +++ b/app/api/playlists/route.ts @@ -1,24 +1,16 @@ import { NextRequest, NextResponse } from 'next/server' -import { getDb } from '@/lib/db' -import { randomBytes } from 'crypto' +import { db } from '@/lib/db' export async function GET() { try { - const db = getDb() - const playlists = db.prepare('SELECT * FROM playlists ORDER BY createdAt DESC').all() as any[] + const playlists = db.getPlaylists() // Get songs for each playlist const playlistsWithSongs = playlists.map(playlist => { - const songs = db.prepare(` - SELECT s.* FROM songs s - INNER JOIN playlistSongs ps ON s.id = ps.songId - WHERE ps.playlistId = ? - ORDER BY ps.position - `).all(playlist.id) - + const playlistSongs = db.getPlaylistSongs(playlist.id) return { ...playlist, - playlistSongs: songs.map((song: any) => ({ song })) + playlistSongs } }) @@ -43,13 +35,9 @@ export async function POST(request: NextRequest) { ) } - const db = getDb() - const playlistId = randomBytes(16).toString('hex') - db.prepare('INSERT INTO playlists (id, name) VALUES (?, ?)').run(playlistId, name) - - const playlist = db.prepare('SELECT * FROM playlists WHERE id = ?').get(playlistId) + const playlist = db.createPlaylist(name) - console.log('[API] Playlist created:', playlistId) + console.log('[API] Playlist created:', playlist.id) return NextResponse.json(playlist) } catch (error) { console.error('[API] Error creating playlist:', error) diff --git a/app/api/songs/[id]/route.ts b/app/api/songs/[id]/route.ts index adad4f2..2d1899f 100644 --- a/app/api/songs/[id]/route.ts +++ b/app/api/songs/[id]/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { getDb } from '@/lib/db' +import { db } from '@/lib/db' export async function DELETE( request: NextRequest, @@ -10,8 +10,7 @@ export async function DELETE( // Instead of deleting, mark as not in library // This keeps the song in playlists but removes it from main library - const db = getDb() - db.prepare('UPDATE songs SET isInLibrary = 0 WHERE id = ?').run(id) + db.updateSong(id, { isInLibrary: false }) console.log('[API] Song removed from library:', id) return NextResponse.json({ success: true }) diff --git a/app/api/songs/route.ts b/app/api/songs/route.ts index c8d63ee..2291450 100644 --- a/app/api/songs/route.ts +++ b/app/api/songs/route.ts @@ -1,7 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -import { getDb } from '@/lib/db' +import { db } from '@/lib/db' import { getYoutubeMetadata } from '@/lib/ytdlp' -import { randomBytes } from 'crypto' export async function POST(request: NextRequest) { try { @@ -21,16 +20,14 @@ export async function POST(request: NextRequest) { console.log('[API] Got metadata:', metadata) // Check if song already exists - const db = getDb() - const existingSong = db.prepare('SELECT * FROM songs WHERE youtubeId = ?').get(metadata.id) as any + const existingSong = db.getSongByYoutubeId(metadata.id) if (existingSong) { console.log('[API] Song already exists:', existingSong.id) // If song was previously removed from library, restore it if (!existingSong.isInLibrary) { - db.prepare('UPDATE songs SET isInLibrary = 1 WHERE id = ?').run(existingSong.id) - const updatedSong = db.prepare('SELECT * FROM songs WHERE id = ?').get(existingSong.id) + const updatedSong = db.updateSong(existingSong.id, { isInLibrary: true }) console.log('[API] Song restored to library:', existingSong.id) return NextResponse.json(updatedSong) } @@ -39,15 +36,15 @@ export async function POST(request: NextRequest) { } // Create new song in database - const songId = randomBytes(16).toString('hex') - db.prepare(` - INSERT INTO songs (id, youtubeId, title, channelName, thumbnail, duration) - VALUES (?, ?, ?, ?, ?, ?) - `).run(songId, metadata.id, metadata.title, metadata.channel, metadata.thumbnail, metadata.duration) + const song = db.createSong({ + youtubeId: metadata.id, + title: metadata.title, + channelName: metadata.channel, + thumbnail: metadata.thumbnail, + duration: metadata.duration, + }) - const song = db.prepare('SELECT * FROM songs WHERE id = ?').get(songId) - - console.log('[API] Song created successfully:', songId) + console.log('[API] Song created successfully:', song.id) return NextResponse.json(song) } catch (error) { console.error('[API] Error adding song:', error) @@ -60,8 +57,7 @@ export async function POST(request: NextRequest) { export async function GET() { try { - const db = getDb() - const songs = db.prepare('SELECT * FROM songs WHERE isInLibrary = 1 ORDER BY createdAt DESC').all() + const songs = db.getSongs() return NextResponse.json(songs) } catch (error) { console.error('Error fetching songs:', error) diff --git a/app/api/stream/[id]/route.ts b/app/api/stream/[id]/route.ts index ec83036..2f045b2 100644 --- a/app/api/stream/[id]/route.ts +++ b/app/api/stream/[id]/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { getDb } from '@/lib/db' +import { db } from '@/lib/db' import { getStreamUrl } from '@/lib/ytdlp' import { streamCache } from '@/lib/cache' @@ -11,8 +11,7 @@ export async function GET( const { id } = await params // Get song from database - const db = getDb() - const song = db.prepare('SELECT * FROM songs WHERE id = ?').get(id) as any + const song = db.getSongById(id) if (!song) { return NextResponse.json( diff --git a/lib/db.ts b/lib/db.ts index bf3230f..01b69b4 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -1,60 +1,182 @@ -import Database from 'better-sqlite3' +import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs' import { join } from 'path' -import { mkdirSync, existsSync } from 'fs' +import { randomBytes } from 'crypto' -let db: Database.Database | null = null +interface Song { + id: string + youtubeId: string + title: string + channelName: string + thumbnail: string + duration: number + isInLibrary: boolean + createdAt: number +} -export function getDb() { - if (db) return db +interface Playlist { + id: string + name: string + createdAt: number +} - const dataDir = join(process.cwd(), 'data') - - // Create data directory if it doesn't exist +interface PlaylistSong { + id: string + playlistId: string + songId: string + position: number + addedAt: number +} + +interface Database { + songs: Song[] + playlists: Playlist[] + playlistSongs: PlaylistSong[] +} + +const dataDir = join(process.cwd(), 'data') +const dbPath = join(dataDir, 'db.json') + +function ensureDataDir() { if (!existsSync(dataDir)) { mkdirSync(dataDir, { recursive: true }) } +} - const dbPath = join(dataDir, 'bitify.db') - db = new Database(dbPath) +function readDb(): Database { + ensureDataDir() + + if (!existsSync(dbPath)) { + const initialDb: Database = { + songs: [], + playlists: [], + playlistSongs: [] + } + writeFileSync(dbPath, JSON.stringify(initialDb, null, 2)) + return initialDb + } + + return JSON.parse(readFileSync(dbPath, 'utf-8')) +} - // Enable foreign keys - db.pragma('foreign_keys = ON') +function writeDb(db: Database) { + ensureDataDir() + writeFileSync(dbPath, JSON.stringify(db, null, 2)) +} - // Create tables - db.exec(` - CREATE TABLE IF NOT EXISTS songs ( - id TEXT PRIMARY KEY, - youtubeId TEXT UNIQUE NOT NULL, - title TEXT NOT NULL, - channelName TEXT NOT NULL, - thumbnail TEXT NOT NULL, - duration INTEGER NOT NULL, - isInLibrary INTEGER DEFAULT 1, - createdAt INTEGER DEFAULT (unixepoch()) - ); - - CREATE TABLE IF NOT EXISTS playlists ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - createdAt INTEGER DEFAULT (unixepoch()) - ); - - CREATE TABLE IF NOT EXISTS playlistSongs ( - id TEXT PRIMARY KEY, - playlistId TEXT NOT NULL, - songId TEXT NOT NULL, - position INTEGER NOT NULL, - addedAt INTEGER DEFAULT (unixepoch()), - FOREIGN KEY (playlistId) REFERENCES playlists(id) ON DELETE CASCADE, - FOREIGN KEY (songId) REFERENCES songs(id) ON DELETE CASCADE, - UNIQUE(playlistId, songId) - ); - - CREATE INDEX IF NOT EXISTS idx_songs_youtubeId ON songs(youtubeId); - CREATE INDEX IF NOT EXISTS idx_songs_isInLibrary ON songs(isInLibrary); - CREATE INDEX IF NOT EXISTS idx_playlistSongs_playlistId ON playlistSongs(playlistId); - CREATE INDEX IF NOT EXISTS idx_playlistSongs_songId ON playlistSongs(songId); - `) - - return db +export const db = { + // Songs + getSongs: () => { + const data = readDb() + return data.songs.filter(s => s.isInLibrary).sort((a, b) => b.createdAt - a.createdAt) + }, + + getSongById: (id: string) => { + const data = readDb() + return data.songs.find(s => s.id === id) + }, + + getSongByYoutubeId: (youtubeId: string) => { + const data = readDb() + return data.songs.find(s => s.youtubeId === youtubeId) + }, + + createSong: (song: Omit) => { + const data = readDb() + const newSong: Song = { + ...song, + id: randomBytes(16).toString('hex'), + isInLibrary: true, + createdAt: Date.now() + } + data.songs.push(newSong) + writeDb(data) + return newSong + }, + + updateSong: (id: string, updates: Partial) => { + const data = readDb() + const index = data.songs.findIndex(s => s.id === id) + if (index !== -1) { + data.songs[index] = { ...data.songs[index], ...updates } + writeDb(data) + return data.songs[index] + } + return null + }, + + // Playlists + getPlaylists: () => { + const data = readDb() + return data.playlists.sort((a, b) => b.createdAt - a.createdAt) + }, + + getPlaylistById: (id: string) => { + const data = readDb() + return data.playlists.find(p => p.id === id) + }, + + createPlaylist: (name: string) => { + const data = readDb() + const newPlaylist: Playlist = { + id: randomBytes(16).toString('hex'), + name, + createdAt: Date.now() + } + data.playlists.push(newPlaylist) + writeDb(data) + return newPlaylist + }, + + deletePlaylist: (id: string) => { + const data = readDb() + data.playlists = data.playlists.filter(p => p.id !== id) + data.playlistSongs = data.playlistSongs.filter(ps => ps.playlistId !== id) + writeDb(data) + }, + + // Playlist Songs + getPlaylistSongs: (playlistId: string) => { + const data = readDb() + const playlistSongs = data.playlistSongs + .filter(ps => ps.playlistId === playlistId) + .sort((a, b) => a.position - b.position) + + return playlistSongs.map(ps => { + const song = data.songs.find(s => s.id === ps.songId) + return { ...ps, song } + }).filter(ps => ps.song) + }, + + addSongToPlaylist: (playlistId: string, songId: string) => { + const data = readDb() + + // Check if already exists + if (data.playlistSongs.find(ps => ps.playlistId === playlistId && ps.songId === songId)) { + return null + } + + // Get next position + const playlistSongs = data.playlistSongs.filter(ps => ps.playlistId === playlistId) + const maxPosition = playlistSongs.length > 0 ? Math.max(...playlistSongs.map(ps => ps.position)) : -1 + + const newPlaylistSong: PlaylistSong = { + id: randomBytes(16).toString('hex'), + playlistId, + songId, + position: maxPosition + 1, + addedAt: Date.now() + } + + data.playlistSongs.push(newPlaylistSong) + writeDb(data) + return newPlaylistSong + }, + + removeSongFromPlaylist: (playlistId: string, songId: string) => { + const data = readDb() + data.playlistSongs = data.playlistSongs.filter( + ps => !(ps.playlistId === playlistId && ps.songId === songId) + ) + writeDb(data) + } } diff --git a/package.json b/package.json index 7ddf202..011e4c6 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,6 @@ "lint": "next lint" }, "dependencies": { - "better-sqlite3": "^11.7.0", "lucide-react": "^0.454.0", "next": "^15.0.3", "react": "^18.3.1", @@ -17,7 +16,6 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4.0.0", - "@types/better-sqlite3": "^7.6.12", "@types/node": "^22.9.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1",