Remove recently played from music

This commit is contained in:
SirBlob committed 2026-11-01 18:42:28 -05:00
1 parent 90d61c48e2
commit f5df946e0b
5 files changed
+7 -206

No files matched your search

@@ -5,7 +5,6 @@
import Panel from '../../ui/Panel.svelte';
import ActivityPanel from './ActivityPanel.svelte';
import ListeningFetch from './ListeningFetch.svelte';
import RecentTracks from './RecentTracks.svelte';
import TopCharts from './TopCharts.svelte';
import TrendingArtists from './TrendingArtists.svelte';
@@ -16,18 +15,13 @@
<Panel title="last.fm" icon="simple-icons:lastdotfm">
<ListeningFetch {listening} />
</Panel>
<Panel title={listening.nowPlaying ? 'Now playing' : 'Recently played'} icon="mdi:headphones">
<RecentTracks nowPlaying={listening.nowPlaying} recentTracks={listening.recentTracks} />
</Panel>
</div>
<div class="overview">
<ActivityPanel {listening} />
<Panel title="Trending this week" icon="mdi:trending-up">
<TrendingArtists artists={listening.trending} />
</Panel>
</div>
<ActivityPanel {listening} />
<h3 class="subheading"><Icon icon="mdi:trophy-outline" width="16" /> Top charts</h3>
<TopCharts top={listening.top} />
@@ -1,157 +0,0 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import type { PlayedTrack } from '#lib/lastfm/types';
let { nowPlaying, recentTracks }: { nowPlaying: PlayedTrack | null; recentTracks: PlayedTrack[] } = $props();
const shownCount = 5;
const tracks = $derived(
[...(nowPlaying ? [nowPlaying] : []), ...recentTracks].slice(0, shownCount)
);
function timeAgo(playedAt: number | null): string {
if (playedAt === null) return 'now';
const minutes = Math.floor((Date.now() / 1000 - playedAt) / 60);
if (minutes < 60) return `${Math.max(1, minutes)}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
return `${Math.floor(hours / 24)}d ago`;
}
</script>
{#if tracks.length}
<ul class="tracks">
{#each tracks as track, index (`${track.playedAt}-${index}`)}
<li>
<a class="track" class:playing={track.playedAt === null} href={track.url} target="_blank" rel="noopener noreferrer">
{#if track.image}
<img src={track.image} alt={track.album} loading="lazy" />
{:else}
<span class="cover-fallback"><Icon icon="mdi:music-note" width="18" /></span>
{/if}
<span class="text">
<span class="name">{track.track}</span>
<span class="muted artist">{track.artist}</span>
</span>
<span class="when">
{#if track.playedAt === null}
<span class="equalizer" aria-label="Now playing"><span></span><span></span><span></span></span>
{:else}
<span class="muted">{timeAgo(track.playedAt)}</span>
{/if}
</span>
</a>
</li>
{/each}
</ul>
{:else}
<p class="muted">No scrobbles yet.</p>
{/if}
<style>
.tracks {
display: flex;
flex-direction: column;
gap: 0.3rem;
margin: 0;
padding: 0;
list-style: none;
}
.track {
display: flex;
align-items: center;
gap: 0.65rem;
padding: 0.3rem;
border: 1px solid transparent;
}
.track:hover {
border-color: var(--accent);
}
.track.playing {
border-color: color-mix(in srgb, var(--green) 50%, transparent);
}
img,
.cover-fallback {
flex-shrink: 0;
width: 40px;
height: 40px;
object-fit: cover;
}
.cover-fallback {
display: grid;
place-items: center;
background: color-mix(in srgb, var(--foreground) 8%, transparent);
color: var(--muted);
}
.text {
display: flex;
flex: 1;
flex-direction: column;
min-width: 0;
}
.name,
.artist {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.name {
font-size: 0.88rem;
}
.artist,
.when {
font-size: 0.75rem;
}
.when {
flex-shrink: 0;
}
.equalizer {
display: inline-flex;
align-items: flex-end;
gap: 2px;
height: 14px;
}
.equalizer span {
width: 3px;
background: var(--green);
animation: equalize 0.9s ease-in-out infinite;
}
.equalizer span:nth-child(2) {
animation-delay: 0.2s;
}
.equalizer span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes equalize {
0%,
100% {
height: 30%;
}
50% {
height: 100%;
}
}
@media (prefers-reduced-motion: reduce) {
.equalizer span {
animation: none;
height: 70%;
}
}
</style>
-2
View File
@@ -48,8 +48,6 @@ export interface ListeningProfile {
profileUrl: string;
registeredAt: number | null;
totals: ListeningTotals;
nowPlaying: PlayedTrack | null;
recentTracks: PlayedTrack[];
daily: CountBucket[];
monthly: CountBucket[];
hourly: CountBucket[];
-7
View File
@@ -121,13 +121,6 @@ export async function fetchRecentTracksPage(
};
}
export async function fetchNowPlaying(userName: string): Promise<PlayedTrack | null> {
const { recenttracks } = await callLastfm<RawRecentTracks>('user.getrecenttracks', { user: userName, limit: '1' });
const rawTracks = Array.isArray(recenttracks.track) ? recenttracks.track : [recenttracks.track];
const nowPlayingRaw = rawTracks.find((raw) => raw['@attr']?.nowplaying === 'true');
return nowPlayingRaw ? toPlayedTrack(nowPlayingRaw) : null;
}
export async function fetchTrackImage(artist: string, track: string): Promise<string | null> {
const { track: info } = await callLastfm<RawTrackInfo>('track.getinfo', { artist, track, autocorrect: '1' });
return largestImage(info?.album?.image);
+5 -32
View File
@@ -1,7 +1,7 @@
import type { ListeningProfile, PlayedTrack } from '#lib/lastfm/types';
import { fetchNowPlaying, fetchUserInfo, hasLastfmKey, type UserInfo } from './api';
import type { ListeningProfile } from '#lib/lastfm/types';
import { fetchUserInfo, hasLastfmKey, type UserInfo } from './api';
import { loadScrobbleStore, storedScrobbles } from './store';
import { coverFor, coversVersion, loadCovers, requestTrackCovers } from './covers';
import { coversVersion, loadCovers, requestTrackCovers } from './covers';
import { lastfmSyncedAt } from './sync';
import {
countsByDay,
@@ -14,14 +14,11 @@ import {
} from './trends';
const userInfoDuration = 30 * 60 * 1000;
const nowPlayingDuration = 60 * 1000;
const summaryDuration = 5 * 60 * 1000;
const recentCount = 8;
type Summary = Omit<ListeningProfile, 'userName' | 'profileUrl' | 'registeredAt' | 'totals' | 'nowPlaying' | 'syncedAt'>;
type Summary = Omit<ListeningProfile, 'userName' | 'profileUrl' | 'registeredAt' | 'totals' | 'syncedAt'>;
let userInfoCache: { info: UserInfo; fetchedAt: number } | null = null;
let nowPlayingCache: { track: PlayedTrack | null; fetchedAt: number } | null = null;
let summaryCache: { summary: Summary; scrobbleCount: number; coversVersion: number; builtAt: number } | null = null;
async function cachedUserInfo(userName: string): Promise<UserInfo | null> {
@@ -34,27 +31,6 @@ async function cachedUserInfo(userName: string): Promise<UserInfo | null> {
return userInfoCache?.info ?? null;
}
async function cachedNowPlaying(userName: string): Promise<PlayedTrack | null> {
if (nowPlayingCache && Date.now() - nowPlayingCache.fetchedAt < nowPlayingDuration) return nowPlayingCache.track;
try {
nowPlayingCache = { track: await fetchNowPlaying(userName), fetchedAt: Date.now() };
} catch (error) {
console.error('Last.fm now playing failed:', error instanceof Error ? error.message : error);
}
return nowPlayingCache?.track ?? null;
}
function recentTracks(): PlayedTrack[] {
return storedScrobbles()
.slice(-recentCount)
.reverse()
.map((scrobble) => ({
...scrobble,
url: `https://www.last.fm/music/${encodeURIComponent(scrobble.artist)}/_/${encodeURIComponent(scrobble.track)}`,
image: coverFor(scrobble.artist, scrobble.album, scrobble.track)
}));
}
function buildSummary(): Summary {
const scrobbles = storedScrobbles();
const nowSeconds = Math.floor(Date.now() / 1000);
@@ -63,7 +39,6 @@ function buildSummary(): Summary {
const streaks = listeningStreaks(dayCounts);
return {
recentTracks: recentTracks(),
daily,
monthly: monthlyBuckets(dayCounts),
hourly: hourlyBuckets(scrobbles),
@@ -79,7 +54,6 @@ function buildSummary(): Summary {
function tracksMissingCovers(summary: Summary) {
return Object.values(summary.top)
.flatMap((charts) => charts.tracks)
.concat(summary.recentTracks.map((track) => ({ ...track, name: track.track, count: 0 })))
.filter((item) => !item.image && item.artist)
.map((item) => ({ artist: item.artist!, track: item.name }));
}
@@ -105,7 +79,7 @@ export async function getListeningProfile(userName: string): Promise<ListeningPr
if (!hasLastfmKey()) return null;
await Promise.all([loadScrobbleStore(), loadCovers()]);
const [info, nowPlaying] = await Promise.all([cachedUserInfo(userName), cachedNowPlaying(userName)]);
const info = await cachedUserInfo(userName);
if (!info && !storedScrobbles().length) return null;
const summary = cachedSummary();
@@ -119,7 +93,6 @@ export async function getListeningProfile(userName: string): Promise<ListeningPr
albums: info?.albums ?? 0,
tracks: info?.tracks ?? 0
},
nowPlaying,
syncedAt: lastfmSyncedAt(),
...summary
};