mirror of
https://github.com/heyarne/airsonic-ui.git
synced 2026-05-07 10:43:39 +02:00
Implement all the playlist skipping functionality
This commit is contained in:
parent
f7e4bc58a7
commit
27ac6f9478
4 changed files with 185 additions and 65 deletions
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
10
src/cljs/airsonic_ui/utils/helpers.cljs
Normal file
10
src/cljs/airsonic_ui/utils/helpers.cljs
Normal file
|
|
@ -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)))
|
||||
|
|
@ -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))))))
|
||||
|
|
|
|||
14
test/cljs/airsonic_ui/utils/helpers_test.cljs
Normal file
14
test/cljs/airsonic_ui/utils/helpers_test.cljs
Normal file
|
|
@ -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))))))
|
||||
Loading…
Add table
Add a link
Reference in a new issue