playlist: Update the design and unit test

I updated the code to better match how I use playlists and to make sure
that everything is tested.

Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
This commit is contained in:
Anna Schumaker 2014-05-18 21:09:46 -04:00
parent f6306faac8
commit 11df56139d
20 changed files with 292 additions and 280 deletions

119
DESIGN
View File

@ -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<database :: IndexEntry> 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<unsigned int> &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

View File

@ -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;
}

View File

@ -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<unsigned int> 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<Gtk::ToggleButton>("o_pan");
Gtk::ToggleButton *ban = get_widget<Gtk::ToggleButton>("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<Gtk::ToggleButton>("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");
}

View File

@ -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();

View File

@ -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();
}

View File

@ -20,7 +20,7 @@ public:
PlaylistTab :: PlaylistTab()
: Tab(playlist :: get_pq())
: Tab(playlist :: get_queue())
{
tab_search = get_widget<Gtk::SearchEntry>("o_playlist_entry");
tab_treeview = get_widget<Gtk::TreeView>("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;
}

View File

@ -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;
}

View File

@ -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);

View File

@ -4,25 +4,22 @@
#ifndef OCARINA_PLAYLIST_H
#define OCARINA_PLAYLIST_H
#include <index.h>
#include <queue.h>
#include <list>
#include <set>
#include <string>
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<unsigned int> &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 */

View File

@ -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,

View File

@ -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;

View File

@ -1,103 +1,96 @@
/*
* Copyright 2013 (c) Anna Schumaker.
*/
#include <callback.h>
#include <error.h>
#include <index.h>
#include <playlist.h>
static std::set<unsigned int> 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<unsigned int>::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<unsigned int> ids;
std::set<unsigned int>::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<unsigned int>::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<unsigned int> ids = get_tracks(name);
std::set<unsigned int>::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<unsigned int> &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 */

1
tests/.gitignore vendored
View File

@ -7,3 +7,4 @@ idle
tags
random
queue
playlist

View File

@ -0,0 +1,6 @@
0
2
1 Banned
4 0 1 2 3
1 Favorites
8 16 17 18 19 20 21 22 23

View File

@ -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" ]

114
tests/playlist.cpp Normal file
View File

@ -0,0 +1,114 @@
/*
* Copyright 2013 (c) Anna Schumaker.
*/
#include <playlist.h>
#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;
}

View File

@ -1,6 +0,0 @@
#!/usr/bin/python
Import("Test", "CONFIG")
CONFIG.PLAYLIST = True
Test("playlist", "playlist.cpp")

View File

@ -1,112 +0,0 @@
/*
* Copyright 2013 (c) Anna Schumaker.
*/
#include <error.h>
#include <playlist.h>
#include <print.h>
void list_tracks(const std::string &name)
{
std::set<unsigned int> tracks = playlist :: get_tracks(name);
std::set<unsigned int>::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;
}

View File

@ -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

View File

@ -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, ...) \