@@ -11,6 +11,8 @@ import (
1111 "os"
1212 "strings"
1313 "testing"
14+
15+ "github.com/bogem/id3v2/v2"
1416)
1517
1618// mockTransport allows us to mock HTTP responses
@@ -349,3 +351,168 @@ http://mock/segment.ts
349351 t .Errorf ("file content missing decrypted audio. Got size %d" , len (content ))
350352 }
351353}
354+
355+ func TestDownload_Artwork (t * testing.T ) {
356+ // Setup encryption for mock segment (same as TestDownload)
357+ key := []byte ("1234567890123456" )
358+ iv := make ([]byte , 16 ) // zero IV
359+ playlistContent := []byte ("audio content" )
360+ padding := aes .BlockSize - (len (playlistContent ) % aes .BlockSize )
361+ padded := append (playlistContent , bytes .Repeat ([]byte {byte (padding )}, padding )... )
362+ ciphertext := make ([]byte , len (padded ))
363+ block , _ := aes .NewCipher (key )
364+ mode := cipher .NewCBCEncrypter (block , iv )
365+ mode .CryptBlocks (ciphertext , padded )
366+
367+ // M3U8 content
368+ m3u8Content := `#EXTM3U
369+ #EXT-X-VERSION:3
370+ #EXT-X-TARGETDURATION:10
371+ #EXT-X-KEY:METHOD=AES-128,URI="http://mock/key",IV=0x00000000000000000000000000000000
372+ #EXTINF:10.0,
373+ http://mock/segment.ts
374+ #EXT-X-ENDLIST`
375+
376+ tests := []struct {
377+ name string
378+ artworkURL string
379+ expectArtwork bool
380+ mockTransportFunc func (req * http.Request ) (* http.Response , error )
381+ }{
382+ {
383+ name : "Artwork Success" ,
384+ artworkURL : "http://mock/artwork-large.jpg" ,
385+ expectArtwork : true ,
386+ mockTransportFunc : func (req * http.Request ) (* http.Response , error ) {
387+ u := req .URL .String ()
388+ if u == "http://mock/artwork-t500x500.jpg" { // Replaced URL
389+ return & http.Response {
390+ StatusCode : 200 ,
391+ Body : io .NopCloser (bytes .NewReader ([]byte ("fake image data" ))),
392+ }, nil
393+ }
394+ if strings .Contains (u , "artwork" ) {
395+ return & http.Response {StatusCode : 404 , Body : io .NopCloser (strings .NewReader ("Not Found" ))}, nil
396+ }
397+ // Default handlers
398+ if strings .Contains (u , "/stream/hls" ) {
399+ return & http.Response {StatusCode : 200 , Body : io .NopCloser (strings .NewReader (`{"url": "http://mock/playlist.m3u8"}` ))}, nil
400+ }
401+ if u == "http://mock/playlist.m3u8" {
402+ return & http.Response {StatusCode : 200 , Body : io .NopCloser (strings .NewReader (m3u8Content ))}, nil
403+ }
404+ if u == "http://mock/key" {
405+ return & http.Response {StatusCode : 200 , Body : io .NopCloser (bytes .NewReader (key ))}, nil
406+ }
407+ if u == "http://mock/segment.ts" {
408+ return & http.Response {StatusCode : 200 , Body : io .NopCloser (bytes .NewReader (ciphertext ))}, nil
409+ }
410+ return & http.Response {StatusCode : 404 , Body : io .NopCloser (strings .NewReader ("Not Found: " + u ))}, nil
411+ },
412+ },
413+ {
414+ name : "Artwork Failure Silent" ,
415+ artworkURL : "http://mock/artwork-large.jpg" ,
416+ expectArtwork : false , // Currently fails silently, so we expect NO artwork
417+ mockTransportFunc : func (req * http.Request ) (* http.Response , error ) {
418+ u := req .URL .String ()
419+ if strings .Contains (u , "artwork" ) {
420+ return & http.Response {StatusCode : 404 , Body : io .NopCloser (strings .NewReader ("Not Found" ))}, nil
421+ }
422+ // Default handlers
423+ if strings .Contains (u , "/stream/hls" ) {
424+ return & http.Response {StatusCode : 200 , Body : io .NopCloser (strings .NewReader (`{"url": "http://mock/playlist.m3u8"}` ))}, nil
425+ }
426+ if u == "http://mock/playlist.m3u8" {
427+ return & http.Response {StatusCode : 200 , Body : io .NopCloser (strings .NewReader (m3u8Content ))}, nil
428+ }
429+ if u == "http://mock/key" {
430+ return & http.Response {StatusCode : 200 , Body : io .NopCloser (bytes .NewReader (key ))}, nil
431+ }
432+ if u == "http://mock/segment.ts" {
433+ return & http.Response {StatusCode : 200 , Body : io .NopCloser (bytes .NewReader (ciphertext ))}, nil
434+ }
435+ return & http.Response {StatusCode : 404 , Body : io .NopCloser (strings .NewReader ("Not Found: " + u ))}, nil
436+ },
437+ },
438+ {
439+ name : "Fallback when Replacement Fails" ,
440+ artworkURL : "http://mock/artwork-large.jpg" ,
441+ expectArtwork : true ,
442+ mockTransportFunc : func (req * http.Request ) (* http.Response , error ) {
443+ u := req .URL .String ()
444+ // The replaced URL will be tried first
445+ if u == "http://mock/artwork-t500x500.jpg" {
446+ return & http.Response {StatusCode : 404 , Body : io .NopCloser (strings .NewReader ("Not Found" ))}, nil
447+ }
448+ // The original URL SHOULD be tried as fallback and succeed
449+ if u == "http://mock/artwork-large.jpg" {
450+ return & http.Response {
451+ StatusCode : 200 ,
452+ Body : io .NopCloser (bytes .NewReader ([]byte ("fake image data" ))),
453+ }, nil
454+ }
455+
456+ // Default handlers
457+ if strings .Contains (u , "/stream/hls" ) {
458+ return & http.Response {StatusCode : 200 , Body : io .NopCloser (strings .NewReader (`{"url": "http://mock/playlist.m3u8"}` ))}, nil
459+ }
460+ if u == "http://mock/playlist.m3u8" {
461+ return & http.Response {StatusCode : 200 , Body : io .NopCloser (strings .NewReader (m3u8Content ))}, nil
462+ }
463+ if u == "http://mock/key" {
464+ return & http.Response {StatusCode : 200 , Body : io .NopCloser (bytes .NewReader (key ))}, nil
465+ }
466+ if u == "http://mock/segment.ts" {
467+ return & http.Response {StatusCode : 200 , Body : io .NopCloser (bytes .NewReader (ciphertext ))}, nil
468+ }
469+ return & http.Response {StatusCode : 404 , Body : io .NopCloser (strings .NewReader ("Not Found: " + u ))}, nil
470+ },
471+ },
472+ }
473+
474+ for _ , tc := range tests {
475+ t .Run (tc .name , func (t * testing.T ) {
476+ client := & Client {
477+ clientID : "test-client" ,
478+ httpClient : & http.Client {
479+ Transport : & mockTransport {RoundTripFunc : tc .mockTransportFunc },
480+ },
481+ }
482+
483+ track := & Track {
484+ ID : 123 ,
485+ Title : "MySong" ,
486+ Artist : "MyArtist" ,
487+ HLSURL : "https://api-v2.soundcloud.com/media/soundcloud:tracks:123/token/stream/hls" ,
488+ TrackAuthorization : "auth" ,
489+ ArtworkURL : tc .artworkURL ,
490+ }
491+
492+ outDir := t .TempDir ()
493+ outPath , err := client .Download (context .Background (), track , outDir , nil )
494+ if err != nil {
495+ t .Fatalf ("Download() error = %v" , err )
496+ }
497+
498+ // Read tags
499+ tag , err := id3v2 .Open (outPath , id3v2.Options {Parse : true })
500+ if err != nil {
501+ t .Fatalf ("Error opening tag: %v" , err )
502+ }
503+ defer tag .Close ()
504+
505+ hasPicture := false
506+ if frames := tag .GetFrames (tag .CommonID ("Attached picture" )); len (frames ) > 0 {
507+ hasPicture = true
508+ }
509+
510+ if tc .expectArtwork && ! hasPicture {
511+ t .Error ("Expected artwork to be attached, but it was not" )
512+ }
513+ if ! tc .expectArtwork && hasPicture {
514+ t .Error ("Expected no artwork, but it was found" )
515+ }
516+ })
517+ }
518+ }
0 commit comments