mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-02 09:08:35 +02:00
fix(download): reject mismatched provider tracks
This commit is contained in:
@@ -43,6 +43,23 @@ func attemptExtensionDownload(
|
||||
}
|
||||
})
|
||||
downloadSucceeded := err == nil && result != nil && result.Success
|
||||
if downloadSucceeded {
|
||||
resolved := resolvedTrackInfo{
|
||||
Title: result.Title,
|
||||
ArtistName: result.Artist,
|
||||
AlbumName: result.Album,
|
||||
ISRC: result.ISRC,
|
||||
Duration: result.DurationMS / 1000,
|
||||
SkipNameVerification: strings.EqualFold(strings.TrimSpace(req.Source), strings.TrimSpace(providerLabel)) ||
|
||||
ext.Manifest.HasCustomMatching(),
|
||||
}
|
||||
if !trackMatchesRequest(req, resolved, "Extension "+providerLabel) {
|
||||
discardRejectedExtensionOutput(result, outputPath)
|
||||
*lastErr = fmt.Errorf("provider %s returned a different track", providerLabel)
|
||||
*lastErrType = "not_found"
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
if req.ItemID != "" && downloadSucceeded {
|
||||
SetItemFinalizing(req.ItemID)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package gobackend
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -132,6 +134,39 @@ func normalizeDownloadResultExtension(candidates ...string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// discardRejectedExtensionOutput removes only a newly downloaded file inside
|
||||
// the host-selected output directory. Existing-library hits are never removed,
|
||||
// nor are paths outside that narrow directory.
|
||||
func discardRejectedExtensionOutput(result *ExtDownloadResult, requestedOutputPath string) {
|
||||
if result == nil || result.AlreadyExists {
|
||||
return
|
||||
}
|
||||
|
||||
resultPath := strings.TrimSpace(result.FilePath)
|
||||
requestedPath := strings.TrimSpace(requestedOutputPath)
|
||||
if resultPath == "" || requestedPath == "" ||
|
||||
strings.HasPrefix(resultPath, "content://") ||
|
||||
strings.HasPrefix(resultPath, "/proc/self/fd/") {
|
||||
return
|
||||
}
|
||||
|
||||
resultAbs, resultErr := filepath.Abs(resultPath)
|
||||
outputDirAbs, outputErr := filepath.Abs(filepath.Dir(requestedPath))
|
||||
if resultErr != nil || outputErr != nil {
|
||||
return
|
||||
}
|
||||
relative, err := filepath.Rel(outputDirAbs, resultAbs)
|
||||
if err != nil || relative == "." || relative == ".." ||
|
||||
strings.HasPrefix(relative, ".."+string(filepath.Separator)) ||
|
||||
filepath.IsAbs(relative) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Remove(resultAbs); err != nil && !os.IsNotExist(err) {
|
||||
GoLog("[DownloadWithExtensionFallback] Warning: failed to remove rejected provider output %q: %v\n", resultAbs, err)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeExtensionDownloadResult(result *ExtDownloadResult) (DownloadResult, bool) {
|
||||
if result == nil {
|
||||
return DownloadResult{}, false
|
||||
|
||||
@@ -485,6 +485,7 @@ func parseExtensionDownloadResultValue(vm *goja.Runtime, value goja.Value) ExtDo
|
||||
BitDepth: gojaObjectInt(obj, "bit_depth", "bitDepth"),
|
||||
SampleRate: gojaObjectInt(obj, "sample_rate", "sampleRate"),
|
||||
AudioCodec: gojaObjectString(obj, "audio_codec", "audioCodec", "codec"),
|
||||
DurationMS: gojaObjectInt(obj, "duration_ms", "durationMs"),
|
||||
ErrorMessage: gojaObjectString(obj, "error_message", "errorMessage", "error"),
|
||||
ErrorType: gojaObjectString(obj, "error_type", "errorType"),
|
||||
RetryAfterSeconds: gojaObjectInt(obj, "retry_after_seconds", "retryAfterSeconds"),
|
||||
|
||||
@@ -107,6 +107,7 @@ type ExtDownloadResult struct {
|
||||
BitDepth int `json:"bit_depth,omitempty"`
|
||||
SampleRate int `json:"sample_rate,omitempty"`
|
||||
AudioCodec string `json:"audio_codec,omitempty"`
|
||||
DurationMS int `json:"duration_ms,omitempty"`
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
ErrorType string `json:"error_type,omitempty"`
|
||||
RetryAfterSeconds int `json:"retry_after_seconds,omitempty"`
|
||||
|
||||
@@ -471,6 +471,45 @@ func TestMoveProviderToFrontPreservesExplicitSelection(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscardRejectedExtensionOutputStaysInsideRequestedDirectory(t *testing.T) {
|
||||
outputDir := t.TempDir()
|
||||
requestedPath := filepath.Join(outputDir, "Artist - Song.flac")
|
||||
rejectedPath := filepath.Join(outputDir, "Artist - Song.m4a")
|
||||
outsidePath := filepath.Join(t.TempDir(), "keep.flac")
|
||||
if err := os.WriteFile(rejectedPath, []byte("wrong audio"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(outsidePath, []byte("keep"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
discardRejectedExtensionOutput(&ExtDownloadResult{FilePath: rejectedPath}, requestedPath)
|
||||
if _, err := os.Stat(rejectedPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("rejected output was not removed: %v", err)
|
||||
}
|
||||
|
||||
discardRejectedExtensionOutput(&ExtDownloadResult{FilePath: outsidePath}, requestedPath)
|
||||
if _, err := os.Stat(outsidePath); err != nil {
|
||||
t.Fatalf("output outside requested directory was removed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscardRejectedExtensionOutputPreservesExistingLibraryHit(t *testing.T) {
|
||||
outputDir := t.TempDir()
|
||||
path := filepath.Join(outputDir, "existing.flac")
|
||||
if err := os.WriteFile(path, []byte("existing audio"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
discardRejectedExtensionOutput(&ExtDownloadResult{
|
||||
FilePath: path,
|
||||
AlreadyExists: true,
|
||||
}, filepath.Join(outputDir, "requested.flac"))
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Fatalf("existing library file was removed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildExtensionFallbackStoppedResponsePrefersAvailabilityReason(t *testing.T) {
|
||||
resp := buildExtensionFallbackStoppedResponse("soundcloud", &ExtAvailabilityResult{
|
||||
Reason: "direct SoundCloud track ID",
|
||||
@@ -685,6 +724,7 @@ func TestParseExtensionMetadataAndDownloadResults(t *testing.T) {
|
||||
alreadyExists: true,
|
||||
bitDepth: 24,
|
||||
sampleRate: 96000,
|
||||
durationMs: 181000,
|
||||
title: "Song",
|
||||
albumArtist: "Album Artist",
|
||||
lyricsLrc: "[00:00.00]Line",
|
||||
@@ -706,6 +746,7 @@ func TestParseExtensionMetadataAndDownloadResults(t *testing.T) {
|
||||
!download.AlreadyExists ||
|
||||
download.BitDepth != 24 ||
|
||||
download.SampleRate != 96000 ||
|
||||
download.DurationMS != 181000 ||
|
||||
download.AlbumArtist != "Album Artist" ||
|
||||
download.LyricsLRC != "[00:00.00]Line" ||
|
||||
download.Decryption == nil ||
|
||||
|
||||
@@ -363,6 +363,7 @@ func isLatinScript(value string) bool {
|
||||
type resolvedTrackInfo struct {
|
||||
Title string
|
||||
ArtistName string
|
||||
AlbumName string
|
||||
ISRC string
|
||||
Duration int
|
||||
SkipNameVerification bool
|
||||
@@ -387,6 +388,13 @@ func trackMatchesRequest(req DownloadRequest, resolved resolvedTrackInfo, logPre
|
||||
logPrefix, req.TrackName, resolved.Title)
|
||||
return false
|
||||
}
|
||||
|
||||
if req.AlbumName != "" && resolved.AlbumName != "" &&
|
||||
!titlesMatch(req.AlbumName, resolved.AlbumName) {
|
||||
GoLog("[%s] Verification failed: album mismatch — expected '%s', got '%s'\n",
|
||||
logPrefix, req.AlbumName, resolved.AlbumName)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
expectedDurationSec := req.DurationMS / 1000
|
||||
|
||||
@@ -55,6 +55,42 @@ func TestTrackMatchesRequest_SongLinkStillChecksDuration(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrackMatchesRequestRejectsDifferentAlbumWithoutExactISRC(t *testing.T) {
|
||||
req := DownloadRequest{
|
||||
TrackName: "Bewafa",
|
||||
ArtistName: "Imran Khan",
|
||||
AlbumName: "Unforgettable",
|
||||
ISRC: "GBUM70901234",
|
||||
}
|
||||
resolved := resolvedTrackInfo{
|
||||
Title: "Bewafa",
|
||||
ArtistName: "Imran Khan, Tarandeep Singh",
|
||||
AlbumName: "Bewafa",
|
||||
ISRC: "QZXYZ2600001",
|
||||
}
|
||||
|
||||
if trackMatchesRequest(req, resolved, "test") {
|
||||
t.Fatal("expected same-title cover from a different album to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrackMatchesRequestAcceptsDifferentEditionWithExactISRC(t *testing.T) {
|
||||
req := DownloadRequest{
|
||||
TrackName: "Song",
|
||||
AlbumName: "Original Album",
|
||||
ISRC: "USRC17607839",
|
||||
}
|
||||
resolved := resolvedTrackInfo{
|
||||
Title: "Song",
|
||||
AlbumName: "Deluxe Collection",
|
||||
ISRC: "usrc17607839",
|
||||
}
|
||||
|
||||
if !trackMatchesRequest(req, resolved, "test") {
|
||||
t.Fatal("expected an exact ISRC match to accept another release edition")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTitlesMatch_SeparatorVariants(t *testing.T) {
|
||||
if !titlesMatch("Doctor / Cops", "Doctor _ Cops") {
|
||||
t.Fatal("expected tidal titlesMatch to accept / vs _ variant")
|
||||
|
||||
Reference in New Issue
Block a user