From 06e2962fc5b94741ece0e11120347ec6126f0238 Mon Sep 17 00:00:00 2001 From: bitc0de Date: Wed, 17 Dec 2025 15:32:10 +0100 Subject: [PATCH] Replace Prisma with better-sqlite3 for simpler deployment --- Dockerfile | 15 +----- app/api/playlists/[id]/route.ts | 30 ++++++------ .../playlists/[id]/songs/[songId]/route.ts | 11 +---- app/api/playlists/[id]/songs/route.ts | 31 ++++++------- app/api/playlists/route.ts | 38 +++++++++------ app/api/songs/[id]/route.ts | 7 +-- app/api/songs/route.ts | 39 ++++++---------- app/api/stream/[id]/route.ts | 6 +-- docker-compose.yml | 4 +- lib/db.ts | 46 +++++++++++++++++++ package.json | 10 ++-- 11 files changed, 125 insertions(+), 112 deletions(-) create mode 100644 lib/db.ts diff --git a/Dockerfile b/Dockerfile index c341940..538999c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,6 @@ WORKDIR /app # Copy package files COPY package*.json ./ -COPY prisma ./prisma/ # Install dependencies RUN npm ci @@ -13,9 +12,6 @@ RUN npm ci # Copy source code COPY . . -# Generate Prisma Client -RUN npx prisma generate - # Build Next.js application RUN npm run build @@ -37,19 +33,12 @@ ENV NEXT_TELEMETRY_DISABLED 1 RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs -# Install Prisma CLI for db push command -RUN npm install -g prisma - # Copy necessary files from builder COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static -COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.prisma ./node_modules/.prisma -# Copy Prisma schema and migrations -COPY --chown=nextjs:nodejs prisma ./prisma/ - -# Create database directory and set permissions -RUN mkdir -p /app/prisma && chown -R nextjs:nodejs /app +# Create data directory for database +RUN mkdir -p /app/data && chown -R nextjs:nodejs /app USER nextjs diff --git a/app/api/playlists/[id]/route.ts b/app/api/playlists/[id]/route.ts index adce2e3..8503688 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 { prisma } from '@/lib/prisma' +import db from '@/lib/db' export async function DELETE( request: NextRequest, @@ -8,9 +8,7 @@ export async function DELETE( try { const { id } = await params - await prisma.playlist.delete({ - where: { id }, - }) + db.prepare('DELETE FROM playlists WHERE id = ?').run(id) console.log('[API] Playlist deleted:', id) return NextResponse.json({ success: true }) @@ -30,19 +28,7 @@ export async function GET( try { const { id } = await params - const playlist = await prisma.playlist.findUnique({ - where: { id }, - include: { - playlistSongs: { - include: { - song: true, - }, - orderBy: { - addedAt: 'asc', - }, - }, - }, - }) + const playlist = db.prepare('SELECT * FROM playlists WHERE id = ?').get(id) as any if (!playlist) { return NextResponse.json( @@ -51,6 +37,16 @@ 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) + + playlist.playlistSongs = songs.map((song: any) => ({ song })) + return NextResponse.json(playlist) } catch (error) { console.error('[API] Error fetching playlist:', error) diff --git a/app/api/playlists/[id]/songs/[songId]/route.ts b/app/api/playlists/[id]/songs/[songId]/route.ts index 7e5f1c3..6437ce9 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 { prisma } from '@/lib/prisma' +import db from '@/lib/db' export async function DELETE( request: NextRequest, @@ -8,14 +8,7 @@ export async function DELETE( try { const { id, songId } = await params - await prisma.playlistSong.delete({ - where: { - playlistId_songId: { - playlistId: id, - songId: songId, - }, - }, - }) + db.prepare('DELETE FROM playlistSongs WHERE playlistId = ? AND songId = ?').run(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 b4b942e..7dceb17 100644 --- a/app/api/playlists/[id]/songs/route.ts +++ b/app/api/playlists/[id]/songs/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -import { prisma } from '@/lib/prisma' +import db from '@/lib/db' +import { randomBytes } from 'crypto' export async function POST( request: NextRequest, @@ -17,14 +18,7 @@ export async function POST( } // Check if already in playlist - const existing = await prisma.playlistSong.findUnique({ - where: { - playlistId_songId: { - playlistId: id, - songId: songId, - }, - }, - }) + const existing = db.prepare('SELECT * FROM playlistSongs WHERE playlistId = ? AND songId = ?').get(id, songId) if (existing) { return NextResponse.json( @@ -33,14 +27,19 @@ export async function POST( ) } - const playlistSong = await prisma.playlistSong.create({ - data: { - playlistId: id, - songId: songId, - }, - }) + // 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 - console.log('[API] Song added to playlist:', playlistSong.id) + 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) 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 acb885c..f987b4f 100644 --- a/app/api/playlists/route.ts +++ b/app/api/playlists/route.ts @@ -1,20 +1,27 @@ import { NextRequest, NextResponse } from 'next/server' -import { prisma } from '@/lib/prisma' +import db from '@/lib/db' +import { randomBytes } from 'crypto' export async function GET() { try { - const playlists = await prisma.playlist.findMany({ - orderBy: { createdAt: 'desc' }, - include: { - playlistSongs: { - include: { - song: true, - }, - }, - }, + const playlists = db.prepare('SELECT * FROM playlists ORDER BY createdAt DESC').all() as any[] + + // 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) + + return { + ...playlist, + playlistSongs: songs.map((song: any) => ({ song })) + } }) - return NextResponse.json(playlists) + return NextResponse.json(playlistsWithSongs) } catch (error) { console.error('[API] Error fetching playlists:', error) return NextResponse.json( @@ -35,11 +42,12 @@ export async function POST(request: NextRequest) { ) } - const playlist = await prisma.playlist.create({ - data: { name }, - }) + 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) - console.log('[API] Playlist created:', playlist.id) + console.log('[API] Playlist created:', playlistId) 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 cbac529..7680bfe 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 { prisma } from '@/lib/prisma' +import db from '@/lib/db' export async function DELETE( request: NextRequest, @@ -10,10 +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 - await prisma.song.update({ - where: { id }, - data: { isInLibrary: false }, - }) + db.prepare('UPDATE songs SET isInLibrary = 0 WHERE id = ?').run(id) 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 8009c1a..5339830 100644 --- a/app/api/songs/route.ts +++ b/app/api/songs/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' -import { prisma } from '@/lib/prisma' +import db from '@/lib/db' import { getYoutubeMetadata } from '@/lib/ytdlp' +import { randomBytes } from 'crypto' export async function POST(request: NextRequest) { try { @@ -20,20 +21,16 @@ export async function POST(request: NextRequest) { console.log('[API] Got metadata:', metadata) // Check if song already exists - const existingSong = await prisma.song.findUnique({ - where: { youtubeId: metadata.id }, - }) + const existingSong = db.prepare('SELECT * FROM songs WHERE youtubeId = ?').get(metadata.id) as any if (existingSong) { console.log('[API] Song already exists:', existingSong.id) // If song was previously removed from library, restore it if (!existingSong.isInLibrary) { - const updatedSong = await prisma.song.update({ - where: { id: existingSong.id }, - data: { isInLibrary: true }, - }) - console.log('[API] Song restored to library:', updatedSong.id) + db.prepare('UPDATE songs SET isInLibrary = 1 WHERE id = ?').run(existingSong.id) + const updatedSong = db.prepare('SELECT * FROM songs WHERE id = ?').get(existingSong.id) + console.log('[API] Song restored to library:', existingSong.id) return NextResponse.json(updatedSong) } @@ -41,17 +38,15 @@ export async function POST(request: NextRequest) { } // Create new song in database - const song = await prisma.song.create({ - data: { - youtubeId: metadata.id, - title: metadata.title, - channelName: metadata.channel, - thumbnail: metadata.thumbnail, - duration: metadata.duration, - }, - }) + 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) - console.log('[API] Song created successfully:', song.id) + const song = db.prepare('SELECT * FROM songs WHERE id = ?').get(songId) + + console.log('[API] Song created successfully:', songId) return NextResponse.json(song) } catch (error) { console.error('[API] Error adding song:', error) @@ -64,11 +59,7 @@ export async function POST(request: NextRequest) { export async function GET() { try { - const songs = await prisma.song.findMany({ - where: { isInLibrary: true }, - orderBy: { addedAt: 'desc' }, - }) - + const songs = db.prepare('SELECT * FROM songs WHERE isInLibrary = 1 ORDER BY createdAt DESC').all() 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 780d030..c431e8e 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 { prisma } from '@/lib/prisma' +import db from '@/lib/db' import { getStreamUrl } from '@/lib/ytdlp' import { streamCache } from '@/lib/cache' @@ -11,9 +11,7 @@ export async function GET( const { id } = await params // Get song from database - const song = await prisma.song.findUnique({ - where: { id }, - }) + const song = db.prepare('SELECT * FROM songs WHERE id = ?').get(id) as any if (!song) { return NextResponse.json( diff --git a/docker-compose.yml b/docker-compose.yml index 260d191..fa3647e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,14 +8,12 @@ services: - "3000:3000" volumes: # Persist database - - ./prisma:/app/prisma + - ./data:/app/data # Optional: persist yt-dlp cache - yt-dlp-cache:/home/nextjs/.cache/yt-dlp environment: - NODE_ENV=production - - DATABASE_URL=file:/app/prisma/dev.db restart: unless-stopped - command: sh -c "npx prisma db push && node server.js" volumes: yt-dlp-cache: diff --git a/lib/db.ts b/lib/db.ts new file mode 100644 index 0000000..1cd91a3 --- /dev/null +++ b/lib/db.ts @@ -0,0 +1,46 @@ +import Database from 'better-sqlite3' +import { join } from 'path' + +const dbPath = join(process.cwd(), 'data', 'bitify.db') +const db = new Database(dbPath) + +// Enable foreign keys +db.pragma('foreign_keys = ON') + +// 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); +`) + +export default db diff --git a/package.json b/package.json index bd0602a..7ddf202 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,12 @@ "private": true, "scripts": { "dev": "next dev", - "build": "prisma generate && next build", + "build": "next build", "start": "next start", - "lint": "next lint", - "prisma:generate": "prisma generate", - "prisma:push": "prisma db push" + "lint": "next lint" }, "dependencies": { - "@prisma/client": "^5.22.0", + "better-sqlite3": "^11.7.0", "lucide-react": "^0.454.0", "next": "^15.0.3", "react": "^18.3.1", @@ -19,11 +17,11 @@ }, "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", "postcss": "^8.4.49", - "prisma": "^5.22.0", "tailwindcss": "^4.0.0", "typescript": "^5.6.3" }