This page goes from a feed URL to a queue, an episode page, a player and a sync, at a REPL on the JVM. Each step is one call that returns data. How to build a podcast app explains each call in more depth, and what to use when you need more control. The feed is live, so your outputs will show newer episodes than these.
Add the latest release of podcast-clj from
Clojars to your deps.edn:
dk.simongray/podcast-clj {:mvn/version "0.1.0"}
The subscriptions, feeds, queue and play progress of an app are all kept in one map, the library. Make one for this device, keep it in an atom, and subscribe to a show:
(require '[dk.simongray.like-minded.store.folder :as folder]
'[dk.simongray.podcast :as podcast]
'[dk.simongray.podcast.app.library :as library]
'[dk.simongray.podcast.profile.queue :as profile.queue])
(def url "https://feeds.podcastindex.org/pc20.xml")
(def lib (atom (library/make "laptop" {:name "Laptop"})))
(-> (swap! lib library/subscribe url (podcast/fetch! url) {:auto-queue? true})
library/subscriptions
keys)
;; => ("917393e3-1b1e-5cef-ace4-edaa54e1f810")
Here the id of the device is "laptop". In an app, make one id per
installation and keep it. The call to podcast/fetch! fetched and parsed
the feed. Then subscribe kept the feed along with its ETag and
Last-Modified, added the subscription, and named the show by its guid. The
subscription's setting :auto-queue? puts the show's new episodes in the
listener's queue. Each swap! returns the whole library, with the feed in it,
so the examples look only at the part that a step changed.
Change the library with swap! and the functions of app.library and
profile, but never assoc into :profile. The profile is a CRDT, so
that the listener's devices can merge their changes in any order: each
write stamps what it changed with the time and the device, the later of
two changes wins, and a removal is a tombstone. Syncing
explains how. To keep the library until the next start, save it into a
store with library/save!, and read it back with library/restore!, as
How to build a podcast app
shows.
Pick episodes across all the shows, in the order you want:
(def episodes (library/episodes @lib {:order :newest :limit 3}))
(map (comp :rss/title second) episodes)
;; => ("Episode 274: \"Vibe Cloning\"" "Episode 273: \"Advertising is…\"" …)
(def id
(let [[show item] (first episodes)]
(library/episode-id @lib show item)))
id
;; => ["917393e3-1b1e-5cef-ace4-edaa54e1f810" "PC20-274"]
An episode is a [show item] pair: the feed and its item, as
rss/parse returns them. Its id in the profile is the guid of the show
and the guid of the item.
For an episode page, merge library/episode with library/read-files!,
which reads the episode's chapters file, transcript and media file:
(def page (merge (library/episode @lib id) (library/read-files! @lib id)))
(select-keys page [:title :duration])
;; => {:title "Episode 274: \"Vibe Cloning\"" :duration 6064.692244897959}
(:notes page) ; the show notes as Hiccup
(:source (:chapters page)) ; => :file
(count (:chapters (:document (:chapters page)))) ; => 21
(count (:segments (:document (:transcript page)))) ; => 1483
If a file can't be read, its part has an :error, and nothing is thrown.
For a list of episodes, use library/episode alone, which fetches nothing.
The profile holds the queue and the progress, and you control the media
element. Send each of its events to step, which returns the library
with the effects to apply under :effects:
(-> (swap! lib library/update-profile profile.queue/enqueue id)
:profile
profile.queue/queue)
;; => [["917393e3-1b1e-5cef-ace4-edaa54e1f810" "PC20-274"]]
(:effects (swap! lib library/step [:progress id 754 (:duration page)])) ; => []
(:effects (swap! lib library/step [:skip id :forward]))
;; => [[:seek ["917393e3-1b1e-5cef-ace4-edaa54e1f810" "PC20-274"] 784]]
The profile is (:profile @lib), which every function of profile
takes. To change it, library/update-profile calls one of them with the
time, like enqueue here.
To check for new episodes, call refresh!. It fetches whatever is due:
the feeds, the lists that the listener follows, and the feeds of new
subscriptions. The new episodes of a show with :auto-queue? go to the
queue. The listener can change the library while it fetches, so put the
result into the atom with library/merge, which keeps those changes:
(:refresh (swap! lib library/merge (library/refresh! @lib)))
;; => {:at #inst "…" :added [] :backfilled [] :renamed [] :moved {}
;; :counts {:checked 0 :unchanged 0 :changed 0 :failed 0 :left 0}
;; :seconds 0.001 :report []}
(library/next-refresh @lib)
;; => #inst "…" ; twelve hours later
Nothing is due right after subscribing, so in an app, call refresh! from
a timer, at the time that library/next-refresh returns. The result under
:refresh lists the new episodes, the feeds that moved, and the fetches
that failed. Once the show has a new episode, :added holds its id, e.g.
[["917393e3-…" "PC20-275"]], and the episode is in the queue. A feed
that failed stays as it was, and waits longer before the next try.
Each device writes its profile, a CRDT of
mind-meld, to a store as one
document, and reads the documents of the other devices. A folder that a
sync client mirrors is a store, and so is a WebDAV collection, reached
through webdav-clj. The stores
come from like-minded, which
podcast-clj syncs through and brings along. With library/backend, a
store becomes the backend of a sync, here with the app's name in its
document:
(def dropbox
(library/backend (folder/store "/Users/simon/Dropbox/Podcasts")
{:app "quickstart"}))
(-> (swap! lib library/merge (library/sync! @lib [dropbox]))
:sync
(select-keys [:at :report]))
;; => {:at #inst "…" :report []}
(library/next-sync @lib) ; when to sync again
The result under :sync has the cursors for the next sync and its report,
which names any backend that failed. The folder now holds one
document for this device, and another device that syncs through the same
folder gets the subscription and the queue. The providers
page
of like-minded lists where the common sync clients and WebDAV servers
keep their folders.
(keys @lib) and (:profile @lib) to see what the library
holds.Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |