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:
parent
c715e5025c
commit
644939c618
9 changed files with 206 additions and 229 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`."
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue