Browse Source

Added a randomize over albums option

Polochon-street 3 years ago
parent
commit
56108cea38
2 changed files with 176 additions and 11 deletions
  1. 172
    9
      mpd/client.py
  2. 4
    2
      src/analysis.c

+ 172
- 9
mpd/client.py View File

@@ -9,6 +9,7 @@ and MPD_PORT scheme described in `mpc` man.
9 9
 You can pass an integer argument to the script to change the length of the
10 10
 generated playlist (default is to add 20 songs).
11 11
 """
12
+import argparse
12 13
 import logging
13 14
 import math
14 15
 import os
@@ -102,8 +103,60 @@ if "XDG_DATA_HOME" in os.environ:
102 103
 else:
103 104
     _BLISSIFY_DATA_HOME = os.path.expanduser("~/.local/share/blissify")
104 105
 
106
+# Distance between two songs
107
+def distance(x, y):
108
+    distance = math.sqrt(
109
+        (x["tempo"] - y["tempo"])**2 +
110
+        (x["amplitude"] - y["amplitude"])**2 +
111
+        (x["frequency"] - y["frequency"])**2 +
112
+        (x["attack"] - y["attack"])**2)
113
+    return distance
105 114
 
106
-def main(queue_length):
115
+# Distance between a set and a point
116
+def distance_set(X, y):
117
+    temp_distance = distance(X[0], y);
118
+    for song in X:
119
+        a = distance(song, y); 
120
+        temp_distance = a if a < temp_distance else temp_distance;
121
+    return temp_distance;
122
+
123
+# Hausdorff distance between two sets
124
+def hauff_distance_sets(X, Y):
125
+    temp_distance = distance_set(X, Y[0]);
126
+    for song in X:
127
+        a = distance_set(Y, song);
128
+        temp_distance = a if a > temp_distance else temp_distance;
129
+
130
+    for song in Y:
131
+        a = distance_set(X, song);
132
+        temp_distance = a if a > temp_distance else temp_distance;
133
+    return temp_distance
134
+
135
+def mean_song(X):
136
+    count = 0;
137
+    result = { 'tempo': 0, 'amplitude': 0, 'frequency':0, 'attack':0 } 
138
+   
139
+    for song in X:
140
+        result["tempo"] += song["tempo"];
141
+        result["amplitude"] += song["amplitude"];
142
+        result["frequency"] += song["frequency"];
143
+        result["attack"] += song["attack"];
144
+        count = count + 1;
145
+    result["tempo"] /= count;
146
+    result["amplitude"] /= count;
147
+    result["frequency"] /= count;
148
+    result["attack"] /= count;
149
+    return result;
150
+   
151
+
152
+# Custom distance between two sets
153
+def distance_sets(X, Y):
154
+    a = mean_song(X);
155
+    b = mean_song(Y);
156
+
157
+    return distance(a, b);
158
+
159
+def main_album(queue_length):
107 160
     # Get MPD connection settings
108 161
     try:
109 162
         mpd_host = os.environ["MPD_HOST"]
@@ -142,12 +195,110 @@ def main(queue_length):
142 195
         current_song = playlist[-1].replace("file: ", "").rstrip()
143 196
     # If current playlist is empty
144 197
     else:
145
-        # Add a random song to start with
198
+        # Add a random song to start with TODO add a random album
146 199
         all_songs = [x["file"] for x in client.listall() if "file" in x]
147 200
         current_song = random.choice(all_songs)
148 201
         client.add(current_song)
149 202
     logging.info("Currently played song is %s." % (current_song,))
150 203
 
204
+    # Get current song coordinates
205
+    cur.execute("SELECT id, tempo, amplitude, frequency, attack, filename, album FROM songs WHERE filename=?", (current_song,))
206
+    current_song_coords = cur.fetchone()
207
+    if current_song_coords is None:
208
+        logging.error("Current song %s is not in db. You should update the db." %
209
+                      (current_song,))
210
+        client.close()
211
+        client.disconnect()
212
+        sys.exit(1)
213
+
214
+    for i in range(queue_length):
215
+        # No cache management
216
+        # Get all songs from the current album
217
+        album = current_song_coords["album"];
218
+        cur.execute("SELECT id, tempo, amplitude, frequency, attack, filename, album FROM songs WHERE album=?", (album,))
219
+        target_album_set = cur.fetchall();
220
+
221
+        # Get all other songs
222
+        cur.execute("SELECT id, tempo, amplitude, frequency, attack, filename, album FROM songs ORDER BY album")
223
+        tmp_song_data = cur.fetchone()
224
+        shortest_distance = -1 
225
+
226
+        # Check the best suitable album
227
+        while tmp_song_data:
228
+            current_album_set = list()
229
+            current_album_set.append(tmp_song_data)
230
+            tmp_song_data = cur.fetchone()
231
+
232
+            i = 0
233
+            # Get all songs from the current temporary album
234
+            while tmp_song_data:
235
+                if (current_album_set[i]["album"] == tmp_song_data["album"]):
236
+                    current_album_set.append(tmp_song_data);
237
+                else:
238
+                    break;
239
+                tmp_song_data = cur.fetchone()
240
+                i = i + 1
241
+            # Skip current album and already processed albums    
242
+            if ( (current_album_set[0]["album"] != target_album_set[0]["album"]) and
243
+                not (("file: %s" % (current_album_set[0]["filename"],)) in client.playlist()) ):
244
+                tmp_distance = distance_sets(current_album_set, target_album_set)
245
+                if tmp_distance < shortest_distance or shortest_distance == -1:
246
+                    shortest_distance = tmp_distance
247
+                    closest_album = current_album_set;
248
+
249
+        logging.info("Closest album found is \"%s\". Distance is %f." % (closest_album[0]["album"], shortest_distance))
250
+        for song in closest_album:
251
+            client.add(song["filename"])
252
+        current_song_coords = closest_album[-1]
253
+
254
+    conn.close()
255
+    client.close()
256
+    client.disconnect()
257
+
258
+def main_single(queue_length):
259
+    # Get MPD connection settings
260
+    try:
261
+        mpd_host = os.environ["MPD_HOST"]
262
+        try:
263
+            mpd_password, mpd_host = mpd_host.split("@")
264
+        except ValueError:
265
+            mpd_password = None
266
+    except KeyError:
267
+        mpd_host = "localhost"
268
+        mpd_password = None
269
+    try:
270
+        mpd_port = os.environ["MPD_PORT"]
271
+    except KeyError:
272
+        mpd_port = 6600
273
+
274
+    # Connect to MPD
275
+    client = PersistentMPDClient(host=mpd_host, port=mpd_port)
276
+    if mpd_password is not None:
277
+        client.password(mpd_password)
278
+    # Connect to db
279
+    db_path = os.path.join(_BLISSIFY_DATA_HOME, "db.sqlite3")
280
+    logging.debug("Using DB path: %s." % (db_path,))
281
+    conn = sqlite3.connect(db_path)
282
+    conn.row_factory = sqlite3.Row
283
+    conn.execute('pragma foreign_keys=ON')
284
+    cur = conn.cursor()
285
+
286
+    # Ensure random is not enabled
287
+    status = client.status()
288
+    if int(status["random"]) != 0:
289
+        logging.warning("Random mode is enabled. Are you sure you want it?")
290
+
291
+	# Take the last song from current playlist and iterate from it
292
+    playlist = client.playlist()
293
+    if len(playlist) > 0:
294
+        current_song = playlist[-1].replace("file: ", "").rstrip()
295
+    # If current playlist is empty
296
+    else:
297
+        # Add a random song to start with
298
+        all_songs = [x["file"] for x in client.listall() if "file" in x]
299
+        current_song = random.choice(all_songs)
300
+        client.add(current_song)
301
+    logging.info("Currently played song is %s." % (current_song,))
151 302
     # Get current song coordinates
152 303
     cur.execute("SELECT id, tempo, amplitude, frequency, attack, filename FROM songs WHERE filename=?", (current_song,))
153 304
     current_song_coords = cur.fetchone()
@@ -274,10 +425,22 @@ def main(queue_length):
274 425
 
275 426
 
276 427
 if __name__ == "__main__":
277
-    queue_length = _QUEUE_LENGTH
278
-    if len(sys.argv) > 1:
279
-        try:
280
-            queue_length = int(sys.argv[1])
281
-        except ValueError:
282
-            sys.exit("Usage: %s [PLAYLIST_LENGTH]" % (sys.argv[0],))
283
-    main(queue_length)
428
+    parser = argparse.ArgumentParser()
429
+    parser.add_argument("--queue-length", help="The number of items to add to the MPD playlist.", type=int)
430
+    group = parser.add_mutually_exclusive_group(required=True)
431
+    group.add_argument("--song-based", help="Make a playlist based on single songs.",
432
+        action="store_true", default=False)
433
+    group.add_argument("--album-based", help="Make a playlist based on whole albums.",
434
+        action="store_true", default=False)
435
+
436
+    args = parser.parse_args()
437
+    if args.queue_length:
438
+        queue_length = args.queue_length
439
+    else:
440
+        queue_length = _QUEUE_LENGTH
441
+
442
+    if args.song_based:
443
+        main_single(queue_length)
444
+    elif args.album_based:
445
+        main_album(queue_length)
446
+

+ 4
- 2
src/analysis.c View File

@@ -51,7 +51,8 @@ int _init_db(char *data_folder, char* db_path)
51 51
         amplitude REAL, \
52 52
         frequency REAL, \
53 53
         attack REAL, \
54
-        filename TEXT UNIQUE)",
54
+        filename TEXT UNIQUE, \
55
+		album TEXT)",
55 56
         NULL, NULL, NULL);
56 57
     if (SQLITE_OK != dberr) {
57 58
         fprintf(stderr, "Error creating db: %s.\n", sqlite3_errmsg(dbh));
@@ -151,7 +152,7 @@ int _parse_music_helper(
151 152
     }
152 153
     // Insert song analysis in database
153 154
     dberr = sqlite3_prepare_v2(dbh,
154
-            "INSERT INTO songs(tempo, amplitude, frequency, attack, filename) VALUES(?, ?, ?, ?, ?)",
155
+            "INSERT INTO songs(tempo, amplitude, frequency, attack, filename, album) VALUES(?, ?, ?, ?, ?, ?)",
155 156
             -1, &res, 0);
156 157
     if (SQLITE_OK != dberr) {
157 158
         fprintf(stderr, "Error while inserting data in db: %s\n\n", sqlite3_errmsg(dbh));
@@ -173,6 +174,7 @@ int _parse_music_helper(
173 174
     sqlite3_bind_double(res, 3, song_analysis.force_vector.frequency);
174 175
     sqlite3_bind_double(res, 4, song_analysis.force_vector.attack);
175 176
     sqlite3_bind_text(res, 5, song_uri, strlen(song_uri), SQLITE_STATIC);
177
+	sqlite3_bind_text(res, 6, song_analysis.album, strlen(song_analysis.album), SQLITE_STATIC);
176 178
     dberr = sqlite3_step(res);
177 179
     if (SQLITE_DONE != dberr) {
178 180
         // Free song analysis