1
0
Fork 0
mirror of https://github.com/heyarne/airsonic-ui.git synced 2026-05-07 02:33:39 +02:00

Refactor playlist to use a sorted-map

This commit is contained in:
heyarne 2019-03-05 00:06:04 +01:00
commit 644939c618
9 changed files with 206 additions and 229 deletions

View file

@ -1,6 +1,7 @@
module.exports = function (config) { module.exports = function (config) {
const configuration = { const configuration = {
browsers: ['ChromeHeadless'], browsers: ['ChromeHeadless'],
autoWatchBatchDelay: 1000,
// The directory where the output file lives // The directory where the output file lives
basePath: 'public/test', basePath: 'public/test',
// The file itself // The file itself

6
package-lock.json generated
View file

@ -1512,7 +1512,7 @@
}, },
"engine.io-client": { "engine.io-client": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "http://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz",
"integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==", "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==",
"dev": true, "dev": true,
"requires": { "requires": {
@ -4072,7 +4072,7 @@
}, },
"media-typer": { "media-typer": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
"dev": true "dev": true
}, },
@ -6557,7 +6557,7 @@
}, },
"socket.io-parser": { "socket.io-parser": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "http://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz",
"integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==", "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==",
"dev": true, "dev": true,
"requires": { "requires": {

View file

@ -6,8 +6,6 @@
[airsonic-ui.audio.playlist :as playlist] [airsonic-ui.audio.playlist :as playlist]
[goog.functions :refer [throttle]])) [goog.functions :refer [throttle]]))
;; TODO: Manage buffering
(defonce audio (atom nil)) (defonce audio (atom nil))
(defn normalize-time-ranges [time-ranges] (defn normalize-time-ranges [time-ranges]
@ -28,7 +26,6 @@
; explanation of these events: https://developer.mozilla.org/en-US/Apps/Fundamentals/Audio_and_video_delivery/Cross-browser_audio_basics ; explanation of these events: https://developer.mozilla.org/en-US/Apps/Fundamentals/Audio_and_video_delivery/Cross-browser_audio_basics
(defn attach-listeners! [el] (defn attach-listeners! [el]
(let [emit-audio-update (throttle #(rf/dispatch [:audio/update (->status el)]) 16)] (let [emit-audio-update (throttle #(rf/dispatch [:audio/update (->status el)]) 16)]
(doseq [event ["loadstart" "progress" "play" "timeupdate" "pause" "volumechange"]] (doseq [event ["loadstart" "progress" "play" "timeupdate" "pause" "volumechange"]]
@ -116,7 +113,9 @@
"Gives us information about the currently played song as presented by "Gives us information about the currently played song as presented by
the airsonic api" the airsonic api"
[playlist _] [playlist _]
(playlist/peek playlist)) (when-not (empty? playlist)
(playlist/current-song playlist)))
(rf/reg-sub (rf/reg-sub
:audio/current-song :audio/current-song

View file

@ -1,139 +1,130 @@
(ns airsonic-ui.audio.playlist (ns airsonic-ui.audio.playlist
"Implements playlist queues that support different kinds of repetition and "Implements playlist queues that support different kinds of repetition and
song ordering." song ordering.")
(:refer-clojure :exclude [peek])
(:require [airsonic-ui.helpers :refer [find-where]]))
(defrecord Playlist [items playback-mode repeat-mode] ;; Turns out we can nicely implement this by thinly wrapping a sequence of items
;; We re-use the core ClojureScript protocols internally but provide a nice and
;; explicit API to consume
(defprotocol IPlaylist
(current-song [this])
(next-song [this])
(previous-song [this])
(set-current-song [this song-idx]
"Advances the queue to the song given by song-idx")
(set-playback-mode [this playback-mode]
"Changes the playback mode of a playlist and re-shuffles it if necessary")
(set-repeat-mode [this repeat-mode]
"Allows you to change how the next and previous song are selected")
(enqueue-last [this song])
(enqueue-next [this song]))
;; helpers to manage creating playlists
(defn- mark-original-order
"This function is used if we switch from linear to shuffled; it allows us to
restore the order of the queue when it was created."
[items]
(->> (sort-by (comp meta :playlist/linear-order) items)
(map-indexed (fn [idx item]
(vary-meta item assoc :playlist/linear-order idx)))))
(defn- linear-queue
[items]
(->> (mark-original-order items)
(map-indexed vector)
(into (sorted-map))))
(defn- shuffled-queue
[items]
(let [shuffled-indices (shuffle (range (count items)))]
(->> (mark-original-order items)
(map vector shuffled-indices)
(into (sorted-map)))))
;; the exported interface:
(defrecord Playlist [items current-idx playback-mode repeat-mode]
cljs.core/ICounted cljs.core/ICounted
(-count [this] (-count [this]
(count (:items this)))) (count items))
IPlaylist
(current-song [_]
(get items current-idx))
(next-song [this]
(update this :current-idx
(fn [current-idx]
(cond
(= repeat-mode :repeat-single) current-idx
(or (= repeat-mode :repeat-all)
(< current-idx (dec (count this))))
(mod (inc current-idx) (count this))))))
(previous-song [this]
(update this :current-idx
(fn [current-idx]
(cond
(= repeat-mode :repeat-single) current-idx
(or (= repeat-mode :repeat-all)
(> current-idx 0))
(mod (dec current-idx) (count this))
:else nil))))
(set-current-song [playlist song-idx]
(assoc playlist :current-idx song-idx))
(set-playback-mode [playlist playback-mode]
(let [current-song (current-song playlist)
queue-fn (case playback-mode
:shuffled shuffled-queue
:linear linear-queue)
next-playlist (update playlist :items (comp queue-fn vals))
next-idx (first (keep (fn [[idx song]]
(when (= song current-song)
idx))
(:items next-playlist)))]
;; we have to find out the index of the currently playing song after the
;; playlist was created because it might change when shuffling / unshuffling
(set-current-song next-playlist next-idx)))
(set-repeat-mode [playlist repeat-mode]
(assoc playlist :repeat-mode repeat-mode))
(enqueue-last [this song]
(let [order (inc (key (last items)))]
;; Arguably this is a bit weird; but if you want to play something last in
;; a shuffled playlist, you want to play it last I guess.
(assoc-in this [:items order]
(vary-meta song assoc :playlist/linear-order order))))
(enqueue-next [this song]
;; we slice the songs up until the currently playing one and increase the
;; order for all the songs after
(let [songs (vec (vals items))
reordered (-> (subvec songs 0 (inc current-idx))
(conj (vary-meta song assoc :playlist/linear-order (inc current-idx)))
(concat (subvec songs (inc current-idx))))]
(assoc this :items (->> (map-indexed vector reordered)
(into (sorted-map)))))))
;; constructor wrapper
(defmulti ->playlist (defmulti ->playlist
"Creates a new playlist that behaves according to the given playback- and "Creates a new playlist that behaves according to the given playback- and
repeat-mode parameters." repeat-mode parameters."
(fn [queue & {:keys [playback-mode #_repeat-mode]}] (fn [_ & {:keys [playback-mode]}] playback-mode))
playback-mode))
(defn- mark-first-song [queue]
(let [[first-idx _] (find-where #(= 0 (:playlist/order %)) queue)]
(assoc-in queue [first-idx :playlist/currently-playing?] true)))
(defmethod ->playlist :linear (defmethod ->playlist :linear
[queue & {:keys [playback-mode repeat-mode]}] [items & {:keys [playback-mode repeat-mode]}]
(let [queue (-> (mapv (fn [order song] (assoc song :playlist/order order)) (range) queue) (->Playlist (linear-queue items) 0 playback-mode repeat-mode))
(mark-first-song))]
(->Playlist queue playback-mode repeat-mode)))
(defn- -shuffle-songs [queue]
(->> (shuffle (range (count queue)))
(mapv (fn [song order] (assoc song :playlist/order order)) queue)))
(defmethod ->playlist :shuffled (defmethod ->playlist :shuffled
[queue & {:keys [playback-mode repeat-mode]}] [items & {:keys [playback-mode repeat-mode]}]
(let [queue (conj (mapv #(update % :playlist/order inc) (-shuffle-songs (rest queue))) (->Playlist (shuffled-queue items) 0 playback-mode repeat-mode))
(assoc (first queue) :playlist/order 0 :playlist/currently-playing? true))]
(->Playlist queue playback-mode 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 :playlist/currently-playing? (:items playlist))]
(-> (if current-idx
(update-in playlist [:items current-idx] dissoc :playlist/currently-playing?)
playlist)
(assoc-in [:items next-idx :playlist/currently-playing?] true))))
(defn set-playback-mode
"Changes the playback mode of a playlist and re-shuffles it if necessary"
[playlist playback-mode]
(if (= playback-mode :shuffled)
;; for shuffled playlists we reorder the songs make sure that the currently
;; playing song has order 0
(let [playlist (->playlist (:items playlist) :playback-mode playback-mode :repeat-mode (:repeat-mode playlist))
[current-idx current-song] (find-where :playlist/currently-playing? (:items playlist))
[swap-idx _] (find-where #(= 0 (:playlist/order %)) (:items playlist))]
(-> (assoc-in playlist [:items current-idx :playlist/order] 0)
(assoc-in [:items swap-idx :playlist/order] (:playlist/order current-song))))
;; for linear songs we just make sure that the current does not change
(let [[current-idx _] (find-where :playlist/currently-playing? (:items playlist))]
(-> (->playlist (:items playlist) :playback-mode playback-mode :repeat-mode (:repeat-mode playlist))
(set-current-song current-idx)))))
(defn set-repeat-mode
"Allows to change the way the next and previous song of a playlist is selected"
[playlist repeat-mode]
(assoc playlist :repeat-mode repeat-mode))
(defn peek
"Returns the song in a playlist that is currently playing"
[playlist]
(->> (:items playlist)
(filter :playlist/currently-playing?)
(first)))
(defmulti next-song "Advances the currently playing song" :repeat-mode)
(defmethod next-song :repeat-none
[playlist]
;; this is pretty easy; get the next song and stop playing at the at
(let [[current-idx current-song] (find-where :playlist/currently-playing? (:items playlist))
[next-idx _] (find-where #(= (:playlist/order %) (inc (:playlist/order current-song))) (:items playlist))]
(update playlist :items
(fn [queue]
(cond-> queue
current-idx (update current-idx dissoc :playlist/currently-playing?)
next-idx (assoc-in [next-idx :playlist/currently-playing?] true))))))
(defmethod next-song :repeat-single [playlist] playlist)
(defmethod next-song :repeat-all
[playlist]
(let [[current-idx current-song] (find-where :playlist/currently-playing? (:items playlist))
[next-idx _] (find-where #(= (:playlist/order %) (inc (:playlist/order current-song))) (:items playlist))]
(-> (update-in playlist [:items current-idx] dissoc :playlist/currently-playing?)
(update :items
(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 next-idx
(assoc-in queue [next-idx :playlist/currently-playing?] true)
(case (:playback-mode playlist)
:linear (assoc-in queue [0 :playlist/currently-playing?] true)
:shuffled (let [queue' (-shuffle-songs queue)
[next-idx _] (find-where #(= (:playlist/order %) 0) queue')]
(assoc-in queue' [next-idx :playlist/currently-playing?] true)))))))))
(defmulti previous-song "Goes back along the playback queue" :repeat-mode)
(defmethod previous-song :repeat-single [playlist] playlist)
(defmethod previous-song :repeat-none [playlist]
(let [[current-idx current-song] (find-where :playlist/currently-playing? (:items playlist))
[next-idx _] (find-where #(= (:playlist/order %) (dec (:playlist/order current-song))) (:items playlist))]
(set-current-song playlist (or next-idx current-idx))))
(defmethod previous-song :repeat-all [playlist]
(let [[_ current-song] (find-where :playlist/currently-playing? (:items playlist))
[next-idx _] (find-where #(= (:playlist/order %)
(rem (dec (:playlist/order current-song)) (count playlist)))
(:items playlist))]
(if next-idx
(set-current-song playlist next-idx)
(if (= :shuffled (:playback-mode playlist))
(let [highest-order (dec (count playlist))
playlist (update playlist :items -shuffle-songs)
[last-idx _] (find-where #(= (:playlist/order %) highest-order) (:items playlist))]
(set-current-song playlist last-idx))
(set-current-song playlist (mod (dec (:playlist/order current-song)) (count playlist)))))))
(defn enqueue-last [playlist song]
(let [highest-order (last (sort (map :playlist/order (:items playlist))))]
(update playlist :items conj (assoc song :playlist/order (inc highest-order)))))
(defn enqueue-next [playlist song]
(let [[_ current-song] (find-where :playlist/currently-playing? (:items playlist))]
(update playlist :items
(fn [queue]
(-> (mapv #(if (> (:playlist/order %) (:playlist/order current-song)) (update % :playlist/order inc) %) queue)
(conj (assoc song :playlist/order (inc (:playlist/order current-song)))))))))

View file

@ -9,7 +9,7 @@
(fn [{:keys [db]} [_ songs start-idx]] (fn [{:keys [db]} [_ songs start-idx]]
(let [playlist (-> (playlist/->playlist songs :playback-mode :linear :repeat-mode :repeat-all) (let [playlist (-> (playlist/->playlist songs :playback-mode :linear :repeat-mode :repeat-all)
(playlist/set-current-song start-idx))] (playlist/set-current-song start-idx))]
{:audio/play (api/stream-url (:credentials db) (playlist/peek playlist)) {:audio/play (api/stream-url (:credentials db) (playlist/current-song playlist))
:db (assoc-in db [:audio :current-queue] playlist)}))) :db (assoc-in db [:audio :current-queue] playlist)})))
(rf/reg-event-db (rf/reg-event-db
@ -26,7 +26,7 @@
:audio-player/next-song :audio-player/next-song
(fn [{:keys [db]} _] (fn [{:keys [db]} _]
(let [db (update-in db [:audio :current-queue] playlist/next-song) (let [db (update-in db [:audio :current-queue] playlist/next-song)
next (playlist/peek (get-in db [:audio :current-queue]))] next (playlist/current-song (get-in db [:audio :current-queue]))]
{:db db {:db db
:audio/play (api/stream-url (:credentials db) next)}))) :audio/play (api/stream-url (:credentials db) next)})))
@ -34,7 +34,7 @@
:audio-player/previous-song :audio-player/previous-song
(fn [{:keys [db]} _] (fn [{:keys [db]} _]
(let [db (update-in db [:audio :current-queue] playlist/previous-song) (let [db (update-in db [:audio :current-queue] playlist/previous-song)
prev (playlist/peek (get-in db [:audio :current-queue]))] prev (playlist/current-song (get-in db [:audio :current-queue]))]
{:db db {:db db
:audio/play (api/stream-url (:credentials db) prev)}))) :audio/play (api/stream-url (:credentials db) prev)})))
@ -65,7 +65,7 @@
(rf/reg-event-fx (rf/reg-event-fx
:audio-player/seek :audio-player/seek
(fn [{:keys [db]} [_ percentage]] (fn [{:keys [db]} [_ percentage]]
(let [duration (:duration (playlist/peek (get-in db [:audio :current-queue])))] (let [duration (:duration (playlist/current-song (get-in db [:audio :current-queue])))]
{:audio/seek [percentage duration]}))) {:audio/seek [percentage duration]})))
(rf/reg-event-fx (rf/reg-event-fx

View file

@ -4,14 +4,6 @@
[clojure.string :as str]) [clojure.string :as str])
(:import [goog.string format])) (:import [goog.string format]))
(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)))
(defn muted-dispatch (defn muted-dispatch
"Dispatches a re-frame event while canceling default DOM behavior; to be "Dispatches a re-frame event while canceling default DOM behavior; to be
called for example in `:on-click`." called for example in `:on-click`."

View file

@ -1,17 +1,20 @@
(ns airsonic-ui.audio.core-test (ns airsonic-ui.audio.core-test
(:require [airsonic-ui.audio.core :as audio] (:require [airsonic-ui.audio.core :as audio]
[airsonic-ui.audio.playlist-test :as p] #_[airsonic-ui.audio.playlist-test :as p]
[airsonic-ui.fixtures :as fixtures] #_[airsonic-ui.fixtures :as fixtures]
[cljs.test :refer [deftest testing is]])) [cljs.test :refer [deftest testing is]]))
(enable-console-print!) (enable-console-print!)
(deftest current-song-subscription (deftest current-song-subscription
;; NOTE: Should the subscription be moved to the playlist.cljs?
#_(testing "Should provide information about the song"
(letfn [(current-song [db] (letfn [(current-song [db]
(-> (audio/summary db [:audio/summary]) (-> (audio/summary db [:audio/summary])
(audio/current-song [:audio/current-song])))] (audio/current-song [:audio/current-song])))]
(testing "Should provide information about the song" (= fixtures/song (current-song p/fixture))))
(= fixtures/song (current-song p/fixture))))) (testing "Should work fine when no song is playing"
(is (nil? (audio/current-song nil [:audio/current-song])))))
(deftest playback-status-subscription (deftest playback-status-subscription
(letfn [(is-playing? [playback-status] (letfn [(is-playing? [playback-status]

View file

@ -1,7 +1,6 @@
(ns airsonic-ui.audio.playlist-test (ns airsonic-ui.audio.playlist-test
(:require [cljs.test :refer [deftest testing is]] (:require [cljs.test :refer [deftest testing is]]
[airsonic-ui.audio.playlist :as playlist] [airsonic-ui.audio.playlist :as playlist]
[airsonic-ui.helpers :refer [find-where]]
[airsonic-ui.fixtures :as fixtures] [airsonic-ui.fixtures :as fixtures]
[airsonic-ui.test-helpers :as helpers] [airsonic-ui.test-helpers :as helpers]
[debux.cs.core :refer-macros [dbg]])) [debux.cs.core :refer-macros [dbg]]))
@ -33,14 +32,22 @@
(deftest playlist-creation (deftest playlist-creation
(testing "Playlist creation" (testing "Playlist creation"
(testing "should give us the correct current song" (testing "should give us the correct current song for linear playback-mode"
(let [queue (song-queue 10)] (let [queue (song-queue 10)]
(doseq [playback-mode [:linear :shuffled] (doseq [repeat-mode [:repeat-none :repeat-single :repeat-all]]
repeat-mode [:repeat-none :repeat-single :repeat-all]]
(is (same-song? (first queue) (is (same-song? (first queue)
(-> (playlist/->playlist queue :playback-mode playback-mode :repeat-mode repeat-mode) (-> (playlist/->playlist queue :playback-mode :linear :repeat-mode repeat-mode)
(playlist/peek))) (playlist/current-song)))
(str playback-mode ", " repeat-mode))))) (str "repeat-mode: " repeat-mode)))))
(testing "any current song for shuffled playback mode"
(let [queue (song-queue 10)]
(doseq [repeat-mode [:repeat-none :repeat-single :repeat-all]]
(is (some? ((set queue)
(-> (playlist/->playlist queue :playback-mode :linear :repeat-mode repeat-mode)
(playlist/current-song))))
(str "repeat-mode: " repeat-mode)))))
(testing "should give us a playlist with the correct number of tracks" (testing "should give us a playlist with the correct number of tracks"
(let [queue (song-queue 100)] (let [queue (song-queue 100)]
(doseq [playback-mode [:linear :shuffled] (doseq [playback-mode [:linear :shuffled]
@ -56,9 +63,9 @@
linear (playlist/->playlist queue :playback-mode :linear :repeat-mode :repeat-none) linear (playlist/->playlist queue :playback-mode :linear :repeat-mode :repeat-none)
shuffled (playlist/set-playback-mode linear :shuffled)] shuffled (playlist/set-playback-mode linear :shuffled)]
(testing "should re-order the tracks" (testing "should re-order the tracks"
(is (not= (map :playlist/order (:items shuffled)) (map :playlist/order (:items linear))))) (is (not= (:items shuffled) (:items linear))))
(testing "should not change the currently playing track" (testing "should not change the currently playing track"
(is (same-song? (playlist/peek linear) (playlist/peek shuffled)))) (is (same-song? (playlist/current-song linear) (playlist/current-song shuffled))))
(testing "should not change the repeat mode" (testing "should not change the repeat mode"
(is (= (:repeat-mode shuffled) (:repeat-mode linear)))))) (is (= (:repeat-mode shuffled) (:repeat-mode linear))))))
(testing "from shuffled to linear" (testing "from shuffled to linear"
@ -66,10 +73,11 @@
shuffled (playlist/->playlist queue :playback-mode :shuffled :repeat-mode :repeat-none) shuffled (playlist/->playlist queue :playback-mode :shuffled :repeat-mode :repeat-none)
linear (playlist/set-playback-mode shuffled :linear)] linear (playlist/set-playback-mode shuffled :linear)]
(testing "should set the correct order for tracks" (testing "should set the correct order for tracks"
(is (every? #(apply same-song? %) (interleave queue (:items linear)))) (is (every? #(apply same-song? %) (interleave queue (vals (:items linear)))))
(is (< (:playlist/order (first (:items linear))) (:playlist/order (last (:items linear)))))) (is (< (:playlist/linear-order (meta (first (vals (:items linear)))))
(:playlist/linear-order (meta (last (vals (:items linear))))))))
(testing "should not change the currently playing track" (testing "should not change the currently playing track"
(is (same-song? (playlist/peek linear) (playlist/peek shuffled)))) (is (same-song? (playlist/current-song linear) (playlist/current-song shuffled))))
(testing "should not change the repeat mode" (testing "should not change the repeat mode"
(is (= (:repeat-mode shuffled) (:repeat-mode linear)))))))) (is (= (:repeat-mode shuffled) (:repeat-mode linear))))))))
@ -91,17 +99,18 @@
(let [queue (song-queue 5) (let [queue (song-queue 5)
playlist (playlist/->playlist queue :playback-mode :linear :repeat-mode repeat-mode)] playlist (playlist/->playlist queue :playback-mode :linear :repeat-mode repeat-mode)]
(is (same-song? (nth queue 1) (-> (playlist/next-song playlist) (is (same-song? (nth queue 1) (-> (playlist/next-song playlist)
(playlist/peek))) (playlist/current-song)))
(str repeat-mode ", skipped once")) (str repeat-mode ", skipped once"))
(is (same-song? (nth queue 2) (-> (playlist/next-song playlist) (is (same-song? (nth queue 2) (-> (playlist/next-song playlist)
(playlist/next-song) (playlist/next-song)
(playlist/peek))) (playlist/current-song)))
(str repeat-mode ", skipped twice"))))) (str repeat-mode ", skipped twice")))))
;; TODO: Write this test
(testing "Should go back to the first song when repeat-mode is all and we played the last song") (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" (testing "Should always give the same track when repeat-mode is single"
(let [queue (song-queue 3) (let [queue (song-queue 3)
playlist (playlist/->playlist queue :playback-mode :linear :repeat-mode :repeat-single) playlist (playlist/->playlist queue :playback-mode :linear :repeat-mode :repeat-single)
played-back (map playlist/peek (iterate playlist/next-song playlist))] played-back (map playlist/current-song (iterate playlist/next-song playlist))]
(is (same-song? (first queue) (nth played-back 0))) (is (same-song? (first queue) (nth played-back 0)))
(is (same-song? (first queue) (nth played-back 1))) (is (same-song? (first queue) (nth played-back 1)))
(is (same-song? (first queue) (nth played-back 2))) (is (same-song? (first queue) (nth played-back 2)))
@ -110,7 +119,7 @@
(is (nil? (-> (song-queue 1) (is (nil? (-> (song-queue 1)
(playlist/->playlist :playback-mode :linear :repeat-mode :repeat-none) (playlist/->playlist :playback-mode :linear :repeat-mode :repeat-none)
(playlist/next-song) (playlist/next-song)
(playlist/peek)))))) (playlist/current-song))))))
(deftest shuffled-next-song (deftest shuffled-next-song
(testing "Should play every track once when called for the entire queue" (testing "Should play every track once when called for the entire queue"
@ -118,35 +127,34 @@
(let [length 10 (let [length 10
playlist (playlist/->playlist (song-queue length) :playback-mode :shuffled :repeat-mode repeat-mode) playlist (playlist/->playlist (song-queue length) :playback-mode :shuffled :repeat-mode repeat-mode)
played-tracks (->> (iterate playlist/next-song playlist) played-tracks (->> (iterate playlist/next-song playlist)
(map playlist/peek) (map playlist/current-song)
(take length))] (take length))]
(is (= (count played-tracks) (count (set played-tracks))) (is (= (count played-tracks) (count (set played-tracks)))
(str repeat-mode))))) (str repeat-mode)))))
(testing "Should re-shuffle the playlist when wrapping around and repeat-mode is all" (testing "Should keep the song order when wrapping around and repeat-mode is all"
(let [playlist (playlist/->playlist (song-queue 100) :playback-mode :shuffled :repeat-mode :repeat-all) (let [playlist (playlist/->playlist (song-queue 100) :playback-mode :shuffled :repeat-mode :repeat-all)
[last-idx _] (find-where #(= (:playlist/order %) 99) (:items playlist))] next-playlist (-> (playlist/set-current-song playlist 99)
(is (not= (map :playlist/order (:items playlist)) (playlist/next-song))]
(map :playlist/order (:items (-> (playlist/set-current-song playlist last-idx) (= (playlist/current-song playlist)
(playlist/next-song)))))))) (playlist/current-song next-playlist))))
(testing "Should always give the same track when repeat-mode is single" (testing "Should always give the same track when repeat-mode is single"
(let [queue (song-queue 3) (let [playlist (playlist/->playlist (song-queue 10) :playback-mode :shuffled :repeat-mode :repeat-single)
playlist (playlist/->playlist queue :playback-mode :shuffled :repeat-mode :repeat-single) played-back (map playlist/current-song (iterate playlist/next-song playlist))]
played-back (map playlist/peek (iterate playlist/next-song playlist))] (dotimes [i 3]
(is (same-song? (first queue) (nth played-back 0))) (is (same-song? (nth played-back i) (nth played-back (inc i)))))))
(is (same-song? (first queue) (nth played-back 1)))
(is (same-song? (first queue) (nth played-back 2)))
(is (same-song? (first queue) (nth played-back 3)) "wrapping around")))
(testing "Should stop playing at the end of the queue when repeat-mode is none" (testing "Should stop playing at the end of the queue when repeat-mode is none"
(is (nil? (-> (song-queue 1) (is (nil? (-> (song-queue 1)
(playlist/->playlist :playback-mode :linear :repeat-mode :repeat-none) (playlist/->playlist :playback-mode :linear :repeat-mode :repeat-none)
(playlist/next-song) (playlist/next-song)
(playlist/peek)))))) (playlist/current-song))))))
(deftest linear-previous-song (deftest linear-previous-song
(testing "Should always give the same track when repeat-mode is single" (testing "Should always give the same track when repeat-mode is single"
(let [queue (song-queue 3) (let [queue (song-queue 3)
playlist (playlist/->playlist queue :playback-mode :linear :repeat-mode :repeat-single) playlist (playlist/->playlist queue :playback-mode :linear :repeat-mode :repeat-single)
played-back (map playlist/peek (iterate playlist/next-song playlist))] played-back (map playlist/current-song (iterate playlist/next-song playlist))]
(is (same-song? (first queue) (nth played-back 0))) (is (same-song? (first queue) (nth played-back 0)))
(is (same-song? (first queue) (nth played-back 1))) (is (same-song? (first queue) (nth played-back 1)))
(is (same-song? (first queue) (nth played-back 2))) (is (same-song? (first queue) (nth played-back 2)))
@ -158,61 +166,61 @@
(is (same-song? (nth queue 1) (-> (playlist/next-song playlist) (is (same-song? (nth queue 1) (-> (playlist/next-song playlist)
(playlist/next-song) (playlist/next-song)
(playlist/previous-song) (playlist/previous-song)
(playlist/peek))))))) (playlist/current-song)))))))
(testing "Should repeatedly give the first song when repeat-mode is none" ;; TODO: Should it?
#_(testing "Should repeatedly give the first song when repeat-mode is none"
(let [queue (song-queue 3) (let [queue (song-queue 3)
playlist (playlist/->playlist queue :playback-mode :linear :repeat-mode :repeat-none)] playlist (playlist/->playlist queue :playback-mode :linear :repeat-mode :repeat-none)]
(is (same-song? (first queue) (-> (playlist/previous-song playlist) (is (same-song? (first queue) (-> (playlist/previous-song playlist)
(playlist/peek)))))) (playlist/current-song))))))
(testing "Should wrap around to last song when repeat-mode is all" (testing "Should wrap around to last song when repeat-mode is all"
(let [queue (song-queue 3) (let [queue (song-queue 3)
playlist (playlist/->playlist queue :playback-mode :linear :repeat-mode :repeat-all)] playlist (playlist/->playlist queue :playback-mode :linear :repeat-mode :repeat-all)]
(is (same-song? (last queue) (-> (playlist/previous-song playlist) (is (same-song? (last queue) (-> (playlist/previous-song playlist)
(playlist/peek))))))) (playlist/current-song)))))))
(deftest shuffled-previous-song (deftest shuffled-previous-song
(with-redefs [shuffle reverse] (with-redefs [shuffle reverse]
(testing "Should always give the same track when repeat-mode is single" (testing "Should always give the same track when repeat-mode is single"
(let [queue (song-queue 3) (let [queue (song-queue 3)
playlist (playlist/->playlist queue :playback-mode :shuffled :repeat-mode :repeat-single) playlist (playlist/->playlist queue :playback-mode :shuffled :repeat-mode :repeat-single)
played-back (map playlist/peek (iterate playlist/next-song playlist))] played-back (map playlist/current-song (iterate playlist/next-song playlist))]
(is (same-song? (first queue) (nth played-back 0))) (dotimes [i 3]
(is (same-song? (first queue) (nth played-back 1))) (is (same-song? (nth played-back i) (nth played-back (inc i)))))))
(is (same-song? (first queue) (nth played-back 2)))
(is (same-song? (first queue) (nth played-back 3)) "wrapping around")))
(testing "Should keep the playing order when repeat-mode is not single" (testing "Should keep the playing order when repeat-mode is not single"
(doseq [repeat-mode '(:repeat-none :repeat-all)] (doseq [repeat-mode '(:repeat-none :repeat-all)]
(let [queue (song-queue 3) (let [queue (song-queue 3)
playlist (playlist/->playlist queue :playback-mode :shuffled :repeat-mode repeat-mode)] playlist (playlist/->playlist queue :playback-mode :shuffled :repeat-mode repeat-mode)]
(is (same-song? (playlist/peek playlist) (is (same-song? (playlist/current-song playlist)
(-> playlist (-> playlist
(playlist/next-song) (playlist/next-song)
(playlist/previous-song) (playlist/previous-song)
(playlist/peek))) (playlist/current-song)))
(str "for repeat mode " repeat-mode)) (str "for repeat mode " repeat-mode))
(is (same-song? (-> (playlist/next-song playlist) (is (same-song? (-> (playlist/next-song playlist)
(playlist/peek)) (playlist/current-song))
(-> (playlist/next-song playlist) (-> (playlist/next-song playlist)
(playlist/next-song) (playlist/next-song)
(playlist/previous-song) (playlist/previous-song)
(playlist/peek))) (playlist/current-song)))
(str "for repeat mode " repeat-mode))))) (str "for repeat mode " repeat-mode)))))
(testing "Should re-shuffle when repeat-mode is all and we go back to before the first track" (testing "Should keep the song order when repeat-mode is all and we go back to before the first track"
(let [playlist (with-redefs [shuffle identity] (let [playlist (playlist/->playlist (song-queue 10) :playback-mode :shuffled :repeat-mode :repeat-all)
(playlist/->playlist (song-queue 10) :playback-mode :shuffled :repeat-mode :repeat-all)) next-playlist (-> (playlist/previous-song playlist)
playlist' (with-redefs [shuffle reverse] (playlist/set-current-song 0))]
(playlist/previous-song playlist))] (is (= (playlist/current-song playlist)
(is (not= (map :playlist/order (:items playlist)) (map :playlist/order (:items playlist')))))))) (playlist/current-song next-playlist)))))))
(deftest set-current-song (deftest set-current-song
(testing "Should correctly set the new song" (testing "Should correctly set the new song"
(doseq [repeat-mode [:repeat-all :repeat-none]]
(let [queue (song-queue 3) (let [queue (song-queue 3)
playlist (playlist/->playlist queue :playback-mode :shuffled :repeat-mode :repeat-single) playlist (playlist/->playlist queue :playback-mode :shuffled :repeat-mode repeat-mode)
current-track (first queue)
next-track (-> (playlist/set-current-song playlist 1) next-track (-> (playlist/set-current-song playlist 1)
(playlist/peek))] (playlist/current-song))]
(is (not (nil? next-track))) (is (not (nil? next-track)))
(is (not (same-song? current-track next-track)))))) (is (not (same-song? (playlist/current-song playlist)
next-track)))))))
(deftest enqueue-last (deftest enqueue-last
(testing "Should make sure the song is played last" (testing "Should make sure the song is played last"
@ -223,12 +231,12 @@
(playlist/->playlist queue :playback-mode playback-mode :repeat-mode repeat-mode)) (playlist/->playlist queue :playback-mode playback-mode :repeat-mode repeat-mode))
played-back (->> (iterate playlist/next-song playlist) played-back (->> (iterate playlist/next-song playlist)
(take (dec length)) (take (dec length))
(map #(:id (playlist/peek %))) (map #(:id (playlist/current-song %)))
(set)) (set))
to-enqueue (song) to-enqueue (song)
playlist' (playlist/enqueue-last playlist to-enqueue)] playlist' (playlist/enqueue-last playlist to-enqueue)]
(is (nil? (played-back (-> (->> (iterate playlist/next-song playlist') (is (nil? (played-back (-> (->> (iterate playlist/next-song playlist')
(map playlist/peek)) (map playlist/current-song))
(nth length) (nth length)
(:id)))) (:id))))
(str "for " playback-mode ", " repeat-mode))))) (str "for " playback-mode ", " repeat-mode)))))
@ -240,7 +248,7 @@
played-back-songs (fn played-back-songs [playlist] played-back-songs (fn played-back-songs [playlist]
(->> (iterate playlist/next-song playlist) (->> (iterate playlist/next-song playlist)
(take length) (take length)
(map playlist/peek) (map playlist/current-song)
(map :playlist/order))) (map :playlist/order)))
played-back (played-back-songs playlist) played-back (played-back-songs playlist)
played-back' (played-back-songs (playlist/enqueue-last playlist (song)))] played-back' (played-back-songs (playlist/enqueue-last playlist (song)))]
@ -256,4 +264,4 @@
next-song (song)] next-song (song)]
(is (same-song? next-song (-> (playlist/enqueue-next playlist next-song) (is (same-song? next-song (-> (playlist/enqueue-next playlist next-song)
(playlist/next-song) (playlist/next-song)
(playlist/peek)))))))) (playlist/current-song))))))))

View file

@ -2,17 +2,6 @@
(:require [cljs.test :refer [deftest testing is]] (:require [cljs.test :refer [deftest testing is]]
[airsonic-ui.helpers :as helpers])) [airsonic-ui.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))))))
(deftest add-classes (deftest add-classes
(testing "Should add classes to a simple hiccup keyword" (testing "Should add classes to a simple hiccup keyword"
(is (= :div.foo (helpers/add-classes :div :foo))) (is (= :div.foo (helpers/add-classes :div :foo)))
@ -43,9 +32,3 @@
(is (= "59:00" (helpers/format-duration (* 59 60) :brief? true))) (is (= "59:00" (helpers/format-duration (* 59 60) :brief? true)))
(is (= "01:00" (helpers/format-duration 60 :brief? true))) (is (= "01:00" (helpers/format-duration 60 :brief? true)))
(is (= "00:05" (helpers/format-duration 5 :brief? true))))) (is (= "00:05" (helpers/format-duration 5 :brief? true)))))
(deftest vector-move
(testing "Should correctly move items in a vector"
(is (= [1] (helpers/vector-move [1] 0 0)))
(is (= [2 1] (helpers/vector-move [1 2] 0 1) (helpers/vector-move [1 2] 1 0)))
(is (= [1 4 2 3 5] (helpers/vector-move [1 2 3 4 5] 3 1)))))