Replace Prisma with better-sqlite3 for simpler deployment

This commit is contained in:
bitc0de committed 2025-12-17 15:32:10 +01:00
1 parent fae4fb179a
commit 06e2962fc5
11 files changed
+125 -112

No files matched your search

+2 -13
View File
@@ -5,7 +5,6 @@ WORKDIR /app
# Copy package files # Copy package files
COPY package*.json ./ COPY package*.json ./
COPY prisma ./prisma/
# Install dependencies # Install dependencies
RUN npm ci RUN npm ci
@@ -13,9 +12,6 @@ RUN npm ci
# Copy source code # Copy source code
COPY . . COPY . .
# Generate Prisma Client
RUN npx prisma generate
# Build Next.js application # Build Next.js application
RUN npm run build RUN npm run build
@@ -37,19 +33,12 @@ ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs RUN adduser --system --uid 1001 nextjs
# Install Prisma CLI for db push command
RUN npm install -g prisma
# Copy necessary files from builder # Copy necessary files from builder
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ 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/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.prisma ./node_modules/.prisma
# Copy Prisma schema and migrations # Create data directory for database
COPY --chown=nextjs:nodejs prisma ./prisma/ RUN mkdir -p /app/data && chown -R nextjs:nodejs /app
# Create database directory and set permissions
RUN mkdir -p /app/prisma && chown -R nextjs:nodejs /app
USER nextjs USER nextjs
+13 -17
View File
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma' import db from '@/lib/db'
export async function DELETE( export async function DELETE(
request: NextRequest, request: NextRequest,
@@ -8,9 +8,7 @@ export async function DELETE(
try { try {
const { id } = await params const { id } = await params
await prisma.playlist.delete({ db.prepare('DELETE FROM playlists WHERE id = ?').run(id)
where: { id },
})
console.log('[API] Playlist deleted:', id) console.log('[API] Playlist deleted:', id)
return NextResponse.json({ success: true }) return NextResponse.json({ success: true })
@@ -30,19 +28,7 @@ export async function GET(
try { try {
const { id } = await params const { id } = await params
const playlist = await prisma.playlist.findUnique({ const playlist = db.prepare('SELECT * FROM playlists WHERE id = ?').get(id) as any
where: { id },
include: {
playlistSongs: {
include: {
song: true,
},
orderBy: {
addedAt: 'asc',
},
},
},
})
if (!playlist) { if (!playlist) {
return NextResponse.json( 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) return NextResponse.json(playlist)
} catch (error) { } catch (error) {
console.error('[API] Error fetching playlist:', error) console.error('[API] Error fetching playlist:', error)
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma' import db from '@/lib/db'
export async function DELETE( export async function DELETE(
request: NextRequest, request: NextRequest,
@@ -8,14 +8,7 @@ export async function DELETE(
try { try {
const { id, songId } = await params const { id, songId } = await params
await prisma.playlistSong.delete({ db.prepare('DELETE FROM playlistSongs WHERE playlistId = ? AND songId = ?').run(id, songId)
where: {
playlistId_songId: {
playlistId: id,
songId: songId,
},
},
})
console.log('[API] Song removed from playlist') console.log('[API] Song removed from playlist')
return NextResponse.json({ success: true }) return NextResponse.json({ success: true })
+15 -16
View File
@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma' import db from '@/lib/db'
import { randomBytes } from 'crypto'
export async function POST( export async function POST(
request: NextRequest, request: NextRequest,
@@ -17,14 +18,7 @@ export async function POST(
} }
// Check if already in playlist // Check if already in playlist
const existing = await prisma.playlistSong.findUnique({ const existing = db.prepare('SELECT * FROM playlistSongs WHERE playlistId = ? AND songId = ?').get(id, songId)
where: {
playlistId_songId: {
playlistId: id,
songId: songId,
},
},
})
if (existing) { if (existing) {
return NextResponse.json( return NextResponse.json(
@@ -33,14 +27,19 @@ export async function POST(
) )
} }
const playlistSong = await prisma.playlistSong.create({ // Get next position
data: { const maxPosition = db.prepare('SELECT MAX(position) as max FROM playlistSongs WHERE playlistId = ?').get(id) as any
playlistId: id, const position = (maxPosition?.max ?? -1) + 1
songId: songId,
},
})
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) return NextResponse.json(playlistSong)
} catch (error) { } catch (error) {
console.error('[API] Error adding song to playlist:', error) console.error('[API] Error adding song to playlist:', error)
+23 -15
View File
@@ -1,20 +1,27 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma' import db from '@/lib/db'
import { randomBytes } from 'crypto'
export async function GET() { export async function GET() {
try { try {
const playlists = await prisma.playlist.findMany({ const playlists = db.prepare('SELECT * FROM playlists ORDER BY createdAt DESC').all() as any[]
orderBy: { createdAt: 'desc' },
include: { // Get songs for each playlist
playlistSongs: { const playlistsWithSongs = playlists.map(playlist => {
include: { const songs = db.prepare(`
song: true, 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) { } catch (error) {
console.error('[API] Error fetching playlists:', error) console.error('[API] Error fetching playlists:', error)
return NextResponse.json( return NextResponse.json(
@@ -35,11 +42,12 @@ export async function POST(request: NextRequest) {
) )
} }
const playlist = await prisma.playlist.create({ const playlistId = randomBytes(16).toString('hex')
data: { name }, 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) return NextResponse.json(playlist)
} catch (error) { } catch (error) {
console.error('[API] Error creating playlist:', error) console.error('[API] Error creating playlist:', error)
+2 -5
View File
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma' import db from '@/lib/db'
export async function DELETE( export async function DELETE(
request: NextRequest, request: NextRequest,
@@ -10,10 +10,7 @@ export async function DELETE(
// Instead of deleting, mark as not in library // Instead of deleting, mark as not in library
// This keeps the song in playlists but removes it from main library // This keeps the song in playlists but removes it from main library
await prisma.song.update({ db.prepare('UPDATE songs SET isInLibrary = 0 WHERE id = ?').run(id)
where: { id },
data: { isInLibrary: false },
})
console.log('[API] Song removed from library:', id) console.log('[API] Song removed from library:', id)
return NextResponse.json({ success: true }) return NextResponse.json({ success: true })
+15 -24
View File
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma' import db from '@/lib/db'
import { getYoutubeMetadata } from '@/lib/ytdlp' import { getYoutubeMetadata } from '@/lib/ytdlp'
import { randomBytes } from 'crypto'
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
@@ -20,20 +21,16 @@ export async function POST(request: NextRequest) {
console.log('[API] Got metadata:', metadata) console.log('[API] Got metadata:', metadata)
// Check if song already exists // Check if song already exists
const existingSong = await prisma.song.findUnique({ const existingSong = db.prepare('SELECT * FROM songs WHERE youtubeId = ?').get(metadata.id) as any
where: { youtubeId: metadata.id },
})
if (existingSong) { if (existingSong) {
console.log('[API] Song already exists:', existingSong.id) console.log('[API] Song already exists:', existingSong.id)
// If song was previously removed from library, restore it // If song was previously removed from library, restore it
if (!existingSong.isInLibrary) { if (!existingSong.isInLibrary) {
const updatedSong = await prisma.song.update({ db.prepare('UPDATE songs SET isInLibrary = 1 WHERE id = ?').run(existingSong.id)
where: { id: existingSong.id }, const updatedSong = db.prepare('SELECT * FROM songs WHERE id = ?').get(existingSong.id)
data: { isInLibrary: true }, console.log('[API] Song restored to library:', existingSong.id)
})
console.log('[API] Song restored to library:', updatedSong.id)
return NextResponse.json(updatedSong) return NextResponse.json(updatedSong)
} }
@@ -41,17 +38,15 @@ export async function POST(request: NextRequest) {
} }
// Create new song in database // Create new song in database
const song = await prisma.song.create({ const songId = randomBytes(16).toString('hex')
data: { db.prepare(`
youtubeId: metadata.id, INSERT INTO songs (id, youtubeId, title, channelName, thumbnail, duration)
title: metadata.title, VALUES (?, ?, ?, ?, ?, ?)
channelName: metadata.channel, `).run(songId, metadata.id, metadata.title, metadata.channel, metadata.thumbnail, metadata.duration)
thumbnail: metadata.thumbnail,
duration: 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) return NextResponse.json(song)
} catch (error) { } catch (error) {
console.error('[API] Error adding song:', error) console.error('[API] Error adding song:', error)
@@ -64,11 +59,7 @@ export async function POST(request: NextRequest) {
export async function GET() { export async function GET() {
try { try {
const songs = await prisma.song.findMany({ const songs = db.prepare('SELECT * FROM songs WHERE isInLibrary = 1 ORDER BY createdAt DESC').all()
where: { isInLibrary: true },
orderBy: { addedAt: 'desc' },
})
return NextResponse.json(songs) return NextResponse.json(songs)
} catch (error) { } catch (error) {
console.error('Error fetching songs:', error) console.error('Error fetching songs:', error)
+2 -4
View File
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma' import db from '@/lib/db'
import { getStreamUrl } from '@/lib/ytdlp' import { getStreamUrl } from '@/lib/ytdlp'
import { streamCache } from '@/lib/cache' import { streamCache } from '@/lib/cache'
@@ -11,9 +11,7 @@ export async function GET(
const { id } = await params const { id } = await params
// Get song from database // Get song from database
const song = await prisma.song.findUnique({ const song = db.prepare('SELECT * FROM songs WHERE id = ?').get(id) as any
where: { id },
})
if (!song) { if (!song) {
return NextResponse.json( return NextResponse.json(
+1 -3
View File
@@ -8,14 +8,12 @@ services:
- "3000:3000" - "3000:3000"
volumes: volumes:
# Persist database # Persist database
- ./prisma:/app/prisma - ./data:/app/data
# Optional: persist yt-dlp cache # Optional: persist yt-dlp cache
- yt-dlp-cache:/home/nextjs/.cache/yt-dlp - yt-dlp-cache:/home/nextjs/.cache/yt-dlp
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- DATABASE_URL=file:/app/prisma/dev.db
restart: unless-stopped restart: unless-stopped
command: sh -c "npx prisma db push && node server.js"
volumes: volumes:
yt-dlp-cache: yt-dlp-cache:
+46
View File
@@ -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
+4 -6
View File
@@ -4,14 +4,12 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "prisma generate && next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint"
"prisma:generate": "prisma generate",
"prisma:push": "prisma db push"
}, },
"dependencies": { "dependencies": {
"@prisma/client": "^5.22.0", "better-sqlite3": "^11.7.0",
"lucide-react": "^0.454.0", "lucide-react": "^0.454.0",
"next": "^15.0.3", "next": "^15.0.3",
"react": "^18.3.1", "react": "^18.3.1",
@@ -19,11 +17,11 @@
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4.0.0", "@tailwindcss/postcss": "^4.0.0",
"@types/better-sqlite3": "^7.6.12",
"@types/node": "^22.9.0", "@types/node": "^22.9.0",
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"prisma": "^5.22.0",
"tailwindcss": "^4.0.0", "tailwindcss": "^4.0.0",
"typescript": "^5.6.3" "typescript": "^5.6.3"
} }