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

@ -6,8 +6,6 @@
[airsonic-ui.audio.playlist :as playlist]
[goog.functions :refer [throttle]]))
;; TODO: Manage buffering
(defonce audio (atom nil))
(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
(defn attach-listeners! [el]
(let [emit-audio-update (throttle #(rf/dispatch [:audio/update (->status el)]) 16)]
(doseq [event ["loadstart" "progress" "play" "timeupdate" "pause" "volumechange"]]
@ -116,7 +113,9 @@
"Gives us information about the currently played song as presented by
the airsonic api"
[playlist _]
(playlist/peek playlist))
(when-not (empty? playlist)
(playlist/current-song playlist)))
(rf/reg-sub
:audio/current-song

View file

@ -1,139 +1,130 @@
(ns airsonic-ui.audio.playlist
"Implements playlist queues that support different kinds of repetition and
song ordering."
(:refer-clojure :exclude [peek])
(:require [airsonic-ui.helpers :refer [find-where]]))
song ordering.")
(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
(-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
"Creates a new playlist that behaves according to the given playback- and
repeat-mode parameters."
(fn [queue & {:keys [playback-mode #_repeat-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)))
(fn [_ & {:keys [playback-mode]}] playback-mode))
(defmethod ->playlist :linear
[queue & {:keys [playback-mode repeat-mode]}]
(let [queue (-> (mapv (fn [order song] (assoc song :playlist/order order)) (range) queue)
(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)))
[items & {:keys [playback-mode repeat-mode]}]
(->Playlist (linear-queue items) 0 playback-mode repeat-mode))
(defmethod ->playlist :shuffled
[queue & {:keys [playback-mode repeat-mode]}]
(let [queue (conj (mapv #(update % :playlist/order inc) (-shuffle-songs (rest queue)))
(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)))))))))
[items & {:keys [playback-mode repeat-mode]}]
(->Playlist (shuffled-queue items) 0 playback-mode repeat-mode))

View file

@ -9,7 +9,7 @@
(fn [{:keys [db]} [_ songs start-idx]]
(let [playlist (-> (playlist/->playlist songs :playback-mode :linear :repeat-mode :repeat-all)
(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)})))
(rf/reg-event-db
@ -26,7 +26,7 @@
:audio-player/next-song
(fn [{:keys [db]} _]
(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
:audio/play (api/stream-url (:credentials db) next)})))
@ -34,7 +34,7 @@
:audio-player/previous-song
(fn [{:keys [db]} _]
(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
:audio/play (api/stream-url (:credentials db) prev)})))
@ -65,7 +65,7 @@
(rf/reg-event-fx
:audio-player/seek
(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]})))
(rf/reg-event-fx

View file

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