Skip to content

Commit a200c13

Browse files
Merge pull request #74 from ModioStudio/feature/theme-marketplace
feat(playback): enhance playback functionality and service management
2 parents 0a8db62 + ada2d3c commit a200c13

15 files changed

Lines changed: 751 additions & 62 deletions

File tree

.changeset/smooth-music-flow.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"MiniFy": minor
3+
---
4+
5+
Improved continuous music playback experience
6+
7+
- Added Spotify playlist context playback for seamless auto-advance through playlists
8+
- Implemented YouTube playback queue service for automatic track advancement
9+
- Added autoplay service (Random Queue) that plays recommendations after playlist/song ends
10+
- Added Spotify keep-alive service to prevent connection timeouts after inactivity
11+
- Refined AI DJ to better distinguish single track requests from continuous queue requests
12+
- Fixed AI Queue border to only show when explicit AI Queue is active

apps/desktop/src/hooks/useCurrentlyPlaying.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { invoke } from "@tauri-apps/api/core";
22
import { useEffect, useRef, useState } from "react";
33
import { useAIQueueStore } from "../lib/aiQueueStore";
4+
import { startAutoplayMonitor, stopAutoplayMonitor } from "../lib/playback/autoplayService";
5+
import { startKeepAlive, stopKeepAlive } from "../lib/playback/spotifyKeepAlive";
46
import {
57
type LastPlayedTrack,
68
type MusicProviderType,
@@ -133,7 +135,7 @@ export function useCurrentlyPlaying(pollMs = 3000) {
133135
const initialLoadDone = useRef<boolean>(false);
134136
const cachedTrackLoaded = useRef<boolean>(false);
135137

136-
// Load cached track on mount
138+
// Load cached track on mount and start services
137139
useEffect(() => {
138140
if (cachedTrackLoaded.current) return;
139141
cachedTrackLoaded.current = true;
@@ -156,10 +158,21 @@ export function useCurrentlyPlaying(pollMs = 3000) {
156158
setState(cacheToPlaybackState(cached));
157159
}
158160
}
161+
162+
// Start playback services
163+
startAutoplayMonitor();
164+
if (provider === "spotify") {
165+
startKeepAlive();
166+
}
159167
} catch (err) {
160168
console.error("Failed to load cached track:", err);
161169
}
162170
})();
171+
172+
return () => {
173+
stopAutoplayMonitor();
174+
stopKeepAlive();
175+
};
163176
}, []);
164177

165178
// Poll for current playback state

apps/desktop/src/lib/aiClient.ts

Lines changed: 57 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -19,64 +19,65 @@ Example TOON track data:
1919
Save Your Tears,The Weeknd,spotify:track:5QO79kh1waicV47BqGRL3g
2020
Starboy,The Weeknd & Daft Punk,spotify:track:7MXVkk9YMctZqd1Srtv4MB
2121
22-
## Your Capabilities
23-
24-
### AI Queue (Continuous Playback) - IMPORTANT!
25-
- startAIQueueWithMood: Start continuous music playback based on a mood/genre. Use this when users want ongoing music!
26-
- stopAIQueuePlayback: Stop the AI Queue
27-
- getAIQueueStatus: Check if AI Queue is running
28-
29-
**WHEN TO USE AI QUEUE:**
30-
- User says "play music for X" (work, studying, workout, relaxing, etc.)
31-
- User wants continuous/ongoing music without manually selecting tracks
32-
- User mentions "lofi", "background music", "playlist", "mix", or similar
33-
- User says things like "find me music and keep playing" or "play this kind of music for a while"
34-
35-
**EXAMPLES that should trigger AI Queue:**
36-
- "I want calm work music" → startAIQueueWithMood("calm focus music for working")
37-
- "Play lofi beats" → startAIQueueWithMood("lofi hip hop beats for relaxation")
38-
- "I need workout music" → startAIQueueWithMood("high energy workout music")
39-
- "Play something relaxing for the evening" → startAIQueueWithMood("relaxing evening vibes")
40-
41-
**If you're unsure whether to start the queue, ask:** "Should I start the AI Queue to continuously play [mood] music?"
42-
43-
### Playback Control (Single Tracks)
44-
- getCurrentTrack: See what's currently playing
45-
- playTrack: Play a single specific track by its Spotify URI
46-
- searchTracks: Search for tracks by name, artist, or query
47-
48-
### User Music Profile Analysis
49-
- getRecentlyPlayed: View recently played tracks
50-
- getTopTracks: Get most played tracks (short_term=4 weeks, medium_term=6 months, long_term=years)
51-
- getTopArtists: Get favorite artists with their genres
52-
- getMusicTaste: Deep analysis of listening patterns (energy, mood, danceability, tempo, acousticness)
53-
- getUserProfile: Get account info and library size
54-
55-
### Smart Recommendations
56-
- getRecommendations: Get Spotify-powered recommendations based on seeds and audio targets
57-
58-
## Strategy Guidelines
59-
60-
1. **For continuous playback requests**: Use startAIQueueWithMood - don't play single tracks!
61-
2. **For specific song requests**: Use searchTracks + playTrack
62-
3. **For "play something good"**: Consider AI Queue for ongoing music, or single track for quick play
63-
4. **For mood-based requests**: AI Queue is usually the best choice
64-
5. **When suggesting**: Explain WHY you chose this approach
65-
66-
## Audio Feature Reference
67-
- energy: 0.0 (calm) to 1.0 (intense)
68-
- valence: 0.0 (sad/dark) to 1.0 (happy/cheerful)
69-
- danceability: 0.0 (not danceable) to 1.0 (very danceable)
70-
- tempo: BPM (60-80 slow, 100-130 moderate, 140+ fast)
71-
- acousticness: 0.0 (electronic) to 1.0 (acoustic)
22+
## CRITICAL: Single Track vs AI Queue Decision
23+
24+
### SINGLE TRACK (use playTrack) - DEFAULT CHOICE
25+
Use playTrack for:
26+
- Specific song requests: "play Blinding Lights", "put on Bohemian Rhapsody"
27+
- Artist + song: "play Shape of You by Ed Sheeran"
28+
- "Play this song", "play that track"
29+
- Any request naming a specific song/track
30+
- "Play something by [artist]" (search and play one track)
31+
- Quick requests without mood/continuous keywords
32+
33+
**EXAMPLES - USE playTrack (NOT AI Queue):**
34+
- "Play Blinding Lights" → searchTracks + playTrack
35+
- "Put on some Daft Punk" → searchTracks + playTrack (ONE song)
36+
- "Play that new Taylor Swift song" → searchTracks + playTrack
37+
- "Can you play Starboy?" → searchTracks + playTrack
38+
- "Play something good" → searchTracks + playTrack (recommend ONE track)
39+
40+
### AI QUEUE (use startAIQueueWithMood) - ONLY WHEN EXPLICITLY NEEDED
41+
Use AI Queue ONLY when user explicitly wants continuous/endless music:
42+
- "Play music for working/studying/gym" (activity-based continuous)
43+
- "Start a playlist of..." or "make me a mix of..."
44+
- "Keep playing similar music" or "play more like this"
45+
- "I want background music for..."
46+
- "Start the AI Queue" (explicit request)
47+
- Keywords: "continuous", "keep playing", "for hours", "background music", "mix", "playlist"
48+
49+
**EXAMPLES - USE AI Queue:**
50+
- "Play lofi for studying" → startAIQueueWithMood
51+
- "I need workout music for the next hour" → startAIQueueWithMood
52+
- "Start playing relaxing jazz" → startAIQueueWithMood
53+
- "Keep the music going" → startAIQueueWithMood
54+
55+
### IF UNSURE: Default to playTrack (single song)
56+
The autoplay system will automatically queue similar songs after the track ends.
57+
Only use AI Queue when continuous playback is EXPLICITLY requested.
58+
59+
## Your Tools
60+
61+
### Single Track Playback
62+
- playTrack: Play a specific track (PREFERRED for most requests)
63+
- searchTracks: Search for tracks
64+
- getCurrentTrack: See what's playing
65+
66+
### AI Queue (Continuous Mode)
67+
- startAIQueueWithMood: Start continuous playback (ONLY for explicit continuous requests)
68+
- stopAIQueuePlayback: Stop the queue
69+
- getAIQueueStatus: Check queue status
70+
71+
### User Music Profile
72+
- getRecentlyPlayed: Recent tracks
73+
- getTopTracks: Most played tracks
74+
- getTopArtists: Favorite artists
7275
7376
## Personality
74-
- Be enthusiastic and knowledgeable about music
75-
- Reference specific data from the user's listening history
76-
- Make connections between artists and genres
77-
- Keep responses concise but insightful
78-
- Take action immediately when the user's intent is clear
79-
- If unsure about AI Queue, ask once - don't be overly cautious`;
77+
- Be enthusiastic about music
78+
- Act quickly - don't over-explain
79+
- When user asks to "play X", just play it immediately
80+
- Keep responses short and action-focused`;
8081

8182
export function createAIModel(providerType: AIProviderType, apiKey: string): LanguageModelV1 {
8283
switch (providerType) {
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { getActiveProvider, getActiveProviderType } from "../../providers";
2+
import type { UnifiedTrack } from "../../providers/types";
3+
import { getRelatedVideos, videoItemToTrackData } from "../../providers/youtube/client";
4+
import {
5+
addToQueue as spotifyAddToQueue,
6+
fetchRecommendations,
7+
getQueue as spotifyGetQueue,
8+
} from "../../ui/spotifyClient";
9+
import { useAIQueueStore } from "../aiQueueStore";
10+
import { usePlaybackQueueStore } from "./playbackQueueStore";
11+
12+
interface AutoplayState {
13+
enabled: boolean;
14+
lastProcessedTrackId: string | null;
15+
pendingAutoplayTracks: string[];
16+
}
17+
18+
const state: AutoplayState = {
19+
enabled: true,
20+
lastProcessedTrackId: null,
21+
pendingAutoplayTracks: [],
22+
};
23+
24+
let autoplayMonitorInterval: ReturnType<typeof setInterval> | null = null;
25+
26+
export function setAutoplayEnabled(enabled: boolean): void {
27+
state.enabled = enabled;
28+
if (enabled) {
29+
startAutoplayMonitor();
30+
} else {
31+
stopAutoplayMonitor();
32+
}
33+
}
34+
35+
export function isAutoplayEnabled(): boolean {
36+
return state.enabled;
37+
}
38+
39+
export function startAutoplayMonitor(): void {
40+
if (autoplayMonitorInterval) return;
41+
42+
autoplayMonitorInterval = setInterval(async () => {
43+
if (!state.enabled) return;
44+
45+
try {
46+
await checkAndTriggerAutoplay();
47+
} catch (err) {
48+
console.error("Autoplay monitor error:", err);
49+
}
50+
}, 5000);
51+
}
52+
53+
export function stopAutoplayMonitor(): void {
54+
if (autoplayMonitorInterval) {
55+
clearInterval(autoplayMonitorInterval);
56+
autoplayMonitorInterval = null;
57+
}
58+
}
59+
60+
async function checkAndTriggerAutoplay(): Promise<void> {
61+
const aiQueueState = useAIQueueStore.getState();
62+
if (aiQueueState.isActive) {
63+
return;
64+
}
65+
66+
const providerType = await getActiveProviderType();
67+
const provider = await getActiveProvider();
68+
const playbackState = await provider.getPlaybackState();
69+
70+
if (!playbackState?.track) return;
71+
72+
const { track, isPlaying, progressMs } = playbackState;
73+
const durationMs = track.durationMs;
74+
75+
if (track.id === state.lastProcessedTrackId) return;
76+
77+
const isNearEnd = durationMs > 0 && progressMs >= durationMs - 10000;
78+
79+
if (!isNearEnd) return;
80+
81+
if (providerType === "spotify") {
82+
await handleSpotifyAutoplay(track);
83+
} else if (providerType === "youtube") {
84+
await handleYouTubeAutoplay(track, isPlaying, progressMs, durationMs);
85+
}
86+
87+
state.lastProcessedTrackId = track.id;
88+
}
89+
90+
async function handleSpotifyAutoplay(currentTrack: UnifiedTrack): Promise<void> {
91+
try {
92+
const queue = await spotifyGetQueue();
93+
94+
if (queue.queue.length > 0) {
95+
return;
96+
}
97+
98+
const recommendations = await fetchRecommendations({
99+
seedTracks: [currentTrack.id],
100+
limit: 10,
101+
});
102+
103+
if (recommendations.length === 0) return;
104+
105+
for (const track of recommendations.slice(0, 5)) {
106+
const uri = `spotify:track:${track.id}`;
107+
await spotifyAddToQueue(uri);
108+
state.pendingAutoplayTracks.push(track.id);
109+
}
110+
} catch (err) {
111+
console.error("Spotify autoplay failed:", err);
112+
}
113+
}
114+
115+
async function handleYouTubeAutoplay(
116+
currentTrack: UnifiedTrack,
117+
isPlaying: boolean,
118+
progressMs: number,
119+
durationMs: number
120+
): Promise<void> {
121+
const playbackQueue = usePlaybackQueueStore.getState();
122+
123+
if (playbackQueue.getRemainingCount() > 0) {
124+
return;
125+
}
126+
127+
const isEnded = !isPlaying && progressMs >= durationMs - 2000;
128+
if (!isEnded) return;
129+
130+
try {
131+
const videoId = currentTrack.id;
132+
const relatedVideos = await getRelatedVideos(videoId, 10);
133+
134+
if (relatedVideos.length === 0) return;
135+
136+
const nextVideo = relatedVideos[0];
137+
const trackData = videoItemToTrackData(nextVideo);
138+
139+
const nextTrack: UnifiedTrack = {
140+
id: trackData.id,
141+
name: trackData.name,
142+
durationMs: trackData.durationMs,
143+
artists: trackData.artists.map((name, idx) => ({ id: `yt-artist-${idx}`, name })),
144+
album: {
145+
id: "youtube-music",
146+
name: trackData.album,
147+
images: trackData.albumArt ? [{ url: trackData.albumArt, width: 640, height: 640 }] : [],
148+
},
149+
uri: trackData.uri,
150+
provider: "youtube",
151+
};
152+
153+
playbackQueue.appendTracks([nextTrack]);
154+
155+
const provider = await getActiveProvider();
156+
await provider.playTrack(nextTrack.uri);
157+
} catch (err) {
158+
console.error("YouTube autoplay failed:", err);
159+
}
160+
}
161+
162+
export function resetAutoplayState(): void {
163+
state.lastProcessedTrackId = null;
164+
state.pendingAutoplayTracks = [];
165+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from "./autoplayService";
2+
export * from "./playbackQueueService";
3+
export * from "./playbackQueueStore";
4+
export * from "./spotifyKeepAlive";

0 commit comments

Comments
 (0)