From 4f135f1153ab3f0cacb3163a7db7b851554666ab Mon Sep 17 00:00:00 2001 From: afkarxyz Date: Wed, 14 Jan 2026 08:23:50 +0700 Subject: [PATCH] v7.0.5 --- backend/tidal.go | 142 +++++++++++++++++++++++++++++++------------ frontend/src/App.tsx | 107 +++++++++++++++++--------------- 2 files changed, 160 insertions(+), 89 deletions(-) diff --git a/backend/tidal.go b/backend/tidal.go index 116ca05..1447541 100644 --- a/backend/tidal.go +++ b/backend/tidal.go @@ -740,28 +740,35 @@ func (t *TidalDownloader) Download(spotifyTrackID, outputDir, quality, filenameF return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber, spotifyCoverURL, embedMaxQualityCover, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyTotalDiscs, spotifyCopyright, spotifyPublisher, spotifyURL) } +type SegmentTemplate struct { + Initialization string `xml:"initialization,attr"` + Media string `xml:"media,attr"` + Timeline struct { + Segments []struct { + Duration int64 `xml:"d,attr"` + Repeat int `xml:"r,attr"` + } `xml:"S"` + } `xml:"SegmentTimeline"` +} + type MPD struct { XMLName xml.Name `xml:"MPD"` Period struct { - AdaptationSet struct { - Representation struct { - SegmentTemplate struct { - Initialization string `xml:"initialization,attr"` - Media string `xml:"media,attr"` - Timeline struct { - Segments []struct { - Duration int `xml:"d,attr"` - Repeat int `xml:"r,attr"` - } `xml:"S"` - } `xml:"SegmentTimeline"` - } `xml:"SegmentTemplate"` + AdaptationSets []struct { + MimeType string `xml:"mimeType,attr"` + Codecs string `xml:"codecs,attr"` + Representations []struct { + ID string `xml:"id,attr"` + Codecs string `xml:"codecs,attr"` + Bandwidth int `xml:"bandwidth,attr"` + SegmentTemplate *SegmentTemplate `xml:"SegmentTemplate"` } `xml:"Representation"` + SegmentTemplate *SegmentTemplate `xml:"SegmentTemplate"` } `xml:"AdaptationSet"` } `xml:"Period"` } func parseManifest(manifestB64 string) (directURL string, initURL string, mediaURLs []string, err error) { - manifestBytes, err := base64.StdEncoding.DecodeString(manifestB64) if err != nil { return "", "", nil, fmt.Errorf("failed to decode manifest: %w", err) @@ -769,8 +776,7 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU manifestStr := string(manifestBytes) - if strings.HasPrefix(manifestStr, "{") { - + if strings.HasPrefix(strings.TrimSpace(manifestStr), "{") { var btsManifest TidalBTSManifest if err := json.Unmarshal(manifestBytes, &btsManifest); err != nil { return "", "", nil, fmt.Errorf("failed to parse BTS manifest: %w", err) @@ -787,25 +793,78 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU fmt.Println("Manifest: DASH format") var mpd MPD - if err := xml.Unmarshal(manifestBytes, &mpd); err != nil { - return "", "", nil, fmt.Errorf("failed to parse manifest XML: %w", err) + var segTemplate *SegmentTemplate + + if err := xml.Unmarshal(manifestBytes, &mpd); err == nil { + var selectedBandwidth int + var selectedCodecs string + + for _, as := range mpd.Period.AdaptationSets { + + if as.SegmentTemplate != nil { + + if segTemplate == nil { + segTemplate = as.SegmentTemplate + selectedCodecs = as.Codecs + } + } + + for _, rep := range as.Representations { + if rep.SegmentTemplate != nil { + if rep.Bandwidth > selectedBandwidth { + selectedBandwidth = rep.Bandwidth + segTemplate = rep.SegmentTemplate + + if rep.Codecs != "" { + selectedCodecs = rep.Codecs + } else { + selectedCodecs = as.Codecs + } + } + } + } + } + + if selectedBandwidth > 0 { + fmt.Printf("Selected stream: Codec=%s, Bandwidth=%d bps\n", selectedCodecs, selectedBandwidth) + } } - segTemplate := mpd.Period.AdaptationSet.Representation.SegmentTemplate - initURL = segTemplate.Initialization - mediaTemplate := segTemplate.Media + var mediaTemplate string + segmentCount := 0 - if initURL == "" || mediaTemplate == "" { + if segTemplate != nil { + initURL = segTemplate.Initialization + mediaTemplate = segTemplate.Media - initRe := regexp.MustCompile(`initialization="([^"]+)"`) - mediaRe := regexp.MustCompile(`media="([^"]+)"`) - - if match := initRe.FindStringSubmatch(manifestStr); len(match) > 1 { - initURL = match[1] + for _, seg := range segTemplate.Timeline.Segments { + segmentCount += seg.Repeat + 1 } - if match := mediaRe.FindStringSubmatch(manifestStr); len(match) > 1 { - mediaTemplate = match[1] + } + + if segmentCount > 0 && initURL != "" && mediaTemplate != "" { + initURL = strings.ReplaceAll(initURL, "&", "&") + mediaTemplate = strings.ReplaceAll(mediaTemplate, "&", "&") + + fmt.Printf("Parsed manifest via XML: %d segments\n", segmentCount) + + for i := 1; i <= segmentCount; i++ { + mediaURL := strings.ReplaceAll(mediaTemplate, "$Number$", fmt.Sprintf("%d", i)) + mediaURLs = append(mediaURLs, mediaURL) } + return "", initURL, mediaURLs, nil + } + + fmt.Println("Using regex fallback for DASH manifest...") + + initRe := regexp.MustCompile(`initialization="([^"]+)"`) + mediaRe := regexp.MustCompile(`media="([^"]+)"`) + + if match := initRe.FindStringSubmatch(manifestStr); len(match) > 1 { + initURL = match[1] + } + if match := mediaRe.FindStringSubmatch(manifestStr); len(match) > 1 { + mediaTemplate = match[1] } if initURL == "" { @@ -815,23 +874,26 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU initURL = strings.ReplaceAll(initURL, "&", "&") mediaTemplate = strings.ReplaceAll(mediaTemplate, "&", "&") - segmentCount := 0 - for _, seg := range segTemplate.Timeline.Segments { - segmentCount += seg.Repeat + 1 + segmentCount = 0 + + segTagRe := regexp.MustCompile(`]*>`) + matches := segTagRe.FindAllString(manifestStr, -1) + + for _, match := range matches { + repeat := 0 + rRe := regexp.MustCompile(`r="(\d+)"`) + if rMatch := rRe.FindStringSubmatch(match); len(rMatch) > 1 { + fmt.Sscanf(rMatch[1], "%d", &repeat) + } + segmentCount += repeat + 1 } if segmentCount == 0 { - segRe := regexp.MustCompile(` 1 && match[1] != "" { - fmt.Sscanf(match[1], "%d", &repeat) - } - segmentCount += repeat + 1 - } + return "", "", nil, fmt.Errorf("no segments found in manifest (XML: %d, Regex: 0)", len(matches)) } + fmt.Printf("Parsed manifest via Regex: %d segments\n", segmentCount) + for i := 1; i <= segmentCount; i++ { mediaURL := strings.ReplaceAll(mediaTemplate, "$Number$", fmt.Sprintf("%d", i)) mediaURLs = append(mediaURLs, mediaURL) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8aa7649..4e1ecde 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, useLayoutEffect } from "react"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; @@ -57,6 +57,15 @@ function App() { const cover = useCover(); const availability = useAvailability(); const downloadQueue = useDownloadQueueDialog(); + useLayoutEffect(() => { + const savedSettings = getSettings(); + if (savedSettings) { + applyThemeMode(savedSettings.themeMode); + applyTheme(savedSettings.theme); + applyFont(savedSettings.fontFamily); + } + }, []); + useEffect(() => { const initSettings = async () => { const settings = await loadSettings(); @@ -253,49 +262,49 @@ function App() { return null; if ("track" in metadata.metadata) { const { track } = metadata.metadata; - return ( lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, track.album_name, undefined, albumArtist, releaseDate, discNumber)} onDownloadCover={(coverUrl, trackName, artistName, albumName, _playlistName, _position, trackId, albumArtist, releaseDate, discNumber) => cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, track.album_name, undefined, trackId, albumArtist, releaseDate, discNumber)} onCheckAvailability={availability.checkAvailability} onOpenFolder={handleOpenFolder}/>); + return ( lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, track.album_name, undefined, albumArtist, releaseDate, discNumber)} onDownloadCover={(coverUrl, trackName, artistName, albumName, _playlistName, _position, trackId, albumArtist, releaseDate, discNumber) => cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, track.album_name, undefined, trackId, albumArtist, releaseDate, discNumber)} onCheckAvailability={availability.checkAvailability} onOpenFolder={handleOpenFolder} />); } if ("album_info" in metadata.metadata) { const { album_info, track_list } = metadata.metadata; return ( lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, album_info.name, position, albumArtist, releaseDate, discNumber, true)} onDownloadCover={(coverUrl, trackName, artistName, albumName, _folderName, _isArtistDiscography, position, trackId, albumArtist, releaseDate, discNumber) => cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, album_info.name, position, trackId, albumArtist, releaseDate, discNumber, true)} onCheckAvailability={availability.checkAvailability} onDownloadAllLyrics={() => lyrics.handleDownloadAllLyrics(track_list, album_info.name, undefined, true)} onDownloadAllCovers={() => cover.handleDownloadAllCovers(track_list, album_info.name, true)} onDownloadAll={() => download.handleDownloadAll(track_list, album_info.name, true)} onDownloadSelected={() => download.handleDownloadSelected(selectedTracks, track_list, album_info.name, true)} onStopDownload={download.handleStopDownload} onOpenFolder={handleOpenFolder} onPageChange={setCurrentListPage} onArtistClick={async (artist) => { - const artistUrl = await metadata.handleArtistClick(artist); - if (artistUrl) { - setSpotifyUrl(artistUrl); - } - }} onTrackClick={async (track) => { - if (track.external_urls) { - setSpotifyUrl(track.external_urls); - await metadata.handleFetchMetadata(track.external_urls); - } - }}/>); + const artistUrl = await metadata.handleArtistClick(artist); + if (artistUrl) { + setSpotifyUrl(artistUrl); + } + }} onTrackClick={async (track) => { + if (track.external_urls) { + setSpotifyUrl(track.external_urls); + await metadata.handleFetchMetadata(track.external_urls); + } + }} />); } if ("playlist_info" in metadata.metadata) { const { playlist_info, track_list } = metadata.metadata; return ( lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, playlist_info.name, position, albumArtist, releaseDate, discNumber)} onDownloadCover={(coverUrl, trackName, artistName, albumName, _folderName, _isArtistDiscography, position, trackId, albumArtist, releaseDate, discNumber) => cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, playlist_info.name, position, trackId, albumArtist, releaseDate, discNumber)} onCheckAvailability={availability.checkAvailability} onDownloadAllLyrics={() => lyrics.handleDownloadAllLyrics(track_list, playlist_info.name)} onDownloadAllCovers={() => cover.handleDownloadAllCovers(track_list, playlist_info.name)} onDownloadAll={() => download.handleDownloadAll(track_list, playlist_info.name)} onDownloadSelected={() => download.handleDownloadSelected(selectedTracks, track_list, playlist_info.name)} onStopDownload={download.handleStopDownload} onOpenFolder={handleOpenFolder} onPageChange={setCurrentListPage} onAlbumClick={metadata.handleAlbumClick} onArtistClick={async (artist) => { - const artistUrl = await metadata.handleArtistClick(artist); - if (artistUrl) { - setSpotifyUrl(artistUrl); - } - }} onTrackClick={async (track) => { - if (track.external_urls) { - setSpotifyUrl(track.external_urls); - await metadata.handleFetchMetadata(track.external_urls); - } - }}/>); + const artistUrl = await metadata.handleArtistClick(artist); + if (artistUrl) { + setSpotifyUrl(artistUrl); + } + }} onTrackClick={async (track) => { + if (track.external_urls) { + setSpotifyUrl(track.external_urls); + await metadata.handleFetchMetadata(track.external_urls); + } + }} />); } if ("artist_info" in metadata.metadata) { const { artist_info, album_list, track_list } = metadata.metadata; return ( lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, artist_info.name, position, albumArtist, releaseDate, discNumber)} onDownloadCover={(coverUrl, trackName, artistName, albumName, _folderName, _isArtistDiscography, position, trackId, albumArtist, releaseDate, discNumber) => cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, artist_info.name, position, trackId, albumArtist, releaseDate, discNumber)} onCheckAvailability={availability.checkAvailability} onDownloadAllLyrics={() => lyrics.handleDownloadAllLyrics(track_list, artist_info.name)} onDownloadAllCovers={() => cover.handleDownloadAllCovers(track_list, artist_info.name)} onDownloadAll={() => download.handleDownloadAll(track_list, artist_info.name)} onDownloadSelected={() => download.handleDownloadSelected(selectedTracks, track_list, artist_info.name)} onStopDownload={download.handleStopDownload} onOpenFolder={handleOpenFolder} onAlbumClick={metadata.handleAlbumClick} onArtistClick={async (artist) => { - const artistUrl = await metadata.handleArtistClick(artist); - if (artistUrl) { - setSpotifyUrl(artistUrl); - } - }} onPageChange={setCurrentListPage} onTrackClick={async (track) => { - if (track.external_urls) { - setSpotifyUrl(track.external_urls); - await metadata.handleFetchMetadata(track.external_urls); - } - }}/>); + const artistUrl = await metadata.handleArtistClick(artist); + if (artistUrl) { + setSpotifyUrl(artistUrl); + } + }} onPageChange={setCurrentListPage} onTrackClick={async (track) => { + if (track.external_urls) { + setSpotifyUrl(track.external_urls); + await metadata.handleFetchMetadata(track.external_urls); + } + }} />); } return null; }; @@ -328,7 +337,7 @@ function App() { const renderPage = () => { switch (currentPage) { case "settings": - return ; + return ; case "debug": return ; case "audio-analysis": @@ -339,14 +348,14 @@ function App() { return ; default: return (<> -
+
Fetch Artist @@ -360,7 +369,7 @@ function App() {
- metadata.setTimeoutValue(Number(e.target.value))}/> + metadata.setTimeoutValue(Number(e.target.value))} />

Default: 60 seconds. For large discographies, try 300-600 seconds (5-10 minutes). @@ -372,7 +381,7 @@ function App() { Cancel @@ -384,7 +393,7 @@ function App() {

Fetch Album @@ -399,12 +408,12 @@ function App() { Cancel @@ -417,7 +426,7 @@ function App() { if (updatedUrl) { setSpotifyUrl(updatedUrl); } - }} history={fetchHistory} onHistorySelect={handleHistorySelect} onHistoryRemove={removeFromHistory} hasResult={!!metadata.metadata} searchMode={isSearchMode} onSearchModeChange={setIsSearchMode}/> + }} history={fetchHistory} onHistorySelect={handleHistorySelect} onHistoryRemove={removeFromHistory} hasResult={!!metadata.metadata} searchMode={isSearchMode} onSearchModeChange={setIsSearchMode} /> {!isSearchMode && metadata.metadata && renderMetadata()} ); @@ -426,7 +435,7 @@ function App() { return (
- +
@@ -436,14 +445,14 @@ function App() {
- + - + {showScrollTop && ()}