diff --git a/.joker b/.joker index 8d9e936..e0557b9 100644 --- a/.joker +++ b/.joker @@ -1 +1,2 @@ -{:known-macros [cljs.test/deftest]} \ No newline at end of file +{:known-macros [cljs.test/deftest] + :known-namespaces [cljs.core]} \ No newline at end of file diff --git a/src/cljs/airsonic_ui/audio.cljs b/src/cljs/airsonic_ui/audio/core.cljs similarity index 92% rename from src/cljs/airsonic_ui/audio.cljs rename to src/cljs/airsonic_ui/audio/core.cljs index 297062f..a1747e5 100644 --- a/src/cljs/airsonic_ui/audio.cljs +++ b/src/cljs/airsonic_ui/audio/core.cljs @@ -1,4 +1,7 @@ -(ns airsonic-ui.audio +(ns airsonic-ui.audio.core + "This namespace contains some JS interop code to interact with an audio player + and receive information about the current playback status so we can use it in + our re-frame app." (:require [re-frame.core :as re-frame])) ;; TODO: Manage buffering diff --git a/src/cljs/airsonic_ui/audio/playlist.cljs b/src/cljs/airsonic_ui/audio/playlist.cljs new file mode 100644 index 0000000..3f842ff --- /dev/null +++ b/src/cljs/airsonic_ui/audio/playlist.cljs @@ -0,0 +1,60 @@ +(ns airsonic-ui.audio.playlist + "Implements playlist queues that support different kinds of repetition and + song ordering." + (:refer-clojure :exclude [peek])) + +(defrecord Playlist [queue playback-mode repeat-mode] + cljs.core/ICounted + (-count [this] + (count (:queue this)))) + +(defmulti ->playlist + "Creates a new playlist that behaves according to the given playback- and + repeat-mode parameters." + (fn [queue play-idx playback-mode repeat-mode] playback-mode)) + +(defn- playlist-queue + [queue play-idx] + (concat (take play-idx queue) + [(assoc (nth queue play-idx) :currently-playing? true)] + (drop (inc play-idx) queue))) + +(defmethod ->playlist + :playback-mode/linear + [queue play-idx playback-mode repeat-mode] + (let [queue (->> (playlist-queue queue play-idx) + (map (fn [order song] (assoc song :order order)) + (range (count queue))))] + (->Playlist queue playback-mode repeat-mode))) + +(defmethod ->playlist + :playback-mode/shuffle + [queue play-idx playback-mode repeat-mode] + (let [queue (->> (playlist-queue queue play-idx) + (map (fn [order song] (assoc song :order order)) + (shuffle (range (count queue)))))] + (->Playlist queue playback-mode repeat-mode))) + +(defn set-playback-mode + "Changes the playback mode of a playlist and re-suffles it if necessary" + [playlist playback-mode] + (let [current-idx (first (keep-indexed (fn [idx item] + (when (:currently-playing? item) + idx)) + (: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" + [playlist repeat-mode] + (assoc playlist :repeat-mode repeat-mode)) + +(defn peek + "Returns the song in a playlist that is currently playing" + [playlist] + (->> (:queue playlist) + (filter :currently-playing?) + (first))) + +(defmulti next-song (juxt :playback-mode :repeat-mode)) +(defmulti previous-song (juxt :playback-mode :repeat-mode)) diff --git a/src/cljs/airsonic_ui/core.cljs b/src/cljs/airsonic_ui/core.cljs index d61493e..4bb702d 100644 --- a/src/cljs/airsonic_ui/core.cljs +++ b/src/cljs/airsonic_ui/core.cljs @@ -5,7 +5,7 @@ [day8.re-frame.http-fx] [akiroz.re-frame.storage :as storage] ;; our app - [airsonic-ui.audio] ; <- just registers effects here + [airsonic-ui.audio.core] ; <- just registers effects here [airsonic-ui.events :as events] [airsonic-ui.views :as views] [airsonic-ui.config :as config])) diff --git a/test/cljs/airsonic_ui/audio/core_test.cljs b/test/cljs/airsonic_ui/audio/core_test.cljs new file mode 100644 index 0000000..4866db4 --- /dev/null +++ b/test/cljs/airsonic_ui/audio/core_test.cljs @@ -0,0 +1,23 @@ +(ns airsonic-ui.audio.core-test + (:require [airsonic-ui.audio.core :as audio] + [airsonic-ui.audio.playlist-test :as p] + [airsonic-ui.fixtures :as fixtures] + [cljs.test :refer [deftest testing is]])) + +(enable-console-print!) + +(deftest current-song-subscription + (letfn [(current-song [db] + (-> (audio/summary db [:audio/summary]) + (audio/current-song [:audio/current-song])))] + (testing "Should provide information about the song" + (= fixtures/song (current-song p/fixture))))) + +(deftest playback-status-subscription + (letfn [(is-playing? [playback-status] + (audio/is-playing? playback-status [:audio/is-playing?]))] + (testing "Should be shown as not playing when the song is paused or has ended" + (is (not (is-playing? {:paused? true, :ended? false}))) + (is (not (is-playing? {:paused? false, :ended? true})))) + (testing "Should be shown as playing when the song is not paused or finished" + (is (is-playing? {:paused? false, :ended? false}))))) diff --git a/test/cljs/airsonic_ui/audio/playlist_test.cljs b/test/cljs/airsonic_ui/audio/playlist_test.cljs new file mode 100644 index 0000000..d03d22d --- /dev/null +++ b/test/cljs/airsonic_ui/audio/playlist_test.cljs @@ -0,0 +1,103 @@ +(ns airsonic-ui.audio.playlist-test + (:require [cljs.test :refer [deftest testing is]] + [airsonic-ui.audio.playlist :as playlist] + [airsonic-ui.fixtures :as fixtures] + [airsonic-ui.test-helpers :as helpers])) + +(enable-console-print!) + +(defn- playing-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) + :title (helpers/rand-str) + :album (helpers/rand-str)))) + +(def fixture + {:audio {:current-song fixtures/song + :playlist (playing-queue 20) + :playback-status fixtures/playback-status}}) + +(defn- same-song? [a b] (= (:id a) (:id b))) + +(deftest playlist-creation + (testing "Playlist creation" + (testing "should give us the correct current song" + (let [queue (playing-queue 10) + start-idx (rand-int 10)] + (doseq [playback-mode [:playback-mode/linear, :playback-mode/shuffle] + repeat-mode [:repeat-mode/none, :repeat-mode/single, :repeat-mode/all]] + (is (same-song? (nth queue start-idx) + (-> (playlist/->playlist queue start-idx playback-mode repeat-mode) + (playlist/peek))) + (str playback-mode ", " repeat-mode))))) + (testing "should give us a playlist with the correct number of tracks" + (let [queue (playing-queue 100) + start-idx (rand-int 100)] + (doseq [playback-mode [:playback-mode/linear, :playback-mode/shuffle] + repeat-mode [:repeat-mode/none, :repeat-mode/single, :repeat-mode/all]] + (is (= (count queue) + (count (playlist/->playlist queue start-idx playback-mode repeat-mode))) + (str playback-mode ", " repeat-mode))))))) + +(deftest changing-playback-mode + (testing "Changing playback mode" + (testing "from linear to shuffled" + (let [queue (playing-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/shuffle)] + (testing "should re-order the tracks" + (is (not= (map :order (:queue shuffled)) (map :order (:queue linear))))) + (testing "should not change the currently playing track" + (is (same-song? (playlist/peek linear) (playlist/peek shuffled)))) + (testing "should not change the repeat mode" + (is (= (:repeat-mode shuffled) (:repeat-mode linear))))) + (testing "from shuffled to linear" + (let [queue (playing-queue 10) + start-idx (rand-int 10) + shuffled (playlist/->playlist queue start-idx :playback-mode/shuffle :repeat-mode/none) + linear (playlist/set-playback-mode shuffled :playback-mode/linear)] + (testing "should set the correct order for tracks" + (is (every? #(apply same-song? %) + (interleave (take start-idx (:queue shuffled)) + (take start-idx (:queue linear)))) + "before") + (is (every? #(apply same-song? %) + (interleave (drop (inc start-idx) (:queue shuffled)) + (drop (inc start-idx) (:queue linear)))) + "after") + (is (< (:order (first (:queue linear))) (:order (last (:queue linear)))))) + (testing "should not change the currently playing track" + (is (same-song? (playlist/peek linear) (playlist/peek shuffled)))) + (testing "should not change the repeat mode" + (is (= (:repeat-mode shuffled) (:repeat-mode linear))))))))) + +(deftest chaging-repeat-mode + (testing "Changing the repeat mode" + (testing "should not change the playback mode" + (doseq [playback-mode [:playback-mode/linear, :playback-mode/shuffle] + 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 playback-mode repeat-mode) + (playlist/set-repeat-mode next-repeat-mode))] + (is (= playback-mode (:playback-mode playlist))) + (is (= next-repeat-mode (:repeat-mode playlist)) + (str "from " repeat-mode " to " next-repeat-mode))))))) + +(deftest linear-next-song + (testing "Should follow the same order as the queue used for creation") + (testing "Should go back to the first song when repeat-mode is all") + (testing "Should always give the same track when repeat-mode is single")) + +(deftest shuffled-next-song + (testing "Should play every track once when called for the entire queue") + (testing "Should re-shuffle the playlist when wrapping around and repeat-mode is all") + (testing "Should always give the same track when repeat-mode is single")) + +#_(deftest linear-previous-song) +#_(deftest shuffled-previous-song) diff --git a/test/cljs/airsonic_ui/audio_test.cljs b/test/cljs/airsonic_ui/audio_test.cljs deleted file mode 100644 index 2663774..0000000 --- a/test/cljs/airsonic_ui/audio_test.cljs +++ /dev/null @@ -1,40 +0,0 @@ -(ns airsonic-ui.audio-test - (:require [airsonic-ui.audio :as audio] - [airsonic-ui.fixtures :as fixtures] - [airsonic-ui.test-helpers :as helpers] - [cljs.test :refer [deftest testing is]])) - -(enable-console-print!) - -(defn- simulate-playlist [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) - :title (helpers/rand-str) - :album (helpers/rand-str)))) - -(def fixture - {:audio {:current-song fixtures/song - :playlist (simulate-playlist 20) - :playback-status fixtures/playback-status}}) - -(deftest current-song - (letfn [(current-song [db] - (-> (audio/summary db [:audio/summary]) - (audio/current-song [:audio/current-song])))] - (testing "Should provide information about the song" - (= fixtures/song (current-song fixture))))) - -(deftest playback-status - (letfn [(is-playing? [playback-status] - (audio/is-playing? playback-status [:audio/is-playing?]))] - (testing "Should be shown as not playing when the song is paused or has ended" - (is (not (is-playing? {:paused? true, :ended? false}))) - (is (not (is-playing? {:paused? false, :ended? true})))) - (testing "Should be shown as playing when the song is not paused or finished" - (is (is-playing? {:paused? false, :ended? false}))))) - -#_(deftest current-playlist - (testing "Should show the complete playlist"))