From 115a007ed8e27b0e62ff5fb476dc2d421aeed46f Mon Sep 17 00:00:00 2001 From: Robin Ekman Date: Mon, 9 Oct 2023 01:29:12 +0200 Subject: [PATCH] feat: commands for next/previous album that respect shuffle settings --- Tests/AlbumNavigationTests.cpp | 678 +++++++++++++++++++++++++++++ Tests/TestData/AlbumNavigation.csv | 21 + include/deadbeef/deadbeef.h | 3 + plugins/hotkeys/hotkeys.c | 44 +- src/main.c | 9 + src/streamer.c | 279 ++++++++++++ src/streamer.h | 20 + 7 files changed, 1053 insertions(+), 1 deletion(-) create mode 100644 Tests/AlbumNavigationTests.cpp create mode 100644 Tests/TestData/AlbumNavigation.csv diff --git a/Tests/AlbumNavigationTests.cpp b/Tests/AlbumNavigationTests.cpp new file mode 100644 index 0000000000..e6fff0c8b8 --- /dev/null +++ b/Tests/AlbumNavigationTests.cpp @@ -0,0 +1,678 @@ +// +// AlbumNavigationTests.m +// Tests +// +// Created by Robin Ekman on 05/10/23. +// Copyright © 2023 Robin Ekman. All rights reserved. +// + +#include "conf.h" +#include "logger.h" +#include +#include +#include "conf.h" +#include "fakeout.h" +#include "messagepump.h" +#include "playlist.h" +#include "playmodes.h" +#include "plmeta.h" +#include "plugins.h" +#include "streamer.h" +#include "threading.h" +#include +#include + +extern "C" DB_plugin_t * fakeout_load (DB_functions_t *api); +// super oversimplified mainloop +static void +mainloop (void *ctx) { + for (;;) { + uint32_t msg; + uintptr_t ctx; + uint32_t p1; + uint32_t p2; + int term = 0; + while (messagepump_pop(&msg, &ctx, &p1, &p2) != -1) { + if (!term) { + DB_output_t *output = plug_get_output (); + switch (msg) { + case DB_EV_TERMINATE: + term = 1; + break; + case DB_EV_PLAY_CURRENT: + streamer_play_current_track (); + break; + case DB_EV_PLAY_NUM: + streamer_set_nextsong (p1, 0); + break; + case DB_EV_STOP: + streamer_set_nextsong (-1, 0); + break; + case DB_EV_NEXT: + streamer_move_to_nextsong (1); + break; + case DB_EV_PREV: + streamer_move_to_prevsong (1); + break; + case DB_EV_PAUSE: + if (output->state () != DDB_PLAYBACK_STATE_PAUSED) { + output->pause (); + messagepump_push (DB_EV_PAUSED, 0, 1, 0); + } + break; + case DB_EV_TOGGLE_PAUSE: + if (output->state () != DDB_PLAYBACK_STATE_PLAYING) { + streamer_play_current_track (); + } + else { + output->pause (); + messagepump_push (DB_EV_PAUSED, 0, 1, 0); + } + break; + case DB_EV_PLAY_RANDOM: + streamer_move_to_randomsong (1); + break; + case DB_EV_SEEK: + { + int32_t pos = (int32_t)p1; + if (pos < 0) { + pos = 0; + } + streamer_set_seek (p1 / 1000.f); + } + break; + case DB_EV_SONGSTARTED: + break; + case DB_EV_TRACKINFOCHANGED: + break; + } + } + if (msg >= DB_EV_FIRST && ctx) { + messagepump_event_free ((ddb_event_t *)ctx); + } + } + if (term) { + return; + } + messagepump_wait (); + } +} + +unsigned int read_field(const char *line, char *buf) { + // read a tab-separated field from line into buf + // return the number of characters read including the tab + const char *q = strchr(line, '\t'); + if (!q) { + // this was the end of the line + unsigned int n = strlen(line); + // we don't want the terminating newline + strncpy(buf, line, n - 1); + buf[n-1] = '\0'; + return n; + } else { + memcpy(buf, line, q - line); + buf[q-line] = '\0'; + return q-line + 1; + } +} + +void read_metadata(FILE *f, playItem_t *it) { + // read three tab-separated pieces of metadata from f into it + int p = 0; + char linebuf[1024]; + char metabuf[1024]; + + fgets(linebuf, sizeof(linebuf), f); + p += read_field(linebuf, metabuf); + pl_add_meta(it, "artist", metabuf); + + p += read_field(linebuf + p, metabuf); + pl_add_meta(it, "album", metabuf); + + p += read_field(linebuf + p, metabuf); + pl_add_meta(it, "title", metabuf); +} + +void advance_to(playItem_t* it) { + pl_set_played(it, 1); + streamer_set_last_played(it); + pl_item_unref(it); +} + + + +class AlbumNavigationTests: public ::testing::Test { +protected: + void SetUp() override { + ddb_logger_init (); + conf_init (); + conf_enable_saving (0); + + messagepump_init(); + + plug_init_plugin (fakeout_load, NULL); + _fakeout = (DB_output_t *)fakeout_load (plug_get_api ()); + plug_register_out ((DB_plugin_t *)_fakeout); + + plug_set_output (_fakeout); + + streamer_init(); + streamer_set_repeat (DDB_REPEAT_OFF); + streamer_set_shuffle (DDB_SHUFFLE_OFF); + plt = plt_alloc ("testplt"); + + // read mock playlist from tab-separated file + FILE *f = fopen("Tests/TestData/AlbumNavigation.csv", "r"); + int k = 0; + playItem_t *prev = NULL; + playItem_t *it; + // the first field is the shufflerating of the track + // this lets us know which track/album should be next in the shuffle + // so we can write deterministic tests + while (fscanf(f, "%d\t", srs + k) != EOF) { + it = pl_item_alloc(); + it->shufflerating = srs[k++]; + read_metadata(f, it); + + plt_insert_item(plt, prev, it); + prev = it; + pl_item_unref(it); + } + n_tracks = k; + + plt_set_curr (plt); + streamer_set_streamer_playlist (plt); + + _mainloop_tid = thread_start (mainloop, NULL); + + } + + void reset_shuffle_ratings() { + int n = 0; + for(playItem_t *it = plt->head[PL_MAIN]; it && n < n_tracks; it = it->next[PL_MAIN] ) { + pl_set_played(it, 0); + it->shufflerating = srs[n++]; + } + } + + playItem_t* skip_tracks(unsigned int n, ddb_shuffle_t shuffle, ddb_repeat_t repeat, unsigned int ret) { + // skip n tracks forward + // if ret is truthy, return the new current track; otherwise return NULL + playItem_t *it = NULL; + for (int i = 0; i < n+1; i++) { + if (it) { + pl_item_unref (it); + } + it = streamer_get_next_track_with_direction (1, shuffle, repeat); + pl_set_played(it, 1); + streamer_set_last_played(it); + } + if (ret) { + return it; + } else { + pl_item_unref (it); + return NULL; + } + } + + void TearDown() override { + plt_set_curr(NULL); + _fakeout->stop (); + streamer_free(); + deadbeef->sendmessage (DB_EV_TERMINATE, 0, 0, 0); + thread_join (_mainloop_tid); + messagepump_free(); + conf_free(); + ddb_logger_free(); + } + playlist_t * plt; + int srs[128]; + int n_tracks; + DB_output_t *_fakeout; + uintptr_t _mainloop_tid; +}; + + +// from_same_album tests + +TEST_F(AlbumNavigationTests, test_TracksFromSameAlbum) { + playlist_t *plt = plt_get_curr(); + + playItem_t *head = plt->head[PL_MAIN]; + playItem_t *next = head->next[PL_MAIN]; + EXPECT_TRUE(pl_items_from_same_album(head, next)); + + plt_unref (plt); +} + +TEST_F(AlbumNavigationTests, test_TracksNotFromSameAlbum) { + playlist_t *plt = plt_get_curr(); + + playItem_t *head = plt->head[PL_MAIN]; + playItem_t *tail = plt->tail[PL_MAIN]; + EXPECT_FALSE(pl_items_from_same_album(head, tail)); + + plt_unref (plt); +} + +// SHUFFLE_ALBUMS tests + +TEST_F(AlbumNavigationTests, test_ShuffleAlbumsNextTrackStaysOnSameAlbum) { + playlist_t *plt = plt_get_curr(); + reset_shuffle_ratings(); + + playItem_t *it = skip_tracks(1, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF, 1); + ASSERT_TRUE(it != NULL); + printf("%s\n", pl_find_meta_raw(it, "title")); + + EXPECT_TRUE(pl_items_from_same_album (plt->head[PL_MAIN], it)); + + plt_unref (plt); + pl_item_unref (it); +} + +TEST_F(AlbumNavigationTests, test_ShuffleAlbumsPrevTrackStaysOnSameAlbum) { + playlist_t *plt = plt_get_curr(); + reset_shuffle_ratings(); + + playItem_t *it = skip_tracks(7, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF, 1); + playItem_t *prev = streamer_get_next_track_with_direction (-1, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF); + + EXPECT_TRUE(pl_items_from_same_album(it, prev)); + + plt_unref (plt); + if (it) { + pl_item_unref (it); + } + if (prev) { + pl_item_unref (prev); + } +} + +TEST_F(AlbumNavigationTests, test_ShuffleAlbumsPrevTrackGoesToLastOnAlbum) { + playlist_t *plt = plt_get_curr(); + reset_shuffle_ratings(); + + playItem_t *it = skip_tracks(8, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF, 1); + ASSERT_TRUE(it != NULL); + playItem_t *prev = streamer_get_next_track_with_direction (-1, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF); + ASSERT_TRUE(prev != NULL); + const char *title = pl_find_meta_raw(prev, "title"); + + EXPECT_FALSE(pl_items_from_same_album(it, prev)); + EXPECT_TRUE(pl_items_from_same_album(prev, plt->head[PL_MAIN])); + EXPECT_STREQ(title, "Damage, Inc."); + + plt_unref (plt); + pl_item_unref (it); + pl_item_unref (prev); +} + +TEST_F(AlbumNavigationTests, test_ShuffleAlbumNextAlbumGoesToFirstTrack) { + playlist_t *plt = plt_get_curr(); + reset_shuffle_ratings(); + + skip_tracks(0, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF, 0); + + playItem_t *it = streamer_get_next_album_with_direction(1, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF); + ASSERT_TRUE(it != NULL); + const char *title = pl_find_meta_raw(it, "title"); + + EXPECT_STREQ(title, "Red"); + + plt_unref (plt); + pl_item_unref (it); +} + +TEST_F(AlbumNavigationTests, test_ShuffleAlbumsPrevAlbumGoesToFirstTrackPrevAlbum) { + playlist_t *plt = plt_get_curr(); + reset_shuffle_ratings(); + + skip_tracks(0, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF, 0); + + playItem_t *it = streamer_get_next_album_with_direction(1, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF); + ASSERT_TRUE(it != NULL); + const char *title = pl_find_meta_raw(it, "title"); + EXPECT_STREQ(title, "Red"); + streamer_set_last_played(it); + + playItem_t *prev = streamer_get_next_album_with_direction(-1, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF); + ASSERT_TRUE(prev != NULL); + + title = pl_find_meta_raw(prev, "title"); + EXPECT_STREQ(title, "Battery"); + + plt_unref (plt); + pl_item_unref (it); + pl_item_unref (prev); +} + +TEST_F(AlbumNavigationTests, test_ShuffleAlbumsPrevAlbumGoesToFirstTrackCurrentAlbum) { + playlist_t *plt = plt_get_curr(); + reset_shuffle_ratings(); + + skip_tracks(5, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF, 0); + + playItem_t *prev = streamer_get_next_album_with_direction(-1, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF); + ASSERT_TRUE(prev != NULL); + const char *title = pl_find_meta_raw(prev, "title"); + + EXPECT_STREQ(title, "Battery"); + + plt_unref (plt); + if (prev) { + pl_item_unref (prev); + } +} + +TEST_F(AlbumNavigationTests, test_ShuffleAlbumsNextAlbumMarksPlayed) { + playlist_t *plt = plt_get_curr(); + reset_shuffle_ratings(); + + skip_tracks(0, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF, 0); + + playItem_t *it = streamer_get_next_album_with_direction(1, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF); + ASSERT_TRUE(it != NULL); + pl_item_unref(it); + + it = plt->head[PL_MAIN]; + while(it && pl_items_from_same_album(it, plt->head[PL_MAIN])) { + EXPECT_TRUE(pl_get_played(it)); + it = it->next[PL_MAIN]; + } + + plt_unref (plt); +} + +TEST_F(AlbumNavigationTests, test_ShuffleAlbumsPrevAlbumMarksNotPlayed) { + playlist_t *plt = plt_get_curr(); + reset_shuffle_ratings(); + + skip_tracks(0, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF, 0); + + playItem_t *it = streamer_get_next_album_with_direction(1, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF); + ASSERT_TRUE(it != NULL); + advance_to(it); + + it = streamer_get_next_album_with_direction(-1, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF); + ASSERT_TRUE(it != NULL); + pl_item_unref(it); + + it = plt->head[PL_MAIN]; + while(it && pl_items_from_same_album(it, plt->head[PL_MAIN])) { + EXPECT_FALSE(pl_get_played(it)); + it = it->next[PL_MAIN]; + } + + plt_unref (plt); +} + +TEST_F(AlbumNavigationTests, test_ShuffleAlbumsPrevTrackGoesToLastTrackPrevAlbum) { + playlist_t *plt = plt_get_curr(); + reset_shuffle_ratings(); + + skip_tracks(0, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF, 0); + + playItem_t *it = streamer_get_next_album_with_direction(1, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF); + ASSERT_TRUE(it != NULL); + const char *title = pl_find_meta_raw(it, "title"); + EXPECT_STREQ(title, "Red"); + streamer_set_last_played(it); + + playItem_t *prev = streamer_get_next_track_with_direction(-1, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF); + ASSERT_TRUE(prev != NULL); + + title = pl_find_meta_raw(prev, "title"); + EXPECT_STREQ(title, "Damage, Inc."); + + plt_unref (plt); + pl_item_unref(it); + pl_item_unref(prev); +} + +TEST_F(AlbumNavigationTests, test_ShuffleAlbumsNextAlbumPrevAlbumNextAlbum) { + playlist_t *plt = plt_get_curr(); + reset_shuffle_ratings(); + + skip_tracks(0, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF, 0); + + playItem_t *it = streamer_get_next_album_with_direction(1, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF); + ASSERT_TRUE(it != NULL); + const char *title = pl_find_meta_raw(it, "title"); + EXPECT_STREQ(title, "Red"); + + advance_to(it); + + it = streamer_get_next_album_with_direction(-1, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF); + ASSERT_TRUE(it != NULL); + title = pl_find_meta_raw(it, "title"); + EXPECT_STREQ(title, "Battery"); + + advance_to(it); + + it = streamer_get_next_album_with_direction(1, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF); + ASSERT_TRUE(it != NULL); + title = pl_find_meta_raw(it, "title"); + EXPECT_STREQ(title, "Red"); + + plt_unref (plt); + pl_item_unref (it); +} + +TEST_F(AlbumNavigationTests, test_ShuffleAlbumsRepeatOffNextAlbumStopsAfterAllAlbums) { + playlist_t *plt = plt_get_curr(); + reset_shuffle_ratings(); + skip_tracks(0, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF, 0); + + playItem_t *it; + for (int i = 0; i < 3; i++) { + it = streamer_get_next_album_with_direction(1, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_OFF); + if (it) { + advance_to(it); + } + } + ASSERT_TRUE(it == NULL); + + plt_unref (plt); +} + +TEST_F(AlbumNavigationTests, test_ShuffleAlbumsRepeatAllNextAlbumContinuesAfterAllAlbums) { + playlist_t *plt = plt_get_curr(); + reset_shuffle_ratings(); + skip_tracks(0, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_ALL, 0); + + playItem_t *it; + for (int i = 0; i < 3; i++) { + it = streamer_get_next_album_with_direction(1, DDB_SHUFFLE_ALBUMS, DDB_REPEAT_ALL); + if (it) { + advance_to(it); + } + } + ASSERT_TRUE(it != NULL); + + plt_unref (plt); + pl_item_unref(it); +} + +// SHUFFLE_OFF tests + +TEST_F(AlbumNavigationTests, test_ShuffleOffNextAlbumGoesToFirstTrack) { + playlist_t *plt = plt_get_curr(); + skip_tracks(0, DDB_SHUFFLE_OFF, DDB_REPEAT_OFF, 0); + + playItem_t *it = streamer_get_next_album_with_direction(1, DDB_SHUFFLE_OFF, DDB_REPEAT_OFF); + ASSERT_TRUE(it != NULL); + const char *title = pl_find_meta_raw(it, "title"); + EXPECT_STREQ(title, "Aces High"); + + plt_unref (plt); + pl_item_unref (it); +} + +TEST_F(AlbumNavigationTests, test_ShuffleOffNextAlbumGoesToFirstTrackNoCurrentPlaying) { + playlist_t *plt = plt_get_curr(); + + playItem_t *it = streamer_get_next_album_with_direction(1, DDB_SHUFFLE_OFF, DDB_REPEAT_OFF); + ASSERT_TRUE(it != NULL); + const char *title = pl_find_meta_raw(it, "title"); + EXPECT_STREQ(title, "Battery"); + + plt_unref (plt); + pl_item_unref (it); +} + +TEST_F(AlbumNavigationTests, test_ShuffleOffNextAlbumMarksPlayed) { + playlist_t *plt = plt_get_curr(); + + skip_tracks(0, DDB_SHUFFLE_OFF, DDB_REPEAT_OFF, 0); + + playItem_t *it = streamer_get_next_album_with_direction(1, DDB_SHUFFLE_OFF, DDB_REPEAT_OFF); + playItem_t *prev = it->prev[PL_MAIN]; + pl_item_unref(it); + while(prev != NULL) { + ASSERT_TRUE(pl_get_played(prev)); + prev = prev->prev[PL_MAIN]; + } + + plt_unref (plt); +} + +TEST_F(AlbumNavigationTests, test_ShuffleOffPrevTrackGoesToLastTrackPrevAlbum) { + playlist_t *plt = plt_get_curr(); + + skip_tracks(0, DDB_SHUFFLE_OFF, DDB_REPEAT_OFF, 0); + + playItem_t *it = streamer_get_next_album_with_direction(1, DDB_SHUFFLE_OFF, DDB_REPEAT_OFF); + ASSERT_TRUE(it != NULL); + const char *title = pl_find_meta_raw(it, "title"); + EXPECT_STREQ(title, "Aces High"); + streamer_set_last_played(it); + + playItem_t *prev = streamer_get_next_track_with_direction(-1, DDB_SHUFFLE_OFF, DDB_REPEAT_OFF); + ASSERT_TRUE(prev != NULL); + + title = pl_find_meta_raw(prev, "title"); + EXPECT_STREQ(title, "Damage, Inc."); + + plt_unref (plt); + pl_item_unref(it); + pl_item_unref(prev); +} + +TEST_F(AlbumNavigationTests, test_ShuffleOffPrevAlbumGoesToFirstTrackCurrentAlbum) { + playlist_t *plt = plt_get_curr(); + skip_tracks(0, DDB_SHUFFLE_OFF, DDB_REPEAT_OFF, 0); + + playItem_t *it = streamer_get_next_album_with_direction(1, DDB_SHUFFLE_OFF, DDB_REPEAT_OFF); + ASSERT_TRUE(it != NULL); + advance_to(it); + const char *title = pl_find_meta_raw(it, "title"); + EXPECT_STREQ(title, "Aces High"); + + it = streamer_get_next_track_with_direction(1, DDB_SHUFFLE_OFF, DDB_REPEAT_OFF); + ASSERT_TRUE(it != NULL); + advance_to(it); + title = pl_find_meta_raw(it, "title"); + EXPECT_STREQ(title, "2 Minutes to Midnight"); + + + it = streamer_get_next_album_with_direction(-1, DDB_SHUFFLE_OFF, DDB_REPEAT_OFF); + ASSERT_TRUE(it != NULL); + + title = pl_find_meta_raw(it, "title"); + EXPECT_STREQ(title, "Aces High"); + + plt_unref (plt); + pl_item_unref (it); +} + +TEST_F(AlbumNavigationTests, test_ShuffleOffPrevAlbumOnFirstTrackGoesToFirstTrackPrevAlbum) { + playlist_t *plt = plt_get_curr(); + skip_tracks(0, DDB_SHUFFLE_OFF, DDB_REPEAT_OFF, 0); + + playItem_t *it = streamer_get_next_album_with_direction(1, DDB_SHUFFLE_OFF, DDB_REPEAT_OFF); + ASSERT_TRUE(it != NULL); + advance_to (it); + const char *title = pl_find_meta_raw(it, "title"); + EXPECT_STREQ(title, "Aces High"); + + it = streamer_get_next_album_with_direction(-1, DDB_SHUFFLE_OFF, DDB_REPEAT_OFF); + ASSERT_TRUE(it != NULL); + title = pl_find_meta_raw(it, "title"); + EXPECT_STREQ(title, "Battery"); + + plt_unref (plt); + pl_item_unref (it); +} + +TEST_F(AlbumNavigationTests, test_ShuffleOffPrevAlbumWrapsAround) { + playlist_t *plt = plt_get_curr(); + skip_tracks(0, DDB_SHUFFLE_OFF, DDB_REPEAT_OFF, 0); + + playItem_t *it = streamer_get_next_album_with_direction(-1, DDB_SHUFFLE_OFF, DDB_REPEAT_OFF); + ASSERT_TRUE(it != NULL); + const char *title = pl_find_meta_raw(it, "title"); + EXPECT_STREQ(title, "Red"); + + plt_unref (plt); + pl_item_unref (it); +} + +TEST_F(AlbumNavigationTests, test_ShuffleOffNextAlbumPrevAlbumNextAlbum) { + playlist_t *plt = plt_get_curr(); + skip_tracks(0, DDB_SHUFFLE_OFF, DDB_REPEAT_OFF, 0); + + playItem_t *it = streamer_get_next_album_with_direction(1, DDB_SHUFFLE_OFF, DDB_REPEAT_OFF); + ASSERT_TRUE(it != NULL); + advance_to(it); + const char *title = pl_find_meta_raw(it, "title"); + EXPECT_STREQ(title, "Aces High"); + + it = streamer_get_next_album_with_direction(-1, DDB_SHUFFLE_OFF, DDB_REPEAT_OFF); + ASSERT_TRUE(it != NULL); + advance_to(it); + title = pl_find_meta_raw(it, "title"); + EXPECT_STREQ(title, "Battery"); + + it = streamer_get_next_album_with_direction(1, DDB_SHUFFLE_OFF, DDB_REPEAT_OFF); + ASSERT_TRUE(it != NULL); + title = pl_find_meta_raw(it, "title"); + EXPECT_STREQ(title, "Aces High"); + + plt_unref (plt); + pl_item_unref (it); +} + +TEST_F(AlbumNavigationTests, test_ShuffleOffRepeatOffNextAlbumStopsAtEndOfPlaylist) { + playlist_t *plt = plt_get_curr(); + skip_tracks(0, DDB_SHUFFLE_OFF, DDB_REPEAT_OFF, 0); + + playItem_t *it; + for (int i = 0; i < 3; i++) { + it = streamer_get_next_album_with_direction(1, DDB_SHUFFLE_OFF, DDB_REPEAT_OFF); + if (it) { + advance_to(it); + } + } + ASSERT_TRUE(it == NULL); + + plt_unref (plt); +} + +TEST_F(AlbumNavigationTests, test_ShuffleOffRepeatAllNextAlbumWrapsAround) { + playlist_t *plt = plt_get_curr(); + skip_tracks(0, DDB_SHUFFLE_OFF, DDB_REPEAT_ALL, 0); + + playItem_t *it; + for (int i = 0; i < 3; i++) { + it = streamer_get_next_album_with_direction(1, DDB_SHUFFLE_OFF, DDB_REPEAT_ALL); + if (it) { + advance_to(it); + } + } + ASSERT_TRUE(it != NULL); + EXPECT_STREQ(pl_find_meta_raw(it, "title"), "Battery"); + + plt_unref (plt); + pl_item_unref(it); +} diff --git a/Tests/TestData/AlbumNavigation.csv b/Tests/TestData/AlbumNavigation.csv new file mode 100644 index 0000000000..7606bc5b48 --- /dev/null +++ b/Tests/TestData/AlbumNavigation.csv @@ -0,0 +1,21 @@ +0 Metallica Master of Puppets Battery +0 Metallica Master of Puppets Master of Puppets +0 Metallica Master of Puppets The Thing That Should Not Be +0 Metallica Master of Puppets Welcome Home (Sanitarium) +0 Metallica Master of Puppets Disposable Heroes +0 Metallica Master of Puppets Leper Messiah +0 Metallica Master of Puppets Orion +0 Metallica Master of Puppets Damage, Inc. +2 Iron Maiden Powerslave Aces High +2 Iron Maiden Powerslave 2 Minutes to Midnight +2 Iron Maiden Powerslave Losfer Words (Big 'Orra) +2 Iron Maiden Powerslave Flash of the Blade +2 Iron Maiden Powerslave The Duellists +2 Iron Maiden Powerslave Back in the Village +2 Iron Maiden Powerslave Powerslave +2 Iron Maiden Powerslave Rime of the Ancient Mariner +1 King Crimson Red Red +1 King Crimson Red Fallen Angel +1 King Crimson Red One More Red Nightmare +1 King Crimson Red Providence +1 King Crimson Red Starless diff --git a/include/deadbeef/deadbeef.h b/include/deadbeef/deadbeef.h index 69e59458f1..7113e6f49c 100644 --- a/include/deadbeef/deadbeef.h +++ b/include/deadbeef/deadbeef.h @@ -527,6 +527,9 @@ enum { DB_EV_PLAYBACK_STATE_DID_CHANGE = 25, #endif + DB_EV_PLAY_NEXT_ALBUM = 26, // switch to next album + DB_EV_PLAY_PREV_ALBUM = 27, // switch to prev album + DB_EV_PLAY_RANDOM_ALBUM = 28, // play random album // ----------------- // structured events diff --git a/plugins/hotkeys/hotkeys.c b/plugins/hotkeys/hotkeys.c index 699a2b99b5..300c1d3302 100644 --- a/plugins/hotkeys/hotkeys.c +++ b/plugins/hotkeys/hotkeys.c @@ -675,6 +675,24 @@ action_play_random_cb (struct DB_plugin_action_s *action, ddb_action_context_t c return 0; } +int +action_play_random_album_cb (struct DB_plugin_action_s *action, ddb_action_context_t ctx) { + deadbeef->sendmessage (DB_EV_PLAY_RANDOM_ALBUM, 0, 0, 0); + return 0; +} + +int +action_play_next_album_cb (struct DB_plugin_action_s *action, ddb_action_context_t ctx) { + deadbeef->sendmessage (DB_EV_PLAY_NEXT_ALBUM, 0, 0, 0); + return 0; +} + +int +action_play_prev_album_cb (struct DB_plugin_action_s *action, ddb_action_context_t ctx) { + deadbeef->sendmessage (DB_EV_PLAY_PREV_ALBUM, 0, 0, 0); + return 0; +} + int action_seek_5p_forward_cb (struct DB_plugin_action_s *action, ddb_action_context_t ctx) { DB_playItem_t *it = deadbeef->streamer_get_playing_track_safe (); @@ -1189,12 +1207,36 @@ static DB_plugin_action_t action_play_pause = { .next = &action_toggle_pause }; +static DB_plugin_action_t action_play_next_album = { + .title = "Playback/Play Next Album", + .name = "playback_next_album", + .flags = DB_ACTION_COMMON, + .callback2 = action_play_next_album_cb, + .next = &action_play_pause +}; + +static DB_plugin_action_t action_play_prev_album = { + .title = "Playback/Play Previous Album", + .name = "playback_prev_album", + .flags = DB_ACTION_COMMON, + .callback2 = action_play_prev_album_cb, + .next = &action_play_next_album +}; + +static DB_plugin_action_t action_play_random_album = { + .title = "Playback/Play Random Album", + .name = "playback_random_album", + .flags = DB_ACTION_COMMON, + .callback2 = action_play_random_album_cb, + .next = &action_play_prev_album +}; + static DB_plugin_action_t action_play_random = { .title = "Playback/Play Random", .name = "playback_random", .flags = DB_ACTION_COMMON, .callback2 = action_play_random_cb, - .next = &action_play_pause + .next = &action_play_random_album }; static DB_plugin_action_t action_seek_1s_forward = { diff --git a/src/main.c b/src/main.c index 62848f99b8..85118d1db2 100644 --- a/src/main.c +++ b/src/main.c @@ -871,6 +871,12 @@ player_mainloop (void) { case DB_EV_PREV: streamer_move_to_prevsong (1); break; + case DB_EV_PLAY_NEXT_ALBUM: + streamer_move_to_nextalbum (1); + break; + case DB_EV_PLAY_PREV_ALBUM: + streamer_move_to_prevalbum (1); + break; case DB_EV_PAUSE: if (output->state () != DDB_PLAYBACK_STATE_PAUSED) { output->pause (); @@ -889,6 +895,9 @@ player_mainloop (void) { case DB_EV_PLAY_RANDOM: streamer_move_to_randomsong (1); break; + case DB_EV_PLAY_RANDOM_ALBUM: + streamer_move_to_randomalbum (1); + break; case DB_EV_CONFIGCHANGED: conf_save (); streamer_configchanged (); diff --git a/src/streamer.c b/src/streamer.c index ad8efed6e2..76a0be78e3 100644 --- a/src/streamer.c +++ b/src/streamer.c @@ -191,6 +191,10 @@ play_current (void); static void play_next (int dir, ddb_shuffle_t shuffle, ddb_repeat_t repeat); +static void +play_next_album (int dir, ddb_shuffle_t shuffle, ddb_repeat_t repeat); + + static void streamer_set_current_playlist_real (int plt); @@ -532,6 +536,47 @@ get_random_track (void) { return plt_get_item_for_idx (plt, r, PL_MAIN); } +static playItem_t * +get_random_album (void) { + if (!streamer_playlist) { + playlist_t *plt = plt_get_curr (); + streamer_set_streamer_playlist (plt); + plt_unref (plt); + } + playlist_t *plt = streamer_playlist; + int cnt = plt->count[PL_MAIN]; + if (!cnt) { + return NULL; + } + playItem_t *prev = NULL; + + int album_cnt = 0; + int album_buf_size = 1024; + playItem_t** album_buf = malloc(sizeof(playItem_t*) * album_buf_size); + playItem_t** album_buf2 = NULL; + + for (playItem_t *it = plt->head[PL_MAIN]; it; it = it->next[PL_MAIN]) { + if (!prev || !pl_items_from_same_album(prev, it) ) { + album_buf[album_cnt] = it; + album_cnt++; + if (album_cnt == album_buf_size ) { + album_buf2 = malloc( sizeof(playItem_t*) * album_buf_size * 2); + memcpy(album_buf2, album_buf, album_buf_size * sizeof(playItem_t*) ); + album_buf_size *= 2; + free(album_buf); + album_buf = album_buf2; + } + + } + prev = it; + } + int r = (int)(rand () / (double)RAND_MAX * album_cnt); + playItem_t* ret = album_buf[r]; + free(album_buf); + pl_item_ref( ret ); + return ret; +} + static playItem_t * _streamer_find_minimal_notplayed_imp (playlist_t *plt, unsigned int check_floor, int floor) { // if check_floor is truthy, such that shufflerating > floor @@ -866,6 +911,34 @@ streamer_set_prev_track_to_play(playItem_t *prev) { } } +int +streamer_move_to_nextalbum (int r) { + if (r) { + streamer_abort_files (); + } + handler_push (handler, STR_EV_NEXT_ALBUM, 0, r, 0); + return 0; +} + +int +streamer_move_to_prevalbum (int r) { + if (r) { + streamer_abort_files (); + } + handler_push (handler, STR_EV_PREV_ALBUM, 0, r, 0); + return 0; +} + + +int +streamer_move_to_randomalbum (int r) { + if (r) { + streamer_abort_files (); + } + handler_push (handler, STR_EV_RAND_ALBUM, 0, r, 0); + return 0; +} + // playlist must call that whenever item was removed void streamer_song_removed_notify (playItem_t *it) { @@ -1737,6 +1810,15 @@ streamer_thread (void *unused) { case STR_EV_RAND: play_next (0, shuffle, repeat); break; + case STR_EV_RAND_ALBUM: + play_next_album (0, shuffle, repeat); + break; + case STR_EV_NEXT_ALBUM: + play_next_album (1, shuffle, repeat); + break; + case STR_EV_PREV_ALBUM: + play_next_album (-1, shuffle, repeat); + break; case STR_EV_SEEK: streamer_seek_real(*((float *)&p1)); break; @@ -2751,6 +2833,179 @@ streamer_get_next_track_with_direction (int dir, ddb_shuffle_t shuffle, ddb_repe return next; } +void set_album_played(playItem_t *curr, int played) { + for(playItem_t *it = curr; it && pl_items_from_same_album(curr, it); it = it->next[PL_MAIN]) { + pl_set_played(it, played); + } +} + +playItem_t * +get_next_album (playItem_t *curr, ddb_shuffle_t shuffle, ddb_repeat_t repeat) { + // next album is only distinct from next track if shuffle is ALBUMS or OFF + if (shuffle == DDB_SHUFFLE_TRACKS || shuffle == DDB_SHUFFLE_RANDOM) { + return get_next_track(curr, shuffle, repeat); + } + + pl_lock (); + + if (next_track_to_play != NULL && !pl_items_from_same_album(next_track_to_play, curr)) { + pl_item_ref(next_track_to_play); + pl_unlock(); + return next_track_to_play; + } + + if (!streamer_playlist) { + playlist_t *plt = plt_get_curr (); + streamer_set_streamer_playlist (plt); + plt_unref (plt); + } + + while (playqueue_getcount ()) { + trace ("playqueue_getnext\n"); + playItem_t *it = playqueue_getnext (); + if (it && !pl_items_from_same_album(it, curr)) { + pl_unlock (); + return it; // from playqueue + } + } + + playlist_t *plt = streamer_playlist; + if (!plt->head[PL_MAIN]) { + pl_unlock (); + return NULL; // empty playlist + } + + if (plt_get_item_idx (streamer_playlist, curr, PL_MAIN) == -1) { + playlist_t *item_plt = pl_get_playlist(curr); + if (!item_plt) { + curr = NULL; + } else { + plt_unref (item_plt); + } + } + + playItem_t *it = NULL; + if (shuffle == DDB_SHUFFLE_OFF) { + it = curr; + if (curr) { + do { + pl_set_played(it, 1); + it = it->next[PL_MAIN]; + } while (it != NULL && pl_items_from_same_album(curr, it)); + } else { + it = plt->head[PL_MAIN]; + } + if (!it) { + trace ("streamer_move_nextalbum: reached end of playlist\n"); + if (repeat == DDB_REPEAT_ALL) { + it = plt->head[PL_MAIN]; + } + } + } else if (shuffle == DDB_SHUFFLE_ALBUMS) { + // find the first not played playlist item with minimal shufflerating > curr's shufflerating + // since tracks from the same album have the same shufflerating strict inequality guarantees this is a different album + if (!curr) { + it = _streamer_find_minimal_notplayed(plt); + } else { + set_album_played(curr, 1); + it = _streamer_find_minimal_notplayed_with_floor(plt, pl_get_shufflerating(curr)); + } + if (!it) { + // all songs played, reshuffle + if (repeat == DDB_REPEAT_ALL) { + plt_reshuffle (streamer_playlist, &it, NULL); + } + } + } + + if (it) { + pl_item_ref (it); + } + pl_unlock (); + return it; +} + +playItem_t * +get_prev_album (playItem_t *curr, ddb_shuffle_t shuffle, ddb_repeat_t repeat) { + // prev album is only distinct from prev track if shuffle is ALBUMS or OFF + if (shuffle == DDB_SHUFFLE_TRACKS || shuffle == DDB_SHUFFLE_RANDOM) { + return get_prev_track(curr, shuffle, repeat); + } + + pl_lock (); + + playlist_t *plt = streamer_playlist; + if (!plt->head[PL_MAIN]) { + pl_unlock (); + return NULL; // empty playlist + } + + if (plt_get_item_idx (streamer_playlist, curr, PL_MAIN) == -1) { + playlist_t *item_plt = pl_get_playlist(curr); + if (!item_plt) { + curr = NULL; + } else { + plt_unref (item_plt); + } + } + + playItem_t *it = NULL; + if (shuffle == DDB_SHUFFLE_OFF) { + it = curr->prev[PL_MAIN]; + if (!it) { + it = plt->tail[PL_MAIN]; + } + while (it->prev[PL_MAIN] && pl_items_from_same_album(it, it->prev[PL_MAIN])) { + it = it->prev[PL_MAIN]; + } + } else if (shuffle == DDB_SHUFFLE_ALBUMS) { + if (!curr) { + it = _streamer_find_maximal_played(plt); + } else if (curr->prev[PL_MAIN] && pl_items_from_same_album(curr, curr->prev[PL_MAIN])) { + it = curr; + while (it->prev[PL_MAIN] && pl_items_from_same_album(it, it->prev[PL_MAIN])) { + it = it->prev[PL_MAIN]; + } + } else { + it = _streamer_find_maximal_played_with_ceil(plt, pl_get_shufflerating(curr)); + } + } + if (curr) { + set_album_played(curr, 0); + } + if (it) { + set_album_played(it, 0); + pl_item_ref (it); + } + + pl_unlock (); + return it; +} + +playItem_t * +streamer_get_next_album_with_direction (int dir, ddb_shuffle_t shuffle, ddb_repeat_t repeat) { + playItem_t *origin = NULL; + playItem_t *next = NULL; + if (dir == 0) { + return get_random_album(); + } else if (buffering_track) { + origin = buffering_track; + } + else { + origin = last_played; + } + if (!origin) { + return streamer_get_next_track_with_direction(dir, shuffle, repeat); + } + if (dir > 0) { + next = get_next_album (origin, shuffle, repeat); + } else { + next = get_prev_album (origin, shuffle, repeat); + } + + return next; +} + static void play_next (int dir, ddb_shuffle_t shuffle, ddb_repeat_t repeat) { DB_output_t *output = plug_get_output (); @@ -2773,6 +3028,30 @@ play_next (int dir, ddb_shuffle_t shuffle, ddb_repeat_t repeat) { pl_item_unref(next); } +static void +play_next_album(int dir, ddb_shuffle_t shuffle, ddb_repeat_t repeat) { + DB_output_t *output = plug_get_output (); + + playItem_t *next = streamer_get_next_album_with_direction (dir, shuffle, repeat); + + if (!next) { + streamer_set_last_played (NULL); + output->stop (); + streamer_reset(1); + _handle_playback_stopped (); + return; + } + + if (dir == 0) { + // rebuild shuffle order + _rebuild_shuffle_albums_after_manual_trigger (streamer_playlist, next); + } + streamer_lock(); + _play_track(next, 0); + streamer_unlock(); + pl_item_unref(next); +} + void streamer_play_current_track (void) { handler_push (handler, STR_EV_PLAY_CURR, 0, 0, 0); diff --git a/src/streamer.h b/src/streamer.h index 3973e1759f..1e0b17052f 100644 --- a/src/streamer.h +++ b/src/streamer.h @@ -41,6 +41,9 @@ enum { STR_EV_NEXT, // streamer_move_to_nextsong STR_EV_PREV, // streamer_move_to_prevsong STR_EV_RAND, // streamer_move_to_randomsong + STR_EV_NEXT_ALBUM, // streamer_move_to_nextalbum + STR_EV_PREV_ALBUM, // streamer_move_to_prevalbum + STR_EV_RAND_ALBUM, // streamer_move_to_randomalbum STR_EV_SEEK, // streamer_set_seek; p1: float pos STR_EV_SET_CURR_PLT, // streamer_set_current_playlist STR_EV_DSP_RELOAD, // reload dsp settings @@ -77,6 +80,14 @@ streamer_get_current_track_to_play (playlist_t *plt); playItem_t * streamer_get_next_track_with_direction (int dir, ddb_shuffle_t shuffle, ddb_repeat_t repeat); +// returns the first track of the next album according to repeat and shuffle settings, with specified direction +// if direction is -1 returns +// - the first track of the current album, if the current track is not the first of the album +// - the first track of the previous album, if the current track is the first of the album +// this mimics pressing back once to go to the beginning of the track and twice to go to the previous track +playItem_t * +streamer_get_next_album_with_direction (int dir, ddb_shuffle_t shuffle, ddb_repeat_t repeat); + void streamer_set_last_played (playItem_t *track); @@ -130,6 +141,15 @@ streamer_move_to_prevsong (int r); int streamer_move_to_randomsong (int r); +int +streamer_move_to_nextalbum (int r); + +int +streamer_move_to_prevalbum (int r); + +int +streamer_move_to_randomalbum (int r); + struct DB_fileinfo_s * streamer_get_current_fileinfo (void);