diff --git a/DESIGN b/DESIGN index c018d4b6..653a5e56 100644 --- a/DESIGN +++ b/DESIGN @@ -991,6 +991,81 @@ File Format: +Playlist: + Playlists are a new feature in Ocarina 6 and are modeled after Gmail + labels. Ocarina 6.1 will support two different playlists that the + user can add tracks to: banned and favorites. + + The playlist layer will maintain a queue that is used by the UI to + display tracks in a given playlist. This queue is inherited from + the base Queue class to provide extra features. + + Future releases will add support for more playlists. + +- Index: + Index playlist_db("playlist.db", true); + +- Queue: + class PlaylistQueue : public Queue { + public: + PlaylistQueue(); + fill(IndexEntry *); + }; + +- Default playlists: + Favorites: + The user will add music they really like to this playlist. + + Banned: + The user should add music they do not like to this playlist. + Tracks should be removed from the Library playqueue when they + are banned and added back to the playqueue when they are + un-banned. + +- PlaylistQueue API: + PlaylistQueue :: PlaylistQueue(); + Initialize a Queue with the flags Q_ENABLED, Q_REPEAT, and + Q_NO_SORT set. Default sorting order should be artist, year, + track. + + PlaylistQueue :: fill(IndexEntry *ent); + Remove all tracks in the queue and repopulate using ent. + +- API + void playlist :: init(): + Load the playlist index from file. + + void playlist :: add(Track *track, const std::string &name); + Add track->id to the playlist named "name" and return true. + Return false if the playlist does not exist. + + If "name" is the currently selected playlist, add the track + to the PlaylistQueue. + + void playlist :: del(Track *track, const std::string &name); + Remove track->id from the playlist named "name" and return true. + Return false if the playlist does not exist or if the track + is not in the playlist. + + If "name" is the currently selected playlist, remove the track + from the PlaylistQueue. + + bool playlist :: has(Track *track, const std::string &name); + Return true if the chosen playlist has the given track. + Return false otherwise. + + void playlist :: select(const std::string &name); + Change the currently displayed playlist to "name". + + const IndexEntry *playlist :: get_tracks(const std::string &name); + Return the IndexEntry represeting the requested playlist. + Return NULL if the requested playlist does not exist. + + Queue *playlist :: get_queue(); + Return the PlaylistQueue to the caller. + + + @@ -1101,50 +1176,6 @@ Library: (lib/library.cpp) -Playlists: (lib/playlist.cpp) - Playlists are a new feature in Ocarina 6 and are modeled after Gmail - labels. Ocarina 6.1 will support two different playlists that the - user can add tracks to: banned and favorites. - - Future releases will add support for more playlists. - -- Database: - Database playlist_db - -- Default playlists: - Favorites: - The user will add music they really like to this playlist. - - Banned: - The user should add music they do not like to this playlist. - Tracks should be removed from the Library playqueue when they - are banned and added back to the playqueue when they are - un-banned. - -- API - void playlist :: init(): - Load the playlist database. - - void playlist :: add(name, track_id); - Add the track_id to the playlist named "name". Save the - database to disk. - Throw -EEXIST if "name" does not exist. - - void playlist :: del(name, track_id); - Remove the track_id from the playlist named "name". Save the - database to disk. Attempting to remove a track_id that does - not exist is not an error. - Throw -EEXIST if "name" does not exist. - - const std::set &playlist :: get_tracks(name); - Return the set of tracks representing the requested group. - Throw -EEXIST if "name" does not exist. - - void playlist :: clear(); - This function only exists if CONFIG_TEST is enabled. Clear - (reset) the playlist database. - - Deck: (lib/deck.cpp) The playqueue deck is used to hold the temporary playqueues created by diff --git a/gui/collection.cpp b/gui/collection.cpp index a69fe4e8..a07b9b03 100644 --- a/gui/collection.cpp +++ b/gui/collection.cpp @@ -46,7 +46,7 @@ bool CollectionTab :: on_key_press_event(const std::string &key) tab_selected_ids(ids); for (unsigned int i = 0; i < ids.size(); i++) - playlist :: add("Banned", ids[i]); + playlist :: add(tagdb :: lookup(ids[i]), "Banned"); return true; } diff --git a/gui/gui.cpp b/gui/gui.cpp index 959e62f0..aee1e1fb 100644 --- a/gui/gui.cpp +++ b/gui/gui.cpp @@ -78,11 +78,8 @@ static void on_track_loaded(Track *track) set_label_text(album, "x-large", "From: " + track->album->name); duration->set_text(track->length_str); - std::set ids = playlist :: get_tracks("Banned"); - bool banned = ids.find(track->id) != ids.end(); - - ids = playlist :: get_tracks("Favorites"); - bool favorite = ids.find(track->id) != ids.end(); + bool banned = playlist :: has(track, "Banned"); + bool favorite = playlist :: has(track, "Favorites"); ban_connection.block(); fav_connection.block(); @@ -107,12 +104,12 @@ static void on_pause_count_changed(bool enabled, unsigned int count) static void on_ban_toggled() { - Gtk::ToggleButton *ban = get_widget("o_pan"); + Gtk::ToggleButton *ban = get_widget("o_ban"); if (ban->get_active() == true) - playlist :: add("Banned", audio::current_trackid()); + playlist :: add(tagdb :: lookup(audio :: current_trackid()), "Banned"); else - playlist :: del("Banned", audio::current_trackid()); + playlist :: del(tagdb :: lookup(audio::current_trackid()), "Banned"); } static void on_fav_toggled() @@ -120,9 +117,9 @@ static void on_fav_toggled() Gtk::ToggleButton *fav = get_widget("o_favorite"); if (fav->get_active() == true) - playlist :: add("Favorites", audio::current_trackid()); + playlist :: add(tagdb :: lookup(audio::current_trackid()), "Favorites"); else - playlist :: del("Favorites", audio::current_trackid()); + playlist :: del(tagdb :: lookup(audio::current_trackid()), "Favorites"); } diff --git a/gui/main.cpp b/gui/main.cpp index fb446036..d441020f 100644 --- a/gui/main.cpp +++ b/gui/main.cpp @@ -33,6 +33,7 @@ Gtk::Window *ocarina_init(int *argc, char ***argv) library::init(); playlist::init(); + playlist :: select("Favorites"); share_file("ocarina6.glade"); post_init_tabs(); audio::load_state(); diff --git a/gui/model.cpp b/gui/model.cpp index 2df9d69c..22b4ecf9 100644 --- a/gui/model.cpp +++ b/gui/model.cpp @@ -58,7 +58,7 @@ void QueueModel::on_row_changed(unsigned int row) void QueueModel::on_path_selected(const Gtk::TreePath &path) { audio :: load_trackid(path_to_id(path)); - queue->path_selected(path[0]); + queue->track_selected(path[0]); audio :: play(); } diff --git a/gui/playlist.cpp b/gui/playlist.cpp index 9f9d4500..6d452628 100644 --- a/gui/playlist.cpp +++ b/gui/playlist.cpp @@ -20,7 +20,7 @@ public: PlaylistTab :: PlaylistTab() - : Tab(playlist :: get_pq()) + : Tab(playlist :: get_queue()) { tab_search = get_widget("o_playlist_entry"); tab_treeview = get_widget("o_playlist_pq_treeview"); @@ -43,7 +43,7 @@ bool PlaylistTab :: on_key_press_event(const std::string &key) tab_selected_ids(ids); for (unsigned int i = 0; i < ids.size(); i++) - playlist :: del(current_playlist(), ids[i]); + playlist :: del(tagdb :: lookup(ids[i]), current_playlist()); return true; } diff --git a/gui/tabs.cpp b/gui/tabs.cpp index 8cd102ab..55e506a2 100644 --- a/gui/tabs.cpp +++ b/gui/tabs.cpp @@ -190,7 +190,7 @@ bool Tab :: tab_add_to_playlist(const std::string &playlist) tab_selected_ids(ids); for (unsigned int i = 0; i < ids.size(); i++) - playlist :: add(playlist, ids[i]); + playlist :: add(tagdb :: lookup(ids[i]), playlist); return true; } diff --git a/include/callback.h b/include/callback.h index e7397b53..d06b4a40 100644 --- a/include/callback.h +++ b/include/callback.h @@ -25,11 +25,6 @@ struct Callbacks { void (*on_library_track_add)(unsigned int); void (*on_library_track_del)(unsigned int); void (*on_library_track_updated)(unsigned int); - void (*on_library_import_ban)(unsigned int); - - /* Playlist callbacks */ - void (*on_playlist_ban)(unsigned int); - void (*on_playlist_unban)(unsigned int); /* Queue callbacks */ void (*on_queue_track_add)(Queue *, unsigned int); diff --git a/include/playlist.h b/include/playlist.h index c188aff6..abd491c3 100644 --- a/include/playlist.h +++ b/include/playlist.h @@ -4,25 +4,22 @@ #ifndef OCARINA_PLAYLIST_H #define OCARINA_PLAYLIST_H +#include #include -#include -#include #include namespace playlist { void init(); - void add(const std::string &, unsigned int); - void del(const std::string &, unsigned int); + bool has(Track *, const std::string &); + void add(Track *, const std::string &); + void del(Track *, const std::string &); void select(const std::string &); - const std::set &get_tracks(const std::string &); - Queue *get_pq(); + IndexEntry *get_tracks(const std::string &); + Queue *get_queue(); -#ifdef CONFIG_TEST - void clear(); -#endif /* CONFIG_TEST */ }; #endif /* OCARINA_PLAYLIST_H */ diff --git a/lib/callback.cpp b/lib/callback.cpp index f67054cd..2623c80d 100644 --- a/lib/callback.cpp +++ b/lib/callback.cpp @@ -27,10 +27,6 @@ static struct Callbacks callbacks = { .on_library_track_add = no_op, .on_library_track_del = no_op, .on_library_track_updated = no_op, - .on_library_import_ban = no_op, - - .on_playlist_ban = no_op, - .on_playlist_unban = no_op, .on_queue_track_add = no_op, .on_queue_track_del = no_op, diff --git a/lib/deck.cpp b/lib/deck.cpp index b3a2528e..58401f5e 100644 --- a/lib/deck.cpp +++ b/lib/deck.cpp @@ -37,8 +37,6 @@ void deck :: init() library_playqueue.sort(SORT_TRACK, false); read(); - get_callbacks()->on_playlist_ban = del_library_track; - get_callbacks()->on_playlist_unban = add_library_track; get_callbacks()->on_library_track_add = add_library_track; get_callbacks()->on_library_track_del = del_library_track; get_callbacks()->on_library_track_updated = change_library_track; diff --git a/lib/playlist.cpp b/lib/playlist.cpp index 16dfcbe0..2b971368 100644 --- a/lib/playlist.cpp +++ b/lib/playlist.cpp @@ -1,103 +1,96 @@ /* * Copyright 2013 (c) Anna Schumaker. */ -#include -#include -#include #include -static std::set empty_set; -static Index playlist_db("playlist.db", false); -static Queue playlist_pq(Q_ENABLED | Q_REPEAT | Q_NO_SORT); -static std::string cur_pq; -static void import_ban_track(unsigned int track_id) -{ - playlist :: add("Banned", track_id); -} +class PlaylistQueue : public Queue { +public: + + PlaylistQueue() : Queue(Q_ENABLED | Q_REPEAT) + { + sort(SORT_ARTIST, true); + sort(SORT_YEAR, false); + sort(SORT_TRACK, false); + set_flag(Q_NO_SORT); + } + + void fill(IndexEntry *ent) + { + std::set::iterator it; + + while (size() > 0) + del((unsigned)0); + + for (it = ent->values.begin(); it != ent->values.end(); it++) + add(tagdb :: lookup(*it)); + } + +}; + + +static Index playlist_db("playlist.db", true); +static PlaylistQueue playlist_q; +static std::string cur_plist; + void playlist :: init() { - std::set ids; - std::set::iterator it; - - playlist_pq.sort(SORT_ARTIST, true); - playlist_pq.sort(SORT_YEAR, false); - playlist_pq.sort(SORT_TRACK, false); - - get_callbacks()->on_library_import_ban = import_ban_track; - playlist_db.load(); +} - ids = get_tracks("Banned"); - for (it = ids.begin(); it != ids.end(); it++) - get_callbacks()->on_playlist_ban(*it); +bool playlist :: has(Track *track, const std::string &name) +{ + std::set::iterator it; + IndexEntry *ent = playlist_db.find(name); - if (cur_pq == "") + if (ent == NULL) + return false; + + it = ent->values.find(track->id); + return it != ent->values.end(); +} + +void playlist :: add(Track *track, const std::string &name) +{ + if (!( (name == "Banned") || (name == "Favorites") )) return; - ids = get_tracks(cur_pq); - for (it = ids.begin(); it != ids.end(); it++) - playlist_pq.add(tagdb :: lookup(*it)); + if (!has(track, name)) { + playlist_db.insert(name, track->id); + if (cur_plist == name) + playlist_q.add(track); + } } -void playlist :: add(const std::string &name, unsigned int track_id) +void playlist :: del(Track *track, const std::string &name) { - if ((name == "Banned") || (name == "Favorites")) { - playlist_db.insert(name, track_id); - playlist_db.save(); - if (name == cur_pq) - playlist_pq.add(tagdb :: lookup(track_id)); - if (name == "Banned") - get_callbacks()->on_playlist_ban(track_id); - } else - throw -E_EXIST; -} - -void playlist :: del(const std::string &name, unsigned int track_id) -{ - if ((name == "Banned") || (name == "Favorites")) { - playlist_db.remove(name, track_id); - playlist_db.save(); - if (name == cur_pq) - playlist_pq.del(tagdb :: lookup(track_id)); - if (name == "Banned") - get_callbacks()->on_playlist_unban(track_id); - } else - throw -E_EXIST; + playlist_db.remove(name, track->id); + if (cur_plist == name) + playlist_q.del(track); } void playlist :: select(const std::string &name) { - std::set ids = get_tracks(name); - std::set::iterator it; + IndexEntry *ent; - while (playlist_pq.size() > 0) - playlist_pq.del((unsigned)0); + if (cur_plist == name) + return; - for (it = ids.begin(); it != ids.end(); it++) - playlist_pq.add(tagdb :: lookup(*it)); - cur_pq = name; + ent = playlist_db.find(name); + if (ent == NULL) + return; + + playlist_q.fill(ent); + cur_plist = name; } -const std::set &playlist :: get_tracks(const std::string &name) +IndexEntry *playlist :: get_tracks(const std::string &name) { - if ((name == "Banned") || (name == "Favorites")) { - IndexEntry *it = playlist_db.find(name); - if (it != NULL) - return it->values; - return empty_set; - } - throw -E_EXIST; + return playlist_db.find(name); } -Queue *playlist :: get_pq() +Queue *playlist :: get_queue() { - return &playlist_pq; + return &playlist_q; } - -#ifdef CONFIG_TEST -void playlist :: clear() -{ -} -#endif /* CONFIG_TEST */ diff --git a/tests/.gitignore b/tests/.gitignore index 8bb107e7..f560deb8 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -7,3 +7,4 @@ idle tags random queue +playlist diff --git a/tests/Playlist/playlist.db b/tests/Playlist/playlist.db new file mode 100644 index 00000000..315bdfb2 --- /dev/null +++ b/tests/Playlist/playlist.db @@ -0,0 +1,6 @@ +0 +2 +1 Banned +4 0 1 2 3 +1 Favorites +8 16 17 18 19 20 21 22 23 diff --git a/tests/Sconscript b/tests/Sconscript index 6e485ff7..c6cf7c42 100644 --- a/tests/Sconscript +++ b/tests/Sconscript @@ -18,6 +18,7 @@ tests = [ ("tags.cpp", True, [], [ "taglib" ]), ("random.cpp", False, [ "random.cpp" ], []), ("queue.cpp", True, [ "callback.cpp", "random.cpp" ], []), + ("playlist.cpp", True, [], []), ] @@ -65,8 +66,8 @@ for src, lib, extra, pkgs in tests: name = "%s" % src.rsplit(".")[0] if lib == True: - lib_files += [ src ] - extra = lib_files + extra + lib_files += [ src ] + extra + extra = lib_files for p in pkgs: env.UsePackage(p) @@ -81,4 +82,4 @@ ignore.close(); Return("res") -##scripts = [ "playlist", "library", "deck", "audio", "gui" ] +##scripts = [ "library", "deck", "audio", "gui" ] diff --git a/tests/playlist.cpp b/tests/playlist.cpp new file mode 100644 index 00000000..ccf21a0a --- /dev/null +++ b/tests/playlist.cpp @@ -0,0 +1,114 @@ +/* + * Copyright 2013 (c) Anna Schumaker. + */ +#include +#include "test.h" + +static IndexEntry *IDX_NULL = NULL; +static Queue *Q_NULL = NULL; + +static void test_init() +{ + IndexEntry *ent; + Queue *q = playlist :: get_queue(); + + test_not_equal(q, Q_NULL); + test_equal(q->has_flag(Q_ENABLED), true); + test_equal(q->has_flag(Q_REPEAT), true); + test_equal(q->has_flag(Q_NO_SORT), true); + + tagdb :: init(); + playlist :: init(); + + ent = playlist :: get_tracks("Banned"); + test_equal(ent->values.size(), (size_t)4); + ent = playlist :: get_tracks("Favorites"); + test_equal(ent->values.size(), (size_t)8); + ent = playlist :: get_tracks("No Such Playlist"); + test_equal(ent, IDX_NULL); +} + +static void test_queue() +{ + Queue *q = playlist :: get_queue(); + + playlist :: select("Banned"); + test_equal(q->size(), (unsigned)4); + + playlist :: select("Favorites"); + test_equal(q->size(), (unsigned)8); +} + +static void test_add() +{ + IndexEntry *ent; + Queue *q = playlist :: get_queue(); + + playlist :: add(tagdb :: lookup(5), "Banned"); + ent = playlist :: get_tracks("Banned"); + test_equal(ent->values.size(), (size_t)5); + test_equal(q->size(), (unsigned)8); + + playlist :: add(tagdb :: lookup(16), "Favorites"); + playlist :: add(tagdb :: lookup(5), "Favorites"); + ent = playlist :: get_tracks("Favorites"); + test_equal(ent->values.size(), (size_t)9); + test_equal(q->size(), (unsigned)9); + + playlist :: add(tagdb :: lookup(6), "No Playlist"); + test_equal(playlist :: get_tracks("No Playlist"), IDX_NULL); +} + +static void test_delete() +{ + IndexEntry *ent; + Queue *q = playlist :: get_queue(); + + playlist :: del(tagdb :: lookup(5), "Banned"); + ent = playlist :: get_tracks("Banned"); + test_equal(ent->values.size(), (size_t)4); + test_equal(q->size(), (unsigned)9); + + playlist :: del(tagdb :: lookup(5), "Favorites"); + ent = playlist :: get_tracks("Favorites"); + test_equal(ent->values.size(), (size_t)8); + test_equal(q->size(), (unsigned)8); + + playlist :: del(tagdb :: lookup(6), "No Playlist"); + test_equal(playlist :: get_tracks("No Playlist"), IDX_NULL); +} + +static void test_has() +{ + test :: begin(); + for (unsigned int i = 0; i < 24; i++) { + Track *track = tagdb :: lookup(i); + if (i <= 3) + check_equal(playlist :: has(track, "Banned"), true); + else + check_equal(playlist :: has(track, "Banned"), false); + } + test :: success(); + + test :: begin(); + for (unsigned int i = 0; i < 24; i++) { + Track *track = tagdb :: lookup(i); + if (i >= 16) + check_equal(playlist :: has(track, "Favorites"), true); + else + check_equal(playlist :: has(track, "Favorites"), false); + } + test :: success(); +} + +int main(int argc, char **argv) +{ + test :: cp_playlist(); + + run_test("Playlist Initialization Test", test_init); + run_test("Playlist Queue Test", test_queue); + run_test("Playlist Add Test", test_add); + run_test("Playlist Delete Test", test_delete); + run_test("Playlist Has Test", test_has); + return 0; +} diff --git a/tests/playlist/Sconscript b/tests/playlist/Sconscript deleted file mode 100644 index 4c0ff2c3..00000000 --- a/tests/playlist/Sconscript +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/python -Import("Test", "CONFIG") - -CONFIG.PLAYLIST = True - -Test("playlist", "playlist.cpp") diff --git a/tests/playlist/playlist.cpp b/tests/playlist/playlist.cpp deleted file mode 100644 index 47ad6f87..00000000 --- a/tests/playlist/playlist.cpp +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2013 (c) Anna Schumaker. - */ -#include -#include -#include - - -void list_tracks(const std::string &name) -{ - std::set tracks = playlist :: get_tracks(name); - std::set::iterator it; - - print("Playlist \"%s\": ", name.c_str()); - for (it = tracks.begin(); it != tracks.end(); it++) { - if (it != tracks.begin()) - print(", "); - print("%u", *it); - } - - print("\n"); -} - -void check_error(int expected, int error) -{ - if (expected != error) - print("Exception error: expected %d actual %d", expected, error); -} - -/* - * Add songs to different playlists - */ -void test_0() -{ - for (unsigned int i = 0; i < 128; i++) { - switch (i % 3) { - case 0: - playlist :: add("Banned", i); - break; - case 1: - playlist :: add("Favorites", i); - break; - default: - try { - playlist :: add("No Such Playlist", i); - } catch (int error) { - check_error(-E_EXIST, error); - } - } - } -} - -/* - * Find tracks in a playlist - */ -void test_1() -{ - list_tracks("Banned"); - list_tracks("Favorites"); - - try { - list_tracks("No Such Playlist"); - } catch (int error) { - check_error(-E_EXIST, error); - } -} - -/* - * Delete tracks from a playlist - */ -void test_2() -{ - print("\n"); - for (unsigned int i = 0; i < 30; i+=3) - playlist :: del("Banned", i); - list_tracks("Banned"); - - try { - playlist :: del("No Such Playlist", 2); - } catch (int error) { - check_error(-E_EXIST, error); - } -} - -/* - * Check persistence of playlists - */ -void test_3() -{ - print("\n"); - - playlist :: clear(); - playlist :: init(); - - list_tracks("Banned"); - list_tracks("Favorites"); - - try { - list_tracks("No Schu Playlist"); - } catch (int error) { - check_error(-E_EXIST, error); - } -} - -int main(int argc, char **argv) -{ - test_0(); - test_1(); - test_2(); - test_3(); - return 0; -} diff --git a/tests/playlist/playlist.good b/tests/playlist/playlist.good deleted file mode 100644 index 96e86e10..00000000 --- a/tests/playlist/playlist.good +++ /dev/null @@ -1,7 +0,0 @@ -Playlist "Banned": 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99, 102, 105, 108, 111, 114, 117, 120, 123, 126 -Playlist "Favorites": 1, 4, 7, 10, 13, 16, 19, 22, 25, 28, 31, 34, 37, 40, 43, 46, 49, 52, 55, 58, 61, 64, 67, 70, 73, 76, 79, 82, 85, 88, 91, 94, 97, 100, 103, 106, 109, 112, 115, 118, 121, 124, 127 - -Playlist "Banned": 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99, 102, 105, 108, 111, 114, 117, 120, 123, 126 - -Playlist "Banned": 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99, 102, 105, 108, 111, 114, 117, 120, 123, 126 -Playlist "Favorites": 1, 4, 7, 10, 13, 16, 19, 22, 25, 28, 31, 34, 37, 40, 43, 46, 49, 52, 55, 58, 61, 64, 67, 70, 73, 76, 79, 82, 85, 88, 91, 94, 97, 100, 103, 106, 109, 112, 115, 118, 121, 124, 127 diff --git a/tests/test.h b/tests/test.h index 07cb35b2..cea8f426 100644 --- a/tests/test.h +++ b/tests/test.h @@ -124,6 +124,13 @@ namespace test std::string cmd = "cp -r tests/Library/* " + data_dir(); system(cmd.c_str()); } + + void cp_playlist() + { + cp_library(); + std::string cmd = "cp -r tests/Playlist/* " + data_dir(); + system(cmd.c_str()); + } } #define run_test(name, func, ...) \