Merge pull request #4 from kristy-fournier/sqlite

Complete backend redesign to using SQLite
This commit is contained in:
Kristy Fournier 2025-07-06 14:00:53 -04:00 committed by GitHub
commit 9c35196867
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 107 additions and 68 deletions

2
.gitignore vendored
View file

@ -1,3 +1,3 @@
server/sound/ server/sound/
server/songDatabase.json *.db
start.bat start.bat

View file

@ -314,9 +314,9 @@ document.addEventListener('keydown', function(e){
document.getElementById("playlist-mode").style.display = "none"; document.getElementById("playlist-mode").style.display = "none";
document.getElementById("settings-mode").style.display = "none"; document.getElementById("settings-mode").style.display = "none";
//.ontouch for mobile?? //.ontouch for mobile??
document.getElementById("volumerange").onchange = function() { document.getElementById("volumerange").onchange = async function() {
let returnValue = getFromServer({setting:"volume",level:this.value}, "settings") let returnValue = await getFromServer({setting:"volume",level:this.value}, "settings")
if (returnValue !=0) { if (returnValue["volumePassed"] !=0) {
alertText("Nothing is playing") alertText("Nothing is playing")
document.getElementById("volumerange").value = -1 document.getElementById("volumerange").value = -1
} }

View file

@ -1,7 +1,8 @@
import os import os
from mutagen.easyid3 import EasyID3 from mutagen.easyid3 import EasyID3
from mutagen.mp3 import MP3 from mutagen.mp3 import MP3
import requests, ast, time, math, argparse, json import sqlite3 as sql
import requests, ast, time, math, argparse
loading = ["-","\\","|","/"] loading = ["-","\\","|","/"]
@ -22,32 +23,35 @@ else:
# apikeylastfm = "KeyHere" # apikeylastfm = "KeyHere"
# soundLocation = "directoryHere" # soundLocation = "directoryHere"
songFiles = os.listdir(soundLocation) songFiles = os.listdir(soundLocation)
fileOfDB = sql.connect("songDatabase.db")
songDatabase = fileOfDB.cursor()
# setting song directory
songDatabase.execute("CREATE TABLE IF NOT EXISTS meta (id TEXT PRIMARY KEY, data TEXT);")
try:
songDatabase.execute("INSERT INTO meta (id, data) VALUES (?,?)",("songDirectory",soundLocation))
except:
songDatabase.execute("UPDATE meta SET data = ? WHERE id = 'songDirectory'", (soundLocation,))
if args.mode.lower() == "update": if args.mode.lower() == "update":
try: #Create if not exists
with open('songDatabase.json', 'r') as handle: songDatabase.execute("CREATE TABLE IF NOT EXISTS songs (filename TEXT PRIMARY KEY, title TEXT, artist TEXT, art TEXT, length INTEGER);")
songDatabaseList = json.load(handle) songDatabase.execute("SELECT filename FROM songs;")
except: dBfilelist = songDatabase.fetchall()
songDatabaseList={"songDirectory":soundLocation,'songData':{}} dBfilelistSet = set()
deleteySongs = [] for i in dBfilelist:
for i in songDatabaseList["songData"]: dBfilelistSet.add(i[0])
try: # Delete nonexistant files
if songFiles.index(i) == -1: deleteySongs = list(dBfilelistSet - set(songFiles))
deleteySongs.append(i) songDatabase.executemany("DELETE FROM songs WHERE filename = ?", [(item,) for item in deleteySongs]) # in this line it turns the list of strings into a list of tuples of strings
except: print("Deleted: " + ", ".join(deleteySongs)+ " from database")
deleteySongs.append(i) # only include new files in list to be used
if deleteySongs: songFiles = list(set(songFiles) - dBfilelistSet)
print("deleted: " + ", ".join(deleteySongs)+ " from database")
for i in deleteySongs:
songDatabaseList["songData"].pop(i)
for i in songDatabaseList["songData"]:
songFiles.remove(i)
# This prints everything in the directory, including non mp3s
# theres not agood way to fix this without looping again.
print("new songs: " + ", ".join(songFiles)) print("new songs: " + ", ".join(songFiles))
elif args.mode.lower()=="new": elif args.mode.lower()=="new":
songDatabaseList={"songDirectory":soundLocation,'songData':{}} songDatabase.execute("DROP TABLE IF EXISTS songs;")
songDatabase.execute("CREATE TABLE songs (filename TEXT PRIMARY KEY, title TEXT, artist TEXT, art TEXT, length INTEGER);")
else: else:
raise ValueError("Must be \"new\" or \"update\"") raise ValueError("Must be \"new\" or \"update\"")
if args.art.lower() == "true" and not(args.apikey == ""): if args.art.lower() == "true" and not(args.apikey == ""):
x = len(songFiles)*0.25 x = len(songFiles)*0.25
if x > 60: if x > 60:
@ -65,12 +69,19 @@ for i in songFiles:
title = song['title'][0] title = song['title'][0]
artist = song['artist'][0] artist = song['artist'][0]
except: except:
try: if "_" in i:
# if metadata is missing, try to use file name following title_artist.mp3 # if metadata is missing, try to use file name following title_artist.mp3
song = i.split("_") song = i.split("_")
title = song[0] title = song[0]
artist = song[1].split(".")[0] artist = song[1].split(".")[0]
except: elif "-" in i:
# if there's no underscore, try artist - title.mp3
song = i.split("-")
title = song[1].split(".")[0]
artist = song[0]
title = title.strip()
artist = artist.strip()
else:
#if the file is not formatted with an underscore, the title is the file name #if the file is not formatted with an underscore, the title is the file name
title = i title = i
artist = None artist = None
@ -95,7 +106,8 @@ for i in songFiles:
index = (songFiles.index(i))%4 index = (songFiles.index(i))%4
print("\r" + str(loading[index] + str(math.floor((songFiles.index(i)/(len(songFiles)-1))*100))+ "%"), end='', flush=True) print("\r" + str(loading[index] + str(math.floor((songFiles.index(i)/(len(songFiles)-1))*100))+ "%"), end='', flush=True)
# each "song" is stored as a dictionary/JSON entry following the format seen in the readME # each "song" is stored as a dictionary/JSON entry following the format seen in the readME
songDatabaseList["songData"][i] = ({"title":title,"artist":artist,"art":image,"length":length}) songDatabase.execute(f"INSERT INTO songs (filename, title, artist, art, length) VALUES (?,?,?,?,?)",(i,title,artist,image,length))
with open('songDatabase.json', 'w') as handle:
json.dump(songDatabaseList, handle)
fileOfDB.commit()

View file

@ -1,7 +1,8 @@
from flask import Flask from flask import Flask
from flask import request from flask import request
from flask_cors import CORS from flask_cors import CORS
import json,vlc,threading,time,random, argparse import sqlite3 as sql
import vlc,threading,time,random, argparse
# Argparse Stuff # Argparse Stuff
parser=argparse.ArgumentParser(description="Options for the Webby Bits") parser=argparse.ArgumentParser(description="Options for the Webby Bits")
# this is no longer needed assuming my file works correctly with the generator # this is no longer needed assuming my file works correctly with the generator
@ -9,10 +10,12 @@ parser=argparse.ArgumentParser(description="Options for the Webby Bits")
parser.add_argument('-p','--port',help="Port to host on, not the same as the web (client) port",default='19054') parser.add_argument('-p','--port',help="Port to host on, not the same as the web (client) port",default='19054')
portTheUserPicked=parser.parse_args().port portTheUserPicked=parser.parse_args().port
# open the json file as a dictionary fileofDB = sql.connect("songDatabase.db")
with open('./songDatabase.json', 'r') as handle: songDatabase = fileofDB.cursor()
songDatabaseList = json.load(handle)
soundLocation = songDatabaseList["songDirectory"] #song directory
songDatabase.execute("SELECT * FROM meta WHERE id='songDirectory';")
soundLocation = songDatabase.fetchall()[0][1]
if soundLocation[-1] == "/" or soundLocation[-1] == "\\": if soundLocation[-1] == "/" or soundLocation[-1] == "\\":
pass pass
@ -20,6 +23,13 @@ elif "/" in soundLocation:
soundLocation += "/" soundLocation += "/"
else: else:
soundLocation += "\\" soundLocation += "\\"
print(soundLocation)
#Create Virtual table for searching
songDatabase.execute("DROP TABLE virtualSongs;")
songDatabase.execute("CREATE VIRTUAL TABLE virtualSongs USING fts5(filename, title, artist, art, length);")
songDatabase.execute("INSERT INTO virtualSongs SELECT * FROM songs;")
fileofDB.commit()
fileofDB.close()
#Initializing all the global stuff #Initializing all the global stuff
random.seed() random.seed()
global partyMode global partyMode
@ -55,7 +65,7 @@ def playQueuedSongs():
player.stop() player.stop()
skipNow = False skipNow = False
songNext = playlist.pop(0) songNext = playlist.pop(0)
media = fakeplayer.media_new("sound/"+songNext) media = fakeplayer.media_new(soundLocation+songNext)
player.set_media(media) player.set_media(media)
player.play() player.play()
elif (skipNow==True or (z == "State.Ended" or z == "State.NothingSpecial" or z=="State.Stopped")): elif (skipNow==True or (z == "State.Ended" or z == "State.NothingSpecial" or z=="State.Stopped")):
@ -64,9 +74,13 @@ def playQueuedSongs():
songNext = None songNext = None
player.stop() player.stop()
elif len(playlist)<1 and (partyMode == True): elif len(playlist)<1 and (partyMode == True):
fileofDB = sql.connect("songDatabase.db")
songDatabase = fileofDB.cursor()
songDatabase.execute("SELECT * FROM songs ORDER BY RANDOM() LIMIT 1;")
result = songDatabase.fetchall()
# adds the random songs for party mode # adds the random songs for party mode
# the above 2 means this only applies if (a song is playing (or paused)) and the queue is empty # the above 2 means this only applies if (a song is playing (or paused)) and the queue is empty
playlist.append(random.choice(songDatabaseList)["file"]) playlist.append(result[0][0])
# check for new songs every second # check for new songs every second
# I just didn't want to eat too much processing looping # I just didn't want to eat too much processing looping
time.sleep(1) time.sleep(1)
@ -115,21 +129,27 @@ def settingsControl():
@app.route("/search", methods=['POST']) @app.route("/search", methods=['POST'])
def searchSongDB(): def searchSongDB():
recieveData=request.get_json(force=True) recieveData=request.get_json(force=True)
tempData = {} fileofDB = sql.connect("songDatabase.db")
songDatabase = fileofDB.cursor()
results = []
if (recieveData['search'] == ""): if (recieveData['search'] == ""):
tempData = songDatabaseList["songData"].copy() songDatabase.execute("SELECT * FROM virtualSongs")
results = songDatabase.fetchall()
else: else:
for i in songDatabaseList["songData"]: songDatabase.execute("SELECT * FROM virtualSongs WHERE virtualSongs MATCH ?",[recieveData['search']])
if ((songDatabaseList["songData"][i]["title"].lower().find(recieveData['search'].lower())) > -1): results = songDatabase.fetchall()
tempData[i] = songDatabaseList["songData"][i] tempdata = {}
# this is a temporary solution so i dont have to change the
try: for i in results:
if (songDatabaseList["songData"][i]["artist"].lower().find(recieveData['search'].lower()) > -1): tempdata[i[0]] = {
tempData[i] = songDatabaseList["songData"][i] "title": i[1],
except: "artist": i[2],
pass "art": i[3],
"length": i[4]
}
# print(tempData) # print(tempData)
return tempData fileofDB.close()
return tempdata
@app.route("/songadd", methods=["POST"]) @app.route("/songadd", methods=["POST"])
def songadd(): def songadd():
@ -139,17 +159,36 @@ def songadd():
@app.route("/playlist", methods=["POST"]) @app.route("/playlist", methods=["POST"])
def getPlaylist(): def getPlaylist():
global songNext global songNext
fileofDB = sql.connect("songDatabase.db")
songDatabase = fileofDB.cursor()
tempPlaylist = [] tempPlaylist = []
if songNext != None: if songNext != None:
# Adds the currently playing song # Adds the currently playing song
k = songDatabaseList["songData"][songNext] songDatabase.execute("SELECT * FROM songs WHERE filename = ?",[songNext])
result = songDatabase.fetchall()[0]
# again, this is still using the old JSON format to avoid client changes
k = {
"title": result[1],
"artist": result[2],
"art": result[3],
"length": result[4]
}
temp = k.copy() temp = k.copy()
temp["playing"] = True temp["playing"] = True
temp["time"] = player.get_time()/1000 temp["time"] = player.get_time()/1000
tempPlaylist.append({songNext:temp}) tempPlaylist.append({songNext:temp})
for i in playlist: for i in playlist:
tempPlaylist.append({i:songDatabaseList["songData"][i]}) songDatabase.execute("SELECT * FROM songs WHERE filename = ?",[i])
result = songDatabase.fetchall()[0]
k = {
"title": result[1],
"artist": result[2],
"art": result[3],
"length": result[4]
}
tempPlaylist.append({i:k})
# print(tempPlaylist) # print(tempPlaylist)
fileofDB.close()
return tempPlaylist return tempPlaylist
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -24,7 +24,7 @@ webbyBits.py
1. Place mp3 files in the `sound/` folder 1. Place mp3 files in the `sound/` folder
2. Open `databaseGenerator.py` and put your LastFM API key in at the top or at runtime using `-k APIKey` (*optional*) 2. Open `databaseGenerator.py` and put your LastFM API key in at the top or at runtime using `-k APIKey` (*optional*)
3. Run `databaseGenerator.py` 3. Run `databaseGenerator.py`
* *The `databaseGenerator.py` will index all mp3 files, and save the information to `songDatabase.json`* * *The `databaseGenerator.py` will index all mp3 files, and save the information to `songDatabase.db`*
* *If getting images, this process may take a long time with a large amount of mp3 files* * *If getting images, this process may take a long time with a large amount of mp3 files*
4. Run `webbyBits.py` 4. Run `webbyBits.py`
* *The port can be customized at runtime using* `-p portNumber` *as an atribute* * *The port can be customized at runtime using* `-p portNumber` *as an atribute*
@ -39,7 +39,7 @@ These are specific details on each section of the app, and how to use them
- `sound/` contains all mp3 files by default - `sound/` contains all mp3 files by default
- `databaseGenerator.py` scans through mp3 files and gets information about them - `databaseGenerator.py` scans through mp3 files and gets information about them
- `Filename, Title, Artist, Art, Length` are all saved - `Filename, Title, Artist, Art, Length` are all saved
- *If the title and artist are not in the mp3 metadata, it looks for a format of* `TITLE_ARTIST.mp3` *and otherwise defaults to the file name as the title, and no artist* - *If the title and artist are not in the mp3 metadata, it looks for a format of* `TITLE_ARTIST.mp3` *then of* `ARTIST - TITLE.mp3` *and otherwise defaults to the file name as the title, and no artist*
- Art is retrieved from LastFM - Art is retrieved from LastFM
- Running with `--mode (update/new)` either updates the current database and adds new songs/removes deleted songs, or recreates the entire database (update is default, and is faster in art mode) - Running with `--mode (update/new)` either updates the current database and adds new songs/removes deleted songs, or recreates the entire database (update is default, and is faster in art mode)
- Running with `--art (True/False)` retrieves art from LastFM or doesn't (True is default) - Running with `--art (True/False)` retrieves art from LastFM or doesn't (True is default)
@ -50,19 +50,7 @@ These are specific details on each section of the app, and how to use them
- Default `"./sound/"` - Default `"./sound/"`
- _This setting might be kinda iffy on Linux. You're on Linux just go and edit it if you have issues_ - _This setting might be kinda iffy on Linux. You're on Linux just go and edit it if you have issues_
- ~~__Make certain you only use forward slashes in your directory, even on Windows__~~ I think this should be fine now i'll check later - ~~__Make certain you only use forward slashes in your directory, even on Windows__~~ I think this should be fine now i'll check later
- `songDatabase.json` stores all the information about each song in this format: - `songDatabase.db` stores all the information about each song in a SQLite database with tables `songs` and `meta`
```
{
"songDirectory": "./sound/",
"songData": {
"Circus_Fox Szn.mp3":
{"title": "Circus",
"artist": "Fox Szn",
"art": null,
"length": 141}
}
}
```
- `webbyBits.py` imports the database, runs all music playing, and accepts all commands from clients - `webbyBits.py` imports the database, runs all music playing, and accepts all commands from clients
- Searches return matching songs - Searches return matching songs
- Accepts Play-Pause and Skip commands - Accepts Play-Pause and Skip commands