Skip to content

Commit 271a4c7

Browse files
committed
fixed bugs on history,artist,spotify and some ui issue fixed
1 parent ece2efa commit 271a4c7

14 files changed

Lines changed: 275 additions & 80 deletions

Api/Recommended.js

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,28 @@ import { getCachedData, CACHE_GROUPS, isNetworkAvailable } from './CacheManager'
33

44
async function getRecommendedSongs(id) {
55
try {
6-
// Skip recommendation requests for YouTube Music songs (11-character IDs)
7-
// ONLY if it's not explicitly a Saavn song OR doesn't have Saavn download URLs
8-
const isYouTubeId = id && typeof id === 'string' && id.length === 11 && !/[\\/.]/.test(id);
9-
if (isYouTubeId) {
10-
// If we have access to the song object, we could be more precise.
11-
// For now, let's assume if it's 11 chars AND doesn't look like a Saavn ID (optional)
12-
// But Saavn IDs can be anything. Better to check if we're in a Saavn context.
6+
// Skip if no ID provided
7+
if (!id || typeof id !== 'string') {
8+
return { data: [], success: true, message: "No valid ID provided" };
9+
}
10+
11+
// Skip recommendation requests for Spotify songs (22-character alphanumeric IDs)
12+
// Spotify IDs are exactly 22 chars, containing only letters and numbers (Base62)
13+
const isSpotifyId = id.length === 22 && /^[a-zA-Z0-9]+$/.test(id);
14+
if (isSpotifyId) {
15+
return { data: [], success: true, message: "Spotify uses its own recommendation system" };
1316
}
1417

15-
if (isYouTubeId && !id.startsWith('_')) { // Many Saavn IDs start with underscores
16-
return { data: [], success: true, message: "Recommendations not available for YouTube songs" };
18+
// Skip recommendation requests for YouTube Music songs (11-character IDs)
19+
// YouTube video IDs are 11 chars, can contain letters, numbers, hyphens, and underscores
20+
const isYouTubeId = id.length === 11 && /^[a-zA-Z0-9_-]+$/.test(id);
21+
if (isYouTubeId) {
22+
return { data: [], success: true, message: "YouTube Music uses its own recommendation system" };
1723
}
1824

1925
// Skip recommendation requests for DAB songs (purely numeric, typically 9-12 digits)
2026
// DAB uses Last.fm recommendations via DABRecommendationService, not Saavn
21-
const isDabId = id && typeof id === 'string' && /^\d{6,15}$/.test(id);
27+
const isDabId = /^\d{6,15}$/.test(id);
2228
if (isDabId) {
2329
return { data: [], success: true, message: "DAB uses Last.fm recommendations" };
2430
}

Component/History/HistoryCard.jsx

Lines changed: 94 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,77 @@ export const HistoryCard = memo(function HistoryCard({ historyItem, onRefresh })
6363
// Play song
6464
const playSong = async () => {
6565
try {
66-
const songData = {
67-
id: historyItem.id,
68-
title: historyItem.title,
69-
artist: historyItem.artist,
70-
artwork: historyItem.artwork,
71-
url: historyItem.url,
72-
duration: historyItem.duration,
73-
sourceType: historyItem.sourceType,
74-
isLocal: historyItem.isLocal,
75-
path: historyItem.path,
76-
};
66+
// Check if this is a legacy history entry (empty URL and no videoId)
67+
// For these, we need to search YouTube Music by title/artist
68+
const isLegacyEntry = !historyItem.url && !historyItem.videoId && historyItem.sourceType === 'online';
69+
70+
let songData;
71+
72+
if (isLegacyEntry) {
73+
// Legacy entry without videoId - search YouTube Music first
74+
console.log('🔍 History: Legacy entry, searching YTMusic for:', historyItem.title);
75+
try {
76+
const YouTubeMusicService = require('../../Utils/YouTubeMusicService').default;
77+
const ytResult = await YouTubeMusicService.searchAndStream(
78+
historyItem.title,
79+
historyItem.artist || ''
80+
);
81+
82+
if (ytResult && ytResult.url && !ytResult.error) {
83+
// Successfully found on YouTube Music
84+
songData = {
85+
id: ytResult.videoId,
86+
title: historyItem.title,
87+
artist: historyItem.artist,
88+
artwork: historyItem.artwork,
89+
url: ytResult.url,
90+
headers: ytResult.headers,
91+
duration: historyItem.duration,
92+
sourceType: 'online',
93+
isLocal: false,
94+
source: 'ytmusic',
95+
videoId: ytResult.videoId,
96+
_prefetched: true,
97+
};
98+
console.log('✅ History: Found on YTMusic:', ytResult.videoId);
99+
} else {
100+
throw new Error('Could not find song on YouTube Music');
101+
}
102+
} catch (searchError) {
103+
console.error('❌ History: YTMusic search failed:', searchError.message);
104+
ToastAndroid.show('Could not find song', ToastAndroid.SHORT);
105+
return;
106+
}
107+
} else {
108+
// Standard case - has URL or videoId
109+
const needsStreamFetch = !historyItem.url && (
110+
historyItem.videoId ||
111+
historyItem.source === 'ytmusic' ||
112+
historyItem.source === 'spotify' ||
113+
historyItem.source === 'dab'
114+
);
115+
116+
songData = {
117+
// Use videoId as ID if available (for YTMusic playback), otherwise use original ID
118+
id: historyItem.videoId || historyItem.id,
119+
title: historyItem.title,
120+
artist: historyItem.artist,
121+
artwork: historyItem.artwork,
122+
url: historyItem.url || '', // Empty URL will trigger stream fetch
123+
duration: historyItem.duration,
124+
sourceType: historyItem.sourceType,
125+
isLocal: historyItem.isLocal,
126+
path: historyItem.path,
127+
// Include source info for proper stream fetching
128+
source: historyItem.videoId ? 'ytmusic' : historyItem.source,
129+
videoId: historyItem.videoId,
130+
spotifyId: historyItem.spotifyId,
131+
// Flag to trigger stream fetch if URL is empty
132+
_needsStream: needsStreamFetch,
133+
// Mark if this was mapped from Spotify
134+
mappedFromSpotify: !!historyItem.spotifyId,
135+
};
136+
}
77137

78138
await PlayOneSong(songData);
79139
setIndex(1); // Open full screen player
@@ -209,14 +269,15 @@ export const HistoryCard = memo(function HistoryCard({ historyItem, onRefresh })
209269
}, [historyItem.id, historyItem.sourceType]);
210270

211271
return (
212-
<View style={styles.container}>
213-
<Pressable
214-
onPress={playSong}
215-
android_ripple={{
216-
color: dark ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.08)',
217-
borderless: false,
218-
radius: SCREEN_WIDTH * 0.45
219-
}}
272+
<Pressable
273+
onPress={playSong}
274+
android_ripple={{
275+
color: dark ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.05)',
276+
borderless: false,
277+
}}
278+
style={styles.container}
279+
>
280+
<View
220281
style={styles.pressableContent}
221282
>
222283
<FastImage
@@ -244,11 +305,14 @@ export const HistoryCard = memo(function HistoryCard({ historyItem, onRefresh })
244305
style={[styles.artist, { color: colors.textSecondary }]}
245306
/>
246307
</View>
247-
</Pressable>
308+
</View>
248309

249310
<Pressable
250311
ref={buttonRef}
251-
onPress={showMenu}
312+
onPress={(e) => {
313+
e.stopPropagation();
314+
showMenu();
315+
}}
252316
style={styles.menuButton}
253317
>
254318
<MaterialCommunityIcons name="dots-vertical" size={22} color={colors.text} />
@@ -293,27 +357,32 @@ export const HistoryCard = memo(function HistoryCard({ historyItem, onRefresh })
293357
</View>
294358
</TouchableOpacity>
295359
</Modal>
296-
</View>
360+
</Pressable>
297361
);
298362
});
299363

300364
const getThemedStyles = (colors, dark) => StyleSheet.create({
301365
container: {
302366
flexDirection: 'row',
303367
alignItems: 'center',
304-
paddingHorizontal: 16,
305-
paddingVertical: 8,
368+
paddingVertical: 6,
369+
paddingHorizontal: 10,
370+
marginHorizontal: 10,
371+
marginVertical: 2,
372+
borderRadius: 8,
306373
backgroundColor: colors.background,
307374
},
308375
pressableContent: {
309376
flex: 1,
310377
flexDirection: 'row',
311378
alignItems: 'center',
379+
paddingVertical: 6,
380+
paddingLeft: 4
312381
},
313382
artwork: {
314383
width: 50,
315384
height: 50,
316-
borderRadius: 4,
385+
borderRadius: 8,
317386
marginRight: 12,
318387
},
319388
textContainer: {

Component/MusicPlayer/BottomSheetMusic.jsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,12 @@ const BottomSheetMusic = React.memo(({ color }) => {
133133
// Timing animation config for smooth, predictable open/close transitions
134134
// Using timing instead of spring to avoid stuck transitions
135135
// OPTIMIZED: Higher stiffness spring for fast, responsive open/close
136+
// OPTIMIZED: Ultra-fast spring animation for instant fullscreen transition
137+
// Very high stiffness + very low mass = near-instant snap with no visible bottom gap
136138
const animationConfigs = useBottomSheetSpringConfigs({
137-
damping: 25,
138-
stiffness: 300,
139-
mass: 0.4,
139+
damping: 35,
140+
stiffness: 700,
141+
mass: 0.2,
140142
overshootClamping: true,
141143
restDisplacementThreshold: 0.01,
142144
restSpeedThreshold: 0.01,
@@ -396,6 +398,8 @@ const BottomSheetMusic = React.memo(({ color }) => {
396398
<BottomSheetView
397399
style={{
398400
...styles.contentContainer,
401+
// Solid background when fullscreen to prevent gap during animation
402+
backgroundColor: Index === 1 ? (color || colors.musicPlayerBg) : 'transparent',
399403
}}
400404
>
401405
{Index !== 1 ? (

Component/MusicPlayer/EachSongMenuButton.jsx

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,11 +208,41 @@ export const EachSongMenuButton = ({
208208
const isYouTubeSong = song.id && typeof song.id === 'string' && song.id.length === 11 && !song.isLocalMusic;
209209
// Check if this is a DAB Music track (multiple detection methods)
210210
const isDabTrack = song.isDabTrack || song.source === 'dab' || (!isNaN(song.url) && String(song.url).length > 5);
211+
// Check if this is a Spotify track
212+
const isSpotifyTrack = song.source === 'spotify' || song.spotifyId || song._needsSpotifyMapping || (typeof song.url === 'string' && song.url?.startsWith('spotify://'));
211213

212214
let songUrl = '';
213215
let songMetadata = { ...song };
214216

215-
if (isYouTubeSong) {
217+
if (isSpotifyTrack) {
218+
// For Spotify songs, map to YouTube Music to get stream URL
219+
try {
220+
const ytMusicResult = await YouTubeMusicService.searchAndStream(
221+
song.title || song.name,
222+
song.artist || ''
223+
);
224+
225+
if (ytMusicResult && ytMusicResult.url && !ytMusicResult.error) {
226+
songUrl = ytMusicResult.url;
227+
songMetadata = {
228+
...songMetadata,
229+
url: ytMusicResult.url,
230+
headers: ytMusicResult.headers,
231+
userAgent: ytMusicResult.headers?.['User-Agent'],
232+
artwork: ytMusicResult.thumbnail || songMetadata.artwork,
233+
mappedFromSpotify: true,
234+
};
235+
} else {
236+
console.error('❌ Failed to map Spotify track to YTMusic');
237+
ToastAndroid.show('Failed to get stream URL', ToastAndroid.SHORT);
238+
return;
239+
}
240+
} catch (error) {
241+
console.error('❌ Error mapping Spotify to YTMusic:', error);
242+
ToastAndroid.show('Failed to load Spotify stream', ToastAndroid.SHORT);
243+
return;
244+
}
245+
} else if (isYouTubeSong) {
216246
// For YouTube songs, fetch the actual stream URL
217247
try {
218248
const streamData = await youtubeStreamingService.getStreamUrl(song.id);
@@ -279,6 +309,11 @@ export const EachSongMenuButton = ({
279309
duration: songMetadata.duration || 0,
280310
language: songMetadata.language || '',
281311
artistID: songMetadata.artistID || '',
312+
// Preserve source metadata for info modal
313+
source: isSpotifyTrack ? 'spotify' : (isDabTrack ? 'dab' : (isYouTubeSong ? 'ytmusic' : song.source)),
314+
spotifyId: isSpotifyTrack ? song.id : song.spotifyId,
315+
album: songMetadata.album || song.album || '',
316+
mappedFromSpotify: songMetadata.mappedFromSpotify || false,
282317
...(songMetadata.headers && { headers: songMetadata.headers }),
283318
...(songMetadata.userAgent && { userAgent: songMetadata.userAgent })
284319
});
@@ -303,11 +338,41 @@ export const EachSongMenuButton = ({
303338
const isYouTubeSong = song.id && typeof song.id === 'string' && song.id.length === 11 && !song.isLocalMusic;
304339
// Check if this is a DAB Music track (multiple detection methods)
305340
const isDabTrack = song.isDabTrack || song.source === 'dab' || (!isNaN(song.url) && String(song.url).length > 5);
341+
// Check if this is a Spotify track
342+
const isSpotifyTrack = song.source === 'spotify' || song.spotifyId || song._needsSpotifyMapping || (typeof song.url === 'string' && song.url?.startsWith('spotify://'));
306343

307344
let songUrl = '';
308345
let songMetadata = { ...song };
309346

310-
if (isYouTubeSong) {
347+
if (isSpotifyTrack) {
348+
// For Spotify songs, map to YouTube Music to get stream URL
349+
try {
350+
const ytMusicResult = await YouTubeMusicService.searchAndStream(
351+
song.title || song.name,
352+
song.artist || ''
353+
);
354+
355+
if (ytMusicResult && ytMusicResult.url && !ytMusicResult.error) {
356+
songUrl = ytMusicResult.url;
357+
songMetadata = {
358+
...songMetadata,
359+
url: ytMusicResult.url,
360+
headers: ytMusicResult.headers,
361+
userAgent: ytMusicResult.headers?.['User-Agent'],
362+
artwork: ytMusicResult.thumbnail || songMetadata.artwork,
363+
mappedFromSpotify: true,
364+
};
365+
} else {
366+
console.error('❌ Failed to map Spotify track to YTMusic');
367+
ToastAndroid.show('Failed to get stream URL', ToastAndroid.SHORT);
368+
return;
369+
}
370+
} catch (error) {
371+
console.error('❌ Error mapping Spotify to YTMusic:', error);
372+
ToastAndroid.show('Failed to load Spotify stream', ToastAndroid.SHORT);
373+
return;
374+
}
375+
} else if (isYouTubeSong) {
311376
// For YouTube songs, fetch the actual stream URL
312377
try {
313378
const streamData = await youtubeStreamingService.getStreamUrl(song.id);
@@ -374,6 +439,11 @@ export const EachSongMenuButton = ({
374439
duration: songMetadata.duration || 0,
375440
language: songMetadata.language || '',
376441
artistID: songMetadata.artistID || '',
442+
// Preserve source metadata for info modal
443+
source: isSpotifyTrack ? 'spotify' : (isDabTrack ? 'dab' : (isYouTubeSong ? 'ytmusic' : song.source)),
444+
spotifyId: isSpotifyTrack ? song.id : song.spotifyId,
445+
album: songMetadata.album || song.album || '',
446+
mappedFromSpotify: songMetadata.mappedFromSpotify || false,
377447
...(songMetadata.headers && { headers: songMetadata.headers }),
378448
...(songMetadata.userAgent && { userAgent: songMetadata.userAgent })
379449
};

Component/Playlist/CustomPlaylistView.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -920,6 +920,7 @@ export const CustomPlaylistView = (props) => {
920920
initialNumToRender={15}
921921
windowSize={11}
922922
removeClippedSubviews={true}
923+
contentContainerStyle={{ paddingBottom: 150 }}
923924
ListHeaderComponent={
924925
<PlaylistHeader
925926
imageUrl={getSafeImageSource(Songs[Songs.length - 1] || {})?.uri || getSafeImageSource(Songs[Songs.length - 1] || {})}

MusicPlayerFunctions.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,35 @@ async function AddPlaylist(songs, startSongId = null) {
571571
}
572572
}
573573

574+
// OPTIMISTIC UI: Emit early metadata for first song so mini player shows immediately
575+
// This provides instant feedback while stream URL is being fetched
576+
const firstSong = tracksToAdd[0];
577+
if (firstSong) {
578+
const earlyArtwork = extractArtwork(firstSong) || firstSong.artwork || firstSong.image || '';
579+
// Format artist properly - handle various data structures
580+
let artistDisplay = firstSong.artist || 'Loading...';
581+
if (!artistDisplay || artistDisplay === 'Loading...') {
582+
if (firstSong.artists?.primary && Array.isArray(firstSong.artists.primary)) {
583+
artistDisplay = FormatArtist(firstSong.artists.primary);
584+
} else if (typeof firstSong.artists === 'string') {
585+
artistDisplay = firstSong.artists;
586+
} else if (firstSong.primaryArtists) {
587+
artistDisplay = firstSong.primaryArtists;
588+
}
589+
}
590+
591+
DeviceEventEmitter.emit('song-loading-started', {
592+
id: firstSong.id || firstSong.videoId,
593+
title: firstSong.title || firstSong.name || firstSong.song || 'Loading...',
594+
artist: artistDisplay,
595+
artwork: earlyArtwork,
596+
image: earlyArtwork,
597+
duration: firstSong.duration,
598+
isLoading: true,
599+
isPlaylist: true, // Flag to indicate playlist/album playback
600+
});
601+
}
602+
574603
// Get quality setting ONCE
575604
const qualityIndex = await getIndexQuality();
576605
const qualityNames = ['12kbps', '48kbps', '96kbps', '160kbps', '320kbps'];

0 commit comments

Comments
 (0)