Replace database with simple JSON file storage

This commit is contained in:
bitc0de committed 2025-12-17 15:49:34 +01:00
1 parent e86d4d7d60
commit 491ac6628a
9 files changed
+208 -127

No files matched your search

+9 -14
View File
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { getDb } from '@/lib/db' import { db } from '@/lib/db'
export async function DELETE( export async function DELETE(
request: NextRequest, request: NextRequest,
@@ -8,8 +8,7 @@ export async function DELETE(
try { try {
const { id } = await params const { id } = await params
const db = getDb() db.deletePlaylist(id)
db.prepare('DELETE FROM playlists WHERE id = ?').run(id)
console.log('[API] Playlist deleted:', id) console.log('[API] Playlist deleted:', id)
return NextResponse.json({ success: true }) return NextResponse.json({ success: true })
@@ -29,8 +28,7 @@ export async function GET(
try { try {
const { id } = await params const { id } = await params
const db = getDb() const playlist = db.getPlaylistById(id)
const playlist = db.prepare('SELECT * FROM playlists WHERE id = ?').get(id) as any
if (!playlist) { if (!playlist) {
return NextResponse.json( return NextResponse.json(
@@ -40,16 +38,13 @@ export async function GET(
} }
// Get songs for this playlist // Get songs for this playlist
const songs = db.prepare(` const playlistSongs = db.getPlaylistSongs(id)
SELECT s.*, ps.addedAt FROM songs s const playlistWithSongs = {
INNER JOIN playlistSongs ps ON s.id = ps.songId ...playlist,
WHERE ps.playlistId = ? playlistSongs
ORDER BY ps.addedAt ASC }
`).all(id)
playlist.playlistSongs = songs.map((song: any) => ({ song })) return NextResponse.json(playlistWithSongs)
return NextResponse.json(playlist)
} catch (error) { } catch (error) {
console.error('[API] Error fetching playlist:', error) console.error('[API] Error fetching playlist:', error)
return NextResponse.json( return NextResponse.json(
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { getDb } from '@/lib/db' import { db } from '@/lib/db'
export async function DELETE( export async function DELETE(
request: NextRequest, request: NextRequest,
@@ -8,8 +8,7 @@ export async function DELETE(
try { try {
const { id, songId } = await params const { id, songId } = await params
const db = getDb() db.removeSongFromPlaylist(id, songId)
db.prepare('DELETE FROM playlistSongs WHERE playlistId = ? AND songId = ?').run(id, 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 })
+4 -19
View File
@@ -1,6 +1,5 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { getDb } from '@/lib/db' import { db } from '@/lib/db'
import { randomBytes } from 'crypto'
export async function POST( export async function POST(
request: NextRequest, request: NextRequest,
@@ -17,30 +16,16 @@ export async function POST(
) )
} }
// Check if already in playlist const playlistSong = db.addSongToPlaylist(id, songId)
const db = getDb()
const existing = db.prepare('SELECT * FROM playlistSongs WHERE playlistId = ? AND songId = ?').get(id, songId)
if (existing) { if (!playlistSong) {
return NextResponse.json( return NextResponse.json(
{ error: 'Song already in playlist' }, { error: 'Song already in playlist' },
{ status: 400 } { status: 400 }
) )
} }
// Get next position console.log('[API] Song added to playlist:', playlistSong.id)
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)
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)
+6 -18
View File
@@ -1,24 +1,16 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { getDb } from '@/lib/db' import { db } from '@/lib/db'
import { randomBytes } from 'crypto'
export async function GET() { export async function GET() {
try { try {
const db = getDb() const playlists = db.getPlaylists()
const playlists = db.prepare('SELECT * FROM playlists ORDER BY createdAt DESC').all() as any[]
// Get songs for each playlist // Get songs for each playlist
const playlistsWithSongs = playlists.map(playlist => { const playlistsWithSongs = playlists.map(playlist => {
const songs = db.prepare(` const playlistSongs = db.getPlaylistSongs(playlist.id)
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 { return {
...playlist, ...playlist,
playlistSongs: songs.map((song: any) => ({ song })) playlistSongs
} }
}) })
@@ -43,13 +35,9 @@ export async function POST(request: NextRequest) {
) )
} }
const db = getDb() const playlist = db.createPlaylist(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:', playlistId) console.log('[API] Playlist created:', playlist.id)
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 -3
View File
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { getDb } from '@/lib/db' import { db } from '@/lib/db'
export async function DELETE( export async function DELETE(
request: NextRequest, request: NextRequest,
@@ -10,8 +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
const db = getDb() db.updateSong(id, { isInLibrary: false })
db.prepare('UPDATE songs SET isInLibrary = 0 WHERE id = ?').run(id)
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 })
+12 -16
View File
@@ -1,7 +1,6 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { getDb } from '@/lib/db' 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 {
@@ -21,16 +20,14 @@ 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 db = getDb() const existingSong = db.getSongByYoutubeId(metadata.id)
const existingSong = db.prepare('SELECT * FROM songs WHERE youtubeId = ?').get(metadata.id) as any
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) {
db.prepare('UPDATE songs SET isInLibrary = 1 WHERE id = ?').run(existingSong.id) const updatedSong = db.updateSong(existingSong.id, { isInLibrary: true })
const updatedSong = db.prepare('SELECT * FROM songs WHERE id = ?').get(existingSong.id)
console.log('[API] Song restored to library:', existingSong.id) console.log('[API] Song restored to library:', existingSong.id)
return NextResponse.json(updatedSong) return NextResponse.json(updatedSong)
} }
@@ -39,15 +36,15 @@ export async function POST(request: NextRequest) {
} }
// Create new song in database // Create new song in database
const songId = randomBytes(16).toString('hex') const song = db.createSong({
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,
})
const song = db.prepare('SELECT * FROM songs WHERE id = ?').get(songId) console.log('[API] Song created successfully:', song.id)
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)
@@ -60,8 +57,7 @@ export async function POST(request: NextRequest) {
export async function GET() { export async function GET() {
try { try {
const db = getDb() const songs = db.getSongs()
const songs = db.prepare('SELECT * FROM songs WHERE isInLibrary = 1 ORDER BY createdAt DESC').all()
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 -3
View File
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { getDb } from '@/lib/db' 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,8 +11,7 @@ export async function GET(
const { id } = await params const { id } = await params
// Get song from database // Get song from database
const db = getDb() const song = db.getSongById(id)
const song = db.prepare('SELECT * FROM songs WHERE id = ?').get(id) as any
if (!song) { if (!song) {
return NextResponse.json( return NextResponse.json(
+171 -49
View File
@@ -1,60 +1,182 @@
import Database from 'better-sqlite3' import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'
import { join } from 'path' 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() { interface Playlist {
if (db) return db id: string
name: string
createdAt: number
}
const dataDir = join(process.cwd(), 'data') interface PlaylistSong {
id: string
// Create data directory if it doesn't exist 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)) { if (!existsSync(dataDir)) {
mkdirSync(dataDir, { recursive: true }) mkdirSync(dataDir, { recursive: true })
} }
}
const dbPath = join(dataDir, 'bitify.db') function readDb(): Database {
db = new Database(dbPath) 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 function writeDb(db: Database) {
db.pragma('foreign_keys = ON') ensureDataDir()
writeFileSync(dbPath, JSON.stringify(db, null, 2))
}
// Create tables export const db = {
db.exec(` // Songs
CREATE TABLE IF NOT EXISTS songs ( getSongs: () => {
id TEXT PRIMARY KEY, const data = readDb()
youtubeId TEXT UNIQUE NOT NULL, return data.songs.filter(s => s.isInLibrary).sort((a, b) => b.createdAt - a.createdAt)
title TEXT NOT NULL, },
channelName TEXT NOT NULL,
thumbnail TEXT NOT NULL, getSongById: (id: string) => {
duration INTEGER NOT NULL, const data = readDb()
isInLibrary INTEGER DEFAULT 1, return data.songs.find(s => s.id === id)
createdAt INTEGER DEFAULT (unixepoch()) },
);
getSongByYoutubeId: (youtubeId: string) => {
CREATE TABLE IF NOT EXISTS playlists ( const data = readDb()
id TEXT PRIMARY KEY, return data.songs.find(s => s.youtubeId === youtubeId)
name TEXT NOT NULL, },
createdAt INTEGER DEFAULT (unixepoch())
); createSong: (song: Omit<Song, 'id' | 'createdAt' | 'isInLibrary'>) => {
const data = readDb()
CREATE TABLE IF NOT EXISTS playlistSongs ( const newSong: Song = {
id TEXT PRIMARY KEY, ...song,
playlistId TEXT NOT NULL, id: randomBytes(16).toString('hex'),
songId TEXT NOT NULL, isInLibrary: true,
position INTEGER NOT NULL, createdAt: Date.now()
addedAt INTEGER DEFAULT (unixepoch()), }
FOREIGN KEY (playlistId) REFERENCES playlists(id) ON DELETE CASCADE, data.songs.push(newSong)
FOREIGN KEY (songId) REFERENCES songs(id) ON DELETE CASCADE, writeDb(data)
UNIQUE(playlistId, songId) return newSong
); },
CREATE INDEX IF NOT EXISTS idx_songs_youtubeId ON songs(youtubeId); updateSong: (id: string, updates: Partial<Song>) => {
CREATE INDEX IF NOT EXISTS idx_songs_isInLibrary ON songs(isInLibrary); const data = readDb()
CREATE INDEX IF NOT EXISTS idx_playlistSongs_playlistId ON playlistSongs(playlistId); const index = data.songs.findIndex(s => s.id === id)
CREATE INDEX IF NOT EXISTS idx_playlistSongs_songId ON playlistSongs(songId); if (index !== -1) {
`) data.songs[index] = { ...data.songs[index], ...updates }
writeDb(data)
return db 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)
}
} }
-2
View File
@@ -9,7 +9,6 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"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",
@@ -17,7 +16,6 @@
}, },
"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",