mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-26 12:52:40 +02:00
refactor(metadata): move cover resolution to extensions
This commit is contained in:
@@ -1393,10 +1393,9 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
"downloadCoverToFile" -> {
|
||||
val coverUrl = call.argument<String>("cover_url") ?: ""
|
||||
val outputPath = call.argument<String>("output_path") ?: ""
|
||||
val maxQuality = call.argument<Boolean>("max_quality") ?: true
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
Gobackend.downloadCoverToFile(coverUrl, outputPath, maxQuality)
|
||||
Gobackend.downloadCoverToFile(coverUrl, outputPath, false)
|
||||
"""{"success":true}"""
|
||||
} catch (e: Exception) {
|
||||
"""{"success":false,"error":"${e.message?.replace("\"", "'")}"}"""
|
||||
|
||||
@@ -503,7 +503,7 @@ internal fun NativeDownloadFinalizer.downloadCoverForMetadata(context: Context,
|
||||
Gobackend.downloadCoverToFile(
|
||||
coverUrl,
|
||||
output.absolutePath,
|
||||
input.request.optBoolean("embed_max_quality_cover", true)
|
||||
false
|
||||
)
|
||||
if (output.exists() && output.length() > 0L) {
|
||||
output
|
||||
|
||||
+7
-202
@@ -8,158 +8,28 @@ import (
|
||||
_ "image/png"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
spotifySize300 = "ab67616d00001e02"
|
||||
spotifySize640 = "ab67616d0000b273"
|
||||
spotifySizeMax = "ab67616d000082c1"
|
||||
)
|
||||
|
||||
// Square CDN covers using this path shape may return an image whose decoded
|
||||
// dimensions differ from the dimensions advertised in the URL. Max-quality
|
||||
// selection therefore probes both useful high-resolution variants and checks
|
||||
// the image headers instead of trusting the filename.
|
||||
var squareCoverSizeRegex = regexp.MustCompile(`/(\d+)x(\d+)-\d+-\d+-\d+-\d+\.jpg$`)
|
||||
|
||||
var tidalSizeRegex = regexp.MustCompile(`/\d+x\d+\.jpg$`)
|
||||
|
||||
var qobuzSizeRegex = regexp.MustCompile(`_\d+\.jpg$`)
|
||||
|
||||
func convertSmallToMedium(imageURL string) string {
|
||||
if strings.Contains(imageURL, spotifySize300) {
|
||||
return strings.Replace(imageURL, spotifySize300, spotifySize640, 1)
|
||||
}
|
||||
return imageURL
|
||||
}
|
||||
|
||||
func downloadCoverToMemory(coverURL string, maxQuality bool) ([]byte, error) {
|
||||
// downloadCoverToMemory downloads exactly the URL supplied by the metadata
|
||||
// provider. Cover-resolution selection belongs to the provider extension;
|
||||
// the app must not infer a provider from its CDN URL or rewrite that URL.
|
||||
func downloadCoverToMemory(coverURL string) ([]byte, error) {
|
||||
if coverURL == "" {
|
||||
return nil, fmt.Errorf("no cover URL provided")
|
||||
}
|
||||
|
||||
GoLog("[Cover] Original URL: %s", coverURL)
|
||||
|
||||
downloadURL := convertSmallToMedium(coverURL)
|
||||
if downloadURL != coverURL {
|
||||
GoLog("[Cover] Upgraded 300x300 → 640x640")
|
||||
}
|
||||
|
||||
if !maxQuality {
|
||||
GoLog("[Cover] Final URL: %s", downloadURL)
|
||||
data, err := fetchCoverCached(downloadURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append([]byte(nil), data...), nil
|
||||
}
|
||||
|
||||
candidates := maxQualityCoverCandidateURLs(downloadURL)
|
||||
data, selectedURL, width, height, err := fetchBestCoverCandidate(candidates)
|
||||
GoLog("[Cover] Provider URL: %s", coverURL)
|
||||
data, err := fetchCoverCached(coverURL)
|
||||
if err != nil {
|
||||
// A CDN can reject an upgraded size while the provider-supplied URL is
|
||||
// still valid. Preserve that URL as the final fallback.
|
||||
if len(candidates) == 1 && candidates[0] == downloadURL {
|
||||
return nil, err
|
||||
}
|
||||
data, err = fetchCoverCached(downloadURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
selectedURL = downloadURL
|
||||
width, height = coverDimensions(data)
|
||||
return nil, err
|
||||
}
|
||||
GoLog("[Cover] Selected URL: %s (%dx%d, %d KB)", selectedURL, width, height, len(data)/1024)
|
||||
// Cached bytes are shared across goroutines and must never be mutated;
|
||||
// hand callers their own copy.
|
||||
return append([]byte(nil), data...), nil
|
||||
}
|
||||
|
||||
type fetchedCoverCandidate struct {
|
||||
url string
|
||||
data []byte
|
||||
width, height int
|
||||
err error
|
||||
}
|
||||
|
||||
func maxQualityCoverCandidateURLs(coverURL string) []string {
|
||||
upgraded := upgradeToMaxQuality(coverURL)
|
||||
candidates := []string{upgraded}
|
||||
// This is deliberately based on the URL capability rather than a provider
|
||||
// or extension ID. Any metadata source returning the same square-cover URL
|
||||
// shape receives the same verified candidate selection.
|
||||
if squareCoverSizeRegex.MatchString(coverURL) {
|
||||
candidate1500 := squareCoverSizeRegex.ReplaceAllString(
|
||||
coverURL,
|
||||
"/1500x1500-000000-80-0-0.jpg",
|
||||
)
|
||||
if candidate1500 != upgraded {
|
||||
candidates = append(candidates, candidate1500)
|
||||
}
|
||||
}
|
||||
return uniqueNonEmptyStrings(candidates)
|
||||
}
|
||||
|
||||
func uniqueNonEmptyStrings(values []string) []string {
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
result := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[value]; exists {
|
||||
continue
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
result = append(result, value)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func fetchBestCoverCandidate(urls []string) ([]byte, string, int, int, error) {
|
||||
if len(urls) == 0 {
|
||||
return nil, "", 0, 0, fmt.Errorf("no cover candidates available")
|
||||
}
|
||||
results := make(chan fetchedCoverCandidate, len(urls))
|
||||
for _, candidateURL := range urls {
|
||||
go func(url string) {
|
||||
data, err := fetchCoverCached(url)
|
||||
width, height := coverDimensions(data)
|
||||
results <- fetchedCoverCandidate{
|
||||
url: url, data: data, width: width, height: height, err: err,
|
||||
}
|
||||
}(candidateURL)
|
||||
}
|
||||
|
||||
var best *fetchedCoverCandidate
|
||||
var firstErr error
|
||||
for range urls {
|
||||
candidate := <-results
|
||||
if candidate.err != nil || len(candidate.data) == 0 {
|
||||
if firstErr == nil {
|
||||
firstErr = candidate.err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if best == nil || coverCandidateBetter(candidate, *best) {
|
||||
copy := candidate
|
||||
best = ©
|
||||
}
|
||||
}
|
||||
if best == nil {
|
||||
if firstErr == nil {
|
||||
firstErr = fmt.Errorf("cover candidates returned no image data")
|
||||
}
|
||||
return nil, "", 0, 0, firstErr
|
||||
}
|
||||
return best.data, best.url, best.width, best.height, nil
|
||||
}
|
||||
|
||||
func coverDimensions(data []byte) (int, int) {
|
||||
if len(data) == 0 {
|
||||
return 0, 0
|
||||
@@ -171,15 +41,6 @@ func coverDimensions(data []byte) (int, int) {
|
||||
return config.Width, config.Height
|
||||
}
|
||||
|
||||
func coverCandidateBetter(candidate, current fetchedCoverCandidate) bool {
|
||||
candidatePixels := int64(candidate.width) * int64(candidate.height)
|
||||
currentPixels := int64(current.width) * int64(current.height)
|
||||
if candidatePixels != currentPixels {
|
||||
return candidatePixels > currentPixels
|
||||
}
|
||||
return len(candidate.data) > len(current.data)
|
||||
}
|
||||
|
||||
const (
|
||||
coverCacheMaxBytes = 24 * 1024 * 1024
|
||||
coverCacheTTL = 15 * time.Minute
|
||||
@@ -307,59 +168,3 @@ func fetchCoverBytes(downloadURL string) ([]byte, error) {
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func upgradeToMaxQuality(coverURL string) string {
|
||||
if strings.Contains(coverURL, spotifySize640) {
|
||||
return strings.Replace(coverURL, spotifySize640, spotifySizeMax, 1)
|
||||
}
|
||||
|
||||
if squareCoverSizeRegex.MatchString(coverURL) {
|
||||
return upgradeSquareCover(coverURL)
|
||||
}
|
||||
|
||||
if strings.Contains(coverURL, "resources.tidal.com") {
|
||||
return upgradeTidalCover(coverURL)
|
||||
}
|
||||
|
||||
if strings.Contains(coverURL, "static.qobuz.com") {
|
||||
return upgradeQobuzCover(coverURL)
|
||||
}
|
||||
|
||||
return coverURL
|
||||
}
|
||||
|
||||
func upgradeSquareCover(coverURL string) string {
|
||||
if !squareCoverSizeRegex.MatchString(coverURL) {
|
||||
return coverURL
|
||||
}
|
||||
|
||||
upgraded := squareCoverSizeRegex.ReplaceAllString(coverURL, "/1900x1900-000000-80-0-0.jpg")
|
||||
if upgraded != coverURL {
|
||||
GoLog("[Cover] Square CDN: probing 1900x1900 and 1500x1500")
|
||||
}
|
||||
return upgraded
|
||||
}
|
||||
|
||||
func upgradeTidalCover(coverURL string) string {
|
||||
if !strings.Contains(coverURL, "resources.tidal.com") {
|
||||
return coverURL
|
||||
}
|
||||
|
||||
upgraded := tidalSizeRegex.ReplaceAllString(coverURL, "/origin.jpg")
|
||||
if upgraded != coverURL {
|
||||
GoLog("[Cover] Tidal: upgraded to origin resolution")
|
||||
}
|
||||
return upgraded
|
||||
}
|
||||
|
||||
func upgradeQobuzCover(coverURL string) string {
|
||||
if !strings.Contains(coverURL, "static.qobuz.com") {
|
||||
return coverURL
|
||||
}
|
||||
|
||||
upgraded := qobuzSizeRegex.ReplaceAllString(coverURL, "_max.jpg")
|
||||
if upgraded != coverURL {
|
||||
GoLog("[Cover] Qobuz: upgraded to max resolution")
|
||||
}
|
||||
return upgraded
|
||||
}
|
||||
|
||||
+11
-78
@@ -1,33 +1,12 @@
|
||||
package gobackend
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func testPNG(t *testing.T, width, height int) []byte {
|
||||
t.Helper()
|
||||
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
img.Set(x, y, color.RGBA{R: uint8(x), G: uint8(y), B: 100, A: 255})
|
||||
}
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
if err := png.Encode(&buffer, img); err != nil {
|
||||
t.Fatalf("encode test cover: %v", err)
|
||||
}
|
||||
return buffer.Bytes()
|
||||
}
|
||||
|
||||
func resetCoverCache() {
|
||||
coverMu.Lock()
|
||||
coverCache = map[string]*coverCacheEntry{}
|
||||
@@ -116,72 +95,26 @@ func TestFetchCoverCachedTTLExpiry(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxQualityCoverCandidatesProbe1900And1500(t *testing.T) {
|
||||
url := "https://cdn-images.dzcdn.net/images/cover/abc/1000x1000-000000-80-0-0.jpg"
|
||||
candidates := maxQualityCoverCandidateURLs(url)
|
||||
if len(candidates) != 2 {
|
||||
t.Fatalf("expected two candidates, got %#v", candidates)
|
||||
}
|
||||
if !strings.Contains(candidates[0], "1900x1900") {
|
||||
t.Fatalf("first candidate = %q", candidates[0])
|
||||
}
|
||||
if !strings.Contains(candidates[1], "1500x1500") {
|
||||
t.Fatalf("second candidate = %q", candidates[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadCoverSelectsDecodedDimensionsBeforeByteSize(t *testing.T) {
|
||||
func TestDownloadCoverUsesProviderURLUnchanged(t *testing.T) {
|
||||
orig := coverFetch
|
||||
defer func() { coverFetch = orig }()
|
||||
resetCoverCache()
|
||||
|
||||
advertised1900ButSmaller := append(testPNG(t, 12, 12), bytes.Repeat([]byte{0}, 4096)...)
|
||||
actualLargerDimensions := testPNG(t, 15, 15)
|
||||
const providerURL = "https://i.scdn.co/image/ab67616d00001e02example"
|
||||
var requestedURL string
|
||||
coverFetch = func(url string) ([]byte, error) {
|
||||
switch {
|
||||
case strings.Contains(url, "1900x1900"):
|
||||
return advertised1900ButSmaller, nil
|
||||
case strings.Contains(url, "1500x1500"):
|
||||
return actualLargerDimensions, nil
|
||||
default:
|
||||
return nil, errors.New("unexpected URL")
|
||||
}
|
||||
requestedURL = url
|
||||
return []byte("provider-cover"), nil
|
||||
}
|
||||
|
||||
url := "https://cdn-images.dzcdn.net/images/cover/abc/1000x1000-000000-80-0-0.jpg"
|
||||
got, err := downloadCoverToMemory(url, true)
|
||||
got, err := downloadCoverToMemory(providerURL)
|
||||
if err != nil {
|
||||
t.Fatalf("download max cover: %v", err)
|
||||
t.Fatalf("download provider cover: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, actualLargerDimensions) {
|
||||
t.Fatal("expected decoded 15x15 candidate instead of larger-byte 12x12 candidate")
|
||||
if requestedURL != providerURL {
|
||||
t.Fatalf("requested URL = %q, want provider URL %q", requestedURL, providerURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBestCoverCandidateUsesByteSizeAsDimensionTiebreaker(t *testing.T) {
|
||||
orig := coverFetch
|
||||
defer func() { coverFetch = orig }()
|
||||
resetCoverCache()
|
||||
|
||||
base := testPNG(t, 10, 10)
|
||||
largerFile := append(append([]byte(nil), base...), bytes.Repeat([]byte{0}, 512)...)
|
||||
coverFetch = func(url string) ([]byte, error) {
|
||||
if strings.Contains(url, "large") {
|
||||
return largerFile, nil
|
||||
}
|
||||
return base, nil
|
||||
}
|
||||
|
||||
got, selected, width, height, err := fetchBestCoverCandidate(
|
||||
[]string{"https://covers.test/small", "https://covers.test/large"},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("select cover: %v", err)
|
||||
}
|
||||
if selected != "https://covers.test/large" || width != 10 || height != 10 {
|
||||
t.Fatalf("selected=%q dimensions=%dx%d", selected, width, height)
|
||||
}
|
||||
if !bytes.Equal(got, largerFile) {
|
||||
t.Fatal("expected larger file when decoded dimensions are equal")
|
||||
if string(got) != "provider-cover" {
|
||||
t.Fatalf("downloaded cover = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ type DownloadRequest struct {
|
||||
EmbedMetadata bool `json:"embed_metadata"`
|
||||
ArtistTagMode string `json:"artist_tag_mode,omitempty"`
|
||||
EmbedLyrics bool `json:"embed_lyrics"`
|
||||
EmbedMaxQualityCover bool `json:"embed_max_quality_cover"`
|
||||
EmbedReplayGain bool `json:"embed_replaygain,omitempty"`
|
||||
PostProcessingEnabled bool `json:"post_processing_enabled,omitempty"`
|
||||
TidalHighFormat string `json:"tidal_high_format,omitempty"`
|
||||
|
||||
@@ -527,12 +527,15 @@ func RewriteSplitArtistTagsExport(filePath, artist, albumArtist string) (string,
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func DownloadCoverToFile(coverURL string, outputPath string, maxQuality bool) error {
|
||||
// The final bool is retained for gomobile ABI compatibility with existing
|
||||
// native shells. Resolution selection is extension-owned and the value is
|
||||
// intentionally ignored.
|
||||
func DownloadCoverToFile(coverURL string, outputPath string, _ bool) error {
|
||||
if coverURL == "" {
|
||||
return fmt.Errorf("no cover URL provided")
|
||||
}
|
||||
|
||||
data, err := downloadCoverToMemory(coverURL, maxQuality)
|
||||
data, err := downloadCoverToMemory(coverURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download cover: %w", err)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ var fetchMusicBrainzAlbumArtistByISRC = FetchMusicBrainzAlbumArtistByISRC
|
||||
type reEnrichRequest struct {
|
||||
FilePath string `json:"file_path"`
|
||||
CoverURL string `json:"cover_url"`
|
||||
MaxQuality bool `json:"max_quality"`
|
||||
EmbedLyrics bool `json:"embed_lyrics"`
|
||||
LyricsMode string `json:"lyrics_mode,omitempty"`
|
||||
ArtistTagMode string `json:"artist_tag_mode,omitempty"`
|
||||
@@ -727,7 +726,7 @@ func ReEnrichFile(requestJSON string) (string, error) {
|
||||
var coverTempPath string
|
||||
var coverDataBytes []byte
|
||||
if req.CoverURL != "" && req.shouldUpdateTag("cover", "cover") {
|
||||
coverData, err := downloadCoverToMemory(req.CoverURL, req.MaxQuality)
|
||||
coverData, err := downloadCoverToMemory(req.CoverURL)
|
||||
if err != nil {
|
||||
GoLog("[ReEnrich] Failed to download cover: %v\n", err)
|
||||
} else {
|
||||
|
||||
@@ -146,7 +146,7 @@ func embedExtensionDownloadMetadata(resp DownloadResponse, req DownloadRequest,
|
||||
coverURL := firstNonEmptyTrimmed(resp.CoverURL, req.CoverURL)
|
||||
var coverData []byte
|
||||
if coverURL != "" {
|
||||
data, err := downloadCoverToMemory(coverURL, req.EmbedMaxQualityCover)
|
||||
data, err := downloadCoverToMemory(coverURL)
|
||||
if err != nil {
|
||||
GoLog("[DownloadWithExtensionFallback] Warning: failed to download cover for metadata embed: %v\n", err)
|
||||
} else if len(data) > 0 {
|
||||
|
||||
@@ -72,16 +72,7 @@ func TestExtensionHealthClassificationAndValidation(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCoverAndIDHSHelpers(t *testing.T) {
|
||||
if got := upgradeToMaxQuality("https://cdn-images.dzcdn.net/images/cover/abc/500x500-000000-80-0-0.jpg"); !strings.Contains(got, "1900x1900") {
|
||||
t.Fatalf("deezer cover = %q", got)
|
||||
}
|
||||
if got := upgradeToMaxQuality("https://resources.tidal.com/images/id/320x320.jpg"); !strings.Contains(got, "origin.jpg") {
|
||||
t.Fatalf("tidal cover = %q", got)
|
||||
}
|
||||
if got := upgradeToMaxQuality("https://static.qobuz.com/images/covers/ab/cd/foo_600.jpg"); !strings.Contains(got, "_max.jpg") {
|
||||
t.Fatalf("qobuz cover = %q", got)
|
||||
}
|
||||
if data, err := downloadCoverToMemory("", false); err == nil || data != nil {
|
||||
if data, err := downloadCoverToMemory(""); err == nil || data != nil {
|
||||
t.Fatalf("expected empty cover error")
|
||||
}
|
||||
|
||||
|
||||
@@ -564,8 +564,7 @@ import Gobackend
|
||||
let args = call.arguments as! [String: Any]
|
||||
let coverURL = args["cover_url"] as! String
|
||||
let outputPath = args["output_path"] as! String
|
||||
let maxQuality = args["max_quality"] as? Bool ?? true
|
||||
GobackendDownloadCoverToFile(coverURL, outputPath, maxQuality, &error)
|
||||
GobackendDownloadCoverToFile(coverURL, outputPath, false, &error)
|
||||
if let error = error { throw error }
|
||||
return "{\"success\":true}"
|
||||
|
||||
|
||||
@@ -382,18 +382,6 @@ abstract class AppLocalizations {
|
||||
/// **'Save synced lyrics alongside your downloaded tracks'**
|
||||
String get optionsEmbedLyricsSubtitle;
|
||||
|
||||
/// Download highest quality album art
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Max Quality Cover'**
|
||||
String get optionsMaxQualityCover;
|
||||
|
||||
/// Subtitle for max quality cover
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Download highest resolution cover art'**
|
||||
String get optionsMaxQualityCoverSubtitle;
|
||||
|
||||
/// Title for ReplayGain setting toggle
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -151,13 +151,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get optionsEmbedLyricsSubtitle =>
|
||||
'Speichere synchronisierte Liedtexte zusammen mit heruntergeladenen Titeln';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCover => 'Maximale Cover-Qualität';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitle =>
|
||||
'Cover in höchster Auflösung herunterladen';
|
||||
|
||||
@override
|
||||
String get optionsReplayGain => 'ReplayGain';
|
||||
|
||||
|
||||
@@ -148,13 +148,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get optionsEmbedLyricsSubtitle =>
|
||||
'Save synced lyrics alongside your downloaded tracks';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCover => 'Max Quality Cover';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitle =>
|
||||
'Download highest resolution cover art';
|
||||
|
||||
@override
|
||||
String get optionsReplayGain => 'ReplayGain';
|
||||
|
||||
|
||||
@@ -148,13 +148,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get optionsEmbedLyricsSubtitle =>
|
||||
'Embed synced lyrics into FLAC files';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCover => 'Max Quality Cover';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitle =>
|
||||
'Download highest resolution cover art';
|
||||
|
||||
@override
|
||||
String get optionsReplayGain => 'ReplayGain';
|
||||
|
||||
@@ -5090,13 +5083,6 @@ class AppLocalizationsEsEs extends AppLocalizationsEs {
|
||||
String get optionsEmbedLyricsSubtitle =>
|
||||
'Guarda las letras sincronizadas junto a las pistas descargadas';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCover => 'Máxima calidad de portada';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitle =>
|
||||
'Descargar la portada en la mayor resolución';
|
||||
|
||||
@override
|
||||
String get optionsReplayGain => 'ReplayGain';
|
||||
|
||||
|
||||
@@ -151,13 +151,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get optionsEmbedLyricsSubtitle =>
|
||||
'Enregistrez les paroles synchronisées avec vos morceaux téléchargés';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCover => 'Pochette de qualité supérieure';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitle =>
|
||||
'Télécharger la pochette en haute résolution';
|
||||
|
||||
@override
|
||||
String get optionsReplayGain => 'ReplayGain';
|
||||
|
||||
|
||||
@@ -151,13 +151,6 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
String get optionsEmbedLyricsSubtitle =>
|
||||
'Simpan lirik yang disinkronkan bersama dengan lagu yang Anda unduh';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCover => 'Cover Kualitas Maksimal';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitle =>
|
||||
'Unduh cover art resolusi tertinggi';
|
||||
|
||||
@override
|
||||
String get optionsReplayGain => 'ReplayGain';
|
||||
|
||||
|
||||
@@ -148,12 +148,6 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get optionsEmbedLyricsSubtitle =>
|
||||
'Save synced lyrics alongside your downloaded tracks';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCover => '最大品質のカバー';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitle => '最高解像度のカバーアートをダウンロード';
|
||||
|
||||
@override
|
||||
String get optionsReplayGain => 'ReplayGain';
|
||||
|
||||
|
||||
@@ -143,12 +143,6 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get optionsEmbedLyricsSubtitle => '다운로드된 트랙과 함께 동기화된 가사를 저장합니다';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCover => '고품질 표지 이미지';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitle => '최고 해상도의 표지 이미지를 다운로드';
|
||||
|
||||
@override
|
||||
String get optionsReplayGain => '리플레이게인';
|
||||
|
||||
|
||||
@@ -148,13 +148,6 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get optionsEmbedLyricsSubtitle =>
|
||||
'Embed synced lyrics into FLAC files';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCover => 'Max Quality Cover';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitle =>
|
||||
'Download highest resolution cover art';
|
||||
|
||||
@override
|
||||
String get optionsReplayGain => 'ReplayGain';
|
||||
|
||||
@@ -5089,13 +5082,6 @@ class AppLocalizationsPtPt extends AppLocalizationsPt {
|
||||
String get optionsEmbedLyricsSubtitle =>
|
||||
'Salve letras sincronizadas ao lado das suas faixas baixadas';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCover => 'Capa de Qualidade Máxima';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitle =>
|
||||
'Baixar capa do álbum com a mais alta resolução';
|
||||
|
||||
@override
|
||||
String get optionsReplayGain => 'ReplayGain';
|
||||
|
||||
|
||||
@@ -150,13 +150,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get optionsEmbedLyricsSubtitle =>
|
||||
'Сохранять синхронизированный текст песни рядом с загруженным треком';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCover => 'Максимальное качество обложки';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitle =>
|
||||
'Скачивать обложку в макс. разрешении';
|
||||
|
||||
@override
|
||||
String get optionsReplayGain => 'ReplayGain';
|
||||
|
||||
|
||||
@@ -151,13 +151,6 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
String get optionsEmbedLyricsSubtitle =>
|
||||
'İndirdiğiniz parçaların yanına senkronize edilmiş şarkı sözlerini kaydedin';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCover => 'En Yüksek Kapak Kalitesi';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitle =>
|
||||
'En yüksek kalitedeki albüm kapaklarını indir';
|
||||
|
||||
@override
|
||||
String get optionsReplayGain => 'ReplayGain';
|
||||
|
||||
|
||||
@@ -151,13 +151,6 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String get optionsEmbedLyricsSubtitle =>
|
||||
'Save synced lyrics alongside your downloaded tracks';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCover => 'Максимальна якість обкладинки';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitle =>
|
||||
'Завантажити обкладинку з найвищою роздільною здатністю';
|
||||
|
||||
@override
|
||||
String get optionsReplayGain => 'Нормалізація звуку';
|
||||
|
||||
|
||||
@@ -922,9 +922,6 @@
|
||||
"@audioAnalysisMono": {
|
||||
"description": "Audio channel layout label - mono"
|
||||
},
|
||||
"@optionsMaxQualityCoverSubtitle": {
|
||||
"description": "Subtitle for max quality cover"
|
||||
},
|
||||
"@audioAnalysisNyquist": {
|
||||
"description": "Nyquist frequency metric label"
|
||||
},
|
||||
@@ -3249,7 +3246,6 @@
|
||||
"description": "Tutorial extensions tip 2"
|
||||
},
|
||||
"libraryFolder": "Bibliotheksordner",
|
||||
"optionsMaxQualityCoverSubtitle": "Cover in höchster Auflösung herunterladen",
|
||||
"@metadataProviderPriorityTitle": {
|
||||
"description": "Metadata priority page title"
|
||||
},
|
||||
@@ -5476,9 +5472,6 @@
|
||||
"nowPlayingDetails": "Details",
|
||||
"collectionWishlistEmptyTitle": "Wunschliste ist leer",
|
||||
"downloadAppleQqMultiPersonEnabled": "Sängerlabel für Duette und Gruppentitel enthalten",
|
||||
"@optionsMaxQualityCover": {
|
||||
"description": "Download highest quality album art"
|
||||
},
|
||||
"@snackbarExtensionUpdatedVersion": {
|
||||
"description": "Snackbar after updating an extension from the repo tab",
|
||||
"placeholders": {
|
||||
@@ -5822,7 +5815,6 @@
|
||||
"downloadFrom": "Herunterladen von",
|
||||
"collectionWishlistEmptySubtitle": "Tippe auf das + bei den Titeln, um sie zum späteren Herunterladen zu speichern",
|
||||
"cueSplitSplitting": "CUE-Sheet wird geteilt... ({current}/{total})",
|
||||
"optionsMaxQualityCover": "Maximale Cover-Qualität",
|
||||
"trackOptionAddToWishlist": "Zur Wunschliste hinzufügen",
|
||||
"settingsBackup": "Backup & Restore",
|
||||
"aboutPaxsenixSubtitle": "Lyrics proxy for Musixmatch, Netease, Apple Music, QQ Music, Spotify, Deezer, YouTube, Kugou, and Genius",
|
||||
|
||||
@@ -182,14 +182,6 @@
|
||||
"@optionsEmbedLyricsSubtitle": {
|
||||
"description": "Subtitle for embed lyrics"
|
||||
},
|
||||
"optionsMaxQualityCover": "Max Quality Cover",
|
||||
"@optionsMaxQualityCover": {
|
||||
"description": "Download highest quality album art"
|
||||
},
|
||||
"optionsMaxQualityCoverSubtitle": "Download highest resolution cover art",
|
||||
"@optionsMaxQualityCoverSubtitle": {
|
||||
"description": "Subtitle for max quality cover"
|
||||
},
|
||||
"optionsReplayGain": "ReplayGain",
|
||||
"@optionsReplayGain": {
|
||||
"description": "Title for ReplayGain setting toggle"
|
||||
|
||||
@@ -150,14 +150,6 @@
|
||||
"@optionsEmbedLyricsSubtitle": {
|
||||
"description": "Subtitle for embed lyrics"
|
||||
},
|
||||
"optionsMaxQualityCover": "Max Quality Cover",
|
||||
"@optionsMaxQualityCover": {
|
||||
"description": "Download highest quality album art"
|
||||
},
|
||||
"optionsMaxQualityCoverSubtitle": "Download highest resolution cover art",
|
||||
"@optionsMaxQualityCoverSubtitle": {
|
||||
"description": "Subtitle for max quality cover"
|
||||
},
|
||||
"optionsExtensionStore": "Extension Repo",
|
||||
"@optionsExtensionStore": {
|
||||
"description": "Show/hide store tab"
|
||||
|
||||
@@ -182,14 +182,6 @@
|
||||
"@optionsEmbedLyricsSubtitle": {
|
||||
"description": "Subtitle for embed lyrics"
|
||||
},
|
||||
"optionsMaxQualityCover": "Máxima calidad de portada",
|
||||
"@optionsMaxQualityCover": {
|
||||
"description": "Download highest quality album art"
|
||||
},
|
||||
"optionsMaxQualityCoverSubtitle": "Descargar la portada en la mayor resolución",
|
||||
"@optionsMaxQualityCoverSubtitle": {
|
||||
"description": "Subtitle for max quality cover"
|
||||
},
|
||||
"optionsReplayGain": "ReplayGain",
|
||||
"@optionsReplayGain": {
|
||||
"description": "Title for ReplayGain setting toggle"
|
||||
|
||||
@@ -922,9 +922,6 @@
|
||||
"@audioAnalysisMono": {
|
||||
"description": "Audio channel layout label - mono"
|
||||
},
|
||||
"@optionsMaxQualityCoverSubtitle": {
|
||||
"description": "Subtitle for max quality cover"
|
||||
},
|
||||
"@audioAnalysisNyquist": {
|
||||
"description": "Nyquist frequency metric label"
|
||||
},
|
||||
@@ -3249,7 +3246,6 @@
|
||||
"description": "Tutorial extensions tip 2"
|
||||
},
|
||||
"libraryFolder": "Dossier de bibliothèque",
|
||||
"optionsMaxQualityCoverSubtitle": "Télécharger la pochette en haute résolution",
|
||||
"@metadataProviderPriorityTitle": {
|
||||
"description": "Metadata priority page title"
|
||||
},
|
||||
@@ -5476,9 +5472,6 @@
|
||||
"nowPlayingDetails": "Détails",
|
||||
"collectionWishlistEmptyTitle": "La liste de souhaits est vide",
|
||||
"downloadAppleQqMultiPersonEnabled": "Étiquettes d'intervenants incluses pour les duos et les morceaux en groupe",
|
||||
"@optionsMaxQualityCover": {
|
||||
"description": "Download highest quality album art"
|
||||
},
|
||||
"@snackbarExtensionUpdatedVersion": {
|
||||
"description": "Snackbar after updating an extension from the repo tab",
|
||||
"placeholders": {
|
||||
@@ -5822,7 +5815,6 @@
|
||||
"downloadFrom": "Télécharger depuis",
|
||||
"collectionWishlistEmptySubtitle": "Appuyez sur le signe « + » à côté des morceaux pour enregistrer ceux que vous souhaitez télécharger plus tard",
|
||||
"cueSplitSplitting": "Fractionnement de la liste CUE... ({current}/{total})",
|
||||
"optionsMaxQualityCover": "Pochette de qualité supérieure",
|
||||
"trackOptionAddToWishlist": "Ajouter à la liste de souhaits",
|
||||
"settingsBackup": "Sauvegarde & Restauration",
|
||||
"aboutPaxsenixSubtitle": "Proxy de paroles pour Musixmatch, Netease, Apple Music, QQ Music, Spotify, Deezer, YouTube, Kugou et Genius",
|
||||
|
||||
@@ -941,9 +941,6 @@
|
||||
"@audioAnalysisMono": {
|
||||
"description": "Audio channel layout label - mono"
|
||||
},
|
||||
"@optionsMaxQualityCoverSubtitle": {
|
||||
"description": "Subtitle for max quality cover"
|
||||
},
|
||||
"@audioAnalysisNyquist": {
|
||||
"description": "Nyquist frequency metric label"
|
||||
},
|
||||
@@ -3312,7 +3309,6 @@
|
||||
"description": "Tutorial extensions tip 2"
|
||||
},
|
||||
"libraryFolder": "Library Folder",
|
||||
"optionsMaxQualityCoverSubtitle": "Unduh cover art resolusi tertinggi",
|
||||
"@metadataProviderPriorityTitle": {
|
||||
"description": "Metadata priority page title"
|
||||
},
|
||||
@@ -5587,9 +5583,6 @@
|
||||
"nowPlayingDetails": "Details",
|
||||
"collectionWishlistEmptyTitle": "Wishlist is empty",
|
||||
"downloadAppleQqMultiPersonEnabled": "Speaker labels included for duets and group tracks",
|
||||
"@optionsMaxQualityCover": {
|
||||
"description": "Download highest quality album art"
|
||||
},
|
||||
"@snackbarExtensionUpdatedVersion": {
|
||||
"description": "Snackbar after updating an extension from the repo tab",
|
||||
"placeholders": {
|
||||
@@ -5933,7 +5926,6 @@
|
||||
"downloadFrom": "Unduh Dari",
|
||||
"collectionWishlistEmptySubtitle": "Tap + on tracks to save what you want to download later",
|
||||
"cueSplitSplitting": "Splitting CUE sheet... ({current}/{total})",
|
||||
"optionsMaxQualityCover": "Cover Kualitas Maksimal",
|
||||
"trackOptionAddToWishlist": "Add to Wishlist",
|
||||
"settingsBackup": "Backup & Restore",
|
||||
"aboutPaxsenixSubtitle": "Lyrics proxy for Musixmatch, Netease, Apple Music, QQ Music, Spotify, Deezer, YouTube, Kugou, and Genius",
|
||||
|
||||
@@ -922,9 +922,6 @@
|
||||
"@audioAnalysisMono": {
|
||||
"description": "Audio channel layout label - mono"
|
||||
},
|
||||
"@optionsMaxQualityCoverSubtitle": {
|
||||
"description": "Subtitle for max quality cover"
|
||||
},
|
||||
"@audioAnalysisNyquist": {
|
||||
"description": "Nyquist frequency metric label"
|
||||
},
|
||||
@@ -3249,7 +3246,6 @@
|
||||
"description": "Tutorial extensions tip 2"
|
||||
},
|
||||
"libraryFolder": "ライブラリのフォルダ",
|
||||
"optionsMaxQualityCoverSubtitle": "最高解像度のカバーアートをダウンロード",
|
||||
"@metadataProviderPriorityTitle": {
|
||||
"description": "Metadata priority page title"
|
||||
},
|
||||
@@ -5476,9 +5472,6 @@
|
||||
"nowPlayingDetails": "Details",
|
||||
"collectionWishlistEmptyTitle": "Wishlist is empty",
|
||||
"downloadAppleQqMultiPersonEnabled": "Speaker labels included for duets and group tracks",
|
||||
"@optionsMaxQualityCover": {
|
||||
"description": "Download highest quality album art"
|
||||
},
|
||||
"@snackbarExtensionUpdatedVersion": {
|
||||
"description": "Snackbar after updating an extension from the repo tab",
|
||||
"placeholders": {
|
||||
@@ -5822,7 +5815,6 @@
|
||||
"downloadFrom": "ダウンロード元",
|
||||
"collectionWishlistEmptySubtitle": "Tap + on tracks to save what you want to download later",
|
||||
"cueSplitSplitting": "Splitting CUE sheet... ({current}/{total})",
|
||||
"optionsMaxQualityCover": "最大品質のカバー",
|
||||
"trackOptionAddToWishlist": "ウィッシュリストに追加",
|
||||
"settingsBackup": "Backup & Restore",
|
||||
"aboutPaxsenixSubtitle": "Lyrics proxy for Musixmatch, Netease, Apple Music, QQ Music, Spotify, Deezer, YouTube, Kugou, and Genius",
|
||||
|
||||
@@ -182,14 +182,6 @@
|
||||
"@optionsEmbedLyricsSubtitle": {
|
||||
"description": "Subtitle for embed lyrics"
|
||||
},
|
||||
"optionsMaxQualityCover": "고품질 표지 이미지",
|
||||
"@optionsMaxQualityCover": {
|
||||
"description": "Download highest quality album art"
|
||||
},
|
||||
"optionsMaxQualityCoverSubtitle": "최고 해상도의 표지 이미지를 다운로드",
|
||||
"@optionsMaxQualityCoverSubtitle": {
|
||||
"description": "Subtitle for max quality cover"
|
||||
},
|
||||
"optionsReplayGain": "리플레이게인",
|
||||
"@optionsReplayGain": {
|
||||
"description": "Title for ReplayGain setting toggle"
|
||||
|
||||
@@ -150,14 +150,6 @@
|
||||
"@optionsEmbedLyricsSubtitle": {
|
||||
"description": "Subtitle for embed lyrics"
|
||||
},
|
||||
"optionsMaxQualityCover": "Max Quality Cover",
|
||||
"@optionsMaxQualityCover": {
|
||||
"description": "Download highest quality album art"
|
||||
},
|
||||
"optionsMaxQualityCoverSubtitle": "Download highest resolution cover art",
|
||||
"@optionsMaxQualityCoverSubtitle": {
|
||||
"description": "Subtitle for max quality cover"
|
||||
},
|
||||
"optionsExtensionStore": "Extension Repo",
|
||||
"@optionsExtensionStore": {
|
||||
"description": "Show/hide store tab"
|
||||
|
||||
@@ -922,9 +922,6 @@
|
||||
"@audioAnalysisMono": {
|
||||
"description": "Audio channel layout label - mono"
|
||||
},
|
||||
"@optionsMaxQualityCoverSubtitle": {
|
||||
"description": "Subtitle for max quality cover"
|
||||
},
|
||||
"@audioAnalysisNyquist": {
|
||||
"description": "Nyquist frequency metric label"
|
||||
},
|
||||
@@ -3249,7 +3246,6 @@
|
||||
"description": "Tutorial extensions tip 2"
|
||||
},
|
||||
"libraryFolder": "Library Folder",
|
||||
"optionsMaxQualityCoverSubtitle": "Baixar capa do álbum com a mais alta resolução",
|
||||
"@metadataProviderPriorityTitle": {
|
||||
"description": "Metadata priority page title"
|
||||
},
|
||||
@@ -5476,9 +5472,6 @@
|
||||
"nowPlayingDetails": "Details",
|
||||
"collectionWishlistEmptyTitle": "Wishlist is empty",
|
||||
"downloadAppleQqMultiPersonEnabled": "Speaker labels included for duets and group tracks",
|
||||
"@optionsMaxQualityCover": {
|
||||
"description": "Download highest quality album art"
|
||||
},
|
||||
"@snackbarExtensionUpdatedVersion": {
|
||||
"description": "Snackbar after updating an extension from the repo tab",
|
||||
"placeholders": {
|
||||
@@ -5822,7 +5815,6 @@
|
||||
"downloadFrom": "Baixar De",
|
||||
"collectionWishlistEmptySubtitle": "Tap + on tracks to save what you want to download later",
|
||||
"cueSplitSplitting": "Splitting CUE sheet... ({current}/{total})",
|
||||
"optionsMaxQualityCover": "Capa de Qualidade Máxima",
|
||||
"trackOptionAddToWishlist": "Add to Wishlist",
|
||||
"settingsBackup": "Backup & Restore",
|
||||
"aboutPaxsenixSubtitle": "Lyrics proxy for Musixmatch, Netease, Apple Music, QQ Music, Spotify, Deezer, YouTube, Kugou, and Genius",
|
||||
|
||||
@@ -922,9 +922,6 @@
|
||||
"@audioAnalysisMono": {
|
||||
"description": "Audio channel layout label - mono"
|
||||
},
|
||||
"@optionsMaxQualityCoverSubtitle": {
|
||||
"description": "Subtitle for max quality cover"
|
||||
},
|
||||
"@audioAnalysisNyquist": {
|
||||
"description": "Nyquist frequency metric label"
|
||||
},
|
||||
@@ -3249,7 +3246,6 @@
|
||||
"description": "Tutorial extensions tip 2"
|
||||
},
|
||||
"libraryFolder": "Папка библиотеки",
|
||||
"optionsMaxQualityCoverSubtitle": "Скачивать обложку в макс. разрешении",
|
||||
"@metadataProviderPriorityTitle": {
|
||||
"description": "Metadata priority page title"
|
||||
},
|
||||
@@ -5476,9 +5472,6 @@
|
||||
"nowPlayingDetails": "Details",
|
||||
"collectionWishlistEmptyTitle": "Список желаний пуст",
|
||||
"downloadAppleQqMultiPersonEnabled": "Speaker labels included for duets and group tracks",
|
||||
"@optionsMaxQualityCover": {
|
||||
"description": "Download highest quality album art"
|
||||
},
|
||||
"@snackbarExtensionUpdatedVersion": {
|
||||
"description": "Snackbar after updating an extension from the repo tab",
|
||||
"placeholders": {
|
||||
@@ -5822,7 +5815,6 @@
|
||||
"downloadFrom": "Скачивать из",
|
||||
"collectionWishlistEmptySubtitle": "Нажмите + на треках, чтобы сохранить то, что вы хотите скачать позже",
|
||||
"cueSplitSplitting": "Разделение CUE sheet... ({current}/{total})",
|
||||
"optionsMaxQualityCover": "Максимальное качество обложки",
|
||||
"trackOptionAddToWishlist": "Добавить в список желаний",
|
||||
"settingsBackup": "Backup & Restore",
|
||||
"aboutPaxsenixSubtitle": "Lyrics proxy for Musixmatch, Netease, Apple Music, QQ Music, Spotify, Deezer, YouTube, Kugou, and Genius",
|
||||
|
||||
@@ -922,9 +922,6 @@
|
||||
"@audioAnalysisMono": {
|
||||
"description": "Audio channel layout label - mono"
|
||||
},
|
||||
"@optionsMaxQualityCoverSubtitle": {
|
||||
"description": "Subtitle for max quality cover"
|
||||
},
|
||||
"@audioAnalysisNyquist": {
|
||||
"description": "Nyquist frequency metric label"
|
||||
},
|
||||
@@ -3249,7 +3246,6 @@
|
||||
"description": "Tutorial extensions tip 2"
|
||||
},
|
||||
"libraryFolder": "Kitaplık Klasörü",
|
||||
"optionsMaxQualityCoverSubtitle": "En yüksek kalitedeki albüm kapaklarını indir",
|
||||
"@metadataProviderPriorityTitle": {
|
||||
"description": "Metadata priority page title"
|
||||
},
|
||||
@@ -5476,9 +5472,6 @@
|
||||
"nowPlayingDetails": "Details",
|
||||
"collectionWishlistEmptyTitle": "İstek listesi boş",
|
||||
"downloadAppleQqMultiPersonEnabled": "Speaker labels included for duets and group tracks",
|
||||
"@optionsMaxQualityCover": {
|
||||
"description": "Download highest quality album art"
|
||||
},
|
||||
"@snackbarExtensionUpdatedVersion": {
|
||||
"description": "Snackbar after updating an extension from the repo tab",
|
||||
"placeholders": {
|
||||
@@ -5822,7 +5815,6 @@
|
||||
"downloadFrom": "İndirme Kaynağı",
|
||||
"collectionWishlistEmptySubtitle": "Daha sonra indirmek istediğiniz parçaları kaydetmek için parçaların üzerine + işaretiyle dokunun",
|
||||
"cueSplitSplitting": "CUE sayfası bölünüyor... ({current}/{total})",
|
||||
"optionsMaxQualityCover": "En Yüksek Kapak Kalitesi",
|
||||
"trackOptionAddToWishlist": "Add to Wishlist",
|
||||
"settingsBackup": "Backup & Restore",
|
||||
"aboutPaxsenixSubtitle": "Lyrics proxy for Musixmatch, Netease, Apple Music, QQ Music, Spotify, Deezer, YouTube, Kugou, and Genius",
|
||||
|
||||
@@ -922,9 +922,6 @@
|
||||
"@audioAnalysisMono": {
|
||||
"description": "Audio channel layout label - mono"
|
||||
},
|
||||
"@optionsMaxQualityCoverSubtitle": {
|
||||
"description": "Subtitle for max quality cover"
|
||||
},
|
||||
"@audioAnalysisNyquist": {
|
||||
"description": "Nyquist frequency metric label"
|
||||
},
|
||||
@@ -3249,7 +3246,6 @@
|
||||
"description": "Tutorial extensions tip 2"
|
||||
},
|
||||
"libraryFolder": "Папка бібліотеки",
|
||||
"optionsMaxQualityCoverSubtitle": "Завантажити обкладинку з найвищою роздільною здатністю",
|
||||
"@metadataProviderPriorityTitle": {
|
||||
"description": "Metadata priority page title"
|
||||
},
|
||||
@@ -5476,9 +5472,6 @@
|
||||
"nowPlayingDetails": "Details",
|
||||
"collectionWishlistEmptyTitle": "Список бажань порожній",
|
||||
"downloadAppleQqMultiPersonEnabled": "Speaker labels included for duets and group tracks",
|
||||
"@optionsMaxQualityCover": {
|
||||
"description": "Download highest quality album art"
|
||||
},
|
||||
"@snackbarExtensionUpdatedVersion": {
|
||||
"description": "Snackbar after updating an extension from the repo tab",
|
||||
"placeholders": {
|
||||
@@ -5822,7 +5815,6 @@
|
||||
"downloadFrom": "Завантажити з",
|
||||
"collectionWishlistEmptySubtitle": "Натисніть + на треках, щоб зберегти те, що ви хочете завантажити пізніше",
|
||||
"cueSplitSplitting": "Розділення аркуша CUE... ({current}/{total})",
|
||||
"optionsMaxQualityCover": "Максимальна якість обкладинки",
|
||||
"trackOptionAddToWishlist": "Додати до списку бажань",
|
||||
"settingsBackup": "Backup & Restore",
|
||||
"aboutPaxsenixSubtitle": "Lyrics proxy for Musixmatch, Netease, Apple Music, QQ Music, Spotify, Deezer, YouTube, Kugou, and Genius",
|
||||
|
||||
@@ -25,7 +25,6 @@ class AppSettings {
|
||||
final bool embedReplayGain;
|
||||
// Apply ReplayGain/R128 tags as volume normalization in the built-in player.
|
||||
final bool playbackNormalization;
|
||||
final bool maxQualityCover;
|
||||
final bool isFirstLaunch;
|
||||
final bool checkForUpdates;
|
||||
final String updateChannel;
|
||||
@@ -134,7 +133,6 @@ class AppSettings {
|
||||
this.embedLyrics = true,
|
||||
this.embedReplayGain = false,
|
||||
this.playbackNormalization = false,
|
||||
this.maxQualityCover = true,
|
||||
this.isFirstLaunch = true,
|
||||
this.checkForUpdates = true,
|
||||
this.updateChannel = 'stable',
|
||||
@@ -209,7 +207,6 @@ class AppSettings {
|
||||
bool? embedLyrics,
|
||||
bool? embedReplayGain,
|
||||
bool? playbackNormalization,
|
||||
bool? maxQualityCover,
|
||||
bool? isFirstLaunch,
|
||||
bool? checkForUpdates,
|
||||
String? updateChannel,
|
||||
@@ -288,7 +285,6 @@ class AppSettings {
|
||||
embedReplayGain: embedReplayGain ?? this.embedReplayGain,
|
||||
playbackNormalization:
|
||||
playbackNormalization ?? this.playbackNormalization,
|
||||
maxQualityCover: maxQualityCover ?? this.maxQualityCover,
|
||||
isFirstLaunch: isFirstLaunch ?? this.isFirstLaunch,
|
||||
checkForUpdates: checkForUpdates ?? this.checkForUpdates,
|
||||
updateChannel: updateChannel ?? this.updateChannel,
|
||||
|
||||
@@ -20,7 +20,6 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) => AppSettings(
|
||||
embedLyrics: json['embedLyrics'] as bool? ?? true,
|
||||
embedReplayGain: json['embedReplayGain'] as bool? ?? false,
|
||||
playbackNormalization: json['playbackNormalization'] as bool? ?? false,
|
||||
maxQualityCover: json['maxQualityCover'] as bool? ?? true,
|
||||
isFirstLaunch: json['isFirstLaunch'] as bool? ?? true,
|
||||
checkForUpdates: json['checkForUpdates'] as bool? ?? true,
|
||||
updateChannel: json['updateChannel'] as String? ?? 'stable',
|
||||
@@ -116,7 +115,6 @@ Map<String, dynamic> _$AppSettingsToJson(
|
||||
'embedLyrics': instance.embedLyrics,
|
||||
'embedReplayGain': instance.embedReplayGain,
|
||||
'playbackNormalization': instance.playbackNormalization,
|
||||
'maxQualityCover': instance.maxQualityCover,
|
||||
'isFirstLaunch': instance.isFirstLaunch,
|
||||
'checkForUpdates': instance.checkForUpdates,
|
||||
'updateChannel': instance.updateChannel,
|
||||
|
||||
@@ -705,7 +705,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
settings.embedMetadata &&
|
||||
settings.embedLyrics &&
|
||||
!_shouldSkipLyrics(extensionState, track.source, item.service),
|
||||
embedMaxQualityCover: settings.embedMetadata && settings.maxQualityCover,
|
||||
embedReplayGain: settings.embedReplayGain,
|
||||
postProcessingEnabled: postProcessingEnabled,
|
||||
tidalHighFormat: settings.tidalHighFormat,
|
||||
|
||||
@@ -657,10 +657,7 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
|
||||
// Started here, awaited only after the lyrics fetch below so the two
|
||||
// network round trips overlap. Errors are handled inside the fetch
|
||||
// (it resolves to null), never as an unhandled rejection.
|
||||
coverFuture = _sharedEmbedCover(
|
||||
coverUrl,
|
||||
maxQuality: settings.maxQualityCover,
|
||||
);
|
||||
coverFuture = _sharedEmbedCover(coverUrl);
|
||||
}
|
||||
|
||||
String? lrcContent;
|
||||
@@ -1021,33 +1018,24 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
|
||||
static const _embedCoverCacheMax = 8;
|
||||
|
||||
/// One cover fetch per URL, shared by every track in the batch.
|
||||
Future<String?> _sharedEmbedCover(
|
||||
String coverUrl, {
|
||||
required bool maxQuality,
|
||||
}) {
|
||||
final cacheKey = '${maxQuality ? 'max' : 'original'}|$coverUrl';
|
||||
final existing = _embedCoverCache.remove(cacheKey);
|
||||
Future<String?> _sharedEmbedCover(String coverUrl) {
|
||||
final existing = _embedCoverCache.remove(coverUrl);
|
||||
if (existing != null) {
|
||||
_embedCoverCache[cacheKey] = existing; // LRU touch
|
||||
_embedCoverCache[coverUrl] = existing; // LRU touch
|
||||
return existing;
|
||||
}
|
||||
final fetch = _downloadEmbedCover(coverUrl, maxQuality: maxQuality).then((
|
||||
path,
|
||||
) {
|
||||
if (path == null) _embedCoverCache.remove(cacheKey); // allow retry
|
||||
final fetch = _downloadEmbedCover(coverUrl).then((path) {
|
||||
if (path == null) _embedCoverCache.remove(coverUrl); // allow retry
|
||||
return path;
|
||||
});
|
||||
_embedCoverCache[cacheKey] = fetch;
|
||||
_embedCoverCache[coverUrl] = fetch;
|
||||
while (_embedCoverCache.length > _embedCoverCacheMax) {
|
||||
_evictEmbedCover(_embedCoverCache.keys.first);
|
||||
}
|
||||
return fetch;
|
||||
}
|
||||
|
||||
Future<String?> _downloadEmbedCover(
|
||||
String coverUrl, {
|
||||
required bool maxQuality,
|
||||
}) async {
|
||||
Future<String?> _downloadEmbedCover(String coverUrl) async {
|
||||
try {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final uniqueId =
|
||||
@@ -1058,7 +1046,6 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
|
||||
final result = await PlatformBridge.downloadCoverToFile(
|
||||
coverUrl,
|
||||
coverPath,
|
||||
maxQuality: maxQuality,
|
||||
);
|
||||
if (result['error'] != null) {
|
||||
_log.w('Failed to download cover: ${result['error']}');
|
||||
|
||||
@@ -576,11 +576,6 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
_syncLyricsSettingsToBackend();
|
||||
}
|
||||
|
||||
void setMaxQualityCover(bool enabled) {
|
||||
state = state.copyWith(maxQualityCover: enabled);
|
||||
_saveSettings();
|
||||
}
|
||||
|
||||
void setFirstLaunchComplete() {
|
||||
state = state.copyWith(isFirstLaunch: false);
|
||||
_saveSettings();
|
||||
|
||||
@@ -52,15 +52,6 @@ class MetadataSettingsPage extends ConsumerWidget {
|
||||
settings.artistTagMode,
|
||||
),
|
||||
),
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.image,
|
||||
title: context.l10n.optionsMaxQualityCover,
|
||||
subtitle: context.l10n.optionsMaxQualityCoverSubtitle,
|
||||
value: settings.maxQualityCover,
|
||||
onChanged: (v) => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setMaxQualityCover(v),
|
||||
),
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.graphic_eq,
|
||||
title: context.l10n.optionsReplayGain,
|
||||
|
||||
@@ -233,12 +233,6 @@ class SettingsSearchCatalog {
|
||||
title: l10n.optionsArtistTagMode,
|
||||
keywords: const ['artist separator', 'multiple artists'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.image,
|
||||
title: l10n.optionsMaxQualityCover,
|
||||
subtitle: l10n.optionsMaxQualityCoverSubtitle,
|
||||
keywords: const ['artwork', 'album art', 'cover size'],
|
||||
),
|
||||
SettingsSearchEntry(
|
||||
icon: Icons.graphic_eq,
|
||||
title: l10n.optionsReplayGain,
|
||||
|
||||
@@ -37,11 +37,11 @@ class _MetadataCandidateArtworkState extends State<_MetadataCandidateArtwork> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.coverUrl?.isNotEmpty == true) {
|
||||
unawaited(_loadMaxQualityCover());
|
||||
unawaited(_loadCover());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadMaxQualityCover() async {
|
||||
Future<void> _loadCover() async {
|
||||
final tempDir = await Directory.systemTemp.createTemp(
|
||||
'metadata_candidate_cover_',
|
||||
);
|
||||
@@ -50,7 +50,6 @@ class _MetadataCandidateArtworkState extends State<_MetadataCandidateArtwork> {
|
||||
final result = await PlatformBridge.downloadCoverToFile(
|
||||
widget.coverUrl!,
|
||||
path,
|
||||
maxQuality: true,
|
||||
);
|
||||
if (result['error'] != null || !await File(path).exists()) {
|
||||
await tempDir.delete(recursive: true);
|
||||
@@ -588,11 +587,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
|
||||
);
|
||||
final coverPath = '${tempDir.path}${Platform.pathSeparator}cover.jpg';
|
||||
try {
|
||||
await PlatformBridge.downloadCoverToFile(
|
||||
coverUrl,
|
||||
coverPath,
|
||||
maxQuality: true,
|
||||
);
|
||||
await PlatformBridge.downloadCoverToFile(coverUrl, coverPath);
|
||||
final file = File(coverPath);
|
||||
if (!await file.exists() || await file.length() <= 0) {
|
||||
await tempDir.delete(recursive: true);
|
||||
|
||||
@@ -685,14 +685,12 @@ extension _TrackMetadataLyricsAndSaving on _TrackMetadataScreenState {
|
||||
result = await PlatformBridge.downloadCoverToFile(
|
||||
_coverUrl!,
|
||||
tempOutput,
|
||||
maxQuality: true,
|
||||
);
|
||||
}
|
||||
} else if (_coverUrl != null && _coverUrl!.isNotEmpty) {
|
||||
result = await PlatformBridge.downloadCoverToFile(
|
||||
_coverUrl!,
|
||||
tempOutput,
|
||||
maxQuality: true,
|
||||
);
|
||||
} else {
|
||||
if (mounted) {
|
||||
@@ -781,14 +779,12 @@ extension _TrackMetadataLyricsAndSaving on _TrackMetadataScreenState {
|
||||
result = await PlatformBridge.downloadCoverToFile(
|
||||
_coverUrl!,
|
||||
outputPath,
|
||||
maxQuality: true,
|
||||
);
|
||||
}
|
||||
} else if (_coverUrl != null && _coverUrl!.isNotEmpty) {
|
||||
result = await PlatformBridge.downloadCoverToFile(
|
||||
_coverUrl!,
|
||||
outputPath,
|
||||
maxQuality: true,
|
||||
);
|
||||
} else {
|
||||
if (mounted) {
|
||||
@@ -996,7 +992,6 @@ extension _TrackMetadataLyricsAndSaving on _TrackMetadataScreenState {
|
||||
final request = <String, dynamic>{
|
||||
'file_path': cleanFilePath,
|
||||
'cover_url': _coverUrl ?? '',
|
||||
'max_quality': true,
|
||||
'embed_lyrics': settings.embedLyrics,
|
||||
'lyrics_mode': settings.lyricsMode,
|
||||
'artist_tag_mode': artistTagMode,
|
||||
|
||||
@@ -81,7 +81,6 @@ Map<String, dynamic> buildBatchReEnrichRequest({
|
||||
final request = <String, dynamic>{
|
||||
'file_path': item.filePath,
|
||||
'cover_url': '',
|
||||
'max_quality': true,
|
||||
'embed_lyrics': settings.embedLyrics,
|
||||
'lyrics_mode': settings.lyricsMode,
|
||||
'artist_tag_mode': settings.artistTagMode,
|
||||
|
||||
@@ -37,7 +37,6 @@ class CoverDownloadService {
|
||||
final download = await PlatformBridge.downloadCoverToFile(
|
||||
normalizedUrl,
|
||||
tempPath,
|
||||
maxQuality: true,
|
||||
);
|
||||
final error = download['error']?.toString().trim() ?? '';
|
||||
if (error.isNotEmpty) throw StateError(error);
|
||||
|
||||
@@ -18,7 +18,6 @@ class DownloadRequestPayload {
|
||||
final bool embedMetadata;
|
||||
final String artistTagMode;
|
||||
final bool embedLyrics;
|
||||
final bool embedMaxQualityCover;
|
||||
final bool embedReplayGain;
|
||||
final bool postProcessingEnabled;
|
||||
final String tidalHighFormat;
|
||||
@@ -80,7 +79,6 @@ class DownloadRequestPayload {
|
||||
this.embedMetadata = true,
|
||||
this.artistTagMode = 'joined',
|
||||
this.embedLyrics = true,
|
||||
this.embedMaxQualityCover = true,
|
||||
this.embedReplayGain = false,
|
||||
this.postProcessingEnabled = false,
|
||||
this.tidalHighFormat = 'mp3_320',
|
||||
@@ -144,7 +142,6 @@ class DownloadRequestPayload {
|
||||
'embed_metadata': embedMetadata,
|
||||
'artist_tag_mode': artistTagMode,
|
||||
'embed_lyrics': embedLyrics,
|
||||
'embed_max_quality_cover': embedMaxQualityCover,
|
||||
'embed_replaygain': embedReplayGain,
|
||||
'post_processing_enabled': postProcessingEnabled,
|
||||
'tidal_high_format': tidalHighFormat,
|
||||
@@ -212,7 +209,6 @@ class DownloadRequestPayload {
|
||||
embedMetadata: embedMetadata,
|
||||
artistTagMode: artistTagMode,
|
||||
embedLyrics: embedLyrics,
|
||||
embedMaxQualityCover: embedMaxQualityCover,
|
||||
embedReplayGain: embedReplayGain,
|
||||
postProcessingEnabled: postProcessingEnabled,
|
||||
tidalHighFormat: tidalHighFormat,
|
||||
|
||||
@@ -899,13 +899,11 @@ class PlatformBridge {
|
||||
|
||||
static Future<Map<String, dynamic>> downloadCoverToFile(
|
||||
String coverUrl,
|
||||
String outputPath, {
|
||||
bool maxQuality = true,
|
||||
}) {
|
||||
String outputPath,
|
||||
) {
|
||||
return _invokeMap('downloadCoverToFile', {
|
||||
'cover_url': coverUrl,
|
||||
'output_path': outputPath,
|
||||
'max_quality': maxQuality,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -864,7 +864,6 @@ void main() {
|
||||
embedMetadata: false,
|
||||
artistTagMode: artistTagModeSplitVorbis,
|
||||
embedLyrics: false,
|
||||
embedMaxQualityCover: false,
|
||||
embedReplayGain: true,
|
||||
postProcessingEnabled: true,
|
||||
tidalHighFormat: 'opus_256',
|
||||
@@ -924,7 +923,6 @@ void main() {
|
||||
'embed_metadata': false,
|
||||
'artist_tag_mode': artistTagModeSplitVorbis,
|
||||
'embed_lyrics': false,
|
||||
'embed_max_quality_cover': false,
|
||||
'embed_replaygain': true,
|
||||
'post_processing_enabled': true,
|
||||
'tidal_high_format': 'opus_256',
|
||||
|
||||
Reference in New Issue
Block a user