v7.0.5
This commit is contained in:
@@ -292,7 +292,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
|||||||
downloader := backend.NewAmazonDownloader()
|
downloader := backend.NewAmazonDownloader()
|
||||||
if req.ServiceURL != "" {
|
if req.ServiceURL != "" {
|
||||||
|
|
||||||
filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.CoverURL, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.EmbedMaxQualityCover, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL)
|
filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.CoverURL, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.EmbedMaxQualityCover, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL)
|
||||||
} else {
|
} else {
|
||||||
if req.SpotifyID == "" {
|
if req.SpotifyID == "" {
|
||||||
return DownloadResponse{
|
return DownloadResponse{
|
||||||
@@ -300,7 +300,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
|||||||
Error: "Spotify ID is required for Amazon Music",
|
Error: "Spotify ID is required for Amazon Music",
|
||||||
}, fmt.Errorf("spotify ID is required for Amazon Music")
|
}, fmt.Errorf("spotify ID is required for Amazon Music")
|
||||||
}
|
}
|
||||||
filename, err = downloader.DownloadBySpotifyID(req.SpotifyID, req.OutputDir, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.CoverURL, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.EmbedMaxQualityCover, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL)
|
filename, err = downloader.DownloadBySpotifyID(req.SpotifyID, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.CoverURL, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.EmbedMaxQualityCover, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, spotifyURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "tidal":
|
case "tidal":
|
||||||
|
|||||||
+209
-5
@@ -1,12 +1,15 @@
|
|||||||
package backend
|
package backend
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -44,6 +47,22 @@ type DoubleDoubleStatusResponse struct {
|
|||||||
} `json:"current"`
|
} `json:"current"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LucidaLoadResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Server string `json:"server"`
|
||||||
|
Handoff string `json:"handoff"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LucidaStatusResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Progress struct {
|
||||||
|
Current int64 `json:"current"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
} `json:"progress"`
|
||||||
|
}
|
||||||
|
|
||||||
func NewAmazonDownloader() *AmazonDownloader {
|
func NewAmazonDownloader() *AmazonDownloader {
|
||||||
return &AmazonDownloader{
|
return &AmazonDownloader{
|
||||||
client: &http.Client{
|
client: &http.Client{
|
||||||
@@ -175,8 +194,193 @@ func (a *AmazonDownloader) GetAmazonURLFromSpotify(spotifyTrackID string) (strin
|
|||||||
return amazonURL, nil
|
return amazonURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AmazonDownloader) DownloadFromService(amazonURL, outputDir string) (string, error) {
|
func (a *AmazonDownloader) extractData(html string, patterns []string) string {
|
||||||
|
for _, p := range patterns {
|
||||||
|
re := regexp.MustCompile(p)
|
||||||
|
matches := re.FindStringSubmatch(html)
|
||||||
|
if len(matches) > 1 {
|
||||||
|
return matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AmazonDownloader) DownloadFromLucida(amazonURL, outputDir, quality string) (string, error) {
|
||||||
|
tr := &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
}
|
||||||
|
jar, _ := cookiejar.New(nil)
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: tr,
|
||||||
|
Jar: jar,
|
||||||
|
Timeout: 120 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
userAgent := a.getRandomUserAgent()
|
||||||
|
|
||||||
|
fmt.Printf("Initializing lucida for Amazon Music... (Target: %s)\n", amazonURL)
|
||||||
|
lucidaBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9sdWNpZGEudG8vP3VybD0lcyZjb3VudHJ5PWF1dG8=")
|
||||||
|
lucidaURL := fmt.Sprintf(string(lucidaBase), url.QueryEscape(amazonURL))
|
||||||
|
req, _ := http.NewRequest("GET", lucidaURL, nil)
|
||||||
|
req.Header.Set("User-Agent", userAgent)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||||
|
html := string(bodyBytes)
|
||||||
|
|
||||||
|
token := a.extractData(html, []string{`token:"([^"]+)"`, `"token"\s*:\s*"([^"]+)"`})
|
||||||
|
streamURL := a.extractData(html, []string{`"url":"([^"]+)"`, `url:"([^"]+)"`})
|
||||||
|
expiry := a.extractData(html, []string{`tokenExpiry:(\d+)`, `"tokenExpiry"\s*:\s*(\d+)`})
|
||||||
|
|
||||||
|
if token == "" || streamURL == "" {
|
||||||
|
errorMsg := a.extractData(html, []string{`error:"([^"]+)"`, `"error"\s*:\s*"([^"]+)"`})
|
||||||
|
if errorMsg != "" {
|
||||||
|
return "", fmt.Errorf("lucida error: %s", errorMsg)
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("could not extract required data from lucida")
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedToken := token
|
||||||
|
if secondBase64, err := base64.StdEncoding.DecodeString(token); err == nil {
|
||||||
|
if firstBase64, err := base64.StdEncoding.DecodeString(string(secondBase64)); err == nil {
|
||||||
|
decodedToken = string(firstBase64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
streamURL = strings.ReplaceAll(streamURL, `\/`, `/`)
|
||||||
|
fmt.Printf("Fetching Amazon stream via Lucida...\n")
|
||||||
|
|
||||||
|
loadPayload := map[string]interface{}{
|
||||||
|
"account": map[string]string{"id": "auto", "type": "country"},
|
||||||
|
"compat": "false", "downscale": "original", "handoff": true,
|
||||||
|
"metadata": true, "private": true,
|
||||||
|
"token": map[string]interface{}{"primary": decodedToken, "expiry": expiry},
|
||||||
|
"upload": map[string]bool{"enabled": false},
|
||||||
|
"url": streamURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
payloadBytes, _ := json.Marshal(loadPayload)
|
||||||
|
loadAPI, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9sdWNpZGEudG8vYXBpL2xvYWQ/dXJsPS9hcGkvZmV0Y2gvc3RyZWFtL3Yy")
|
||||||
|
req, _ = http.NewRequest("POST", string(loadAPI), bytes.NewBuffer(payloadBytes))
|
||||||
|
req.Header.Set("User-Agent", userAgent)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
for _, cookie := range client.Jar.Cookies(req.URL) {
|
||||||
|
if cookie.Name == "csrf_token" {
|
||||||
|
req.Header.Set("X-CSRF-Token", cookie.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var loadData LucidaLoadResponse
|
||||||
|
json.NewDecoder(resp.Body).Decode(&loadData)
|
||||||
|
|
||||||
|
if !loadData.Success {
|
||||||
|
return "", fmt.Errorf("lucida load request failed: %s", loadData.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly8=")
|
||||||
|
completionBase, _ := base64.StdEncoding.DecodeString("Lmx1Y2lkYS50by9hcGkvZmV0Y2gvcmVxdWVzdC8=")
|
||||||
|
completionURL := fmt.Sprintf("%s%s%s%s", string(serviceBase), loadData.Server, string(completionBase), loadData.Handoff)
|
||||||
|
fmt.Println("Processing on Lucida server...")
|
||||||
|
|
||||||
|
var finalStatus LucidaStatusResponse
|
||||||
|
for {
|
||||||
|
req, _ = http.NewRequest("GET", completionURL, nil)
|
||||||
|
req.Header.Set("User-Agent", userAgent)
|
||||||
|
resp, err = client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewDecoder(resp.Body).Decode(&finalStatus)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
if finalStatus.Status == "completed" {
|
||||||
|
fmt.Println("\nTrack processing completed!")
|
||||||
|
break
|
||||||
|
} else if finalStatus.Status == "error" {
|
||||||
|
return "", fmt.Errorf("lucida processing failed: %s", finalStatus.Message)
|
||||||
|
} else if finalStatus.Progress.Total > 0 {
|
||||||
|
percent := (finalStatus.Progress.Current * 100) / finalStatus.Progress.Total
|
||||||
|
fmt.Printf("\rLucida Progress: %d%%", percent)
|
||||||
|
}
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadSuffix, _ := base64.StdEncoding.DecodeString("L2Rvd25sb2Fk")
|
||||||
|
downloadURL := fmt.Sprintf("%s%s%s%s%s", string(serviceBase), loadData.Server, string(completionBase), loadData.Handoff, string(downloadSuffix))
|
||||||
|
req, _ = http.NewRequest("GET", downloadURL, nil)
|
||||||
|
req.Header.Set("User-Agent", userAgent)
|
||||||
|
resp, err = client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return "", fmt.Errorf("lucida download failed with status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName := "track.flac"
|
||||||
|
contentDisp := resp.Header.Get("Content-Disposition")
|
||||||
|
if contentDisp != "" {
|
||||||
|
re := regexp.MustCompile(`filename[*]?=([^;]+)`)
|
||||||
|
if matches := re.FindStringSubmatch(contentDisp); len(matches) > 1 {
|
||||||
|
rawName := strings.Trim(matches[1], `"'`)
|
||||||
|
if strings.HasPrefix(rawName, "UTF-8''") {
|
||||||
|
decodedName, _ := url.PathUnescape(rawName[7:])
|
||||||
|
fileName = decodedName
|
||||||
|
} else {
|
||||||
|
fileName = rawName
|
||||||
|
}
|
||||||
|
|
||||||
|
reg := regexp.MustCompile(`[<>:"/\\|?*]`)
|
||||||
|
fileName = reg.ReplaceAllString(fileName, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := filepath.Join(outputDir, fileName)
|
||||||
|
out, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
fmt.Printf("Downloading from Lucida: %s\n", fileName)
|
||||||
|
|
||||||
|
pw := NewProgressWriter(out)
|
||||||
|
_, err = io.Copy(pw, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
out.Close()
|
||||||
|
os.Remove(filePath)
|
||||||
|
return "", fmt.Errorf("failed to write file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\rDownloaded: %.2f MB (Complete)\n", float64(pw.GetTotal())/(1024*1024))
|
||||||
|
return filePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AmazonDownloader) DownloadFromService(amazonURL, outputDir, quality string) (string, error) {
|
||||||
|
fmt.Println("Attempting download via Lucida (Priority)...")
|
||||||
|
filePath, err := a.DownloadFromLucida(amazonURL, outputDir, quality)
|
||||||
|
if err == nil {
|
||||||
|
return filePath, nil
|
||||||
|
}
|
||||||
|
fmt.Printf("Lucida failed: %v\nTrying Double-Double as fallback...\n", err)
|
||||||
|
|
||||||
var lastError error
|
var lastError error
|
||||||
|
lastError = err
|
||||||
|
|
||||||
for _, region := range a.regions {
|
for _, region := range a.regions {
|
||||||
fmt.Printf("\nTrying region: %s...\n", region)
|
fmt.Printf("\nTrying region: %s...\n", region)
|
||||||
@@ -357,7 +561,7 @@ func (a *AmazonDownloader) DownloadFromService(amazonURL, outputDir string) (str
|
|||||||
return "", fmt.Errorf("all regions failed. Last error: %v", lastError)
|
return "", fmt.Errorf("all regions failed. Last error: %v", lastError)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL string, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, embedMaxQualityCover bool, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyURL string) (string, error) {
|
func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL string, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, embedMaxQualityCover bool, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyURL string) (string, error) {
|
||||||
|
|
||||||
if outputDir != "." {
|
if outputDir != "." {
|
||||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||||
@@ -377,7 +581,7 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, filenameFormat st
|
|||||||
|
|
||||||
fmt.Printf("Using Amazon URL: %s\n", amazonURL)
|
fmt.Printf("Using Amazon URL: %s\n", amazonURL)
|
||||||
|
|
||||||
filePath, err := a.DownloadFromService(amazonURL, outputDir)
|
filePath, err := a.DownloadFromService(amazonURL, outputDir, quality)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -492,12 +696,12 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, filenameFormat st
|
|||||||
return filePath, nil
|
return filePath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AmazonDownloader) DownloadBySpotifyID(spotifyTrackID, outputDir, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL string, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, embedMaxQualityCover bool, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyURL string) (string, error) {
|
func (a *AmazonDownloader) DownloadBySpotifyID(spotifyTrackID, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL string, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, embedMaxQualityCover bool, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyURL string) (string, error) {
|
||||||
|
|
||||||
amazonURL, err := a.GetAmazonURLFromSpotify(spotifyTrackID)
|
amazonURL, err := a.GetAmazonURLFromSpotify(spotifyTrackID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return a.DownloadByURL(amazonURL, outputDir, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, embedMaxQualityCover, spotifyTotalDiscs, spotifyCopyright, spotifyPublisher, spotifyURL)
|
return a.DownloadByURL(amazonURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, embedMaxQualityCover, spotifyTotalDiscs, spotifyCopyright, spotifyPublisher, spotifyURL)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -253,11 +253,11 @@ 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.getAvailability(track.spotify_id || "")} downloadingCover={cover.downloadingCover} downloadedCover={cover.downloadedCovers.has(track.spotify_id || "")} failedCover={cover.failedCovers.has(track.spotify_id || "")} skippedCover={cover.skippedCovers.has(track.spotify_id || "")} onDownload={download.handleDownloadTrack} onDownloadLyrics={(spotifyId, name, artists, albumName, albumArtist, releaseDate, discNumber) => lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, undefined, undefined, albumArtist, releaseDate, discNumber)} onCheckAvailability={availability.checkAvailability} onDownloadCover={(coverUrl, trackName, artistName, albumName, _playlistName, _position, trackId, albumArtist, releaseDate, discNumber) => cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, undefined, undefined, trackId, albumArtist, releaseDate, discNumber)} 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, undefined, true)} onDownloadSelected={() => download.handleDownloadSelected(selectedTracks, track_list, undefined, 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);
|
||||||
@@ -271,7 +271,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
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.owner.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.owner.name, position, trackId, albumArtist, releaseDate, discNumber)} onCheckAvailability={availability.checkAvailability} onDownloadAllLyrics={() => lyrics.handleDownloadAllLyrics(track_list, playlist_info.owner.name)} onDownloadAllCovers={() => cover.handleDownloadAllCovers(track_list, playlist_info.owner.name)} onDownloadAll={() => download.handleDownloadAll(track_list, playlist_info.owner.name)} onDownloadSelected={() => download.handleDownloadSelected(selectedTracks, track_list, playlist_info.owner.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);
|
||||||
|
|||||||
@@ -279,14 +279,11 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest }: Setting
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>)}
|
</Select>)}
|
||||||
|
|
||||||
{tempSettings.downloader === "amazon" && (<Select value={tempSettings.amazonQuality} onValueChange={(value: "HI_RES") => setTempSettings((prev) => ({ ...prev, amazonQuality: value }))}>
|
{tempSettings.downloader === "amazon" && (
|
||||||
<SelectTrigger className="h-9 w-fit">
|
<div className="h-9 px-3 flex items-center text-sm font-medium border border-input rounded-md bg-muted/30 text-muted-foreground whitespace-nowrap cursor-default">
|
||||||
<SelectValue />
|
16-bit/44.1kHz or 24-bit/48kHz+
|
||||||
</SelectTrigger>
|
</div>
|
||||||
<SelectContent>
|
)}
|
||||||
<SelectItem value="HI_RES">Hi-Res (24-bit/48kHz+)</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ export function useCover() {
|
|||||||
track: position,
|
track: position,
|
||||||
playlist: playlistName?.replace(/\//g, placeholder),
|
playlist: playlistName?.replace(/\//g, placeholder),
|
||||||
};
|
};
|
||||||
if (playlistName && !isAlbum) {
|
const folderTemplate = settings.folderTemplate || "";
|
||||||
|
const useAlbumSubfolder = folderTemplate.includes("{album}") || folderTemplate.includes("{album_artist}") || folderTemplate.includes("{playlist}");
|
||||||
|
if (playlistName && (!isAlbum || !useAlbumSubfolder)) {
|
||||||
outputDir = joinPath(os, outputDir, sanitizePath(playlistName.replace(/\//g, " "), os));
|
outputDir = joinPath(os, outputDir, sanitizePath(playlistName.replace(/\//g, " "), os));
|
||||||
}
|
}
|
||||||
if (settings.folderTemplate) {
|
if (settings.folderTemplate) {
|
||||||
@@ -129,7 +131,9 @@ export function useCover() {
|
|||||||
track: trackPosition,
|
track: trackPosition,
|
||||||
playlist: playlistName?.replace(/\//g, placeholder),
|
playlist: playlistName?.replace(/\//g, placeholder),
|
||||||
};
|
};
|
||||||
if (playlistName && !isAlbum) {
|
const folderTemplate = settings.folderTemplate || "";
|
||||||
|
const useAlbumSubfolder = folderTemplate.includes("{album}") || folderTemplate.includes("{album_artist}") || folderTemplate.includes("{playlist}");
|
||||||
|
if (playlistName && (!isAlbum || !useAlbumSubfolder)) {
|
||||||
outputDir = joinPath(os, outputDir, sanitizePath(playlistName.replace(/\//g, " "), os));
|
outputDir = joinPath(os, outputDir, sanitizePath(playlistName.replace(/\//g, " "), os));
|
||||||
}
|
}
|
||||||
if (settings.folderTemplate) {
|
if (settings.folderTemplate) {
|
||||||
|
|||||||
@@ -82,7 +82,9 @@ export function useDownload() {
|
|||||||
year: yearValue,
|
year: yearValue,
|
||||||
playlist: playlistName?.replace(/\//g, placeholder),
|
playlist: playlistName?.replace(/\//g, placeholder),
|
||||||
};
|
};
|
||||||
if (playlistName) {
|
const folderTemplate = settings.folderTemplate || "";
|
||||||
|
const useAlbumSubfolder = folderTemplate.includes("{album}") || folderTemplate.includes("{album_artist}") || folderTemplate.includes("{playlist}");
|
||||||
|
if (playlistName && !useAlbumSubfolder) {
|
||||||
outputDir = joinPath(os, outputDir, sanitizePath(playlistName.replace(/\//g, " "), os));
|
outputDir = joinPath(os, outputDir, sanitizePath(playlistName.replace(/\//g, " "), os));
|
||||||
}
|
}
|
||||||
if (settings.folderTemplate) {
|
if (settings.folderTemplate) {
|
||||||
@@ -346,7 +348,8 @@ export function useDownload() {
|
|||||||
year: yearValue,
|
year: yearValue,
|
||||||
playlist: folderName?.replace(/\//g, placeholder),
|
playlist: folderName?.replace(/\//g, placeholder),
|
||||||
};
|
};
|
||||||
if (folderName && !isAlbum) {
|
const useAlbumTag = settings.folderTemplate?.includes("{album}");
|
||||||
|
if (folderName && (!isAlbum || !useAlbumTag)) {
|
||||||
outputDir = joinPath(os, outputDir, sanitizePath(folderName.replace(/\//g, " "), os));
|
outputDir = joinPath(os, outputDir, sanitizePath(folderName.replace(/\//g, " "), os));
|
||||||
}
|
}
|
||||||
if (settings.folderTemplate) {
|
if (settings.folderTemplate) {
|
||||||
@@ -575,7 +578,8 @@ export function useDownload() {
|
|||||||
setDownloadProgress(0);
|
setDownloadProgress(0);
|
||||||
let outputDir = settings.downloadPath;
|
let outputDir = settings.downloadPath;
|
||||||
const os = settings.operatingSystem;
|
const os = settings.operatingSystem;
|
||||||
if (folderName && !isAlbum) {
|
const useAlbumTag = settings.folderTemplate?.includes("{album}");
|
||||||
|
if (folderName && (!isAlbum || !useAlbumTag)) {
|
||||||
outputDir = joinPath(os, outputDir, sanitizePath(folderName.replace(/\//g, " "), os));
|
outputDir = joinPath(os, outputDir, sanitizePath(folderName.replace(/\//g, " "), os));
|
||||||
}
|
}
|
||||||
const selectedTrackObjects = selectedTracks
|
const selectedTrackObjects = selectedTracks
|
||||||
@@ -723,7 +727,8 @@ export function useDownload() {
|
|||||||
setDownloadProgress(0);
|
setDownloadProgress(0);
|
||||||
let outputDir = settings.downloadPath;
|
let outputDir = settings.downloadPath;
|
||||||
const os = settings.operatingSystem;
|
const os = settings.operatingSystem;
|
||||||
if (folderName && !isAlbum) {
|
const useAlbumTag = settings.folderTemplate?.includes("{album}");
|
||||||
|
if (folderName && (!isAlbum || !useAlbumTag)) {
|
||||||
outputDir = joinPath(os, outputDir, sanitizePath(folderName.replace(/\//g, " "), os));
|
outputDir = joinPath(os, outputDir, sanitizePath(folderName.replace(/\//g, " "), os));
|
||||||
}
|
}
|
||||||
logger.info(`checking existing files in parallel...`);
|
logger.info(`checking existing files in parallel...`);
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ export function useLyrics() {
|
|||||||
track: position,
|
track: position,
|
||||||
playlist: playlistName?.replace(/\//g, placeholder),
|
playlist: playlistName?.replace(/\//g, placeholder),
|
||||||
};
|
};
|
||||||
if (playlistName && !isAlbum) {
|
const folderTemplate = settings.folderTemplate || "";
|
||||||
|
const useAlbumSubfolder = folderTemplate.includes("{album}") || folderTemplate.includes("{album_artist}") || folderTemplate.includes("{playlist}");
|
||||||
|
if (playlistName && (!isAlbum || !useAlbumSubfolder)) {
|
||||||
outputDir = joinPath(os, outputDir, sanitizePath(playlistName.replace(/\//g, " "), os));
|
outputDir = joinPath(os, outputDir, sanitizePath(playlistName.replace(/\//g, " "), os));
|
||||||
}
|
}
|
||||||
if (settings.folderTemplate) {
|
if (settings.folderTemplate) {
|
||||||
@@ -125,7 +127,9 @@ export function useLyrics() {
|
|||||||
track: trackPosition,
|
track: trackPosition,
|
||||||
playlist: playlistName?.replace(/\//g, placeholder),
|
playlist: playlistName?.replace(/\//g, placeholder),
|
||||||
};
|
};
|
||||||
if (playlistName && !isAlbum) {
|
const folderTemplate = settings.folderTemplate || "";
|
||||||
|
const useAlbumSubfolder = folderTemplate.includes("{album}") || folderTemplate.includes("{album_artist}") || folderTemplate.includes("{playlist}");
|
||||||
|
if (playlistName && (!isAlbum || !useAlbumSubfolder)) {
|
||||||
outputDir = joinPath(os, outputDir, sanitizePath(playlistName.replace(/\//g, " "), os));
|
outputDir = joinPath(os, outputDir, sanitizePath(playlistName.replace(/\//g, " "), os));
|
||||||
}
|
}
|
||||||
if (settings.folderTemplate) {
|
if (settings.folderTemplate) {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export interface Settings {
|
|||||||
operatingSystem: "Windows" | "linux/MacOS";
|
operatingSystem: "Windows" | "linux/MacOS";
|
||||||
tidalQuality: "LOSSLESS" | "HI_RES_LOSSLESS";
|
tidalQuality: "LOSSLESS" | "HI_RES_LOSSLESS";
|
||||||
qobuzQuality: "6" | "7";
|
qobuzQuality: "6" | "7";
|
||||||
amazonQuality: "HI_RES";
|
amazonQuality: "original";
|
||||||
}
|
}
|
||||||
export const FOLDER_PRESETS: Record<FolderPreset, {
|
export const FOLDER_PRESETS: Record<FolderPreset, {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -95,7 +95,7 @@ export const DEFAULT_SETTINGS: Settings = {
|
|||||||
operatingSystem: detectOS(),
|
operatingSystem: detectOS(),
|
||||||
tidalQuality: "LOSSLESS",
|
tidalQuality: "LOSSLESS",
|
||||||
qobuzQuality: "6",
|
qobuzQuality: "6",
|
||||||
amazonQuality: "HI_RES"
|
amazonQuality: "original"
|
||||||
};
|
};
|
||||||
export const FONT_OPTIONS: {
|
export const FONT_OPTIONS: {
|
||||||
value: FontFamily;
|
value: FontFamily;
|
||||||
@@ -194,7 +194,7 @@ function getSettingsFromLocalStorage(): Settings {
|
|||||||
parsed.qobuzQuality = "6";
|
parsed.qobuzQuality = "6";
|
||||||
}
|
}
|
||||||
if (!('amazonQuality' in parsed)) {
|
if (!('amazonQuality' in parsed)) {
|
||||||
parsed.amazonQuality = "HI_RES";
|
parsed.amazonQuality = "original";
|
||||||
}
|
}
|
||||||
return { ...DEFAULT_SETTINGS, ...parsed };
|
return { ...DEFAULT_SETTINGS, ...parsed };
|
||||||
}
|
}
|
||||||
@@ -264,7 +264,7 @@ export async function loadSettings(): Promise<Settings> {
|
|||||||
parsed.qobuzQuality = "6";
|
parsed.qobuzQuality = "6";
|
||||||
}
|
}
|
||||||
if (!('amazonQuality' in parsed)) {
|
if (!('amazonQuality' in parsed)) {
|
||||||
parsed.amazonQuality = "HI_RES";
|
parsed.amazonQuality = "original";
|
||||||
}
|
}
|
||||||
cachedSettings = { ...DEFAULT_SETTINGS, ...parsed };
|
cachedSettings = { ...DEFAULT_SETTINGS, ...parsed };
|
||||||
return cachedSettings!;
|
return cachedSettings!;
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export interface AlbumResponse {
|
|||||||
track_list: TrackMetadata[];
|
track_list: TrackMetadata[];
|
||||||
}
|
}
|
||||||
export interface PlaylistInfo {
|
export interface PlaylistInfo {
|
||||||
|
name: string;
|
||||||
tracks: {
|
tracks: {
|
||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user