This commit is contained in:
afkarxyz
2026-01-14 08:23:50 +07:00
parent 4ee252f438
commit 4f135f1153
2 changed files with 160 additions and 89 deletions
+102 -40
View File
@@ -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) 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 { type MPD struct {
XMLName xml.Name `xml:"MPD"` XMLName xml.Name `xml:"MPD"`
Period struct { Period struct {
AdaptationSet struct { AdaptationSets []struct {
Representation struct { MimeType string `xml:"mimeType,attr"`
SegmentTemplate struct { Codecs string `xml:"codecs,attr"`
Initialization string `xml:"initialization,attr"` Representations []struct {
Media string `xml:"media,attr"` ID string `xml:"id,attr"`
Timeline struct { Codecs string `xml:"codecs,attr"`
Segments []struct { Bandwidth int `xml:"bandwidth,attr"`
Duration int `xml:"d,attr"` SegmentTemplate *SegmentTemplate `xml:"SegmentTemplate"`
Repeat int `xml:"r,attr"`
} `xml:"S"`
} `xml:"SegmentTimeline"`
} `xml:"SegmentTemplate"`
} `xml:"Representation"` } `xml:"Representation"`
SegmentTemplate *SegmentTemplate `xml:"SegmentTemplate"`
} `xml:"AdaptationSet"` } `xml:"AdaptationSet"`
} `xml:"Period"` } `xml:"Period"`
} }
func parseManifest(manifestB64 string) (directURL string, initURL string, mediaURLs []string, err error) { func parseManifest(manifestB64 string) (directURL string, initURL string, mediaURLs []string, err error) {
manifestBytes, err := base64.StdEncoding.DecodeString(manifestB64) manifestBytes, err := base64.StdEncoding.DecodeString(manifestB64)
if err != nil { if err != nil {
return "", "", nil, fmt.Errorf("failed to decode manifest: %w", err) 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) manifestStr := string(manifestBytes)
if strings.HasPrefix(manifestStr, "{") { if strings.HasPrefix(strings.TrimSpace(manifestStr), "{") {
var btsManifest TidalBTSManifest var btsManifest TidalBTSManifest
if err := json.Unmarshal(manifestBytes, &btsManifest); err != nil { if err := json.Unmarshal(manifestBytes, &btsManifest); err != nil {
return "", "", nil, fmt.Errorf("failed to parse BTS manifest: %w", err) 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") fmt.Println("Manifest: DASH format")
var mpd MPD var mpd MPD
if err := xml.Unmarshal(manifestBytes, &mpd); err != nil { var segTemplate *SegmentTemplate
return "", "", nil, fmt.Errorf("failed to parse manifest XML: %w", err)
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 var mediaTemplate string
initURL = segTemplate.Initialization segmentCount := 0
mediaTemplate := segTemplate.Media
if initURL == "" || mediaTemplate == "" { if segTemplate != nil {
initURL = segTemplate.Initialization
mediaTemplate = segTemplate.Media
initRe := regexp.MustCompile(`initialization="([^"]+)"`) for _, seg := range segTemplate.Timeline.Segments {
mediaRe := regexp.MustCompile(`media="([^"]+)"`) segmentCount += seg.Repeat + 1
if match := initRe.FindStringSubmatch(manifestStr); len(match) > 1 {
initURL = match[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 == "" { if initURL == "" {
@@ -815,23 +874,26 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU
initURL = strings.ReplaceAll(initURL, "&amp;", "&") initURL = strings.ReplaceAll(initURL, "&amp;", "&")
mediaTemplate = strings.ReplaceAll(mediaTemplate, "&amp;", "&") mediaTemplate = strings.ReplaceAll(mediaTemplate, "&amp;", "&")
segmentCount := 0 segmentCount = 0
for _, seg := range segTemplate.Timeline.Segments {
segmentCount += seg.Repeat + 1 segTagRe := regexp.MustCompile(`<S\s+[^>]*>`)
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 { if segmentCount == 0 {
segRe := regexp.MustCompile(`<S d="\d+"(?: r="(\d+)")?`) return "", "", nil, fmt.Errorf("no segments found in manifest (XML: %d, Regex: 0)", len(matches))
matches := segRe.FindAllStringSubmatch(manifestStr, -1)
for _, match := range matches {
repeat := 0
if len(match) > 1 && match[1] != "" {
fmt.Sscanf(match[1], "%d", &repeat)
}
segmentCount += repeat + 1
}
} }
fmt.Printf("Parsed manifest via Regex: %d segments\n", segmentCount)
for i := 1; i <= segmentCount; i++ { for i := 1; i <= segmentCount; i++ {
mediaURL := strings.ReplaceAll(mediaTemplate, "$Number$", fmt.Sprintf("%d", i)) mediaURL := strings.ReplaceAll(mediaTemplate, "$Number$", fmt.Sprintf("%d", i))
mediaURLs = append(mediaURLs, mediaURL) mediaURLs = append(mediaURLs, mediaURL)
+58 -49
View File
@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback, useLayoutEffect } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
@@ -57,6 +57,15 @@ function App() {
const cover = useCover(); const cover = useCover();
const availability = useAvailability(); const availability = useAvailability();
const downloadQueue = useDownloadQueueDialog(); const downloadQueue = useDownloadQueueDialog();
useLayoutEffect(() => {
const savedSettings = getSettings();
if (savedSettings) {
applyThemeMode(savedSettings.themeMode);
applyTheme(savedSettings.theme);
applyFont(savedSettings.fontFamily);
}
}, []);
useEffect(() => { useEffect(() => {
const initSettings = async () => { const initSettings = async () => {
const settings = await loadSettings(); const settings = await loadSettings();
@@ -253,49 +262,49 @@ function App() {
return null; return null;
if ("track" in metadata.metadata) { if ("track" in metadata.metadata) {
const { track } = metadata.metadata; const { track } = metadata.metadata;
return (<TrackInfo track={track} isDownloading={download.isDownloading} downloadingTrack={download.downloadingTrack} isDownloaded={download.downloadedTracks.has(track.isrc)} isFailed={download.failedTracks.has(track.isrc)} isSkipped={download.skippedTracks.has(track.isrc)} downloadingLyricsTrack={lyrics.downloadingLyricsTrack} downloadedLyrics={lyrics.downloadedLyrics.has(track.spotify_id || "")} failedLyrics={lyrics.failedLyrics.has(track.spotify_id || "")} skippedLyrics={lyrics.skippedLyrics.has(track.spotify_id || "")} checkingAvailability={availability.checkingTrackId === track.spotify_id} availability={availability.availabilityMap.get(track.spotify_id || "")} downloadingCover={cover.downloadingCoverTrack === (track.spotify_id || `${track.name}-${track.artists}`)} downloadedCover={cover.downloadedCovers.has(track.spotify_id || `${track.name}-${track.artists}`)} failedCover={cover.failedCovers.has(track.spotify_id || `${track.name}-${track.artists}`)} skippedCover={cover.skippedCovers.has(track.spotify_id || `${track.name}-${track.artists}`)} onDownload={download.handleDownloadTrack} onDownloadLyrics={(spotifyId, name, artists, albumName, albumArtist, releaseDate, discNumber) => 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 (<TrackInfo track={track} isDownloading={download.isDownloading} downloadingTrack={download.downloadingTrack} isDownloaded={download.downloadedTracks.has(track.isrc)} isFailed={download.failedTracks.has(track.isrc)} isSkipped={download.skippedTracks.has(track.isrc)} downloadingLyricsTrack={lyrics.downloadingLyricsTrack} downloadedLyrics={lyrics.downloadedLyrics.has(track.spotify_id || "")} failedLyrics={lyrics.failedLyrics.has(track.spotify_id || "")} skippedLyrics={lyrics.skippedLyrics.has(track.spotify_id || "")} checkingAvailability={availability.checkingTrackId === track.spotify_id} availability={availability.availabilityMap.get(track.spotify_id || "")} downloadingCover={cover.downloadingCoverTrack === (track.spotify_id || `${track.name}-${track.artists}`)} downloadedCover={cover.downloadedCovers.has(track.spotify_id || `${track.name}-${track.artists}`)} failedCover={cover.failedCovers.has(track.spotify_id || `${track.name}-${track.artists}`)} skippedCover={cover.skippedCovers.has(track.spotify_id || `${track.name}-${track.artists}`)} onDownload={download.handleDownloadTrack} onDownloadLyrics={(spotifyId, name, artists, albumName, albumArtist, releaseDate, discNumber) => 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) { if ("album_info" in metadata.metadata) {
const { album_info, track_list } = metadata.metadata; const { album_info, track_list } = metadata.metadata;
return (<AlbumInfo albumInfo={album_info} trackList={track_list} searchQuery={searchQuery} sortBy={sortBy} selectedTracks={selectedTracks} downloadedTracks={download.downloadedTracks} failedTracks={download.failedTracks} skippedTracks={download.skippedTracks} downloadingTrack={download.downloadingTrack} isDownloading={download.isDownloading} bulkDownloadType={download.bulkDownloadType} downloadProgress={download.downloadProgress} currentDownloadInfo={download.currentDownloadInfo} currentPage={currentListPage} itemsPerPage={ITEMS_PER_PAGE} downloadedLyrics={lyrics.downloadedLyrics} failedLyrics={lyrics.failedLyrics} skippedLyrics={lyrics.skippedLyrics} downloadingLyricsTrack={lyrics.downloadingLyricsTrack} checkingAvailabilityTrack={availability.checkingTrackId} availabilityMap={availability.availabilityMap} downloadedCovers={cover.downloadedCovers} failedCovers={cover.failedCovers} skippedCovers={cover.skippedCovers} downloadingCoverTrack={cover.downloadingCoverTrack} isBulkDownloadingCovers={cover.isBulkDownloadingCovers} isBulkDownloadingLyrics={lyrics.isBulkDownloadingLyrics} onSearchChange={handleSearchChange} onSortChange={setSortBy} onToggleTrack={toggleTrackSelection} onToggleSelectAll={toggleSelectAll} onDownloadTrack={download.handleDownloadTrack} onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, _isArtistDiscography, position, albumArtist, releaseDate, discNumber) => 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) => { return (<AlbumInfo albumInfo={album_info} trackList={track_list} searchQuery={searchQuery} sortBy={sortBy} selectedTracks={selectedTracks} downloadedTracks={download.downloadedTracks} failedTracks={download.failedTracks} skippedTracks={download.skippedTracks} downloadingTrack={download.downloadingTrack} isDownloading={download.isDownloading} bulkDownloadType={download.bulkDownloadType} downloadProgress={download.downloadProgress} currentDownloadInfo={download.currentDownloadInfo} currentPage={currentListPage} itemsPerPage={ITEMS_PER_PAGE} downloadedLyrics={lyrics.downloadedLyrics} failedLyrics={lyrics.failedLyrics} skippedLyrics={lyrics.skippedLyrics} downloadingLyricsTrack={lyrics.downloadingLyricsTrack} checkingAvailabilityTrack={availability.checkingTrackId} availabilityMap={availability.availabilityMap} downloadedCovers={cover.downloadedCovers} failedCovers={cover.failedCovers} skippedCovers={cover.skippedCovers} downloadingCoverTrack={cover.downloadingCoverTrack} isBulkDownloadingCovers={cover.isBulkDownloadingCovers} isBulkDownloadingLyrics={lyrics.isBulkDownloadingLyrics} onSearchChange={handleSearchChange} onSortChange={setSortBy} onToggleTrack={toggleTrackSelection} onToggleSelectAll={toggleSelectAll} onDownloadTrack={download.handleDownloadTrack} onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, _isArtistDiscography, position, albumArtist, releaseDate, discNumber) => 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); const artistUrl = await metadata.handleArtistClick(artist);
if (artistUrl) { if (artistUrl) {
setSpotifyUrl(artistUrl); setSpotifyUrl(artistUrl);
} }
}} onTrackClick={async (track) => { }} onTrackClick={async (track) => {
if (track.external_urls) { if (track.external_urls) {
setSpotifyUrl(track.external_urls); setSpotifyUrl(track.external_urls);
await metadata.handleFetchMetadata(track.external_urls); await metadata.handleFetchMetadata(track.external_urls);
} }
}}/>); }} />);
} }
if ("playlist_info" in metadata.metadata) { if ("playlist_info" in metadata.metadata) {
const { playlist_info, track_list } = metadata.metadata; const { playlist_info, track_list } = metadata.metadata;
return (<PlaylistInfo playlistInfo={playlist_info} trackList={track_list} searchQuery={searchQuery} sortBy={sortBy} selectedTracks={selectedTracks} downloadedTracks={download.downloadedTracks} failedTracks={download.failedTracks} skippedTracks={download.skippedTracks} downloadingTrack={download.downloadingTrack} isDownloading={download.isDownloading} bulkDownloadType={download.bulkDownloadType} downloadProgress={download.downloadProgress} currentDownloadInfo={download.currentDownloadInfo} currentPage={currentListPage} itemsPerPage={ITEMS_PER_PAGE} downloadedLyrics={lyrics.downloadedLyrics} failedLyrics={lyrics.failedLyrics} skippedLyrics={lyrics.skippedLyrics} downloadingLyricsTrack={lyrics.downloadingLyricsTrack} checkingAvailabilityTrack={availability.checkingTrackId} availabilityMap={availability.availabilityMap} downloadedCovers={cover.downloadedCovers} failedCovers={cover.failedCovers} skippedCovers={cover.skippedCovers} downloadingCoverTrack={cover.downloadingCoverTrack} isBulkDownloadingCovers={cover.isBulkDownloadingCovers} isBulkDownloadingLyrics={lyrics.isBulkDownloadingLyrics} onSearchChange={handleSearchChange} onSortChange={setSortBy} onToggleTrack={toggleTrackSelection} onToggleSelectAll={toggleSelectAll} onDownloadTrack={download.handleDownloadTrack} onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, _isArtistDiscography, position, albumArtist, releaseDate, discNumber) => 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) => { return (<PlaylistInfo playlistInfo={playlist_info} trackList={track_list} searchQuery={searchQuery} sortBy={sortBy} selectedTracks={selectedTracks} downloadedTracks={download.downloadedTracks} failedTracks={download.failedTracks} skippedTracks={download.skippedTracks} downloadingTrack={download.downloadingTrack} isDownloading={download.isDownloading} bulkDownloadType={download.bulkDownloadType} downloadProgress={download.downloadProgress} currentDownloadInfo={download.currentDownloadInfo} currentPage={currentListPage} itemsPerPage={ITEMS_PER_PAGE} downloadedLyrics={lyrics.downloadedLyrics} failedLyrics={lyrics.failedLyrics} skippedLyrics={lyrics.skippedLyrics} downloadingLyricsTrack={lyrics.downloadingLyricsTrack} checkingAvailabilityTrack={availability.checkingTrackId} availabilityMap={availability.availabilityMap} downloadedCovers={cover.downloadedCovers} failedCovers={cover.failedCovers} skippedCovers={cover.skippedCovers} downloadingCoverTrack={cover.downloadingCoverTrack} isBulkDownloadingCovers={cover.isBulkDownloadingCovers} isBulkDownloadingLyrics={lyrics.isBulkDownloadingLyrics} onSearchChange={handleSearchChange} onSortChange={setSortBy} onToggleTrack={toggleTrackSelection} onToggleSelectAll={toggleSelectAll} onDownloadTrack={download.handleDownloadTrack} onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, _isArtistDiscography, position, albumArtist, releaseDate, discNumber) => 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); const artistUrl = await metadata.handleArtistClick(artist);
if (artistUrl) { if (artistUrl) {
setSpotifyUrl(artistUrl); setSpotifyUrl(artistUrl);
} }
}} onTrackClick={async (track) => { }} onTrackClick={async (track) => {
if (track.external_urls) { if (track.external_urls) {
setSpotifyUrl(track.external_urls); setSpotifyUrl(track.external_urls);
await metadata.handleFetchMetadata(track.external_urls); await metadata.handleFetchMetadata(track.external_urls);
} }
}}/>); }} />);
} }
if ("artist_info" in metadata.metadata) { if ("artist_info" in metadata.metadata) {
const { artist_info, album_list, track_list } = metadata.metadata; const { artist_info, album_list, track_list } = metadata.metadata;
return (<ArtistInfo artistInfo={artist_info} albumList={album_list} trackList={track_list} searchQuery={searchQuery} sortBy={sortBy} selectedTracks={selectedTracks} downloadedTracks={download.downloadedTracks} failedTracks={download.failedTracks} skippedTracks={download.skippedTracks} downloadingTrack={download.downloadingTrack} isDownloading={download.isDownloading} bulkDownloadType={download.bulkDownloadType} downloadProgress={download.downloadProgress} currentDownloadInfo={download.currentDownloadInfo} currentPage={currentListPage} itemsPerPage={ITEMS_PER_PAGE} downloadedLyrics={lyrics.downloadedLyrics} failedLyrics={lyrics.failedLyrics} skippedLyrics={lyrics.skippedLyrics} downloadingLyricsTrack={lyrics.downloadingLyricsTrack} checkingAvailabilityTrack={availability.checkingTrackId} availabilityMap={availability.availabilityMap} downloadedCovers={cover.downloadedCovers} failedCovers={cover.failedCovers} skippedCovers={cover.skippedCovers} downloadingCoverTrack={cover.downloadingCoverTrack} isBulkDownloadingCovers={cover.isBulkDownloadingCovers} isBulkDownloadingLyrics={lyrics.isBulkDownloadingLyrics} onSearchChange={handleSearchChange} onSortChange={setSortBy} onToggleTrack={toggleTrackSelection} onToggleSelectAll={toggleSelectAll} onDownloadTrack={download.handleDownloadTrack} onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, _isArtistDiscography, position, albumArtist, releaseDate, discNumber) => 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) => { return (<ArtistInfo artistInfo={artist_info} albumList={album_list} trackList={track_list} searchQuery={searchQuery} sortBy={sortBy} selectedTracks={selectedTracks} downloadedTracks={download.downloadedTracks} failedTracks={download.failedTracks} skippedTracks={download.skippedTracks} downloadingTrack={download.downloadingTrack} isDownloading={download.isDownloading} bulkDownloadType={download.bulkDownloadType} downloadProgress={download.downloadProgress} currentDownloadInfo={download.currentDownloadInfo} currentPage={currentListPage} itemsPerPage={ITEMS_PER_PAGE} downloadedLyrics={lyrics.downloadedLyrics} failedLyrics={lyrics.failedLyrics} skippedLyrics={lyrics.skippedLyrics} downloadingLyricsTrack={lyrics.downloadingLyricsTrack} checkingAvailabilityTrack={availability.checkingTrackId} availabilityMap={availability.availabilityMap} downloadedCovers={cover.downloadedCovers} failedCovers={cover.failedCovers} skippedCovers={cover.skippedCovers} downloadingCoverTrack={cover.downloadingCoverTrack} isBulkDownloadingCovers={cover.isBulkDownloadingCovers} isBulkDownloadingLyrics={lyrics.isBulkDownloadingLyrics} onSearchChange={handleSearchChange} onSortChange={setSortBy} onToggleTrack={toggleTrackSelection} onToggleSelectAll={toggleSelectAll} onDownloadTrack={download.handleDownloadTrack} onDownloadLyrics={(spotifyId, name, artists, albumName, _folderName, _isArtistDiscography, position, albumArtist, releaseDate, discNumber) => 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); const artistUrl = await metadata.handleArtistClick(artist);
if (artistUrl) { if (artistUrl) {
setSpotifyUrl(artistUrl); setSpotifyUrl(artistUrl);
} }
}} onPageChange={setCurrentListPage} onTrackClick={async (track) => { }} onPageChange={setCurrentListPage} onTrackClick={async (track) => {
if (track.external_urls) { if (track.external_urls) {
setSpotifyUrl(track.external_urls); setSpotifyUrl(track.external_urls);
await metadata.handleFetchMetadata(track.external_urls); await metadata.handleFetchMetadata(track.external_urls);
} }
}}/>); }} />);
} }
return null; return null;
}; };
@@ -328,7 +337,7 @@ function App() {
const renderPage = () => { const renderPage = () => {
switch (currentPage) { switch (currentPage) {
case "settings": case "settings":
return <SettingsPage onUnsavedChangesChange={setHasUnsavedSettings} onResetRequest={setResetSettingsFn}/>; return <SettingsPage onUnsavedChangesChange={setHasUnsavedSettings} onResetRequest={setResetSettingsFn} />;
case "debug": case "debug":
return <DebugLoggerPage />; return <DebugLoggerPage />;
case "audio-analysis": case "audio-analysis":
@@ -339,14 +348,14 @@ function App() {
return <FileManagerPage />; return <FileManagerPage />;
default: default:
return (<> return (<>
<Header version={CURRENT_VERSION} hasUpdate={hasUpdate} releaseDate={releaseDate}/> <Header version={CURRENT_VERSION} hasUpdate={hasUpdate} releaseDate={releaseDate} />
<Dialog open={metadata.showTimeoutDialog} onOpenChange={metadata.setShowTimeoutDialog}> <Dialog open={metadata.showTimeoutDialog} onOpenChange={metadata.setShowTimeoutDialog}>
<DialogContent className="sm:max-w-[425px] p-6 [&>button]:hidden"> <DialogContent className="sm:max-w-[425px] p-6 [&>button]:hidden">
<div className="absolute right-4 top-4"> <div className="absolute right-4 top-4">
<Button variant="ghost" size="icon" className="h-6 w-6 opacity-70 hover:opacity-100" onClick={() => metadata.setShowTimeoutDialog(false)}> <Button variant="ghost" size="icon" className="h-6 w-6 opacity-70 hover:opacity-100" onClick={() => metadata.setShowTimeoutDialog(false)}>
<X className="h-4 w-4"/> <X className="h-4 w-4" />
</Button> </Button>
</div> </div>
<DialogTitle className="text-sm font-medium">Fetch Artist</DialogTitle> <DialogTitle className="text-sm font-medium">Fetch Artist</DialogTitle>
@@ -360,7 +369,7 @@ function App() {
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="timeout">Timeout (seconds)</Label> <Label htmlFor="timeout">Timeout (seconds)</Label>
<Input id="timeout" type="number" min="10" max="600" value={metadata.timeoutValue} onChange={(e) => metadata.setTimeoutValue(Number(e.target.value))}/> <Input id="timeout" type="number" min="10" max="600" value={metadata.timeoutValue} onChange={(e) => metadata.setTimeoutValue(Number(e.target.value))} />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Default: 60 seconds. For large discographies, try 300-600 seconds (5-10 Default: 60 seconds. For large discographies, try 300-600 seconds (5-10
minutes). minutes).
@@ -372,7 +381,7 @@ function App() {
Cancel Cancel
</Button> </Button>
<Button onClick={metadata.handleConfirmFetch}> <Button onClick={metadata.handleConfirmFetch}>
<Search className="h-4 w-4"/> <Search className="h-4 w-4" />
Fetch Fetch
</Button> </Button>
</DialogFooter> </DialogFooter>
@@ -384,7 +393,7 @@ function App() {
<DialogContent className="sm:max-w-[425px] p-6 [&>button]:hidden"> <DialogContent className="sm:max-w-[425px] p-6 [&>button]:hidden">
<div className="absolute right-4 top-4"> <div className="absolute right-4 top-4">
<Button variant="ghost" size="icon" className="h-6 w-6 opacity-70 hover:opacity-100" onClick={() => metadata.setShowAlbumDialog(false)}> <Button variant="ghost" size="icon" className="h-6 w-6 opacity-70 hover:opacity-100" onClick={() => metadata.setShowAlbumDialog(false)}>
<X className="h-4 w-4"/> <X className="h-4 w-4" />
</Button> </Button>
</div> </div>
<DialogTitle className="text-sm font-medium">Fetch Album</DialogTitle> <DialogTitle className="text-sm font-medium">Fetch Album</DialogTitle>
@@ -399,12 +408,12 @@ function App() {
Cancel Cancel
</Button> </Button>
<Button onClick={async () => { <Button onClick={async () => {
const albumUrl = await metadata.handleConfirmAlbumFetch(); const albumUrl = await metadata.handleConfirmAlbumFetch();
if (albumUrl) { if (albumUrl) {
setSpotifyUrl(albumUrl); setSpotifyUrl(albumUrl);
} }
}}> }}>
<Search className="h-4 w-4"/> <Search className="h-4 w-4" />
Fetch Album Fetch Album
</Button> </Button>
</DialogFooter> </DialogFooter>
@@ -417,7 +426,7 @@ function App() {
if (updatedUrl) { if (updatedUrl) {
setSpotifyUrl(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()} {!isSearchMode && metadata.metadata && renderMetadata()}
</>); </>);
@@ -426,7 +435,7 @@ function App() {
return (<TooltipProvider> return (<TooltipProvider>
<div className="min-h-screen bg-background flex flex-col"> <div className="min-h-screen bg-background flex flex-col">
<TitleBar /> <TitleBar />
<Sidebar currentPage={currentPage} onPageChange={handlePageChange}/> <Sidebar currentPage={currentPage} onPageChange={handlePageChange} />
<div className="flex-1 ml-14 mt-10 p-4 md:p-8"> <div className="flex-1 ml-14 mt-10 p-4 md:p-8">
@@ -436,14 +445,14 @@ function App() {
</div> </div>
<DownloadProgressToast onClick={downloadQueue.openQueue}/> <DownloadProgressToast onClick={downloadQueue.openQueue} />
<DownloadQueue isOpen={downloadQueue.isOpen} onClose={downloadQueue.closeQueue}/> <DownloadQueue isOpen={downloadQueue.isOpen} onClose={downloadQueue.closeQueue} />
{showScrollTop && (<Button onClick={scrollToTop} className="fixed bottom-6 right-6 z-50 h-10 w-10 rounded-full shadow-lg" size="icon"> {showScrollTop && (<Button onClick={scrollToTop} className="fixed bottom-6 right-6 z-50 h-10 w-10 rounded-full shadow-lg" size="icon">
<ArrowUp className="h-5 w-5"/> <ArrowUp className="h-5 w-5" />
</Button>)} </Button>)}