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)
|
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 {
|
type SegmentTemplate struct {
|
||||||
XMLName xml.Name `xml:"MPD"`
|
|
||||||
Period struct {
|
|
||||||
AdaptationSet struct {
|
|
||||||
Representation struct {
|
|
||||||
SegmentTemplate struct {
|
|
||||||
Initialization string `xml:"initialization,attr"`
|
Initialization string `xml:"initialization,attr"`
|
||||||
Media string `xml:"media,attr"`
|
Media string `xml:"media,attr"`
|
||||||
Timeline struct {
|
Timeline struct {
|
||||||
Segments []struct {
|
Segments []struct {
|
||||||
Duration int `xml:"d,attr"`
|
Duration int64 `xml:"d,attr"`
|
||||||
Repeat int `xml:"r,attr"`
|
Repeat int `xml:"r,attr"`
|
||||||
} `xml:"S"`
|
} `xml:"S"`
|
||||||
} `xml:"SegmentTimeline"`
|
} `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"`
|
} `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,15 +793,69 @@ 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
segTemplate := mpd.Period.AdaptationSet.Representation.SegmentTemplate
|
for _, rep := range as.Representations {
|
||||||
initURL = segTemplate.Initialization
|
if rep.SegmentTemplate != nil {
|
||||||
mediaTemplate := segTemplate.Media
|
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="([^"]+)"`)
|
initRe := regexp.MustCompile(`initialization="([^"]+)"`)
|
||||||
mediaRe := regexp.MustCompile(`media="([^"]+)"`)
|
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 {
|
if match := mediaRe.FindStringSubmatch(manifestStr); len(match) > 1 {
|
||||||
mediaTemplate = match[1]
|
mediaTemplate = match[1]
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if initURL == "" {
|
if initURL == "" {
|
||||||
return "", "", nil, fmt.Errorf("no initialization URL found in manifest")
|
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, "&", "&")
|
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)
|
||||||
|
|
||||||
if segmentCount == 0 {
|
|
||||||
segRe := regexp.MustCompile(`<S d="\d+"(?: r="(\d+)")?`)
|
|
||||||
matches := segRe.FindAllStringSubmatch(manifestStr, -1)
|
|
||||||
for _, match := range matches {
|
for _, match := range matches {
|
||||||
repeat := 0
|
repeat := 0
|
||||||
if len(match) > 1 && match[1] != "" {
|
rRe := regexp.MustCompile(`r="(\d+)"`)
|
||||||
fmt.Sscanf(match[1], "%d", &repeat)
|
if rMatch := rRe.FindStringSubmatch(match); len(rMatch) > 1 {
|
||||||
|
fmt.Sscanf(rMatch[1], "%d", &repeat)
|
||||||
}
|
}
|
||||||
segmentCount += repeat + 1
|
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++ {
|
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)
|
||||||
|
|||||||
+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 { 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();
|
||||||
|
|||||||
Reference in New Issue
Block a user