v1.0.2: Mood mix optimizations and media player improvements

- Fixed player seek flicker on podcasts (30s skip buttons)
- Added dual-layer seek lock mechanism to prevent stale time updates
- Optimized cached podcast seeking (direct seek before reload fallback)
- Large skips now execute immediately for responsive feel
- Mood mix performance optimizations
This commit is contained in:
Kevin O'Neill
2025-12-26 13:06:17 -06:00
parent d8c608cf70
commit f8b464feec
28 changed files with 5328 additions and 1615 deletions
@@ -2,7 +2,7 @@
import Image from "next/image";
import { SimilarArtist } from "../types";
import { Music } from "lucide-react";
import { Music, Library } from "lucide-react";
import { api } from "@/lib/api";
interface SimilarArtistsProps {
@@ -20,10 +20,11 @@ export function SimilarArtists({
return (
<section>
<h2 className="text-xl font-bold mb-4">
Fans Also Like
</h2>
<div data-tv-section="similar-artists" className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
<h2 className="text-xl font-bold mb-4">Fans Also Like</h2>
<div
data-tv-section="similar-artists"
className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4"
>
{similarArtists.map((artist, index) => {
const rawImage = artist.coverArt || artist.image;
const imageUrl = rawImage
@@ -33,17 +34,22 @@ export function SimilarArtists({
? Math.round(artist.weight * 100)
: null;
// For library artists, use the library ID; otherwise use mbid or name
const navigationId = artist.inLibrary
? artist.id
: artist.mbid || artist.id;
return (
<div
key={artist.id || artist.name}
data-tv-card
data-tv-card-index={index}
tabIndex={0}
onClick={() => onNavigate(artist.mbid || artist.id)}
onClick={() => onNavigate(navigationId)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
if (e.key === "Enter") {
e.preventDefault();
onNavigate(artist.mbid || artist.id);
onNavigate(navigationId);
}
}}
className="bg-transparent hover:bg-white/5 transition-all p-3 rounded-md cursor-pointer group"
@@ -64,6 +70,15 @@ export function SimilarArtists({
<Music className="w-12 h-12 text-gray-600" />
</div>
)}
{/* Library indicator badge */}
{artist.inLibrary && (
<div
className="absolute bottom-1 right-1 bg-[#ecb200] rounded-full p-1"
title="In your library"
>
<Library className="w-3 h-3 text-black" />
</div>
)}
</div>
{/* Artist Name */}
@@ -71,13 +86,13 @@ export function SimilarArtists({
{artist.name}
</h3>
{/* Album Count */}
{/* Album Count - show owned count if in library */}
<p className="text-xs text-gray-400 truncate">
{artist.ownedAlbumCount &&
artist.ownedAlbumCount > 0
? `${artist.ownedAlbumCount}/${artist.albumCount} albums`
: artist.albumCount && artist.albumCount > 0
? `${artist.albumCount} albums`
? `${artist.ownedAlbumCount} album${
artist.ownedAlbumCount > 1 ? "s" : ""
} in library`
: "Artist"}
</p>
+1
View File
@@ -56,4 +56,5 @@ export interface SimilarArtist {
albumCount?: number;
ownedAlbumCount?: number;
weight?: number;
inLibrary?: boolean;
}
+37 -22
View File
@@ -5,10 +5,10 @@
* and providing refresh functionality for mixes.
*/
import { useEffect } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { useAuth } from '@/lib/auth-context';
import { toast } from 'sonner';
import { useEffect } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { useAuth } from "@/lib/auth-context";
import { toast } from "sonner";
import type {
Artist,
ListenedItem,
@@ -16,7 +16,7 @@ import type {
Audiobook,
Mix,
PopularArtist,
} from '../types';
} from "../types";
import {
useRecentlyListenedQuery,
useRecentlyAddedQuery,
@@ -28,7 +28,7 @@ import {
useRefreshMixesMutation,
useBrowseAllQuery,
queryKeys,
} from '@/hooks/useQueries';
} from "@/hooks/useQueries";
interface PlaylistPreview {
id: string;
@@ -81,27 +81,38 @@ export function useHomeData(): UseHomeDataReturn {
const queryClient = useQueryClient();
// Listen for mixes-updated event (fired when user saves mood preferences)
// Use refetchQueries instead of invalidateQueries to force immediate UI update
useEffect(() => {
const handleMixesUpdated = () => {
queryClient.invalidateQueries({ queryKey: queryKeys.mixes() });
// refetchQueries forces immediate refetch, unlike invalidateQueries which only marks stale
queryClient.refetchQueries({ queryKey: queryKeys.mixes() });
};
window.addEventListener('mixes-updated', handleMixesUpdated);
return () => window.removeEventListener('mixes-updated', handleMixesUpdated);
window.addEventListener("mixes-updated", handleMixesUpdated);
return () =>
window.removeEventListener("mixes-updated", handleMixesUpdated);
}, [queryClient]);
// React Query hooks - these automatically handle caching, refetching, and loading states
const { data: recentlyListenedData, isLoading: isLoadingListened } = useRecentlyListenedQuery(10);
const { data: recentlyAddedData, isLoading: isLoadingAdded } = useRecentlyAddedQuery(10);
const { data: recommendedData, isLoading: isLoadingRecommended } = useRecommendationsQuery(10);
const { data: recentlyListenedData, isLoading: isLoadingListened } =
useRecentlyListenedQuery(10);
const { data: recentlyAddedData, isLoading: isLoadingAdded } =
useRecentlyAddedQuery(10);
const { data: recommendedData, isLoading: isLoadingRecommended } =
useRecommendationsQuery(10);
const { data: mixesData, isLoading: isLoadingMixes } = useMixesQuery();
const { data: popularData, isLoading: isLoadingPopular } = usePopularArtistsQuery(20);
const { data: podcastsData, isLoading: isLoadingPodcasts } = useTopPodcastsQuery(10);
const { data: audiobooksData, isLoading: isLoadingAudiobooks } = useAudiobooksQuery();
const { data: browseData, isLoading: isBrowseLoading } = useBrowseAllQuery();
const { data: popularData, isLoading: isLoadingPopular } =
usePopularArtistsQuery(20);
const { data: podcastsData, isLoading: isLoadingPodcasts } =
useTopPodcastsQuery(10);
const { data: audiobooksData, isLoading: isLoadingAudiobooks } =
useAudiobooksQuery();
const { data: browseData, isLoading: isBrowseLoading } =
useBrowseAllQuery();
// Mutation for refreshing mixes
const { mutateAsync: refreshMixes, isPending: isRefreshingMixes } = useRefreshMixesMutation();
const { mutateAsync: refreshMixes, isPending: isRefreshingMixes } =
useRefreshMixesMutation();
/**
* Refresh mixes and update cache
@@ -109,10 +120,10 @@ export function useHomeData(): UseHomeDataReturn {
const handleRefreshMixes = async () => {
try {
await refreshMixes();
toast.success('Mixes refreshed! Check out your new daily picks');
toast.success("Mixes refreshed! Check out your new daily picks");
} catch (error) {
console.error('Failed to refresh mixes:', error);
toast.error('Failed to refresh mixes');
console.error("Failed to refresh mixes:", error);
toast.error("Failed to refresh mixes");
}
};
@@ -136,8 +147,12 @@ export function useHomeData(): UseHomeDataReturn {
recommended: recommendedData?.artists || [],
mixes: Array.isArray(mixesData) ? mixesData : [],
popularArtists: popularData?.artists || [],
recentPodcasts: Array.isArray(podcastsData) ? podcastsData.slice(0, 10) : [],
recentAudiobooks: Array.isArray(audiobooksData) ? audiobooksData.slice(0, 10) : [],
recentPodcasts: Array.isArray(podcastsData)
? podcastsData.slice(0, 10)
: [],
recentAudiobooks: Array.isArray(audiobooksData)
? audiobooksData.slice(0, 10)
: [],
featuredPlaylists: browseData?.playlists || [],
isLoading,
isRefreshingMixes,