feat: add Last.fm integration to listening page
- Add lastfmActivity.js data fetcher for Indiekit Last.fm API - Add listening.njk combined page with Funkwhale + Last.fm data - Source filter tabs, combined stats, recent listens timeline - Loved tracks section for Last.fm Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
83
_data/lastfmActivity.js
Normal file
83
_data/lastfmActivity.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Last.fm Activity Data
|
||||
* Fetches from Indiekit's endpoint-lastfm public API
|
||||
*/
|
||||
|
||||
import EleventyFetch from "@11ty/eleventy-fetch";
|
||||
|
||||
const INDIEKIT_URL = process.env.SITE_URL || "https://example.com";
|
||||
const LASTFM_USERNAME = process.env.LASTFM_USERNAME || "";
|
||||
|
||||
/**
|
||||
* Fetch from Indiekit's public Last.fm API endpoint
|
||||
*/
|
||||
async function fetchFromIndiekit(endpoint) {
|
||||
try {
|
||||
const url = `${INDIEKIT_URL}/lastfmapi/api/${endpoint}`;
|
||||
console.log(`[lastfmActivity] Fetching from Indiekit: ${url}`);
|
||||
const data = await EleventyFetch(url, {
|
||||
duration: "15m",
|
||||
type: "json",
|
||||
});
|
||||
console.log(`[lastfmActivity] Indiekit ${endpoint} success`);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.log(
|
||||
`[lastfmActivity] Indiekit API unavailable for ${endpoint}: ${error.message}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function () {
|
||||
try {
|
||||
console.log("[lastfmActivity] Fetching Last.fm data...");
|
||||
|
||||
// Fetch all data from Indiekit API
|
||||
const [nowPlaying, scrobbles, loved, stats] = await Promise.all([
|
||||
fetchFromIndiekit("now-playing"),
|
||||
fetchFromIndiekit("scrobbles"),
|
||||
fetchFromIndiekit("loved"),
|
||||
fetchFromIndiekit("stats"),
|
||||
]);
|
||||
|
||||
// Check if we got data
|
||||
const hasData = nowPlaying || scrobbles?.scrobbles?.length || stats?.summary;
|
||||
|
||||
if (!hasData) {
|
||||
console.log("[lastfmActivity] No data available from Indiekit");
|
||||
return {
|
||||
nowPlaying: null,
|
||||
scrobbles: [],
|
||||
loved: [],
|
||||
stats: null,
|
||||
username: LASTFM_USERNAME,
|
||||
profileUrl: LASTFM_USERNAME ? `https://www.last.fm/user/${LASTFM_USERNAME}` : null,
|
||||
source: "unavailable",
|
||||
};
|
||||
}
|
||||
|
||||
console.log("[lastfmActivity] Using Indiekit API data");
|
||||
|
||||
return {
|
||||
nowPlaying: nowPlaying || null,
|
||||
scrobbles: scrobbles?.scrobbles || [],
|
||||
loved: loved?.loved || [],
|
||||
stats: stats || null,
|
||||
username: LASTFM_USERNAME,
|
||||
profileUrl: LASTFM_USERNAME ? `https://www.last.fm/user/${LASTFM_USERNAME}` : null,
|
||||
source: "indiekit",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[lastfmActivity] Error:", error.message);
|
||||
return {
|
||||
nowPlaying: null,
|
||||
scrobbles: [],
|
||||
loved: [],
|
||||
stats: null,
|
||||
username: LASTFM_USERNAME,
|
||||
profileUrl: null,
|
||||
source: "error",
|
||||
};
|
||||
}
|
||||
}
|
||||
432
listening.njk
Normal file
432
listening.njk
Normal file
@@ -0,0 +1,432 @@
|
||||
---
|
||||
layout: layouts/base.njk
|
||||
title: Listening Activity
|
||||
permalink: /listening/
|
||||
withSidebar: true
|
||||
---
|
||||
<div class="listening-page" x-data="{ activeTab: 'all', activeSource: 'all' }">
|
||||
<header class="mb-8">
|
||||
<h1 class="text-4xl font-bold text-surface-900 dark:text-surface-100 mb-2">Listening Activity</h1>
|
||||
<p class="text-surface-600 dark:text-surface-400">
|
||||
What I've been listening to on
|
||||
{% if funkwhaleActivity.instanceUrl %}
|
||||
<a href="{{ funkwhaleActivity.instanceUrl }}" class="text-primary-600 dark:text-primary-400 hover:underline" target="_blank" rel="noopener">Funkwhale</a>
|
||||
{% endif %}
|
||||
{% if funkwhaleActivity.instanceUrl and lastfmActivity.profileUrl %} and {% endif %}
|
||||
{% if lastfmActivity.profileUrl %}
|
||||
<a href="{{ lastfmActivity.profileUrl }}" class="text-red-600 dark:text-red-400 hover:underline" target="_blank" rel="noopener">Last.fm</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{# Source Filter Tabs #}
|
||||
<div class="flex gap-2 mb-8 flex-wrap">
|
||||
<button
|
||||
@click="activeSource = 'all'"
|
||||
:class="activeSource === 'all' ? 'bg-primary-600 text-white' : 'bg-surface-100 dark:bg-surface-800 text-surface-700 dark:text-surface-300 hover:bg-surface-200 dark:hover:bg-surface-700'"
|
||||
class="px-4 py-2 rounded-full text-sm font-medium transition-colors"
|
||||
>
|
||||
All Sources
|
||||
</button>
|
||||
{% if funkwhaleActivity.source == 'indiekit' %}
|
||||
<button
|
||||
@click="activeSource = 'funkwhale'"
|
||||
:class="activeSource === 'funkwhale' ? 'bg-purple-600 text-white' : 'bg-surface-100 dark:bg-surface-800 text-surface-700 dark:text-surface-300 hover:bg-surface-200 dark:hover:bg-surface-700'"
|
||||
class="px-4 py-2 rounded-full text-sm font-medium transition-colors flex items-center gap-2"
|
||||
>
|
||||
<span class="w-2 h-2 rounded-full bg-purple-500"></span>
|
||||
Funkwhale
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if lastfmActivity.source == 'indiekit' %}
|
||||
<button
|
||||
@click="activeSource = 'lastfm'"
|
||||
:class="activeSource === 'lastfm' ? 'bg-red-600 text-white' : 'bg-surface-100 dark:bg-surface-800 text-surface-700 dark:text-surface-300 hover:bg-surface-200 dark:hover:bg-surface-700'"
|
||||
class="px-4 py-2 rounded-full text-sm font-medium transition-colors flex items-center gap-2"
|
||||
>
|
||||
<span class="w-2 h-2 rounded-full bg-red-500"></span>
|
||||
Last.fm
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Now Playing Section - Combined #}
|
||||
{% set fwNowPlaying = funkwhaleActivity.nowPlaying if funkwhaleActivity.nowPlaying and funkwhaleActivity.nowPlaying.status else null %}
|
||||
{% set lfmNowPlaying = lastfmActivity.nowPlaying if lastfmActivity.nowPlaying and lastfmActivity.nowPlaying.status else null %}
|
||||
|
||||
{% if fwNowPlaying or lfmNowPlaying %}
|
||||
<section class="mb-12">
|
||||
{# Funkwhale Now Playing #}
|
||||
{% if fwNowPlaying %}
|
||||
<div x-show="activeSource === 'all' || activeSource === 'funkwhale'" class="mb-4">
|
||||
<div class="relative p-6 rounded-2xl overflow-hidden {% if fwNowPlaying.status == 'now-playing' %}bg-gradient-to-br from-green-500/10 to-green-600/5 border-2 border-green-500/30{% else %}bg-gradient-to-br from-purple-500/10 to-purple-600/5 border border-purple-500/20{% endif %}">
|
||||
<div class="flex items-center gap-5">
|
||||
{% if fwNowPlaying.coverUrl %}
|
||||
<img src="{{ fwNowPlaying.coverUrl }}" alt="" class="w-24 h-24 rounded-lg shadow-lg object-cover" loading="lazy">
|
||||
{% else %}
|
||||
<div class="w-24 h-24 rounded-lg bg-surface-200 dark:bg-surface-700 flex items-center justify-center">
|
||||
<svg class="w-10 h-10 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 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium bg-purple-500/20 text-purple-700 dark:text-purple-400 rounded-full">Funkwhale</span>
|
||||
{% if fwNowPlaying.status == 'now-playing' %}
|
||||
<span class="inline-flex items-center gap-1.5 px-2 py-1 text-xs font-medium bg-green-500/20 text-green-700 dark:text-green-400 rounded-full">
|
||||
<span class="flex gap-0.5 items-end h-3">
|
||||
<span class="w-0.5 bg-green-500 animate-pulse" style="height: 30%; animation-delay: 0s;"></span>
|
||||
<span class="w-0.5 bg-green-500 animate-pulse" style="height: 70%; animation-delay: 0.2s;"></span>
|
||||
<span class="w-0.5 bg-green-500 animate-pulse" style="height: 50%; animation-delay: 0.4s;"></span>
|
||||
</span>
|
||||
Now Playing
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center gap-1.5 px-2 py-1 text-xs font-medium bg-primary-500/20 text-primary-700 dark:text-primary-400 rounded-full">
|
||||
Recently Played
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h2 class="text-xl font-bold text-surface-900 dark:text-surface-100 truncate">
|
||||
{% if fwNowPlaying.trackUrl %}
|
||||
<a href="{{ fwNowPlaying.trackUrl }}" class="hover:text-primary-600 dark:hover:text-primary-400" target="_blank" rel="noopener">{{ fwNowPlaying.track }}</a>
|
||||
{% else %}
|
||||
{{ fwNowPlaying.track }}
|
||||
{% endif %}
|
||||
</h2>
|
||||
<p class="text-surface-600 dark:text-surface-400">{{ fwNowPlaying.artist }}</p>
|
||||
{% if fwNowPlaying.album %}
|
||||
<p class="text-sm text-surface-500 mt-1">{{ fwNowPlaying.album }}</p>
|
||||
{% endif %}
|
||||
<p class="text-xs text-surface-500 mt-2">{{ fwNowPlaying.relativeTime }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Last.fm Now Playing #}
|
||||
{% if lfmNowPlaying %}
|
||||
<div x-show="activeSource === 'all' || activeSource === 'lastfm'" class="mb-4">
|
||||
<div class="relative p-6 rounded-2xl overflow-hidden {% if lfmNowPlaying.status == 'now-playing' %}bg-gradient-to-br from-green-500/10 to-green-600/5 border-2 border-green-500/30{% else %}bg-gradient-to-br from-red-500/10 to-red-600/5 border border-red-500/20{% endif %}">
|
||||
<div class="flex items-center gap-5">
|
||||
{% if lfmNowPlaying.coverUrl %}
|
||||
<img src="{{ lfmNowPlaying.coverUrl }}" alt="" class="w-24 h-24 rounded-lg shadow-lg object-cover" loading="lazy">
|
||||
{% else %}
|
||||
<div class="w-24 h-24 rounded-lg bg-surface-200 dark:bg-surface-700 flex items-center justify-center">
|
||||
<svg class="w-10 h-10 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 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium bg-red-500/20 text-red-700 dark:text-red-400 rounded-full">Last.fm</span>
|
||||
{% if lfmNowPlaying.status == 'now-playing' %}
|
||||
<span class="inline-flex items-center gap-1.5 px-2 py-1 text-xs font-medium bg-green-500/20 text-green-700 dark:text-green-400 rounded-full">
|
||||
<span class="flex gap-0.5 items-end h-3">
|
||||
<span class="w-0.5 bg-green-500 animate-pulse" style="height: 30%; animation-delay: 0s;"></span>
|
||||
<span class="w-0.5 bg-green-500 animate-pulse" style="height: 70%; animation-delay: 0.2s;"></span>
|
||||
<span class="w-0.5 bg-green-500 animate-pulse" style="height: 50%; animation-delay: 0.4s;"></span>
|
||||
</span>
|
||||
Now Playing
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center gap-1.5 px-2 py-1 text-xs font-medium bg-primary-500/20 text-primary-700 dark:text-primary-400 rounded-full">
|
||||
Recently Played
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if lfmNowPlaying.loved %}
|
||||
<span class="text-red-500" title="Loved">♥</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h2 class="text-xl font-bold text-surface-900 dark:text-surface-100 truncate">
|
||||
{% if lfmNowPlaying.trackUrl %}
|
||||
<a href="{{ lfmNowPlaying.trackUrl }}" class="hover:text-red-600 dark:hover:text-red-400" target="_blank" rel="noopener">{{ lfmNowPlaying.track }}</a>
|
||||
{% else %}
|
||||
{{ lfmNowPlaying.track }}
|
||||
{% endif %}
|
||||
</h2>
|
||||
<p class="text-surface-600 dark:text-surface-400">{{ lfmNowPlaying.artist }}</p>
|
||||
{% if lfmNowPlaying.album %}
|
||||
<p class="text-sm text-surface-500 mt-1">{{ lfmNowPlaying.album }}</p>
|
||||
{% endif %}
|
||||
<p class="text-xs text-surface-500 mt-2">{{ lfmNowPlaying.relativeTime }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{# Combined Stats Section #}
|
||||
{% if funkwhaleActivity.stats or lastfmActivity.stats %}
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-100 mb-6 flex items-center gap-2">
|
||||
<svg class="w-6 h-6 text-primary-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||||
</svg>
|
||||
Listening Statistics
|
||||
</h2>
|
||||
|
||||
{# Stats Cards Grid - Side by Side #}
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
{# Funkwhale Stats #}
|
||||
{% if funkwhaleActivity.stats %}
|
||||
<div x-show="activeSource === 'all' || activeSource === 'funkwhale'" class="bg-white dark:bg-surface-800 rounded-xl p-6 border border-purple-200 dark:border-purple-800">
|
||||
<h3 class="text-lg font-semibold text-purple-700 dark:text-purple-400 mb-4 flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-full bg-purple-500"></span>
|
||||
Funkwhale
|
||||
</h3>
|
||||
<div class="grid grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-surface-900 dark:text-surface-100">{{ funkwhaleActivity.stats.summary.all.totalPlays | default(0) }}</div>
|
||||
<div class="text-xs text-surface-500 uppercase">Plays</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-surface-900 dark:text-surface-100">{{ funkwhaleActivity.stats.summary.all.uniqueArtists | default(0) }}</div>
|
||||
<div class="text-xs text-surface-500 uppercase">Artists</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-surface-900 dark:text-surface-100">{{ funkwhaleActivity.stats.summary.all.uniqueTracks | default(0) }}</div>
|
||||
<div class="text-xs text-surface-500 uppercase">Tracks</div>
|
||||
</div>
|
||||
</div>
|
||||
{# Top Artists #}
|
||||
{% if funkwhaleActivity.stats.topArtists.all.length %}
|
||||
<div class="mt-4 pt-4 border-t border-surface-200 dark:border-surface-700">
|
||||
<h4 class="text-sm font-medium text-surface-700 dark:text-surface-300 mb-2">Top Artists</h4>
|
||||
<div class="space-y-1">
|
||||
{% for artist in funkwhaleActivity.stats.topArtists.all | head(5) %}
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-surface-600 dark:text-surface-400 truncate">{{ artist.name }}</span>
|
||||
<span class="text-surface-500 ml-2">{{ artist.playCount }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Last.fm Stats #}
|
||||
{% if lastfmActivity.stats %}
|
||||
<div x-show="activeSource === 'all' || activeSource === 'lastfm'" class="bg-white dark:bg-surface-800 rounded-xl p-6 border border-red-200 dark:border-red-800">
|
||||
<h3 class="text-lg font-semibold text-red-700 dark:text-red-400 mb-4 flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-full bg-red-500"></span>
|
||||
Last.fm
|
||||
</h3>
|
||||
<div class="grid grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-surface-900 dark:text-surface-100">{{ lastfmActivity.stats.summary.all.totalPlays | default(0) }}</div>
|
||||
<div class="text-xs text-surface-500 uppercase">Scrobbles</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-surface-900 dark:text-surface-100">{{ lastfmActivity.stats.summary.all.uniqueArtists | default(0) }}</div>
|
||||
<div class="text-xs text-surface-500 uppercase">Artists</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-surface-900 dark:text-surface-100">{{ lastfmActivity.stats.summary.all.lovedCount | default(0) }}</div>
|
||||
<div class="text-xs text-surface-500 uppercase">Loved</div>
|
||||
</div>
|
||||
</div>
|
||||
{# Top Artists from Last.fm #}
|
||||
{% if lastfmActivity.stats.topArtists.all.length %}
|
||||
<div class="mt-4 pt-4 border-t border-surface-200 dark:border-surface-700">
|
||||
<h4 class="text-sm font-medium text-surface-700 dark:text-surface-300 mb-2">Top Artists</h4>
|
||||
<div class="space-y-1">
|
||||
{% for artist in lastfmActivity.stats.topArtists.all | head(5) %}
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-surface-600 dark:text-surface-400 truncate">{{ artist.name }}</span>
|
||||
<span class="text-surface-500 ml-2">{{ artist.playCount }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{# Recent Listens - Combined Timeline #}
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-100 mb-6 flex items-center gap-2">
|
||||
<svg class="w-6 h-6 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
Recent Listens
|
||||
</h2>
|
||||
|
||||
<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-white 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">
|
||||
{% if listening.coverUrl %}
|
||||
<img src="{{ listening.coverUrl }}" alt="" class="w-12 h-12 rounded object-cover flex-shrink-0" loading="lazy">
|
||||
{% 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 %}
|
||||
</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-500 block">{{ listening.relativeTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% 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-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-red-400 dark:hover:border-red-600 transition-colors mb-2">
|
||||
{% if scrobble.coverUrl %}
|
||||
<img src="{{ scrobble.coverUrl }}" alt="" class="w-12 h-12 rounded object-cover flex-shrink-0" loading="lazy">
|
||||
{% 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-red-600 dark:hover:text-red-400" target="_blank" rel="noopener">{{ scrobble.track }}</a>
|
||||
{% else %}
|
||||
{{ scrobble.track }}
|
||||
{% endif %}
|
||||
{% if scrobble.loved %}
|
||||
<span class="text-red-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-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 rounded-full mb-1">Last.fm</span>
|
||||
<span class="text-xs text-surface-500 block">{{ scrobble.relativeTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not funkwhaleActivity.listenings.length and not lastfmActivity.scrobbles.length %}
|
||||
<p class="text-surface-600 dark:text-surface-400">No recent listening history available.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{# Loved Tracks from Last.fm #}
|
||||
{% if lastfmActivity.loved.length %}
|
||||
<section class="mb-12" x-show="activeSource === 'all' || activeSource === 'lastfm'">
|
||||
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-100 mb-6 flex items-center gap-2">
|
||||
<svg class="w-6 h-6 text-red-500" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
|
||||
</svg>
|
||||
Loved Tracks
|
||||
<span class="text-sm font-normal text-surface-500">(Last.fm)</span>
|
||||
</h2>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
{% for track in lastfmActivity.loved | head(10) %}
|
||||
<div class="flex items-center gap-3 p-3 bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700">
|
||||
{% if track.coverUrl %}
|
||||
<img src="{{ track.coverUrl }}" alt="" class="w-14 h-14 rounded object-cover flex-shrink-0" loading="lazy">
|
||||
{% else %}
|
||||
<div class="w-14 h-14 rounded bg-surface-200 dark:bg-surface-700 flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-red-400" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-medium text-surface-900 dark:text-surface-100 truncate">
|
||||
{% if track.trackUrl %}
|
||||
<a href="{{ track.trackUrl }}" class="hover:text-red-600 dark:hover:text-red-400" target="_blank" rel="noopener">{{ track.track }}</a>
|
||||
{% else %}
|
||||
{{ track.track }}
|
||||
{% endif %}
|
||||
</h3>
|
||||
<p class="text-sm text-surface-600 dark:text-surface-400 truncate">{{ track.artist }}</p>
|
||||
</div>
|
||||
|
||||
<span class="text-red-500 flex-shrink-0">♥</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{# Funkwhale Favorites #}
|
||||
{% if funkwhaleActivity.favorites.length %}
|
||||
<section class="mb-12" x-show="activeSource === 'all' || activeSource === 'funkwhale'">
|
||||
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-100 mb-6 flex items-center gap-2">
|
||||
<svg class="w-6 h-6 text-purple-500" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
|
||||
</svg>
|
||||
Favorite Tracks
|
||||
<span class="text-sm font-normal text-surface-500">(Funkwhale)</span>
|
||||
</h2>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
{% for favorite in funkwhaleActivity.favorites | head(10) %}
|
||||
<div class="flex items-center gap-3 p-3 bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700">
|
||||
{% if favorite.coverUrl %}
|
||||
<img src="{{ favorite.coverUrl }}" alt="" class="w-14 h-14 rounded object-cover flex-shrink-0" loading="lazy">
|
||||
{% else %}
|
||||
<div class="w-14 h-14 rounded bg-surface-200 dark:bg-surface-700 flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-purple-400" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-medium text-surface-900 dark:text-surface-100 truncate">
|
||||
{% if favorite.trackUrl %}
|
||||
<a href="{{ favorite.trackUrl }}" class="hover:text-purple-600 dark:hover:text-purple-400" target="_blank" rel="noopener">{{ favorite.track }}</a>
|
||||
{% else %}
|
||||
{{ favorite.track }}
|
||||
{% endif %}
|
||||
</h3>
|
||||
<p class="text-sm text-surface-600 dark:text-surface-400 truncate">{{ favorite.artist }}</p>
|
||||
{% if favorite.album %}
|
||||
<p class="text-xs text-surface-500 truncate">{{ favorite.album }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
Reference in New Issue
Block a user