From b700b6483cbd6ba5503522dde737def1647906c6 Mon Sep 17 00:00:00 2001 From: Mathis Maquenne <124215603+mathismqn@users.noreply.github.com> Date: Wed, 9 Oct 2024 01:18:26 +0200 Subject: [PATCH] feat(media): download and decrypt media --- cmd/download.go | 17 +++++++-- go.mod | 1 + go.sum | 2 ++ internal/crypto/crypto.go | 39 +++++++++++++++++++++ internal/deezer/media.go | 72 +++++++++++++++++++++++++++++++++++++++ internal/deezer/song.go | 10 ++++-- 6 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 internal/crypto/crypto.go diff --git a/cmd/download.go b/cmd/download.go index ae587a7..ba17703 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -27,11 +27,24 @@ var downloadCmd = &cobra.Command{ for _, song := range album.Songs.Data { media, err := song.GetMediaData() if err != nil { - fmt.Fprintf(os.Stderr, "could not fetch media data: %v\n", err) + fmt.Fprintf(os.Stderr, "could not get media data: %v\n", err) + if err.Error() == "invalid license token" { + os.Exit(1) + } continue } - fmt.Println(media) + songTitle := song.Title + if song.Version != "" { + songTitle = fmt.Sprintf("%s %s", song.Title, song.Version) + } + + filename := fmt.Sprintf("%s - %s.flac", song.ArtistName, songTitle) + err = media.Download(filename, song.ID) + if err != nil { + fmt.Fprintf(os.Stderr, "could not download song: %v\n", err) + continue + } } } }, diff --git a/go.mod b/go.mod index ce4af79..ef7282f 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,5 @@ require github.com/spf13/cobra v1.8.1 require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/crypto v0.28.0 ) diff --git a/go.sum b/go.sum index 912390a..86de154 100644 --- a/go.sum +++ b/go.sum @@ -6,5 +6,7 @@ github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go new file mode 100644 index 0000000..436c899 --- /dev/null +++ b/internal/crypto/crypto.go @@ -0,0 +1,39 @@ +package crypto + +import ( + "crypto/cipher" + "crypto/md5" + "fmt" + + "golang.org/x/crypto/blowfish" +) + +const ( + SecretKey = "" + IV = "" +) + +func GetBlowfishKey(songID string) []byte { + hash := md5.Sum([]byte(songID)) + hashHex := fmt.Sprintf("%x", hash) + + key := []byte(SecretKey) + for i := 0; i < len(hash); i++ { + key[i] = key[i] ^ hashHex[i] ^ hashHex[i+16] + } + + return key +} + +func DecryptBlowfish(data, key []byte) ([]byte, error) { + block, err := blowfish.NewCipher(key) + if err != nil { + return nil, err + } + + mode := cipher.NewCBCDecrypter(block, []byte(IV)) + decrypted := make([]byte, len(data)) + mode.CryptBlocks(decrypted, data) + + return decrypted, nil +} diff --git a/internal/deezer/media.go b/internal/deezer/media.go index e6309ee..8473b7a 100644 --- a/internal/deezer/media.go +++ b/internal/deezer/media.go @@ -1,5 +1,13 @@ package deezer +import ( + "fmt" + "net/http" + "os" + + "github.com/mathismqn/godeez/internal/crypto" +) + type Media struct { Errors []MediaError `json:"errors"` Data []struct { @@ -25,3 +33,67 @@ type Source struct { URL string `json:"url"` Provider string `json:"provider"` } + +const ChunkSize = 2048 + +func (m *Media) Download(filename, songID string) error { + url := m.Data[0].Media[0].Sources[0].URL + + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + + key := crypto.GetBlowfishKey(songID) + buffer := make([]byte, ChunkSize) + + for chunk := 0; ; chunk++ { + totalRead := 0 + for totalRead < ChunkSize { + n, err := resp.Body.Read(buffer[totalRead:]) + if err != nil { + if err.Error() == "EOF" { + break + } + return err + } + + if n > 0 { + totalRead += n + } + } + + if totalRead == 0 { + break + } + + if chunk%3 == 0 && totalRead == ChunkSize { + buffer, err = crypto.DecryptBlowfish(buffer, key) + if err != nil { + return err + } + } + + _, err = file.Write(buffer[:totalRead]) + if err != nil { + return err + } + + if totalRead < ChunkSize { + break + } + } + + return nil +} diff --git a/internal/deezer/song.go b/internal/deezer/song.go index 1c4801b..797f743 100644 --- a/internal/deezer/song.go +++ b/internal/deezer/song.go @@ -12,12 +12,14 @@ type Song struct { ID string `json:"SNG_ID"` ArtistName string `json:"ART_NAME"` Title string `json:"SNG_TITLE"` + Version string `json:"VERSION"` TrackToken string `json:"TRACK_TOKEN"` } +const LicenseToken = "" + func (s *Song) GetMediaData() (*Media, error) { - licenseToken := "" - reqBody := fmt.Sprintf(`{"license_token":"%s","media":[{"type":"FULL","formats":[{"cipher":"BF_CBC_STRIPE","format":"FLAC"}]}],"track_tokens":["%s"]}`, licenseToken, s.TrackToken) + reqBody := fmt.Sprintf(`{"license_token":"%s","media":[{"type":"FULL","formats":[{"cipher":"BF_CBC_STRIPE","format":"FLAC"}]}],"track_tokens":["%s"]}`, LicenseToken, s.TrackToken) resp, err := http.Post("https://media.deezer.com/v1/get_url", "application/json", bytes.NewBuffer([]byte(reqBody))) if err != nil { @@ -25,6 +27,10 @@ func (s *Song) GetMediaData() (*Media, error) { } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusBadRequest { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + body, _ := io.ReadAll(resp.Body) var media Media