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 defer src="http://home/ipfs/bafkreidregenuw7nhqewvimq7p3vwwlrqcxjcrs4tiocrujjzggg26gzcu?orig=https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js"></script>
    <script defer src="http://home/ipfs/bafkreighmyou4lhqizpdzvutdeg6xnpvskwhfxgez7tfawason3hkwfspm?orig=https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
    <script defer src="http://home/ipfs/bafkreic33rowgvvugzgzajagkuwidfnit2dyqyn465iygfs67agsukk24i?orig=https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
    <script src="http://home/ipfs/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{
        // 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: 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: {
        playing: false,
        episode: Alpine.$persist(null).as("episode"),
        podcast: Alpine.$persist(null).as("podcast"),
        dir: Alpine.$persist(null).as("dir"),
        },
        key(stuff) {
        return `${stuff}_${this.current.episode.url}`
        },
        async goto_next() {
        localStorage.setItem(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.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)
        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
        },
        }))
        })
    </script>
  </head>
  <body x-data="app" x-init="init_app"
        @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)
                                   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 || localStorage.getItem(podcast.name + '_looped_first_episode') !== podcast.episodes[0].url"
                     >
                  <h2
                    :class="{
    'bg-blue-100': current.podcast && current.podcast.name === podcast.name,
    'text-red-100': localStorage.getItem(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)
                                         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': localStorage.getItem(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 = localStorage.getItem(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 = localStorage.getItem(key('time'))
        if(cur)
        {
            this.$el.currentTime = cur
        }
        let episode_rate = localStorage.getItem(key('rate'))
        if(episode_rate)
        {
            this.$el.playbackRate = parseFloat(episode_rate)
        }
        else
        {
            let podcast_rate = localStorage.getItem(`rate_${current.podcast.name}`)
            if(podcast_rate)
            {
                this.$el.playbackRate = parseFloat(podcast_rate)
            }
        }
        window.v = this.$el
    },
    onended() {
        localStorage.setItem(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'
            }
            if(this.current.playing) {
                await $nextTick()
                this.$el.play()
            }
            this.current_progress = {cur: this.$el.currentTime, duration: this.$el.duration}
        },
        ['@pleasesetspeed.window']() {
            this.$el.playbackRate = this.$event.detail.speed
        },
        ['@pleasescroll.window']() {
            this.$el.scrollIntoView()
        },
        ['@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)
            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)
            localStorage.setItem(`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'))">
        <div class="relative w-full h-screen">
        <video class="w-full h-full 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>
        </div>
      </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: In my experience in brave, the orientation “any” does not follow the user setting to lock the rotation. And the natural one keeps the screen in portrait mode whatever I do.

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

// 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 = [
    "http://home/ipfs/bafkreidregenuw7nhqewvimq7p3vwwlrqcxjcrs4tiocrujjzggg26gzcu?orig=https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js",
    "http://home/ipfs/bafkreighmyou4lhqizpdzvutdeg6xnpvskwhfxgez7tfawason3hkwfspm?orig=https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js",
    "http://home/ipfs/bafkreic33rowgvvugzgzajagkuwidfnit2dyqyn465iygfs67agsukk24i?orig=https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js",
    "http://home/ipfs/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