├── config.json ├── go.mod ├── go.sum ├── README.md ├── structs.go └── main.go /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "sonySelectId": "", 3 | "outPath": "Sony Kuke downloads", 4 | "trackTemplate": "{{.trackPad}}. {{.title}}", 5 | "omitArtists": false, 6 | "keepCover": true 7 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/alexflint/go-arg v1.4.3 7 | github.com/go-flac/flacpicture v0.2.0 8 | github.com/go-flac/flacvorbis v0.1.0 9 | github.com/go-flac/go-flac v0.3.1 10 | ) 11 | 12 | require github.com/alexflint/go-scalar v1.1.0 // indirect 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alexflint/go-arg v1.4.3 h1:9rwwEBpMXfKQKceuZfYcwuc/7YY7tWJbFsgG5cAU/uo= 2 | github.com/alexflint/go-arg v1.4.3/go.mod h1:3PZ/wp/8HuqRZMUUgu7I+e1qcpUbvmS258mRXkFH4IA= 3 | github.com/alexflint/go-scalar v1.1.0 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4PnltbhM= 4 | github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/ddliu/go-httpclient v0.5.1 h1:ys4KozrhBaGdI1yuWIFwNNILqhnMU9ozTvRNfCTorvs= 9 | github.com/ddliu/go-httpclient v0.5.1/go.mod h1:8QVbjq00YK2f2MQyiKuWMdaKOFRcoD9VuubkNCNOuZo= 10 | github.com/go-flac/flacpicture v0.2.0 h1:rS/ZOR/ZxlEwMf3yOPFcTAmGyoV6rDtcYdd+6CwWQAw= 11 | github.com/go-flac/flacpicture v0.2.0/go.mod h1:M4a1J0v6B5NHsck4GA1yZg0vFQzETVPd3kuj6Ow+q9o= 12 | github.com/go-flac/flacvorbis v0.1.0 h1:xStJfPrZ/IoA2oBUEwgrlaSf+Opo6/YuQfkqVhkP0cM= 13 | github.com/go-flac/flacvorbis v0.1.0/go.mod h1:70N9vVkQ4Jew0oBWkwqDMIE21h7pMUtQJpnMD0js6XY= 14 | github.com/go-flac/go-flac v0.3.1 h1:BWA7HdO67S4ZLWSVHCxsDHuedFFu5RiV/wmuhvO6Hxo= 15 | github.com/go-flac/go-flac v0.3.1/go.mod h1:jG9IumOfAXr+7J40x0AiQIbJzXf9Y7+Zs/2CNWe4LMk= 16 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 17 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 22 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 23 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 24 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 25 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 26 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sony-Kuke-Downloader 2 | [Sony Kuke](https://dereferer.me/?https://hi-resmusic.sonyselect.kuke.com/) downloader written in Go. This one was a pain, enjoy. 3 | ![](https://i.imgur.com/jF6FXQu.png) 4 | [Windows, Linux, macOS and Android binaries](https://github.com/Sorrow446/Sony-Kuke-Downloader/releases) 5 | 6 | # Setup 7 | Active subscription required. Input Sony Select user ID into config file. 8 | ### Sony Select ID 9 | 10 | ~~1. Login on browser.~~ 11 | 12 | ~~2. Right click, view page source.~~ 13 | 14 | ~~3. Ctrl+f, search for `sonyselectid`.~~ 15 | 16 | Sniff Windows app, see post data of any of the API posts or PM. 17 | `https://www.reddit.com/user/Sorrow446/` 18 | `Sorrow#6826` 19 | 20 | Configure any other options if needed. 21 | |Option|Info| 22 | | --- | --- | 23 | |sonySelectId|Sony Select user ID. 24 | |outPath|Where to download to. Path will be made if it doesn't already exist. 25 | |trackTemplate|Track filename naming template. Vars: album, albumArtist, artist, title, track, trackPad, trackTotal, year. 26 | |omitArtists|Omit album artists from album folder names. 27 | |keepCover|Don't delete covers from album folders. 28 | 29 | **FFmpeg is needed to concat FLAC segments.** 30 | [Windows (gpl)](https://github.com/BtbN/FFmpeg-Builds/releases) 31 | Linux: `sudo apt install ffmpeg` 32 | Termux `pkg install ffmpeg` 33 | 34 | # Usage 35 | Args take priority over the same config file options. 36 | 37 | Download two albums: 38 | `sk_dl_x64.exe https://hi-resmusic.sonyselect.kuke.com/page/album.html?id=10628 https://hi-resmusic.sonyselect.kuke.com/page/album.html?id=10896` 39 | 40 | Download a single album and from two text files: 41 | `sk_dl_x64.exe https://hi-resmusic.sonyselect.kuke.com/page/album.html?id=10628 G:\1.txt G:\2.txt` 42 | 43 | ``` 44 | _____ _____ _ ____ _ _ 45 | | __|___ ___ _ _ | | |_ _| |_ ___ | \ ___ _ _ _ ___| |___ ___ _| |___ ___ 46 | |__ | . | | | | | -| | | '_| -_| | | | . | | | | | | . | .'| . | -_| _| 47 | |_____|___|_|_|_ | |__|__|___|_,_|___| |____/|___|_____|_|_|_|___|__,|___|___|_| 48 | |___| 49 | 50 | Usage: sk_dl_x64.exe [--outpath OUTPATH] URLS [URLS ...] 51 | 52 | Positional arguments: 53 | URLS 54 | 55 | Options: 56 | --outpath OUTPATH, -o OUTPATH 57 | Where to download to. Path will be made if it doesn't already exist. 58 | --help, -h display this help and exit 59 | ``` 60 | 61 | # Disclaimer 62 | - I will not be responsible for how you use Sony Kuke Downloader. 63 | - Sony brand and name is the registered trademark of its respective owner. 64 | - Sony Kuke Downloader has no partnership, sponsorship or endorsement with Sony. 65 | -------------------------------------------------------------------------------- /structs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Transport struct{} 4 | 5 | type Config struct { 6 | SonySelectID string 7 | OutPath string 8 | TrackTemplate string 9 | OmitArtists bool 10 | KeepCover bool 11 | Urls []string 12 | } 13 | 14 | type Args struct { 15 | Urls []string `arg:"positional, required"` 16 | OutPath string `arg:"-o" help:"Where to download to. Path will be made if it doesn't already exist."` 17 | } 18 | 19 | type PostData struct { 20 | Content struct { 21 | AlbumID string `json:"albumId"` 22 | MusicId int `json:"musicId"` 23 | IndexID int `json:"indexId"` 24 | } `json:"content"` 25 | Header struct { 26 | AccessKey string `json:"accessKey"` 27 | ContentEncryption bool `json:"contentEncryption"` 28 | Imei string `json:"imei"` 29 | Model string `json:"model"` 30 | Nonce string `json:"nonce"` 31 | SignEncryption bool `json:"signEncryption"` 32 | SonySelectID string `json:"sonySelectId"` 33 | Timestamp int64 `json:"timestamp"` 34 | Version string `json:"version"` 35 | } `json:"header"` 36 | } 37 | 38 | type AlbumMetaPost struct { 39 | Content struct { 40 | AlbumID string `json:"albumId"` 41 | } `json:"content"` 42 | Header struct { 43 | AccessKey string `json:"accessKey"` 44 | ContentEncryption bool `json:"contentEncryption"` 45 | Imei string `json:"imei"` 46 | Model string `json:"model"` 47 | Nonce string `json:"nonce"` 48 | SignEncryption bool `json:"signEncryption"` 49 | SonySelectID string `json:"sonySelectId"` 50 | Timestamp int64 `json:"timestamp"` 51 | Version string `json:"version"` 52 | } `json:"header"` 53 | } 54 | 55 | type TrackMetaPost struct { 56 | Content struct { 57 | MusicId int `json:"musicId"` 58 | } `json:"content"` 59 | Header struct { 60 | AccessKey string `json:"accessKey"` 61 | ContentEncryption bool `json:"contentEncryption"` 62 | Imei string `json:"imei"` 63 | Model string `json:"model"` 64 | Nonce string `json:"nonce"` 65 | SignEncryption bool `json:"signEncryption"` 66 | SonySelectID string `json:"sonySelectId"` 67 | Timestamp int64 `json:"timestamp"` 68 | Version string `json:"version"` 69 | } `json:"header"` 70 | } 71 | 72 | type FileMetaPost struct { 73 | Content struct { 74 | IndexID int `json:"indexId"` 75 | } `json:"content"` 76 | Header struct { 77 | AccessKey string `json:"accessKey"` 78 | ContentEncryption bool `json:"contentEncryption"` 79 | Imei string `json:"imei"` 80 | Model string `json:"model"` 81 | Nonce string `json:"nonce"` 82 | SignEncryption bool `json:"signEncryption"` 83 | SonySelectID string `json:"sonySelectId"` 84 | Timestamp int64 `json:"timestamp"` 85 | Version string `json:"version"` 86 | } `json:"header"` 87 | } 88 | 89 | type AlbumMeta struct { 90 | Result struct { 91 | Code string `json:"code"` 92 | Msg string `json:"msg"` 93 | } `json:"result"` 94 | Content struct { 95 | ReleaseTime string `json:"releaseTime"` 96 | Singer string `json:"singer"` 97 | Artist string `json:"artist"` 98 | IsHiRes bool `json:"isHiRes"` 99 | DiscountPrice float64 `json:"discountPrice"` 100 | AlbumID int `json:"albumId"` 101 | Description string `json:"description"` 102 | Bitrate string `json:"bitrate"` 103 | CdList []struct { 104 | Composer string `json:"composer"` 105 | Type string `json:"type"` 106 | WorkName string `json:"workName"` 107 | Musiclist []struct { 108 | Duration string `json:"duration"` 109 | MusicID int `json:"musicId"` 110 | Size float64 `json:"size"` 111 | Artist string `json:"artist"` 112 | Composer string `json:"composer"` 113 | Property string `json:"property"` 114 | TrackNo int `json:"trackNo"` 115 | MusicName string `json:"musicName"` 116 | Isfollow bool `json:"isfollow"` 117 | Promotion string `json:"promotion"` 118 | } `json:"musiclist"` 119 | } `json:"cdList"` 120 | ListCa []struct { 121 | CategoryType string `json:"categoryType"` 122 | CategoryName string `json:"categoryName"` 123 | CategoryID int `json:"categoryId"` 124 | } `json:"listCa"` 125 | Price float64 `json:"price"` 126 | BackCover interface{} `json:"backCover"` 127 | SmallIcon string `json:"smallIcon"` 128 | Brand string `json:"brand"` 129 | Player string `json:"player"` 130 | CommentNumber string `json:"commentNumber"` 131 | Resource struct { 132 | Image []interface{} `json:"image"` 133 | Pdf []interface{} `json:"pdf"` 134 | Video []interface{} `json:"video"` 135 | Interview []interface{} `json:"interview"` 136 | } `json:"resource"` 137 | Composer string `json:"composer"` 138 | Format string `json:"format"` 139 | PlayModels []struct { 140 | Size float64 `json:"size"` 141 | Property string `json:"property,omitempty"` 142 | Format string `json:"format"` 143 | AlbumID int `json:"albumId"` 144 | Bitrate string `json:"bitrate"` 145 | Permission bool `json:"permission"` 146 | Type string `json:"type"` 147 | Selected bool `json:"selected"` 148 | } `json:"playModels"` 149 | HasComment int `json:"hasComment"` 150 | Conductor string `json:"conductor"` 151 | IsFollow int `json:"isFollow"` 152 | Size float64 `json:"size"` 153 | Name string `json:"name"` 154 | LargeIcon string `json:"largeIcon"` 155 | } `json:"content"` 156 | ContentEncryption bool `json:"contentEncryption"` 157 | } 158 | 159 | type TrackMeta struct { 160 | Result struct { 161 | Code string `json:"code"` 162 | Msg string `json:"msg"` 163 | } `json:"result"` 164 | Content struct { 165 | AlbumName string `json:"albumName"` 166 | ReleaseTime string `json:"releaseTime"` 167 | Singer string `json:"singer"` 168 | Artist string `json:"artist"` 169 | IsHiRes bool `json:"isHiRes"` 170 | Icon string `json:"icon"` 171 | AlbumID int `json:"albumId"` 172 | Description string `json:"description"` 173 | Bitrate string `json:"bitrate"` 174 | Duration int `json:"duration"` 175 | MusicID int `json:"musicId"` 176 | ListCa []struct { 177 | CategoryType string `json:"categoryType"` 178 | CategoryName string `json:"categoryName"` 179 | CategoryID int `json:"categoryId"` 180 | } `json:"listCa"` 181 | Brand string `json:"brand"` 182 | Player string `json:"player"` 183 | CommentNumber string `json:"commentNumber"` 184 | Is360RA bool `json:"is360RA"` 185 | Composer string `json:"composer"` 186 | HasFollow int `json:"hasFollow"` 187 | PlayModels []struct { 188 | MusicID int `json:"musicId"` 189 | Size float64 `json:"size"` 190 | Format string `json:"format"` 191 | AlbumID int `json:"albumId"` 192 | IndexID int `json:"indexId"` 193 | Bitrate string `json:"bitrate"` 194 | Permission bool `json:"permission"` 195 | Type string `json:"type"` 196 | Selected bool `json:"selected"` 197 | } `json:"playModels"` 198 | FollowNumber string `json:"followNumber"` 199 | WorkName string `json:"workName"` 200 | HasComment int `json:"hasComment"` 201 | Conductor string `json:"conductor"` 202 | ListRate []string `json:"listRate"` 203 | MusicName string `json:"musicName"` 204 | IsFollow int `json:"isFollow"` 205 | Corporation struct { 206 | Name interface{} `json:"name"` 207 | ID int `json:"id"` 208 | } `json:"Corporation"` 209 | Size float64 `json:"size"` 210 | ListFormat []string `json:"listFormat"` 211 | } `json:"content"` 212 | ContentEncryption bool `json:"contentEncryption"` 213 | } 214 | 215 | type FileMetaEnc struct { 216 | Result struct { 217 | Code string `json:"code"` 218 | Msg string `json:"msg"` 219 | } `json:"result"` 220 | Content struct { 221 | Encrypcontent string `json:"encrypcontent"` 222 | } `json:"content"` 223 | ContentEncryption bool `json:"contentEncryption"` 224 | } 225 | 226 | type FileMeta struct { 227 | SecretKey string `json:"secretKey"` 228 | SegmentSize int `json:"segmentSize"` 229 | Format string `json:"format"` 230 | AlbumID int `json:"albumId"` 231 | SampleRate int `json:"sampleRate"` 232 | EncryptionAlgorithm string `json:"encryptionAlgorithm"` 233 | Samples int `json:"samples"` 234 | Segments int `json:"segments"` 235 | BaseURL string `json:"baseUrl"` 236 | FrameSize int `json:"frameSize"` 237 | Names []string `json:"names"` 238 | MusicID int `json:"musicId"` 239 | SampleBit int `json:"sampleBit"` 240 | Property string `json:"property"` 241 | } 242 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/aes" 7 | "crypto/cipher" 8 | "crypto/md5" 9 | "encoding/base64" 10 | "encoding/hex" 11 | "encoding/json" 12 | "errors" 13 | "fmt" 14 | "html" 15 | "io" 16 | "io/ioutil" 17 | "net/http" 18 | "net/url" 19 | "os" 20 | "os/exec" 21 | "path/filepath" 22 | "regexp" 23 | "runtime" 24 | "strconv" 25 | "strings" 26 | "text/template" 27 | "time" 28 | 29 | "github.com/alexflint/go-arg" 30 | "github.com/go-flac/flacpicture" 31 | "github.com/go-flac/flacvorbis" 32 | "github.com/go-flac/go-flac" 33 | ) 34 | 35 | const ( 36 | regexString = `^https://hi-resmusic.sonyselect.kuke.com/page/album.html\?id=(\d+)$` 37 | apiBase = "https://api.sonyselect.com.cn/streaming/" 38 | key = "RF9q4w<|]`) 360 | sanitized := regex.ReplaceAllString(filename, "_") 361 | return sanitized 362 | } 363 | 364 | func generateUrl(_url string) (string, error) { 365 | date := time.Now().Format("200601021504") 366 | u, err := url.Parse(_url) 367 | if err != nil { 368 | return "", err 369 | } 370 | pathAndQuery := u.Path + u.RawQuery 371 | h := md5.New() 372 | _, err = h.Write([]byte(urlKey + date + pathAndQuery)) 373 | if err != nil { 374 | return "", err 375 | } 376 | hash := hex.EncodeToString(h.Sum(nil)) 377 | genUrl := u.Scheme + "://" + u.Host + "/" + date + "/" + hash + pathAndQuery 378 | return genUrl, nil 379 | } 380 | 381 | func downloadSegs(tmpPath string, meta *FileMeta) ([]string, error) { 382 | var segPaths []string 383 | base := meta.BaseURL 384 | segTotal := len(meta.Names) 385 | for segNum, fname := range meta.Names { 386 | segNum++ 387 | segPath := filepath.Join(tmpPath, fname) 388 | f, err := os.OpenFile(segPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0755) 389 | if err != nil { 390 | return nil, err 391 | } 392 | fmt.Printf("\rSegment %d of %d.", segNum, segTotal) 393 | url, err := generateUrl(base + fname) 394 | if err != nil { 395 | f.Close() 396 | return nil, err 397 | } 398 | req, err := http.NewRequest(http.MethodGet, url, nil) 399 | if err != nil { 400 | f.Close() 401 | return nil, err 402 | } 403 | do, err := client.Do(req) 404 | if err != nil { 405 | f.Close() 406 | return nil, err 407 | } 408 | if do.StatusCode != http.StatusOK { 409 | do.Body.Close() 410 | f.Close() 411 | return nil, errors.New(do.Status) 412 | } 413 | _, err = io.Copy(f, do.Body) 414 | do.Body.Close() 415 | f.Close() 416 | if err != nil { 417 | return nil, err 418 | } 419 | segPaths = append(segPaths, segPath) 420 | } 421 | fmt.Println("") 422 | return segPaths, nil 423 | } 424 | 425 | func getTmpPath() (string, error) { 426 | return os.MkdirTemp(os.TempDir(), "") 427 | } 428 | 429 | func cleanup(segPaths []string, txtPath string) { 430 | exists, _ := fileExists(txtPath) 431 | if exists { 432 | os.Remove(txtPath) 433 | } 434 | for _, segPath := range segPaths { 435 | os.Remove(segPath) 436 | } 437 | } 438 | 439 | func mergeSegs(trackPath, tmpPath string, segPaths []string) error { 440 | txtPath := filepath.Join(tmpPath, "tmp.txt") 441 | defer cleanup(segPaths, txtPath) 442 | f, err := os.OpenFile(txtPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0755) 443 | if err != nil { 444 | return err 445 | } 446 | for _, segPath := range segPaths { 447 | line := fmt.Sprintf("file '%s'\n", segPath) 448 | _, err := f.WriteString(line) 449 | if err != nil { 450 | f.Close() 451 | return err 452 | } 453 | } 454 | f.Close() 455 | var ( 456 | errBuffer bytes.Buffer 457 | args = []string{"-f", "concat", "-safe", "0", "-i", txtPath, "-c", "flac", "-y", trackPath} 458 | ) 459 | cmd := exec.Command("ffmpeg", args...) 460 | cmd.Stderr = &errBuffer 461 | err = cmd.Run() 462 | if err != nil { 463 | errString := fmt.Sprintf("%s\n%s", err, errBuffer.String()) 464 | return errors.New(errString) 465 | } 466 | return nil 467 | } 468 | 469 | func getTracktotal(albumMeta *AlbumMeta) int { 470 | trackTotal := 0 471 | for _, cd := range albumMeta.Content.CdList { 472 | trackTotal += len(cd.Musiclist) 473 | } 474 | return trackTotal 475 | } 476 | 477 | func parseTemplate(templateText string, tags map[string]string) string { 478 | var buffer bytes.Buffer 479 | for { 480 | err := template.Must(template.New("").Parse(templateText)).Execute(&buffer, tags) 481 | if err == nil { 482 | break 483 | } 484 | fmt.Println("Failed to parse template. Default will be used instead.") 485 | templateText = "{{.trackPad}}. {{.title}}" 486 | buffer.Reset() 487 | } 488 | return html.UnescapeString(buffer.String()) 489 | } 490 | 491 | func formatFreq(freq int) string { 492 | freqStr := strconv.Itoa(freq / 100) 493 | if strings.HasSuffix(freqStr, "0") { 494 | return strconv.Itoa(freq / 1000) 495 | } else { 496 | freqStrLen := len(freqStr) 497 | return freqStr[:freqStrLen-1] + "." + freqStr[freqStrLen-1:] 498 | } 499 | } 500 | 501 | func downloadCover(_url, path string) error { 502 | f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0755) 503 | if err != nil { 504 | return err 505 | } 506 | defer f.Close() 507 | req, err := client.Get(_url) 508 | if err != nil { 509 | return err 510 | } 511 | defer req.Body.Close() 512 | if req.StatusCode != http.StatusOK { 513 | return errors.New(req.Status) 514 | } 515 | _, err = io.Copy(f, req.Body) 516 | return err 517 | } 518 | 519 | func readCover(coverPath string) ([]byte, error) { 520 | imgData, err := ioutil.ReadFile(coverPath) 521 | if err != nil { 522 | return nil, err 523 | } 524 | return imgData, nil 525 | } 526 | 527 | func writeTags(trackPath, coverPath string, tags map[string]string) error { 528 | var ( 529 | err error 530 | imgData []byte 531 | ) 532 | if coverPath != "" { 533 | imgData, err = readCover(coverPath) 534 | if err != nil { 535 | return err 536 | } 537 | } 538 | delete(tags, "trackPad") 539 | f, err := flac.ParseFile(trackPath) 540 | if err != nil { 541 | return err 542 | } 543 | tag := flacvorbis.New() 544 | for k, v := range tags { 545 | tag.Add(strings.ToUpper(k), v) 546 | } 547 | tagMeta := tag.Marshal() 548 | f.Meta = append(f.Meta, &tagMeta) 549 | if imgData != nil { 550 | picture, err := flacpicture.NewFromImageData( 551 | flacpicture.PictureTypeFrontCover, "", imgData, "image/jpeg", 552 | ) 553 | if err != nil { 554 | return err 555 | } 556 | pictureMeta := picture.Marshal() 557 | f.Meta = append(f.Meta, &pictureMeta) 558 | } 559 | return f.Save(trackPath) 560 | } 561 | 562 | func init() { 563 | fmt.Println(` 564 | _____ _____ _ ____ _ _ 565 | | __|___ ___ _ _ | | |_ _| |_ ___ | \ ___ _ _ _ ___| |___ ___ _| |___ ___ 566 | |__ | . | | | | | -| | | '_| -_| | | | . | | | | | | . | .'| . | -_| _| 567 | |_____|___|_|_|_ | |__|__|___|_,_|___| |____/|___|_____|_|_|_|___|__,|___|___|_| 568 | |___| 569 | `) 570 | } 571 | 572 | func main() { 573 | var albumFolder string 574 | scriptDir, err := getScriptDir() 575 | if err != nil { 576 | panic(err) 577 | } 578 | tmpPath, err := getTmpPath() 579 | if err != nil { 580 | panic(err) 581 | } 582 | err = os.Chdir(scriptDir) 583 | if err != nil { 584 | panic(err) 585 | } 586 | cfg, err := parseCfg() 587 | if err != nil { 588 | handleErr("Failed to parse config file.", err, true) 589 | } 590 | err = makeDirs(cfg.OutPath) 591 | if err != nil { 592 | handleErr("Failed to make output folder.", err, true) 593 | } 594 | postData := getPostData(cfg.SonySelectID) 595 | albumTotal := len(cfg.Urls) 596 | out: 597 | for albumNum, _url := range cfg.Urls { 598 | fmt.Printf("Album %d of %d:\n", albumNum+1, albumTotal) 599 | albumId := checkUrl(_url) 600 | if albumId == "" { 601 | fmt.Println("Invalid URL:", _url) 602 | continue 603 | } 604 | albumMeta, err := getAlbumMeta(albumId, postData) 605 | if err != nil { 606 | handleErr("Failed to fetch album metadata.", err, false) 607 | continue 608 | } 609 | parsedAlbMeta := parseAlbumMeta(albumMeta) 610 | if cfg.OmitArtists { 611 | albumFolder = parsedAlbMeta["album"] 612 | } else { 613 | albumFolder = parsedAlbMeta["albumArtist"] + " - " + parsedAlbMeta["album"] 614 | } 615 | fmt.Println(albumFolder) 616 | if len(albumFolder) > 120 { 617 | fmt.Println("Album folder was chopped as it exceeds 120 characters.") 618 | albumFolder = albumFolder[:120] 619 | } 620 | albumPath := filepath.Join(cfg.OutPath, sanitize(albumFolder)) 621 | err = makeDirs(albumPath) 622 | if err != nil { 623 | handleErr("Failed to make album folder.", err, false) 624 | continue 625 | } 626 | coverPath := filepath.Join(albumPath, "cover.jpg") 627 | err = downloadCover(albumMeta.Content.LargeIcon, coverPath) 628 | if err != nil { 629 | handleErr("Failed to get cover.", err, false) 630 | coverPath = "" 631 | } 632 | trackNum := 0 633 | trackTotal := getTracktotal(albumMeta) 634 | for _, cd := range albumMeta.Content.CdList { 635 | for _, track := range cd.Musiclist { 636 | trackNum++ 637 | trackMeta, err := getTrackMeta(track.MusicID, postData) 638 | if err != nil { 639 | handleErr("Failed to get track metadata.", err, false) 640 | continue 641 | } 642 | parsedMeta := parseTrackMeta(trackMeta, parsedAlbMeta, trackNum, trackTotal) 643 | if trackMeta.Content.PlayModels[0].Type != "streaming" { 644 | fmt.Println("Album is purchase-only.") 645 | continue out 646 | } 647 | trackFname := parseTemplate(cfg.TrackTemplate, parsedMeta) 648 | sanTrackFname := sanitize(trackFname) 649 | trackPath := filepath.Join(albumPath, sanTrackFname+".flac") 650 | exists, err := fileExists(trackPath) 651 | if err != nil { 652 | handleErr("Failed to check if track already exists locally.", err, false) 653 | continue 654 | } 655 | if exists { 656 | fmt.Println("Track already exists locally.") 657 | continue 658 | } 659 | fileMeta, err := getFileMeta(trackMeta.Content.PlayModels[0].IndexID, postData) 660 | if err != nil { 661 | handleErr("Failed to get file metadata.", err, false) 662 | continue 663 | } 664 | fmt.Printf( 665 | "Downloading track %d of %d: %s - %d-bit / %s kHz FLAC\n", 666 | trackNum, trackTotal, parsedMeta["title"], fileMeta.SampleBit, formatFreq(fileMeta.SampleRate), 667 | ) 668 | segPaths, err := downloadSegs(tmpPath, fileMeta) 669 | if err != nil { 670 | handleErr("Failed to download segments.", err, false) 671 | continue 672 | } 673 | err = mergeSegs(trackPath, tmpPath, segPaths) 674 | if err != nil { 675 | handleErr("Failed to merge segments.", err, false) 676 | continue 677 | } 678 | err = writeTags(trackPath, coverPath, parsedMeta) 679 | if err != nil { 680 | fmt.Printf("Failed to write tags.\n%s", err) 681 | continue 682 | } 683 | if coverPath != "" && !cfg.KeepCover { 684 | err := os.Remove(coverPath) 685 | if err != nil { 686 | handleErr("Failed to delete cover.", err, false) 687 | } 688 | } 689 | } 690 | } 691 | } 692 | } 693 | --------------------------------------------------------------------------------