Compare commits

..

73 Commits

Author SHA1 Message Date
ZHAAREY
7d6a02163c Merge pull request #88 from itouakirai/main 2025-10-22 08:36:23 +08:00
itouakirai
b48121296a fix: 不嵌入cover时,部分播放器无法播放 2025-10-22 08:20:38 +08:00
itouakirai
e065ab219c fix: artistcover panic 2025-10-22 00:59:58 +08:00
itouakirai
d6a616fef6 fix: mv下载失败时未能删除tmp文件 2025-10-18 17:44:13 +08:00
itouakirai
765983c959 fix: mv下载错误时未及时删除tmp文件 2025-10-18 17:36:18 +08:00
ZHAAREY
0d33a68dc0 Merge pull request #86 from itouakirai/main
fix: 部分aac下载失败
2025-10-01 00:01:00 +08:00
itouakirai
17486d221e fix:部分aac下载失败 2025-09-30 21:52:11 +08:00
itouakirai
9c6569e239 Merge branch 'zhaarey:main' into main 2025-09-30 20:03:39 +08:00
ZHAAREY
0ea4e077db Merge pull request #85 from itouakirai/patch-2
fix: 获取艺术家名称时未添加语言参数
2025-09-29 14:51:32 +08:00
itouakirai
848f0911d4 fix: 获取艺术家名称时未添加语言参数 2025-09-29 14:47:39 +08:00
ZHAAREY
fec52def2a Merge pull request #83 from lollilol/patch-1
added info where to paste
2025-09-29 14:35:18 +08:00
itouakirai
dffaa9fb7a update: itouakirai/mp4ff 2025-09-29 14:24:33 +08:00
zhaarey
656b2f9f45 fix 2025-09-29 14:17:37 +08:00
quinn
1a5f336015 added info where to paste
the media-user-token
this is also missing for the next paragraph (syllable-lyrics) and i don't know which config value i need to use
2025-09-27 23:27:32 +02:00
ZHAAREY
a13f98f04e Merge pull request #82 from itouakirai/main
fix:重建缺失的sample size来修复部分mdat未解密
2025-09-27 17:09:59 +08:00
itouakirai
04653a6cd0 fix:重建缺失的sample size来修复部分mdat未解密 2025-09-27 16:52:35 +08:00
ZHAAREY
f432884fe3 Merge pull request #81 from itouakirai/main
fix:部分stream类型的station下载失败
2025-09-23 00:10:20 +08:00
ZHAAREY
5ac4b29521 Merge pull request #80 from york9675/main
Add post-download conversion feature
2025-09-23 00:10:03 +08:00
York
da62f1772e Update track existence checks in ripTrack function 2025-09-22 12:54:07 +08:00
itouakirai
ab396c7721 fix:部分stream类型的station下载失败 2025-09-21 01:24:53 +08:00
York
90f97e0d14 Add post-download conversion feature 2025-09-20 13:46:30 +08:00
ZHAAREY
5578600359 Merge pull request #79 from itouakirai/main
fix:requests to resty
2025-09-18 08:07:23 +08:00
itouakirai
61da35b844 fix:requests to resty 2025-09-18 01:29:22 +08:00
zhaarey
98d37775a3 fix cant dl cover 2025-08-30 09:40:28 +08:00
zhaarey
d9db2222c5 Update main.go 2025-08-28 19:22:32 +08:00
ZHAAREY
2207604015 Merge pull request #70 from PurelyAndy/main
Fix album cover/artist image downloading in some broken cases
2025-08-28 19:19:32 +08:00
PurelyAndy
d2118dba7f Merge remote-tracking branch 'upstream/main' 2025-08-24 18:19:49 -04:00
ZHAAREY
4bd24bf537 Merge pull request #78 from NovaSeele/fix_dev_branch
Fix invalid type on Jp song like Jpop and Vocaloid
2025-08-15 15:24:34 +08:00
Nguyễn Hoàng Anh Dũng
d6fa4fcce3 Fix invalid type on Jp song like Jpop and Vocaloid 2025-08-10 17:14:39 +07:00
ZHAAREY
a652ee12b4 Merge pull request #77 from rwnk-12/feature/apple-music-search-download
More info about storefront in config
2025-08-08 19:27:24 +08:00
rwnk-12
3e7d4de4cf Update config.yaml 2025-08-08 12:39:10 +05:30
rwnk-12
e60c6b7b3d Update config.yaml 2025-08-08 12:28:52 +05:30
rwnk-12
5a67800701 info about storefront 2025-08-08 12:25:02 +05:30
zhaarey
795af53c36 update 2025-08-08 09:10:16 +08:00
ZHAAREY
b73c586755 Merge pull request #76 from rwnk-12/feature/apple-music-search-download
feat: Add interactive search with arrow-key navigation
2025-08-08 08:54:41 +08:00
rwnk-12
15d7d9548d Update config.yaml 2025-08-07 11:36:00 +05:30
raun1203
6402f3826f Merge remote changes into feature branch 2025-08-07 11:25:32 +05:30
raun1203
fd227343f2 fix: Add survey dependency and update go.sum 2025-08-07 11:19:02 +05:30
rwnk-12
3a0090d78b Create search.go 2025-08-07 08:21:28 +05:30
raun1203
bc243557b2 feat: Add interactive search with arrow-key navigation 2025-08-07 08:13:55 +05:30
ZHAAREY
63e9e16085 Merge pull request #75 from HorrorTroll/pronoun
Added support for pronunciation lyrics
2025-08-06 17:14:09 +08:00
ZHAAREY
577bec067f Merge pull request #74 from BondingforToday/main
Option All Songs for All Artists
2025-08-06 17:13:58 +08:00
HorrorTroll
3445b440a8 Added support for pronunciation lyrics feature 2025-08-05 22:36:38 +07:00
Bonding for Today™️
c913087db8 Update README.md 2025-08-04 23:48:06 +02:00
Bonding for Today™️
329def738f Update README.md
Test
2025-08-04 23:44:55 +02:00
ZHAAREY
c6f5748259 Merge pull request #73 from itouakirai/main
fix: lyrics panic
2025-08-03 23:42:18 +08:00
itouakirai
aa08ebc0f8 fix: lyrics panic 2025-08-03 20:55:01 +08:00
ZHAAREY
4a4d3c993f Merge pull request #72 from rahulhingve/main
Add Separate Folder Support for AAC Downloads
2025-07-23 22:28:04 +08:00
rahulhingve
3f173c1187 Add AAC folder support for downloads 2025-07-19 19:11:02 +05:30
ZHAAREY
a4b4ee17b3 Merge pull request #71 from itouakirai/main
代码重构,mv下载速度优化,station下载支持,翻译歌词获取
2025-07-16 20:48:05 +08:00
ZHAAREY
480acb7d7c Merge branch 'main' into main 2025-07-16 20:45:45 +08:00
itouakirai
040716e93a fix:歌词行末尾时间错误 2025-07-12 23:29:44 +08:00
itouakirai
be89d9e4c1 暂时移除lrc的背景人声翻译 2025-07-12 18:50:10 +08:00
itouakirai
db3f537e9e beta: Get translated lyrics 2025-07-12 18:07:29 +08:00
itouakirai
85c1c9fc60 Enhanced LRC support by @AAGaming00 2025-07-12 06:47:37 +08:00
itouakirai
b6bc55ce1c Classcial link support 2025-07-12 06:39:07 +08:00
itouakirai
beb95b8730 fix: unable to collect information about albums with more than 300 tracks 2025-07-12 06:32:17 +08:00
itouakirai
421db91731 更改mv下载逻辑 2025-07-12 05:37:12 +08:00
PurelyAndy
bfe1b913c2 Fix album cover/artist image downloading in some broken cases 2025-07-02 05:41:24 -04:00
ZHAAREY
a086da99c0 Merge pull request #68 from MDSVJ/main
Enhanced LRC support by @AAGaming00
2025-06-24 19:19:44 +08:00
ZHAAREY
2ee7b50082 Merge pull request #69 from WorldObservationLog/main
fix: unable to collect information about albums with more than 300 tracks
2025-06-24 19:15:56 +08:00
世界观察日志
5d25210432 fix: unable to collect information about albums with more than 300 tracks 2025-06-24 18:43:41 +08:00
MDSVJ
6940c82d70 Enhanced LRC support by @AAGaming00 2025-06-15 23:46:26 -07:00
itouakirai
7c590213ef fix:播放列表offset获取失败 2025-03-06 12:11:42 +08:00
itouakirai
65172a3d3d fix: 下载协程阻塞 2025-03-06 08:38:15 +08:00
itouakirai
8f284cadea fix: mv tag panic,artist cover miss 2025-03-05 07:44:27 +08:00
itouakirai
56d37ccce7 fix: herf string 2025-03-04 14:54:00 +08:00
itouakirai
c9ecc16a3b style: 显示优化 2025-03-04 13:45:48 +08:00
itouakirai
7dcd164885 opt: mv and radio dl speed 2025-03-04 04:37:27 +08:00
itouakirai
be1467340d fix: cover embed 2025-03-03 20:36:33 +08:00
itouakirai
acb5a7ce1e add: station dl(need media-user-token) 2025-03-03 08:52:26 +08:00
itouakirai
21728cabb6 test: station dl (need media-user-token) 2025-03-03 03:37:57 +08:00
itouakirai
b2bcdfde88 test: 重构 2025-03-02 23:44:05 +08:00
23 changed files with 4888 additions and 3130 deletions

View File

@@ -9,6 +9,7 @@ English / [简体中文](./README-CN.md)
3. Support downloading singers `go run main.go https://music.apple.com/us/artist/taylor-swift/159260351` `--all-album` Automatically select all albums of the artist
4. The download decryption part is replaced with Sendy McSenderson to decrypt while downloading, and solve the lack of memory when decrypting large files
5. MV Download, installation required[mp4decrypt](https://www.bento4.com/downloads/)
6. Add interactive search with arrow-key navigation `go run main.go --search [song/album/artist] "search_term"`
### Special thanks to `chocomint` for creating `agent-arm64.js`
@@ -43,5 +44,21 @@ Original script by Sorrow. Modified by me to include some fixes and improvements
1. Open [Apple Music](https://music.apple.com) and log in
2. Open the Developer tools, Click `Application -> Storage -> Cookies -> https://music.apple.com`
3. Find the cookie named `media-user-token` and copy its value
4. Paste the cookie value obtained in step 3 into the config.yaml and save it
4. Paste the cookie value obtained in step 3 into the setting called "media-user-token" in config.yaml and save it
5. Start the script as usual
## Get translation and pronunciation lyrics (Beta)
1. Open [Apple Music](https://beta.music.apple.com) and log in.
2. Open the Developer tools, click `Network` tab.
3. Search a song which is available for translation and pronunciation lyrics (recommend K-Pop songs).
4. Press Ctrl+R and let Developer tools sniff network data.
5. Play a song and then click lyric button, sniff will show a data called `syllable-lyrics`.
6. Stop sniff (small red circles button on top left), then click `Fetch/XHR` tabs.
7. Click `syllable-lyrics` data, see requested URL.
8. Find this line `.../syllable-lyrics?l=<copy all the language value from here>&extend=ttmlLocalizations`.
9. Paste the language value obtained in step 8 into the config.yaml and save it.
10. If don't need pronunciation, do this `...%5D=<remove this value>&extend...` on config.yaml and save it.
11. Start the script as usual.
Noted: These features are only in beta version right now.

View File

@@ -13,6 +13,7 @@ cover-size: 5000x5000
cover-format: jpg #jpg png or original
alac-save-folder: AM-DL downloads
atmos-save-folder: AM-DL-Atmos downloads
aac-save-folder: AM-DL-AAC downloads
max-memory-limit: 256 # MB
decrypt-m3u8-port: "127.0.0.1:10020"
get-m3u8-port: "127.0.0.1:20020"
@@ -44,3 +45,15 @@ use-songinfo-for-playlist: false
dl-albumcover-for-playlist: false
mv-audio-type: atmos #atmos ac3 aac
mv-max: 2160
# storefront will be used only in searching.
# storefront is the 2-letter country code that are available in the urls (jp, ca, us etc.).
# if your account is from Japan, you must use jp.
# if the storefront is different from your account, you will see a "failed to get lyrics" error in most of the songs. By default the storefront is set to US if not set.
storefront: "enter your account storefront"
# Conversion settings
convert-after-download: false # Enable post-download conversion (requires ffmpeg)
convert-format: "flac" # flac | mp3 | opus | wav | copy (no re-encode)
convert-keep-original: false # Keep original file after successful conversion
convert-skip-if-source-matches: true # If already in target format, skip
ffmpeg-path: "ffmpeg" # Override if ffmpeg is not in PATH
convert-extra-args: "" # Additional raw args appended (advanced)

86
go.mod
View File

@@ -3,9 +3,7 @@ module main
go 1.23.1
require (
github.com/Eyevinn/mp4ff v0.46.0
github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1
github.com/gospider007/requests v0.0.0-20250114011338-9562a203fa04
github.com/grafov/m3u8 v0.11.1
github.com/schollz/progressbar/v3 v3.14.6
github.com/spf13/pflag v1.0.5
@@ -14,90 +12,34 @@ require (
)
require (
github.com/PuerkitoBio/goquery v1.10.1 // indirect
github.com/STARRY-S/zip v0.2.1 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.0 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/caddyserver/certmagic v0.21.5 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/cloudflare/circl v1.5.0 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/gaukas/clienthellod v0.4.2 // indirect
github.com/gaukas/godicttls v0.0.4 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
github.com/gospider007/bar v0.0.0-20241205091858-06f3c7e16dd9 // indirect
github.com/gospider007/blog v0.0.0-20241205091827-6bcaf48620d4 // indirect
github.com/gospider007/bs4 v0.0.0-20241216122612-aa3f29179e6f // indirect
github.com/gospider007/gson v0.0.0-20241216122450-fb66d0ba2a07 // indirect
github.com/gospider007/gtls v0.0.0-20250107100054-2a14fa6fc9c5 // indirect
github.com/gospider007/http2 v0.0.0-20250107095809-0f8c60040d4a // indirect
github.com/gospider007/http3 v0.0.0-20250107095941-a36b210aca37 // indirect
github.com/gospider007/ja3 v0.0.0-20250107091445-e294e50de314 // indirect
github.com/gospider007/kinds v0.0.0-20240929092451-8f867acde255 // indirect
github.com/gospider007/re v0.0.0-20241216142712-efbef8d55ea2 // indirect
github.com/gospider007/tools v0.0.0-20250107090942-5b406ab8a72b // indirect
github.com/gospider007/websocket v0.0.0-20250107091829-c78035a103b2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/libdns/libdns v0.2.2 // indirect
github.com/Eyevinn/mp4ff v0.50.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/mholt/acmez/v3 v3.0.0 // indirect
github.com/mholt/archives v0.1.0 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nwaples/rardecode/v2 v2.0.1 // indirect
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.48.2 // indirect
github.com/refraction-networking/uquic v0.0.6 // indirect
github.com/refraction-networking/utls v1.6.7 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sorairolake/lzip-go v0.3.5 // indirect
github.com/therootcompany/xz v1.0.1 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.mongodb.org/mongo-driver v1.17.2 // indirect
go.uber.org/mock v0.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
golang.org/x/image v0.23.0 // indirect
golang.org/x/mod v0.22.0 // indirect
github.com/stretchr/testify v1.10.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/term v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/tools v0.29.0 // indirect
golang.org/x/time v0.8.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)
require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/beevik/etree v1.3.0
github.com/fatih/color v1.18.0
github.com/go-resty/resty/v2 v2.16.5
github.com/itouakirai/mp4ff v0.0.0-20250930132656-98812935a1c7
github.com/olekukonko/tablewriter v0.0.5
github.com/zhaarey/go-mp4tag v0.0.0-20250210094042-22578afc09bf
github.com/zhaarey/go-mp4tag v0.0.0-20251021234435-2c70f6b1bf76
gopkg.in/yaml.v2 v2.2.8
)

441
go.sum
View File

@@ -1,510 +1,123 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Eyevinn/mp4ff v0.46.0 h1:A8oJA4A3C9fDbX38jEw/26utjNdvmRmrO37tVI5pDk0=
github.com/Eyevinn/mp4ff v0.46.0/go.mod h1:hJNUUqOBryLAzUW9wpCJyw2HaI+TCd2rUPhafoS5lgg=
github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU=
github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY=
github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg=
github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/Eyevinn/mp4ff v0.50.0 h1:vFlsvpQh5Jfz++cuaeTI90vbID5dAabebvvN/l9lom0=
github.com/Eyevinn/mp4ff v0.50.0/go.mod h1:hJNUUqOBryLAzUW9wpCJyw2HaI+TCd2rUPhafoS5lgg=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1 h1:+JkXLHME8vLJafGhOH4aoV2Iu8bR55nU6iKMVfYVLjY=
github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1/go.mod h1:nuudZmJhzWtx2212z+pkuy7B6nkBqa+xwNXZHL1j8cg=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
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/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
github.com/bodgit/sevenzip v1.6.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A=
github.com/bodgit/sevenzip v1.6.0/go.mod h1:zOBh9nJUof7tcrlqJFv1koWRrhz3LbDbUNngkuZxLMc=
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/caddyserver/certmagic v0.21.5 h1:iIga4nZRgd27EIEbX7RZmoRMul+EVBn/h7bAGL83dnY=
github.com/caddyserver/certmagic v0.21.5/go.mod h1:n1sCo7zV1Ez2j+89wrzDxo4N/T1Ws/Vx8u5NvuBFabw=
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/gaukas/clienthellod v0.4.2 h1:LPJ+LSeqt99pqeCV4C0cllk+pyWmERisP7w6qWr7eqE=
github.com/gaukas/clienthellod v0.4.2/go.mod h1:M57+dsu0ZScvmdnNxaxsDPM46WhSEdPYAOdNgfL7IKA=
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gospider007/bar v0.0.0-20241205091858-06f3c7e16dd9 h1:mKDRxvZNftY8msmyonqGLlTAbCUYTJNYbX5Ha0REwzs=
github.com/gospider007/bar v0.0.0-20241205091858-06f3c7e16dd9/go.mod h1:dZ50Fil3qVmYCGGR9hne3RwgZRfZj+NZQE7NaKBNdPI=
github.com/gospider007/blog v0.0.0-20241205091827-6bcaf48620d4 h1:MA0vcRGiy5JPlQYPU9wg7reG4dZkW3v5Q/DVtAUJ6yQ=
github.com/gospider007/blog v0.0.0-20241205091827-6bcaf48620d4/go.mod h1:y7eLYgD+hFH2v8qrlrO5KtFTGwYxQaQuWyGAuZh7QHU=
github.com/gospider007/bs4 v0.0.0-20241216122612-aa3f29179e6f h1:/pbT94pif600rZwdOKPi5VaZ6D2pdnQuBSmiqOzRkSA=
github.com/gospider007/bs4 v0.0.0-20241216122612-aa3f29179e6f/go.mod h1:M3XPdOCrwkB1UJiaF506o8KCDpRKnkzZBQ5PWitVc90=
github.com/gospider007/gson v0.0.0-20241216122450-fb66d0ba2a07 h1:cqsuxxIKxbRG/xRPNwgFUfvGKc992snPjn5WlXkCtnA=
github.com/gospider007/gson v0.0.0-20241216122450-fb66d0ba2a07/go.mod h1:XcFiXq3t2+b1x9zezQV/FbuB5Y1rdUrtvpJRbxZXef4=
github.com/gospider007/gtls v0.0.0-20250107100054-2a14fa6fc9c5 h1:c0payCC+agt1c+SK66VYXcs8eFktGlxWhK+t6V/6PPc=
github.com/gospider007/gtls v0.0.0-20250107100054-2a14fa6fc9c5/go.mod h1:qIlB6X0WLv9QVqo/LLqkml0JDKSjeL3x4tI1QCINU94=
github.com/gospider007/http2 v0.0.0-20250107095809-0f8c60040d4a h1:Gx/aemk7BFyjguVXWS9oxNHjhmcz67gtGNbNyVaahOE=
github.com/gospider007/http2 v0.0.0-20250107095809-0f8c60040d4a/go.mod h1:3uzGktvy4KnYZm+DwWNui4RsvPUQSSphJ1VTtfO1tE0=
github.com/gospider007/http3 v0.0.0-20250107095941-a36b210aca37 h1:i9WhiyjKwaU2imUof0i95WhcGCx8HHTTCnkHzEIlWq8=
github.com/gospider007/http3 v0.0.0-20250107095941-a36b210aca37/go.mod h1:6zmeD45uQFfHLWTsgAUP4FXgdj6MUTGyQar091dTDtw=
github.com/gospider007/ja3 v0.0.0-20250107091445-e294e50de314 h1:HAdIVngGI5Hj7tSF5nV0YCze+G24ck+9qGf5H/VT4hw=
github.com/gospider007/ja3 v0.0.0-20250107091445-e294e50de314/go.mod h1:LyhufzO6wrBlxeilbUJElfEdDNdD+1v9fB5p30pDAjE=
github.com/gospider007/kinds v0.0.0-20240929092451-8f867acde255 h1:X+AM/mgmh/EfyQUjKZp1VFc9TSlrhkwS0eSYeo5fMs4=
github.com/gospider007/kinds v0.0.0-20240929092451-8f867acde255/go.mod h1:yZx7Zfp1I4P6CO3TcDyDY5SuXQYr0bZjzT9zG0XrJAI=
github.com/gospider007/re v0.0.0-20241216142712-efbef8d55ea2 h1:ixXFS1DqP0NnHna+b0JKaPqMRYRmahzUADZn7PawQq0=
github.com/gospider007/re v0.0.0-20241216142712-efbef8d55ea2/go.mod h1:kr9bUaC42FS019Ak23fSctbTRB2JpfPPg/pSVjQmsws=
github.com/gospider007/requests v0.0.0-20250114011338-9562a203fa04 h1:QgWCKic6UawnAIGdfGEe2M9cjvaXKwBL/zU3LCQkki4=
github.com/gospider007/requests v0.0.0-20250114011338-9562a203fa04/go.mod h1:nWEyJT8cCQAJr+b3zPoaYuOo1z3YJT6SNz4cfi8Y1hc=
github.com/gospider007/tools v0.0.0-20250107090942-5b406ab8a72b h1:iiAxiufROgDsuQInI6dAx1CG3m7nMMj3C0SAE84oY7g=
github.com/gospider007/tools v0.0.0-20250107090942-5b406ab8a72b/go.mod h1:mTtocamaDuVtgdEXu72OGw6uOhDIOCr98zcvZyBLiro=
github.com/gospider007/websocket v0.0.0-20250107091829-c78035a103b2 h1:hp8IZHhmq4tAkJRFZ3QXRoRwwNyvAeYyqecV/JvFPYc=
github.com/gospider007/websocket v0.0.0-20250107091829-c78035a103b2/go.mod h1:0DHN5ImV48fYE0NkMKynEQLTo+CxMUMCqvIMUE8qUQI=
github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/itouakirai/mp4ff v0.0.0-20250930132656-98812935a1c7 h1:KhMNPs/FW9fsiei6qD8h07ArJqYjvpIjk1zqeyv06Xw=
github.com/itouakirai/mp4ff v0.0.0-20250930132656-98812935a1c7/go.mod h1:epcaTcHNf0fgnK8d2MP/gPQNji50kvZa9ZxzX06UOoA=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mholt/acmez/v3 v3.0.0 h1:r1NcjuWR0VaKP2BTjDK9LRFBw/WvURx3jlaEUl9Ht8E=
github.com/mholt/acmez/v3 v3.0.0/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/mholt/archives v0.1.0 h1:FacgJyrjiuyomTuNA92X5GyRBRZjE43Y/lrzKIlF35Q=
github.com/mholt/archives v0.1.0/go.mod h1:j/Ire/jm42GN7h90F5kzj6hf6ZFzEH66de+hmjEKu+I=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nwaples/rardecode/v2 v2.0.1 h1:3MN6/R+Y4c7e+21U3yhWuUcf72sYmcmr6jtiuAVSH1A=
github.com/nwaples/rardecode/v2 v2.0.1/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
github.com/refraction-networking/uquic v0.0.6 h1:9ol1oOaOpHDeeDlBY7u228jK+T5oic35QrFimHVaCMM=
github.com/refraction-networking/uquic v0.0.6/go.mod h1:TFgTmV/yqVCMEXVwP7z7PMAhzye02rFHLV6cRAg59jc=
github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/schollz/progressbar/v3 v3.14.6 h1:GyjwcWBAf+GFDMLziwerKvpuS7ZF+mNTAXIB2aspiZs=
github.com/schollz/progressbar/v3 v3.14.6/go.mod h1:Nrzpuw3Nl0srLY0VlTvC4V6RL50pcEymjy6qyJAaLa0=
github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg=
github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
github.com/zhaarey/go-mp4tag v0.0.0-20250210094042-22578afc09bf h1:WzZoh9wvukQu2We8dw/bFmLfb5XsC5bGGU/Izhd/UOo=
github.com/zhaarey/go-mp4tag v0.0.0-20250210094042-22578afc09bf/go.mod h1:cqL6le//aG0AE1/VE1um2m+8dKa8te/WhHWqzrHMDys=
go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM=
go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
github.com/zhaarey/go-mp4tag v0.0.0-20251021234435-2c70f6b1bf76 h1:ON+3W/tNJ6Hujez1ITh9cy3RpFUfLg3NKuKb2PJBg8Q=
github.com/zhaarey/go-mp4tag v0.0.0-20251021234435-2c70f6b1bf76/go.mod h1:cqL6le//aG0AE1/VE1um2m+8dKa8te/WhHWqzrHMDys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
lukechampine.com/frand v1.5.1 h1:fg0eRtdmGFIxhP5zQJzM1lFDbD6CUfu/f+7WgAZd5/w=
lukechampine.com/frand v1.5.1/go.mod h1:4VstaWc2plN4Mjr10chUD46RAVGWhpkZ5Nja8+Azp0Q=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

4479
main.go

File diff suppressed because it is too large Load Diff

243
utils/ampapi/album.go Normal file
View File

@@ -0,0 +1,243 @@
package ampapi
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
)
func GetAlbumResp(storefront string, id string, language string, token string) (*AlbumResp, error) {
var err error
if token == "" {
token, err = GetToken()
if err != nil {
return nil, err
}
}
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/albums/%s", storefront, id), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Origin", "https://music.apple.com")
query := url.Values{}
query.Set("omit[resource]", "autos")
query.Set("include", "tracks,artists,record-labels")
query.Set("include[songs]", "artists")
//query.Set("fields[artists]", "name,artwork")
//query.Set("fields[albums:albums]", "artistName,artwork,name,releaseDate,url")
//query.Set("fields[record-labels]", "name")
query.Set("extend", "editorialVideo,extendedAssetUrls")
query.Set("l", language)
req.URL.RawQuery = query.Encode()
do, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer do.Body.Close()
if do.StatusCode != http.StatusOK {
return nil, errors.New(do.Status)
}
obj := new(AlbumResp)
err = json.NewDecoder(do.Body).Decode(&obj)
if err != nil {
return nil, err
}
if len(obj.Data[0].Relationships.Tracks.Next) > 0 {
next := obj.Data[0].Relationships.Tracks.Next
for {
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com%s", next), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Origin", "https://music.apple.com")
query := req.URL.Query()
query.Set("omit[resource]", "autos")
query.Set("include", "artists")
query.Set("extend", "editorialVideo,extendedAssetUrls")
req.URL.RawQuery = query.Encode()
do, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer do.Body.Close()
if do.StatusCode != http.StatusOK {
return nil, errors.New(do.Status)
}
obj2 := new(TrackResp)
err = json.NewDecoder(do.Body).Decode(&obj2)
if err != nil {
return nil, err
}
obj.Data[0].Relationships.Tracks.Data = append(obj.Data[0].Relationships.Tracks.Data, obj2.Data...)
next = obj2.Next
if len(next) == 0 {
break
}
}
}
return obj, nil
}
func GetAlbumRespByHref(href string, language string, token string) (*AlbumResp, error) {
var err error
if token == "" {
token, err = GetToken()
if err != nil {
return nil, err
}
}
href = strings.Split(href, "?")[0]
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com%s/albums", href), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Origin", "https://music.apple.com")
query := url.Values{}
query.Set("omit[resource]", "autos")
query.Set("include", "tracks,artists,record-labels")
query.Set("include[songs]", "artists")
//query.Set("fields[artists]", "name,artwork")
//query.Set("fields[albums:albums]", "artistName,artwork,name,releaseDate,url")
//query.Set("fields[record-labels]", "name")
query.Set("extend", "editorialVideo,extendedAssetUrls")
query.Set("l", language)
req.URL.RawQuery = query.Encode()
do, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer do.Body.Close()
if do.StatusCode != http.StatusOK {
return nil, errors.New(do.Status)
}
obj := new(AlbumResp)
err = json.NewDecoder(do.Body).Decode(&obj)
if err != nil {
return nil, err
}
if len(obj.Data[0].Relationships.Tracks.Next) > 0 {
next := obj.Data[0].Relationships.Tracks.Next
for {
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com%s", next), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Origin", "https://music.apple.com")
query := req.URL.Query()
query.Set("omit[resource]", "autos")
query.Set("include", "artists")
query.Set("extend", "editorialVideo,extendedAssetUrls")
req.URL.RawQuery = query.Encode()
do, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer do.Body.Close()
if do.StatusCode != http.StatusOK {
return nil, errors.New(do.Status)
}
obj2 := new(TrackResp)
err = json.NewDecoder(do.Body).Decode(&obj2)
if err != nil {
return nil, err
}
obj.Data[0].Relationships.Tracks.Data = append(obj.Data[0].Relationships.Tracks.Data, obj2.Data...)
next = obj2.Next
if len(next) == 0 {
break
}
}
}
return obj, nil
}
type AlbumResp struct {
Href string `json:"href"`
Next string `json:"next"`
Data []AlbumRespData `json:"data"`
}
type AlbumRespData 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 {
MotionTall struct {
Video string `json:"video"`
} `json:"motionTallVideo3x4"`
MotionSquare struct {
Video string `json:"video"`
} `json:"motionSquareVideo1x1"`
MotionDetailTall struct {
Video string `json:"video"`
} `json:"motionDetailTall"`
MotionDetailSquare struct {
Video string `json:"video"`
} `json:"motionDetailSquare"`
} `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 TrackResp `json:"tracks"`
} `json:"relationships"`
}

1
utils/ampapi/artist.go Normal file
View File

@@ -0,0 +1 @@
package ampapi

145
utils/ampapi/musicvideo.go Normal file
View File

@@ -0,0 +1,145 @@
package ampapi
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
)
func GetMusicVideoResp(storefront string, id string, language string, token string) (*MusicVideoResp, error) {
var err error
if token == "" {
token, err = GetToken()
if err != nil {
return nil, err
}
}
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/music-videos/%s", storefront, id), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Origin", "https://music.apple.com")
query := url.Values{}
//query.Set("omit[resource]", "autos")
query.Set("include", "albums,artists")
//query.Set("extend", "extendedAssetUrls")
//query.Set("include[songs]", "artists")
//query.Set("fields[artists]", "name,artwork")
//query.Set("fields[albums:albums]", "artistName,artwork,name,releaseDate,url")
//query.Set("fields[record-labels]", "name")
//query.Set("extend", "editorialVideo")
query.Set("l", language)
req.URL.RawQuery = query.Encode()
do, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer do.Body.Close()
if do.StatusCode != http.StatusOK {
return nil, errors.New(do.Status)
}
obj := new(MusicVideoResp)
err = json.NewDecoder(do.Body).Decode(&obj)
if err != nil {
return nil, err
}
return obj, nil
}
type MusicVideoResp struct {
Href string `json:"href"`
Next string `json:"next"`
Data []MusicVideoRespData `json:"data"`
}
type MusicVideoRespData 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"`
AlbumName string `json:"albumName"`
ArtistName string `json:"artistName"`
URL string `json:"url"`
GenreNames []string `json:"genreNames"`
DurationInMillis int `json:"durationInMillis"`
Isrc string `json:"isrc"`
TrackNumber int `json:"trackNumber"`
DiscNumber int `json:"discNumber"`
ContentRating string `json:"contentRating"`
ReleaseDate string `json:"releaseDate"`
Name string `json:"name"`
Has4K bool `json:"has4K"`
HasHDR bool `json:"hasHDR"`
PlayParams struct {
ID string `json:"id"`
Kind string `json:"kind"`
} `json:"playParams"`
} `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"`
Albums struct {
Href string `json:"href"`
Data []struct {
ID string `json:"id"`
Type string `json:"type"`
Href string `json:"href"`
Attributes struct {
ArtistName string `json:"artistName"`
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"`
GenreNames []string `json:"genreNames"`
IsCompilation bool `json:"isCompilation"`
IsComplete bool `json:"isComplete"`
IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsPrerelease bool `json:"isPrerelease"`
IsSingle bool `json:"isSingle"`
Name string `json:"name"`
PlayParams struct {
ID string `json:"id"`
Kind string `json:"kind"`
} `json:"playParams"`
ReleaseDate string `json:"releaseDate"`
TrackCount int `json:"trackCount"`
Upc string `json:"upc"`
URL string `json:"url"`
} `json:"attributes"`
} `json:"data"`
} `json:"albums"`
} `json:"relationships"`
}

165
utils/ampapi/playlist.go Normal file
View File

@@ -0,0 +1,165 @@
package ampapi
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
)
func GetPlaylistResp(storefront string, id string, language string, token string) (*PlaylistResp, error) {
var err error
if token == "" {
token, err = GetToken()
if err != nil {
return nil, err
}
}
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/playlists/%s", storefront, id), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Origin", "https://music.apple.com")
query := url.Values{}
query.Set("omit[resource]", "autos")
query.Set("include", "tracks,artists,record-labels")
query.Set("include[songs]", "artists")
//query.Set("fields[artists]", "name,artwork")
//query.Set("fields[albums:albums]", "artistName,artwork,name,releaseDate,url")
//query.Set("fields[record-labels]", "name")
query.Set("extend", "editorialVideo,extendedAssetUrls")
query.Set("l", language)
req.URL.RawQuery = query.Encode()
do, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer do.Body.Close()
if do.StatusCode != http.StatusOK {
return nil, errors.New(do.Status)
}
obj := new(PlaylistResp)
err = json.NewDecoder(do.Body).Decode(&obj)
if err != nil {
return nil, err
}
if len(obj.Data[0].Relationships.Tracks.Next) > 0 {
next := obj.Data[0].Relationships.Tracks.Next
for {
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com%s", next), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Origin", "https://music.apple.com")
query := req.URL.Query()
query.Set("omit[resource]", "autos")
query.Set("include", "artists")
query.Set("extend", "editorialVideo,extendedAssetUrls")
req.URL.RawQuery = query.Encode()
do, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer do.Body.Close()
if do.StatusCode != http.StatusOK {
return nil, errors.New(do.Status)
}
obj2 := new(TrackResp)
err = json.NewDecoder(do.Body).Decode(&obj2)
if err != nil {
return nil, err
}
obj.Data[0].Relationships.Tracks.Data = append(obj.Data[0].Relationships.Tracks.Data, obj2.Data...)
next = obj2.Next
if len(next) == 0 {
break
}
}
}
return obj, nil
}
type PlaylistResp struct {
Href string `json:"href"`
Next string `json:"next"`
Data []PlaylistRespData `json:"data"`
}
type PlaylistRespData 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 {
MotionTall struct {
Video string `json:"video"`
} `json:"motionTallVideo3x4"`
MotionSquare struct {
Video string `json:"video"`
} `json:"motionSquareVideo1x1"`
MotionDetailTall struct {
Video string `json:"video"`
} `json:"motionDetailTall"`
MotionDetailSquare struct {
Video string `json:"video"`
} `json:"motionDetailSquare"`
} `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 TrackResp `json:"tracks"`
} `json:"relationships"`
}

95
utils/ampapi/search.go Normal file
View File

@@ -0,0 +1,95 @@
package ampapi
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
)
// SearchResp represents the top-level response from the search API.
type SearchResp struct {
Results SearchResults `json:"results"`
}
// SearchResults contains the different types of search results.
type SearchResults struct {
Songs *SongResults `json:"songs,omitempty"`
Albums *AlbumResults `json:"albums,omitempty"`
Artists *ArtistResults `json:"artists,omitempty"`
}
// SongResults contains a list of song search results.
type SongResults struct {
Href string `json:"href"`
Next string `json:"next"`
Data []SongRespData `json:"data"`
}
// AlbumResults contains a list of album search results.
type AlbumResults struct {
Href string `json:"href"`
Next string `json:"next"`
Data []AlbumRespData `json:"data"`
}
// ArtistResults contains a list of artist search results.
type ArtistResults 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 {
Name string `json:"name"`
GenreNames []string `json:"genreNames"`
URL string `json:"url"`
} `json:"attributes"`
} `json:"data"`
}
// Search performs a search query against the Apple Music API.
func Search(storefront, term, types, language, token string, limit, offset int) (*SearchResp, error) {
var err error
if token == "" {
token, err = GetToken()
if err != nil {
return nil, err
}
}
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/search", storefront), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Origin", "https://music.apple.com")
query := url.Values{}
query.Set("term", term)
query.Set("types", types)
query.Set("limit", fmt.Sprintf("%d", limit))
query.Set("offset", fmt.Sprintf("%d", offset))
query.Set("l", language)
req.URL.RawQuery = query.Encode()
do, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer do.Body.Close()
if do.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API request failed with status: %s", do.Status)
}
obj := new(SearchResp)
err = json.NewDecoder(do.Body).Decode(&obj)
if err != nil {
return nil, err
}
return obj, nil
}

153
utils/ampapi/song.go Normal file
View File

@@ -0,0 +1,153 @@
package ampapi
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
)
func GetSongResp(storefront string, id string, language string, token string) (*SongResp, error) {
var err error
if token == "" {
token, err = GetToken()
if err != nil {
return nil, err
}
}
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/songs/%s", storefront, id), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Origin", "https://music.apple.com")
query := url.Values{}
//query.Set("omit[resource]", "autos")
query.Set("include", "albums,artists")
query.Set("extend", "extendedAssetUrls")
//query.Set("include[songs]", "artists")
//query.Set("fields[artists]", "name,artwork")
//query.Set("fields[albums:albums]", "artistName,artwork,name,releaseDate,url")
//query.Set("fields[record-labels]", "name")
//query.Set("extend", "editorialVideo")
query.Set("l", language)
req.URL.RawQuery = query.Encode()
do, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer do.Body.Close()
if do.StatusCode != http.StatusOK {
return nil, errors.New(do.Status)
}
obj := new(SongResp)
err = json.NewDecoder(do.Body).Decode(&obj)
if err != nil {
return nil, err
}
return obj, nil
}
type SongResp struct {
Href string `json:"href"`
Next string `json:"next"`
Data []SongRespData `json:"data"`
}
type SongRespData 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"`
ExtendedAssetUrls struct {
EnhancedHls string `json:"enhancedHls"`
} `json:"extendedAssetUrls"`
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"`
Albums struct {
Href string `json:"href"`
Data []struct {
ID string `json:"id"`
Type string `json:"type"`
Href string `json:"href"`
Attributes struct {
ArtistName string `json:"artistName"`
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"`
GenreNames []string `json:"genreNames"`
IsCompilation bool `json:"isCompilation"`
IsComplete bool `json:"isComplete"`
IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsPrerelease bool `json:"isPrerelease"`
IsSingle bool `json:"isSingle"`
Name string `json:"name"`
PlayParams struct {
ID string `json:"id"`
Kind string `json:"kind"`
} `json:"playParams"`
ReleaseDate string `json:"releaseDate"`
TrackCount int `json:"trackCount"`
Upc string `json:"upc"`
URL string `json:"url"`
} `json:"attributes"`
} `json:"data"`
} `json:"albums"`
} `json:"relationships"`
}

185
utils/ampapi/station.go Normal file
View File

@@ -0,0 +1,185 @@
package ampapi
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
)
func GetStationResp(storefront string, id string, language string, token string) (*StationResp, error) {
var err error
if token == "" {
token, err = GetToken()
if err != nil {
return nil, err
}
}
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/stations/%s", storefront, id), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Origin", "https://music.apple.com")
query := url.Values{}
query.Set("omit[resource]", "autos")
query.Set("extend", "editorialVideo")
query.Set("l", language)
req.URL.RawQuery = query.Encode()
do, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer do.Body.Close()
if do.StatusCode != http.StatusOK {
return nil, errors.New(do.Status)
}
obj := new(StationResp)
err = json.NewDecoder(do.Body).Decode(&obj)
if err != nil {
return nil, err
}
return obj, nil
}
func GetStationAssetsUrlAndServerUrl(id string, mutoken string, token string) (string, string, error) {
var err error
if token == "" {
token, err = GetToken()
if err != nil {
return "", "", err
}
}
req, err := http.NewRequest("GET", "https://amp-api.music.apple.com/v1/play/assets", nil)
if err != nil {
return "", "", err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Origin", "https://music.apple.com")
req.Header.Set("Media-User-Token", mutoken)
query := url.Values{}
//query.Set("omit[resource]", "autos")
//query.Set("extend", "editorialVideo")
query.Set("id", id)
query.Set("kind", "radioStation")
query.Set("keyFormat", "web")
req.URL.RawQuery = query.Encode()
do, err := http.DefaultClient.Do(req)
if err != nil {
return "", "", err
}
defer do.Body.Close()
if do.StatusCode != http.StatusOK {
return "", "", errors.New(do.Status)
}
obj := new(StationAssets)
err = json.NewDecoder(do.Body).Decode(&obj)
if err != nil {
return "", "", err
}
return obj.Results.Assets[0].Url, obj.Results.Assets[0].KeyServerUrl, nil
}
func GetStationNextTracks(id, mutoken, language, token string) (*TrackResp, error) {
var err error
if token == "" {
token, err = GetToken()
if err != nil {
return nil, err
}
}
req, err := http.NewRequest("POST", fmt.Sprintf("https://amp-api.music.apple.com/v1/me/stations/next-tracks/%s", id), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Origin", "https://music.apple.com")
req.Header.Set("Media-User-Token", mutoken)
query := url.Values{}
query.Set("omit[resource]", "autos")
//query.Set("include", "tracks,artists,record-labels")
query.Set("include[songs]", "artists,albums")
query.Set("limit", "10")
query.Set("extend", "editorialVideo,extendedAssetUrls")
query.Set("l", language)
req.URL.RawQuery = query.Encode()
do, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer do.Body.Close()
if do.StatusCode != http.StatusOK {
return nil, errors.New(do.Status)
}
obj := new(TrackResp)
err = json.NewDecoder(do.Body).Decode(&obj)
if err != nil {
return nil, err
}
return obj, nil
}
type StationResp struct {
Href string `json:"href"`
Next string `json:"next"`
Data []StationRespData `json:"data"`
}
type StationAssets struct {
Results struct {
Assets []struct {
KeyServerUrl string `json:"keyServerUrl"`
Url string `json:"url"`
WidevineKeyCertificateUrl string `json:"widevineKeyCertificateUrl"`
FairPlayKeyCertificateUrl string `json:"fairPlayKeyCertificateUrl"`
} `json:"assets"`
} `json:"results"`
}
type StationRespData 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"`
IsLive bool `json:"isLive"`
URL string `json:"url"`
Name string `json:"name"`
EditorialVideo struct {
MotionTall struct {
Video string `json:"video"`
} `json:"motionTallVideo3x4"`
MotionSquare struct {
Video string `json:"video"`
} `json:"motionSquareVideo1x1"`
MotionDetailTall struct {
Video string `json:"video"`
} `json:"motionDetailTall"`
MotionDetailSquare struct {
Video string `json:"video"`
} `json:"motionDetailSquare"`
} `json:"editorialVideo"`
PlayParams struct {
ID string `json:"id"`
Kind string `json:"kind"`
Format string `json:"format"`
StationHash string `json:"stationHash"`
} `json:"playParams"`
} `json:"attributes"`
}

49
utils/ampapi/token.go Normal file
View File

@@ -0,0 +1,49 @@
package ampapi
import (
"io"
"net/http"
"regexp"
)
func GetToken() (string, error) {
req, err := http.NewRequest("GET", "https://beta.music.apple.com", nil)
if err != nil {
return "", err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
regex := regexp.MustCompile(`/assets/index-legacy-[^/]+\.js`)
indexJsUri := regex.FindString(string(body))
req, err = http.NewRequest("GET", "https://beta.music.apple.com"+indexJsUri, nil)
if err != nil {
return "", err
}
resp, err = http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err = io.ReadAll(resp.Body)
if err != nil {
return "", err
}
regex = regexp.MustCompile(`eyJh([^"]*)`)
token := regex.FindString(string(body))
return token, nil
}

103
utils/ampapi/track.go Normal file
View File

@@ -0,0 +1,103 @@
package ampapi
type TrackResp struct {
Href string `json:"href"`
Next string `json:"next"`
Data []TrackRespData `json:"data"`
}
// 类型为song 或者 music-video
type TrackRespData 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"`
ExtendedAssetUrls struct {
EnhancedHls string `json:"enhancedHls"`
} `json:"extendedAssetUrls"`
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"`
Albums struct {
Href string `json:"href"`
Data []struct {
ID string `json:"id"`
Type string `json:"type"`
Href string `json:"href"`
Attributes struct {
ArtistName string `json:"artistName"`
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"`
GenreNames []string `json:"genreNames"`
IsCompilation bool `json:"isCompilation"`
IsComplete bool `json:"isComplete"`
IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsPrerelease bool `json:"isPrerelease"`
IsSingle bool `json:"isSingle"`
Name string `json:"name"`
PlayParams struct {
ID string `json:"id"`
Kind string `json:"kind"`
} `json:"playParams"`
ReleaseDate string `json:"releaseDate"`
TrackCount int `json:"trackCount"`
Upc string `json:"upc"`
URL string `json:"url"`
} `json:"attributes"`
} `json:"data"`
} `json:"albums"`
} `json:"relationships"`
}

View File

@@ -16,6 +16,7 @@ type SongLyrics struct {
Type string `json:"type"`
Attributes struct {
Ttml string `json:"ttml"`
TtmlLocalizations string `json:"ttmlLocalizations"`
PlayParams struct {
Id string `json:"id"`
Kind string `json:"kind"`
@@ -26,7 +27,6 @@ type SongLyrics struct {
} `json:"data"`
}
func Get(storefront, songId, lrcType, language, lrcFormat, token, mediaUserToken string) (string, error) {
if len(mediaUserToken) < 50 {
return "", errors.New("MediaUserToken not set")
@@ -48,9 +48,10 @@ func Get(storefront, songId, lrcType, language, lrcFormat, token, mediaUserToken
return lrc, nil
}
func getSongLyrics(songId string, storefront string, token string, userToken string, lrcType string, language string) (string, error) {
req, err := http.NewRequest("GET",
fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/songs/%s/%s?l=%s", storefront, songId, lrcType, language), nil)
fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/songs/%s/%s?l=%s&extend=ttmlLocalizations", storefront, songId, lrcType, language), nil)
if err != nil {
return "", err
}
@@ -67,12 +68,59 @@ func getSongLyrics(songId string, storefront string, token string, userToken str
obj := new(SongLyrics)
_ = json.NewDecoder(do.Body).Decode(&obj)
if obj.Data != nil {
return obj.Data[0].Attributes.Ttml, nil
if len(obj.Data[0].Attributes.Ttml) > 0 {
return obj.Data[0].Attributes.Ttml, nil
}
return obj.Data[0].Attributes.TtmlLocalizations, nil
} else {
return "", errors.New("failed to get lyrics")
}
}
// Use for detect if lyrics have CJK, will be replaced by transliteration if exist.
func containsCJK(s string) bool {
for _, r := range s {
if (r >= 0x1100 && r <= 0x11FF) || // Hangul Jamo
(r >= 0x2E80 && r <= 0x2EFF) || // CJK Radicals Supplement
(r >= 0x2F00 && r <= 0x2FDF) || // Kangxi Radicals
(r >= 0x2FF0 && r <= 0x2FFF) || // Ideographic Description Characters
(r >= 0x3000 && r <= 0x303F) || // CJK Symbols and Punctuation
(r >= 0x3040 && r <= 0x309F) || // Hiragana
(r >= 0x30A0 && r <= 0x30FF) || // Katakana
(r >= 0x3130 && r <= 0x318F) || // Hangul Compatibility Jamo
(r >= 0x31C0 && r <= 0x31EF) || // CJK Strokes
(r >= 0x31F0 && r <= 0x31FF) || // Katakana Phonetic Extensions
(r >= 0x3200 && r <= 0x32FF) || // Enclosed CJK Letters and Months
(r >= 0x3300 && r <= 0x33FF) || // CJK Compatibility
(r >= 0x3400 && r <= 0x4DBF) || // CJK Unified Ideographs Extension A
(r >= 0x4E00 && r <= 0x9FFF) || // CJK Unified Ideographs
(r >= 0xA960 && r <= 0xA97F) || // Hangul Jamo Extended-A
(r >= 0xAC00 && r <= 0xD7AF) || // Hangul Syllables
(r >= 0xD7B0 && r <= 0xD7FF) || // Hangul Jamo Extended-B
(r >= 0xF900 && r <= 0xFAFF) || // CJK Compatibility Ideographs
(r >= 0xFE30 && r <= 0xFE4F) || // CJK Compatibility Forms
(r >= 0xFF65 && r <= 0xFF9F) || // Halfwidth Katakana
(r >= 0xFFA0 && r <= 0xFFDC) || // Halfwidth Jamo
(r >= 0x1AFF0 && r <= 0x1AFFF) || // Kana Extended-B
(r >= 0x1B000 && r <= 0x1B0FF) || // Kana Supplement
(r >= 0x1B100 && r <= 0x1B12F) || // Kana Extended-A
(r >= 0x1B130 && r <= 0x1B16F) || // Small Kana Extension
(r >= 0x1F200 && r <= 0x1F2FF) || // Enclosed Ideographic Supplement
(r >= 0x20000 && r <= 0x2A6DF) || // CJK Unified Ideographs Extension B
(r >= 0x2A700 && r <= 0x2B73F) || // CJK Unified Ideographs Extension C
(r >= 0x2B740 && r <= 0x2B81F) || // CJK Unified Ideographs Extension D
(r >= 0x2B820 && r <= 0x2CEAF) || // CJK Unified Ideographs Extension E
(r >= 0x2CEB0 && r <= 0x2EBEF) || // CJK Unified Ideographs Extension F
(r >= 0x2EBF0 && r <= 0x2EE5F) || // CJK Unified Ideographs Extension I
(r >= 0x2F800 && r <= 0x2FA1F) || // CJK Compatibility Ideographs Supplement
(r >= 0x30000 && r <= 0x3134F) || // CJK Unified Ideographs Extension G
(r >= 0x31350 && r <= 0x323AF) { // CJK Unified Ideographs Extension H
return true
}
}
return false
}
func TtmlToLrc(ttml string) (string, error) {
parsedTTML := etree.NewDocument()
err := parsedTTML.ReadFromString(ttml)
@@ -102,37 +150,76 @@ func TtmlToLrc(ttml string) (string, error) {
for _, item := range parsedTTML.FindElement("tt").FindElement("body").ChildElements() {
for _, lyric := range item.ChildElements() {
var h, m, s, ms int
if lyric.SelectAttr("begin") == nil {
beginAttr := lyric.SelectAttr("begin")
if beginAttr == nil {
return "", errors.New("no synchronised lyrics")
}
if strings.Contains(lyric.SelectAttr("begin").Value, ":") {
_, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d:%d:%d.%d", &h, &m, &s, &ms)
beginValue := beginAttr.Value
if strings.Contains(beginValue, ":") {
_, err = fmt.Sscanf(beginValue, "%d:%d:%d.%d", &h, &m, &s, &ms)
if err != nil {
_, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d:%d.%d", &m, &s, &ms)
_, err = fmt.Sscanf(beginValue, "%d:%d.%d", &m, &s, &ms)
if err != nil {
_, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d:%d", &m, &s)
_, err = fmt.Sscanf(beginValue, "%d:%d", &m, &s)
}
h = 0
}
} else {
_, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d.%d", &s, &ms)
_, err = fmt.Sscanf(beginValue, "%d.%d", &s, &ms)
h, m = 0, 0
}
if err != nil {
return "", err
}
var text string
//GET trans
m += h * 60
ms = ms / 10
var text, transText, translitText string
//GET trans and translit
if len(parsedTTML.FindElement("tt").FindElements("head")) > 0 {
if len(parsedTTML.FindElement("tt").FindElement("head").FindElements("metadata")) > 0 {
Metadata := parsedTTML.FindElement("tt").FindElement("head").FindElement("metadata")
if len(Metadata.FindElements("iTunesMetadata")) > 0 {
iTunesMetadata := Metadata.FindElement("iTunesMetadata")
if len(iTunesMetadata.FindElements("transliterations")) > 0 {
if len(iTunesMetadata.FindElement("transliterations").FindElements("transliteration")) > 0 {
xpath := fmt.Sprintf("text[@for='%s']", lyric.SelectAttr("itunes:key").Value)
translit := iTunesMetadata.FindElement("transliterations").FindElement("transliteration").FindElement(xpath)
if translit != nil {
if translit.SelectAttr("text") != nil {
translitText = translit.SelectAttr("text").Value
} else {
var translitTmp []string
for _, span := range translit.Child {
if c, ok := span.(*etree.CharData); ok {
translitTmp = append(translitTmp, c.Data)
} else if e, ok := span.(*etree.Element); ok {
translitTmp = append(translitTmp, e.Text())
}
}
translitText = strings.Join(translitTmp, "")
}
}
}
}
if len(iTunesMetadata.FindElements("translations")) > 0 {
if len(iTunesMetadata.FindElement("translations").FindElements("translation")) > 0 {
xpath := fmt.Sprintf("//text[@for='%s']", lyric.SelectAttr("itunes:key").Value)
trans := iTunesMetadata.FindElement("translations").FindElement("translation").FindElement(xpath)
lyric = trans
if trans != nil {
if trans.SelectAttr("text") != nil {
transText = trans.SelectAttr("text").Value
} else {
var transTmp []string
for _, span := range trans.Child {
if c, ok := span.(*etree.CharData); ok {
transTmp = append(transTmp, c.Data)
} else if e, ok := span.(*etree.Element); ok {
transTmp = append(transTmp, e.Text())
}
}
transText = strings.Join(transTmp, "")
}
}
}
}
}
@@ -151,9 +238,14 @@ func TtmlToLrc(ttml string) (string, error) {
} else {
text = lyric.SelectAttr("text").Value
}
m += h * 60
ms = ms / 10
lrcLines = append(lrcLines, fmt.Sprintf("[%02d:%02d.%02d]%s", m, s, ms, text))
if len(transText) > 0 {
lrcLines = append(lrcLines, fmt.Sprintf("[%02d:%02d.%02d]%s", m, s, ms, transText))
}
if len(translitText) > 0 && containsCJK(text) {
lrcLines = append(lrcLines, fmt.Sprintf("[%02d:%02d.%02d]%s", m, s, ms, translitText))
} else {
lrcLines = append(lrcLines, fmt.Sprintf("[%02d:%02d.%02d]%s", m, s, ms, text))
}
}
}
return strings.Join(lrcLines, "\n"), nil
@@ -166,7 +258,7 @@ func conventSyllableTTMLToLRC(ttml string) (string, error) {
return "", err
}
var lrcLines []string
parseTime := func(timeValue string) (string, error) {
parseTime := func(timeValue string, newLine int) (string, error) {
var h, m, s, ms int
if strings.Contains(timeValue, ":") {
_, err = fmt.Sscanf(timeValue, "%d:%d:%d.%d", &h, &m, &s, &ms)
@@ -183,30 +275,22 @@ func conventSyllableTTMLToLRC(ttml string) (string, error) {
}
m += h * 60
ms = ms / 10
return fmt.Sprintf("[%02d:%02d.%02d]", m, s, ms), nil
}
divs := parsedTTML.FindElement("tt").FindElement("body").FindElements("div")
//get trans
if len(parsedTTML.FindElement("tt").FindElements("head")) > 0 {
if len(parsedTTML.FindElement("tt").FindElement("head").FindElements("metadata")) > 0 {
Metadata := parsedTTML.FindElement("tt").FindElement("head").FindElement("metadata")
if len(Metadata.FindElements("iTunesMetadata")) > 0 {
iTunesMetadata := Metadata.FindElement("iTunesMetadata")
if len(iTunesMetadata.FindElements("translations")) > 0 {
if len(iTunesMetadata.FindElement("translations").FindElements("translation")) > 0 {
divs = iTunesMetadata.FindElement("translations").FindElements("translation")
}
}
}
if newLine == 0 {
return fmt.Sprintf("[%02d:%02d.%02d]<%02d:%02d.%02d>", m, s, ms, m, s, ms), nil
} else if newLine == -1 {
return fmt.Sprintf("[%02d:%02d.%02d]", m, s, ms), nil
} else {
return fmt.Sprintf("<%02d:%02d.%02d>", m, s, ms), nil
}
}
divs := parsedTTML.FindElement("tt").FindElement("body").FindElements("div")
for _, div := range divs {
for _, item := range div.ChildElements() {
for _, item := range div.ChildElements() { //LINES
var lrcSyllables []string
var i int = 0
var endTime string
for _, lyrics := range item.Child {
if _, ok := lyrics.(*etree.CharData); ok {
var endTime, translitLine, transLine string
for _, lyrics := range item.Child { //WORDS
if _, ok := lyrics.(*etree.CharData); ok { //是否为span之间的空格
if i > 0 {
lrcSyllables = append(lrcSyllables, " ")
continue
@@ -217,11 +301,12 @@ func conventSyllableTTMLToLRC(ttml string) (string, error) {
if lyric.SelectAttr("begin") == nil {
continue
}
beginTime, err := parseTime(lyric.SelectAttr("begin").Value)
beginTime, err := parseTime(lyric.SelectAttr("begin").Value, i)
if err != nil {
return "", err
return "", err
}
endTime, err = parseTime(lyric.SelectAttr("end").Value)
endTime, err = parseTime(lyric.SelectAttr("end").Value, 1)
if err != nil {
return "", err
}
@@ -240,13 +325,88 @@ func conventSyllableTTMLToLRC(ttml string) (string, error) {
text = lyric.SelectAttr("text").Value
}
lrcSyllables = append(lrcSyllables, fmt.Sprintf("%s%s", beginTime, text))
if i == 0 {
transBeginTime, _ := parseTime(lyric.SelectAttr("begin").Value, -1)
sharedTimestamp := ""
if len(parsedTTML.FindElement("tt").FindElements("head")) > 0 {
if len(parsedTTML.FindElement("tt").FindElement("head").FindElements("metadata")) > 0 {
Metadata := parsedTTML.FindElement("tt").FindElement("head").FindElement("metadata")
if len(Metadata.FindElements("iTunesMetadata")) > 0 {
iTunesMetadata := Metadata.FindElement("iTunesMetadata")
if len(iTunesMetadata.FindElements("transliterations")) > 0 {
if len(iTunesMetadata.FindElement("transliterations").FindElements("transliteration")) > 0 {
xpath := fmt.Sprintf("text[@for='%s']", item.SelectAttr("itunes:key").Value)
trans := iTunesMetadata.FindElement("transliterations").FindElement("transliteration").FindElement(xpath)
// Get text content
var transTxtParts []string
var transStartTime string
for i, span := range trans.ChildElements() {
if span.Tag == "span" {
spanBegin := span.SelectAttrValue("begin", "")
spanText := span.Text()
if spanBegin == "" {
continue
}
// Get timestamp
timestamp, err := parseTime(spanBegin, 2)
if err != nil {
return "", err
}
if i == 0 {
// For [mm:ss.xx] prefix
transStartTime, _ = parseTime(spanBegin, -1)
sharedTimestamp = transStartTime
}
transTxtParts = append(transTxtParts, fmt.Sprintf("%s%s", timestamp, spanText))
}
}
translitLine = fmt.Sprintf("%s%s", transStartTime, strings.Join(transTxtParts, " "))
}
}
if len(iTunesMetadata.FindElements("translations")) > 0 {
if len(iTunesMetadata.FindElement("translations").FindElements("translation")) > 0 {
xpath := fmt.Sprintf("//text[@for='%s']", item.SelectAttr("itunes:key").Value)
trans := iTunesMetadata.FindElement("translations").FindElement("translation").FindElement(xpath)
var transTxt string
if trans.SelectAttr("text") == nil {
var textTmp []string
for _, span := range trans.Child {
if _, ok := span.(*etree.CharData); ok {
textTmp = append(textTmp, span.(*etree.CharData).Data)
} /*else {
textTmp = append(textTmp, span.(*etree.Element).Text())
}*/
}
transTxt = strings.Join(textTmp, "")
} else {
transTxt = trans.SelectAttr("text").Value
}
//fmt.Println(transTxt)
if sharedTimestamp != "" {
transLine = sharedTimestamp + transTxt
} else {
transLine = transBeginTime + transTxt
}
}
}
}
}
}
}
i += 1
}
//endTime, err := parseTime(item.SelectAttr("end").Value)
//if err != nil {
// return "", err
//}
lrcLines = append(lrcLines, strings.Join(lrcSyllables, "")+endTime)
if len(transLine) > 0 {
lrcLines = append(lrcLines, transLine)
}
if len(translitLine) > 0 && containsCJK(strings.Join(lrcSyllables, "")) {
lrcLines = append(lrcLines, translitLine)
} else {
lrcLines = append(lrcLines, strings.Join(lrcSyllables, "")+endTime)
}
}
}
return strings.Join(lrcLines, "\n"), nil

View File

@@ -13,7 +13,7 @@ import (
"os"
"time"
"github.com/Eyevinn/mp4ff/mp4"
"github.com/itouakirai/mp4ff/mp4"
"github.com/grafov/m3u8"
"encoding/binary"
@@ -624,7 +624,6 @@ func cbcsDecryptSamples(samples []mp4.FullSample, conn *bufio.ReadWriter,
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]
@@ -675,9 +674,6 @@ func DecryptFragment(frag *mp4.Fragment, tracks map[uint32]mp4.DecryptTrackInfo,
}
bytesRemoved += traf.RemoveEncryptionBoxes()
// remove sbgp and sgpd
traf.Children, sxxxBytesRemoved = FilterSbgpSgpd(traf.Children)
bytesRemoved += sxxxBytesRemoved
}
_, psshBytesRemoved := moof.RemovePsshs()
bytesRemoved += psshBytesRemoved

View File

@@ -4,20 +4,25 @@ import (
"context"
"encoding/base64"
"encoding/hex"
"github.com/gospider007/requests"
"log/slog"
"github.com/go-resty/resty/v2"
"main/utils/runv3/cdm"
)
type Key struct {
ReqCli *requests.Client
BeforeRequest func(cl *requests.Client, preCtx context.Context, method string, href string, options ...requests.RequestOption) (resp *requests.Response, err error)
AfterRequest func(*requests.Response) ([]byte, error)
ReqCli *resty.Client
BeforeRequest func(cl *resty.Client, ctx context.Context, url string, body []byte) (*resty.Response, error)
AfterRequest func(*resty.Response) ([]byte, error)
}
func (w *Key) CdmInit() {
wv.InitConstants()
}
func (w *Key) GetKey(ctx context.Context, licenseServerURL string, PSSH string, headers map[string][]string) (string, []byte, error) {
initData, err := base64.StdEncoding.DecodeString(PSSH)
var keybt []byte
@@ -35,21 +40,23 @@ func (w *Key) GetKey(ctx context.Context, licenseServerURL string, PSSH string,
slog.Error("license request error: %v", err)
return "", keybt, err
}
var response *requests.Response
var response *resty.Response
if w.BeforeRequest != nil {
response, err = w.BeforeRequest(w.ReqCli, ctx, "post", licenseServerURL, requests.RequestOption{
Data: licenseRequest,
})
response, err = w.BeforeRequest(w.ReqCli, ctx, licenseServerURL, licenseRequest)
} else {
response, err = w.ReqCli.Request(nil, "post", licenseServerURL, requests.RequestOption{
Data: licenseRequest,
})
response, err = w.ReqCli.R().
SetContext(ctx).
SetBody(licenseRequest).
Post(licenseServerURL)
}
if err != nil {
slog.Error("license request error: %s", err)
return "", keybt, err
}
var licenseResponse []byte
if w.AfterRequest != nil {
licenseResponse, err = w.AfterRequest(response)
@@ -57,14 +64,14 @@ func (w *Key) GetKey(ctx context.Context, licenseServerURL string, PSSH string,
return "", keybt, err
}
} else {
licenseResponse = response.Content()
licenseResponse = response.Body()
}
keys, err := cdm.GetLicenseKeys(licenseRequest, licenseResponse)
command := ""
for _, key := range keys {
if key.Type == wv.License_KeyContainer_CONTENT {
//command += hex.EncodeToString(key.ID) + ":" + hex.EncodeToString(key.Value)
command += hex.EncodeToString(key.Value)
keybt = key.Value
}

View File

@@ -5,10 +5,10 @@ import (
"encoding/base64"
"fmt"
"path/filepath"
"github.com/gospider007/requests"
"github.com/go-resty/resty/v2"
"google.golang.org/protobuf/proto"
//"log/slog"
cdm "main/utils/runv3/cdm"
key "main/utils/runv3/key"
"os"
@@ -17,13 +17,13 @@ import (
"errors"
"io"
"github.com/Eyevinn/mp4ff/mp4"
"github.com/itouakirai/mp4ff/mp4"
//"io/ioutil"
"encoding/json"
"net/http"
"os/exec"
"strings"
"sync"
"github.com/grafov/m3u8"
"github.com/schollz/progressbar/v3"
@@ -36,24 +36,6 @@ type PlaybackLicense struct {
Status int `json:"status"`
}
// func log() {
// f, err := os.OpenFile("log.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
// if err != nil {
// slog.Error("error opening file: %s", err)
// }
// defer func(f *os.File) {
// err := f.Close()
// if err != nil {
// slog.Error("error closing file: %s", err)
// }
// }(f)
// opts := slog.HandlerOptions{
// AddSource: true,
// Level: slog.LevelDebug,
// }
// logger := slog.New(slog.NewJSONHandler(os.Stdout, &opts))
// slog.SetDefault(logger)
// }
func getPSSH(contentId string, kidBase64 string) (string, error) {
kidBytes, err := base64.StdEncoding.DecodeString(kidBase64)
if err != nil {
@@ -77,41 +59,50 @@ func getPSSH(contentId string, kidBase64 string) (string, error) {
pssh := base64.StdEncoding.EncodeToString(widevineCenc)
return pssh, nil
}
func BeforeRequest(cl *requests.Client, preCtx context.Context, method string, href string, options ...requests.RequestOption) (resp *requests.Response, err error) {
data := options[0].Data
func BeforeRequest(cl *resty.Client, ctx context.Context, url string, body []byte) (*resty.Response, error) {
jsondata := map[string]interface{}{
"challenge": base64.StdEncoding.EncodeToString(data.([]byte)),
"challenge": base64.StdEncoding.EncodeToString(body), // 'body' is passed in directly
"key-system": "com.widevine.alpha",
"uri": "data:;base64," + preCtx.Value("pssh").(string),
"adamId": preCtx.Value("adamId").(string),
"uri": ctx.Value("uriPrefix").(string) + "," + ctx.Value("pssh").(string),
"adamId": ctx.Value("adamId").(string),
"isLibrary": false,
"user-initiated": true,
}
options[0].Data = nil
options[0].Json = jsondata
resp, err = cl.Request(preCtx, method, href, options...)
resp, err := cl.R().
SetContext(ctx).
SetBody(jsondata).
Post(url)
if err != nil {
fmt.Println(err)
}
return
return resp, err
}
func AfterRequest(Response *requests.Response) ([]byte, error) {
var ResponseData PlaybackLicense
_, err := Response.Json(&ResponseData)
func AfterRequest(response *resty.Response) ([]byte, error) {
var responseData PlaybackLicense
err := json.Unmarshal(response.Body(), &responseData)
if err != nil {
return nil, fmt.Errorf("failed to parse response: %v", err)
return nil, fmt.Errorf("failed to parse response JSON: %v", err)
}
if ResponseData.ErrorCode != 0 || ResponseData.Status != 0 {
return nil, fmt.Errorf("error code: %d", ResponseData.ErrorCode)
if responseData.ErrorCode != 0 || responseData.Status != 0 {
return nil, fmt.Errorf("error in license response, code: %d, status: %d", responseData.ErrorCode, responseData.Status)
}
License, err := base64.StdEncoding.DecodeString(ResponseData.License)
license, err := base64.StdEncoding.DecodeString(responseData.License)
if err != nil {
return nil, fmt.Errorf("failed to decode license: %v", err)
}
return License, nil
return license, nil
}
func GetWebplayback(adamId string, authtoken string, mutoken string, mvmode bool) (string, string, error) {
func GetWebplayback(adamId string, authtoken string, mutoken string, mvmode bool) (string, string, string, error) {
url := "https://play.music.apple.com/WebObjects/MZPlay.woa/wa/webPlayback"
postData := map[string]string{
"salableAdamId": adamId,
@@ -119,12 +110,12 @@ func GetWebplayback(adamId string, authtoken string, mutoken string, mvmode bool
jsonData, err := json.Marshal(postData)
if err != nil {
fmt.Println("Error encoding JSON:", err)
return "", "", err
return "", "", "", err
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(jsonData)))
if err != nil {
fmt.Println("Error creating request:", err)
return "", "", err
return "", "", "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Origin", "https://music.apple.com")
@@ -139,7 +130,7 @@ func GetWebplayback(adamId string, authtoken string, mutoken string, mvmode bool
//resp, err := client.Do(req)
if err != nil {
fmt.Println("Error sending request:", err)
return "", "", err
return "", "", "", err
}
defer resp.Body.Close()
//fmt.Println("Response Status:", resp.Status)
@@ -147,25 +138,25 @@ func GetWebplayback(adamId string, authtoken string, mutoken string, mvmode bool
err = json.NewDecoder(resp.Body).Decode(&obj)
if err != nil {
fmt.Println("json err:", err)
return "", "", err
return "", "", "", err
}
if len(obj.List) > 0 {
if mvmode {
return obj.List[0].HlsPlaylistUrl, "", nil
return obj.List[0].HlsPlaylistUrl, "", "", nil
}
// 遍历 Assets
for i, _ := range obj.List[0].Assets {
for i := range obj.List[0].Assets {
if obj.List[0].Assets[i].Flavor == "28:ctrp256" {
kidBase64, fileurl, err := extractKidBase64(obj.List[0].Assets[i].URL, false)
kidBase64, fileurl, uriPrefix, err := extractKidBase64(obj.List[0].Assets[i].URL, false)
if err != nil {
return "", "", err
return "", "", "", err
}
return fileurl, kidBase64, nil
return fileurl, kidBase64, uriPrefix, nil
}
continue
}
}
return "", "", nil
return "", "", "", errors.New("Unavailable")
}
type Songlist struct {
@@ -180,30 +171,32 @@ type Songlist struct {
Status int `json:"status"`
}
func extractKidBase64(b string, mvmode bool) (string, string, error) {
func extractKidBase64(b string, mvmode bool) (string, string, string, error) {
resp, err := http.Get(b)
if err != nil {
return "", "", err
return "", "", "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", "", errors.New(resp.Status)
return "", "", "", errors.New(resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", "", err
return "", "", "", err
}
masterString := string(body)
from, listType, err := m3u8.DecodeFrom(strings.NewReader(masterString), true)
if err != nil {
return "", "", err
return "", "", "", err
}
var kidbase64 string
var uriPrefix string
var urlBuilder strings.Builder
if listType == m3u8.MEDIA {
mediaPlaylist := from.(*m3u8.MediaPlaylist)
if mediaPlaylist.Key != nil {
split := strings.Split(mediaPlaylist.Key.URI, ",")
uriPrefix = split[0]
kidbase64 = split[1]
lastSlashIndex := strings.LastIndex(b, "/")
// 截取最后一个斜杠之前的部分
@@ -230,7 +223,7 @@ func extractKidBase64(b string, mvmode bool) (string, string, error) {
} else {
fmt.Println("Not a media playlist")
}
return kidbase64, urlBuilder.String(), nil
return kidbase64, urlBuilder.String(), uriPrefix, nil
}
func extsong(b string) bytes.Buffer {
resp, err := http.Get(b)
@@ -260,18 +253,19 @@ func extsong(b string) bytes.Buffer {
io.Copy(io.MultiWriter(&buffer, bar), resp.Body)
return buffer
}
func Run(adamId string, trackpath string, authtoken string, mutoken string, mvmode bool) (string, error) {
func Run(adamId string, trackpath string, authtoken string, mutoken string, mvmode bool, serverUrl string) (string, error) {
var keystr string //for mv key
var fileurl string
var kidBase64 string
var uriPrefix string
var err error
if mvmode {
kidBase64, fileurl, err = extractKidBase64(trackpath, true)
kidBase64, fileurl, uriPrefix, err = extractKidBase64(trackpath, true)
if err != nil {
return "", err
}
} else {
fileurl, kidBase64, err = GetWebplayback(adamId, authtoken, mutoken, false)
fileurl, kidBase64, uriPrefix, err = GetWebplayback(adamId, authtoken, mutoken, false)
if err != nil {
return "", err
}
@@ -279,29 +273,38 @@ func Run(adamId string, trackpath string, authtoken string, mutoken string, mvmo
ctx := context.Background()
ctx = context.WithValue(ctx, "pssh", kidBase64)
ctx = context.WithValue(ctx, "adamId", adamId)
ctx = context.WithValue(ctx, "uriPrefix", uriPrefix)
pssh, err := getPSSH("", kidBase64)
//fmt.Println(pssh)
if err != nil {
fmt.Println(err)
return "", err
}
headers := map[string]interface{}{
headers := map[string]string{
"authorization": "Bearer " + authtoken,
"x-apple-music-user-token": mutoken,
}
client, _ := requests.NewClient(nil, requests.ClientOption{
Headers: headers,
})
client := resty.New()
client.SetHeaders(headers)
key := key.Key{
ReqCli: client,
BeforeRequest: BeforeRequest,
AfterRequest: AfterRequest,
}
key.CdmInit()
keystr, keybt, err := key.GetKey(ctx, "https://play.itunes.apple.com/WebObjects/MZPlay.woa/wa/acquireWebPlaybackLicense", pssh, nil)
if err != nil {
fmt.Println(err)
return "", err
var keybt []byte
if serverUrl != "" {
keystr, keybt, err = key.GetKey(ctx, serverUrl, pssh, nil)
if err != nil {
fmt.Println(err)
return "", err
}
} else {
keystr, keybt, err = key.GetKey(ctx, "https://play.itunes.apple.com/WebObjects/MZPlay.woa/wa/acquireWebPlaybackLicense", pssh, nil)
if err != nil {
fmt.Println(err)
return "", err
}
}
if mvmode {
keyAndUrls := "1:" + keystr + ";" + fileurl
@@ -335,6 +338,95 @@ func Run(adamId string, trackpath string, authtoken string, mutoken string, mvmo
return "", nil
}
// Segment 结构体用于在 Channel 中传递分段数据
type Segment struct {
Index int
Data []byte
}
func downloadSegment(url string, index int, wg *sync.WaitGroup, segmentsChan chan<- Segment, client *http.Client, limiter chan struct{}) {
// 函数退出时,从 limiter 中接收一个值,释放一个并发槽位
defer func() {
<-limiter
wg.Done()
}()
req, err := http.NewRequest("GET", url, nil)
if err != nil {
fmt.Printf("错误(分段 %d): 创建请求失败: %v\n", index, err)
return
}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("错误(分段 %d): 下载失败: %v\n", index, err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Printf("错误(分段 %d): 服务器返回状态码 %d\n", index, resp.StatusCode)
return
}
data, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("错误(分段 %d): 读取数据失败: %v\n", index, err)
return
}
// 将下载好的分段(包含序号和数据)发送到 Channel
segmentsChan <- Segment{Index: index, Data: data}
}
// fileWriter 从 Channel 接收分段并按顺序写入文件
func fileWriter(wg *sync.WaitGroup, segmentsChan <-chan Segment, outputFile io.Writer, totalSegments int) {
defer wg.Done()
// 缓冲区,用于存放乱序到达的分段
// key 是分段序号value 是分段数据
segmentBuffer := make(map[int][]byte)
nextIndex := 0 // 期望写入的下一个分段的序号
for segment := range segmentsChan {
// 检查收到的分段是否是当前期望的
if segment.Index == nextIndex {
//fmt.Printf("写入分段 %d\n", segment.Index)
_, err := outputFile.Write(segment.Data)
if err != nil {
fmt.Printf("错误(分段 %d): 写入文件失败: %v\n", segment.Index, err)
}
nextIndex++
// 检查缓冲区中是否有下一个连续的分段
for {
data, ok := segmentBuffer[nextIndex]
if !ok {
break // 缓冲区里没有下一个,跳出循环,等待下一个分段到达
}
//fmt.Printf("从缓冲区写入分段 %d\n", nextIndex)
_, err := outputFile.Write(data)
if err != nil {
fmt.Printf("错误(分段 %d): 从缓冲区写入文件失败: %v\n", nextIndex, err)
}
// 从缓冲区删除已写入的分段,释放内存
delete(segmentBuffer, nextIndex)
nextIndex++
}
} else {
// 如果不是期望的分段,先存入缓冲区
//fmt.Printf("缓冲分段 %d (等待 %d)\n", segment.Index, nextIndex)
segmentBuffer[segment.Index] = segment.Data
}
}
// 确保所有分段都已写入
if nextIndex != totalSegments {
fmt.Printf("警告: 写入完成,但似乎有分段丢失。期望 %d 个, 实际写入 %d 个。\n", totalSegments, nextIndex)
}
}
func ExtMvData(keyAndUrls string, savePath string) error {
segments := strings.Split(keyAndUrls, ";")
key := segments[0]
@@ -345,36 +437,51 @@ func ExtMvData(keyAndUrls string, savePath string) error {
fmt.Printf("创建文件失败:%v\n", err)
return err
}
defer tempFile.Close()
defer os.Remove(tempFile.Name())
defer tempFile.Close()
// 依次下载每个链接并写入文件
bar := progressbar.DefaultBytes(
-1,
"Downloading...",
)
var downloadWg, writerWg sync.WaitGroup
segmentsChan := make(chan Segment, len(urls))
// --- 新增代码: 定义最大并发数 ---
const maxConcurrency = 10
// --- 新增代码: 创建带缓冲的 Channel 作为信号量 ---
limiter := make(chan struct{}, maxConcurrency)
client := &http.Client{}
// 初始化进度条
bar := progressbar.DefaultBytes(-1, "Downloading...")
barWriter := io.MultiWriter(tempFile, bar)
for _, url := range urls {
resp, err := http.Get(url)
if err != nil {
fmt.Printf("下载链接 %s 失败:%v\n", url, err)
return err
}
if resp.StatusCode != http.StatusOK {
fmt.Printf("链接 %s 响应失败:%v\n", url, resp.Status)
return errors.New(resp.Status)
}
// 将响应体写入输出文件
_, err = io.Copy(barWriter, resp.Body)
defer resp.Body.Close() // 注意及时关闭响应体,避免资源泄露
if err != nil {
fmt.Printf("写入文件失败:%v\n", err)
return err
}
//fmt.Printf("第 %d 个链接 %s 下载并写入完成\n", idx+1, url)
// 启动写入 Goroutine
writerWg.Add(1)
go fileWriter(&writerWg, segmentsChan, barWriter, len(urls))
// 启动下载 Goroutines
for i, url := range urls {
// 在启动 Goroutine 前,向 limiter 发送一个值来“获取”一个槽位
// 如果 limiter 已满 (达到10个),这里会阻塞,直到有其他任务完成并释放槽位
//fmt.Printf("请求启动任务 %d...\n", i)
limiter <- struct{}{}
//fmt.Printf("...任务 %d 已启动\n", i)
downloadWg.Add(1)
// 将 limiter 传递给下载函数
go downloadSegment(url, i, &downloadWg, segmentsChan, client, limiter)
}
// 等待所有下载任务完成
downloadWg.Wait()
// 下载完成后,关闭 Channel。写入 Goroutine 会在处理完 Channel 中所有数据后退出。
close(segmentsChan)
// 等待写入 Goroutine 完成所有写入和缓冲处理
writerWg.Wait()
// 显式关闭文件defer会再次调用但重复关闭是安全的
if err := tempFile.Close(); err != nil {
fmt.Printf("关闭临时文件失败: %v\n", err)
return err
}
tempFile.Close()
fmt.Println("\nDownloaded.")
cmd1 := exec.Command("mp4decrypt", "--key", key, tempFile.Name(), filepath.Base(savePath))

View File

@@ -1,523 +1,103 @@
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"`
MVAudioType string `yaml:"mv-audio-type"`
MVMax int `yaml:"mv-max"`
}
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 TrackData 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"`
Albums struct {
Href string `json:"href"`
Data []AlbumData `json:"data"`
}
} `json:"relationships"`
}
type AlbumData struct {
ID string `json:"id"`
Type string `json:"type"`
Href string `json:"href"`
Attributes struct {
ArtistName string `json:"artistName"`
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"`
GenreNames []string `json:"genreNames"`
IsCompilation bool `json:"isCompilation"`
IsComplete bool `json:"isComplete"`
IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsPrerelease bool `json:"isPrerelease"`
IsSingle bool `json:"isSingle"`
Name string `json:"name"`
PlayParams struct {
ID string `json:"id"`
Kind string `json:"kind"`
} `json:"playParams"`
ReleaseDate string `json:"releaseDate"`
TrackCount int `json:"trackCount"`
Upc string `json:"upc"`
URL string `json:"url"`
}
}
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 {
MotionTall struct {
Video string `json:"video"`
} `json:"motionTallVideo3x4"`
MotionSquare struct {
Video string `json:"video"`
} `json:"motionSquareVideo1x1"`
MotionDetailTall struct {
Video string `json:"video"`
} `json:"motionDetailTall"`
MotionDetailSquare struct {
Video string `json:"video"`
} `json:"motionDetailSquare"`
} `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 []TrackData `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"`
Albums struct {
Href string `json:"href"`
Data []AlbumData `json:"data"`
}
} `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 AutoGeneratedMusicVideo struct {
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"`
AlbumName string `json:"albumName"`
ArtistName string `json:"artistName"`
URL string `json:"url"`
GenreNames []string `json:"genreNames"`
DurationInMillis int `json:"durationInMillis"`
Isrc string `json:"isrc"`
TrackNumber int `json:"trackNumber"`
DiscNumber int `json:"discNumber"`
ContentRating string `json:"contentRating"`
ReleaseDate string `json:"releaseDate"`
Name string `json:"name"`
Has4K bool `json:"has4K"`
HasHDR bool `json:"hasHDR"`
PlayParams struct {
ID string `json:"id"`
Kind string `json:"kind"`
} `json:"playParams"`
} `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"`
}
package structs
type ConfigSet struct {
Storefront string `yaml:"storefront"`
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"`
AacSaveFolder string `yaml:"aac-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"`
MVAudioType string `yaml:"mv-audio-type"`
MVMax int `yaml:"mv-max"`
ConvertAfterDownload bool `yaml:"convert-after-download"`
ConvertFormat string `yaml:"convert-format"`
ConvertKeepOriginal bool `yaml:"convert-keep-original"`
ConvertSkipIfSourceMatch bool `yaml:"convert-skip-if-source-matches"`
FFmpegPath string `yaml:"ffmpeg-path"`
ConvertExtraArgs string `yaml:"convert-extra-args"`
ConvertWarnLossyToLossless bool `yaml:"convert-warn-lossy-to-lossless"`
}
type Counter struct {
Unavailable int
NotSong int
Error int
Success int
Total int
}
// 艺术家页面
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"`
}

193
utils/task/album.go Normal file
View File

@@ -0,0 +1,193 @@
package task
import (
"bufio"
"errors"
"fmt"
"os"
"strconv"
"strings"
"github.com/fatih/color"
"github.com/olekukonko/tablewriter"
"main/utils/ampapi"
)
type Album struct {
Storefront string
ID string
SaveDir string
SaveName string
Codec string
CoverPath string
Language string
Resp ampapi.AlbumResp
Name string
Tracks []Track
}
func NewAlbum(st string, id string) *Album {
a := new(Album)
a.Storefront = st
a.ID = id
//fmt.Println("Album created")
return a
}
func (a *Album) GetResp(token, l string) error {
var err error
a.Language = l
resp, err := ampapi.GetAlbumResp(a.Storefront, a.ID, a.Language, token)
if err != nil {
return errors.New("error getting album response")
}
a.Resp = *resp
//简化高频调用名称
a.Name = a.Resp.Data[0].Attributes.Name
//fmt.Println("Getting album response")
//从resp中的Tracks数据中提取trackData信息到新的Track结构体中
for i, trackData := range a.Resp.Data[0].Relationships.Tracks.Data {
len := len(a.Resp.Data[0].Relationships.Tracks.Data)
a.Tracks = append(a.Tracks, Track{
ID: trackData.ID,
Type: trackData.Type,
Name: trackData.Attributes.Name,
Language: a.Language,
Storefront: a.Storefront,
//SaveDir: filepath.Join(a.SaveDir, a.SaveName),
//Codec: a.Codec,
TaskNum: i + 1,
TaskTotal: len,
M3u8: trackData.Attributes.ExtendedAssetUrls.EnhancedHls,
WebM3u8: trackData.Attributes.ExtendedAssetUrls.EnhancedHls,
//CoverPath: a.CoverPath,
Resp: trackData,
PreType: "albums",
DiscTotal: a.Resp.Data[0].Relationships.Tracks.Data[len-1].Attributes.DiscNumber,
PreID: a.ID,
AlbumData: a.Resp.Data[0],
})
}
return nil
}
func (a *Album) GetArtwork() string {
return a.Resp.Data[0].Attributes.Artwork.URL
}
func (a *Album) ShowSelect() []int {
meta := a.Resp
trackTotal := len(meta.Data[0].Relationships.Tracks.Data)
arr := make([]int, trackTotal)
for i := 0; i < trackTotal; i++ {
arr[i] = i + 1
}
selected := []int{}
var data [][]string
for trackNum, track := range meta.Data[0].Relationships.Tracks.Data {
trackNum++
trackName := fmt.Sprintf("%02d. %s", track.Attributes.TrackNumber, track.Attributes.Name)
data = append(data, []string{fmt.Sprint(trackNum),
trackName,
track.Attributes.ContentRating,
track.Type})
}
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"", "Track Name", "Rating", "Type"})
//table.SetFooter([]string{"", "", "Footer", "Footer4"})
table.SetRowLine(false)
//table.SetAutoMergeCells(true)
table.SetCaption(true, fmt.Sprintf("Storefront: %s, %d tracks missing", strings.ToUpper(a.Storefront), meta.Data[0].Attributes.TrackCount-trackTotal))
table.SetHeaderColor(tablewriter.Colors{},
tablewriter.Colors{tablewriter.FgRedColor, tablewriter.Bold},
tablewriter.Colors{tablewriter.FgBlackColor, tablewriter.Bold},
tablewriter.Colors{tablewriter.FgBlackColor, tablewriter.Bold})
table.SetColumnColor(tablewriter.Colors{tablewriter.FgCyanColor},
tablewriter.Colors{tablewriter.Bold, tablewriter.FgRedColor},
tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor},
tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor})
for _, row := range data {
if row[2] == "explicit" {
row[2] = "E"
} else if row[2] == "clean" {
row[2] = "C"
} else {
row[2] = "None"
}
if row[3] == "music-videos" {
row[3] = "MV"
} else if row[3] == "songs" {
row[3] = "SONG"
}
table.Append(row)
}
//table.AppendBulk(data)
table.Render()
fmt.Println("Please select from the track options above (multiple options separated by commas, ranges supported, or type 'all' to select all)")
cyanColor := color.New(color.FgCyan)
cyanColor.Print("select: ")
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
fmt.Println(err)
}
input = strings.TrimSpace(input)
if input == "all" {
fmt.Println("You have selected all options:")
selected = arr
} else {
selectedOptions := [][]string{}
parts := strings.Split(input, ",")
for _, part := range parts {
if strings.Contains(part, "-") { // Range setting
rangeParts := strings.Split(part, "-")
selectedOptions = append(selectedOptions, rangeParts)
} else { // Single option
selectedOptions = append(selectedOptions, []string{part})
}
}
//
for _, opt := range selectedOptions {
if len(opt) == 1 { // Single option
num, err := strconv.Atoi(opt[0])
if err != nil {
fmt.Println("Invalid option:", opt[0])
continue
}
if num > 0 && num <= len(arr) {
selected = append(selected, num)
//args = append(args, urls[num-1])
} else {
fmt.Println("Option out of range:", opt[0])
}
} else if len(opt) == 2 { // Range
start, err1 := strconv.Atoi(opt[0])
end, err2 := strconv.Atoi(opt[1])
if err1 != nil || err2 != nil {
fmt.Println("Invalid range:", opt)
continue
}
if start < 1 || end > len(arr) || start > end {
fmt.Println("Range out of range:", opt)
continue
}
for i := start; i <= end; i++ {
//fmt.Println(options[i-1])
selected = append(selected, i)
}
} else {
fmt.Println("Invalid option:", opt)
}
}
}
return selected
}

195
utils/task/playlist.go Normal file
View File

@@ -0,0 +1,195 @@
package task
import (
"bufio"
"errors"
"fmt"
"os"
"strconv"
"strings"
"github.com/fatih/color"
"github.com/olekukonko/tablewriter"
"main/utils/ampapi"
)
type Playlist struct {
Storefront string
ID string
SaveDir string
SaveName string
Codec string
CoverPath string
Language string
Resp ampapi.PlaylistResp
Name string
Tracks []Track
}
func NewPlaylist(st string, id string) *Playlist {
a := new(Playlist)
a.Storefront = st
a.ID = id
//fmt.Println("Album created")
return a
}
func (a *Playlist) GetResp(token, l string) error {
var err error
a.Language = l
resp, err := ampapi.GetPlaylistResp(a.Storefront, a.ID, a.Language, token)
if err != nil {
return errors.New("error getting album response")
}
a.Resp = *resp
a.Resp.Data[0].Attributes.ArtistName = "Apple Music"
//简化高频调用名称
a.Name = a.Resp.Data[0].Attributes.Name
//fmt.Println("Getting album response")
//从resp中的Tracks数据中提取trackData信息到新的Track结构体中
for i, trackData := range a.Resp.Data[0].Relationships.Tracks.Data {
len := len(a.Resp.Data[0].Relationships.Tracks.Data)
a.Tracks = append(a.Tracks, Track{
ID: trackData.ID,
Type: trackData.Type,
Name: trackData.Attributes.Name,
Language: a.Language,
Storefront: a.Storefront,
//SaveDir: filepath.Join(a.SaveDir, a.SaveName),
//Codec: a.Codec,
TaskNum: i + 1,
TaskTotal: len,
M3u8: trackData.Attributes.ExtendedAssetUrls.EnhancedHls,
WebM3u8: trackData.Attributes.ExtendedAssetUrls.EnhancedHls,
//CoverPath: a.CoverPath,
Resp: trackData,
PreType: "playlists",
//DiscTotal: a.Resp.Data[0].Relationships.Tracks.Data[len-1].Attributes.DiscNumber, 在它处获取
PreID: a.ID,
PlaylistData: a.Resp.Data[0],
})
}
return nil
}
func (a *Playlist) GetArtwork() string {
return a.Resp.Data[0].Attributes.Artwork.URL
}
func (a *Playlist) ShowSelect() []int {
meta := a.Resp
trackTotal := len(meta.Data[0].Relationships.Tracks.Data)
arr := make([]int, trackTotal)
for i := 0; i < trackTotal; i++ {
arr[i] = i + 1
}
selected := []int{}
var data [][]string
for trackNum, track := range meta.Data[0].Relationships.Tracks.Data {
trackNum++
trackName := fmt.Sprintf("%s - %s", track.Attributes.Name, track.Attributes.ArtistName)
data = append(data, []string{fmt.Sprint(trackNum),
trackName,
track.Attributes.ContentRating,
track.Type})
}
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"", "Track Name", "Rating", "Type"})
//table.SetFooter([]string{"", "", "Footer", "Footer4"})
table.SetRowLine(false)
//table.SetAutoMergeCells(true)
table.SetCaption(true, fmt.Sprintf("Playlists: %d tracks", trackTotal))
table.SetHeaderColor(tablewriter.Colors{},
tablewriter.Colors{tablewriter.FgRedColor, tablewriter.Bold},
tablewriter.Colors{tablewriter.FgBlackColor, tablewriter.Bold},
tablewriter.Colors{tablewriter.FgBlackColor, tablewriter.Bold})
table.SetColumnColor(tablewriter.Colors{tablewriter.FgCyanColor},
tablewriter.Colors{tablewriter.Bold, tablewriter.FgRedColor},
tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor},
tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor})
for _, row := range data {
if row[2] == "explicit" {
row[2] = "E"
} else if row[2] == "clean" {
row[2] = "C"
} else {
row[2] = "None"
}
if row[3] == "music-videos" {
row[3] = "MV"
} else if row[3] == "songs" {
row[3] = "SONG"
}
table.Append(row)
}
//table.AppendBulk(data)
table.Render()
fmt.Println("Please select from the track options above (multiple options separated by commas, ranges supported, or type 'all' to select all)")
cyanColor := color.New(color.FgCyan)
cyanColor.Print("select: ")
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
fmt.Println(err)
}
input = strings.TrimSpace(input)
if input == "all" {
fmt.Println("You have selected all options:")
selected = arr
} else {
selectedOptions := [][]string{}
parts := strings.Split(input, ",")
for _, part := range parts {
if strings.Contains(part, "-") { // Range setting
rangeParts := strings.Split(part, "-")
selectedOptions = append(selectedOptions, rangeParts)
} else { // Single option
selectedOptions = append(selectedOptions, []string{part})
}
}
//
for _, opt := range selectedOptions {
if len(opt) == 1 { // Single option
num, err := strconv.Atoi(opt[0])
if err != nil {
fmt.Println("Invalid option:", opt[0])
continue
}
if num > 0 && num <= len(arr) {
selected = append(selected, num)
//args = append(args, urls[num-1])
} else {
fmt.Println("Option out of range:", opt[0])
}
} else if len(opt) == 2 { // Range
start, err1 := strconv.Atoi(opt[0])
end, err2 := strconv.Atoi(opt[1])
if err1 != nil || err2 != nil {
fmt.Println("Invalid range:", opt)
continue
}
if start < 1 || end > len(arr) || start > end {
fmt.Println("Range out of range:", opt)
continue
}
for i := start; i <= end; i++ {
//fmt.Println(options[i-1])
selected = append(selected, i)
}
} else {
fmt.Println("Invalid option:", opt)
}
}
}
return selected
}

209
utils/task/station.go Normal file
View File

@@ -0,0 +1,209 @@
package task
import (
//"bufio"
"errors"
"fmt"
//"os"
//"strconv"
//"strings"
//"github.com/fatih/color"
//"github.com/olekukonko/tablewriter"
"main/utils/ampapi"
)
type Station struct {
Storefront string
ID string
SaveDir string
SaveName string
Codec string
CoverPath string
Language string
Resp ampapi.StationResp
Type string
Name string
Tracks []Track
}
func NewStation(st string, id string) *Station {
a := new(Station)
a.Storefront = st
a.ID = id
//fmt.Println("Album created")
return a
}
func (a *Station) GetResp(mutoken, token, l string) error {
var err error
a.Language = l
resp, err := ampapi.GetStationResp(a.Storefront, a.ID, a.Language, token)
if err != nil {
return errors.New("error getting station response")
}
a.Resp = *resp
//简化高频调用名称
a.Type = a.Resp.Data[0].Attributes.PlayParams.Format
a.Name = a.Resp.Data[0].Attributes.Name
if a.Type != "tracks" {
return nil
}
tracksResp, err := ampapi.GetStationNextTracks(a.ID, mutoken, a.Language, token)
if err != nil {
return errors.New("error getting station tracks response")
}
//fmt.Println("Getting album response")
//从resp中的Tracks数据中提取trackData信息到新的Track结构体中
for i, trackData := range tracksResp.Data {
albumResp, err := ampapi.GetAlbumRespByHref(trackData.Href, a.Language, token)
if err != nil {
fmt.Println("Error getting album response:", err)
continue
}
albumLen := len(albumResp.Data[0].Relationships.Tracks.Data)
a.Tracks = append(a.Tracks, Track{
ID: trackData.ID,
Type: trackData.Type,
Name: trackData.Attributes.Name,
Language: a.Language,
Storefront: a.Storefront,
//SaveDir: filepath.Join(a.SaveDir, a.SaveName),
//Codec: a.Codec,
TaskNum: i + 1,
TaskTotal: len(tracksResp.Data),
M3u8: trackData.Attributes.ExtendedAssetUrls.EnhancedHls,
WebM3u8: trackData.Attributes.ExtendedAssetUrls.EnhancedHls,
//CoverPath: a.CoverPath,
Resp: trackData,
PreType: "stations",
DiscTotal: albumResp.Data[0].Relationships.Tracks.Data[albumLen-1].Attributes.DiscNumber,
PreID: a.ID,
AlbumData: albumResp.Data[0],
})
a.Tracks[i].PlaylistData.Attributes.Name = a.Name
a.Tracks[i].PlaylistData.Attributes.ArtistName = "Apple Music Station"
}
return nil
}
func (a *Station) GetArtwork() string {
return a.Resp.Data[0].Attributes.Artwork.URL
}
// func (a *Album) ShowSelect() []int {
// meta := a.Resp
// trackTotal := len(meta.Data[0].Relationships.Tracks.Data)
// arr := make([]int, trackTotal)
// for i := 0; i < trackTotal; i++ {
// arr[i] = i + 1
// }
// selected := []int{}
// var data [][]string
// for trackNum, track := range meta.Data[0].Relationships.Tracks.Data {
// trackNum++
// trackName := fmt.Sprintf("%02d. %s", track.Attributes.TrackNumber, track.Attributes.Name)
// data = append(data, []string{fmt.Sprint(trackNum),
// trackName,
// track.Attributes.ContentRating,
// track.Type})
// }
// table := tablewriter.NewWriter(os.Stdout)
// table.SetHeader([]string{"", "Track Name", "Rating", "Type"})
// //table.SetFooter([]string{"", "", "Footer", "Footer4"})
// table.SetRowLine(false)
// //table.SetAutoMergeCells(true)
// table.SetCaption(true, fmt.Sprintf("Storefront: %s, %d tracks missing", strings.ToUpper(a.Storefront), meta.Data[0].Attributes.TrackCount-trackTotal))
// table.SetHeaderColor(tablewriter.Colors{},
// tablewriter.Colors{tablewriter.FgRedColor, tablewriter.Bold},
// tablewriter.Colors{tablewriter.FgBlackColor, tablewriter.Bold},
// tablewriter.Colors{tablewriter.FgBlackColor, tablewriter.Bold})
// table.SetColumnColor(tablewriter.Colors{tablewriter.FgCyanColor},
// tablewriter.Colors{tablewriter.Bold, tablewriter.FgRedColor},
// tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor},
// tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor})
// for _, row := range data {
// if row[2] == "explicit" {
// row[2] = "E"
// } else if row[2] == "clean" {
// row[2] = "C"
// } else {
// row[2] = "None"
// }
// if row[3] == "music-videos" {
// row[3] = "MV"
// } else if row[3] == "songs" {
// row[3] = "SONG"
// }
// table.Append(row)
// }
// //table.AppendBulk(data)
// table.Render()
// fmt.Println("Please select from the track options above (multiple options separated by commas, ranges supported, or type 'all' to select all)")
// cyanColor := color.New(color.FgCyan)
// cyanColor.Print("select: ")
// reader := bufio.NewReader(os.Stdin)
// input, err := reader.ReadString('\n')
// if err != nil {
// fmt.Println(err)
// }
// input = strings.TrimSpace(input)
// if input == "all" {
// fmt.Println("You have selected all options:")
// selected = arr
// } else {
// selectedOptions := [][]string{}
// parts := strings.Split(input, ",")
// for _, part := range parts {
// if strings.Contains(part, "-") { // Range setting
// rangeParts := strings.Split(part, "-")
// selectedOptions = append(selectedOptions, rangeParts)
// } else { // Single option
// selectedOptions = append(selectedOptions, []string{part})
// }
// }
// //
// for _, opt := range selectedOptions {
// if len(opt) == 1 { // Single option
// num, err := strconv.Atoi(opt[0])
// if err != nil {
// fmt.Println("Invalid option:", opt[0])
// continue
// }
// if num > 0 && num <= len(arr) {
// selected = append(selected, num)
// //args = append(args, urls[num-1])
// } else {
// fmt.Println("Option out of range:", opt[0])
// }
// } else if len(opt) == 2 { // Range
// start, err1 := strconv.Atoi(opt[0])
// end, err2 := strconv.Atoi(opt[1])
// if err1 != nil || err2 != nil {
// fmt.Println("Invalid range:", opt)
// continue
// }
// if start < 1 || end > len(arr) || start > end {
// fmt.Println("Range out of range:", opt)
// continue
// }
// for i := start; i <= end; i++ {
// //fmt.Println(options[i-1])
// selected = append(selected, i)
// }
// } else {
// fmt.Println("Invalid option:", opt)
// }
// }
// }
// return selected
// }

50
utils/task/track.go Normal file
View File

@@ -0,0 +1,50 @@
package task
import (
"main/utils/ampapi"
)
type Track struct {
ID string
Type string
Name string
Storefront string
Language string
SaveDir string
SaveName string
SavePath string
Codec string
TaskNum int
TaskTotal int
M3u8 string
WebM3u8 string
DeviceM3u8 string
Quality string
CoverPath string
Resp ampapi.TrackRespData
PreType string // 上级类型 专辑或者歌单
PreID string // 上级ID
DiscTotal int
AlbumData ampapi.AlbumRespData
PlaylistData ampapi.PlaylistRespData
}
func (t *Track) GetAlbumData(token string) error {
var err error
resp, err := ampapi.GetAlbumRespByHref(t.Resp.Href, t.Language, token)
if err != nil {
return err
}
t.AlbumData = resp.Data[0]
//尝试获取该track所在album的disk总数
if len(resp.Data) > 0 {
len := len(resp.Data[0].Relationships.Tracks.Data)
if len > 0 {
t.DiscTotal = resp.Data[0].Relationships.Tracks.Data[len-1].Attributes.DiscNumber
}
}
return nil
}