diff --git a/src/cljs/airsonic_ui/audio/playlist.cljs b/src/cljs/airsonic_ui/audio/playlist.cljs index bf530c1..c9ed4fc 100644 --- a/src/cljs/airsonic_ui/audio/playlist.cljs +++ b/src/cljs/airsonic_ui/audio/playlist.cljs @@ -1,7 +1,8 @@ (ns airsonic-ui.audio.playlist "Implements playlist queues that support different kinds of repetition and song ordering." - (:refer-clojure :exclude [peek])) + (:refer-clojure :exclude [peek]) + (:require [airsonic-ui.utils.helpers :refer [find-where]])) (defrecord Playlist [queue playback-mode repeat-mode] cljs.core/ICounted @@ -15,19 +16,17 @@ (defn- playlist-queue [queue playing-idx] - (concat (take playing-idx queue) - [(assoc (nth queue playing-idx) :currently-playing? true)] - (drop (inc playing-idx) queue))) + (assoc-in (vec queue) [playing-idx :currently-playing?] true)) (defmethod ->playlist :playback-mode/linear [queue playing-idx playback-mode repeat-mode] (let [queue (->> (playlist-queue queue playing-idx) - (mapv (fn [order song] (assoc song :order order)) - (range (count queue))))] + (mapv (fn [order song] (assoc song :order order)) (range)))] (->Playlist queue playback-mode repeat-mode))) (defn- -shuffle-songs [queue] - (mapv (fn [song order] (assoc song :order order)) queue (shuffle (range (count queue))))) + (->> (shuffle (range (count queue))) + (mapv (fn [song order] (assoc song :order order)) queue))) (defmethod ->playlist :playback-mode/shuffled [queue playing-idx playback-mode repeat-mode] @@ -35,17 +34,11 @@ (-shuffle-songs))] (->Playlist queue playback-mode repeat-mode))) -(defn- -current-song [playlist] - (first (keep-indexed (fn [idx song] - (when (:currently-playing? song) - [idx song])) - (:queue playlist)))) - (defn set-playback-mode "Changes the playback mode of a playlist and re-shuffles it if necessary" [playlist playback-mode] - (->playlist (:queue playlist) (first (-current-song playlist)) - playback-mode (:repeat-mode playlist))) + (let [[current-idx _] (find-where :currently-playing? (:queue playlist))] + (->playlist (:queue playlist) current-idx playback-mode (:repeat-mode playlist)))) (defn set-repeat-mode "Allows to change the way the next and previous song of a playlist is selected" @@ -59,45 +52,71 @@ (filter :currently-playing?) (first))) -(defmulti next-song :repeat-mode) +(defn set-current-song + "Marks a song in the queue as currently playing, given its ID" + [playlist next-idx] + (let [[current-idx _] (find-where :currently-playing? (:queue playlist))] + (-> (if current-idx + (update-in playlist [:queue current-idx] dissoc :currently-playing?) + playlist) + (assoc-in [:queue next-idx :currently-playing?] true)))) + +(defmulti next-song "Advances the currently playing song" :repeat-mode) (defmethod next-song :repeat-mode/none [playlist] ;; this is pretty easy; get the next song and stop playing at the at - (let [[current-idx _] (-current-song playlist) - next-idx (inc current-idx)] + (let [[current-idx current-song] (find-where :currently-playing? (:queue playlist)) + [next-idx _] (find-where #(= (:order %) (inc (:order current-song))) (:queue playlist))] (update playlist :queue - (fn [queue] (cond-> queue - :always (update current-idx dissoc :currently-playing?) - (< next-idx (count playlist)) (assoc-in [next-idx :currently-playing?] true)))))) + (fn [queue] + (cond-> queue + current-idx (update current-idx dissoc :currently-playing?) + next-idx (assoc-in [next-idx :currently-playing?] true)))))) (defmethod next-song :repeat-mode/single [playlist] playlist) (defmethod next-song :repeat-mode/all [playlist] - (let [[current-idx _] (-current-song playlist)] + (let [[current-idx current-song] (find-where :currently-playing? (:queue playlist)) + [next-idx _] (find-where #(= (:order %) (inc (:order current-song))) (:queue playlist))] (-> (update-in playlist [:queue current-idx] dissoc :currently-playing?) (update :queue (fn [queue] ;; we need special treatment here if we're playing the last song and ;; have a shuffled playlist because we need to re-shuffle - (if (and (= current-idx (dec (count playlist))) - (= :playback-mode/shuffled (:playback-mode playlist))) - (->> (-shuffle-songs queue) - (mapv #(if (= (:order %) 0) (assoc % :currently-playing? true) %))) - (let [next-idx (mod (inc current-idx) (count playlist))] - (assoc-in queue [next-idx :currently-playing?] true)))))))) + (if next-idx + (assoc-in queue [next-idx :currently-playing?] true) + (case (:playback-mode playlist) + :playback-mode/linear (assoc-in queue [0 :currently-playing?] true) + :playback-mode/shuffled (let [queue' (-shuffle-songs queue) + [next-idx _] (find-where #(= (:order %) 0) queue')] + (assoc-in queue' [next-idx :currently-playing?] true))))))))) -(defmulti previous-song :repeat-mode) +(defmulti previous-song "Goes back along the playback queue" :repeat-mode) -(defn set-current-song [playlist playing-idx] +(defmethod previous-song :repeat-mode/single [playlist] playlist) + +(defmethod previous-song :repeat-mode/none [playlist] + (let [[current-idx current-song] (find-where :currently-playing? (:queue playlist)) + [next-idx _] (find-where #(= (:order %) (dec (:order current-song))) (:queue playlist))] + (set-current-song playlist (or next-idx current-idx)))) + +(defmethod previous-song :repeat-mode/all [playlist] + (let [[_ current-song] (find-where :currently-playing? (:queue playlist)) + [next-idx _] (find-where #(= (:order %) + (rem (dec (:order current-song)) (count playlist))) + (:queue playlist))] + (if next-idx + (set-current-song playlist next-idx) + (if (= :playback-mode/shuffled (:playback-mode playlist)) + (set-current-song (update playlist :queue -shuffle-songs) 0) + (set-current-song playlist (mod (dec (:order current-song)) (count playlist))))))) + +(defn enqueue-last [playlist song] ;; TODO: Implementation ) -(defn add-song-to-end [playlist] - ;; TODO: Implementation - ) - -(defn add-next-song [playlist] +(defn enqueue-next [playlist song] ;; TODO: Implementation ) diff --git a/src/cljs/airsonic_ui/utils/helpers.cljs b/src/cljs/airsonic_ui/utils/helpers.cljs new file mode 100644 index 0000000..b9e86b7 --- /dev/null +++ b/src/cljs/airsonic_ui/utils/helpers.cljs @@ -0,0 +1,10 @@ +(ns airsonic-ui.utils.helpers + "Assorted helper functions") + +(defn find-where + "Returns the the first item in `coll` with its index for which `(p song)` + is truthy" + [p coll] + (->> (map-indexed vector coll) + (reduce (fn [_ [idx song]] + (when (p song) (reduced [idx song]))) nil))) diff --git a/test/cljs/airsonic_ui/audio/playlist_test.cljs b/test/cljs/airsonic_ui/audio/playlist_test.cljs index d8f12fc..f168f7c 100644 --- a/test/cljs/airsonic_ui/audio/playlist_test.cljs +++ b/test/cljs/airsonic_ui/audio/playlist_test.cljs @@ -1,26 +1,27 @@ (ns airsonic-ui.audio.playlist-test (:require [cljs.test :refer [deftest testing is]] [airsonic-ui.audio.playlist :as playlist] + [airsonic-ui.utils.helpers :refer [find-where]] [airsonic-ui.fixtures :as fixtures] [airsonic-ui.test-helpers :as helpers] [debux.cs.core :refer-macros [dbg]])) (enable-console-print!) -(defn- playing-queue +(defn- song-queue "Generates a seq of n different songs" [n] (repeatedly n #(hash-map :id (rand-int 9999) :coverArt (rand-int 9999) :year (+ 1900 (rand-int 118)) :artist (helpers/rand-str) - :aristId (rand-int 100000) + :artistId (rand-int 100000) :title (helpers/rand-str) :album (helpers/rand-str)))) (def fixture {:audio {:current-song fixtures/song - :playlist (playing-queue 20) + :playlist (song-queue 20) :playback-status fixtures/playback-status}}) (defn- same-song? [a b] (= (:id a) (:id b))) @@ -28,7 +29,7 @@ (deftest playlist-creation (testing "Playlist creation" (testing "should give us the correct current song" - (let [queue (playing-queue 10) + (let [queue (song-queue 10) start-idx (rand-int 10)] (doseq [playback-mode [:playback-mode/linear, :playback-mode/shuffled] repeat-mode [:repeat-mode/none, :repeat-mode/single, :repeat-mode/all]] @@ -37,7 +38,7 @@ (playlist/peek))) (str playback-mode ", " repeat-mode))))) (testing "should give us a playlist with the correct number of tracks" - (let [queue (playing-queue 100) + (let [queue (song-queue 100) start-idx (rand-int 100)] (doseq [playback-mode [:playback-mode/linear, :playback-mode/shuffled] repeat-mode [:repeat-mode/none, :repeat-mode/single, :repeat-mode/all]] @@ -48,7 +49,7 @@ (deftest changing-playback-mode (testing "Changing playback mode" (testing "from linear to shuffled" - (let [queue (playing-queue 10) + (let [queue (song-queue 10) start-idx (rand-int 10) linear (playlist/->playlist queue start-idx :playback-mode/linear :repeat-mode/node) shuffled (playlist/set-playback-mode linear :playback-mode/shuffled)] @@ -59,7 +60,7 @@ (testing "should not change the repeat mode" (is (= (:repeat-mode shuffled) (:repeat-mode linear))))) (testing "from shuffled to linear" - (let [queue (playing-queue 10) + (let [queue (song-queue 10) start-idx (rand-int 10) shuffled (playlist/->playlist queue start-idx :playback-mode/shuffled :repeat-mode/none) @@ -85,7 +86,7 @@ (doseq [playback-mode [:playback-mode/linear, :playback-mode/shuffled] repeat-mode [:repeat-mode/none, :repeat-mode/single, :repeat-mode/all] next-repeat-mode [:repeat-mode/none, :repeat-mode/single, :repeat-mode/all]] - (let [playlist (-> (playlist/->playlist (playing-queue 1) 0 + (let [playlist (-> (playlist/->playlist (song-queue 1) 0 playback-mode repeat-mode) (playlist/set-repeat-mode next-repeat-mode))] (is (= playback-mode (:playback-mode playlist))) @@ -95,7 +96,7 @@ (deftest linear-next-song (testing "Should follow the same order as the queue used for creation" (doseq [repeat-mode [:repeat-mode/none :repeat-mode/all]] - (let [queue (playing-queue 5) + (let [queue (song-queue 5) playlist (playlist/->playlist queue 0 :playback-mode/linear repeat-mode)] (is (same-song? (nth queue 1) (-> (playlist/next-song playlist) (playlist/peek))) @@ -106,7 +107,7 @@ (str repeat-mode ", skipped twice"))))) (testing "Should go back to the first song when repeat-mode is all and we played the last song") (testing "Should always give the same track when repeat-mode is single" - (let [queue (playing-queue 3) + (let [queue (song-queue 3) playing-idx (rand-int 3) playlist (playlist/->playlist queue playing-idx :playback-mode/linear :repeat-mode/single) @@ -116,30 +117,31 @@ (is (same-song? (nth queue playing-idx) (nth played-back 2))) (is (same-song? (nth queue playing-idx) (nth played-back 3)) "wrapping around"))) (testing "Should stop playing at the end of the queue when repeat-mode is none" - (is (nil? (-> (playing-queue 1) + (is (nil? (-> (song-queue 1) (playlist/->playlist 0 :playback-mode/linear :repeat-mode/none) (playlist/next-song) (playlist/peek)))))) (deftest shuffled-next-song (testing "Should play every track once when called for the entire queue" - (doseq [repeat-mode '(:repeat-mode/none :repeat-mode/all)] - (let [length 10 - playlist (playlist/->playlist (playing-queue length) 0 - :playback-mode/shuffled repeat-mode) - played-tracks (->> (iterate playlist/next-song playlist) - (map playlist/peek) - (take length))] - (is (= (count played-tracks) (count (set played-tracks))) - (str repeat-mode))))) + (with-redefs [shuffle reverse] + (doseq [repeat-mode '(:repeat-mode/none :repeat-mode/all)] + (let [length 10 + playlist (playlist/->playlist (song-queue length) 9 + :playback-mode/shuffled repeat-mode) + played-tracks (->> (iterate playlist/next-song playlist) + (map playlist/peek) + (take length))] + (is (= (count played-tracks) (count (set played-tracks))) + (str repeat-mode)))))) (testing "Should re-shuffle the playlist when wrapping around and repeat-mode is all" - (let [length 100 - playlist (playlist/->playlist (playing-queue length) (dec length) - :playback-mode/shuffled :repeat-mode/all)] + (let [playlist (playlist/->playlist (song-queue 100) 0 :playback-mode/shuffled :repeat-mode/all) + [last-idx _] (find-where #(= (:order %) 99) (:queue playlist))] (is (not= (map :order (:queue playlist)) - (map :order (:queue (playlist/next-song playlist))))))) + (map :order (:queue (-> (playlist/set-current-song playlist last-idx) + (playlist/next-song)))))))) (testing "Should always give the same track when repeat-mode is single" - (let [queue (playing-queue 3) + (let [queue (song-queue 3) playing-idx (rand-int 3) playlist (playlist/->playlist queue playing-idx :playback-mode/shuffled :repeat-mode/single) @@ -149,10 +151,85 @@ (is (same-song? (nth queue playing-idx) (nth played-back 2))) (is (same-song? (nth queue playing-idx) (nth played-back 3)) "wrapping around"))) (testing "Should stop playing at the end of the queue when repeat-mode is none" - (is (nil? (-> (playing-queue 1) + (is (nil? (-> (song-queue 1) (playlist/->playlist 0 :playback-mode/linear :repeat-mode/none) (playlist/next-song) (playlist/peek)))))) -#_(deftest linear-previous-song) -#_(deftest shuffled-previous-song) +(deftest linear-previous-song + (testing "Should always give the same track when repeat-mode is single" + (let [queue (song-queue 3) + playing-idx (rand-int 3) + playlist (playlist/->playlist queue playing-idx + :playback-mode/linear :repeat-mode/single) + played-back (map playlist/peek (iterate playlist/next-song playlist))] + (is (same-song? (nth queue playing-idx) (nth played-back 0))) + (is (same-song? (nth queue playing-idx) (nth played-back 1))) + (is (same-song? (nth queue playing-idx) (nth played-back 2))) + (is (same-song? (nth queue playing-idx) (nth played-back 3)) "wrapping around"))) + (testing "Should keep the linear order when repeat-mode is not single" + (doseq [repeat-mode '(:repeat-mode/none :repeat-mode/all)] + (let [queue (song-queue 3) + playlist (playlist/->playlist queue 0 :playback-mode/linear repeat-mode)] + (is (same-song? (nth queue 1) (-> (playlist/next-song playlist) + (playlist/next-song) + (playlist/previous-song) + (playlist/peek))))))) + (testing "Should repeatedly give the first song when repeat-mode is none" + (let [queue (song-queue 3) + playlist (playlist/->playlist queue 0 :playback-mode/linear :repeat-mode/none)] + (is (same-song? (first queue) (-> (playlist/previous-song playlist) + (playlist/peek)))))) + (testing "Should wrap around to last song when repeat-mode is all" + (let [queue (song-queue 3) + playlist (playlist/->playlist queue 0 :playback-mode/linear :repeat-mode/all)] + (is (same-song? (last queue) (-> (playlist/previous-song playlist) + (playlist/peek))))))) + +(deftest shuffled-previous-song + (with-redefs [shuffle reverse] + (testing "Should always give the same track when repeat-mode is single" + (let [queue (song-queue 3) + playing-idx (rand-int 3) + playlist (playlist/->playlist queue playing-idx + :playback-mode/shuffled :repeat-mode/single) + played-back (map playlist/peek (iterate playlist/next-song playlist))] + (is (same-song? (nth queue playing-idx) (nth played-back 0))) + (is (same-song? (nth queue playing-idx) (nth played-back 1))) + (is (same-song? (nth queue playing-idx) (nth played-back 2))) + (is (same-song? (nth queue playing-idx) (nth played-back 3)) "wrapping around"))) + (testing "Should keep the playing order when repeat-mode is not single" + (doseq [repeat-mode '(:repeat-mode/none :repeat-mode/all)] + (let [queue (song-queue 3) + playlist' (playlist/->playlist queue 0 :playback-mode/shuffled repeat-mode) + [first-idx _] (find-where #(= (:order %) 0) (:queue playlist')) + playlist (playlist/set-current-song playlist' first-idx)] + (is (same-song? (playlist/peek playlist) + (-> playlist + (playlist/next-song) + (playlist/previous-song) + (playlist/peek))) + (str "for repeat mode " repeat-mode)) + (is (same-song? (-> (playlist/next-song playlist) + (playlist/peek)) + (-> (playlist/next-song playlist) + (playlist/next-song) + (playlist/previous-song) + (playlist/peek))) + (str "for repeat mode " repeat-mode))))) + (testing "Should re-shuffle when repeat-mode is all and we pass the start" + (with-redefs [shuffle identity] + (let [playlist (playlist/->playlist (song-queue 2) 0 :playback-mode/shuffled :repeat-mode/all) + playlist' (with-redefs [shuffle reverse] + (playlist/previous-song playlist))] + (is (not= (map :order (:queue playlist)) (map :order (:queue playlist'))))))))) + +(deftest set-current-song + (testing "Should correctly set the new song" + (let [queue (song-queue 3) + playlist (playlist/->playlist queue 0 :playback-mode/shuffled :repeat-mode/single) + current-track (first queue) + next-track (-> (playlist/set-current-song playlist 1) + (playlist/peek))] + (is (not (nil? next-track))) + (is (not (same-song? current-track next-track)))))) diff --git a/test/cljs/airsonic_ui/utils/helpers_test.cljs b/test/cljs/airsonic_ui/utils/helpers_test.cljs new file mode 100644 index 0000000..a53c34d --- /dev/null +++ b/test/cljs/airsonic_ui/utils/helpers_test.cljs @@ -0,0 +1,14 @@ +(ns airsonic-ui.utils.helpers-test + (:require [cljs.test :refer [deftest testing is]] + [airsonic-ui.utils.helpers :as helpers])) + +(deftest find-where + (testing "Finds the correct item and index" + (is (= [0 1] (helpers/find-where (partial = 1) (range 1 10)))) + (is (= [2 {:foo true, :bar false}] (helpers/find-where :foo '({} + {:foo false + :bar true} + {:foo true + :bar false}))))) + (testing "Returns nil when nothing is found" + (is (nil? (helpers/find-where (partial = 2) (range 2))))))