Large refactor

* Fix strings overflows and associated segfaults
* Add some client code in Python
This commit is contained in:
Lucas Verney 2016-05-10 19:16:34 +02:00
parent 54cf7074e8
commit d5a1855b1d
9 changed files with 222 additions and 82 deletions

View File

@ -57,3 +57,13 @@ database and `mpdbliss` database in sync.
Check the `client.sh` script for an example client script to build smooth MPD
playlists.
out any flag, `mpdbliss` will listen for MPD IDLE protocol, and trigger
an update of the database whenever the MPD database is modified.
Typical usage would be to run a `--rescan` first, and then either do periodic
`--update` or let it run listening at MPD IDLE protocol to maintain MPD
database and `mpdbliss` database in sync.
Check the `client/client.py` script for an example client script to build smooth MPD
playlists.

View File

@ -1,24 +0,0 @@
#!/bin/sh
QUEUE_LENGTH=100
if [ -z "$XDG_DATA_HOME" ]; then
mpdbliss_data_home="$HOME/.local/share/mpdbliss"
else
mpdbliss_data_home="$XDG_DATA_HOME/mpdbliss"
fi
current_song=`mpc current --format "%file%"`
current_song="bad/_Compilations/8 Mile_ Music From and Inspired by the Motion Picture/01 - Lose Yourself.mp3"
for i in {1..$QUEUE_LENGTH}; do
# Find closest song
closest_song=`sqlite3 "$mpdbliss_data_home/db.sqlite3" "SELECT filename FROM (SELECT s2.filename AS filename, distances.distance AS distance FROM distances INNER JOIN songs AS s1 ON s1.id=distances.song1 INNER JOIN songs AS s2 on s2.id=distances.song2 WHERE s1.filename='$current_song' UNION SELECT s1.filename AS filename, distances.distance as distance FROM distances INNER JOIN songs AS s1 ON s1.id=distances.song1 INNER JOIN songs AS s2 on s2.id=distances.song2 WHERE s2.filename=\"$current_song\") ORDER BY distance ASC LIMIT 1"`
if [ ! -z "$closest_song" ]; then
# Push it on the queue
mpc add "$closest_song" 2>&1 > /dev/null
# Continue using latest pushed song as current song
current_song="$closest_song"
# Note: if song could not be found by mpd, it is just not added to the
# queue and skipped
fi
done

73
client/build_cache.py Normal file
View File

@ -0,0 +1,73 @@
#!/usr/bin/env python3
import logging
import math
import os
import sqlite3
logging.basicConfig(level=logging.INFO)
if "XDG_DATA_HOME" in os.environ:
_MPDBLISS_DATA_HOME = os.path.expandvars("$XDG_DATA_HOME/mpdbliss")
else:
_MPDBLISS_DATA_HOME = os.path.expanduser("~/.local/share/mpdbliss")
def main():
db_path = os.path.join(_MPDBLISS_DATA_HOME, "db.sqlite3")
logging.debug("Using DB path: %s." % (db_path,))
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
conn.execute('pragma foreign_keys=ON')
cur = conn.cursor()
# Get cached distances from db
cur.execute("SELECT song1, song2, distance FROM distances")
cached_distances = cur.fetchall()
# Get all songs
cur.execute("SELECT id, tempo, amplitude, frequency, attack, filename FROM songs")
all_songs = cur.fetchall()
for i in range(len(all_songs)):
for j in range(i + 1, len(all_songs)):
song1 = all_songs[i]
song2 = all_songs[j]
is_cached = len([i for i in cached_distances
if(i["song1"] == song1["id"] and
i["song2"] == song2["id"]) or
(i["song1"] == song2["id"] and
i["song2"] == song1["id"])]) > 0
if is_cached:
# Pass pair if cached value is already there
continue
# Compute distance
distance = math.sqrt(
(song1["tempo"] - song2["tempo"])**2 +
(song1["amplitude"] - song2["amplitude"])**2 +
(song1["frequency"] - song2["frequency"])**2 +
(song1["attack"] - song2["attack"])**2
)
logging.debug("Distance between %s and %s is %f." %
(song1["filename"], song2["filename"], distance))
# Store distance in db cache
try:
logging.debug("Storing distance in database.")
conn.execute(
"INSERT INTO distances(song1, song2, distance) VALUES(?, ?, ?)",
(song1["id"], song2["id"], distance))
conn.commit()
# Update cached_distances list
cached_distances.append({
"song1": song1["id"],
"song2": song2["id"],
"distance": distance
})
except sqlite3.IntegrityError:
logging.warning("Unable to insert distance in database.")
conn.rollback()
# Close connection
conn.close()
if __name__ == "__main__":
main()

121
client/client.py Normal file
View File

@ -0,0 +1,121 @@
#!/usr/bin/env python3
import logging
import math
import os
import sqlite3
import subprocess
import sys
logging.basicConfig(level=logging.INFO)
_QUEUE_LENGTH = 100
# TODO: Use cosine similarity as well
_DISTANCE_THRESHOLD = 4.0
if "XDG_DATA_HOME" in os.environ:
_MPDBLISS_DATA_HOME = os.path.expandvars("$XDG_DATA_HOME/mpdbliss")
else:
_MPDBLISS_DATA_HOME = os.path.expanduser("~/.local/share/mpdbliss")
def main():
mpd_queue = []
db_path = os.path.join(_MPDBLISS_DATA_HOME, "db.sqlite3")
logging.debug("Using DB path: %s." % (db_path,))
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
conn.execute('pragma foreign_keys=ON')
cur = conn.cursor()
current_song = subprocess.check_output(
["mpc", "current", '--format', '"%file%"'])
current_song = current_song.decode("utf-8").strip('" \r\n\t')
if current_song is "":
logging.warning("Currently played song could not be found.")
sys.exit(1)
logging.info("Currently played song is %s." % (current_song,))
# Get current song coordinates
cur.execute("SELECT id, tempo, amplitude, frequency, attack, filename FROM songs WHERE filename=?", (current_song,))
current_song = cur.fetchone()
if current_song is None:
logging.warning("Current song %s is not in db. You should update the db." %
(current_song["filename"],))
sys.exit(1)
for i in range(_QUEUE_LENGTH):
# Append current song to the mpd queue to avoid duplicates
mpd_queue.append(current_song["filename"])
# Get cached distances from db
cur.execute(
"SELECT id, filename, distance FROM (SELECT s2.id AS id, s2.filename AS filename, distances.distance AS distance FROM distances INNER JOIN songs AS s1 ON s1.id=distances.song1 INNER JOIN songs AS s2 on s2.id=distances.song2 WHERE s1.filename=? UNION SELECT s1.id as id, s1.filename AS filename, distances.distance as distance FROM distances INNER JOIN songs AS s1 ON s1.id=distances.song1 INNER JOIN songs AS s2 on s2.id=distances.song2 WHERE s2.filename=?) ORDER BY distance ASC",
(current_song["filename"], current_song["filename"]))
cached_distances = [row
for row in cur.fetchall()
if row["filename"] not in mpd_queue]
cached_distances_songs = [i["filename"] for i in cached_distances]
# If distance to closest song is ok, just add the song
if len(cached_distances) > 0:
if cached_distances[0]["distance"] < _DISTANCE_THRESHOLD:
# Push it on the queue
subprocess.check_call(["mpc", "add",
cached_distances[0]["filename"]])
# Continue using latest pushed song as current song
logging.info("Using cached distance. Found %s. Distance is %f." %
(current_song["filename"], cached_distances[0]["distance"]))
current_song = cached_distances[0]
continue
# Get all other songs coordinates
closest_song = None
cur.execute("SELECT id, tempo, amplitude, frequency, attack, filename FROM songs")
for tmp_song_data in cur.fetchall():
if(tmp_song_data["filename"] == current_song["filename"] or
tmp_song_data["filename"] in cached_distances_songs or
tmp_song_data["filename"] in mpd_queue):
# Skip current song and already processed songs
logging.debug("Skipping %s." % (tmp_song_data["filename"]))
continue
# Compute distance
distance = math.sqrt(
(current_song["tempo"] - tmp_song_data["tempo"])**2 +
(current_song["amplitude"] - tmp_song_data["amplitude"])**2 +
(current_song["frequency"] - tmp_song_data["frequency"])**2 +
(current_song["attack"] - tmp_song_data["attack"])**2
)
logging.debug("Distance between %s and %s is %f." %
(current_song["filename"],
tmp_song_data["filename"], distance))
# Store distance in db cache
try:
logging.debug("Storing distance in database.")
conn.execute(
"INSERT INTO distances(song1, song2, distance) VALUES(?, ?, ?)",
(current_song["id"], tmp_song_data["id"], distance))
conn.commit()
except sqlite3.IntegrityError:
logging.warning("Unable to insert distance in database.")
conn.rollback()
# If distance is ok, just add the song
if distance < _DISTANCE_THRESHOLD:
# Push it on the queue
subprocess.check_call(["mpc", "add", tmp_song_data["filename"]])
# Continue using latest pushed song as current song
logging.info("Found a close song: %s. Distance is %f." %
(tmp_song_data["filename"], distance))
current_song = tmp_song_data
break
elif closest_song is None or distance < closest_song[1]:
closest_song = (tmp_song_data, distance)
# If no song found, take the closest one
logging.info("No close enough song found. Using %s. Distance is %f." %
(closest_song[0]["filename"], closest_song[1]))
current_song = closest_song[0]
subprocess.check_call(["mpc", "add", closest_song[0]["filename"]])
conn.close()
if __name__ == "__main__":
main()

6
include/constants.h Normal file
View File

@ -0,0 +1,6 @@
#ifndef CONSTANTS_H
#define CONSTANTS_H
#define DEFAULT_STRING_LENGTH 10000
#endif // CONSTANTS_H

View File

@ -1,8 +1,6 @@
#ifndef UTILITIES_H
#define UTILITIES_H
#define DEFAULT_STRING_LENGTH 1024
/**
* Strip the trailing slash from a string.

View File

@ -5,6 +5,7 @@
#include <sqlite3.h>
#include "analysis.h"
#include "constants.h"
#include "utilities.h"
// TODO: Handle deletions from db
@ -17,8 +18,8 @@ int main(int argc, char** argv) {
}
// Get data directory, init db file
char mpdbliss_data_folder[DEFAULT_STRING_LENGTH] = "";
char mpdbliss_data_db[DEFAULT_STRING_LENGTH] = "";
char mpdbliss_data_folder[DEFAULT_STRING_LENGTH + 1] = "";
char mpdbliss_data_db[DEFAULT_STRING_LENGTH + 1] = "";
if (0 != _init_db(mpdbliss_data_folder, mpdbliss_data_db)) {
exit(EXIT_FAILURE);
}

View File

@ -9,6 +9,7 @@
#include "analysis.h"
#include "cmdline.h"
#include "constants.h"
#include "utilities.h"
// TODO: Handle deletions from db
@ -186,20 +187,20 @@ int main(int argc, char** argv) {
}
// Handle mpd_root argument
char mpd_base_path[DEFAULT_STRING_LENGTH] = "";
char mpd_base_path[DEFAULT_STRING_LENGTH + 1] = "";
strncat(mpd_base_path, args_info.mpd_root_arg, DEFAULT_STRING_LENGTH);
strip_trailing_slash(mpd_base_path);
strncat(mpd_base_path, "/", DEFAULT_STRING_LENGTH - strlen(mpd_base_path));
// Get data directory, init db file
char mpdbliss_data_folder[DEFAULT_STRING_LENGTH] = "";
char mpdbliss_data_db[DEFAULT_STRING_LENGTH] = "";
char mpdbliss_data_folder[DEFAULT_STRING_LENGTH + 1] = "";
char mpdbliss_data_db[DEFAULT_STRING_LENGTH + 1] = "";
if (0 != _init_db(mpdbliss_data_folder, mpdbliss_data_db)) {
exit(EXIT_FAILURE);
}
// Set data file path
char mpdbliss_data_file[DEFAULT_STRING_LENGTH] = "";
char mpdbliss_data_file[DEFAULT_STRING_LENGTH + 1] = "";
strncat(mpdbliss_data_file, mpdbliss_data_folder, DEFAULT_STRING_LENGTH);
strncat(mpdbliss_data_file, "/latest_mtime.txt", DEFAULT_STRING_LENGTH - strlen(mpdbliss_data_file));

View File

@ -7,6 +7,7 @@
#include <bliss.h>
#include "constants.h"
#include "utilities.h"
@ -21,7 +22,7 @@ int _init_db(char *data_folder, char* db_path)
strncat(data_folder, "/.local/share/mpdbliss", DEFAULT_STRING_LENGTH - strlen(data_folder));
}
else {
strncpy(data_folder, xdg_data_home_env, DEFAULT_STRING_LENGTH);
strncat(data_folder, xdg_data_home_env, DEFAULT_STRING_LENGTH);
strip_trailing_slash(data_folder);
strncat(data_folder, "/mpdbliss", DEFAULT_STRING_LENGTH - strlen(data_folder));
}
@ -92,9 +93,9 @@ int _parse_music_helper(
// Compute full uri
printf("\nAdding new song to db: %s\n", song_uri);
char song_full_uri[DEFAULT_STRING_LENGTH] = "";
char song_full_uri[DEFAULT_STRING_LENGTH + 1] = "";
strncat(song_full_uri, base_path, DEFAULT_STRING_LENGTH);
strncat(song_full_uri, song_uri, DEFAULT_STRING_LENGTH);
strncat(song_full_uri, song_uri, DEFAULT_STRING_LENGTH - strlen(song_full_uri));
// Pass it to bliss
struct bl_song song_analysis;
@ -156,54 +157,7 @@ int _parse_music_helper(
sqlite3_bind_double(res, 4, song_analysis.force_vector.attack);
sqlite3_bind_text(res, 5, song_uri, strlen(song_uri), SQLITE_STATIC);
sqlite3_step(res);
sqlite3_finalize(res);
int last_id = sqlite3_last_insert_rowid(dbh);
// Insert updated distances
dberr = sqlite3_prepare_v2(dbh, "SELECT id, tempo, amplitude, frequency, attack FROM songs", -1, &res, 0);
if (SQLITE_OK != dberr) {
fprintf(stderr, "Error while inserting data in db: %s\n\n", sqlite3_errmsg(dbh));
// Free song analysis
bl_free_song(&song_analysis);
sqlite3_exec(dbh, "ROLLBACK", NULL, NULL, NULL);
// Store error in db
sqlite3_prepare_v2(dbh,
"INSERT INTO errors(filename) VALUES(?)",
-1, &res, 0);
sqlite3_bind_text(res, 1, song_uri, strlen(song_uri), SQLITE_STATIC);
sqlite3_step(res);
sqlite3_finalize(res);
// Pass file
return 1;
}
int dberr2 = SQLITE_OK;
while (sqlite3_step(res) == SQLITE_ROW) {
int id = sqlite3_column_int(res, 0);
if (id == last_id) {
// Skip last inserted item
continue;
}
struct force_vector_s song_db;
song_db.tempo = sqlite3_column_double(res, 1);
song_db.amplitude = sqlite3_column_double(res, 2);
song_db.frequency = sqlite3_column_double(res, 3);
song_db.attack = sqlite3_column_double(res, 4);
float distance = bl_distance(song_analysis.force_vector, song_db);
sqlite3_stmt *res2;
dberr2 = sqlite3_prepare_v2(dbh,
"INSERT INTO distances(song1, song2, distance) VALUES(?, ?, ?)",
-1, &res2, 0);
if (SQLITE_OK != dberr2) {
fprintf(stderr, "Error while inserting data in db: %s\n\n", sqlite3_errmsg(dbh));
break;
}
sqlite3_bind_int(res2, 1, last_id);
sqlite3_bind_int(res2, 2, id);
sqlite3_bind_double(res2, 3, distance);
sqlite3_step(res2);
sqlite3_finalize(res2);
}
if (SQLITE_OK != dberr2) {
// Free song analysis
bl_free_song(&song_analysis);
sqlite3_exec(dbh, "ROLLBACK", NULL, NULL, NULL);