Liking cljdoc? Tell your friends :D

Quickstart: a podcast app at a REPL

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 library

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.

The episodes

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.

An episode page

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 player

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.

New episodes

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.

The other devices

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.

What next

  • How to build a podcast app: every part of an app, with the lower-level functions for more control.
  • Principles: what guides the design of podcast-clj.
  • Namespaces: a line for each namespace.
  • Look at (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

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close