Ephes Blog

Miscellaneous things. Mostly Weeknotes and links I stumbled upon.


Podlove Web Player Performance Improvements

, Jochen

I recently attended the SUBSCRIBE 11 podcasting conference, where I had the opportunity to chat with the author of Pods-Blitz about the Podlove Web Player. After we both listened to a talk about it, he asked me how I liked the Web Player. I mentioned that I really enjoyed it but suspected it was slowing down my website. He immediately agreed, noting that he had observed the same behavior.

Here's a screenshot illustrating the issue:

python-podcast-performance_original_scores

The poor performance seems to stem from the player loading a substantial amount of JavaScript and CSS assets.

podlove-player_slow_assets

Today, I decided to ask on Discord what might be causing this problem. Below is my original JavaScript code used to initialize the five audio players on the overview page of my podcast:

<script defer src="/static/cast/js/web-player/embed.5.28903a742256.js"></script>
<script>
  function initializePodlovePlayers() {
    document.querySelectorAll('section.block-audio div[id^="audio_"]').forEach(div => {
      const audioId = div.id;
      const url = div.getAttribute('data-url');
      podlovePlayer(`#${audioId}`, url, "/api/audios/player_config/");
    });
  }

  document.body.addEventListener('htmx:afterSwap', (event) => {
    if (event.detail.target.id === 'paging-area') {
      initializePodlovePlayers();
    }
  });

  // Call the function on initial load
  document.addEventListener('DOMContentLoaded', (event) => {
    initializePodlovePlayers();
  });
</script>

Alexander Heimbuch suggested that I use the load event instead of DOMContentLoaded to avoid blocking the main thread. He also advised using the IntersectionObserver API to defer the initialization until the player becomes visible in the viewport. I'm really grateful for Alex's help—it made a significant difference in optimizing my website. Based on his suggestions, I updated my code:

<script defer src="{% static 'cast/js/web-player/embed.5.js' %}"></script>
<script>
  function initializePodlovePlayerWhenVisible(div) {
    const observer = new IntersectionObserver((entries, observer) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const audioId = div.id;
          const url = div.getAttribute('data-url');
          podlovePlayer(`#${audioId}`, url, "/api/audios/player_config/");
          observer.unobserve(div); // Stop observing since we don't need to initialize again
        }
      });
    });
    observer.observe(div);
  }

  function initializePodlovePlayers() {
    document.querySelectorAll('.podlove-player-container').forEach(div => {
      initializePodlovePlayerWhenVisible(div);
    });
  }

  document.body.addEventListener('htmx:afterSettle', (event) => {
    if (event.detail.target.id === 'paging-area') {
      setTimeout(() => {
        initializePodlovePlayers();
      }, 1500); // Adjust the delay as needed -> wait to finish scrolling up
    }
   });

  // Attach to the load event to ensure the page has fully loaded
  window.addEventListener('load', function() {
    initializePodlovePlayers();
  });
</script>

These changes made a significant difference—the performance score for mobile increased from 60 to 83. Realizing that it wouldn't be easy to reduce the connections to cdn.podlove.org—since each player is isolated in its own iframe and the URL is hardcoded in the pre-built player bundle—I added an additional optimization by including a preconnect link in the header of my website:

<link rel="preconnect" href="https://cdn.podlove.org" />

This also helped a lot to improve performance. Here are the final results:

python-podcast_optimized_scores

After grappling with htmx and pagination issues, I decided to encapsulate all the JavaScript into a web component. Now, the player script is dynamically imported when the web component becomes visible.

{% load static %}
{% if page.pk %}
  <podlove-player
    id="audio_{{ value.pk }}"
    data-variant="xl"
    data-url="{% url 'cast:api:audio_podlove_detail' pk=value.pk post_id=page.pk %}"
    data-embed="{% static 'cast/js/web-player/embed.5.js' %}"
    data-config="{% url 'cast:api:player_config' %}"
  >
  </podlove-player>
{% endif %}
// podlove-player.ts
class PodlovePlayerElement extends HTMLElement {
  constructor() {
    super();
    this.observer = null;
    this.shadow = this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.renderPlaceholder();

    if (document.readyState === 'complete') {
      // The page is already fully loaded
      this.observeElement();
    } else {
      // Wait for the 'load' event before initializing
      window.addEventListener('load', () => {
        this.observeElement();
      }, { once: true });
    }
  }

  disconnectedCallback() {
    if (this.observer) {
      this.observer.disconnect();
    }
  }

  renderPlaceholder() {
    // Reserve space to prevent layout shifts
    const container = document.createElement('div');
    container.classList.add('podlove-player-container');

    // Apply styles
    const style = document.createElement('style');
    style.textContent = `
      .podlove-player-container {
        width: 100%;
        max-width: 936px;
        height: 300px;
        margin: 0 auto;
      }
      @media (max-width: 768px) {
        .podlove-player-container {
          max-width: 366px;
          height: 500px;
        }
      }
      .podlove-player-container iframe {
        width: 100%;
        height: 100%;
        border: none;
      }
    `;

    this.shadow.appendChild(style);
    this.shadow.appendChild(container);
  }

  observeElement() {
    this.observer = new IntersectionObserver((entries, observer) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          this.initializePlayer();
          observer.unobserve(this);
        }
      });
    });
    this.observer.observe(this);
  }

  initializePlayer() {
    const container = this.shadow.querySelector('.podlove-player-container');
    const audioId = this.getAttribute('id') || `podlove-player-${Date.now()}`;
    const url = this.getAttribute('data-url');
    const configUrl = this.getAttribute('data-config') || '/api/audios/player_config/';
    const podloveTemplate = this.getAttribute('data-template');
    let embedUrl = this.getAttribute('data-embed') || 'https://cdn.podlove.org/web-player/5.x/embed.js';

    // If host ist localhost use local embed url
    const { protocol, hostname, port } = window.location;


    console.log("data template: ", podloveTemplate);
    const playerDiv = document.createElement('div');
    playerDiv.id = audioId;

    // set the template attribute if it is set (needed for pp theme)
    if (podloveTemplate !== null) {
        playerDiv.setAttribute('data-template', podloveTemplate);
    }
    container.appendChild(playerDiv);

    if (typeof podlovePlayer === 'function') {
      podlovePlayer(playerDiv, url, configUrl);
    } else {
      // If in dev mode on localhost and embedUrl starts with a slash, use the local embedUrl
      // otherwise the vite url 5173 will be used -> which will not work
      if (hostname === 'localhost' && embedUrl.startsWith("/")) {
        embedUrl = `http://localhost:${port}${embedUrl}`;
      }
      // Dynamically load the Podlove player script
      import(embedUrl).then(() => {
        podlovePlayer(playerDiv, url, configUrl);
      });
    }
  }
}

console.log("Registering podlove-player!");
customElements.define('podlove-player', PodlovePlayerElement);

Type Annotations can Cause Unexpected Behavior in Python

, Jochen

Today I learned that type annotations in Python, while being valid code, can sometimes lead to unexpected problems. In a Django view, I had the following line:

def some_view(request):
    ...
    context["form"]: form
    return render(request, some_template, context=context)

This line doesn't raise a SyntaxError because the colon is interpreted as part of a type annotation, which is ignored at runtime. As a result, the line effectively becomes just context["form"], which is a no-op but still valid Python code.


It took me some time to figure out why {% if form.field.errors %} used later on was was always false. 🤦‍♂️


Unwrapping functools.wraps: How to Remove Decorators from a Python Function

, Jochen

Sometimes, you need to call a Python function without its decorators. I encountered this need while implementing custom exception handling for the ACS (Assertion Consumer Service) view in django-saml2-auth. This view was decorated with an exception_handler that rendered responses, which interfered with my custom handling. Fortunately, functools.wraps comes to the rescue, storing the original function in a __wrapped__ attribute.

Here’s an example to demonstrate:

from functools import wraps

def heading(f):
    @wraps(f)
    def wrapper(*args, **kwds):
        to_decorate = f()
        return f"<h1>{to_decorate}</h1>"
    return wrapper


@heading
def decorated_hello():
    return "Hello World!"


# Using the decorated function
print(decorated_hello())
# Output: '<h1>Hello World!</h1>'

# Accessing the original, undecorated function
print(decorated_hello.__wrapped__())
# Output: 'Hello World!'

Python String Capitalization

, Jochen

I frequently copy titles from websites to use as anchor texts for links. However, these titles are often in all caps, and I don’t want to seem like I’m shouting. Previously, I would paste the all-caps title into a Python REPL, convert it to lowercase, and then manually capitalize the necessary words to make it look like a proper title. But today, I discovered a much simpler built-in solution:

>>> print("THIS IS A LOUD TITLE".title())
'This Is A Loud Title'

I had no idea about this function! 😅


Workaround for Paying OpenAI via bunq

, Jochen

tldr: The chatGPT app on iOS allows you to pay openAI using Apple Pay


Last week openAI stopped working for me and I got an error about a billing issue. I use a credit card from bunq to pay openAI, which worked fine for almost a year. When I investigated the issue, I got several misleading error messages from bunq like "your monthly limit is not sufficient" or "you have already added the merchant to your allowed merchants list". I couldn't fix the problem. So I contacted bunq support and they couldn't help either.

Today I thought about using another credit card, but then I found a workaround:

The chatGPT app on iOS allows you to pay openAI using Apple Pay. I couldn't do it with Chrome, and for Safari the Stripe interface lets you choose Apple Pay, but that didn't work either. But the app finally worked 🎉.