Merge pull request #38 from itouakirai/main

增加边下边解密和adm-aac下载
This commit is contained in:
ZHAAREY
2025-01-13 00:42:36 +08:00
committed by GitHub
9 changed files with 2551 additions and 3125 deletions

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@
!go.sum
!main.go
!README.md
!utils/*

View File

@@ -2,40 +2,31 @@
### 添加功能
1. 调用外部MP4Box自动封装ec3为m4a
1. 调用外部MP4Box添加tag
2. 更改目录结构为 歌手名\专辑名 ;Atmos下载文件则另外移动到AM-DL-Atmos downloads并更改目录结构为 歌手名\专辑名 [Atmos]
3. 运行结束后显示总体完成情况
4. 自动内嵌封面和LRC歌词需要media-user-token获取方式看最后的说明
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
8. 文件夹和文件支持模板
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运行解密速度超快基本秒解
11. `limit-max`支持限制长度 默认200
12. 支持逐词与未同步歌词
13. 现已支持arm64解密
12. 现已支持arm64解密
13. 下载解密部分更换为Sendy McSenderson的代码实现边下载边解密
### Special thanks to `chocomint` for creating `agent-arm64.js`
本项目仅支持ALACAtmos
本项目仅支持ALACAtmos和苹果数字母带生成的AACadm-aac不支持仅有AAC的专辑only-aac及MV
- `alac (audio-alac-stereo)`
- `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-binaural (audio-stereo-binaural)`
- `aac-downmix (audio-stereo-downmix)`
# Apple Music ALAC / Dolby Atmos Downloader
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.
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`.
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)

View File

@@ -13,13 +13,13 @@ cover-size: 5000x5000
cover-format: jpg #jpg png or original
alac-save-folder: AM-DL downloads
atmos-save-folder: AM-DL-Atmos downloads
check: "" # API or .txt
force-api: false
max-memory-limit: 256 # MB
decrypt-m3u8-port: "127.0.0.1:10020"
get-m3u8-port: "127.0.0.1:20020"
get-m3u8-from-device: true
#set 'all' to retrieve all m3u8, and set 'hires' to only detect hires m3u8.
get-m3u8-mode: hires # all hires
aac-type: aac # aac aac-binaural aac-downmix
alac-max: 192000 #192000 96000 48000 44100
atmos-max: 2768 #2768 2448
limit-max: 200

5
go.mod
View File

@@ -3,15 +3,16 @@ module main
go 1.17
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/schollz/progressbar/v3 v3.14.6
github.com/spf13/pflag v1.0.5
)
require (
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // 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/term v0.22.0 // indirect
)

8
go.sum
View File

@@ -1,11 +1,15 @@
github.com/abema/go-mp4 v0.7.2 h1:ugTC8gfEmjyaDKpXs3vi2QzgJbDu9B8m6UMMIpbYbGg=
github.com/abema/go-mp4 v0.7.2/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=
github.com/Eyevinn/mp4ff v0.46.0 h1:A8oJA4A3C9fDbX38jEw/26utjNdvmRmrO37tVI5pDk0=
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/go.mod h1:aiPf89g/1k3AShMVAzriilpcE4R/Vuor90y83zVZWFc=
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=

1977
main.go

File diff suppressed because it is too large Load Diff

21
utils/runv2/LICENSE Normal file
View 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
View 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
View 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"`
}