feat(listening): merge Funkwhale and Last.fm into single sorted timeline

Adds a `mergeListens` Eleventy filter that combines both sources into one
array sorted newest-first by timestamp (listenedAt / scrobbledAt). The
Recent Listens section now renders a unified chronological feed with
per-source badges and Alpine filter tabs still working.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
svemagie
2026-03-21 16:38:56 +01:00
parent 9d32075b9e
commit 20e4403b00
2 changed files with 73 additions and 111 deletions

View File

@@ -265,122 +265,69 @@ withSidebar: true
Recent Listens
</h2>
{% set combinedListens = funkwhaleActivity.listenings | mergeListens(lastfmActivity.scrobbles) | head(20) %}
<div class="space-y-3">
{# Funkwhale Listenings #}
{% if funkwhaleActivity.listenings.length %}
<div x-show="activeSource === 'all' || activeSource === 'funkwhale'">
{% for listening in funkwhaleActivity.listenings | head(10) %}
<div class="flex items-center gap-4 p-3 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-purple-400 dark:hover:border-purple-600 transition-colors mb-2 shadow-sm">
{% if listening.coverUrl %}
<img src="{{ listening.coverUrl }}" alt="" class="w-12 h-12 rounded object-cover flex-shrink-0" loading="lazy" eleventy:ignore>
{% else %}
<div class="w-12 h-12 rounded bg-surface-200 dark:bg-surface-700 flex items-center justify-center flex-shrink-0">
<svg class="w-6 h-6 text-surface-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2z"/>
</svg>
</div>
{% endif %}
<div class="flex-1 min-w-0">
<h3 class="font-medium text-surface-900 dark:text-surface-100 truncate">
{% if listening.trackUrl %}
<a href="{{ listening.trackUrl }}" class="hover:text-purple-600 dark:hover:text-purple-400" target="_blank" rel="noopener">{{ listening.track }}</a>
{% else %}
{{ listening.track }}
{% endif %}
{% if listening.favorite %}
<span class="text-purple-500 ml-1" title="Favorite">&#9829;</span>
{% endif %}
</h3>
<p class="text-sm text-surface-600 dark:text-surface-400 truncate">{{ listening.artist }}</p>
</div>
<div class="text-right flex-shrink-0">
<span class="inline-block px-2 py-0.5 text-xs font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400 rounded-full mb-1">Funkwhale</span>
<span class="text-xs text-surface-600 dark:text-surface-400 block">{{ listening.relativeTime }}</span>
<button
class="share-post-btn mt-1"
data-share-url="{{ listening.trackUrl }}"
data-share-title="{{ listening.track }} — {{ listening.artist }}"
title="Create post"
aria-label="Create post"
>
<span class="share-post-icon">✏️</span>
</button>
<button
class="save-later-btn mt-1"
data-save-url="{{ listening.trackUrl }}"
data-save-title="{{ listening.track }} — {{ listening.artist }}"
data-save-source="listening"
title="Save for later"
aria-label="Save for later"
>
<span class="save-later-icon">📑</span>
</button>
</div>
{% if combinedListens.length %}
{% for item in combinedListens %}
<div
x-show="activeSource === 'all' || activeSource === '{{ item._source }}'"
class="flex items-center gap-4 p-3 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-purple-400 dark:hover:border-purple-600 transition-colors shadow-sm"
>
{% if item.coverUrl %}
<img src="{{ item.coverUrl }}" alt="" class="w-12 h-12 rounded object-cover flex-shrink-0" loading="lazy" eleventy:ignore>
{% else %}
<div class="w-12 h-12 rounded bg-surface-200 dark:bg-surface-700 flex items-center justify-center flex-shrink-0">
<svg class="w-6 h-6 text-surface-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2z"/>
</svg>
</div>
{% endfor %}
</div>
{% endif %}
{% endif %}
{# Last.fm Scrobbles #}
{% if lastfmActivity.scrobbles.length %}
<div x-show="activeSource === 'all' || activeSource === 'lastfm'">
{% for scrobble in lastfmActivity.scrobbles | head(10) %}
<div class="flex items-center gap-4 p-3 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-purple-400 dark:hover:border-purple-600 transition-colors mb-2 shadow-sm">
{% if scrobble.coverUrl %}
<img src="{{ scrobble.coverUrl }}" alt="" class="w-12 h-12 rounded object-cover flex-shrink-0" loading="lazy" eleventy:ignore>
{% else %}
<div class="w-12 h-12 rounded bg-surface-200 dark:bg-surface-700 flex items-center justify-center flex-shrink-0">
<svg class="w-6 h-6 text-surface-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2z"/>
</svg>
</div>
{% endif %}
<div class="flex-1 min-w-0">
<h3 class="font-medium text-surface-900 dark:text-surface-100 truncate">
{% if scrobble.trackUrl %}
<a href="{{ scrobble.trackUrl }}" class="hover:text-purple-600 dark:hover:text-purple-400" target="_blank" rel="noopener">{{ scrobble.track }}</a>
{% else %}
{{ scrobble.track }}
{% endif %}
{% if scrobble.loved %}
<span class="text-purple-500 ml-1" title="Loved">&#9829;</span>
{% endif %}
</h3>
<p class="text-sm text-surface-600 dark:text-surface-400 truncate">{{ scrobble.artist }}</p>
</div>
<div class="text-right flex-shrink-0">
<span class="inline-block px-2 py-0.5 text-xs font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400 rounded-full mb-1">Last.fm</span>
<span class="text-xs text-surface-600 dark:text-surface-400 block">{{ scrobble.relativeTime }}</span>
<button
class="share-post-btn mt-1"
data-share-url="{{ scrobble.trackUrl }}"
data-share-title="{{ scrobble.track }} — {{ scrobble.artist }}"
title="Create post"
aria-label="Create post"
>
<span class="share-post-icon">✏️</span>
</button>
<button
class="save-later-btn mt-1"
data-save-url="{{ scrobble.trackUrl }}"
data-save-title="{{ scrobble.track }} — {{ scrobble.artist }}"
data-save-source="listening"
title="Save for later"
aria-label="Save for later"
>
<span class="save-later-icon">📑</span>
</button>
</div>
<div class="flex-1 min-w-0">
<h3 class="font-medium text-surface-900 dark:text-surface-100 truncate">
{% if item.trackUrl %}
<a href="{{ item.trackUrl }}" class="hover:text-purple-600 dark:hover:text-purple-400" target="_blank" rel="noopener">{{ item.track }}</a>
{% else %}
{{ item.track }}
{% endif %}
{% if item.favorite or item.loved %}
<span class="text-purple-500 ml-1" title="{% if item._source == 'funkwhale' %}Favorite{% else %}Loved{% endif %}">&#9829;</span>
{% endif %}
</h3>
<p class="text-sm text-surface-600 dark:text-surface-400 truncate">{{ item.artist }}</p>
</div>
{% endfor %}
</div>
{% endif %}
{% if not funkwhaleActivity.listenings.length and not lastfmActivity.scrobbles.length %}
<div class="text-right flex-shrink-0">
{% if item._source == 'funkwhale' %}
<span class="inline-block px-2 py-0.5 text-xs font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400 rounded-full mb-1">Funkwhale</span>
{% else %}
<span class="inline-block px-2 py-0.5 text-xs font-medium bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 rounded-full mb-1">Last.fm</span>
{% endif %}
<span class="text-xs text-surface-600 dark:text-surface-400 block">{{ item.relativeTime }}</span>
<button
class="share-post-btn mt-1"
data-share-url="{{ item.trackUrl }}"
data-share-title="{{ item.track }} — {{ item.artist }}"
title="Create post"
aria-label="Create post"
>
<span class="share-post-icon">✏️</span>
</button>
<button
class="save-later-btn mt-1"
data-save-url="{{ item.trackUrl }}"
data-save-title="{{ item.track }} — {{ item.artist }}"
data-save-source="listening"
title="Save for later"
aria-label="Save for later"
>
<span class="save-later-icon">📑</span>
</button>
</div>
</div>
{% endfor %}
{% else %}
<p class="text-surface-600 dark:text-surface-400">No recent listening history available.</p>
{% endif %}
</div>