diff --git a/README.md b/README.md index 4fea993..7d3640d 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/client.sh b/client.sh deleted file mode 100755 index 8123b1d..0000000 --- a/client.sh +++ /dev/null @@ -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 diff --git a/client/build_cache.py b/client/build_cache.py new file mode 100644 index 0000000..e2c6d70 --- /dev/null +++ b/client/build_cache.py @@ -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() diff --git a/client/client.py b/client/client.py new file mode 100644 index 0000000..d6385f8 --- /dev/null +++ b/client/client.py @@ -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() diff --git a/include/constants.h b/include/constants.h new file mode 100644 index 0000000..f7aa188 --- /dev/null +++ b/include/constants.h @@ -0,0 +1,6 @@ +#ifndef CONSTANTS_H +#define CONSTANTS_H + +#define DEFAULT_STRING_LENGTH 10000 + +#endif // CONSTANTS_H diff --git a/include/utilities.h b/include/utilities.h index 946af3f..319016f 100644 --- a/include/utilities.h +++ b/include/utilities.h @@ -1,8 +1,6 @@ #ifndef UTILITIES_H #define UTILITIES_H -#define DEFAULT_STRING_LENGTH 1024 - /** * Strip the trailing slash from a string. diff --git a/main_args.c b/main_args.c index 235821c..c65afd8 100644 --- a/main_args.c +++ b/main_args.c @@ -5,6 +5,7 @@ #include #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); } diff --git a/main_mpd.c b/main_mpd.c index a1c7452..fc41cb3 100644 --- a/main_mpd.c +++ b/main_mpd.c @@ -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)); diff --git a/src/analysis.c b/src/analysis.c index 0881565..de9f98b 100644 --- a/src/analysis.c +++ b/src/analysis.c @@ -7,6 +7,7 @@ #include +#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);