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:
169
listening.njk
169
listening.njk
@@ -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">♥</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">♥</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 %}">♥</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>
|
||||
|
||||
Reference in New Issue
Block a user