diff --git a/TODO b/TODO index 72479f8b..97a5f178 100644 --- a/TODO +++ b/TODO @@ -4,14 +4,6 @@ Future work: to the wild. This section will be a list of features that I want, but should be deferred to a future release so basic support can be coded. - - User created song groups: - Basic add and remove features can be implemented using the - Library and Banned Songs groups. This will give me a chance - to test saving groups on a small scale before letting users - create anything they want. - - - Save a user's playlist as a group: - - Fix track durations: Some tracks in my library are tagged with the wrong duration, so fix them as they are played. diff --git a/core/playlist.c b/core/playlist.c index cb16c9f5..ffdf61a7 100644 --- a/core/playlist.c +++ b/core/playlist.c @@ -11,6 +11,7 @@ struct playlist_type *playlist_types[] = { [PL_SYSTEM] = &pl_system, [PL_ARTIST] = &pl_artist, [PL_LIBRARY] = &pl_library, + [PL_USER] = &pl_user, }; @@ -18,6 +19,7 @@ void playlist_init(struct queue_ops *ops) { pl_system_init(ops); pl_artist_init(ops); + pl_user_init(ops); pl_library_init(ops); if (!settings_has(SETTINGS_CUR_TYPE) || @@ -29,6 +31,7 @@ void playlist_deinit() { pl_system_deinit(); pl_artist_deinit(); + pl_user_deinit(); pl_library_deinit(); } diff --git a/core/playlists/user.c b/core/playlists/user.c new file mode 100644 index 00000000..a3927125 --- /dev/null +++ b/core/playlists/user.c @@ -0,0 +1,210 @@ +/* + * Copyright 2016 (c) Anna Schumaker. + */ +#include + +static struct queue_ops *user_pl_ops = NULL; +static struct database user_db; + +static struct user_playlist *__user_db_alloc(gchar *name) +{ + struct user_playlist *playlist = g_malloc(sizeof(struct user_playlist)); + + dbe_init(&playlist->pl_dbe, playlist); + playlist->pl_playlist.pl_name = name; + playlist->pl_playlist.pl_type = PL_USER; + playlist_generic_init(&playlist->pl_playlist, Q_REPEAT, user_pl_ops); + + return playlist; +} + +static struct db_entry *user_db_alloc(const gchar *name) +{ + return &__user_db_alloc(g_strdup(name))->pl_dbe; +} + +static void user_db_free(struct db_entry *dbe) +{ + queue_deinit(&USER_PLAYLIST(dbe)->pl_playlist.pl_queue); + g_free(USER_PLAYLIST(dbe)); +} + +static gchar *user_db_key(struct db_entry *dbe) +{ + return g_strdup(USER_PLAYLIST(dbe)->pl_playlist.pl_name); +} + +static struct db_entry *user_db_read(struct file *file) +{ + gchar *name = file_readl(file); + struct user_playlist *playlist = __user_db_alloc(name); + + queue_load_flags(&playlist->pl_playlist.pl_queue, file, true); + queue_load_tracks(&playlist->pl_playlist.pl_queue, file); + queue_iter_set(&playlist->pl_playlist.pl_queue, + &playlist->pl_playlist.pl_queue.q_cur, + playlist->pl_playlist.pl_queue.q_cur.it_pos); + + return &playlist->pl_dbe; +} + +static void user_db_write(struct file *file, struct db_entry *dbe) +{ + struct playlist *playlist = &USER_PLAYLIST(dbe)->pl_playlist; + + file_writef(file, "%s\n", playlist->pl_name); + queue_save_flags(&playlist->pl_queue, file, true); + queue_save_tracks(&playlist->pl_queue, file); +} + + +static const struct db_ops user_db_ops = { + .dbe_alloc = user_db_alloc, + .dbe_free = user_db_free, + .dbe_key = user_db_key, + .dbe_read = user_db_read, + .dbe_write = user_db_write, +}; + + +static struct playlist *__user_pl_lookup(const gchar *name) +{ + struct db_entry *dbe = db_get(&user_db, name); + return dbe ? &USER_PLAYLIST(dbe)->pl_playlist : NULL; +} + +static void pl_user_save(void) +{ + db_save(&user_db); +} + +static struct queue *pl_user_get_queue(const gchar *name) +{ + struct playlist *playlist = __user_pl_lookup(name); + return playlist ? &playlist->pl_queue : NULL; +} + +static unsigned int pl_user_get_id(const gchar *name) +{ + struct db_entry *dbe = db_get(&user_db, name); + return dbe ? dbe->dbe_index : -1; +} + +static gchar *pl_user_get_name(unsigned int id) +{ + struct db_entry *dbe = db_at(&user_db, id); + return dbe ? g_strdup(USER_PLAYLIST(dbe)->pl_playlist.pl_name) : NULL; +} + +static bool pl_user_can_select(const gchar *name) +{ + return db_get(&user_db, name) != NULL; +} + +static bool pl_user_new(const gchar *name) +{ + if (db_get(&user_db, name)) + return false; + db_insert(&user_db, name); + return true; +} + +static bool pl_user_delete(const gchar *name) +{ + struct db_entry *dbe = db_get(&user_db, name); + if (dbe) { + db_remove(&user_db, dbe); + db_defrag(&user_db); + } + return dbe != NULL; +} + +static bool pl_user_add(const gchar *name, struct track *track) +{ + struct playlist *playlist = __user_pl_lookup(name); + bool ret = false; + + if (playlist) { + ret = playlist_generic_add_track(playlist, track); + if (ret) + pl_user_save(); + } + + return ret; +} + +static bool pl_user_remove(const gchar *name, struct track *track) +{ + struct playlist *playlist = __user_pl_lookup(name); + bool ret = false; + + if (playlist) { + ret = playlist_generic_remove_track(playlist, track); + if (ret) + pl_user_save(); + } + + return ret; +} + +static void pl_user_update(const gchar *name) +{ +} + +static void pl_user_set_flag(const gchar *name, enum queue_flags flag, + bool enabled) +{ + struct playlist *playlist = __user_pl_lookup(name); + playlist_generic_set_flag(playlist, flag, enabled); + pl_user_save(); +} + +static void pl_user_sort(const gchar *name, enum compare_t sort, bool reset) +{ + struct playlist *playlist = __user_pl_lookup(name); + playlist_generic_sort(playlist, sort, reset); + pl_user_save(); +} + +static struct track *pl_user_next(const gchar *name) +{ + struct playlist *playlist = __user_pl_lookup(name); + struct track *track = playlist_generic_next(playlist); + pl_user_save(); + return track; +} + + +struct playlist_type pl_user = { + .pl_save = pl_user_save, + .pl_get_queue = pl_user_get_queue, + .pl_get_id = pl_user_get_id, + .pl_get_name = pl_user_get_name, + .pl_can_select = pl_user_can_select, + .pl_new = pl_user_new, + .pl_delete = pl_user_delete, + .pl_add_track = pl_user_add, + .pl_remove_track = pl_user_remove, + .pl_update = pl_user_update, + .pl_set_flag = pl_user_set_flag, + .pl_sort = pl_user_sort, + .pl_next = pl_user_next, +}; + + +void pl_user_init(struct queue_ops *ops) +{ + user_pl_ops = ops; + db_init(&user_db, "playlist.user", true, &user_db_ops, 0, 0); + db_load_idle(&user_db); +} + +void pl_user_deinit() +{ + db_deinit(&user_db); +} + +struct database *pl_user_db_get() +{ + return &user_db; +} diff --git a/include/core/playlist.h b/include/core/playlist.h index bf82b64f..5aa37204 100644 --- a/include/core/playlist.h +++ b/include/core/playlist.h @@ -11,6 +11,7 @@ #include #include #include +#include #include diff --git a/include/core/playlists/type.h b/include/core/playlists/type.h index ff361e68..e1efb1a2 100644 --- a/include/core/playlists/type.h +++ b/include/core/playlists/type.h @@ -13,6 +13,7 @@ enum playlist_type_t { PL_SYSTEM, PL_ARTIST, PL_LIBRARY, + PL_USER, PL_MAX_TYPE, }; diff --git a/include/core/playlists/user.h b/include/core/playlists/user.h new file mode 100644 index 00000000..e07b7906 --- /dev/null +++ b/include/core/playlists/user.h @@ -0,0 +1,28 @@ +/* + * Copyright 2016 (c) Anna Schumaker. + */ +#ifndef OCARINA_CORE_PLAYLISTS_USER_H +#define OCARINA_CORE_PLAYLISTS_USER_H +#include + + +struct user_playlist { + struct playlist pl_playlist; + struct db_entry pl_dbe; +}; + +#define USER_PLAYLIST(dbe) ((struct user_playlist *)DBE_DATA(dbe)) + + +/* User playlist type. */ +extern struct playlist_type pl_user; + + +/* Called to initialize user playlists. */ +void pl_user_init(struct queue_ops *ops); + +/* Called to deinitialize user playlists. */ +void pl_user_deinit(); + +struct database *pl_user_db_get(); +#endif /* OCARINA_CORE_PLAYLISTS_USER_H */ diff --git a/tests/core/playlists/.gitignore b/tests/core/playlists/.gitignore index 14edbbf9..fb98c011 100644 --- a/tests/core/playlists/.gitignore +++ b/tests/core/playlists/.gitignore @@ -1,3 +1,4 @@ system artist +user library diff --git a/tests/core/playlists/CMakeLists.txt b/tests/core/playlists/CMakeLists.txt index 8dd66eb0..ba2e5253 100644 --- a/tests/core/playlists/CMakeLists.txt +++ b/tests/core/playlists/CMakeLists.txt @@ -6,4 +6,5 @@ endfunction() playlist_unit_test(System) playlist_unit_test(Artist) +playlist_unit_test(User) playlist_unit_test(Library) diff --git a/tests/core/playlists/user.c b/tests/core/playlists/user.c new file mode 100644 index 00000000..d0b08b4b --- /dev/null +++ b/tests/core/playlists/user.c @@ -0,0 +1,86 @@ +/* + * Copyright 2016 (c) Anna Schumaker. + */ +#include +#include +#include +#include +#include +#include +#include + +void test_user() +{ + struct database *db = pl_user_db_get(); + + g_assert_cmpuint(db->db_size, ==, 0); + g_assert_true( playlist_new(PL_USER, "Test Playlist")); + g_assert_false(playlist_new(PL_USER, "Test Playlist")); + g_assert_cmpuint(db->db_size, ==, 1); + + g_assert_false(playlist_get_random(PL_USER, "Test Playlist")); + playlist_set_random(PL_USER, "Test Playlist", true); + g_assert_true(playlist_get_random(PL_USER, "Test Playlist")); + playlist_set_random(PL_USER, "Test Playlist", false); + g_assert_false(playlist_get_random(PL_USER, "Test Playlist")); + + g_assert_cmpuint(playlist_get_id(PL_USER, "Test Playlist"), ==, 0); + g_assert_cmpuint(playlist_get_id(PL_USER, "No Playlist"), ==, + (unsigned int)-1); + + g_assert_cmpstr_free(playlist_get_name(PL_USER, 0), ==, "Test Playlist"); + g_assert_null(playlist_get_name(PL_USER, 1)); + + g_assert_true( playlist_select(PL_USER, "Test Playlist")); + g_assert_false(playlist_select(PL_USER, "No Playlist")); + + g_assert_cmpuint(playlist_size(PL_USER, "Test Playlist"), ==, 0); + g_assert_false(playlist_has( PL_USER, "Test Playlist", track_get(0))); + g_assert_true( playlist_add( PL_USER, "Test Playlist", track_get(0))); + g_assert_false(playlist_add( PL_USER, "Test Playlist", track_get(0))); + g_assert_true( playlist_has( PL_USER, "Test Playlist", track_get(0))); + g_assert_cmpuint(playlist_size(PL_USER, "Test Playlist"), ==, 1); + + playlist_update(PL_USER, "Test Playlist"); + pl_user_deinit(); + g_assert_cmpuint(db->db_size, ==, 0); + pl_user_init(NULL); + while (idle_run_task()) {}; + g_assert_cmpuint(db->db_size, ==, 1); + + g_assert_cmpuint(playlist_size(PL_USER, "Test Playlist"), ==, 1); + g_assert_true( playlist_has( PL_USER, "Test Playlist", track_get(0))); + g_assert_true( playlist_remove(PL_USER, "Test Playlist", track_get(0))); + g_assert_false(playlist_remove(PL_USER, "Test Playlist", track_get(0))); + g_assert_false(playlist_has( PL_USER, "Test Playlist", track_get(0))); + g_assert_cmpuint(playlist_size(PL_USER, "Test Playlist"), ==, 0); + + g_assert_true( playlist_delete(PL_USER, "Test Playlist")); + g_assert_false(playlist_delete(PL_USER, "Test Playlist")); + g_assert_cmpuint(db->db_size, ==, 0); + g_assert_cmpuint(db_actual_size(db), ==, 0); +} + +int main(int argc, char **argv) +{ + int ret; + + idle_init_sync(); + settings_init(); + tags_init(); + playlist_init(NULL); + while (idle_run_task()) {}; + + playlist_new(PL_LIBRARY, "tests/Music/Hyrule Symphony"); + while (idle_run_task()) {}; + + g_test_init(&argc, &argv, NULL); + g_test_add_func("/Core/Playlists/User", test_user); + ret = g_test_run(); + + playlist_deinit(); + tags_deinit(); + settings_deinit(); + idle_deinit(); + return ret; +}