Konubinix' opinionated web of thoughts

Some Simple Podcast Player

Fleeting

some simple podcast tool, very specialized to my stack.

For audiobooks, prefer using Voice Audiobook Player, much more attractive (I think).

<html>
  <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="manifest" href="./podcastmanifest.json">
    <script type="module" src="https://ipfs.konubinix.eu/bafkreiap4qt2hkswcidxgqnzdt4irhslfc6kbpa2bw2lscz7zsd3gwr7mu?filename=yjs.js"></script>
    <script type="module">
      import { Doc, WebsocketProvider, IndexeddbPersistence } from 'http://localhost:9682/ipfs/bafkreiap4qt2hkswcidxgqnzdt4irhslfc6kbpa2bw2lscz7zsd3gwr7mu?filename=yjs.js'

      const doc = new Doc();
      const ymap = doc.getMap("state")

      const wsProvider = new WebsocketProvider(
          'ws://192.168.2.14:9905', 'podcast',
          doc,
      )

      const indexeddb = new IndexeddbPersistence('podcast', doc)

      // I can export the Doc so that it will be used in the dom
      window.doc = doc
      window.ymap = ymap
      window.wsProvider = wsProvider
      window.indexeddb = indexeddb
    </script>
    <script defer src="https://ipfs.konubinix.eu/bafkreidregenuw7nhqewvimq7p3vwwlrqcxjcrs4tiocrujjzggg26gzcu?orig=https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js"></script>
    <script defer src="https://ipfs.konubinix.eu/bafkreighmyou4lhqizpdzvutdeg6xnpvskwhfxgez7tfawason3hkwfspm?orig=https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
    <script defer src="https://ipfs.konubinix.eu/bafkreic33rowgvvugzgzajagkuwidfnit2dyqyn465iygfs67agsukk24i?orig=https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
    <script src="https://ipfs.konubinix.eu/bafybeihp5kzlgqt56dmy5l4z7kpymfc4kn3fnehrrtr7cid7cn7ra36yha?orig=https://cdn.tailwindcss.com/3.4.3"></script>
    [[wip_ipfsdocs_slider_alpinejs.org:toast-code-ex()]]
    <script>
      if ('serviceWorker' in navigator) {
          navigator.serviceWorker
              .register('./swpodcast.js')
              .then(function() {
                  console.log('Service Worker Registered');
              })
              .catch(error => {
                  console.error('Service Worker registration failed:', error);
                  alert(`Service Worker registration failed: ${error}`);
              });
      }

      function formatTime(seconds) {
          // remove the milliseconds
          seconds = Math.floor(seconds)
          // Calculate hours, minutes, and remaining seconds
          let hours = Math.floor(seconds / 3600);
          let minutes = Math.floor((seconds % 3600) / 60);
          let remainingSeconds = seconds % 60;

          // Create an array to store parts of the time
          let parts = [];

          // Add hours if its greater than 0
          if (hours > 0) {
              parts.push(hours.toString().padStart(2, '0'));
          }

          // Always add minutes and seconds
          parts.push(minutes.toString().padStart(2, '0'));
          parts.push(remainingSeconds.toString().padStart(2, '0'));

          // Join the parts with ':' and return
          return parts.join(':');
      }

      document.addEventListener('alpine:init', () => {
          Alpine.data('app', () => ({
              src: "podcasts.json",
              async init_app () {
                  try{
                      wsProvider.on('status', event => {
                          info(`WS: ${event.status}`) ;
                      })

                      indexeddb.on('synced', () => {
                          info(`IDB: ready`) ;
                      })

                      wsProvider.on('sync', event => {
                          // something new from outside
                      });

                      ymap.observe(ymapEvent => {
                          // set the values, such as show_old,
                          window.event = ymapEvent
                          for(const keychanged of ymapEvent.keysChanged) {
                              // info(`Setting: ${keychanged}`)
                              this[keychanged] = ymap.get(keychanged)
                          }
                      });


                      // no-store because the browser cache will hide changes in
                      // the file.  This is dealt with by the service worker
                      // anyway
                      res = await fetch(`./${this.src}`, {cache: 'no-store'})
                      this.dirs = await res.json()
                      var lastpodcast
                      var lastdir
                      var keepcurrent = false
                      var keeplastdir
                      var keeplastpodcast

                      for(const dir in this.dirs) {
                          lastdir = this.dir_last_episode[dir]
                          keeplastdir = false
                          for(const podcast of this.dirs[dir]){
                              lastpodcast = this.podcast_last_episode[podcast.name]
                              keeplastpodcast = false
                              for(const episode of podcast.episodes){
                                  if(lastdir && episode.url === lastdir.episode.url){
                                      keeplastdir = true
                                  }
                                  if(lastpodcast && episode.url === lastpodcast.episode.url){
                                      keeplastpodcast = true
                                  }
                                  if(this.current_episode && this.current_episode.url === episode.url) {
                                      keepcurrent = true
                                  }
                              }
                              if(lastpodcast && !keeplastpodcast) {
                                  delete this.podcast_last_episode[podcast.name]
                                  this.podcast_last_episode = {...this.podcast_last_episode}
                              }
                          }
                          if(lastdir && !keeplastdir) {
                              delete this.dir_last_episode[dir]
                              this.dir_last_episode = {...this.dir_last_episode}
                          }

                      }
                      if(! keepcurrent) {
                          this.current_episode = null
                      }
                  }
                  catch(err)
                  {
                      error(err)
                  }
              },
              dirs: undefined,

              show_old: false, // Alpine.$persist(false).as("show_old"),

              podcast_last_episode: {}, // Alpine.$persist({}).as("podcast_last_episode3"),
              dir_last_episode: {}, // Alpine.$persist({}).as("dir_last_episode"),

              current_speed: null,
              current_progress: {cur: 0, duration: 0},
              current_dir: null, // Alpine.$persist(null).as("dir"),
              current_episode: null, // Alpine.$persist(null).as("episode"),
              current_podcast: null, // Alpine.$persist(null).as("podcast"),
              current_playing: false,
              sync_state: {}, // Alpine.$persist({}).as("sync_state2"),
              key(stuff) {
                  return `${stuff}_${this.current_episode.url}`
              },
              async goto_next() {
                  localStorage.setItem(this.key('time'), 0)
                  this.sync_state[this.key('time')] = 0
                  let pos_current_episode = this.current_podcast.episodes.findIndex(
                      (episode) => episode.url === this.current_episode.url
                  )
                  if(pos_current_episode + 1 === this.current_podcast.episodes.length) {
                      localStorage.setItem(this.current_podcast.name + "_looped_first_episode", this.current_podcast.episodes[0].url)
                      this.sync_state[this.current_podcast.name + "_looped_first_episode"] = this.current_podcast.episodes[0].url
                  }

                  this.current_episode = null
                  await this.$nextTick() // give time to the player to disappear
                  this.current_episode = this.current_podcast.episodes[(pos_current_episode + 1) % this.current_podcast.episodes.length]
                  await this.$nextTick() // give time to the player to appear
              },
              async goto_prev() {
                  localStorage.setItem(this.key('time'), 0)
                  this.sync_state[this.key('time')] = 0
                  let pos_current_episode = this.current_podcast.episodes.findIndex(
                      (episode) => episode.url === this.current_episode.url
                  )
                  this.current_episode = null
                  await this.$nextTick() // give time to the player to disapear
                  this.current_episode = this.current_podcast.episodes[(pos_current_episode - 1 + this.current_podcast.episodes.length) % this.current_podcast.episodes.length]
                  await this.$nextTick() // give time to the player to appear
              },
              localstoragegetter(key) {
                  //return localStorage.getItem(key)
                  return this.sync_state[key]
              }
          }))
      })
    </script>
  </head>
  <body x-data="app" x-init="init_app"

        x-effect.podcast_last_episode="ymap.set('podcast_last_episode', podcast_last_episode)"
        x-effect.dir_last_episode="ymap.set('dir_last_episode', dir_last_episode)"
        x-effect.show_old="ymap.set('show_old', show_old)"
        x-effect.current_episode="ymap.set('current_episode', current_episode)"
        x-effect.current_podcast="ymap.set('current_podcast', current_podcast)"
        x-effect.current_dir="ymap.set('current_dir', current_dir)"
        x-effect.sync-state="ymap.set('sync_state', sync_state)"
        @gotonext="
                   if($event.detail.playing){
                   current_playing = true
                   }
                   await goto_next()
                   "
        @gotoprev="
                   if($event.detail.playing){
                   current_playing = true
                   }
                   await goto_prev()
                   "
        >
    <div id="listings" class="h-screen flex flex-col">
      <div id="podcasts" class="flex-grow border-red-100 border-4 overflow-scroll"
           x-effect.save-old-podcats="
                                      if(current_podcast && current_episode && current_dir){
                                          let old = {...podcast_last_episode}
                                          old[current_podcast.name] = {episode: current_episode, dir: current_dir}
                                          podcast_last_episode = old

                                          old = {...dir_last_episode}
                                          old[current_dir] = {episode: current_episode, podcast: current_podcast}
                                          dir_last_episode = old
                                      }
                                      "
           >
        <template x-for="(podcasts, dir) in dirs">
          <div class="dir"
               x-data="{extended: false}">
            <h1
              :class="{
    'bg-yellow-100': current_dir === dir
}"
              class="rounded" @click="extended = !extended">
              <span class="w-8 inline-block text-right" x-text="extended ? '↓' : '→'"></span>
              <span class="text-2xl" x-text="dir"></span>
              <button class="text-sm"
                      @click.stop="
                                   current_podcast = podcasts[0]
                                   current_dir = dir
                                   current_episode = null // to trigger reload
                                   await $nextTick()
                                   current_episode = podcasts[0].episodes[0]
                                   localStorage.setItem(`time_${current_episode.url}`, 0)
                                   sync_state[`time_${current_episode.url}`] = 0
                                   await $nextTick() // wait for it to be ready
                                   $dispatch('pleaseplay')
                                   $dispatch('pleaserewind')">▶</button>
              <button class="text-sm" x-show="dir in dir_last_episode"
                      @click.stop="
                                   let podcast = dir_last_episode[dir].podcast
                                   let episode = dir_last_episode[dir].episode
                                   current_podcast = podcast
                                   current_dir = dir
                                   current_episode = null // to trigger reload
                                   await $nextTick()
                                   current_episode = episode
                                   await $nextTick() // wait for it to be ready
                                   $dispatch('pleaseplay')
                                   $dispatch('pleaserewind')"
                      >resume</button> <span class="text-xs" x-text="dir_last_episode[dir] && dir_last_episode[dir].podcast.name"></span>
            </h1>
            <div x-collapse x-show="extended" class="border-2 border-yellow-100 rounded">
              <template x-for="podcast in podcasts">
                <div class="podcast"
                     x-data="{extended: false}"
                     x-show="show_old || localstoragegetter(podcast.name + '_looped_first_episode') !== podcast.episodes[0].url"
                     >
                  <h2
                    :class="{
    'bg-blue-100': current_podcast && current_podcast.name === podcast.name,
    'text-red-100': localstoragegetter(podcast.name + '_looped_first_episode') === podcast.episodes[0].url,
    'font-bold': dir_last_episode[dir] && (podcast.name === dir_last_episode[dir].podcast.name),
}	"
                    @click="extended = !extended"
                    class="rounded pl-8"
                    >
                    <span class="w-8 inline-block text-right" x-text="extended ? '↓' : '→'"></span>
                    <span x-text="podcast.name" class="text-lg"></span>
                    <button class="text-sm"
                            @click.stop="
                                         current_podcast = podcast
                                         current_dir = dir
                                         current_episode = null // to trigger reload
                                         await $nextTick()
                                         current_episode = podcast.episodes[0]
                                         localStorage.setItem(`time_${current_episode.url}`, 0)
                                         sync_state[`time_${current_episode.url}`] = 0
                                         await $nextTick() // wait for it to be ready
                                         $dispatch('pleaseplay')
                                         $dispatch('pleaserewind')">▶</button>
                    <button class="text-sm" x-show="podcast.name in podcast_last_episode"
                            @click.stop="
                                         let dir = podcast_last_episode[podcast.name].dir
                                         let episode = podcast_last_episode[podcast.name].episode
                                         current_dir = dir
                                         current_podcast = podcast
                                         current_episode = null // to trigger reload
                                         await $nextTick()
                                         current_episode = episode
                                         await $nextTick() // wait for it to be ready
                                         $dispatch('pleaseplay')
                                         $dispatch('pleaserewind')">resume</button>
                  </h2>
                  <div id="episodes" class="bg-slate-100 border-2 overflow-x-scroll" x-collapse x-show="extended">
                    <template x-for="(episode, index) in podcast.episodes">
                      <div id="episode"
                           @dblclick="current_episode = null ; await $nextTick() ; current_episode = episode ; current_podcast = podcast ; current_dir = dir ; current_playing = true"
                           :class="{
    'bg-green-100': current_episode && current_episode.url === episode.url,
    'text-red-100': localstoragegetter(podcast.name + '_looped_first_episode') === podcast.episodes[0].url,
    'font-bold': podcast.name in podcast_last_episode && podcast_last_episode[podcast.name].episode.url === episode.url,
}"
                           class="rounded w-fit text-nowrap"
                           >
                        <div>
                          <span x-show="episode.album">
                            <span x-text="episode.album"></span>:
                            <span x-text="episode.index"></span> -
                          </span>
                          <span x-text="episode.name"></span><span x-text="episode.date ? ' - ' + episode.date : ''"></span>
                        </div>
                      </div>
                    </template>
                  </div>
                </div>
              </template>
            </div>
          </div>
        </template>
      </div>
      <div id="countdown"
           x-show='current_progress.duration'
           x-data="{motionreplaytimer: null, countdown: null, remaining_time: 0, time: $persist(1),
 start() {
     this.$dispatch('pleaseplay')
     this.remaining_time = this.time * 60
     if(this.countdown)
     {
         clearInterval(this.countdown)
         this.countdown = null
     }
     this.countdown = setInterval(() => {
         this.remaining_time-=1
         if(this.remaining_time == 4)
         {
             this.$dispatch('pleasevolume', {volume: 0.8})
         }
         if(this.remaining_time == 3)
         {
             this.$dispatch('pleasevolume', {volume: 0.6})
         }
         if(this.remaining_time == 2)
         {
             this.$dispatch('pleasevolume', {volume: 0.4})
         }
         if(this.remaining_time == 1)
         {
             this.$dispatch('pleasevolume', {volume: 0.2})
         }
         if(this.remaining_time <= 0){
             this.$dispatch('pleasepause')
             this.$dispatch('pleaserewind')
             this.remaining_time = 0
             clearInterval(this.countdown)
             this.countdown=null
             if(this.motionreplaytimer){clearTimeout(this.motionreplaytimer)}
             this.motionreplaytimer = setTimeout(() => {
                 info('end of time to shake to play')
                 this.motionreplaytimer = null
                 this.$dispatch('pleaserewind', {time: 50}) // I most likely missed the end
             }, 10000)
         }
     }, 1000)
 },
}" class="w-full flex"
           @devicemotion.window="let motion = (
                                 $event.acceleration.x * $event.acceleration.x
                                 + $event.acceleration.y * $event.acceleration.y
                                 + $event.acceleration.z * $event.acceleration.z
                                 )
                                 if(motion > 400 && motionreplaytimer){
                                 clearTimeout(motionreplaytimer)
                                 motionreplaytimer = null
                                 info('shaked, again!!')
                                 start()
                                 }
                                 "
           >
        <input type="number" class="w-8" x-model="time"/>
        <input type="number" disabled class="w-8" x-model="remaining_time"/>
        <input type="range" min="0" disabled :max="time * 60" :value="time * 60 - remaining_time" class="flex-grow"/>
        <button @click="
                        if(countdown){
                        clearInterval(countdown)
                        countdown = null
                        $dispatch('pleasepause')
                        $dispatch('pleaserewind')
                        remaining_time = 0
                        }
                        else
                        {
                        start()
                        }
                        "
                x-text="countdown ? 'stop' : 'start'"></button>
      </div>
      <template x-if="current_progress.duration">
        <div id="controls" x-show="current_episode && dirs">
          <span id="controls" class="flex place-content-evenly text-3xl">
            <button @click="$dispatch('gotoprev')">⏮</button>
            <button @click="$dispatch('pleaserewind')">⏪</button>
            <button @click="$dispatch(current_playing ? 'pleasepause' : 'pleaseplay')" x-text="current_playing ? '⏸' : '▶'"></button>
            <button @click="$dispatch('pleaseffwd')">⏩</button>
            <button @click="$dispatch('gotonext')">⏭</button>
            <input type="checkbox" x-model="show_old"/>
            <span class="text-sm"
                  x-data="{
    disabled: true,
    disabled_timer: null,
    async set_timer () {
        if(this.disabled_timer !== null){
            clearTimeout(this.disabled_timer)
        }
        this.disabled_timer = setTimeout(() => {this.disabled = true; this.disabled_timer = null}, 5000)
    },
}"
                  @dblclick="disabled = !disabled ; if(! disabled) {await set_timer()} else if (disabled_timer !== null) {clearTimeout(disabled_timer) ; disabled_timer=null;}"
                  >
              <span class="w-8 text-right inline-block" x-text="current_speed"></span>:
              <input class="align-middle"
                     type="range"
                     x-effect.follow-current-episode="if(current_episode){
    current_speed = localstoragegetter(key('rate')) || 1
}"
                     :value="current_speed"
                     :disabled="disabled"
                     @input="$dispatch('pleasesetspeed', {speed: $el.value}) ; await set_timer()"
                     min="0.25" max="4" step="0.25"/>
            </span>
          </span>
          <span id="progress" class="text-sm w-full flex flex-row"
                x-data="{
    disabled: true,
    disabled_timer: null,
    async set_timer () {
        if(this.disabled_timer !== null){
            clearTimeout(this.disabled_timer)
        }
        this.disabled_timer = setTimeout(() => {this.disabled = true; this.disabled_timer = null}, 5000)
    },
}"
                @dblclick="disabled = !disabled ; if(! disabled) {await set_timer()} else if (disabled_timer !== null) {clearTimeout(disabled_timer) ; disabled_timer=null;}"
                >
            <span>
              <span x-text="formatTime(current_progress.cur)"></span>/<span x-text="formatTime(current_progress.duration)"></span>
            </span>
            <input
              type="range"
              class="flex-grow"
              min="0" :max="current_progress.duration"
              :disabled="disabled"
              :value="current_progress.cur"
              @input="$dispatch('setcurrenttime', {value: $el.value}) ; await set_timer()"
              />
          </span>
        </div>
      </template>
    </div>
    <div id="player"
         class="flex justify-center bg-black"
         x-data="{
    init_values() {
        let cur = localstoragegetter(key('time'))
        if(cur)
        {
            this.$el.currentTime = cur
        }
        let episode_rate = localstoragegetter(key('rate'))
        if(episode_rate)
        {
            this.$el.playbackRate = parseFloat(episode_rate)
        }
        else
        {
            let podcast_rate = localstoragegetter(`rate_${current_podcast.name}`)
            if(podcast_rate)
            {
                this.$el.playbackRate = parseFloat(podcast_rate)
            }
        }
        window.v = this.$el
    },
    onended() {
        localStorage.setItem(this.key('time'), 0)
        sync_state[this.key('time')] = 0
        this.$dispatch('gotonext', {playing: true})
    },
    medium: {
        async ['x-init']() {
            this.init_values($el)

            if('mediaSession' in navigator) {
                var artist = ''
                if(this.current_episode.index !== null)
                {
                    artist += `${this.current_episode.index}`
                }
                if(this.current_episode.number !== null)
                {
                    artist += `/${this.current_episode.number}`
                }
                if(artist !== '')
                {
                    artist += ' '
                }
                artist += this.current_podcast.name
                navigator.mediaSession.metadata = new MediaMetadata({
                    title: this.current_episode.name,
                    artist: artist,
                    album: this.current_episode.album || this.current_podcast.name,
                });
                navigator.mediaSession.setActionHandler('nexttrack', () => {this.$dispatch('gotonext', {playing: true})});
                navigator.mediaSession.setActionHandler('previoustrack', () => {this.$dispatch('gotoprev', {playing: true})});
                navigator.mediaSession.playbackState = 'paused'
            }
        },
        ['@pleasesetspeed.window']() {
            this.$el.playbackRate = this.$event.detail.speed
        },
        ['@pleasescroll.window']() {
            this.$el.scrollIntoView()
        },
        ['@canplay']() {
            if(this.current_playing) {
                this.$el.play()
            }
            this.current_progress = {cur: this.$el.currentTime, duration: this.$el.duration}
        },
        ['@play']() {
            this.$el.volume = 1
            this.current_playing = true
        },
        ['@pleaseplay.window'](){
            this.$el.play()
        },
        ['@pleasevolume.window'](){
            this.$el.volume = this.$event.detail.volume
        },
        ['@pleasepause.window'](){
            this.$el.pause()
        },
        ['@pleaseffwd.window'](){
            this.$el.currentTime += 10
        },
        ['@pleaserewind.window'](){
            this.$el.currentTime -= this.$event.detail.time || 10
        },
        ['@pause.window'](){
            this.$el.pause()
        },
        async ['@loadeddata'](){
            await $nextTick()
            this.$el.scrollIntoView()
        },
        ['@pause']() {
            this.current_playing = false
            if(this.$el.currentTime === this.$el.duration)
            {
                this.onended()
            }
        },
        ['@ended']() {
            // this is not triggerd for audio in qutebrowser
        },
        ['@timeupdate'](){
            localStorage.setItem(this.key('time'), this.$el.currentTime)
            sync_state[this.key('time')] = this.$el.currentTime
            this.current_progress = {cur: this.$el.currentTime, duration: this.$el.duration}
        },
        ['@setcurrenttime.window']() {
            this.$el.currentTime = this.$event.detail.value
        },
        ['@ratechange'](){
            localStorage.setItem(this.key('rate'), this.$el.playbackRate)
            sync_state[this.key('rate')] = this.$el.playbackRate
            localStorage.setItem(`rate_${current_podcast.name}`, this.$el.playbackRate)
            sync_state[`rate_${current_podcast.name}`] = this.$el.playbackRate
            this.current_speed = this.$el.playbackRate
        },
    },
}"
         >
      <template x-if="current_episode && ( current_episode.url.endsWith('mp3') || current_episode.url.endsWith('ogg') || current_episode.url.endsWith('m4a'))">
        <audio class="w-full"
               x-bind="medium"
               >
          <source :src="current_episode && current_episode.url" type="audio/mpeg">
          Your browser does not support the audio element.
        </audio>
      </template>
      <template x-if="current_episode && ! ( current_episode.url.endsWith('mp3') || current_episode.url.endsWith('ogg') || current_episode.url.endsWith('m4a'))">
        <video class="w-screen max-h-screen object-contain"
               x-bind="medium"
               x-data="{
                       async click(event) {
                       xpos = event.offsetX / event.target.clientWidth
                       ypos = event.offsetY / event.target.clientHeight
                       if(xpos < 0.25) {
                       info('rewind')
                       this.$dispatch('pleaserewind')
                       }
                       else if (xpos > 0.75) {
                       info('ffwd')
                       this.$dispatch('pleaseffwd')
                       }
                       else {
                       if(ypos > 0.80)
                       {
                       info('PIP')
                       try{
                       await this.$el.requestPictureInPicture()
                       } catch(e) {
                       info(JSON.stringify(e))
                       }
                       }
                       else
                       {
                       info('toggling')
                       if(this.current_playing){
                       this.$dispatch('pleasepause')
                       }
                       else
                       {
                       this.$dispatch('pleaseplay')
                       }
                       }
                       }
                       }
                       }"
               @dblclick="await click($event)"
               >
          <source :src="current_episode && current_episode.url" type="audio/mpeg">
        </video>
      </template>
    </div>
  </body>
</html>

{
    "name": "podcast",
    "short_name": "podcast",
    "description": "podcast",
    "start_url": "./podcast.html",
    "display": "fullscreen",
    "background_color": "#cccccc",
    "theme_color": "#4285f4",
    "orientation": "any",
    "icons": [
        {
            "src": "./podcast.png",
            "type": "image/png",
            "sizes": "192x192"
        }
    ],
    "scope": "./",
    "permissions": [],
    "splash_pages": null,
    "categories": []
}

Note: Beware that we should not provide an orientation so as to follow the device one.

I put this icon aside with the manifest (generated with the prompt “generate a 192x192 image on the theme podcast” in chatgpt).

podcast.png

// taken from https://googlechrome.github.io/samples/service-worker/basic/

const CACHE_NAME = 'podcastcache';

const DYNAMIC_URLS = [
    "./podcast.html",
    "./podcasts.json",
    "./podcastmanifest.json",
    "./swpodcast.js",
    "./podcast.png",
]

// A list of local resources we always want to be cached.
const PRECACHE_URLS = [
    "https://ipfs.konubinix.eu/bafybeienb3tpzdewluwh5i5mzrc7wq3vibqqsp52gf6z4j26winzlsbqlm/toastify.min.css?orig=https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css",
    "https://ipfs.konubinix.eu/bafybeienb3tpzdewluwh5i5mzrc7wq3vibqqsp52gf6z4j26winzlsbqlm/toastify-js?orig=https://cdn.jsdelivr.net/npm/toastify-js",
    "https://ipfs.konubinix.eu/bafkreidregenuw7nhqewvimq7p3vwwlrqcxjcrs4tiocrujjzggg26gzcu?orig=https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js",
    "https://ipfs.konubinix.eu/bafkreighmyou4lhqizpdzvutdeg6xnpvskwhfxgez7tfawason3hkwfspm?orig=https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js",
    "https://ipfs.konubinix.eu/bafkreic33rowgvvugzgzajagkuwidfnit2dyqyn465iygfs67agsukk24i?orig=https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js",
    "https://ipfs.konubinix.eu/bafybeihp5kzlgqt56dmy5l4z7kpymfc4kn3fnehrrtr7cid7cn7ra36yha?orig=https://cdn.tailwindcss.com/3.4.3",

];
self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => cache.addAll([
                ...PRECACHE_URLS,
                ...DYNAMIC_URLS
            ]))
            .then(self.skipWaiting())
    );
});

// clean up old caches
self.addEventListener('activate', event => {
    const currentCaches = [CACHE_NAME];
    event.waitUntil(
        caches.keys().then(cacheNames => {
            return cacheNames.filter(cacheName => !currentCaches.includes(cacheName));
        }).then(cachesToDelete => {
            return Promise.all(cachesToDelete.map(cacheToDelete => {
                return caches.delete(cacheToDelete);
            }));
        }).then(() => self.clients.claim())
    );
});

// taken from https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Caching

async function cacheFirst(request) {
    const cachedResponse = await caches.match(request);
    if (cachedResponse) {
        console.log(`Get ${request.url} from the cache (cacheFirst)`)
        return cachedResponse;
    }
    try {
        // for some reason, the intercepted requests uses crendentials: include,
        // leading to an error, explicitely tell to omit credentials then. I
        // also have to explicitely tell it that I want to use cors.
        const networkResponse = await fetch(request, { mode: 'cors', credentials: 'omit' });
        console.log(`cacheFirst: ${request.url}: ${request.credentials}, ${networkResponse.ok}, ${networkResponse.status}, ${networkResponse.statusText}`)
        if (networkResponse.ok) {
            console.log(`cacheFirst: Put ${request.url} into the cache`)
            const cache = await caches.open(CACHE_NAME);
            cache.put(request, networkResponse.clone());
        }
        else
        {
            console.log(`cacheFirst: bad response with ${request.url}`)
        }
        return networkResponse;
    } catch (error) {
        console.log(`cacheFirst: error with ${request.url}`)
        return Response.error();
    }
}

async function networkFirst(request) {
    try {
        const networkResponse = await fetch(request);
        if (networkResponse.ok) {
            const cache = await caches.open(CACHE_NAME);
            console.log(`Put ${request.url} into the cache (networkfirst), ${request.credentials}`)
            cache.put(request, networkResponse.clone());
        }
        return networkResponse;
    } catch (error) {
        const cachedResponse = await caches.match(request);
        console.log(`Got ${request.url} from the cache (networkfirst)`)
        return cachedResponse || Response.error();
    }
}

self.addEventListener('fetch', event => {
    const dynamicUrlsRegex = new RegExp(`^.*(${DYNAMIC_URLS.join('|')}).*`);
    if (event.request.url.match(dynamicUrlsRegex)) {
        // this is likely to change from time to time, use a network first approach
        console.log(`Dealing with ${event.request.url} (networkfirst)`)
        event.respondWith(networkFirst(event.request));
    } else if (event.request.url.includes("/ipfs/")) {
        // ipfs content never changes, get it from the cache if possible
        console.log(`Dealing with ${event.request.url} (cacheFirst)`)
        event.respondWith(cacheFirst(event.request));
    }
    else
    {
        console.log(`Leaving ${event.request.url} to the browser`)
    }
});

Notes linking here