fix(download): reject mismatched provider tracks

This commit is contained in:
zarzet
2026-07-28 14:15:44 +07:00
parent b4f239c692
commit 385afe290a
7 changed files with 139 additions and 0 deletions
+17
View File
@@ -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)
}
+35
View File
@@ -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
+1
View File
@@ -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"),
+1
View File
@@ -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"`
+41
View File
@@ -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 ||
+8
View File
@@ -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
+36
View File
@@ -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")