Now playing

This commit is contained in:
Justin Edmund 2025-06-13 21:50:13 -04:00
parent cc6eba7df1
commit 199abb294f
5 changed files with 220 additions and 11 deletions

View file

@ -2,6 +2,7 @@
import { spring } from 'svelte/motion'
import type { Album } from '$lib/types/lastfm'
import { audioPreview } from '$lib/stores/audio-preview'
import NowPlaying from './NowPlaying.svelte'
interface AlbumProps {
album?: Album
@ -109,6 +110,9 @@
style="transform: scale({$scale})"
loading="lazy"
/>
{#if album.isNowPlaying}
<NowPlaying trackName={album.nowPlayingTrack !== album.name ? album.nowPlayingTrack : undefined} />
{/if}
{#if hasPreview && isHovering}
<button
class="preview-button"

View file

@ -0,0 +1,105 @@
<script lang="ts">
interface Props {
trackName?: string
}
let { trackName }: Props = $props()
</script>
<div class="now-playing">
<div class="equalizer" aria-label="Now playing">
<span class="bar"></span>
<span class="bar"></span>
<span class="bar"></span>
</div>
{#if trackName}
<span class="track-name">{trackName}</span>
{/if}
</div>
<style lang="scss">
.now-playing {
position: absolute;
top: $unit;
left: $unit;
display: flex;
align-items: center;
gap: $unit-half;
padding: $unit-half $unit;
background: rgba(0, 0, 0, 0.9);
color: white;
border-radius: $unit * 2;
font-size: $font-size-small;
backdrop-filter: blur(10px);
z-index: 10;
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.equalizer {
display: flex;
align-items: flex-end;
gap: 2px;
height: 16px;
}
.bar {
width: 3px;
background: $accent-color;
animation: dance 0.6s ease-in-out infinite;
transform-origin: bottom;
}
.bar:nth-child(1) {
height: 40%;
animation-delay: 0s;
}
.bar:nth-child(2) {
height: 60%;
animation-delay: 0.2s;
}
.bar:nth-child(3) {
height: 50%;
animation-delay: 0.4s;
}
@keyframes dance {
0%, 100% {
transform: scaleY(1);
}
50% {
transform: scaleY(1.8);
}
}
.track-name {
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: $font-weight-med;
}
@include breakpoint('phone') {
.now-playing {
font-size: $font-size-extra-small;
padding: $unit-fourth $unit-half;
}
.track-name {
display: none;
}
}
</style>

View file

@ -175,7 +175,7 @@ export async function transformAlbumData(appleMusicAlbum: AppleMusicAlbum) {
// Get preview URL from tracks if album doesn't have one
let previewUrl = attributes.previews?.[0]?.url
let tracks: Array<{ name: string; previewUrl?: string }> = []
let tracks: Array<{ name: string; previewUrl?: string; durationMs?: number }> = []
// Always fetch tracks to get preview URLs
if (appleMusicAlbum.id) {
@ -209,12 +209,13 @@ export async function transformAlbumData(appleMusicAlbum: AppleMusicAlbum) {
.filter((item: any) => item.type === 'songs')
.map((track: any) => ({
name: track.attributes?.name || 'Unknown',
previewUrl: track.attributes?.previews?.[0]?.url
previewUrl: track.attributes?.previews?.[0]?.url,
durationMs: track.attributes?.durationInMillis
}))
// Log track details
tracks.forEach((track, index) => {
console.log(`Track ${index + 1}: ${track.name} - Preview: ${track.previewUrl ? 'Yes' : 'No'}`)
console.log(`Track ${index + 1}: ${track.name} - Preview: ${track.previewUrl ? 'Yes' : 'No'} - Duration: ${track.durationMs}ms`)
})
// Find the first track with a preview if we don't have one

View file

@ -26,6 +26,8 @@ export interface Album {
url: string
rank: number
images: AlbumImages
isNowPlaying?: boolean
nowPlayingTrack?: string
appleMusicData?: {
appleMusicId?: string
highResArtwork?: string
@ -40,6 +42,7 @@ export interface Album {
tracks?: Array<{
name: string
previewUrl?: string
durationMs?: number
}>
}
}

View file

@ -10,13 +10,24 @@ const LASTFM_API_KEY = process.env.LASTFM_API_KEY
const USERNAME = 'jedmund'
const ALBUM_LIMIT = 10
// Store last played tracks with timestamps
interface TrackPlayInfo {
albumName: string
trackName: string
scrobbleTime: Date
durationMs?: number
}
let recentTracks: TrackPlayInfo[] = []
export const GET: RequestHandler = async ({ url }) => {
const client = new LastClient(LASTFM_API_KEY || '')
const testMode = url.searchParams.get('test') === 'nowplaying'
try {
// const albums = await getWeeklyAlbumChart(client, USERNAME)
const albums = await getRecentAlbums(client, USERNAME, ALBUM_LIMIT)
const albums = await getRecentAlbums(client, USERNAME, ALBUM_LIMIT, testMode)
// console.log(albums)
const enrichedAlbums = await Promise.all(
albums.slice(0, ALBUM_LIMIT).map(async (album) => {
@ -58,16 +69,39 @@ async function getWeeklyAlbumChart(client: LastClient, username: string): Promis
async function getRecentAlbums(
client: LastClient,
username: string,
limit: number
limit: number,
testMode: boolean = false
): Promise<Album[]> {
const recentTracks = await client.user.getRecentTracks(username, { limit: 50, extended: true })
const recentTracksResponse = await client.user.getRecentTracks(username, { limit: 50, extended: true })
const uniqueAlbums = new Map<string, Album>()
for (const track of recentTracks.tracks) {
let nowPlayingTrack: string | undefined
let isFirstAlbum = true
// Clear old tracks and collect new track play information
recentTracks = []
for (const track of recentTracksResponse.tracks) {
// Store track play information for now playing calculation
if (track.date) {
recentTracks.push({
albumName: track.album.name,
trackName: track.name,
scrobbleTime: track.date
})
}
if (uniqueAlbums.size >= limit) break
// Check if this is the currently playing track
if (track.nowPlaying && !nowPlayingTrack) {
nowPlayingTrack = track.name
}
const albumKey = `${track.album.mbid || track.album.name}`
if (!uniqueAlbums.has(albumKey)) {
// For testing: mark first album as now playing
const isNowPlaying = testMode && isFirstAlbum ? true : (track.nowPlaying || false)
uniqueAlbums.set(albumKey, {
name: track.album.name,
artist: {
@ -78,7 +112,19 @@ async function getRecentAlbums(
images: transformImages(track.images),
mbid: track.album.mbid || '',
url: track.url,
rank: uniqueAlbums.size + 1
rank: uniqueAlbums.size + 1,
// Mark if this album contains the now playing track
isNowPlaying: isNowPlaying,
nowPlayingTrack: isNowPlaying ? track.name : undefined
})
isFirstAlbum = false
} else if (track.nowPlaying) {
// If album already exists but this track is now playing, update it
const existingAlbum = uniqueAlbums.get(albumKey)!
uniqueAlbums.set(albumKey, {
...existingAlbum,
isNowPlaying: true,
nowPlayingTrack: track.name
})
}
}
@ -111,8 +157,12 @@ async function searchAppleMusicForAlbum(album: Album): Promise<Album> {
hasPreview: !!cachedData.previewUrl,
trackCount: cachedData.tracks?.length || 0
})
// Check if this album is currently playing based on track durations
const updatedAlbum = checkNowPlaying(album, cachedData)
return {
...album,
...updatedAlbum,
images: {
...album.images,
itunes: cachedData.highResArtwork || album.images.itunes
@ -129,9 +179,12 @@ async function searchAppleMusicForAlbum(album: Album): Promise<Album> {
// Cache the result for 24 hours
await redis.set(cacheKey, JSON.stringify(transformedData), 'EX', 86400)
// Check if this album is currently playing based on track durations
const updatedAlbum = checkNowPlaying(album, transformedData)
return {
...album,
...updatedAlbum,
images: {
...album.images,
itunes: transformedData.highResArtwork || album.images.itunes
@ -182,3 +235,46 @@ function transformImages(images: LastfmImage[]): AlbumImages {
return transformedImages
}
function checkNowPlaying(album: Album, appleMusicData: any): Album {
// Don't override if already marked as now playing by Last.fm
if (album.isNowPlaying) {
return album
}
// Check if any recent track from this album could still be playing
const now = new Date()
const SCROBBLE_LAG = 3 * 60 * 1000 // 3 minutes in milliseconds
for (const trackInfo of recentTracks) {
if (trackInfo.albumName !== album.name) continue
// Find the track duration from Apple Music data
const trackData = appleMusicData.tracks?.find((t: any) =>
t.name.toLowerCase() === trackInfo.trackName.toLowerCase()
)
if (trackData?.durationMs) {
// Calculate when the track should end (scrobble time + duration + lag)
const trackEndTime = new Date(trackInfo.scrobbleTime.getTime() + trackData.durationMs + SCROBBLE_LAG)
// If current time is before track end time, it's likely still playing
if (now < trackEndTime) {
console.log(`Detected now playing: "${trackInfo.trackName}" from "${album.name}"`, {
scrobbleTime: trackInfo.scrobbleTime,
durationMs: trackData.durationMs,
estimatedEndTime: trackEndTime,
currentTime: now
})
return {
...album,
isNowPlaying: true,
nowPlayingTrack: trackInfo.trackName
}
}
}
}
return album
}