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
+209 -128

No files matched your search

+9 -14
View File
@@ -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(
@@ -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 })
+4 -19
View File
@@ -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)
+6 -18
View File
@@ -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.createPlaylist(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)
} catch (error) {
console.error('[API] Error creating playlist:', error)
+2 -3
View File
@@ -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 })
+12 -16
View File
@@ -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)
+2 -3
View File
@@ -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(
+172 -50
View File
@@ -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')
interface PlaylistSong {
id: string
playlistId: string
songId: string
position: number
addedAt: number
}
// Create data directory if it doesn't exist
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)
// 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);
`)
return db
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'))
}
function writeDb(db: Database) {
ensureDataDir()
writeFileSync(dbPath, JSON.stringify(db, null, 2))
}
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<Song, 'id' | 'createdAt' | 'isInLibrary'>) => {
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<Song>) => {
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)
}
}
-2
View File
@@ -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",