-
-
Notifications
You must be signed in to change notification settings - Fork 349
Fix download metadata / video UI cards, fix dash downloads #586
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a2be340
b6b0748
312e0ad
b77df88
1c73c29
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -183,7 +183,7 @@ export class LosslessAPI { | |||||||||||||||||||
| return response; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const shouldTryNative = type !== 'streaming'; | ||||||||||||||||||||
| const shouldTryNative = type !== 'streaming' || localStorage.getItem('allTidal') === 'true'; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (shouldTryNative) { | ||||||||||||||||||||
| try { | ||||||||||||||||||||
|
|
@@ -289,6 +289,16 @@ export class LosslessAPI { | |||||||||||||||||||
| normalized = { ...normalized, audioQuality: derivedQuality }; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const copyrightText = | ||||||||||||||||||||
| typeof normalized.copyright === 'string' | ||||||||||||||||||||
| ? normalized.copyright | ||||||||||||||||||||
| : normalized.copyright && typeof normalized.copyright === 'object' | ||||||||||||||||||||
| ? normalized.copyright.text | ||||||||||||||||||||
| : normalized.copyright; | ||||||||||||||||||||
| if (copyrightText !== normalized.copyright) { | ||||||||||||||||||||
| normalized = { ...normalized, copyright: copyrightText ?? '' }; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| normalized.isUnavailable = isTrackUnavailable(normalized); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return normalized.type == 'video' ? new PreparedVideo(normalized) : new PreparedTrack(normalized); | ||||||||||||||||||||
|
|
@@ -312,6 +322,20 @@ export class LosslessAPI { | |||||||||||||||||||
| normalized.artist = video.artists[0]; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (!normalized.imageId) { | ||||||||||||||||||||
| const imageCandidate = video.imageId || video.squareImage || video.image || video.cover; | ||||||||||||||||||||
| if (typeof imageCandidate === 'string' || typeof imageCandidate === 'number') { | ||||||||||||||||||||
| normalized.imageId = imageCandidate; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (!normalized.image) { | ||||||||||||||||||||
| const imageCandidate = video.image || video.squareImage || video.cover || normalized.imageId; | ||||||||||||||||||||
| if (typeof imageCandidate === 'string' || typeof imageCandidate === 'number') { | ||||||||||||||||||||
| normalized.image = imageCandidate; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return normalized; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -1133,6 +1157,23 @@ export class LosslessAPI { | |||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (!options.lightweight) { | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| // v2 /artist?id can return a partial album relationship set; merge with | ||||||||||||||||||||
| // the dedicated releases route to avoid dropping albums on artist pages. | ||||||||||||||||||||
| const releasesResponse = await this.fetchWithRetry(`/artist/?f=${artistId}&skip_tracks=true`); | ||||||||||||||||||||
| const releasesJson = await releasesResponse.json(); | ||||||||||||||||||||
| const releasesData = releasesJson.data || releasesJson; | ||||||||||||||||||||
| const releaseItems = releasesData?.albums?.items || []; | ||||||||||||||||||||
| for (const entry of releaseItems) { | ||||||||||||||||||||
| const release = entry?.item || entry; | ||||||||||||||||||||
| if (release?.id) { | ||||||||||||||||||||
| albumMap.set(release.id, this.prepareAlbum(release)); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||
| console.warn('Failed to fetch additional artist releases:', e); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| try { | ||||||||||||||||||||
| const videoSearch = await this.searchVideos(artist.name); | ||||||||||||||||||||
| if (videoSearch && videoSearch.items) { | ||||||||||||||||||||
|
|
@@ -1147,18 +1188,31 @@ export class LosslessAPI { | |||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const rawReleases = Array.from(albumMap.values()).filter(matchesArtistId); | ||||||||||||||||||||
| const topTracksPool = Array.from(trackMap.values()).filter(matchesArtistId); | ||||||||||||||||||||
| for (const track of topTracksPool) { | ||||||||||||||||||||
| if (!track?.album?.id || albumMap.has(track.album.id)) continue; | ||||||||||||||||||||
| albumMap.set( | ||||||||||||||||||||
| track.album.id, | ||||||||||||||||||||
| this.prepareAlbum({ | ||||||||||||||||||||
| ...track.album, | ||||||||||||||||||||
| artist: track.artist || track.album.artist, | ||||||||||||||||||||
| artists: track.artists?.length ? track.artists : track.album.artists, | ||||||||||||||||||||
| }) | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const topTrackAlbumIds = new Set(topTracksPool.map((track) => Number(track?.album?.id)).filter(Boolean)); | ||||||||||||||||||||
| const rawReleases = Array.from(albumMap.values()).filter( | ||||||||||||||||||||
| (album) => matchesArtistId(album) || topTrackAlbumIds.has(Number(album?.id)) | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| const allReleases = this.deduplicateAlbums(rawReleases).sort( | ||||||||||||||||||||
| (a, b) => new Date(b.releaseDate || 0) - new Date(a.releaseDate || 0) | ||||||||||||||||||||
| ); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const eps = allReleases.filter((a) => a.type === 'EP' || a.type === 'SINGLE'); | ||||||||||||||||||||
| const albums = allReleases.filter((a) => !eps.includes(a)); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const topTracks = Array.from(trackMap.values()) | ||||||||||||||||||||
| .filter(matchesArtistId) | ||||||||||||||||||||
| .sort((a, b) => (b.popularity || 0) - (a.popularity || 0)) | ||||||||||||||||||||
| .slice(0, 15); | ||||||||||||||||||||
| const topTracks = topTracksPool.sort((a, b) => (b.popularity || 0) - (a.popularity || 0)).slice(0, 15); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const videos = Array.from(videoMap.values()).sort( | ||||||||||||||||||||
| (a, b) => new Date(b.releaseDate || 0) - new Date(a.releaseDate || 0) | ||||||||||||||||||||
|
|
@@ -1614,7 +1668,36 @@ export class LosslessAPI { | |||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const id = input?.id || input; | ||||||||||||||||||||
| const track = typeof input === 'object' ? input : await this.getTrack(id, downloadQuality); | ||||||||||||||||||||
| const hasMissingDownloadMetadata = (candidate) => | ||||||||||||||||||||
| candidate?.trackNumber == null || | ||||||||||||||||||||
| (candidate?.volumeNumber == null && candidate?.discNumber == null) || | ||||||||||||||||||||
| candidate?.album?.numberOfTracks == null; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let track = typeof input === 'object' ? this.prepareTrack(input) : await this.getTrack(id, downloadQuality); | ||||||||||||||||||||
| if ( | ||||||||||||||||||||
| typeof input === 'object' && | ||||||||||||||||||||
| !track?.type?.toLowerCase?.().includes('video') && | ||||||||||||||||||||
| hasMissingDownloadMetadata(track) | ||||||||||||||||||||
| ) { | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| const fullTrack = await this.getTrackMetadata(id); | ||||||||||||||||||||
| track = this.prepareTrack({ | ||||||||||||||||||||
| ...fullTrack, | ||||||||||||||||||||
| ...track, | ||||||||||||||||||||
| trackNumber: track?.trackNumber ?? fullTrack?.trackNumber, | ||||||||||||||||||||
| volumeNumber: track?.volumeNumber ?? fullTrack?.volumeNumber, | ||||||||||||||||||||
| discNumber: track?.discNumber ?? fullTrack?.discNumber, | ||||||||||||||||||||
| album: { | ||||||||||||||||||||
| ...(fullTrack?.album || {}), | ||||||||||||||||||||
| ...(track?.album || {}), | ||||||||||||||||||||
| }, | ||||||||||||||||||||
|
Comment on lines
+1690
to
+1693
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preserve hydrated album fields when the original value is nullish. Because 🔧 Proposed fix album: {
...(fullTrack?.album || {}),
...(track?.album || {}),
+ numberOfTracks: track?.album?.numberOfTracks ?? fullTrack?.album?.numberOfTracks,
},📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| artist: track?.artist || fullTrack?.artist, | ||||||||||||||||||||
| artists: track?.artists?.length ? track.artists : fullTrack?.artists, | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||
| console.warn('Failed to hydrate full track metadata for download:', e); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| const isVideo = track?.type?.toLowerCase().includes('video'); | ||||||||||||||||||||
|
||||||||||||||||||||
| const isVideo = track?.type?.toLowerCase().includes('video'); | |
| const isVideo = (track?.type?.toLowerCase?.() || '').includes('video'); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -315,8 +315,19 @@ function removeBulkDownloadTask(notifEl) { | |
| } | ||
|
|
||
| async function downloadTrackBlob(track, quality, api, signal = null, onProgress = null) { | ||
| const tidalAPI = api.tidalAPI || api; | ||
| let downloadTrack = track; | ||
| try { | ||
| if (typeof tidalAPI.enrichTrack === 'function') { | ||
| const { enrichedTrack } = await tidalAPI.enrichTrack(track, { downloadQuality: quality }); | ||
| if (enrichedTrack) downloadTrack = enrichedTrack; | ||
| } | ||
| } catch (e) { | ||
| console.warn('Failed to enrich track metadata before bulk download:', e); | ||
| } | ||
|
|
||
| const blob = await api.downloadTrack(track.id, quality, undefined, { | ||
| track, | ||
| track: downloadTrack, | ||
| signal, | ||
| onProgress, | ||
| triggerDownload: false, | ||
|
|
@@ -326,7 +337,7 @@ async function downloadTrackBlob(track, quality, api, signal = null, onProgress | |
| // Detect actual format from blob signature BEFORE adding metadata | ||
| const extension = await getExtensionFromBlob(blob); | ||
|
|
||
| return { blob, extension }; | ||
| return { blob, extension, track: downloadTrack }; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Apply the enriched-track return value to discography downloads too.
🔧 Proposed follow-up fix for the discography caller- const { blob, extension } = await downloadTrackBlob(track, quality, api, signal, null);
- const filename = buildTrackFilename(track, quality, extension);
+ const {
+ blob,
+ extension,
+ track: enrichedTrack,
+ } = await downloadTrackBlob(track, quality, api, signal, null);
+ const effectiveTrack = enrichedTrack || track;
+ const filename = buildTrackFilename(effectiveTrack, quality, extension);- const lyricsData = await lyricsManager.fetchLyrics(track.id, track);
+ const lyricsData = await lyricsManager.fetchLyrics(effectiveTrack.id, effectiveTrack);
if (lyricsData) {
- const lrcContent = lyricsManager.generateLRCContent(lyricsData, track);
+ const lrcContent = lyricsManager.generateLRCContent(lyricsData, effectiveTrack);🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| async function bulkDownload({ | ||
|
|
@@ -365,7 +376,11 @@ async function bulkDownload({ | |
| updateBulkDownloadProgress(notification, i, tracks.length, trackTitle); | ||
|
|
||
| try { | ||
| const { blob, extension } = await downloadTrackBlob(track, quality, api, signal, (p) => { | ||
| const { | ||
| blob, | ||
| extension, | ||
| track: enrichedTrack, | ||
| } = await downloadTrackBlob(track, quality, api, signal, (p) => { | ||
|
Comment on lines
+379
to
+383
|
||
| if (p instanceof DownloadProgress && p.totalBytes && p.receivedBytes) { | ||
| fileFraction = p.receivedBytes / p.totalBytes; | ||
| } else if (p instanceof SegmentedDownloadProgress && p.currentSegment && p.totalSegments) { | ||
|
|
@@ -375,7 +390,8 @@ async function bulkDownload({ | |
| fileFraction = Math.min(fileFraction, 0.99); // Cap at 99% to avoid showing 100% before finalization | ||
| updateBulkDownloadProgress(notification, i + fileFraction, tracks.length, trackTitle, p); | ||
| }); | ||
| const filename = buildTrackFilename(track, quality, extension); | ||
| const effectiveTrack = enrichedTrack || track; | ||
| const filename = buildTrackFilename(effectiveTrack, quality, extension); | ||
|
Comment on lines
+393
to
+394
|
||
| const discNumber = discLayout.resolveDiscNumber(i); | ||
| const discPath = separateByDisc ? `${getDiscFolderName(discNumber)}/${filename}` : filename; | ||
|
|
||
|
|
@@ -389,9 +405,9 @@ async function bulkDownload({ | |
|
|
||
| if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) { | ||
| try { | ||
| const lyricsData = await lyricsManager.fetchLyrics(track.id, track); | ||
| const lyricsData = await lyricsManager.fetchLyrics(effectiveTrack.id, effectiveTrack); | ||
| if (lyricsData) { | ||
| const lrcContent = lyricsManager.generateLRCContent(lyricsData, track); | ||
| const lrcContent = lyricsManager.generateLRCContent(lyricsData, effectiveTrack); | ||
| if (lrcContent) { | ||
| const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc'); | ||
| yield { | ||
|
|
@@ -1079,7 +1095,7 @@ export async function downloadTrackWithMetadata( | |
| triggerDownload: false, | ||
| }); | ||
|
|
||
| const finalFilename = buildTrackFilename(track, quality, await getExtensionFromBlob(blob)) | ||
| const finalFilename = buildTrackFilename(enrichedTrack, quality, await getExtensionFromBlob(blob)) | ||
| .split('/') | ||
| .pop(); | ||
|
|
||
|
|
@@ -1102,13 +1118,13 @@ export async function downloadTrackWithMetadata( | |
|
|
||
| if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) { | ||
| try { | ||
| const lyricsData = await lyricsManager.fetchLyrics(track.id, track); | ||
| const lyricsData = await lyricsManager.fetchLyrics(enrichedTrack.id, enrichedTrack); | ||
| if (lyricsData) { | ||
| await folderWriter.write( | ||
| singleWriterEntry({ | ||
| name: [...entryName.split('.').slice(0, -1), 'lrc'].join('.'), | ||
| lastModified: new Date(), | ||
| input: lyricsManager.getLRC(lyricsData, track), | ||
| input: lyricsManager.getLRC(lyricsData, enrichedTrack), | ||
| }) | ||
| ); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -34,4 +34,5 @@ type WithRequiredKeys<T> = { | |||||
|
|
||||||
| declare global { | ||||||
| const __COMMIT_HASH__: string | undefined; | ||||||
| const __VITE_PROXY__: string | undefined; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify the injected __VITE_PROXY__ value and ambient declaration stay aligned.
rg -n -C2 '__VITE_PROXY__' vite.config.ts js/global.d.ts js/proxy-utils.jsRepository: monochrome-music/monochrome Length of output: 866 Align the global type with the injected value.
🛠️ Proposed type fix- const __VITE_PROXY__: string | undefined;
+ const __VITE_PROXY__: string | null;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.filter(Boolean)on numeric album IDs drops the id0.Number(track?.album?.id)returns0for an album id of0or"0", and.filter(Boolean)then strips it from the set. TherawReleasesfilter compares against this set withtopTrackAlbumIds.has(Number(album?.id)), so albums with id0would be excluded from the artist page. Album0is unlikely in practice, but the safer predicate is an explicit nullish/NaNcheck to match the style ofmatchesArtistId.🔧 Suggested tweak
📝 Committable suggestion
🤖 Prompt for AI Agents