mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
feat yandex desktop downloads
This commit is contained in:
@@ -3,8 +3,10 @@ package download
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -182,6 +184,120 @@ func (d *Downloader) FileDeezerEncrypted(ctx context.Context, sourceURL, outputP
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Downloader) FileYandexEncrypted(ctx context.Context, sourceURL, outputPath, key string) error {
|
||||
logDownloadStart(sourceURL, outputPath)
|
||||
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
keyBytes, err := hex.DecodeString(strings.TrimSpace(key))
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid yandex key: %w", err)
|
||||
}
|
||||
if len(keyBytes) != 16 {
|
||||
return fmt.Errorf("invalid yandex key length: %d", len(keyBytes))
|
||||
}
|
||||
block, err := aes.NewCipher(keyBytes)
|
||||
if 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)
|
||||
}
|
||||
out, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
success := false
|
||||
defer func() {
|
||||
_ = out.Close()
|
||||
if !success {
|
||||
_ = os.Remove(outputPath)
|
||||
}
|
||||
}()
|
||||
|
||||
var bar *mpb.Bar
|
||||
if d.ProgressEnabled() {
|
||||
d.barStarted.Store(1)
|
||||
desc := shortenName(filepath.Base(outputPath), 54)
|
||||
if resp.ContentLength > 0 {
|
||||
bar = d.progress.AddBar(
|
||||
resp.ContentLength,
|
||||
mpb.PrependDecorators(
|
||||
decor.Name(desc+" ", decor.WC{W: 56, C: decor.DSyncWidth | decor.DindentRight}),
|
||||
decor.Percentage(decor.WCSyncWidthR),
|
||||
),
|
||||
mpb.AppendDecorators(
|
||||
decor.CountersKibiByte("% .1f / % .1f", decor.WCSyncWidthR),
|
||||
decor.Name(" | ", decor.WCSyncWidth),
|
||||
decor.AverageSpeed(decor.SizeB1024(0), "% .1f", decor.WCSyncWidthR),
|
||||
decor.Name(" | ETA ", decor.WCSyncWidth),
|
||||
decor.AverageETA(decor.ET_STYLE_GO, decor.WCSyncWidthR),
|
||||
),
|
||||
mpb.BarRemoveOnComplete(),
|
||||
)
|
||||
} else {
|
||||
bar = d.progress.AddSpinner(
|
||||
0,
|
||||
mpb.PrependDecorators(
|
||||
decor.Name(desc+" ", decor.WC{W: 56, C: decor.DSyncWidth | decor.DindentRight}),
|
||||
),
|
||||
mpb.AppendDecorators(
|
||||
decor.CurrentKibiByte("% .1f", decor.WCSyncWidthR),
|
||||
decor.Name(" | ", decor.WCSyncWidth),
|
||||
decor.Elapsed(decor.ET_STYLE_GO, decor.WCSyncWidthR),
|
||||
),
|
||||
mpb.BarRemoveOnComplete(),
|
||||
)
|
||||
defer bar.SetTotal(-1, true)
|
||||
}
|
||||
defer func() {
|
||||
if !success && bar != nil {
|
||||
bar.Abort(true)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
stream := cipher.NewCTR(block, make([]byte, aes.BlockSize))
|
||||
reader := &cipher.StreamReader{S: stream, R: resp.Body}
|
||||
buf := make([]byte, downloadBufferSize)
|
||||
totalWritten := int64(0)
|
||||
for {
|
||||
n, readErr := reader.Read(buf)
|
||||
if n > 0 {
|
||||
if _, writeErr := out.Write(buf[:n]); writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
totalWritten += int64(n)
|
||||
if bar != nil {
|
||||
bar.IncrBy(n)
|
||||
}
|
||||
}
|
||||
if readErr != nil {
|
||||
if readErr == io.EOF {
|
||||
break
|
||||
}
|
||||
return readErr
|
||||
}
|
||||
}
|
||||
if resp.ContentLength > 0 && totalWritten != resp.ContentLength {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
if err = out.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
success = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Downloader) file(ctx context.Context, sourceURL, outputPath string, allowProgress bool, includeVideo bool) error {
|
||||
logDownloadStart(sourceURL, outputPath)
|
||||
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
|
||||
|
||||
@@ -2,8 +2,10 @@ package download
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"errors"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -110,6 +112,45 @@ func TestFileDeezerEncrypted(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileYandexEncrypted(t *testing.T) {
|
||||
plain := make([]byte, 8192+333)
|
||||
for i := range plain {
|
||||
plain[i] = byte((i * 11) % 251)
|
||||
}
|
||||
keyHex := "00112233445566778899aabbccddeeff"
|
||||
key, err := hex.DecodeString(keyHex)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeString() error = %v", err)
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
t.Fatalf("NewCipher() error = %v", err)
|
||||
}
|
||||
enc := make([]byte, len(plain))
|
||||
copy(enc, plain)
|
||||
stream := cipher.NewCTR(block, make([]byte, aes.BlockSize))
|
||||
stream.XORKeyStream(enc, enc)
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write(enc)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
d := NewWithOptions(true, false, 0)
|
||||
out := filepath.Join(t.TempDir(), "x", "a.m4a")
|
||||
if err = d.FileYandexEncrypted(context.Background(), ts.URL, out, keyHex); err != nil {
|
||||
t.Fatalf("FileYandexEncrypted() error = %v", err)
|
||||
}
|
||||
|
||||
got, err := os.ReadFile(out)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile() error = %v", err)
|
||||
}
|
||||
if string(got) != string(plain) {
|
||||
t.Fatalf("decrypted file mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloaderFileTruncatedResponseRemovesPartialFile(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Length", "10")
|
||||
@@ -160,6 +201,15 @@ func TestFileDeezerEncryptedBadStatus(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileYandexEncryptedBadKey(t *testing.T) {
|
||||
d := NewWithOptions(true, false, 0)
|
||||
out := filepath.Join(t.TempDir(), "x", "a.m4a")
|
||||
err := d.FileYandexEncrypted(context.Background(), "https://example.com/file", out, "abcd")
|
||||
if err == nil || !strings.Contains(err.Error(), "invalid yandex key length") {
|
||||
t.Fatalf("expected key length error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloaderFileContextCancellationRemovesPartialFile(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
|
||||
Reference in New Issue
Block a user