mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
implement native Deezer download/decrypt pipeline
Replace Deezer yt-dlp usage with native ARL session + media.get_url resolution, add BF_CBC_STRIPE decryption in downloader, and wire cipher-aware Deezer downloads through the main rip pipeline. Includes validation hardening and metadata/source-id improvements used by tagging flows.
This commit is contained in:
@@ -3,12 +3,15 @@ package download
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/cipher"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
@@ -17,6 +20,8 @@ import (
|
||||
"golang.org/x/term"
|
||||
|
||||
"streamrip-go/internal/netutil"
|
||||
|
||||
"golang.org/x/crypto/blowfish"
|
||||
)
|
||||
|
||||
type Downloader struct {
|
||||
@@ -56,6 +61,33 @@ func (d *Downloader) FileVideo(ctx context.Context, sourceURL, outputPath string
|
||||
return d.file(ctx, sourceURL, outputPath, true, true)
|
||||
}
|
||||
|
||||
func (d *Downloader) FileDeezerEncrypted(ctx context.Context, sourceURL, outputPath, trackID string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, sourceURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := d.http.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("download failed: status=%d", resp.StatusCode)
|
||||
}
|
||||
encrypted, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
plain, err := decryptDeezerBFCBCStripe(encrypted, trackID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(outputPath, plain, 0o644)
|
||||
}
|
||||
|
||||
func (d *Downloader) file(ctx context.Context, sourceURL, outputPath string, allowProgress bool, includeVideo bool) error {
|
||||
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
|
||||
return err
|
||||
@@ -204,3 +236,60 @@ func isManifestResponse(contentType string, peek []byte) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const deezerBFChunkSize = 2048
|
||||
|
||||
var deezerBFIV = []byte{0, 1, 2, 3, 4, 5, 6, 7}
|
||||
|
||||
func decryptDeezerBFCBCStripe(in []byte, trackID string) ([]byte, error) {
|
||||
block, err := blowfish.NewCipher(deriveDeezerBlowfishKey(trackID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]byte, len(in))
|
||||
for i := 0; i*deezerBFChunkSize < len(in); i++ {
|
||||
start := i * deezerBFChunkSize
|
||||
end := start + deezerBFChunkSize
|
||||
if end > len(in) {
|
||||
end = len(in)
|
||||
}
|
||||
chunk := in[start:end]
|
||||
if i%3 == 0 && len(chunk) == deezerBFChunkSize {
|
||||
dec := make([]byte, len(chunk))
|
||||
mode := cipher.NewCBCDecrypter(block, deezerBFIV)
|
||||
mode.CryptBlocks(dec, chunk)
|
||||
copy(out[start:end], dec)
|
||||
} else {
|
||||
copy(out[start:end], chunk)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func deriveDeezerBlowfishKey(trackID string) []byte {
|
||||
sum := md5.Sum([]byte(trackID))
|
||||
md5Hex := fmt.Sprintf("%x", sum)
|
||||
secret := "g4el58wc0zvf9na1"
|
||||
key := make([]byte, 16)
|
||||
for i := 0; i < 16; i++ {
|
||||
key[i] = md5Hex[i] ^ md5Hex[i+16] ^ secret[i]
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func normalizeDeezerTrackID(raw string) string {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
if _, err := strconv.Atoi(trimmed); err == nil {
|
||||
return trimmed
|
||||
}
|
||||
parts := strings.Split(strings.Trim(trimmed, "/"), "/")
|
||||
for i := len(parts) - 1; i >= 0; i-- {
|
||||
if _, err := strconv.Atoi(parts[i]); err == nil {
|
||||
return parts[i]
|
||||
}
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@ package download
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/cipher"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/blowfish"
|
||||
)
|
||||
|
||||
func TestDownloaderHasNoClientTimeout(t *testing.T) {
|
||||
@@ -51,3 +54,33 @@ func TestManifestDetection(t *testing.T) {
|
||||
t.Fatalf("did not expect flac to be manifest")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeDeezerTrackID(t *testing.T) {
|
||||
if got := normalizeDeezerTrackID("https://www.deezer.com/track/3135556"); got != "3135556" {
|
||||
t.Fatalf("normalize track id = %q, want 3135556", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecryptDeezerBFCBCStripe(t *testing.T) {
|
||||
trackID := "3135556"
|
||||
plain := make([]byte, deezerBFChunkSize*2)
|
||||
for i := range plain {
|
||||
plain[i] = byte(i % 251)
|
||||
}
|
||||
enc := make([]byte, len(plain))
|
||||
copy(enc, plain)
|
||||
block, err := blowfish.NewCipher(deriveDeezerBlowfishKey(trackID))
|
||||
if err != nil {
|
||||
t.Fatalf("cipher error: %v", err)
|
||||
}
|
||||
cbc := cipher.NewCBCEncrypter(block, deezerBFIV)
|
||||
cbc.CryptBlocks(enc[:deezerBFChunkSize], enc[:deezerBFChunkSize])
|
||||
|
||||
dec, err := decryptDeezerBFCBCStripe(enc, trackID)
|
||||
if err != nil {
|
||||
t.Fatalf("decrypt error: %v", err)
|
||||
}
|
||||
if len(dec) != len(plain) || string(dec) != string(plain) {
|
||||
t.Fatalf("decrypted data mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user