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

Merge incomplete podcast support

commit 4ac35d6530f7770e7b80307321c72541a55e2c8e
Author: Arne Schlüter <arne@schlueter.is>
Date:   Mon Oct 8 21:09:04 2018 +0200

    Stub out podcast detail view

commit 60742a22e93bfe6f432e06d56d3e4da671184559
Author: Arne Schlüter <arne@schlueter.is>
Date:   Tue Sep 18 23:02:39 2018 +0200

    Simplify api helpers; closes #16

commit 8bbc79ebf4dbbe3dbfa08cb4c7c1edd341d507eb
Author: Arne Schlüter <arne@schlueter.is>
Date:   Tue Sep 18 19:39:17 2018 +0200

    Adjust `stream-url` to work with podcast episodes

commit 991ba5b65230a7429c160ca1b7968ecbb8595e0b
Author: Arne Schlüter <arne@schlueter.is>
Date:   Tue Sep 18 19:14:08 2018 +0200

    Fix breadcrumbs for podcasts

commit 37c3a894eded2fe37f9af031d3132c7175702266
Author: Arne Schlüter <arne@schlueter.is>
Date:   Tue Sep 18 15:11:54 2018 +0200

    Stub out overview for podcasts
This commit is contained in:
Arne Schlüter 2018-10-08 21:15:29 +02:00
commit fa485bbf42
19 changed files with 350 additions and 133 deletions

View file

@ -6,12 +6,6 @@
[ajax.core :as ajax]
[airsonic-ui.api.helpers :as api]))
(defn- api-url
"Small helper function which makes constructing API URLs a bit easier"
[db endpoint params]
(let [creds (:credentials db)]
(api/url (:server creds) endpoint (merge params (select-keys creds [:u :p])))))
(defn- cache-path [endpoint params] [:api/responses [endpoint params]])
(defn api-request
@ -19,7 +13,7 @@
current app state."
[{:keys [db]} [_ endpoint params]]
{:http-xhrio {:method :get
:uri (api-url db endpoint params)
:uri (api/url (:credentials db) endpoint params)
:response-format (ajax/json-response-format {:keywords? true})
:on-success [:api/good-response endpoint params]
:on-failure [:api/failed-response endpoint params]}

View file

@ -6,22 +6,25 @@
:c "airsonic-ui-cljs"
:v "1.15.0"})
(defn- encode [c]
(js/encodeURIComponent c))
(def ^:private encode js/encodeURIComponent)
(defn url
"Returns an absolute url to an API endpoint"
[server endpoint params]
(let [query (->> (merge default-params params)
[credentials endpoint params]
(let [server (:server credentials)
query (->> (merge default-params (select-keys credentials [:u :p]) params)
(map (fn [[k v]] (str (encode (name k)) "=" (encode v))))
(str/join "&"))]
(str server (when-not (str/ends-with? server "/") "/") "rest/" endpoint "?" query)))
(defn song-url [server credentials song]
(url server "stream" (merge (select-keys song [:id]) credentials)))
(defn stream-url [credentials song-or-episode]
;; podcasts have a stream-id, normal songs just use their id
(let [params {:id (or (:streamId song-or-episode)
(:id song-or-episode))}]
(url credentials "stream" params)))
(defn cover-url [server credentials item size]
(url server "getCoverArt" (merge {:id (:coverArt item) :size size} credentials)))
(defn cover-url [credentials item size]
(url credentials "getCoverArt" {:id (:coverArt item) :size size}))
(defn is-error? [response]
(= "failed" (get-in response [:subsonic-response :status])))

View file

@ -28,12 +28,12 @@
(re-frame/reg-fx
:audio/play
(fn [song-url]
(fn [stream-url]
(when-not @audio
(reset! audio (js/Audio.))
(attach-listeners! @audio))
(.pause @audio)
(set! (.-src @audio) song-url)
(set! (.-src @audio) stream-url)
(.play @audio)))
(re-frame/reg-fx

View file

@ -3,17 +3,13 @@
[airsonic-ui.audio.playlist :as playlist]
[airsonic-ui.api.helpers :as api]))
(defn- song-url [db song]
(let [creds (:credentials db)]
(api/song-url (:server creds) (select-keys creds [:u :p]) song)))
(re-frame/reg-event-fx
; sets up the db, starts to play a song and adds the rest to a playlist
:audio-player/play-all
(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 (song-url db (playlist/peek playlist))
{:audio/play (api/stream-url (:credentials db) (playlist/peek playlist))
:db (assoc-in db [:audio :playlist] playlist)})))
;; FIXME: :audio/play might not get the right argument here
@ -34,7 +30,7 @@
(let [db (update-in db [:audio :playlist] playlist/next-song)
next (playlist/peek (get-in db [:audio :playlist]))]
{:db db
:audio/play (song-url db next)})))
:audio/play (api/stream-url (:credentials db) next)})))
(re-frame/reg-event-fx
:audio-player/previous-song
@ -42,7 +38,7 @@
(let [db (update-in db [:audio :playlist] playlist/previous-song)
prev (playlist/peek (get-in db [:audio :playlist]))]
{:db db
:audio/play (song-url db prev)})))
:audio/play (api/stream-url (:credentials db) prev)})))
(re-frame/reg-event-db
:audio-player/enqueue-next

View file

@ -22,17 +22,17 @@
year (conj [:li [icon :calendar] (str "Released in " year)]))))
(defn preview [album]
(defn album-card [album]
(let [{:keys [artist artistId name id]} album]
[card album
:url-fn #(url-for ::routes/album-view {:id id})
:url-fn #(url-for ::routes/album.detail {:id id})
:content [:div
;; link to album
[:div.title.is-5
[:a {:href (url-for ::routes/album-view {:id id})
[:a {:href (url-for ::routes/album.detail {:id id})
:title name} name]]
;; link to artist page
[:div.subtitle.is-6 [:a {:href (url-for ::routes/artist-view {:id artistId})
[:div.subtitle.is-6 [:a {:href (url-for ::routes/artist.detail {:id artistId})
:title artist} artist]]]]))
(defn listing [albums]
@ -40,7 +40,7 @@
[:div.columns.is-multiline.is-mobile
(for [[idx album] (map-indexed vector albums)]
^{:key idx} [:div.column.is-one-fifth-desktop.is-one-quarter-tablet.is-half-mobile
[preview album]])])
[album-card album]])])
(defn detail
"Lists all songs in an album"

View file

@ -0,0 +1,13 @@
(ns airsonic-ui.components.podcast.events)
(defn subscribe-to-channel
[db [_ channel-url]])
(defn delete-podcast-channel
[db [_ channel-id]])
(defn download-episode
[db [_ episode-id]])
(defn delete-episode
[db [_ episode-id]])

View file

@ -0,0 +1,52 @@
(ns airsonic-ui.components.podcast.subs
(:require [re-frame.core :refer [reg-sub]]))
;; this unwraps the api response into a collection
(reg-sub
::podcast.response
:<- [:api/response-for "getPodcasts"]
(fn [response]
(:channel response)))
(defn podcast-channels
"Given a podcast response, returns information about the channels that have
been subscribed to."
[response _]
(when response
(map #(dissoc % :episode) response)))
(reg-sub
::podcast.channels
:<- [::podcast.response]
podcast-channels)
(defn sorted-podcast-episodes
"Given a response of all podcasts, returns all episodes sorted by a given function"
[response [_ key-fn & {:keys [n reverse?]
:or {n 15
reverse? true}}]]
;; some podcasts have an :artist and some don't, we make sure all of them have one
(let [id->channel (into {} (map (juxt :id :title) response))]
(let [sorted (->> (mapcat :episode response)
(map (fn [episode]
(assoc episode :artist (id->channel (:channelId episode)))))
(sort-by (or key-fn identity)))]
(take n (if reverse? (reverse sorted) sorted)))))
(reg-sub
::podcast.all-episodes-by
:<- [::podcast.response]
sorted-podcast-episodes)
(defn podcast-detail
"Since there's no real detail request, this function provides some abstraction
for that in providing a lense to only the podcast with a specific channel-id."
[[response [_ params _]] _]
(let [channel-id (:id params)]
(first (filter #(= channel-id (:id %)) response))))
(reg-sub
::podcast.detail-from-route
:<- [::podcast.response]
:<- [:routes/current-route]
podcast-detail)

View file

@ -0,0 +1,83 @@
(ns airsonic-ui.components.podcast.views
(:require [re-frame.core :refer [subscribe]]
[airsonic-ui.helpers :refer [muted-dispatch]]
[airsonic-ui.routes :as routes :refer [url-for]]
[airsonic-ui.components.podcast.subs :as subs]
[airsonic-ui.views.cover :refer [cover card]]
[airsonic-ui.views.icon :refer [icon]]
[airsonic-ui.components.debug.views :refer [debug]]))
;; TODO: Implement detail pages for podcasts
;; TODO: Implement CRUD frontend for podcasts
;; TODO: Error handling for channels and episodes
(defn channel-card
"Displays the cover of a podcast and links to the podcasts detail page"
[channel]
[card channel
:url-fn #(url-for ::routes/podcast.detail {:id (:id channel)})
:content [:div.title.is-5
[:a {:href (url-for ::routes/podcast.detail {:id (:id channel)})
:title (:title channel)} (:title channel)]]])
(defn- channel-overview [channels]
[:div.columns.is-multiline.is-mobile
(for [[idx channel] (map-indexed vector channels)]
^{:key idx}
[:div.column.is-one-fifth-desktop.is-one-quarter-tablet.is-half-mobile
[channel-card channel]])])
(defn- episode-actions [episode]
(case (:status episode)
"completed"
[[:td>a {:title "Play next"
:href "#"
:on-click (muted-dispatch [:audio-player/enqueue-next episode])}
[icon :plus]]
[:td>a {:title "Play last"
:href "#"
:on-click (muted-dispatch [:audio-player/enqueue-last episode])}
[icon :caret-right]]]
"skipped" ;; FIXME: Show download button
[[:td] [:td]]))
(defn- episode-list [episodes]
[:table.table.is-striped.is-hoverable.is-fullwidth>tbody
(for [[idx episode] (map-indexed vector episodes)]
^{:key idx}
(into
[:tr
[:td.grow [:span
[:a {:href (url-for ::routes/podcast.detail {:id (:channelId episode)})}
(:artist episode)]
" - "
[:a {:title (:title episode)
:href "#"
:on-click (muted-dispatch [:audio-player/play-all episodes idx])}
(:title episode)]]]]
(episode-actions episode)))])
(defn detail
"Detail page for a single channel"
[_]
;; NOTE: This isn't especially pretty, but it works. The detail page can only
;; ever be displayed for the podcast the current route points to
(let [channel @(subscribe [::subs/podcast.detail-from-route])]
[:div
[:section.section>div.hero-body
[:div.container>article.media
[:div.media-left [cover channel 128]]
[:div.media-content
[:h2.title (:title channel)]
[:p (:description channel)]]]]
[:section.section>div.container [episode-list (:episode channel)]]]))
(defn overview
"All channels and most recently published shows"
[_]
(let [channels @(subscribe [::subs/podcast.channels])
episodes @(subscribe [::subs/podcast.all-episodes-by :created])]
[:section.section>div.container
[:h1.title "Subscriptions"]
[channel-overview channels]
[:h1.title "Latest Episodes"]
[episode-list episodes]]))

View file

@ -19,10 +19,11 @@
:default-value search-term
:placeholder "Search"}]]])))
(defn artist-results [{:keys [artist]}]
[:div.columns.is-multiline.is-mobile
(for [[idx artist] (map-indexed vector artist)]
(let [url #(url-for ::routes/artist-view (select-keys % [:id]))]
(let [url #(url-for ::routes/artist.detail (select-keys % [:id]))]
^{:key idx} [:div.column.is-2
[card artist
:url-fn url
@ -33,7 +34,7 @@
(defn album-results [{:keys [album]}]
[:div.columns.is-multiline.is-mobile
(for [[idx album] (map-indexed vector album)]
(let [url #(url-for ::routes/album-view (select-keys % [:id]))
(let [url #(url-for ::routes/album.detail (select-keys % [:id]))
title (str (:name album) " (" (:artist album) ")")]
^{:key idx} [:div.column.is-2 [card album
:url-fn url

View file

@ -67,9 +67,7 @@
the credentials when the request was successful."
[cofx [_ credentials]]
(assoc cofx :http-xhrio {:method :get
:uri (api/url (:server credentials) "getUser"
(merge (select-keys credentials [:u :p])
{:username (:u credentials)}))
:uri (api/url credentials "getUser" {:username (:u credentials)})
:response-format (ajax/json-response-format {:keywords? true})
:on-success [:credentials/authentication-response credentials]
:on-failure [:api/failed-response]})) ; <- we don't need endpoint and params here because the response is not cached

View file

@ -9,9 +9,11 @@
(r/router [["/" ::login]
["/library" ::library]
["/library/:criteria" ::library]
["/artist/:id" ::artist-view]
["/album/:id" ::album-view]
["/search" ::search]]))
["/artist/:id" ::artist.detail]
["/album/:id" ::album.detail]
["/search" ::search]
["/podcast" ::podcast.overview]
["/podcast/:id" ::podcast.detail]]))
;; use this in views to construct a url
(defn url-for
@ -20,15 +22,20 @@
([k params query] (str "#" (r/resolve router k params query))))
;; which routes need valid login credentials?
(def protected-routes #{::library ::artist-view ::album-view ::search})
(def protected-routes #{::library ::artist.detail ::album.detail ::search
::podcast.overview ::podcast.detail})
;; which data should be requested for which route? can either be a vector or a function returning a vector
;; TODO: It's not so nice to have this all so close to the routing logic;
;; it would be nicer to abstract this away, so the components themselves
;; could tell what kind of events they expect
(defmulti -route-events
"Returns the events that take care of correct data being fetched."
(fn [route-id & _] route-id))
(defmethod -route-events :default [route-id params query] nil)
(defmethod -route-events :default [route-id params query])
(defmethod -route-events ::library
[route-id {:keys [criteria]} {:keys [page]}]
@ -37,13 +44,13 @@
[:api/request "getAlbumList2" {:type criteria, :size 20, :offset (* 20 (dec page))}]]
[:routes/do-navigation [route-id {:criteria "recent"} {:page 1}]]))
(defmethod -route-events ::artist-view
(defmethod -route-events ::artist.detail
[route-id params query]
(let [params (select-keys params [:id])]
[[:api/request "getArtist" params]
[:api/request "getArtistInfo2" params]]))
(defmethod -route-events ::album-view
(defmethod -route-events ::album.detail
[route-id params query]
[:api/request "getAlbum" (select-keys params [:id])])
@ -52,6 +59,15 @@
[[:search/restore-term-from-param (:query query)]
[:api/request "search3" query]])
(defmethod -route-events ::podcast.overview
[route-id params query]
[[:api/request "getPodcasts"]])
(defmethod -route-events ::podcast.detail
[route-id params query]
;; this is identical to ::podcast.overview on purpose
[[:api/request "getPodcasts"]])
;; shouldn't need to change anything below
(defn- n-events?

View file

@ -80,12 +80,12 @@
(defn cover-url
"Provides a convenient way for views to get cover images so they don't have
to build them themselves and can live a simple and happy life."
[[{:keys [server u p]}] [_ song size]]
(api/cover-url server {:u u :p p} song size))
[credentials [_ song size]]
(api/cover-url credentials song size))
(reg-sub
::cover-url
(fn [_ _] [(subscribe [::credentials])])
:<- [::credentials]
cover-url)
;; user notifications

View file

@ -15,43 +15,62 @@
[airsonic-ui.components.search.views :as search]
[airsonic-ui.components.library.views :as library]
[airsonic-ui.components.artist.views :as artist]
[airsonic-ui.components.collection.views :as collection]))
[airsonic-ui.components.collection.views :as collection]
[airsonic-ui.components.podcast.views :as podcast]))
(def logo-url "./img/airsonic-light-350x100.png")
;; ---
;; top navigation
;; ---
(defonce navbar-active? (r/atom false))
(def toggle-navbar-active! #(swap! navbar-active? not))
(defn navbar-item [{:keys [href]} label]
[:a.navbar-item {:href href :on-click toggle-navbar-active!} label])
(defn navbar-dropdown
([label items] (navbar-dropdown label {} items))
([label label-opts items]
[:div.navbar-item.has-dropdown.is-hoverable
[:div.navbar-link label-opts label]
[:div.navbar-dropdown
(for [[idx [opts label]] (map-indexed vector items)]
^{:key (str "navbar-dropdown-" idx)}
[navbar-item
(merge {:on-click toggle-navbar-active!} opts)
label])]]))
(defn navbar-top
"Contains search, some navigational links and the logo"
[]
(let [active? (r/atom false)
toggle-active #(swap! active? not)
navbar-item (fn navbar-item [{:keys [href]} label]
[:a.navbar-item {:href href :on-click toggle-active} label])
user @(subscribe [:user/info])
(let [user @(subscribe [:user/info])
stream-role @(subscribe [:user/roles :stream])
podcast-role @(subscribe [:user/roles :podcast])
playlist-role @(subscribe [:user/roles :playlist])
share-role @(subscribe [:user/roles :share])
settings-role @(subscribe [:user/roles :settings])]
(fn []
[:nav.navbar.is-fixed-top.is-dark {:role "navigation", :aria-label "search and navigation"}
;; user is `nil` when we're not logged in, we can hide the extended navigation
[:div.navbar-brand
[:div.navbar-item>img {:src logo-url}]
[:div.navbar-burger.burger {:on-click toggle-active} (repeat 3 [:span])]]
[:div.navbar-burger.burger {:on-click toggle-navbar-active!}
(for [idx (range 3)] ^{:key (str "burger-" idx)} [:span])]]
(when user
[(if @active? :div.navbar-menu.is-active :div.navbar-menu)
[(if @navbar-active? :div.navbar-menu.is-active :div.navbar-menu)
[:div.navbar-start
[:div.navbar-item [search/form]]]
[:div.navbar-end
(when stream-role
[:div.navbar-item.has-dropdown.is-hoverable
[:div.navbar-link "Library"]
[:div.navbar-dropdown
[navbar-item {:href (url-for ::routes/library {:criteria "recent"})} "Recently played"]
[navbar-item {:href (url-for ::routes/library {:criteria "newest"})} "Newest additions"]
[navbar-item {:href (url-for ::routes/library {:criteria "starred"})} "Starred"]]])
[navbar-dropdown "Library"
[[{:href (url-for ::routes/library {:criteria "recent"})} "Recently played"]
[{:href (url-for ::routes/library {:criteria "newest"})} "Newest additions"]
[{:href (url-for ::routes/library {:criteria "starred"})} "Starred"]]])
(when podcast-role
[navbar-item {} "Podcasts"])
(let [podcast-url (url-for ::routes/podcast.overview)]
[navbar-dropdown "Podcast" {:href podcast-url}
[[{:href podcast-url} "Overview"]]]))
(when playlist-role
[navbar-item {} "Playlists"])
(when share-role
@ -60,13 +79,17 @@
[:div.navbar-link "More"]
[:div.navbar-dropdown.is-right
(when settings-role
[navbar-item "Settings"])
[navbar-item {} "Settings"])
[:a.navbar-item
{:on-click (fn [_]
(toggle-active)
(toggle-navbar-active!)
(dispatch [::events/logout]))
:href "#"}
(str "Logout (" (:username user) ")")]]]]])])))
(str "Logout (" (:username user) ")")]]]]])]))
;; ---
;; this is the section the user mainly interacts with
;; ---
(defn media-content
"Provides the complete UI to browse the media library, interact with search
@ -80,12 +103,17 @@
[breadcrumbs content]
(case route-id
::routes/library [library/main [route-id params query] content]
::routes/artist-view [artist/detail content]
::routes/album-view [collection/detail content]
::routes/search [search/results content])]
::routes/artist.detail [artist/detail content]
::routes/album.detail [collection/detail content]
::routes/search [search/results content]
::routes/podcast.overview [podcast/overview content]
::routes/podcast.detail [podcast/detail content])]
[audio-player]]))
(defn main-panel []
(defn main-panel
"The outermost wrapper; handles display of the login form if necessary,
makes the code in media-content a bit easier to follow"
[]
(let [notifications @(subscribe [::subs/notifications])
is-booting? @(subscribe [::subs/is-booting?])
[route-id params query] @(subscribe [:routes/current-route])]

View file

@ -12,6 +12,7 @@
#{:artist :artist-info} :artist
#{:album} :album
#{:search} :search
#{:podcasts} :podcast
:other-content))
(defn- bulma-breadcrumbs [& items]
@ -26,18 +27,20 @@
(defmethod breadcrumbs :default [content]
[bulma-breadcrumbs "Start"])
(def start [(url-for ::routes/library) "Start"])
(defmethod breadcrumbs :artist [{:keys [artist]}]
[bulma-breadcrumbs
[(url-for ::routes/library) "Start"]
[bulma-breadcrumbs start
(:name artist)])
(defmethod breadcrumbs :album [{:keys [album]}]
[bulma-breadcrumbs
[(url-for ::routes/library) "Start"]
[(url-for ::routes/artist-view {:id (:artistId album)}) (:artist album)]
[bulma-breadcrumbs start
[(url-for ::routes/artist.detail {:id (:artistId album)}) (:artist album)]
(:name album)])
(defmethod breadcrumbs :search [_]
[bulma-breadcrumbs
[(url-for ::routes/library) "Start"]
"Search"])
[bulma-breadcrumbs start "Search"])
(defmethod breadcrumbs :podcast [{:keys [channel]}]
;; TODO: Detail view
[bulma-breadcrumbs start "Podcasts"])

View file

@ -7,7 +7,7 @@
(let [artist-id (:artistId song)]
[:div
(if artist-id
[:a {:href (url-for ::routes/artist-view {:id artist-id})} (:artist song)]
[:a {:href (url-for ::routes/artist.detail {:id artist-id})} (:artist song)]
(:artist song))
" - "
[:a

View file

@ -132,7 +132,8 @@
.preview-card .card-content
padding: 0.375rem 0.75rem 0.75rem
.album-view
.album
&.detail
.collection-info
list-style: none
@ -159,3 +160,5 @@
content: counter(track)
display: inline
padding-right: 0.375rem
.no-wrap

View file

@ -10,12 +10,12 @@
(api/url server endpoint {}))
(def fixtures
{:default-url (url "http://localhost:8080" "ping")})
{:default-url (url {:server "http://localhost:8080"} "ping")})
(deftest general-url-construction
(testing "Handles missing slashes"
(is (true? (str/starts-with? (fixtures :default-url) "http://localhost:8080/rest/ping")))
(is (true? (str/starts-with? (url "http://localhost:8080/" "ping") "http://localhost:8080/rest/ping"))))
(is (true? (str/starts-with? (url {:server "http://localhost:8080"} "ping") "http://localhost:8080/rest/ping")))
(is (true? (str/starts-with? (url {:server "http://localhost:8080/"} "ping") "http://localhost:8080/rest/ping"))))
(testing "Should set correct default parameters"
(is (string? (re-find #"f=json" (fixtures :default-url))))
(is (string? (re-find #"v=1\.15\.0" (fixtures :default-url))))))
@ -24,19 +24,22 @@
(testing "Should escape url parameters"
(let [query "äöüß"
encoded-str (js/encodeURIComponent query)]
(is (str/includes? (api/url "http://localhost" "search3" {:query query}) encoded-str)))))
(is (str/includes? (api/url {:server "http://localhost"} "search3" {:query query}) encoded-str)))))
(deftest song-urls
(deftest stream-urls
(testing "Should construct the url based on a song's id"
(let [song {:id 1234}]
(is (true? (str/includes? (api/song-url "http://localhost" {} song) (str "id=" (:id song))))))))
(let [stream-url (api/stream-url {:server "http://localhost"} fixtures/song)]
(is (str/includes? stream-url (str "id=" (:id fixtures/song))))))
(testing "Should also work for podcasts"
(let [stream-url (api/stream-url {:server "http://localhost"} fixtures/podcast-episode)]
(is (str/includes? stream-url (str "id=" (:streamId fixtures/podcast-episode)))))))
(deftest cover-urls
(let [album {:coverArt "cover-99999"}]
(testing "Should construct the url based on an item's cover-id"
(is (true? (str/includes? (api/cover-url "http://server.tld" {} album -1) (str "id=" (:coverArt album))))))
(is (true? (str/includes? (api/cover-url {:server "http://server.tld"} album -1) (str "id=" (:coverArt album))))))
(testing "Should scale an image to a given size"
(is (true? (str/includes? (api/cover-url "http://server.tld" {} album 48) "size=48"))))))
(is (true? (str/includes? (api/cover-url {:server "http://server.tld"} album 48) "size=48"))))))
(deftest response-handling
(testing "Should unwrap responses"

View file

@ -86,3 +86,30 @@
:paused? false
:current-src "https://londe.arnes.space/rest/stream?f=json&c=airsonic-ui-cljs&v=1.15.0&id=9574&u=arne&p=27h-%25bO%5B8-.ys%40SQ%7Bg%24-%5B5NZkX%7Dw%24NNwY%263DPATi%2CgaFoH%40e"
:current-time 3.477029})
(def podcast-episode
{:genre "Vocal",
:description
"Themen der Sendung: Druck auf Maaßen nach Äußerungen zu Chemnitz wächst, Köthen: 22-Jähriger stirbt nach Streit an Herzversagen, Parlamentswahl in Schweden, Russland und Syrien setzen Luftangriffe auf syrische Provinz Idlib fort, Tote und Verletzte bei Ausschreitungen im irakischem Basra, Nordkorea feiert 70. Jubiläum seiner Staatsgründung, Zahl der Toten nach Erdbeben in Japan steigt auf 39, Pläne von CDU und CSU: Fluggesellschaften sollen Auskunft über Verspätungen geben, Menschenkette in Dangast als Zeichen gegen Flüchtlingssterben im Mittelmeer, Das Wetter",
:suffix "mp3",
:isDir false,
:bitRate 64,
:parent "10409",
:channelId "4",
:type "podcast",
:created "2018-09-09T19:41:13.000Z",
:duration 965,
:artist "Tagesschau (Audio-Podcast)",
:isVideo false,
:publishDate "2018-09-09T18:00:00.000Z",
:size 7812758,
:title "09.09.2018 - tagesschau 20:00 Uhr",
:playCount 0,
:year 2018,
:streamId "11181",
:status "completed",
:id "507",
:coverArt "10409",
:contentType "audio/mpeg",
:album "tagesschau",
:track 1})

View file

@ -31,11 +31,8 @@
:u "test-user"
:p "some-random-password"}]
(testing "Should give the correct path once the credentials are set"
(is (= (api/cover-url (:server credentials)
(select-keys credentials [:u :p])
fixtures/song
48)
(subs/cover-url [credentials] [:subs/cover-image fixtures/song 48]))))))
(is (= (api/cover-url credentials fixtures/song 48)
(subs/cover-url credentials [:subs/cover-image fixtures/song 48]))))))
(def successful-auth-db
"For the details see event_test.cljs"