commit
484e718060
5 changed files with 32 additions and 14 deletions
20
js/api.js
20
js/api.js
|
|
@ -219,12 +219,12 @@ export class LosslessAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchTracks(query) {
|
async searchTracks(query, options = {}) {
|
||||||
const cached = await this.cache.get('search_tracks', query);
|
const cached = await this.cache.get('search_tracks', query);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.fetchWithRetry(`/search/?s=${encodeURIComponent(query)}`);
|
const response = await this.fetchWithRetry(`/search/?s=${encodeURIComponent(query)}`, options);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const normalized = this.normalizeSearchResponse(data, 'tracks');
|
const normalized = this.normalizeSearchResponse(data, 'tracks');
|
||||||
const result = {
|
const result = {
|
||||||
|
|
@ -235,17 +235,18 @@ export class LosslessAPI {
|
||||||
await this.cache.set('search_tracks', query, result);
|
await this.cache.set('search_tracks', query, result);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error.name === 'AbortError') throw error;
|
||||||
console.error('Track search failed:', error);
|
console.error('Track search failed:', error);
|
||||||
return { items: [], limit: 0, offset: 0, totalNumberOfItems: 0 };
|
return { items: [], limit: 0, offset: 0, totalNumberOfItems: 0 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchArtists(query) {
|
async searchArtists(query, options = {}) {
|
||||||
const cached = await this.cache.get('search_artists', query);
|
const cached = await this.cache.get('search_artists', query);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.fetchWithRetry(`/search/?a=${encodeURIComponent(query)}`);
|
const response = await this.fetchWithRetry(`/search/?a=${encodeURIComponent(query)}`, options);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const normalized = this.normalizeSearchResponse(data, 'artists');
|
const normalized = this.normalizeSearchResponse(data, 'artists');
|
||||||
const result = {
|
const result = {
|
||||||
|
|
@ -256,17 +257,18 @@ export class LosslessAPI {
|
||||||
await this.cache.set('search_artists', query, result);
|
await this.cache.set('search_artists', query, result);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error.name === 'AbortError') throw error;
|
||||||
console.error('Artist search failed:', error);
|
console.error('Artist search failed:', error);
|
||||||
return { items: [], limit: 0, offset: 0, totalNumberOfItems: 0 };
|
return { items: [], limit: 0, offset: 0, totalNumberOfItems: 0 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchAlbums(query) {
|
async searchAlbums(query, options = {}) {
|
||||||
const cached = await this.cache.get('search_albums', query);
|
const cached = await this.cache.get('search_albums', query);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.fetchWithRetry(`/search/?al=${encodeURIComponent(query)}`);
|
const response = await this.fetchWithRetry(`/search/?al=${encodeURIComponent(query)}`, options);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const normalized = this.normalizeSearchResponse(data, 'albums');
|
const normalized = this.normalizeSearchResponse(data, 'albums');
|
||||||
const result = {
|
const result = {
|
||||||
|
|
@ -277,17 +279,18 @@ export class LosslessAPI {
|
||||||
await this.cache.set('search_albums', query, result);
|
await this.cache.set('search_albums', query, result);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error.name === 'AbortError') throw error;
|
||||||
console.error('Album search failed:', error);
|
console.error('Album search failed:', error);
|
||||||
return { items: [], limit: 0, offset: 0, totalNumberOfItems: 0 };
|
return { items: [], limit: 0, offset: 0, totalNumberOfItems: 0 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchPlaylists(query) {
|
async searchPlaylists(query, options = {}) {
|
||||||
const cached = await this.cache.get('search_playlists', query);
|
const cached = await this.cache.get('search_playlists', query);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.fetchWithRetry(`/search/?p=${encodeURIComponent(query)}`);
|
const response = await this.fetchWithRetry(`/search/?p=${encodeURIComponent(query)}`, options);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const normalized = this.normalizeSearchResponse(data, 'playlists');
|
const normalized = this.normalizeSearchResponse(data, 'playlists');
|
||||||
const result = {
|
const result = {
|
||||||
|
|
@ -298,6 +301,7 @@ export class LosslessAPI {
|
||||||
await this.cache.set('search_playlists', query, result);
|
await this.cache.set('search_playlists', query, result);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error.name === 'AbortError') throw error;
|
||||||
console.error('Playlist search failed:', error);
|
console.error('Playlist search failed:', error);
|
||||||
return { items: [], limit: 0, offset: 0, totalNumberOfItems: 0 };
|
return { items: [], limit: 0, offset: 0, totalNumberOfItems: 0 };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,12 +33,14 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler) {
|
||||||
}
|
}
|
||||||
playPauseBtn.innerHTML = SVG_PAUSE;
|
playPauseBtn.innerHTML = SVG_PAUSE;
|
||||||
player.updateMediaSessionPlaybackState();
|
player.updateMediaSessionPlaybackState();
|
||||||
|
player.updateMediaSessionPositionState();
|
||||||
updateTabTitle(player);
|
updateTabTitle(player);
|
||||||
});
|
});
|
||||||
|
|
||||||
audioPlayer.addEventListener('pause', () => {
|
audioPlayer.addEventListener('pause', () => {
|
||||||
playPauseBtn.innerHTML = SVG_PLAY;
|
playPauseBtn.innerHTML = SVG_PLAY;
|
||||||
player.updateMediaSessionPlaybackState();
|
player.updateMediaSessionPlaybackState();
|
||||||
|
player.updateMediaSessionPositionState();
|
||||||
});
|
});
|
||||||
|
|
||||||
audioPlayer.addEventListener('ended', () => {
|
audioPlayer.addEventListener('ended', () => {
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,9 @@ export class Player {
|
||||||
if (this.preloadAbortController.signal.aborted) break;
|
if (this.preloadAbortController.signal.aborted) break;
|
||||||
|
|
||||||
this.preloadCache.set(track.id, streamUrl);
|
this.preloadCache.set(track.id, streamUrl);
|
||||||
|
|
||||||
|
// Warm connection/cache
|
||||||
|
fetch(streamUrl, { method: 'HEAD', signal: this.preloadAbortController.signal }).catch(() => {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name !== 'AbortError') {
|
if (error.name !== 'AbortError') {
|
||||||
console.debug('Failed to get stream URL for preload:', trackTitle);
|
console.debug('Failed to get stream URL for preload:', trackTitle);
|
||||||
|
|
@ -250,6 +253,7 @@ export class Player {
|
||||||
|
|
||||||
if (this.audio.paused) {
|
if (this.audio.paused) {
|
||||||
this.audio.play().catch(e => {
|
this.audio.play().catch(e => {
|
||||||
|
if (e.name === 'NotAllowedError' || e.name === 'AbortError') return;
|
||||||
console.error("Play failed, reloading track:", e);
|
console.error("Play failed, reloading track:", e);
|
||||||
if (this.currentTrack) {
|
if (this.currentTrack) {
|
||||||
this.playTrackFromQueue();
|
this.playTrackFromQueue();
|
||||||
|
|
|
||||||
16
js/ui.js
16
js/ui.js
|
|
@ -6,6 +6,7 @@ export class UIRenderer {
|
||||||
constructor(api) {
|
constructor(api) {
|
||||||
this.api = api;
|
this.api = api;
|
||||||
this.currentTrack = null;
|
this.currentTrack = null;
|
||||||
|
this.searchAbortController = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentTrack(track) {
|
setCurrentTrack(track) {
|
||||||
|
|
@ -404,12 +405,18 @@ export class UIRenderer {
|
||||||
albumsContainer.innerHTML = this.createSkeletonCards(6, false);
|
albumsContainer.innerHTML = this.createSkeletonCards(6, false);
|
||||||
playlistsContainer.innerHTML = this.createSkeletonCards(6, false);
|
playlistsContainer.innerHTML = this.createSkeletonCards(6, false);
|
||||||
|
|
||||||
|
if (this.searchAbortController) {
|
||||||
|
this.searchAbortController.abort();
|
||||||
|
}
|
||||||
|
this.searchAbortController = new AbortController();
|
||||||
|
const signal = this.searchAbortController.signal;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [tracksResult, artistsResult, albumsResult, playlistsResult] = await Promise.all([
|
const [tracksResult, artistsResult, albumsResult, playlistsResult] = await Promise.all([
|
||||||
this.api.searchTracks(query),
|
this.api.searchTracks(query, { signal }),
|
||||||
this.api.searchArtists(query),
|
this.api.searchArtists(query, { signal }),
|
||||||
this.api.searchAlbums(query),
|
this.api.searchAlbums(query, { signal }),
|
||||||
this.api.searchPlaylists(query)
|
this.api.searchPlaylists(query, { signal })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let finalTracks = tracksResult.items;
|
let finalTracks = tracksResult.items;
|
||||||
|
|
@ -463,6 +470,7 @@ export class UIRenderer {
|
||||||
: createPlaceholder('No playlists found.');
|
: createPlaceholder('No playlists found.');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error.name === 'AbortError') return;
|
||||||
console.error("Search failed:", error);
|
console.error("Search failed:", error);
|
||||||
const errorMsg = createPlaceholder(`Error during search. ${error.message}`);
|
const errorMsg = createPlaceholder(`Error during search. ${error.message}`);
|
||||||
tracksContainer.innerHTML = errorMsg;
|
tracksContainer.innerHTML = errorMsg;
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ export const QUALITY_TOKENS = {
|
||||||
|
|
||||||
export const RATE_LIMIT_ERROR_MESSAGE = 'Too Many Requests. Please wait a moment and try again.';
|
export const RATE_LIMIT_ERROR_MESSAGE = 'Too Many Requests. Please wait a moment and try again.';
|
||||||
|
|
||||||
export const SVG_PLAY = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>';
|
export const SVG_PLAY = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><polygon points="7 3 21 12 7 21 7 3"></polygon></svg>';
|
||||||
export const SVG_PAUSE = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>';
|
export const SVG_PAUSE = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>';
|
||||||
export const SVG_VOLUME = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path></svg>';
|
export const SVG_VOLUME = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path></svg>';
|
||||||
export const SVG_MUTE = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><line x1="23" y1="9" x2="17" y2="15"></line><line x1="17" y1="9" x2="23" y2="15"></line></svg>';
|
export const SVG_MUTE = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><line x1="23" y1="9" x2="17" y2="15"></line><line x1="17" y1="9" x2="23" y2="15"></line></svg>';
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue