mirror of
https://github.com/zhaarey/apple-music-downloader.git
synced 2025-10-23 15:11:05 +00:00
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@
|
|||||||
!go.sum
|
!go.sum
|
||||||
!main.go
|
!main.go
|
||||||
!README.md
|
!README.md
|
||||||
|
!utils/*
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -2,40 +2,31 @@
|
|||||||
|
|
||||||
### 添加功能
|
### 添加功能
|
||||||
|
|
||||||
1. 调用外部MP4Box自动封装ec3为m4a
|
1. 调用外部MP4Box添加tag
|
||||||
2. 更改目录结构为 歌手名\专辑名 ;Atmos下载文件则另外移动到AM-DL-Atmos downloads,并更改目录结构为 歌手名\专辑名 [Atmos]
|
2. 更改目录结构为 歌手名\专辑名 ;Atmos下载文件则另外移动到AM-DL-Atmos downloads,并更改目录结构为 歌手名\专辑名 [Atmos]
|
||||||
3. 运行结束后显示总体完成情况
|
3. 运行结束后显示总体完成情况
|
||||||
4. 自动内嵌封面和LRC歌词(需要media-user-token,获取方式看最后的说明)
|
4. 自动内嵌封面和LRC歌词(需要media-user-token,获取方式看最后的说明)
|
||||||
5. 自动构建 可以到 [Actions](https://github.com/zhaarey/apple-music-alac-atmos-downloader/actions) 页面下载最新自动构建版本 可以直接`main.exe url`
|
5. 自动构建 可以到 [Actions](https://github.com/zhaarey/apple-music-alac-atmos-downloader/actions) 页面下载最新自动构建版本 可以直接`main.exe url`
|
||||||
6. main 支持check 可以填入文本地址 或API数据库.
|
6. 支持逐词与未同步歌词
|
||||||
7. 新增get-m3u8-from-device 改为true 且设置端口`adb forward tcp:20020 tcp:20020`即从模拟器获取m3u8
|
7. 新增get-m3u8-from-device 改为true 且设置端口`adb forward tcp:20020 tcp:20020`即从模拟器获取m3u8
|
||||||
8. 文件夹和文件支持模板
|
8. 文件夹和文件支持模板
|
||||||
9. 支持下载歌手 `go run main.go https://music.apple.com/us/artist/taylor-swift/159260351` `--all-album` 自动选择歌手的所有专辑
|
9. 支持下载歌手 `go run main.go https://music.apple.com/us/artist/taylor-swift/159260351` `--all-album` 自动选择歌手的所有专辑
|
||||||
10. 新增[wrapper](https://github.com/zhaarey/wrapper/releases)模式 目前只能linux运行,解密速度超快,基本秒解
|
10. 新增[wrapper](https://github.com/zhaarey/wrapper/releases)模式 目前只能linux运行,解密速度超快,基本秒解
|
||||||
11. `limit-max`支持限制长度 默认200
|
11. `limit-max`支持限制长度 默认200
|
||||||
12. 支持逐词与未同步歌词
|
12. 现已支持arm64解密
|
||||||
13. 现已支持arm64解密
|
13. 下载解密部分更换为Sendy McSenderson的代码,实现边下载边解密
|
||||||
|
|
||||||
### Special thanks to `chocomint` for creating `agent-arm64.js`
|
### Special thanks to `chocomint` for creating `agent-arm64.js`
|
||||||
|
|
||||||
本项目仅支持ALAC和Atmos
|
本项目仅支持ALAC、Atmos和苹果数字母带生成的AAC(adm-aac),不支持仅有AAC的专辑(only-aac)及MV
|
||||||
|
|
||||||
- `alac (audio-alac-stereo)`
|
- `alac (audio-alac-stereo)`
|
||||||
- `ec3 (audio-atmos / audio-ec3)`
|
- `ec3 (audio-atmos / audio-ec3)`
|
||||||
|
|
||||||
### Python项目
|
|
||||||
|
|
||||||
如需下载AAC推荐使用WorldObservationLog的[AppleMusicDecrypt](https://github.com/WorldObservationLog/AppleMusicDecrypt)
|
|
||||||
|
|
||||||
[AppleMusicDecrypt](https://github.com/WorldObservationLog/AppleMusicDecrypt)支持以下编码
|
|
||||||
|
|
||||||
- `alac (audio-alac-stereo)`
|
|
||||||
- `ec3 (audio-atmos / audio-ec3)`
|
|
||||||
- `ac3 (audio-ac3)`
|
|
||||||
- `aac (audio-stereo)`
|
- `aac (audio-stereo)`
|
||||||
- `aac-binaural (audio-stereo-binaural)`
|
- `aac-binaural (audio-stereo-binaural)`
|
||||||
- `aac-downmix (audio-stereo-downmix)`
|
- `aac-downmix (audio-stereo-downmix)`
|
||||||
|
|
||||||
|
|
||||||
# Apple Music ALAC / Dolby Atmos Downloader
|
# Apple Music ALAC / Dolby Atmos Downloader
|
||||||
|
|
||||||
Original script by Sorrow. Modified by me to include some fixes and improvements.
|
Original script by Sorrow. Modified by me to include some fixes and improvements.
|
||||||
@@ -63,6 +54,7 @@ Original script by Sorrow. Modified by me to include some fixes and improvements
|
|||||||
8. Start downloading singles: `go run main.go --select https://music.apple.com/us/album/whenever-you-need-somebody-2022-remaster/1624945511` input numbers separated by spaces.
|
8. Start downloading singles: `go run main.go --select https://music.apple.com/us/album/whenever-you-need-somebody-2022-remaster/1624945511` input numbers separated by spaces.
|
||||||
9. Start downloading some playlists: `go run main.go https://music.apple.com/us/playlist/taylor-swift-essentials/pl.3950454ced8c45a3b0cc693c2a7db97b` or `go run main.go https://music.apple.com/us/playlist/hi-res-lossless-24-bit-192khz/pl.u-MDAWvpjt38370N`.
|
9. Start downloading some playlists: `go run main.go https://music.apple.com/us/playlist/taylor-swift-essentials/pl.3950454ced8c45a3b0cc693c2a7db97b` or `go run main.go https://music.apple.com/us/playlist/hi-res-lossless-24-bit-192khz/pl.u-MDAWvpjt38370N`.
|
||||||
10. For dolby atmos: `go run main.go --atmos https://music.apple.com/us/album/1989-taylors-version-deluxe/1713845538`.
|
10. For dolby atmos: `go run main.go --atmos https://music.apple.com/us/album/1989-taylors-version-deluxe/1713845538`.
|
||||||
|
11. For adm aac: `go run main.go --aac https://music.apple.com/us/album/1989-taylors-version-deluxe/1713845538`.
|
||||||
|
|
||||||
[中文教程-详见方法三](https://telegra.ph/Apple-Music-Alac高解析度无损音乐下载教程-04-02-2)
|
[中文教程-详见方法三](https://telegra.ph/Apple-Music-Alac高解析度无损音乐下载教程-04-02-2)
|
||||||
|
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ cover-size: 5000x5000
|
|||||||
cover-format: jpg #jpg png or original
|
cover-format: jpg #jpg png or original
|
||||||
alac-save-folder: AM-DL downloads
|
alac-save-folder: AM-DL downloads
|
||||||
atmos-save-folder: AM-DL-Atmos downloads
|
atmos-save-folder: AM-DL-Atmos downloads
|
||||||
check: "" # API or .txt
|
max-memory-limit: 256 # MB
|
||||||
force-api: false
|
|
||||||
decrypt-m3u8-port: "127.0.0.1:10020"
|
decrypt-m3u8-port: "127.0.0.1:10020"
|
||||||
get-m3u8-port: "127.0.0.1:20020"
|
get-m3u8-port: "127.0.0.1:20020"
|
||||||
get-m3u8-from-device: true
|
get-m3u8-from-device: true
|
||||||
#set 'all' to retrieve all m3u8, and set 'hires' to only detect hires m3u8.
|
#set 'all' to retrieve all m3u8, and set 'hires' to only detect hires m3u8.
|
||||||
get-m3u8-mode: hires # all hires
|
get-m3u8-mode: hires # all hires
|
||||||
|
aac-type: aac # aac aac-binaural aac-downmix
|
||||||
alac-max: 192000 #192000 96000 48000 44100
|
alac-max: 192000 #192000 96000 48000 44100
|
||||||
atmos-max: 2768 #2768 2448
|
atmos-max: 2768 #2768 2448
|
||||||
limit-max: 200
|
limit-max: 200
|
||||||
|
|||||||
5
go.mod
5
go.mod
@@ -3,15 +3,16 @@ module main
|
|||||||
go 1.17
|
go 1.17
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/abema/go-mp4 v0.7.2
|
github.com/Eyevinn/mp4ff v0.46.0
|
||||||
|
github.com/abema/go-mp4 v1.3.0
|
||||||
github.com/grafov/m3u8 v0.11.1
|
github.com/grafov/m3u8 v0.11.1
|
||||||
|
github.com/schollz/progressbar/v3 v3.14.6
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/schollz/progressbar/v3 v3.14.6 // indirect
|
|
||||||
golang.org/x/sys v0.22.0 // indirect
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
golang.org/x/term v0.22.0 // indirect
|
golang.org/x/term v0.22.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
8
go.sum
8
go.sum
@@ -1,11 +1,15 @@
|
|||||||
github.com/abema/go-mp4 v0.7.2 h1:ugTC8gfEmjyaDKpXs3vi2QzgJbDu9B8m6UMMIpbYbGg=
|
github.com/Eyevinn/mp4ff v0.46.0 h1:A8oJA4A3C9fDbX38jEw/26utjNdvmRmrO37tVI5pDk0=
|
||||||
github.com/abema/go-mp4 v0.7.2/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=
|
github.com/Eyevinn/mp4ff v0.46.0/go.mod h1:hJNUUqOBryLAzUW9wpCJyw2HaI+TCd2rUPhafoS5lgg=
|
||||||
|
github.com/abema/go-mp4 v1.3.0 h1:vr0PX0jk3E4GO1c28fNRsyZdkLwz38R+XRVncIH1XDk=
|
||||||
|
github.com/abema/go-mp4 v1.3.0/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=
|
||||||
github.com/beevik/etree v1.3.0 h1:hQTc+pylzIKDb23yYprodCWWTt+ojFfUZyzU09a/hmU=
|
github.com/beevik/etree v1.3.0 h1:hQTc+pylzIKDb23yYprodCWWTt+ojFfUZyzU09a/hmU=
|
||||||
github.com/beevik/etree v1.3.0/go.mod h1:aiPf89g/1k3AShMVAzriilpcE4R/Vuor90y83zVZWFc=
|
github.com/beevik/etree v1.3.0/go.mod h1:aiPf89g/1k3AShMVAzriilpcE4R/Vuor90y83zVZWFc=
|
||||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
|
||||||
|
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||||
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
|
github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
|
||||||
|
|||||||
21
utils/runv2/LICENSE
Normal file
21
utils/runv2/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Sendy McSenderson
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
681
utils/runv2/runv2.go
Normal file
681
utils/runv2/runv2.go
Normal file
@@ -0,0 +1,681 @@
|
|||||||
|
package runv2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Eyevinn/mp4ff/mp4"
|
||||||
|
"github.com/grafov/m3u8"
|
||||||
|
|
||||||
|
"encoding/binary"
|
||||||
|
"github.com/schollz/progressbar/v3"
|
||||||
|
|
||||||
|
"main/utils/structs"
|
||||||
|
)
|
||||||
|
const prefetchKey = "skd://itunes.apple.com/P000000000/s1/e1"
|
||||||
|
var ErrTimeout = errors.New("response timed out")
|
||||||
|
|
||||||
|
type TimedResponseBody struct {
|
||||||
|
timeout time.Duration
|
||||||
|
timer *time.Timer
|
||||||
|
threshold int
|
||||||
|
body io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *TimedResponseBody) Read(p []byte) (int, error) {
|
||||||
|
n, err := b.body.Read(p)
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
// fmt.Printf("Read %d bytes, buffer size %d bytes", n, len(p))
|
||||||
|
if n >= b.threshold {
|
||||||
|
b.timer.Reset(b.timeout)
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func Run(adamId string, playlistUrl string, outfile string, Config structs.ConfigSet) error {
|
||||||
|
var err error
|
||||||
|
var optstimeout uint
|
||||||
|
optstimeout = 0
|
||||||
|
timeout := time.Duration(optstimeout * uint(time.Millisecond))
|
||||||
|
header := make(http.Header)
|
||||||
|
|
||||||
|
// request media playlist
|
||||||
|
req, err := http.NewRequest("GET", playlistUrl, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header = header
|
||||||
|
// requesting an HLS playlist should be relatively fast, so we set the timeout directly on the client
|
||||||
|
do, err := (&http.Client{Timeout: timeout}).Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse m3u8
|
||||||
|
segments, err := parseMediaPlaylist(do.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
segment := segments[0]
|
||||||
|
if segment == nil {
|
||||||
|
return errors.New("no segments extracted from playlist")
|
||||||
|
}
|
||||||
|
if segment.Limit <= 0 {
|
||||||
|
return errors.New("non-byterange playlists are currently unsupported")
|
||||||
|
}
|
||||||
|
|
||||||
|
// get URL to the actual file
|
||||||
|
parsedUrl, err := url.Parse(playlistUrl)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fileUrl, err := parsedUrl.Parse(segment.URI)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// request mp4
|
||||||
|
ctx, cancel := context.WithCancelCause(context.Background())
|
||||||
|
defer cancel(nil)
|
||||||
|
req, err = http.NewRequestWithContext(ctx, "GET", fileUrl.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header = header
|
||||||
|
|
||||||
|
var body io.Reader
|
||||||
|
client := &http.Client{Timeout: timeout}
|
||||||
|
if optstimeout > 0 {
|
||||||
|
// create the timer before calling Do so that the timeout covers TCP handshake,
|
||||||
|
// TLS handshake, sending the request and receiving HTTP headers
|
||||||
|
timer := time.AfterFunc(timeout, func() { cancel(ErrTimeout) })
|
||||||
|
do, err = client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer do.Body.Close()
|
||||||
|
body = &TimedResponseBody{
|
||||||
|
timeout: timeout,
|
||||||
|
timer: timer,
|
||||||
|
threshold: 256,
|
||||||
|
body: do.Body,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
do, err = client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer do.Body.Close()
|
||||||
|
body = do.Body
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalLen int64
|
||||||
|
totalLen = do.ContentLength
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// connect to decryptor
|
||||||
|
//addr := fmt.Sprintf("127.0.0.1:10020")
|
||||||
|
addr := Config.DecryptM3u8Port
|
||||||
|
conn, err := net.Dial("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Print("Downloading...\n")
|
||||||
|
defer Close(conn)
|
||||||
|
|
||||||
|
err = downloadAndDecryptFile(conn, body, outfile, adamId, segments, totalLen, Config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Print("Decryption finished\n")
|
||||||
|
// create output file
|
||||||
|
// ofh, err := os.Create(outfile)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// defer ofh.Close()
|
||||||
|
//
|
||||||
|
// _, err = ofh.Write(buffer.Bytes())
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadAndDecryptFile(conn io.ReadWriter, in io.Reader, outfile string,
|
||||||
|
adamId string, playlistSegments []*m3u8.MediaSegment, totalLen int64, Config structs.ConfigSet) error {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
var outBuf *bufio.Writer
|
||||||
|
MaxMemorySize := int64(Config.MaxMemoryLimit * 1024 * 1024)
|
||||||
|
inBuf := bufio.NewReader(in)
|
||||||
|
if totalLen <= MaxMemorySize {
|
||||||
|
outBuf = bufio.NewWriter(&buffer)
|
||||||
|
} else {
|
||||||
|
ofh, err := os.Create(outfile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer ofh.Close()
|
||||||
|
outBuf = bufio.NewWriter(ofh)
|
||||||
|
}
|
||||||
|
init, offset, err := ReadInitSegment(inBuf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if init == nil {
|
||||||
|
return errors.New("no init segment found")
|
||||||
|
}
|
||||||
|
|
||||||
|
tracks, err := TransformInit(init)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = sanitizeInit(init)
|
||||||
|
if err != nil {
|
||||||
|
// errors returned by sanitizeInit are non-fatal
|
||||||
|
fmt.Printf("Warning: unable to sanitize init completely: %s\n", err)
|
||||||
|
}
|
||||||
|
err = init.Encode(outBuf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'segment' in m3u8 == 'fragment' in mp4ff
|
||||||
|
//fmt.Println("Starting decryption...")
|
||||||
|
bar := progressbar.NewOptions64(totalLen,
|
||||||
|
progressbar.OptionClearOnFinish(),
|
||||||
|
progressbar.OptionSetElapsedTime(false),
|
||||||
|
progressbar.OptionSetPredictTime(false),
|
||||||
|
progressbar.OptionShowElapsedTimeOnFinish(),
|
||||||
|
progressbar.OptionShowCount(),
|
||||||
|
progressbar.OptionEnableColorCodes(true),
|
||||||
|
progressbar.OptionShowBytes(true),
|
||||||
|
//progressbar.OptionSetDescription("Decrypting..."),
|
||||||
|
progressbar.OptionSetTheme(progressbar.Theme{
|
||||||
|
Saucer: "",
|
||||||
|
SaucerHead: "",
|
||||||
|
SaucerPadding: "",
|
||||||
|
BarStart: "",
|
||||||
|
BarEnd: "",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
bar.Add64(int64(offset))
|
||||||
|
rw := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
|
||||||
|
for i := 0; ; i++ {
|
||||||
|
var frag *mp4.Fragment
|
||||||
|
rawoffset := offset
|
||||||
|
frag, offset, err = ReadNextFragment(inBuf, offset)
|
||||||
|
rawoffset = offset - rawoffset
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if frag == nil {
|
||||||
|
// check offset against Content-Length?
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// print progress
|
||||||
|
|
||||||
|
// if totalLen > 0 {
|
||||||
|
// fmt.Printf("%.2f%% of %d bytes\n", 100*float32(offset)/float32(totalLen), totalLen)
|
||||||
|
// }
|
||||||
|
segment := playlistSegments[i]
|
||||||
|
if segment == nil {
|
||||||
|
return errors.New("segment number out of sync")
|
||||||
|
}
|
||||||
|
key := segment.Key
|
||||||
|
if key != nil {
|
||||||
|
if i != 0 {
|
||||||
|
SwitchKeys(rw)
|
||||||
|
}
|
||||||
|
if key.URI == prefetchKey {
|
||||||
|
SendString(rw, "0")
|
||||||
|
} else {
|
||||||
|
SendString(rw, adamId)
|
||||||
|
}
|
||||||
|
SendString(rw, key.URI)
|
||||||
|
}
|
||||||
|
// flushes the buffer
|
||||||
|
err = DecryptFragment(frag, tracks, rw)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("decryptFragment: %w", err)
|
||||||
|
}
|
||||||
|
err = frag.Encode(outBuf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bar.Add64(int64(rawoffset))
|
||||||
|
}
|
||||||
|
err = outBuf.Flush()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if totalLen <= MaxMemorySize {
|
||||||
|
// create output file
|
||||||
|
ofh, err := os.Create(outfile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer ofh.Close()
|
||||||
|
|
||||||
|
_, err = ofh.Write(buffer.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove boxes in the init segment that are known to cause compatibility issues
|
||||||
|
func sanitizeInit(init *mp4.InitSegment) error {
|
||||||
|
traks := init.Moov.Traks
|
||||||
|
if len(traks) > 1 {
|
||||||
|
return errors.New("more than 1 track found")
|
||||||
|
}
|
||||||
|
// Remove duplicate ec-3 or alac boxes in stsd since some programs (e.g. cuetools) don't
|
||||||
|
// like it when there's more than 1 entry in stsd.
|
||||||
|
// Every audio track contains two of these boxes because two IVs are needed to decrypt the
|
||||||
|
// track. The two boxes become identical after removing encryption info.
|
||||||
|
stsd := traks[0].Mdia.Minf.Stbl.Stsd
|
||||||
|
if stsd.SampleCount == 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if stsd.SampleCount > 2 {
|
||||||
|
return fmt.Errorf("expected only 1 or 2 entries in stsd, got %d", stsd.SampleCount)
|
||||||
|
}
|
||||||
|
children := stsd.Children
|
||||||
|
if children[0].Type() != children[1].Type() {
|
||||||
|
return errors.New("children in stsd are not of the same type")
|
||||||
|
}
|
||||||
|
stsd.Children = children[:1]
|
||||||
|
stsd.SampleCount = 1
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workaround for m3u8 not supporting multiple keys - remove
|
||||||
|
// PlayReady and Widevine
|
||||||
|
func filterResponse(f io.Reader) (*bytes.Buffer, error) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
|
||||||
|
prefix := []byte("#EXT-X-KEY:")
|
||||||
|
keyFormat := []byte("streamingkeydelivery")
|
||||||
|
for scanner.Scan() {
|
||||||
|
lineBytes := scanner.Bytes()
|
||||||
|
if bytes.HasPrefix(lineBytes, prefix) && !bytes.Contains(lineBytes, keyFormat) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, err := buf.Write(lineBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = buf.WriteString("\n")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMediaPlaylist(r io.ReadCloser) ([]*m3u8.MediaSegment, error) {
|
||||||
|
defer r.Close()
|
||||||
|
playlistBuf, err := filterResponse(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
playlist, listType, err := m3u8.Decode(*playlistBuf, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if listType != m3u8.MEDIA {
|
||||||
|
return nil, errors.New("m3u8 not of media type")
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaPlaylist := playlist.(*m3u8.MediaPlaylist)
|
||||||
|
return mediaPlaylist.Segments, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//pasing
|
||||||
|
func ReadInitSegment(r io.Reader) (*mp4.InitSegment, uint64, error) {
|
||||||
|
var offset uint64 = 0
|
||||||
|
init := mp4.NewMP4Init()
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
box, err := mp4.DecodeBox(offset, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, offset, err
|
||||||
|
}
|
||||||
|
boxType := box.Type()
|
||||||
|
if boxType != "ftyp" && boxType != "moov" {
|
||||||
|
return nil, offset, fmt.Errorf("unexpected box type %s, should be ftyp or moov", boxType)
|
||||||
|
}
|
||||||
|
init.AddChild(box)
|
||||||
|
offset += box.Size()
|
||||||
|
}
|
||||||
|
return init, offset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the next fragment. Returns nil and no error on EOF
|
||||||
|
func ReadNextFragment(r io.Reader, offset uint64) (*mp4.Fragment, uint64, error) {
|
||||||
|
frag := mp4.NewFragment()
|
||||||
|
for {
|
||||||
|
box, err := mp4.DecodeBox(offset, r)
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil, offset, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, offset, err
|
||||||
|
}
|
||||||
|
boxType := box.Type()
|
||||||
|
// fmt.Printf("processing %s, box starts @ offset %d\n", boxType, offset)
|
||||||
|
offset += box.Size()
|
||||||
|
if boxType == "moof" || boxType == "emsg" || boxType == "prft" {
|
||||||
|
frag.AddChild(box)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if boxType == "mdat" {
|
||||||
|
frag.AddChild(box)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fmt.Printf("ignoring a %s box found mid-stream", boxType)
|
||||||
|
}
|
||||||
|
// only 1 mdat box in fragment, meaning that the box doesn't have a preceding moof box
|
||||||
|
if frag.Moof == nil {
|
||||||
|
return nil, offset, fmt.Errorf("more than one mdat box in fragment (box ends @ offset %d)", offset)
|
||||||
|
}
|
||||||
|
return frag, offset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a new slice of boxes with encryption-related sbgp and sgpd removed,
|
||||||
|
// and the total number of bytes removed.
|
||||||
|
// Non-encryption-related ones such as 'roll' are left untouched.
|
||||||
|
func FilterSbgpSgpd(children []mp4.Box) ([]mp4.Box, uint64) {
|
||||||
|
var bytesRemoved uint64 = 0
|
||||||
|
remainingChildren := make([]mp4.Box, 0, len(children))
|
||||||
|
for _, child := range children {
|
||||||
|
switch box := child.(type) {
|
||||||
|
case *mp4.SbgpBox:
|
||||||
|
if box.GroupingType == "seam" || box.GroupingType == "seig" {
|
||||||
|
bytesRemoved += child.Size()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
case *mp4.SgpdBox:
|
||||||
|
if box.GroupingType == "seam" || box.GroupingType == "seig" {
|
||||||
|
bytesRemoved += child.Size()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
remainingChildren = append(remainingChildren, child)
|
||||||
|
}
|
||||||
|
return remainingChildren, bytesRemoved
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get decryption info for tracks from init segment and remove encryption-related boxes
|
||||||
|
func TransformInit(init *mp4.InitSegment) (map[uint32]mp4.DecryptTrackInfo, error) {
|
||||||
|
di, err := mp4.DecryptInit(init)
|
||||||
|
tracks := make(map[uint32]mp4.DecryptTrackInfo, len(di.TrackInfos))
|
||||||
|
for _, ti := range di.TrackInfos {
|
||||||
|
tracks[ti.TrackID] = ti
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return tracks, err
|
||||||
|
}
|
||||||
|
// remove encryption-related sbgp and sgpd
|
||||||
|
for _, trak := range init.Moov.Traks {
|
||||||
|
stbl := trak.Mdia.Minf.Stbl
|
||||||
|
stbl.Children, _ = FilterSbgpSgpd(stbl.Children)
|
||||||
|
}
|
||||||
|
return tracks, nil
|
||||||
|
}
|
||||||
|
//remote
|
||||||
|
// Reset the loops on the script's end and close the connection
|
||||||
|
func Close(conn io.WriteCloser) error {
|
||||||
|
defer conn.Close()
|
||||||
|
_, err := conn.Write([]byte{0, 0, 0, 0, 0})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func SwitchKeys(conn io.Writer) error {
|
||||||
|
_, err := conn.Write([]byte{0, 0, 0, 0})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send id or keyUri
|
||||||
|
func SendString(conn io.Writer, uri string) error {
|
||||||
|
_, err := conn.Write([]byte{byte(len(uri))})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = io.WriteString(conn, uri)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func cbcsFullSubsampleDecrypt(data []byte, conn *bufio.ReadWriter) error {
|
||||||
|
// Drops 4 last bits -> multiple of 16
|
||||||
|
// It wouldn't hurt to send the remaining bytes also because the decryption
|
||||||
|
// function would just return them as-is, but we're truncating the data here
|
||||||
|
// for clarity and interoperability
|
||||||
|
truncatedLen := len(data) & ^0xf
|
||||||
|
// send the whole chunk at once
|
||||||
|
err := binary.Write(conn, binary.LittleEndian, uint32(truncatedLen))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = conn.Write(data[:truncatedLen])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = conn.Flush()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = io.ReadFull(conn, data[:truncatedLen])
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func cbcsStripeDecrypt(data []byte, conn *bufio.ReadWriter, decryptBlockLen, skipBlockLen int) error {
|
||||||
|
size := len(data)
|
||||||
|
|
||||||
|
// block too small, ignore
|
||||||
|
if size < decryptBlockLen {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// number of encrypted blocks in this sample
|
||||||
|
count := ((size - decryptBlockLen) / (decryptBlockLen + skipBlockLen)) + 1
|
||||||
|
totalLen := count * decryptBlockLen
|
||||||
|
|
||||||
|
err := binary.Write(conn, binary.LittleEndian, uint32(totalLen))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pos := 0
|
||||||
|
for {
|
||||||
|
if size-pos < decryptBlockLen { // Leave the rest
|
||||||
|
break
|
||||||
|
}
|
||||||
|
_, err = conn.Write(data[pos : pos+decryptBlockLen])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pos += decryptBlockLen
|
||||||
|
if size-pos < skipBlockLen {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pos += skipBlockLen
|
||||||
|
}
|
||||||
|
err = conn.Flush()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = 0
|
||||||
|
for {
|
||||||
|
if size-pos < decryptBlockLen {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
_, err = io.ReadFull(conn, data[pos:pos+decryptBlockLen])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pos += decryptBlockLen
|
||||||
|
if size-pos < skipBlockLen {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pos += skipBlockLen
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decryption function dispatcher
|
||||||
|
func cbcsDecryptRaw(data []byte, conn *bufio.ReadWriter, decryptBlockLen, skipBlockLen int) error {
|
||||||
|
if skipBlockLen == 0 {
|
||||||
|
// Full encryption of subsamples
|
||||||
|
// e.g. Apple Music ALAC
|
||||||
|
return cbcsFullSubsampleDecrypt(data, conn)
|
||||||
|
} else {
|
||||||
|
// Pattern (stripe) encryption of subsamples
|
||||||
|
// e.g. most AVC and HEVC applications
|
||||||
|
return cbcsStripeDecrypt(data, conn, decryptBlockLen, skipBlockLen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt a cbcs-encrypted sample in-place
|
||||||
|
func cbcsDecryptSample(sample []byte, conn *bufio.ReadWriter,
|
||||||
|
subSamplePatterns []mp4.SubSamplePattern, tenc *mp4.TencBox) error {
|
||||||
|
|
||||||
|
decryptBlockLen := int(tenc.DefaultCryptByteBlock) * 16
|
||||||
|
skipBlockLen := int(tenc.DefaultSkipByteBlock) * 16
|
||||||
|
var pos uint32 = 0
|
||||||
|
|
||||||
|
// Full sample encryption
|
||||||
|
if len(subSamplePatterns) == 0 {
|
||||||
|
return cbcsDecryptRaw(sample, conn, decryptBlockLen, skipBlockLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has subsamples
|
||||||
|
for j := 0; j < len(subSamplePatterns); j++ {
|
||||||
|
ss := subSamplePatterns[j]
|
||||||
|
pos += uint32(ss.BytesOfClearData)
|
||||||
|
|
||||||
|
// Nothing to decrypt!
|
||||||
|
if ss.BytesOfProtectedData <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cbcsDecryptRaw(sample[pos:pos+ss.BytesOfProtectedData],
|
||||||
|
conn, decryptBlockLen, skipBlockLen)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pos += ss.BytesOfProtectedData
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt an array of cbcs-encrypted samples in-place
|
||||||
|
func cbcsDecryptSamples(samples []mp4.FullSample, conn *bufio.ReadWriter,
|
||||||
|
tenc *mp4.TencBox, senc *mp4.SencBox) error {
|
||||||
|
|
||||||
|
for i := range samples {
|
||||||
|
var subSamplePatterns []mp4.SubSamplePattern
|
||||||
|
if len(senc.SubSamples) != 0 {
|
||||||
|
subSamplePatterns = senc.SubSamples[i]
|
||||||
|
}
|
||||||
|
err := cbcsDecryptSample(samples[i].Data, conn, subSamplePatterns, tenc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecryptFragment(frag *mp4.Fragment, tracks map[uint32]mp4.DecryptTrackInfo, conn *bufio.ReadWriter) error {
|
||||||
|
moof := frag.Moof
|
||||||
|
var bytesRemoved uint64 = 0
|
||||||
|
var sxxxBytesRemoved uint64
|
||||||
|
|
||||||
|
for _, traf := range moof.Trafs {
|
||||||
|
ti, ok := tracks[traf.Tfhd.TrackID]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("could not find decryption info for track %d", traf.Tfhd.TrackID)
|
||||||
|
}
|
||||||
|
if ti.Sinf == nil {
|
||||||
|
// unencrypted track
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
schemeType := ti.Sinf.Schm.SchemeType
|
||||||
|
if schemeType != "cbcs" {
|
||||||
|
return fmt.Errorf("scheme type %s not supported", schemeType)
|
||||||
|
}
|
||||||
|
hasSenc, isParsed := traf.ContainsSencBox()
|
||||||
|
if !hasSenc {
|
||||||
|
return fmt.Errorf("no senc box in traf")
|
||||||
|
}
|
||||||
|
|
||||||
|
var senc *mp4.SencBox
|
||||||
|
if traf.Senc != nil {
|
||||||
|
senc = traf.Senc
|
||||||
|
} else {
|
||||||
|
senc = traf.UUIDSenc.Senc
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isParsed {
|
||||||
|
// simply ignore sbgp and sgpd
|
||||||
|
// "Sample To Group Box ('sbgp') and Sample Group Description Box ('sgpd')
|
||||||
|
// of type 'seig' are used to indicate the KID applied to each sample, and changes
|
||||||
|
// to KIDs over time (i.e. 'key rotation')"
|
||||||
|
// (ref: https://dashif.org/docs/DASH-IF-IOP-v3.2.pdf)
|
||||||
|
err := senc.ParseReadBox(ti.Sinf.Schi.Tenc.DefaultPerSampleIVSize, traf.Saiz)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
samples, err := frag.GetFullSamples(ti.Trex)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cbcsDecryptSamples(samples, conn, ti.Sinf.Schi.Tenc, senc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
bytesRemoved += traf.RemoveEncryptionBoxes()
|
||||||
|
// remove sbgp and sgpd
|
||||||
|
traf.Children, sxxxBytesRemoved = FilterSbgpSgpd(traf.Children)
|
||||||
|
bytesRemoved += sxxxBytesRemoved
|
||||||
|
}
|
||||||
|
_, psshBytesRemoved := moof.RemovePsshs()
|
||||||
|
bytesRemoved += psshBytesRemoved
|
||||||
|
for _, traf := range moof.Trafs {
|
||||||
|
for _, trun := range traf.Truns {
|
||||||
|
trun.DataOffset -= int32(bytesRemoved)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
431
utils/structs/structs.go
Normal file
431
utils/structs/structs.go
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
package structs
|
||||||
|
|
||||||
|
type ConfigSet struct {
|
||||||
|
MediaUserToken string `yaml:"media-user-token"`
|
||||||
|
AuthorizationToken string `yaml:"authorization-token"`
|
||||||
|
Language string `yaml:"language"`
|
||||||
|
SaveLrcFile bool `yaml:"save-lrc-file"`
|
||||||
|
LrcType string `yaml:"lrc-type"`
|
||||||
|
LrcFormat string `yaml:"lrc-format"`
|
||||||
|
SaveAnimatedArtwork bool `yaml:"save-animated-artwork"`
|
||||||
|
EmbyAnimatedArtwork bool `yaml:"emby-animated-artwork"`
|
||||||
|
EmbedLrc bool `yaml:"embed-lrc"`
|
||||||
|
EmbedCover bool `yaml:"embed-cover"`
|
||||||
|
SaveArtistCover bool `yaml:"save-artist-cover"`
|
||||||
|
CoverSize string `yaml:"cover-size"`
|
||||||
|
CoverFormat string `yaml:"cover-format"`
|
||||||
|
AlacSaveFolder string `yaml:"alac-save-folder"`
|
||||||
|
AtmosSaveFolder string `yaml:"atmos-save-folder"`
|
||||||
|
AlbumFolderFormat string `yaml:"album-folder-format"`
|
||||||
|
PlaylistFolderFormat string `yaml:"playlist-folder-format"`
|
||||||
|
ArtistFolderFormat string `yaml:"artist-folder-format"`
|
||||||
|
SongFileFormat string `yaml:"song-file-format"`
|
||||||
|
ExplicitChoice string `yaml:"explicit-choice"`
|
||||||
|
CleanChoice string `yaml:"clean-choice"`
|
||||||
|
AppleMasterChoice string `yaml:"apple-master-choice"`
|
||||||
|
MaxMemoryLimit int `yaml:"max-memory-limit"`
|
||||||
|
DecryptM3u8Port string `yaml:"decrypt-m3u8-port"`
|
||||||
|
GetM3u8Port string `yaml:"get-m3u8-port"`
|
||||||
|
GetM3u8Mode string `yaml:"get-m3u8-mode"`
|
||||||
|
GetM3u8FromDevice bool `yaml:"get-m3u8-from-device"`
|
||||||
|
AacType string `yaml:"aac-type"`
|
||||||
|
AlacMax int `yaml:"alac-max"`
|
||||||
|
AtmosMax int `yaml:"atmos-max"`
|
||||||
|
LimitMax int `yaml:"limit-max"`
|
||||||
|
UseSongInfoForPlaylist bool `yaml:"use-songinfo-for-playlist"`
|
||||||
|
DlAlbumcoverForPlaylist bool `yaml:"dl-albumcover-for-playlist"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Counter struct {
|
||||||
|
Unavailable int
|
||||||
|
NotSong int
|
||||||
|
Error int
|
||||||
|
Success int
|
||||||
|
Total int
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApiResult struct {
|
||||||
|
Data []SongData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SongAttributes struct {
|
||||||
|
ArtistName string `json:"artistName"`
|
||||||
|
DiscNumber int `json:"discNumber"`
|
||||||
|
GenreNames []string `json:"genreNames"`
|
||||||
|
ExtendedAssetUrls struct {
|
||||||
|
EnhancedHls string `json:"enhancedHls"`
|
||||||
|
} `json:"extendedAssetUrls"`
|
||||||
|
IsMasteredForItunes bool `json:"isMasteredForItunes"`
|
||||||
|
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
|
||||||
|
ContentRating string `json:"contentRating"`
|
||||||
|
ReleaseDate string `json:"releaseDate"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Isrc string `json:"isrc"`
|
||||||
|
AlbumName string `json:"albumName"`
|
||||||
|
TrackNumber int `json:"trackNumber"`
|
||||||
|
ComposerName string `json:"composerName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AlbumAttributes struct {
|
||||||
|
ArtistName string `json:"artistName"`
|
||||||
|
IsSingle bool `json:"isSingle"`
|
||||||
|
IsComplete bool `json:"isComplete"`
|
||||||
|
GenreNames []string `json:"genreNames"`
|
||||||
|
TrackCount int `json:"trackCount"`
|
||||||
|
IsMasteredForItunes bool `json:"isMasteredForItunes"`
|
||||||
|
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
|
||||||
|
ContentRating string `json:"contentRating"`
|
||||||
|
ReleaseDate string `json:"releaseDate"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
RecordLabel string `json:"recordLabel"`
|
||||||
|
Upc string `json:"upc"`
|
||||||
|
Copyright string `json:"copyright"`
|
||||||
|
IsCompilation bool `json:"isCompilation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SongData struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Attributes SongAttributes `json:"attributes"`
|
||||||
|
Relationships struct {
|
||||||
|
Albums struct {
|
||||||
|
Data []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Href string `json:"href"`
|
||||||
|
Attributes AlbumAttributes `json:"attributes"`
|
||||||
|
} `json:"data"`
|
||||||
|
} `json:"albums"`
|
||||||
|
Artists struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
Data []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Href string `json:"href"`
|
||||||
|
} `json:"data"`
|
||||||
|
} `json:"artists"`
|
||||||
|
} `json:"relationships"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SongResult struct {
|
||||||
|
Artwork struct {
|
||||||
|
Width int `json:"width"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
TextColor3 string `json:"textColor3"`
|
||||||
|
TextColor2 string `json:"textColor2"`
|
||||||
|
TextColor4 string `json:"textColor4"`
|
||||||
|
HasAlpha bool `json:"hasAlpha"`
|
||||||
|
TextColor1 string `json:"textColor1"`
|
||||||
|
BgColor string `json:"bgColor"`
|
||||||
|
HasP3 bool `json:"hasP3"`
|
||||||
|
SupportsLayeredImage bool `json:"supportsLayeredImage"`
|
||||||
|
} `json:"artwork"`
|
||||||
|
ArtistName string `json:"artistName"`
|
||||||
|
CollectionID string `json:"collectionId"`
|
||||||
|
DiscNumber int `json:"discNumber"`
|
||||||
|
GenreNames []string `json:"genreNames"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
DurationInMillis int `json:"durationInMillis"`
|
||||||
|
ReleaseDate string `json:"releaseDate"`
|
||||||
|
ContentRatingsBySystem struct {
|
||||||
|
} `json:"contentRatingsBySystem"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Composer struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
} `json:"composer"`
|
||||||
|
EditorialArtwork struct {
|
||||||
|
} `json:"editorialArtwork"`
|
||||||
|
CollectionName string `json:"collectionName"`
|
||||||
|
AssetUrls struct {
|
||||||
|
Plus string `json:"plus"`
|
||||||
|
Lightweight string `json:"lightweight"`
|
||||||
|
SuperLightweight string `json:"superLightweight"`
|
||||||
|
LightweightPlus string `json:"lightweightPlus"`
|
||||||
|
EnhancedHls string `json:"enhancedHls"`
|
||||||
|
} `json:"assetUrls"`
|
||||||
|
AudioTraits []string `json:"audioTraits"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Copyright string `json:"copyright"`
|
||||||
|
ArtistID string `json:"artistId"`
|
||||||
|
Genres []struct {
|
||||||
|
GenreID string `json:"genreId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
MediaType string `json:"mediaType"`
|
||||||
|
} `json:"genres"`
|
||||||
|
TrackNumber int `json:"trackNumber"`
|
||||||
|
AudioLocale string `json:"audioLocale"`
|
||||||
|
Offers []struct {
|
||||||
|
ActionText struct {
|
||||||
|
Short string `json:"short"`
|
||||||
|
Medium string `json:"medium"`
|
||||||
|
Long string `json:"long"`
|
||||||
|
Downloaded string `json:"downloaded"`
|
||||||
|
Downloading string `json:"downloading"`
|
||||||
|
} `json:"actionText"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
PriceFormatted string `json:"priceFormatted"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
BuyParams string `json:"buyParams"`
|
||||||
|
Variant string `json:"variant,omitempty"`
|
||||||
|
Assets []struct {
|
||||||
|
Flavor string `json:"flavor"`
|
||||||
|
Preview struct {
|
||||||
|
Duration int `json:"duration"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
} `json:"preview"`
|
||||||
|
Size int `json:"size"`
|
||||||
|
Duration int `json:"duration"`
|
||||||
|
} `json:"assets"`
|
||||||
|
} `json:"offers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AutoGenerated struct {
|
||||||
|
Data []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Href string `json:"href"`
|
||||||
|
Attributes struct {
|
||||||
|
Artwork struct {
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
BgColor string `json:"bgColor"`
|
||||||
|
TextColor1 string `json:"textColor1"`
|
||||||
|
TextColor2 string `json:"textColor2"`
|
||||||
|
TextColor3 string `json:"textColor3"`
|
||||||
|
TextColor4 string `json:"textColor4"`
|
||||||
|
} `json:"artwork"`
|
||||||
|
ArtistName string `json:"artistName"`
|
||||||
|
IsSingle bool `json:"isSingle"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
IsComplete bool `json:"isComplete"`
|
||||||
|
GenreNames []string `json:"genreNames"`
|
||||||
|
TrackCount int `json:"trackCount"`
|
||||||
|
IsMasteredForItunes bool `json:"isMasteredForItunes"`
|
||||||
|
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
|
||||||
|
ContentRating string `json:"contentRating"`
|
||||||
|
ReleaseDate string `json:"releaseDate"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
RecordLabel string `json:"recordLabel"`
|
||||||
|
Upc string `json:"upc"`
|
||||||
|
AudioTraits []string `json:"audioTraits"`
|
||||||
|
Copyright string `json:"copyright"`
|
||||||
|
PlayParams struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
} `json:"playParams"`
|
||||||
|
IsCompilation bool `json:"isCompilation"`
|
||||||
|
EditorialVideo struct {
|
||||||
|
MotionDetailSquare struct {
|
||||||
|
Video string `json:"video"`
|
||||||
|
} `json:"motionDetailSquare"`
|
||||||
|
MotionSquareVideo1x1 struct {
|
||||||
|
Video string `json:"video"`
|
||||||
|
} `json:"motionSquareVideo1x1"`
|
||||||
|
} `json:"editorialVideo"`
|
||||||
|
} `json:"attributes"`
|
||||||
|
Relationships struct {
|
||||||
|
RecordLabels struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
Data []interface{} `json:"data"`
|
||||||
|
} `json:"record-labels"`
|
||||||
|
Artists struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
Data []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Href string `json:"href"`
|
||||||
|
Attributes struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Artwork struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
} `json:"artwork"`
|
||||||
|
} `json:"attributes"`
|
||||||
|
} `json:"data"`
|
||||||
|
} `json:"artists"`
|
||||||
|
Tracks struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
Next string `json:"next"`
|
||||||
|
Data []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Href string `json:"href"`
|
||||||
|
Attributes struct {
|
||||||
|
Previews []struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
} `json:"previews"`
|
||||||
|
Artwork struct {
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
BgColor string `json:"bgColor"`
|
||||||
|
TextColor1 string `json:"textColor1"`
|
||||||
|
TextColor2 string `json:"textColor2"`
|
||||||
|
TextColor3 string `json:"textColor3"`
|
||||||
|
TextColor4 string `json:"textColor4"`
|
||||||
|
} `json:"artwork"`
|
||||||
|
ArtistName string `json:"artistName"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
DiscNumber int `json:"discNumber"`
|
||||||
|
GenreNames []string `json:"genreNames"`
|
||||||
|
HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"`
|
||||||
|
IsMasteredForItunes bool `json:"isMasteredForItunes"`
|
||||||
|
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
|
||||||
|
ContentRating string `json:"contentRating"`
|
||||||
|
DurationInMillis int `json:"durationInMillis"`
|
||||||
|
ReleaseDate string `json:"releaseDate"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Isrc string `json:"isrc"`
|
||||||
|
AudioTraits []string `json:"audioTraits"`
|
||||||
|
HasLyrics bool `json:"hasLyrics"`
|
||||||
|
AlbumName string `json:"albumName"`
|
||||||
|
PlayParams struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
} `json:"playParams"`
|
||||||
|
TrackNumber int `json:"trackNumber"`
|
||||||
|
AudioLocale string `json:"audioLocale"`
|
||||||
|
ComposerName string `json:"composerName"`
|
||||||
|
} `json:"attributes"`
|
||||||
|
Relationships struct {
|
||||||
|
Artists struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
Data []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Href string `json:"href"`
|
||||||
|
Attributes struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"attributes"`
|
||||||
|
} `json:"data"`
|
||||||
|
} `json:"artists"`
|
||||||
|
} `json:"relationships"`
|
||||||
|
} `json:"data"`
|
||||||
|
} `json:"tracks"`
|
||||||
|
} `json:"relationships"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AutoGeneratedTrack struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
Next string `json:"next"`
|
||||||
|
Data []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Href string `json:"href"`
|
||||||
|
Attributes struct {
|
||||||
|
Previews []struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
} `json:"previews"`
|
||||||
|
Artwork struct {
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
BgColor string `json:"bgColor"`
|
||||||
|
TextColor1 string `json:"textColor1"`
|
||||||
|
TextColor2 string `json:"textColor2"`
|
||||||
|
TextColor3 string `json:"textColor3"`
|
||||||
|
TextColor4 string `json:"textColor4"`
|
||||||
|
} `json:"artwork"`
|
||||||
|
ArtistName string `json:"artistName"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
DiscNumber int `json:"discNumber"`
|
||||||
|
GenreNames []string `json:"genreNames"`
|
||||||
|
HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"`
|
||||||
|
IsMasteredForItunes bool `json:"isMasteredForItunes"`
|
||||||
|
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
|
||||||
|
ContentRating string `json:"contentRating"`
|
||||||
|
DurationInMillis int `json:"durationInMillis"`
|
||||||
|
ReleaseDate string `json:"releaseDate"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Isrc string `json:"isrc"`
|
||||||
|
AudioTraits []string `json:"audioTraits"`
|
||||||
|
HasLyrics bool `json:"hasLyrics"`
|
||||||
|
AlbumName string `json:"albumName"`
|
||||||
|
PlayParams struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
} `json:"playParams"`
|
||||||
|
TrackNumber int `json:"trackNumber"`
|
||||||
|
AudioLocale string `json:"audioLocale"`
|
||||||
|
ComposerName string `json:"composerName"`
|
||||||
|
} `json:"attributes"`
|
||||||
|
Relationships struct {
|
||||||
|
Artists struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
Data []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Href string `json:"href"`
|
||||||
|
Attributes struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"attributes"`
|
||||||
|
} `json:"data"`
|
||||||
|
} `json:"artists"`
|
||||||
|
} `json:"relationships"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AutoGeneratedArtist struct {
|
||||||
|
Next string `json:"next"`
|
||||||
|
Data []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Href string `json:"href"`
|
||||||
|
Attributes struct {
|
||||||
|
Previews []struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
} `json:"previews"`
|
||||||
|
Artwork struct {
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
BgColor string `json:"bgColor"`
|
||||||
|
TextColor1 string `json:"textColor1"`
|
||||||
|
TextColor2 string `json:"textColor2"`
|
||||||
|
TextColor3 string `json:"textColor3"`
|
||||||
|
TextColor4 string `json:"textColor4"`
|
||||||
|
} `json:"artwork"`
|
||||||
|
ArtistName string `json:"artistName"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
DiscNumber int `json:"discNumber"`
|
||||||
|
GenreNames []string `json:"genreNames"`
|
||||||
|
HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"`
|
||||||
|
IsMasteredForItunes bool `json:"isMasteredForItunes"`
|
||||||
|
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
|
||||||
|
ContentRating string `json:"contentRating"`
|
||||||
|
DurationInMillis int `json:"durationInMillis"`
|
||||||
|
ReleaseDate string `json:"releaseDate"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Isrc string `json:"isrc"`
|
||||||
|
AudioTraits []string `json:"audioTraits"`
|
||||||
|
HasLyrics bool `json:"hasLyrics"`
|
||||||
|
AlbumName string `json:"albumName"`
|
||||||
|
PlayParams struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
} `json:"playParams"`
|
||||||
|
TrackNumber int `json:"trackNumber"`
|
||||||
|
AudioLocale string `json:"audioLocale"`
|
||||||
|
ComposerName string `json:"composerName"`
|
||||||
|
} `json:"attributes"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SongLyrics struct {
|
||||||
|
Data []struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Attributes struct {
|
||||||
|
Ttml string `json:"ttml"`
|
||||||
|
PlayParams struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
CatalogId string `json:"catalogId"`
|
||||||
|
DisplayType int `json:"displayType"`
|
||||||
|
} `json:"playParams"`
|
||||||
|
} `json:"attributes"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user