v7.0.5
This commit is contained in:
+102
-40
@@ -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, "&", "&")
|
initURL = strings.ReplaceAll(initURL, "&", "&")
|
||||||
mediaTemplate = strings.ReplaceAll(mediaTemplate, "&", "&")
|
mediaTemplate = strings.ReplaceAll(mediaTemplate, "&", "&")
|
||||||
|
|
||||||
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
@@ -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>)}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user