v7.0.5
This commit is contained in:
+89
-27
@@ -740,28 +740,35 @@ func (t *TidalDownloader) Download(spotifyTrackID, outputDir, quality, filenameF
|
||||
return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber, spotifyCoverURL, embedMaxQualityCover, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyTotalDiscs, spotifyCopyright, spotifyPublisher, spotifyURL)
|
||||
}
|
||||
|
||||
type MPD struct {
|
||||
XMLName xml.Name `xml:"MPD"`
|
||||
Period struct {
|
||||
AdaptationSet struct {
|
||||
Representation struct {
|
||||
SegmentTemplate struct {
|
||||
type SegmentTemplate struct {
|
||||
Initialization string `xml:"initialization,attr"`
|
||||
Media string `xml:"media,attr"`
|
||||
Timeline struct {
|
||||
Segments []struct {
|
||||
Duration int `xml:"d,attr"`
|
||||
Duration int64 `xml:"d,attr"`
|
||||
Repeat int `xml:"r,attr"`
|
||||
} `xml:"S"`
|
||||
} `xml:"SegmentTimeline"`
|
||||
} `xml:"SegmentTemplate"`
|
||||
}
|
||||
|
||||
type MPD struct {
|
||||
XMLName xml.Name `xml:"MPD"`
|
||||
Period struct {
|
||||
AdaptationSets []struct {
|
||||
MimeType string `xml:"mimeType,attr"`
|
||||
Codecs string `xml:"codecs,attr"`
|
||||
Representations []struct {
|
||||
ID string `xml:"id,attr"`
|
||||
Codecs string `xml:"codecs,attr"`
|
||||
Bandwidth int `xml:"bandwidth,attr"`
|
||||
SegmentTemplate *SegmentTemplate `xml:"SegmentTemplate"`
|
||||
} `xml:"Representation"`
|
||||
SegmentTemplate *SegmentTemplate `xml:"SegmentTemplate"`
|
||||
} `xml:"AdaptationSet"`
|
||||
} `xml:"Period"`
|
||||
}
|
||||
|
||||
func parseManifest(manifestB64 string) (directURL string, initURL string, mediaURLs []string, err error) {
|
||||
|
||||
manifestBytes, err := base64.StdEncoding.DecodeString(manifestB64)
|
||||
if err != nil {
|
||||
return "", "", nil, fmt.Errorf("failed to decode manifest: %w", err)
|
||||
@@ -769,8 +776,7 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU
|
||||
|
||||
manifestStr := string(manifestBytes)
|
||||
|
||||
if strings.HasPrefix(manifestStr, "{") {
|
||||
|
||||
if strings.HasPrefix(strings.TrimSpace(manifestStr), "{") {
|
||||
var btsManifest TidalBTSManifest
|
||||
if err := json.Unmarshal(manifestBytes, &btsManifest); err != nil {
|
||||
return "", "", nil, fmt.Errorf("failed to parse BTS manifest: %w", err)
|
||||
@@ -787,15 +793,69 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU
|
||||
fmt.Println("Manifest: DASH format")
|
||||
|
||||
var mpd MPD
|
||||
if err := xml.Unmarshal(manifestBytes, &mpd); err != nil {
|
||||
return "", "", nil, fmt.Errorf("failed to parse manifest XML: %w", err)
|
||||
var segTemplate *SegmentTemplate
|
||||
|
||||
if err := xml.Unmarshal(manifestBytes, &mpd); err == nil {
|
||||
var selectedBandwidth int
|
||||
var selectedCodecs string
|
||||
|
||||
for _, as := range mpd.Period.AdaptationSets {
|
||||
|
||||
if as.SegmentTemplate != nil {
|
||||
|
||||
if segTemplate == nil {
|
||||
segTemplate = as.SegmentTemplate
|
||||
selectedCodecs = as.Codecs
|
||||
}
|
||||
}
|
||||
|
||||
segTemplate := mpd.Period.AdaptationSet.Representation.SegmentTemplate
|
||||
initURL = segTemplate.Initialization
|
||||
mediaTemplate := segTemplate.Media
|
||||
for _, rep := range as.Representations {
|
||||
if rep.SegmentTemplate != nil {
|
||||
if rep.Bandwidth > selectedBandwidth {
|
||||
selectedBandwidth = rep.Bandwidth
|
||||
segTemplate = rep.SegmentTemplate
|
||||
|
||||
if initURL == "" || mediaTemplate == "" {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
var mediaTemplate string
|
||||
segmentCount := 0
|
||||
|
||||
if segTemplate != nil {
|
||||
initURL = segTemplate.Initialization
|
||||
mediaTemplate = segTemplate.Media
|
||||
|
||||
for _, seg := range segTemplate.Timeline.Segments {
|
||||
segmentCount += seg.Repeat + 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="([^"]+)"`)
|
||||
@@ -806,7 +866,6 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU
|
||||
if match := mediaRe.FindStringSubmatch(manifestStr); len(match) > 1 {
|
||||
mediaTemplate = match[1]
|
||||
}
|
||||
}
|
||||
|
||||
if initURL == "" {
|
||||
return "", "", nil, fmt.Errorf("no initialization URL found in manifest")
|
||||
@@ -815,23 +874,26 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU
|
||||
initURL = strings.ReplaceAll(initURL, "&", "&")
|
||||
mediaTemplate = strings.ReplaceAll(mediaTemplate, "&", "&")
|
||||
|
||||
segmentCount := 0
|
||||
for _, seg := range segTemplate.Timeline.Segments {
|
||||
segmentCount += seg.Repeat + 1
|
||||
}
|
||||
segmentCount = 0
|
||||
|
||||
segTagRe := regexp.MustCompile(`<S\s+[^>]*>`)
|
||||
matches := segTagRe.FindAllString(manifestStr, -1)
|
||||
|
||||
if segmentCount == 0 {
|
||||
segRe := regexp.MustCompile(`<S d="\d+"(?: r="(\d+)")?`)
|
||||
matches := segRe.FindAllStringSubmatch(manifestStr, -1)
|
||||
for _, match := range matches {
|
||||
repeat := 0
|
||||
if len(match) > 1 && match[1] != "" {
|
||||
fmt.Sscanf(match[1], "%d", &repeat)
|
||||
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 {
|
||||
return "", "", nil, fmt.Errorf("no segments found in manifest (XML: %d, Regex: 0)", len(matches))
|
||||
}
|
||||
|
||||
fmt.Printf("Parsed manifest via Regex: %d segments\n", segmentCount)
|
||||
|
||||
for i := 1; i <= segmentCount; i++ {
|
||||
mediaURL := strings.ReplaceAll(mediaTemplate, "$Number$", fmt.Sprintf("%d", i))
|
||||
mediaURLs = append(mediaURLs, mediaURL)
|
||||
|
||||
+10
-1
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect, useCallback, useLayoutEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -57,6 +57,15 @@ function App() {
|
||||
const cover = useCover();
|
||||
const availability = useAvailability();
|
||||
const downloadQueue = useDownloadQueueDialog();
|
||||
useLayoutEffect(() => {
|
||||
const savedSettings = getSettings();
|
||||
if (savedSettings) {
|
||||
applyThemeMode(savedSettings.themeMode);
|
||||
applyTheme(savedSettings.theme);
|
||||
applyFont(savedSettings.fontFamily);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const initSettings = async () => {
|
||||
const settings = await loadSettings();
|
||||
|
||||
Reference in New Issue
Block a user