package main
import (
"bufio"
"bytes"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"math"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"time"
"github.com/spf13/pflag"
"gopkg.in/yaml.v2"
"github.com/abema/go-mp4"
"github.com/beevik/etree"
"github.com/grafov/m3u8"
"github.com/schollz/progressbar/v3"
)
const (
defaultId = "0"
prefetchKey = "skd://itunes.apple.com/P000000000/s1/e1"
)
var (
forbiddenNames = regexp . MustCompile ( ` [/\\<>:"|?*] ` )
)
var (
dl_atmos bool
dl_select bool
artist_select bool
alac_max * int
atmos_max * int
)
type Config struct {
MediaUserToken string ` yaml:"media-user-token" `
AuthorizationToken string ` yaml:"authorization-token" `
SaveLrcFile bool ` yaml:"save-lrc-file" `
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" `
ForceApi bool ` yaml:"force-api" `
Check string ` yaml:"check" `
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" `
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" `
}
var config Config
var txtpath string
//统计结果
var counter Counter
type Counter struct {
Unavailable int
NotSong int
Error int
Success int
Total int
}
var okDict = make ( map [ string ] [ ] int )
type SampleInfo struct {
data [ ] byte
duration uint32
descIndex uint32
}
type SongInfo struct {
r io . ReadSeeker
alacParam * Alac
samples [ ] SampleInfo
totalDataSize int64
}
func loadConfig ( ) error {
// 读取config.yaml文件内容
data , err := ioutil . ReadFile ( "config.yaml" )
if err != nil {
return err
}
// 将yaml解析到config变量中
err = yaml . Unmarshal ( data , & config )
if err != nil {
return err
}
return nil
}
func LimitString ( s string ) string {
if len ( [ ] rune ( s ) ) > config . LimitMax {
return string ( [ ] rune ( s ) [ : config . LimitMax ] )
}
return s
}
func ( s * SongInfo ) Duration ( ) ( ret uint64 ) {
for i := range s . samples {
ret += uint64 ( s . samples [ i ] . duration )
}
return
}
func ( * Alac ) GetType ( ) mp4 . BoxType {
return BoxTypeAlac ( )
}
func isInArray ( arr [ ] int , target int ) bool {
for _ , num := range arr {
if num == target {
return true
}
}
return false
}
func fileExists ( path string ) ( bool , error ) {
f , err := os . Stat ( path )
if err == nil {
return ! f . IsDir ( ) , nil
} else if os . IsNotExist ( err ) {
return false , nil
}
return false , err
}
func writeM4a ( w * mp4 . Writer , info * SongInfo , meta * AutoGenerated , data [ ] byte , trackNum , trackTotal int ) error {
index := trackNum - 1
{ // ftyp
box , err := w . StartBox ( & mp4 . BoxInfo { Type : mp4 . BoxTypeFtyp ( ) } )
if err != nil {
return err
}
_ , err = mp4 . Marshal ( w , & mp4 . Ftyp {
MajorBrand : [ 4 ] byte { 'M' , '4' , 'A' , ' ' } ,
MinorVersion : 0 ,
CompatibleBrands : [ ] mp4 . CompatibleBrandElem {
{ CompatibleBrand : [ 4 ] byte { 'M' , '4' , 'A' , ' ' } } ,
{ CompatibleBrand : [ 4 ] byte { 'm' , 'p' , '4' , '2' } } ,
{ CompatibleBrand : mp4 . BrandISOM ( ) } ,
{ CompatibleBrand : [ 4 ] byte { 0 , 0 , 0 , 0 } } ,
} ,
} , box . Context )
if err != nil {
return err
}
_ , err = w . EndBox ( )
if err != nil {
return err
}
}
const chunkSize uint32 = 5
duration := info . Duration ( )
numSamples := uint32 ( len ( info . samples ) )
var stco * mp4 . BoxInfo
{ // moov
_ , err := w . StartBox ( & mp4 . BoxInfo { Type : mp4 . BoxTypeMoov ( ) } )
if err != nil {
return err
}
box , err := mp4 . ExtractBox ( info . r , nil , mp4 . BoxPath { mp4 . BoxTypeMoov ( ) } )
if err != nil {
return err
}
moovOri := box [ 0 ]
{ // mvhd
_ , err = w . StartBox ( & mp4 . BoxInfo { Type : mp4 . BoxTypeMvhd ( ) } )
if err != nil {
return err
}
oriBox , err := mp4 . ExtractBoxWithPayload ( info . r , moovOri , mp4 . BoxPath { mp4 . BoxTypeMvhd ( ) } )
if err != nil {
return err
}
mvhd := oriBox [ 0 ] . Payload . ( * mp4 . Mvhd )
if mvhd . Version == 0 {
mvhd . DurationV0 = uint32 ( duration )
} else {
mvhd . DurationV1 = duration
}
_ , err = mp4 . Marshal ( w , mvhd , oriBox [ 0 ] . Info . Context )
if err != nil {
return err
}
_ , err = w . EndBox ( )
if err != nil {
return err
}
}
{ // trak
_ , err = w . StartBox ( & mp4 . BoxInfo { Type : mp4 . BoxTypeTrak ( ) } )
if err != nil {
return err
}
box , err := mp4 . ExtractBox ( info . r , moovOri , mp4 . BoxPath { mp4 . BoxTypeTrak ( ) } )
if err != nil {
return err
}
trakOri := box [ 0 ]
{ // tkhd
_ , err = w . StartBox ( & mp4 . BoxInfo { Type : mp4 . BoxTypeTkhd ( ) } )
if err != nil {
return err
}
oriBox , err := mp4 . ExtractBoxWithPayload ( info . r , trakOri , mp4 . BoxPath { mp4 . BoxTypeTkhd ( ) } )
if err != nil {
return err
}
tkhd := oriBox [ 0 ] . Payload . ( * mp4 . Tkhd )
if tkhd . Version == 0 {
tkhd . DurationV0 = uint32 ( duration )
} else {
tkhd . DurationV1 = duration
}
tkhd . SetFlags ( 0x7 )
_ , err = mp4 . Marshal ( w , tkhd , oriBox [ 0 ] . Info . Context )
if err != nil {
return err
}
_ , err = w . EndBox ( )
if err != nil {
return err
}
}
{ // mdia
_ , err = w . StartBox ( & mp4 . BoxInfo { Type : mp4 . BoxTypeMdia ( ) } )
if err != nil {
return err
}
box , err := mp4 . ExtractBox ( info . r , trakOri , mp4 . BoxPath { mp4 . BoxTypeMdia ( ) } )
if err != nil {
return err
}
mdiaOri := box [ 0 ]
{ // mdhd
_ , err = w . StartBox ( & mp4 . BoxInfo { Type : mp4 . BoxTypeMdhd ( ) } )
if err != nil {
return err
}
oriBox , err := mp4 . ExtractBoxWithPayload ( info . r , mdiaOri , mp4 . BoxPath { mp4 . BoxTypeMdhd ( ) } )
if err != nil {
return err
}
mdhd := oriBox [ 0 ] . Payload . ( * mp4 . Mdhd )
if mdhd . Version == 0 {
mdhd . DurationV0 = uint32 ( duration )
} else {
mdhd . DurationV1 = duration
}
_ , err = mp4 . Marshal ( w , mdhd , oriBox [ 0 ] . Info . Context )
if err != nil {
return err
}
_ , err = w . EndBox ( )
if err != nil {
return err
}
}
{ // hdlr
oriBox , err := mp4 . ExtractBox ( info . r , mdiaOri , mp4 . BoxPath { mp4 . BoxTypeHdlr ( ) } )
if err != nil {
return err
}
err = w . CopyBox ( info . r , oriBox [ 0 ] )
if err != nil {
return err
}
}
{ // minf
_ , err = w . StartBox ( & mp4 . BoxInfo { Type : mp4 . BoxTypeMinf ( ) } )
if err != nil {
return err
}
box , err := mp4 . ExtractBox ( info . r , mdiaOri , mp4 . BoxPath { mp4 . BoxTypeMinf ( ) } )
if err != nil {
return err
}
minfOri := box [ 0 ]
{ // smhd, dinf
boxes , err := mp4 . ExtractBoxes ( info . r , minfOri , [ ] mp4 . BoxPath {
{ mp4 . BoxTypeSmhd ( ) } ,
{ mp4 . BoxTypeDinf ( ) } ,
} )
if err != nil {
return err
}
for _ , b := range boxes {
err = w . CopyBox ( info . r , b )
if err != nil {
return err
}
}
}
{ // stbl
_ , err = w . StartBox ( & mp4 . BoxInfo { Type : mp4 . BoxTypeStbl ( ) } )
if err != nil {
return err
}
{ // stsd
box , err := w . StartBox ( & mp4 . BoxInfo { Type : mp4 . BoxTypeStsd ( ) } )
if err != nil {
return err
}
_ , err = mp4 . Marshal ( w , & mp4 . Stsd { EntryCount : 1 } , box . Context )
if err != nil {
return err
}
{ // alac
_ , err = w . StartBox ( & mp4 . BoxInfo { Type : BoxTypeAlac ( ) } )
if err != nil {
return err
}
_ , err = w . Write ( [ ] byte {
0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 ,
0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 } )
if err != nil {
return err
}
err = binary . Write ( w , binary . BigEndian , uint16 ( info . alacParam . NumChannels ) )
if err != nil {
return err
}
err = binary . Write ( w , binary . BigEndian , uint16 ( info . alacParam . BitDepth ) )
if err != nil {
return err
}
_ , err = w . Write ( [ ] byte { 0 , 0 } )
if err != nil {
return err
}
err = binary . Write ( w , binary . BigEndian , info . alacParam . SampleRate )
if err != nil {
return err
}
_ , err = w . Write ( [ ] byte { 0 , 0 } )
if err != nil {
return err
}
box , err := w . StartBox ( & mp4 . BoxInfo { Type : BoxTypeAlac ( ) } )
if err != nil {
return err
}
_ , err = mp4 . Marshal ( w , info . alacParam , box . Context )
if err != nil {
return err
}
_ , err = w . EndBox ( )
if err != nil {
return err
}
_ , err = w . EndBox ( )
if err != nil {
return err
}
}
_ , err = w . EndBox ( )
if err != nil {
return err
}
}
{ // stts
box , err := w . StartBox ( & mp4 . BoxInfo { Type : mp4 . BoxTypeStts ( ) } )
if err != nil {
return err
}
var stts mp4 . Stts
for _ , sample := range info . samples {
if len ( stts . Entries ) != 0 {
last := & stts . Entries [ len ( stts . Entries ) - 1 ]
if last . SampleDelta == sample . duration {
last . SampleCount ++
continue
}
}
stts . Entries = append ( stts . Entries , mp4 . SttsEntry {
SampleCount : 1 ,
SampleDelta : sample . duration ,
} )
}
stts . EntryCount = uint32 ( len ( stts . Entries ) )
_ , err = mp4 . Marshal ( w , & stts , box . Context )
if err != nil {
return err
}
_ , err = w . EndBox ( )
if err != nil {
return err
}
}
{ // stsc
box , err := w . StartBox ( & mp4 . BoxInfo { Type : mp4 . BoxTypeStsc ( ) } )
if err != nil {
return err
}
if numSamples % chunkSize == 0 {
_ , err = mp4 . Marshal ( w , & mp4 . Stsc {
EntryCount : 1 ,
Entries : [ ] mp4 . StscEntry {
{
FirstChunk : 1 ,
SamplesPerChunk : chunkSize ,
SampleDescriptionIndex : 1 ,
} ,
} ,
} , box . Context )
} else {
_ , err = mp4 . Marshal ( w , & mp4 . Stsc {
EntryCount : 2 ,
Entries : [ ] mp4 . StscEntry {
{
FirstChunk : 1 ,
SamplesPerChunk : chunkSize ,
SampleDescriptionIndex : 1 ,
} , {
FirstChunk : numSamples / chunkSize + 1 ,
SamplesPerChunk : numSamples % chunkSize ,
SampleDescriptionIndex : 1 ,
} ,
} ,
} , box . Context )
}
_ , err = w . EndBox ( )
if err != nil {
return err
}
}
{ // stsz
box , err := w . StartBox ( & mp4 . BoxInfo { Type : mp4 . BoxTypeStsz ( ) } )
if err != nil {
return err
}
stsz := mp4 . Stsz { SampleCount : numSamples }
for _ , sample := range info . samples {
stsz . EntrySize = append ( stsz . EntrySize , uint32 ( len ( sample . data ) ) )
}
_ , err = mp4 . Marshal ( w , & stsz , box . Context )
if err != nil {
return err
}
_ , err = w . EndBox ( )
if err != nil {
return err
}
}
{ // stco
box , err := w . StartBox ( & mp4 . BoxInfo { Type : mp4 . BoxTypeStco ( ) } )
if err != nil {
return err
}
l := ( numSamples + chunkSize - 1 ) / chunkSize
_ , err = mp4 . Marshal ( w , & mp4 . Stco {
EntryCount : l ,
ChunkOffset : make ( [ ] uint32 , l ) ,
} , box . Context )
stco , err = w . EndBox ( )
if err != nil {
return err
}
}
_ , err = w . EndBox ( )
if err != nil {
return err
}
}
_ , err = w . EndBox ( )
if err != nil {
return err
}
}
_ , err = w . EndBox ( )
if err != nil {
return err
}
}
_ , err = w . EndBox ( )
if err != nil {
return err
}
}
{ // udta
ctx := mp4 . Context { UnderUdta : true }
_ , err := w . StartBox ( & mp4 . BoxInfo { Type : mp4 . BoxTypeUdta ( ) , Context : ctx } )
if err != nil {
return err
}
{ // meta
ctx . UnderIlstMeta = true
_ , err = w . StartBox ( & mp4 . BoxInfo { Type : mp4 . BoxTypeMeta ( ) , Context : ctx } )
if err != nil {
return err
}
_ , err = mp4 . Marshal ( w , & mp4 . Meta { } , ctx )
if err != nil {
return err
}
{ // hdlr
_ , err = w . StartBox ( & mp4 . BoxInfo { Type : mp4 . BoxTypeHdlr ( ) , Context : ctx } )
if err != nil {
return err
}
_ , err = mp4 . Marshal ( w , & mp4 . Hdlr {
HandlerType : [ 4 ] byte { 'm' , 'd' , 'i' , 'r' } ,
Reserved : [ 3 ] uint32 { 0x6170706c , 0 , 0 } ,
} , ctx )
if err != nil {
return err
}
_ , err = w . EndBox ( )
if err != nil {
return err
}
}
{ // ilst
ctx . UnderIlst = true
_ , err = w . StartBox ( & mp4 . BoxInfo { Type : mp4 . BoxTypeIlst ( ) , Context : ctx } )
if err != nil {
return err
}
marshalData := func ( val interface { } ) error {
_ , err = w . StartBox ( & mp4 . BoxInfo { Type : mp4 . BoxTypeData ( ) } )
if err != nil {
return err
}
var boxData mp4 . Data
switch v := val . ( type ) {
case string :
boxData . DataType = mp4 . DataTypeStringUTF8
boxData . Data = [ ] byte ( v )
case uint8 :
boxData . DataType = mp4 . DataTypeSignedIntBigEndian
boxData . Data = [ ] byte { v }
case uint32 :
boxData . DataType = mp4 . DataTypeSignedIntBigEndian
boxData . Data = make ( [ ] byte , 4 )
binary . BigEndian . PutUint32 ( boxData . Data , v )
case [ ] byte :
boxData . DataType = mp4 . DataTypeBinary
boxData . Data = v
default :
panic ( "unsupported value" )
}
_ , err = mp4 . Marshal ( w , & boxData , ctx )
if err != nil {
return err
}
_ , err = w . EndBox ( )
return err
}
addMeta := func ( tag mp4 . BoxType , val interface { } ) error {
_ , err = w . StartBox ( & mp4 . BoxInfo { Type : tag } )
if err != nil {
return err
}
err = marshalData ( val )
if err != nil {
return err
}
_ , err = w . EndBox ( )
return err
}
addExtendedMeta := func ( name string , val interface { } ) error {
ctx . UnderIlstFreeMeta = true
_ , err = w . StartBox ( & mp4 . BoxInfo { Type : mp4 . BoxType { '-' , '-' , '-' , '-' } , Context : ctx } )
if err != nil {
return err
}
{
_ , err = w . StartBox ( & mp4 . BoxInfo { Type : mp4 . BoxType { 'm' , 'e' , 'a' , 'n' } , Context : ctx } )
if err != nil {
return err
}
_ , err = w . Write ( [ ] byte { 0 , 0 , 0 , 0 } )
if err != nil {
return err
}
_ , err = io . WriteString ( w , "com.apple.iTunes" )
if err != nil {
return err
}
_ , err = w . EndBox ( )
if err != nil {
return err
}
}
{
_ , err = w . StartBox ( & mp4 . BoxInfo { Type : mp4 . BoxType { 'n' , 'a' , 'm' , 'e' } , Context : ctx } )
if err != nil {
return err
}
_ , err = w . Write ( [ ] byte { 0 , 0 , 0 , 0 } )
if err != nil {
return err
}
_ , err = io . WriteString ( w , name )
if err != nil {
return err
}
_ , err = w . EndBox ( )
if err != nil {
return err
}
}
err = marshalData ( val )
if err != nil {
return err
}
ctx . UnderIlstFreeMeta = false
_ , err = w . EndBox ( )
return err
}
err = addMeta ( mp4 . BoxType { '\251' , 'n' , 'a' , 'm' } , meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . Name )
if err != nil {
return err
}
err = addMeta ( mp4 . BoxType { 's' , 'o' , 'n' , 'm' } , meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . Name )
if err != nil {
return err
}
AlbumName := meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . AlbumName
if strings . Contains ( meta . Data [ 0 ] . ID , "pl." ) {
if ! config . UseSongInfoForPlaylist {
AlbumName = meta . Data [ 0 ] . Attributes . Name
}
}
err = addMeta ( mp4 . BoxType { '\251' , 'a' , 'l' , 'b' } , AlbumName )
if err != nil {
return err
}
err = addMeta ( mp4 . BoxType { 's' , 'o' , 'a' , 'l' } , AlbumName )
if err != nil {
return err
}
err = addMeta ( mp4 . BoxType { '\251' , 'A' , 'R' , 'T' } , meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . ArtistName )
if err != nil {
return err
}
err = addMeta ( mp4 . BoxType { 's' , 'o' , 'a' , 'r' } , meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . ArtistName )
if err != nil {
return err
}
err = addMeta ( mp4 . BoxType { '\251' , 'p' , 'r' , 'f' } , meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . ArtistName )
if err != nil {
return err
}
err = addExtendedMeta ( "PERFORMER" , meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . ArtistName )
if err != nil {
return err
}
err = addExtendedMeta ( "ITUNESALBUMID" , meta . Data [ 0 ] . ID )
if err != nil {
return err
}
err = addMeta ( mp4 . BoxType { '\251' , 'w' , 'r' , 't' } , meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . ComposerName )
if err != nil {
return err
}
err = addMeta ( mp4 . BoxType { 's' , 'o' , 'c' , 'o' } , meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . ComposerName )
if err != nil {
return err
}
err = addMeta ( mp4 . BoxType { '\251' , 'd' , 'a' , 'y' } , meta . Data [ 0 ] . Attributes . ReleaseDate )
if err != nil {
return err
}
err = addExtendedMeta ( "RELEASETIME" , meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . ReleaseDate )
if err != nil {
return err
}
cnID , err := strconv . ParseUint ( meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . ID , 10 , 32 )
if err != nil {
return err
}
err = addMeta ( mp4 . BoxType { 'c' , 'n' , 'I' , 'D' } , uint32 ( cnID ) )
if err != nil {
return err
}
err = addExtendedMeta ( "ISRC" , meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . Isrc )
if err != nil {
return err
}
if len ( meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . GenreNames ) > 0 {
err = addMeta ( mp4 . BoxType { '\251' , 'g' , 'e' , 'n' } , meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . GenreNames [ 0 ] )
if err != nil {
return err
}
}
if len ( meta . Data ) > 0 {
album := meta . Data [ 0 ]
err = addMeta ( mp4 . BoxType { 'a' , 'A' , 'R' , 'T' } , meta . Data [ 0 ] . Attributes . ArtistName )
if err != nil {
return err
}
err = addMeta ( mp4 . BoxType { 's' , 'o' , 'a' , 'a' } , meta . Data [ 0 ] . Attributes . ArtistName )
if err != nil {
return err
}
err = addMeta ( mp4 . BoxType { 'c' , 'p' , 'r' , 't' } , album . Attributes . Copyright )
if err != nil {
return err
}
var isCpil uint8
if album . Attributes . IsCompilation {
isCpil = 1
}
err = addMeta ( mp4 . BoxType { 'c' , 'p' , 'i' , 'l' } , isCpil )
if err != nil {
return err
}
err = addMeta ( mp4 . BoxType { '\251' , 'p' , 'u' , 'b' } , album . Attributes . RecordLabel )
if err != nil {
return err
}
err = addExtendedMeta ( "LABEL" , album . Attributes . RecordLabel )
if err != nil {
return err
}
err = addExtendedMeta ( "UPC" , album . Attributes . Upc )
if err != nil {
return err
}
if ! strings . Contains ( meta . Data [ 0 ] . ID , "pl." ) {
plID , err := strconv . ParseUint ( meta . Data [ 0 ] . ID , 10 , 32 )
if err != nil {
return err
}
err = addMeta ( mp4 . BoxType { 'p' , 'l' , 'I' , 'D' } , uint32 ( plID ) )
if err != nil {
return err
}
}
}
if len ( meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Relationships . Artists . Data ) > 0 {
if len ( meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Relationships . Artists . Data [ 0 ] . ID ) > 0 {
atID , err := strconv . ParseUint ( meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Relationships . Artists . Data [ 0 ] . ID , 10 , 32 )
if err != nil {
return err
}
err = addMeta ( mp4 . BoxType { 'a' , 't' , 'I' , 'D' } , uint32 ( atID ) )
if err != nil {
return err
}
}
}
trkn := make ( [ ] byte , 8 )
disk := make ( [ ] byte , 8 )
binary . BigEndian . PutUint32 ( trkn , uint32 ( meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . TrackNumber ) )
binary . BigEndian . PutUint16 ( trkn [ 4 : ] , uint16 ( trackTotal ) )
binary . BigEndian . PutUint32 ( disk , uint32 ( meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . DiscNumber ) )
binary . BigEndian . PutUint16 ( disk [ 4 : ] , uint16 ( meta . Data [ 0 ] . Relationships . Tracks . Data [ trackTotal - 1 ] . Attributes . DiscNumber ) )
if strings . Contains ( meta . Data [ 0 ] . ID , "pl." ) {
if ! config . UseSongInfoForPlaylist {
binary . BigEndian . PutUint32 ( trkn , uint32 ( trackNum ) )
binary . BigEndian . PutUint16 ( trkn [ 4 : ] , uint16 ( trackTotal ) )
binary . BigEndian . PutUint32 ( disk , uint32 ( 1 ) )
binary . BigEndian . PutUint16 ( disk [ 4 : ] , uint16 ( 1 ) )
}
}
err = addMeta ( mp4 . BoxType { 't' , 'r' , 'k' , 'n' } , trkn )
if err != nil {
return err
}
err = addMeta ( mp4 . BoxType { 'd' , 'i' , 's' , 'k' } , disk )
if err != nil {
return err
}
ctx . UnderIlst = false
_ , err = w . EndBox ( )
if err != nil {
return err
}
}
ctx . UnderIlstMeta = false
_ , err = w . EndBox ( )
if err != nil {
return err
}
}
ctx . UnderUdta = false
_ , err = w . EndBox ( )
if err != nil {
return err
}
}
_ , err = w . EndBox ( )
if err != nil {
return err
}
}
{
box , err := w . StartBox ( & mp4 . BoxInfo { Type : mp4 . BoxTypeMdat ( ) } )
if err != nil {
return err
}
_ , err = mp4 . Marshal ( w , & mp4 . Mdat { Data : data } , box . Context )
if err != nil {
return err
}
mdat , err := w . EndBox ( )
var realStco mp4 . Stco
offset := mdat . Offset + mdat . HeaderSize
for i := uint32 ( 0 ) ; i < numSamples ; i ++ {
if i % chunkSize == 0 {
realStco . EntryCount ++
realStco . ChunkOffset = append ( realStco . ChunkOffset , uint32 ( offset ) )
}
offset += uint64 ( len ( info . samples [ i ] . data ) )
}
_ , err = stco . SeekToPayload ( w )
if err != nil {
return err
}
_ , err = mp4 . Marshal ( w , & realStco , box . Context )
if err != nil {
return err
}
}
return nil
}
func decryptSong ( info * SongInfo , keys [ ] string , manifest * AutoGenerated , filename string , trackNum , trackTotal int ) error {
//fmt.Printf("%d-bit / %d Hz\n", info.bitDepth, info.bitRate)
conn , err := net . Dial ( "tcp" , config . DecryptM3u8Port )
if err != nil {
return err
}
defer conn . Close ( )
var decrypted [ ] byte
var lastIndex uint32 = math . MaxUint8
fmt . Println ( "Decrypting..." )
bar := progressbar . NewOptions64 ( info . totalDataSize ,
progressbar . OptionClearOnFinish ( ) ,
progressbar . OptionSetElapsedTime ( false ) ,
progressbar . OptionSetPredictTime ( false ) ,
progressbar . OptionShowElapsedTimeOnFinish ( ) ,
progressbar . OptionShowCount ( ) ,
progressbar . OptionEnableColorCodes ( true ) ,
progressbar . OptionShowBytes ( true ) ,
//progressbar.OptionSetDescription("Decrypting..."),
progressbar . OptionSetTheme ( progressbar . Theme {
Saucer : "" ,
SaucerHead : "" ,
SaucerPadding : "" ,
BarStart : "" ,
BarEnd : "" ,
} ) ,
)
for _ , sp := range info . samples {
if lastIndex != sp . descIndex {
if len ( decrypted ) != 0 {
_ , err := conn . Write ( [ ] byte { 0 , 0 , 0 , 0 } )
if err != nil {
return err
}
}
keyUri := keys [ sp . descIndex ]
id := manifest . Data [ 0 ] . Relationships . Tracks . Data [ trackNum - 1 ] . ID
if keyUri == prefetchKey {
id = defaultId
}
_ , err := conn . Write ( [ ] byte { byte ( len ( id ) ) } )
if err != nil {
return err
}
_ , err = io . WriteString ( conn , id )
if err != nil {
return err
}
_ , err = conn . Write ( [ ] byte { byte ( len ( keyUri ) ) } )
if err != nil {
return err
}
_ , err = io . WriteString ( conn , keyUri )
if err != nil {
return err
}
}
lastIndex = sp . descIndex
err := binary . Write ( conn , binary . LittleEndian , uint32 ( len ( sp . data ) ) )
if err != nil {
return err
}
_ , err = conn . Write ( sp . data )
if err != nil {
return err
}
de := make ( [ ] byte , len ( sp . data ) )
_ , err = io . ReadFull ( conn , de )
if err != nil {
return err
}
decrypted = append ( decrypted , de ... )
bar . Add ( len ( sp . data ) )
}
_ , _ = conn . Write ( [ ] byte { 0 , 0 , 0 , 0 , 0 } )
fmt . Println ( "Decrypted." )
create , err := os . Create ( filename )
if err != nil {
return err
}
defer create . Close ( )
if dl_atmos {
_ , err = create . Write ( decrypted )
if err != nil {
panic ( err )
}
return nil
}
return writeM4a ( mp4 . NewWriter ( create ) , info , manifest , decrypted , trackNum , trackTotal )
}
func checkUrl ( url string ) ( string , string ) {
pat := regexp . MustCompile ( ` ^(?:https:\/\/(?:beta\.music|music)\.apple\.com\/(\w { 2})(?:\/album|\/album\/.+))\/(?:id)?(\d[^\D]+)(?:$|\?) ` )
matches := pat . FindAllStringSubmatch ( url , - 1 )
if matches == nil {
return "" , ""
} else {
return matches [ 0 ] [ 1 ] , matches [ 0 ] [ 2 ]
}
}
func checkUrlPlaylist ( url string ) ( string , string ) {
pat := regexp . MustCompile ( ` ^(?:https:\/\/(?:beta\.music|music)\.apple\.com\/(\w { 2})(?:\/playlist|\/playlist\/.+))\/(?:id)?(pl\.[\w-]+)(?:$|\?) ` )
matches := pat . FindAllStringSubmatch ( url , - 1 )
if matches == nil {
return "" , ""
} else {
return matches [ 0 ] [ 1 ] , matches [ 0 ] [ 2 ]
}
}
func checkUrlArtist ( url string ) ( string , string ) {
pat := regexp . MustCompile ( ` ^(?:https:\/\/(?:beta\.music|music)\.apple\.com\/(\w { 2})(?:\/artist|\/artist\/.+))\/(?:id)?(\d[^\D]+)(?:$|\?) ` )
matches := pat . FindAllStringSubmatch ( url , - 1 )
if matches == nil {
return "" , ""
} else {
return matches [ 0 ] [ 1 ] , matches [ 0 ] [ 2 ]
}
}
func getUrlArtistName ( artistUrl string , token string ) ( string , error ) {
storefront , artistId := checkUrlArtist ( artistUrl )
req , err := http . NewRequest ( "GET" , fmt . Sprintf ( "https://amp-api.music.apple.com/v1/catalog/%s/artists/%s" , storefront , artistId ) , 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" )
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 ( AutoGeneratedArtist )
err = json . NewDecoder ( do . Body ) . Decode ( & obj )
if err != nil {
return "" , err
}
return obj . Data [ 0 ] . Attributes . Name , nil
}
func checkArtist ( artistUrl string , token string ) ( [ ] string , error ) {
storefront , artistId := checkUrlArtist ( artistUrl )
Num := 0
var args [ ] string
var urls [ ] string
var options [ ] string
for {
req , err := http . NewRequest ( "GET" , fmt . Sprintf ( "https://amp-api.music.apple.com/v1/catalog/%s/artists/%s/albums?limit=100&offset=%d" , storefront , artistId , Num ) , 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" )
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 ( AutoGeneratedArtist )
err = json . NewDecoder ( do . Body ) . Decode ( & obj )
if err != nil {
return nil , err
}
for _ , album := range obj . Data {
urls = append ( urls , album . Attributes . URL )
options = append ( options , fmt . Sprintf ( "%s(%s)" , album . Attributes . Name , album . ID ) )
}
Num = Num + 100
if len ( obj . Next ) == 0 {
break
}
}
for i , option := range options {
fmt . Printf ( "%02d: %s\n" , i + 1 , option )
}
if artist_select {
fmt . Println ( "You have selected all options:" )
return urls , nil
}
reader := bufio . NewReader ( os . Stdin )
fmt . Println ( "Please select from the following options (multiple options separated by commas, ranges supported, or type 'all' to select all)" )
fmt . Print ( "Enter your choice: " )
input , _ := reader . ReadString ( '\n' )
// Remove newline and whitespace
input = strings . TrimSpace ( input )
if input == "all" {
fmt . Println ( "You have selected all options:" )
return urls , nil
}
// Split input into string slices
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 } )
}
}
// Print selected options
fmt . Println ( "You have selected the following options:" )
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 ( options ) {
fmt . Println ( options [ num - 1 ] )
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 ( options ) || start > end {
fmt . Println ( "Range out of range:" , opt )
continue
}
for i := start ; i <= end ; i ++ {
fmt . Println ( options [ i - 1 ] )
args = append ( args , urls [ i - 1 ] )
}
} else {
fmt . Println ( "Invalid option:" , opt )
}
}
return args , nil
}
func getMeta ( albumId string , token string , storefront string ) ( * AutoGenerated , error ) {
var mtype string
var page int
if strings . Contains ( albumId , "pl." ) {
mtype = "playlists"
} else {
mtype = "albums"
}
req , err := http . NewRequest ( "GET" , fmt . Sprintf ( "https://amp-api.music.apple.com/v1/catalog/%s/%s/%s" , storefront , mtype , albumId ) , 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" )
// query.Set("l", "en-gb")
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 ( AutoGenerated )
err = json . NewDecoder ( do . Body ) . Decode ( & obj )
if err != nil {
return nil , err
}
if strings . Contains ( albumId , "pl." ) {
obj . Data [ 0 ] . Attributes . ArtistName = "Apple Music"
if len ( obj . Data [ 0 ] . Relationships . Tracks . Next ) > 0 {
page = 0
for {
page = page + 100
pageStr := strconv . Itoa ( page )
req , err := http . NewRequest ( "GET" , fmt . Sprintf ( "https://amp-api.music.apple.com/v1/catalog/%s/%s/%s/tracks?offset=%s" , storefront , mtype , albumId , pageStr ) , 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" )
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 ( AutoGeneratedTrack )
err = json . NewDecoder ( do . Body ) . Decode ( & obj2 )
if err != nil {
return nil , err
}
for _ , value := range obj2 . Data {
obj . Data [ 0 ] . Relationships . Tracks . Data = append ( obj . Data [ 0 ] . Relationships . Tracks . Data , value )
}
if len ( obj2 . Next ) == 0 {
break
}
}
}
}
return obj , nil
}
func getSongLyrics ( songId string , storefront string , token string , userToken string ) ( string , error ) {
req , err := http . NewRequest ( "GET" ,
fmt . Sprintf ( "https://amp-api.music.apple.com/v1/catalog/%s/songs/%s/%s" , storefront , songId , config . LrcFormat ) , nil )
if err != nil {
return "" , err
}
req . Header . Set ( "Origin" , "https://music.apple.com" )
req . Header . Set ( "Referer" , "https://music.apple.com/" )
req . Header . Set ( "Authorization" , fmt . Sprintf ( "Bearer %s" , token ) )
cookie := http . Cookie { Name : "media-user-token" , Value : userToken }
req . AddCookie ( & cookie )
do , err := http . DefaultClient . Do ( req )
if err != nil {
return "" , err
}
defer do . Body . Close ( )
obj := new ( SongLyrics )
err = json . NewDecoder ( do . Body ) . Decode ( & obj )
if obj . Data != nil {
return obj . Data [ 0 ] . Attributes . Ttml , nil
} else {
return "" , errors . New ( "failed to get lyrics" )
}
}
func writeCover ( sanAlbumFolder , name string , url string ) error {
covPath := filepath . Join ( sanAlbumFolder , name + "." + config . CoverFormat )
if config . CoverFormat == "original" {
ext := strings . Split ( url , "/" ) [ len ( strings . Split ( url , "/" ) ) - 2 ]
ext = ext [ strings . LastIndex ( ext , "." ) + 1 : ]
covPath = filepath . Join ( sanAlbumFolder , name + "." + ext )
}
exists , err := fileExists ( covPath )
if err != nil {
fmt . Println ( "Failed to check if cover exists." )
return err
}
if exists {
return nil
}
if config . CoverFormat == "png" {
re := regexp . MustCompile ( ` \ { w\}x\ { h\} ` )
parts := re . Split ( url , 2 )
url = parts [ 0 ] + "{w}x{h}" + strings . Replace ( parts [ 1 ] , ".jpg" , ".png" , 1 )
}
url = strings . Replace ( url , "{w}x{h}" , config . CoverSize , 1 )
if config . CoverFormat == "original" {
url = strings . Replace ( url , "is1-ssl.mzstatic.com/image/thumb" , "a5.mzstatic.com/us/r1000/0" , 1 )
url = url [ : strings . LastIndex ( url , "/" ) ]
}
req , err := http . NewRequest ( "GET" , url , nil )
if err != nil {
return err
}
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" )
do , err := http . DefaultClient . Do ( req )
if err != nil {
return err
}
defer do . Body . Close ( )
if do . StatusCode != http . StatusOK {
errors . New ( do . Status )
}
f , err := os . Create ( covPath )
if err != nil {
return err
}
defer f . Close ( )
_ , err = io . Copy ( f , do . Body )
if err != nil {
return err
}
return nil
}
func writeLyrics ( sanAlbumFolder , filename string , lrc string ) error {
lyricspath := filepath . Join ( sanAlbumFolder , filename )
f , err := os . Create ( lyricspath )
if err != nil {
return err
}
defer f . Close ( )
_ , err = f . WriteString ( lrc )
if err != nil {
return err
}
return nil
}
func contains ( slice [ ] string , item string ) bool {
for _ , v := range slice {
if v == item {
return true
}
}
return false
}
func rip ( albumId string , token string , storefront string , userToken string ) error {
var Codec string
if dl_atmos {
Codec = "Atmos"
} else {
Codec = "ALAC"
}
meta , err := getMeta ( albumId , token , storefront )
if err != nil {
fmt . Println ( "Failed to get album metadata.\n" )
return err
}
var singerFoldername string
if config . ArtistFolderFormat != "" {
if strings . Contains ( albumId , "pl." ) {
singerFoldername = strings . NewReplacer (
"{ArtistName}" , "Apple Music" ,
"{ArtistId}" , "" ,
"{UrlArtistName}" , "Apple Music" ,
) . Replace ( config . ArtistFolderFormat )
} else if len ( meta . Data [ 0 ] . Relationships . Artists . Data ) > 0 {
singerFoldername = strings . NewReplacer (
"{UrlArtistName}" , LimitString ( meta . Data [ 0 ] . Attributes . ArtistName ) ,
"{ArtistName}" , LimitString ( meta . Data [ 0 ] . Attributes . ArtistName ) ,
"{ArtistId}" , meta . Data [ 0 ] . Relationships . Artists . Data [ 0 ] . ID ,
) . Replace ( config . ArtistFolderFormat )
} else {
singerFoldername = strings . NewReplacer (
"{UrlArtistName}" , LimitString ( meta . Data [ 0 ] . Attributes . ArtistName ) ,
"{ArtistName}" , LimitString ( meta . Data [ 0 ] . Attributes . ArtistName ) ,
"{ArtistId}" , "" ,
) . Replace ( config . ArtistFolderFormat )
}
if strings . HasSuffix ( singerFoldername , "." ) {
singerFoldername = strings . ReplaceAll ( singerFoldername , "." , "" )
}
singerFoldername = strings . TrimSpace ( singerFoldername )
fmt . Println ( singerFoldername )
}
singerFolder := filepath . Join ( config . AlacSaveFolder , forbiddenNames . ReplaceAllString ( singerFoldername , "_" ) )
if dl_atmos {
singerFolder = filepath . Join ( config . AtmosSaveFolder , forbiddenNames . ReplaceAllString ( singerFoldername , "_" ) )
}
var Quality string
if strings . Contains ( config . AlbumFolderFormat , "Quality" ) {
if dl_atmos {
Quality = fmt . Sprintf ( "%dkbps" , config . AtmosMax - 2000 )
} else {
manifest1 , err := getInfoFromAdam ( meta . Data [ 0 ] . Relationships . Tracks . Data [ 0 ] . ID , token , storefront )
if err != nil {
fmt . Println ( "Failed to get manifest.\n" , err )
} else {
if manifest1 . Attributes . ExtendedAssetUrls . EnhancedHls == "" {
fmt . Println ( "Unavailable.\n" )
} else {
needCheck := false
if config . GetM3u8Mode == "all" {
needCheck = true
} else if config . GetM3u8Mode == "hires" && contains ( meta . Data [ 0 ] . Relationships . Tracks . Data [ 0 ] . Attributes . AudioTraits , "hi-res-lossless" ) {
needCheck = true
}
var EnhancedHls_m3u8 string
if needCheck {
EnhancedHls_m3u8 , err = checkM3u8 ( meta . Data [ 0 ] . Relationships . Tracks . Data [ 0 ] . ID , "album" )
if strings . HasSuffix ( EnhancedHls_m3u8 , ".m3u8" ) {
manifest1 . Attributes . ExtendedAssetUrls . EnhancedHls = EnhancedHls_m3u8
}
}
Quality , err = extractMediaQuality ( manifest1 . Attributes . ExtendedAssetUrls . EnhancedHls )
if err != nil {
fmt . Println ( "Failed to extract quality from manifest.\n" , err )
}
}
}
}
}
stringsToJoin := [ ] string { }
if meta . Data [ 0 ] . Attributes . IsAppleDigitalMaster || meta . Data [ 0 ] . Attributes . IsMasteredForItunes {
if config . AppleMasterChoice != "" {
stringsToJoin = append ( stringsToJoin , config . AppleMasterChoice )
}
}
if meta . Data [ 0 ] . Attributes . ContentRating == "explicit" {
if config . ExplicitChoice != "" {
stringsToJoin = append ( stringsToJoin , config . ExplicitChoice )
}
}
if meta . Data [ 0 ] . Attributes . ContentRating == "clean" {
if config . CleanChoice != "" {
stringsToJoin = append ( stringsToJoin , config . CleanChoice )
}
}
Tag_string := strings . Join ( stringsToJoin , " " )
var albumFolder string
if strings . Contains ( albumId , "pl." ) {
albumFolder = strings . NewReplacer (
"{ArtistName}" , "Apple Music" ,
"{PlaylistName}" , LimitString ( meta . Data [ 0 ] . Attributes . Name ) ,
"{PlaylistId}" , albumId ,
"{Quality}" , Quality ,
"{Codec}" , Codec ,
"{Tag}" , Tag_string ,
) . Replace ( config . PlaylistFolderFormat )
} else {
albumFolder = strings . NewReplacer (
"{ReleaseDate}" , meta . Data [ 0 ] . Attributes . ReleaseDate ,
"{ReleaseYear}" , meta . Data [ 0 ] . Attributes . ReleaseDate [ : 4 ] ,
"{ArtistName}" , LimitString ( meta . Data [ 0 ] . Attributes . ArtistName ) ,
"{AlbumName}" , LimitString ( meta . Data [ 0 ] . Attributes . Name ) ,
"{UPC}" , meta . Data [ 0 ] . Attributes . Upc ,
"{RecordLabel}" , meta . Data [ 0 ] . Attributes . RecordLabel ,
"{Copyright}" , meta . Data [ 0 ] . Attributes . Copyright ,
"{AlbumId}" , albumId ,
"{Quality}" , Quality ,
"{Codec}" , Codec ,
"{Tag}" , Tag_string ,
) . Replace ( config . AlbumFolderFormat )
}
if strings . HasSuffix ( albumFolder , "." ) {
albumFolder = strings . ReplaceAll ( albumFolder , "." , "" )
}
albumFolder = strings . TrimSpace ( albumFolder )
sanAlbumFolder := filepath . Join ( singerFolder , forbiddenNames . ReplaceAllString ( albumFolder , "_" ) )
os . MkdirAll ( sanAlbumFolder , os . ModePerm )
fmt . Println ( albumFolder )
//get artist cover
if config . SaveArtistCover && ! ( strings . Contains ( albumId , "pl." ) ) {
if len ( meta . Data [ 0 ] . Relationships . Artists . Data ) > 0 {
err = writeCover ( singerFolder , "folder" , meta . Data [ 0 ] . Relationships . Artists . Data [ 0 ] . Attributes . Artwork . Url )
if err != nil {
fmt . Println ( "Failed to write artist cover." )
}
}
}
//get album cover
err = writeCover ( sanAlbumFolder , "cover" , meta . Data [ 0 ] . Attributes . Artwork . URL )
if err != nil {
fmt . Println ( "Failed to write cover." )
}
//get animated artwork
if config . SaveAnimatedArtwork && meta . Data [ 0 ] . Attributes . EditorialVideo . MotionDetailSquare . Video != "" {
fmt . Println ( "Found Animation Artwork." )
motionvideoUrl , err := extractVideo ( meta . Data [ 0 ] . Attributes . EditorialVideo . MotionDetailSquare . Video )
if err != nil {
fmt . Println ( "no motion video.\n" , err )
}
exists , err := fileExists ( filepath . Join ( sanAlbumFolder , "animated_artwork.mp4" ) )
if err != nil {
fmt . Println ( "Failed to check if animated artwork exists." )
}
if exists {
fmt . Println ( "Animated artwork already exists locally." )
} else {
fmt . Println ( "Animation Artwork Downloading..." )
cmd := exec . Command ( "ffmpeg" , "-loglevel" , "quiet" , "-y" , "-i" , motionvideoUrl , "-c" , "copy" , filepath . Join ( sanAlbumFolder , "animated_artwork.mp4" ) )
if err := cmd . Run ( ) ; err != nil {
fmt . Printf ( "animated artwork dl err: %v\n" , err )
} else {
fmt . Println ( "Animation Artwork Downloaded" )
}
if config . EmbyAnimatedArtwork {
cmd2 := exec . Command ( "ffmpeg" , "-i" , filepath . Join ( sanAlbumFolder , "animated_artwork.mp4" ) , "-vf" , "scale=440:-1" , "-r" , "24" , "-f" , "gif" , filepath . Join ( sanAlbumFolder , "folder.jpg" ) )
if err := cmd2 . Run ( ) ; err != nil {
fmt . Printf ( "animated artwork to gif err: %v\n" , err )
}
}
}
}
trackTotal := len ( meta . Data [ 0 ] . Relationships . Tracks . Data )
arr := make ( [ ] int , trackTotal )
for i := 0 ; i < trackTotal ; i ++ {
arr [ i ] = i + 1
}
selected := [ ] int { }
if ! dl_select {
selected = arr
} else {
fmt . Print ( "select: " )
reader := bufio . NewReader ( os . Stdin )
input , err := reader . ReadString ( '\n' )
if err != nil {
fmt . Println ( err )
}
input = strings . TrimSpace ( input )
inputs := strings . Fields ( input )
for _ , str := range inputs {
num , err := strconv . Atoi ( str )
if err != nil {
fmt . Printf ( "wrong '%s', skip...\n" , str )
continue
}
found := false
for i := 0 ; i < len ( arr ) ; i ++ {
if arr [ i ] == num {
selected = append ( selected , num )
found = true
break
}
}
if ! found {
fmt . Printf ( "Option '%d' not found or already selected, skipping...\n" , num )
}
}
fmt . Println ( "Selected options:" , selected )
}
for trackNum , track := range meta . Data [ 0 ] . Relationships . Tracks . Data {
trackNum ++
if isInArray ( okDict [ albumId ] , trackNum ) {
//fmt.Println("已完成直接跳过.\n")
counter . Total ++
counter . Success ++
continue
}
if isInArray ( selected , trackNum ) {
counter . Total ++
fmt . Printf ( "Track %d of %d:\n" , trackNum , trackTotal )
manifest , err := getInfoFromAdam ( track . ID , token , storefront )
if err != nil {
fmt . Println ( "\u26A0 Failed to get manifest:" , err )
counter . NotSong ++
continue
}
if manifest . Attributes . ExtendedAssetUrls . EnhancedHls == "" {
fmt . Println ( "\u26A0 Unavailable." )
counter . Unavailable ++
continue
}
needCheck := false
if config . GetM3u8Mode == "all" {
needCheck = true
} else if config . GetM3u8Mode == "hires" && contains ( track . Attributes . AudioTraits , "hi-res-lossless" ) {
needCheck = true
}
var EnhancedHls_m3u8 string
if needCheck {
EnhancedHls_m3u8 , err = checkM3u8 ( track . ID , "song" )
if strings . HasSuffix ( EnhancedHls_m3u8 , ".m3u8" ) {
manifest . Attributes . ExtendedAssetUrls . EnhancedHls = EnhancedHls_m3u8
}
}
var Quality string
if strings . Contains ( config . SongFileFormat , "Quality" ) {
if dl_atmos {
Quality = fmt . Sprintf ( "%dkbps" , config . AtmosMax - 2000 )
} else {
Quality , err = extractMediaQuality ( manifest . Attributes . ExtendedAssetUrls . EnhancedHls )
if err != nil {
fmt . Println ( "Failed to extract quality from manifest.\n" , err )
counter . Error ++
continue
}
}
}
stringsToJoin := [ ] string { }
if track . Attributes . IsAppleDigitalMaster {
if config . AppleMasterChoice != "" {
stringsToJoin = append ( stringsToJoin , config . AppleMasterChoice )
}
}
if track . Attributes . ContentRating == "explicit" {
if config . ExplicitChoice != "" {
stringsToJoin = append ( stringsToJoin , config . ExplicitChoice )
}
}
if track . Attributes . ContentRating == "clean" {
if config . CleanChoice != "" {
stringsToJoin = append ( stringsToJoin , config . CleanChoice )
}
}
Tag_string := strings . Join ( stringsToJoin , " " )
songName := strings . NewReplacer (
"{SongId}" , track . ID ,
"{SongNumer}" , fmt . Sprintf ( "%02d" , trackNum ) ,
"{SongName}" , LimitString ( track . Attributes . Name ) ,
"{DiscNumber}" , fmt . Sprintf ( "%0d" , track . Attributes . DiscNumber ) ,
"{TrackNumber}" , fmt . Sprintf ( "%0d" , track . Attributes . TrackNumber ) ,
"{Quality}" , Quality ,
"{Tag}" , Tag_string ,
"{Codec}" , Codec ,
) . Replace ( config . SongFileFormat )
fmt . Println ( songName )
filename := fmt . Sprintf ( "%s.m4a" , forbiddenNames . ReplaceAllString ( songName , "_" ) )
if dl_atmos {
filename = fmt . Sprintf ( "%s.ec3" , forbiddenNames . ReplaceAllString ( songName , "_" ) )
}
m4afilename := fmt . Sprintf ( "%s.m4a" , forbiddenNames . ReplaceAllString ( songName , "_" ) )
lrcFilename := fmt . Sprintf ( "%s.lrc" , forbiddenNames . ReplaceAllString ( songName , "_" ) )
trackPath := filepath . Join ( sanAlbumFolder , filename )
m4atrackPath := filepath . Join ( sanAlbumFolder , m4afilename )
var lrc string = ""
if userToken != "your-media-user-token" && ( config . EmbedLrc || config . SaveLrcFile ) {
ttml , err := getSongLyrics ( track . ID , storefront , token , userToken )
if err != nil {
fmt . Println ( "Failed to get lyrics" )
} else {
lrc , err = conventTTMLToLRC ( ttml )
if err != nil {
fmt . Printf ( "Failed to parse lyrics: %s \n" , err )
} else {
if config . SaveLrcFile {
err := writeLyrics ( sanAlbumFolder , lrcFilename , lrc )
if err != nil {
fmt . Printf ( "Failed to write lyrics" )
}
if ! config . EmbedLrc {
lrc = ""
}
}
}
}
}
exists , err := fileExists ( trackPath )
if err != nil {
fmt . Println ( "Failed to check if track exists." )
}
if exists {
fmt . Println ( "Track already exists locally." )
counter . Success ++
okDict [ albumId ] = append ( okDict [ albumId ] , trackNum )
continue
}
m4aexists , err := fileExists ( m4atrackPath )
if err != nil {
fmt . Println ( "Failed to check if track exists." )
}
if m4aexists {
fmt . Println ( "Track already exists locally." )
counter . Success ++
okDict [ albumId ] = append ( okDict [ albumId ] , trackNum )
continue
}
trackUrl , keys , err := extractMedia ( manifest . Attributes . ExtendedAssetUrls . EnhancedHls )
if err != nil {
fmt . Println ( "\u26A0 Failed to extract info from manifest:" , err )
counter . Unavailable ++
continue
}
info , err := extractSong ( trackUrl )
if err != nil {
fmt . Println ( "Failed to extract track." , err )
counter . Error ++
continue
}
samplesOk := true
for samplesOk {
var totalSize int64 = 0
for _ , i := range info . samples {
totalSize += int64 ( len ( i . data ) )
if int ( i . descIndex ) >= len ( keys ) {
fmt . Println ( "Decryption size mismatch." )
samplesOk = false
}
}
info . totalDataSize = totalSize
break
}
if ! samplesOk {
counter . Error ++
continue
}
err = decryptSong ( info , keys , meta , trackPath , trackNum , trackTotal )
if err != nil {
fmt . Println ( "Failed to decrypt track.\n" , err )
counter . Error ++
continue
}
tags := [ ] string {
fmt . Sprintf ( "lyrics=%s" , lrc ) ,
}
index := trackNum - 1
if dl_atmos {
tags = [ ] string {
"tool=" ,
fmt . Sprintf ( "lyrics=%s" , lrc ) ,
fmt . Sprintf ( "title=%s" , meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . Name ) ,
fmt . Sprintf ( "artist=%s" , meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . ArtistName ) ,
fmt . Sprintf ( "genre=%s" , meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . GenreNames [ 0 ] ) ,
fmt . Sprintf ( "created=%s" , meta . Data [ 0 ] . Attributes . ReleaseDate ) ,
fmt . Sprintf ( "album_artist=%s" , meta . Data [ 0 ] . Attributes . ArtistName ) ,
fmt . Sprintf ( "composer=%s" , meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . ComposerName ) ,
fmt . Sprintf ( "writer=%s" , meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . ComposerName ) ,
fmt . Sprintf ( "performer=%s" , meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . ArtistName ) ,
fmt . Sprintf ( "copyright=%s" , meta . Data [ 0 ] . Attributes . Copyright ) ,
fmt . Sprintf ( "ISRC=%s" , meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . Isrc ) ,
fmt . Sprintf ( "UPC=%s" , meta . Data [ 0 ] . Attributes . Upc ) ,
}
if strings . Contains ( albumId , "pl." ) && ! config . UseSongInfoForPlaylist {
tags = append ( tags , "disk=1/1" )
tags = append ( tags , fmt . Sprintf ( "track=%d" , trackNum ) )
tags = append ( tags , fmt . Sprintf ( "tracknum=%d/%d" , trackNum , trackTotal ) )
tags = append ( tags , fmt . Sprintf ( "album=%s" , meta . Data [ 0 ] . Attributes . Name ) )
} else {
tags = append ( tags , fmt . Sprintf ( "disk=%d/%d" , meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . DiscNumber , meta . Data [ 0 ] . Relationships . Tracks . Data [ trackTotal - 1 ] . Attributes . DiscNumber ) )
tags = append ( tags , fmt . Sprintf ( "track=%d" , meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . TrackNumber ) )
tags = append ( tags , fmt . Sprintf ( "tracknum=%d/%d" , meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . TrackNumber , trackTotal ) )
tags = append ( tags , fmt . Sprintf ( "album=%s" , meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . AlbumName ) )
}
}
if track . Attributes . ContentRating == "explicit" {
tags = append ( tags , "rating=1" )
} else if track . Attributes . ContentRating == "clean" {
tags = append ( tags , "rating=2" )
} else {
tags = append ( tags , "rating=0" )
}
if config . EmbedCover {
if strings . Contains ( albumId , "pl." ) && config . DlAlbumcoverForPlaylist {
err = writeCover ( sanAlbumFolder , track . ID , track . Attributes . Artwork . URL )
if err != nil {
fmt . Println ( "Failed to write cover." )
}
tags = append ( tags , fmt . Sprintf ( "cover=%s/%s.%s" , sanAlbumFolder , track . ID , config . CoverFormat ) )
} else {
tags = append ( tags , fmt . Sprintf ( "cover=%s/%s.%s" , sanAlbumFolder , "cover" , config . CoverFormat ) )
}
}
tagsString := strings . Join ( tags , ":" )
cmd := exec . Command ( "MP4Box" , "-itags" , tagsString , trackPath )
if dl_atmos {
cmd = exec . Command ( "MP4Box" , "-add" , trackPath , "-name" , fmt . Sprintf ( "1=%s" , meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . Name ) , "-itags" , tagsString , "-brand" , "mp42" , "-ab" , "dby1" , m4atrackPath )
}
if err := cmd . Run ( ) ; err != nil {
fmt . Printf ( "Embed failed: %v\n" , err )
counter . Error ++
continue
}
if strings . Contains ( albumId , "pl." ) && config . DlAlbumcoverForPlaylist {
if err := os . Remove ( fmt . Sprintf ( "%s/%s.%s" , sanAlbumFolder , track . ID , config . CoverFormat ) ) ; err != nil {
fmt . Printf ( "Error deleting file: %s/%s.%s\n" , sanAlbumFolder , track . ID , config . CoverFormat )
counter . Error ++
continue
}
}
if dl_atmos {
fmt . Printf ( "Deleting original EC3 file: %s\n" , filepath . Base ( trackPath ) )
if err := os . Remove ( trackPath ) ; err != nil {
fmt . Printf ( "Error deleting file: %v\n" , err )
counter . Error ++
continue
}
fmt . Printf ( "Successfully processed and deleted %s\n" , filepath . Base ( trackPath ) )
}
counter . Success ++
okDict [ albumId ] = append ( okDict [ albumId ] , trackNum )
}
}
return err
}
func main ( ) {
err := loadConfig ( )
if err != nil {
fmt . Printf ( "load config failed: %v" , err )
return
}
token , err := getToken ( )
if err != nil {
if config . AuthorizationToken != "" && config . AuthorizationToken != "your-authorization-token" {
token = strings . Replace ( config . AuthorizationToken , "Bearer " , "" , - 1 )
} else {
fmt . Println ( "Failed to get token." )
return
}
}
// Define command-line flags
pflag . BoolVar ( & dl_atmos , "atmos" , false , "Enable atmos download mode" )
pflag . BoolVar ( & dl_select , "select" , false , "Enable selective download" )
pflag . BoolVar ( & artist_select , "all-album" , false , "Download all artist albums" )
alac_max = pflag . Int ( "alac-max" , - 1 , "Specify the max quality for download alac" )
atmos_max = pflag . Int ( "atmos-max" , - 1 , "Specify the max quality for download atmos" )
// Custom usage message for help
pflag . Usage = func ( ) {
fmt . Fprintf ( os . Stderr , "Usage: %s [options] url1 url2 ...\n" , "[main | main.exe | go run main.go]" )
fmt . Println ( "Options:" )
pflag . PrintDefaults ( )
}
// Parse the flag arguments
pflag . Parse ( )
if * alac_max != - 1 {
config . AlacMax = * alac_max
}
if * atmos_max != - 1 {
config . AtmosMax = * atmos_max
}
args := pflag . Args ( )
if len ( args ) == 0 {
fmt . Println ( "No URLs provided. Please provide at least one URL." )
pflag . Usage ( )
return
}
os . Args = args
if strings . Contains ( os . Args [ 0 ] , "/artist/" ) {
urlArtistName , err := getUrlArtistName ( os . Args [ 0 ] , token )
if err != nil {
fmt . Println ( "Failed to get artistname." )
return
}
//fmt.Println("get artistname:", urlArtistName)
config . ArtistFolderFormat = strings . NewReplacer (
"{UrlArtistName}" , LimitString ( urlArtistName ) ,
) . Replace ( config . ArtistFolderFormat )
newArgs , err := checkArtist ( os . Args [ 0 ] , token )
if err != nil {
fmt . Println ( "Failed to get artist." )
return
}
os . Args = newArgs
}
albumTotal := len ( os . Args )
for {
for albumNum , url := range os . Args {
fmt . Printf ( "Album %d of %d:\n" , albumNum + 1 , albumTotal )
var storefront , albumId string
if strings . Contains ( url , ".txt" ) {
txtpath = url
fileName := filepath . Base ( url )
parts := strings . SplitN ( fileName , "_" , 3 )
storefront = parts [ 0 ]
albumId = parts [ 1 ]
} else {
if strings . Contains ( url , "/playlist/" ) {
storefront , albumId = checkUrlPlaylist ( url )
txtpath = ""
} else {
storefront , albumId = checkUrl ( url )
txtpath = ""
}
}
if albumId == "" {
fmt . Printf ( "Invalid URL: %s\n" , url )
continue
}
err = rip ( albumId , token , storefront , config . MediaUserToken )
if err != nil {
fmt . Println ( "Album failed." )
fmt . Println ( err )
}
}
fmt . Printf ( "======= [\u2714 ] Completed: %d/%d | [\u26A0 ] Warnings: %d | [\u2716 ] Errors: %d =======\n" , counter . Success , counter . Total , counter . Unavailable + counter . NotSong , counter . Error )
if counter . Error == 0 {
break
}
fmt . Println ( "Error detected, press Enter to try again..." )
fmt . Scanln ( )
fmt . Println ( "Start trying again..." )
counter = Counter { }
}
}
func conventSyllableTTMLToLRC ( ttml string ) ( string , error ) {
parsedTTML := etree . NewDocument ( )
err := parsedTTML . ReadFromString ( ttml )
if err != nil {
return "" , err
}
var lrcLines [ ] string
parseTime := func ( timeValue string ) ( string , error ) {
var h , m , s , ms int
if strings . Contains ( timeValue , ":" ) {
_ , err = fmt . Sscanf ( timeValue , "%d:%d:%d.%d" , & h , & m , & s , & ms )
if err != nil {
_ , err = fmt . Sscanf ( timeValue , "%d:%d.%d" , & m , & s , & ms )
h = 0
}
} else {
_ , err = fmt . Sscanf ( timeValue , "%d.%d" , & s , & ms )
h , m = 0 , 0
}
if err != nil {
return "" , err
}
m += h * 60
ms = ms / 10
return fmt . Sprintf ( "[%02d:%02d.%02d]" , m , s , ms ) , nil
}
for _ , div := range parsedTTML . FindElement ( "tt" ) . FindElement ( "body" ) . FindElements ( "div" ) {
for _ , item := range div . ChildElements ( ) {
var lrcSyllables [ ] string
var i int = 0
for _ , lyrics := range item . Child {
if _ , ok := lyrics . ( * etree . CharData ) ; ok {
if i > 0 {
lrcSyllables = append ( lrcSyllables , " " )
continue
}
continue
}
lyric := lyrics . ( * etree . Element )
if lyric . SelectAttr ( "begin" ) == nil {
continue
}
beginTime , err := parseTime ( lyric . SelectAttr ( "begin" ) . Value )
if err != nil {
return "" , err
}
var text string
if lyric . SelectAttr ( "text" ) == nil {
var textTmp [ ] string
for _ , span := range lyric . Child {
if _ , ok := span . ( * etree . CharData ) ; ok {
textTmp = append ( textTmp , span . ( * etree . CharData ) . Data )
} else {
textTmp = append ( textTmp , span . ( * etree . Element ) . Text ( ) )
}
}
text = strings . Join ( textTmp , "" )
} else {
text = lyric . SelectAttr ( "text" ) . Value
}
lrcSyllables = append ( lrcSyllables , fmt . Sprintf ( "%s%s" , beginTime , text ) )
i += 1
}
endTime , err := parseTime ( item . SelectAttr ( "end" ) . Value )
if err != nil {
return "" , err
}
lrcLines = append ( lrcLines , strings . Join ( lrcSyllables , "" ) + endTime )
}
}
return strings . Join ( lrcLines , "\n" ) , nil
}
func conventTTMLToLRC ( ttml string ) ( string , error ) {
parsedTTML := etree . NewDocument ( )
err := parsedTTML . ReadFromString ( ttml )
if err != nil {
return "" , err
}
var lrcLines [ ] string
timingAttr := parsedTTML . FindElement ( "tt" ) . SelectAttr ( "itunes:timing" )
if timingAttr != nil {
if timingAttr . Value == "Word" {
lrc , err := conventSyllableTTMLToLRC ( ttml )
return lrc , err
}
if timingAttr . Value == "None" {
for _ , p := range parsedTTML . FindElements ( "//p" ) {
line := p . Text ( )
line = strings . TrimSpace ( line )
if line != "" {
lrcLines = append ( lrcLines , line )
}
}
return strings . Join ( lrcLines , "\n" ) , nil
}
}
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 {
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 )
if err != nil {
_ , err = fmt . Sscanf ( lyric . SelectAttr ( "begin" ) . Value , "%d:%d.%d" , & m , & s , & ms )
if err != nil {
_ , err = fmt . Sscanf ( lyric . SelectAttr ( "begin" ) . Value , "%d:%d" , & m , & s )
}
h = 0
}
} else {
_ , err = fmt . Sscanf ( lyric . SelectAttr ( "begin" ) . Value , "%d.%d" , & s , & ms )
h , m = 0 , 0
}
if err != nil {
return "" , err
}
var text string
if lyric . SelectAttr ( "text" ) == nil {
var textTmp [ ] string
for _ , span := range lyric . Child {
if _ , ok := span . ( * etree . CharData ) ; ok {
textTmp = append ( textTmp , span . ( * etree . CharData ) . Data )
} else {
textTmp = append ( textTmp , span . ( * etree . Element ) . Text ( ) )
}
}
text = strings . Join ( textTmp , "" )
} 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 ) )
}
}
return strings . Join ( lrcLines , "\n" ) , nil
}
func checkM3u8 ( b string , f string ) ( string , error ) {
var EnhancedHls string
if config . Check != "" {
config . Check = strings . TrimSpace ( config . Check )
if strings . HasSuffix ( config . Check , "txt" ) {
txtpath = config . Check
}
if strings . HasPrefix ( config . Check , "http" ) {
req , err := http . NewRequest ( "GET" , config . Check , nil )
if err != nil {
fmt . Println ( err )
}
query := req . URL . Query ( )
query . Set ( "songid" , b )
req . URL . RawQuery = query . Encode ( )
do , err := http . DefaultClient . Do ( req )
if err != nil {
fmt . Println ( err )
}
defer do . Body . Close ( )
Checkbody , err := ioutil . ReadAll ( do . Body )
if err != nil {
fmt . Println ( err )
}
if string ( Checkbody ) != "no_found" {
EnhancedHls = string ( Checkbody )
fmt . Println ( "Found m3u8 from API" )
} else {
if config . ForceApi {
fmt . Println ( " Not Found m3u8 from API, Skip" )
}
fmt . Println ( " Not Found m3u8 from API" )
}
}
}
if config . GetM3u8FromDevice {
adamID := b
conn , err := net . Dial ( "tcp" , config . GetM3u8Port )
if err != nil {
fmt . Println ( "Error connecting to device:" , err )
return "none" , err
}
defer conn . Close ( )
if f == "song" {
fmt . Println ( "Connected to device" )
}
// Send the length of adamID and the adamID itself
adamIDBuffer := [ ] byte ( adamID )
lengthBuffer := [ ] byte { byte ( len ( adamIDBuffer ) ) }
// Write length and adamID to the connection
_ , err = conn . Write ( lengthBuffer )
if err != nil {
fmt . Println ( "Error writing length to device:" , err )
return "none" , err
}
_ , err = conn . Write ( adamIDBuffer )
if err != nil {
fmt . Println ( "Error writing adamID to device:" , err )
return "none" , err
}
// Read the response (URL) from the device
response , err := bufio . NewReader ( conn ) . ReadBytes ( '\n' )
if err != nil {
fmt . Println ( "Error reading response from device:" , err )
return "none" , err
}
// Trim any newline characters from the response
response = bytes . TrimSpace ( response )
if len ( response ) > 0 {
if f == "song" {
fmt . Println ( "Received URL:" , string ( response ) )
}
EnhancedHls = string ( response )
} else {
fmt . Println ( "Received an empty response" )
}
}
if txtpath != "" {
file , err := os . Open ( txtpath )
if err != nil {
fmt . Println ( "cant open txt:" , err )
}
defer file . Close ( )
scanner := bufio . NewScanner ( file )
for scanner . Scan ( ) {
line := scanner . Text ( )
if strings . HasPrefix ( line , b ) {
parts := strings . SplitN ( line , "," , 2 )
if len ( parts ) == 2 {
EnhancedHls = parts [ 1 ]
fmt . Println ( "Found m3u8 from txt" )
}
}
}
if err := scanner . Err ( ) ; err != nil {
fmt . Println ( err )
}
}
return EnhancedHls , nil
}
func extractMediaQuality ( b string ) ( string , error ) {
resp , err := http . Get ( b )
if err != nil {
return "" , err
}
defer resp . Body . Close ( )
if resp . StatusCode != http . StatusOK {
return "" , errors . New ( resp . Status )
}
body , err := io . ReadAll ( resp . Body )
if err != nil {
return "" , err
}
masterString := string ( body )
from , listType , err := m3u8 . DecodeFrom ( strings . NewReader ( masterString ) , true )
if err != nil || listType != m3u8 . MASTER {
return "" , errors . New ( "m3u8 not of master type" )
}
master := from . ( * m3u8 . MasterPlaylist )
sort . Slice ( master . Variants , func ( i , j int ) bool {
return master . Variants [ i ] . AverageBandwidth > master . Variants [ j ] . AverageBandwidth
} )
var Quality string
for _ , variant := range master . Variants {
if variant . Codecs == "alac" {
split := strings . Split ( variant . Audio , "-" )
length := len ( split )
length_int , err := strconv . Atoi ( split [ length - 2 ] )
if err != nil {
return "" , err
}
if length_int <= config . AlacMax {
HZ , err := strconv . Atoi ( split [ length - 2 ] )
if err != nil {
fmt . Println ( err )
}
KHZ := float64 ( HZ ) / 1000.0
Quality = fmt . Sprintf ( "%sB-%.1fkHz" , split [ length - 1 ] , KHZ )
break
}
}
}
return Quality , nil
}
func extractMedia ( b string ) ( string , [ ] string , error ) {
masterUrl , err := url . Parse ( b )
if err != nil {
return "" , nil , err
}
resp , err := http . Get ( b )
if err != nil {
return "" , nil , err
}
defer resp . Body . Close ( )
if resp . StatusCode != http . StatusOK {
return "" , nil , errors . New ( resp . Status )
}
body , err := io . ReadAll ( resp . Body )
if err != nil {
return "" , nil , err
}
masterString := string ( body )
from , listType , err := m3u8 . DecodeFrom ( strings . NewReader ( masterString ) , true )
if err != nil || listType != m3u8 . MASTER {
return "" , nil , errors . New ( "m3u8 not of master type" )
}
master := from . ( * m3u8 . MasterPlaylist )
var streamUrl * url . URL
sort . Slice ( master . Variants , func ( i , j int ) bool {
return master . Variants [ i ] . AverageBandwidth > master . Variants [ j ] . AverageBandwidth
} )
for _ , variant := range master . Variants {
if dl_atmos {
if variant . Codecs == "ec-3" {
split := strings . Split ( variant . Audio , "-" )
length := len ( split )
length_int , err := strconv . Atoi ( split [ length - 1 ] )
if err != nil {
return "" , nil , err
}
if length_int <= config . AtmosMax {
fmt . Printf ( "%s\n" , variant . Audio )
streamUrlTemp , err := masterUrl . Parse ( variant . URI )
if err != nil {
panic ( err )
}
streamUrl = streamUrlTemp
break
}
}
} else {
if variant . Codecs == "alac" {
split := strings . Split ( variant . Audio , "-" )
length := len ( split )
length_int , err := strconv . Atoi ( split [ length - 2 ] )
if err != nil {
return "" , nil , err
}
if length_int <= config . AlacMax {
fmt . Printf ( "%s-bit / %s Hz\n" , split [ length - 1 ] , split [ length - 2 ] )
streamUrlTemp , err := masterUrl . Parse ( variant . URI )
if err != nil {
panic ( err )
}
streamUrl = streamUrlTemp
break
}
}
}
}
if streamUrl == nil {
return "" , nil , errors . New ( "no codec found" )
}
var keys [ ] string
keys = append ( keys , prefetchKey )
streamUrl . Path = strings . TrimSuffix ( streamUrl . Path , ".m3u8" ) + "_m.mp4"
regex := regexp . MustCompile ( ` "(skd?://[^"]*)" ` )
matches := regex . FindAllStringSubmatch ( masterString , - 1 )
for _ , match := range matches {
if dl_atmos {
if strings . HasSuffix ( match [ 1 ] , "c24" ) || strings . HasSuffix ( match [ 1 ] , "c6" ) {
keys = append ( keys , match [ 1 ] )
}
} else {
if strings . HasSuffix ( match [ 1 ] , "c23" ) || strings . HasSuffix ( match [ 1 ] , "c6" ) {
keys = append ( keys , match [ 1 ] )
}
}
}
return streamUrl . String ( ) , keys , nil
}
func extractVideo ( c string ) ( string , error ) {
MediaUrl , err := url . Parse ( c )
if err != nil {
return "" , err
}
resp , err := http . Get ( c )
if err != nil {
return "" , err
}
defer resp . Body . Close ( )
if resp . StatusCode != http . StatusOK {
return "" , errors . New ( resp . Status )
}
body , err := io . ReadAll ( resp . Body )
if err != nil {
return "" , err
}
videoString := string ( body )
from , listType , err := m3u8 . DecodeFrom ( strings . NewReader ( videoString ) , true )
if err != nil || listType != m3u8 . MASTER {
return "" , errors . New ( "m3u8 not of media type" )
}
video := from . ( * m3u8 . MasterPlaylist )
var streamUrl * url . URL
sort . Slice ( video . Variants , func ( i , j int ) bool {
return video . Variants [ i ] . AverageBandwidth > video . Variants [ j ] . AverageBandwidth
} )
if len ( video . Variants ) > 0 {
highestBandwidthVariant := video . Variants [ 0 ]
streamUrl , err = MediaUrl . Parse ( highestBandwidthVariant . URI )
if err != nil {
return "" , err
}
}
if streamUrl == nil {
return "" , errors . New ( "no video codec found" )
}
return streamUrl . String ( ) , nil
}
func extractSong ( url string ) ( * SongInfo , error ) {
fmt . Println ( "Downloading..." )
track , err := http . Get ( url )
if err != nil {
return nil , err
}
defer track . Body . Close ( )
if track . StatusCode != http . StatusOK {
return nil , errors . New ( track . Status )
}
contentLength := track . ContentLength
bar := progressbar . NewOptions64 ( contentLength ,
progressbar . OptionClearOnFinish ( ) ,
progressbar . OptionSetElapsedTime ( false ) ,
progressbar . OptionSetPredictTime ( false ) ,
progressbar . OptionShowElapsedTimeOnFinish ( ) ,
progressbar . OptionShowCount ( ) ,
progressbar . OptionEnableColorCodes ( true ) ,
progressbar . OptionShowBytes ( true ) ,
//progressbar.OptionSetDescription("Downloading..."),
progressbar . OptionSetTheme ( progressbar . Theme {
Saucer : "" ,
SaucerHead : "" ,
SaucerPadding : "" ,
BarStart : "" ,
BarEnd : "" ,
} ) ,
)
rawSong , err := ioutil . ReadAll ( io . TeeReader ( track . Body , bar ) )
if err != nil {
return nil , err
}
fmt . Println ( "Downloaded." )
f := bytes . NewReader ( rawSong )
trex , err := mp4 . ExtractBoxWithPayload ( f , nil , [ ] mp4 . BoxType {
mp4 . BoxTypeMoov ( ) ,
mp4 . BoxTypeMvex ( ) ,
mp4 . BoxTypeTrex ( ) ,
} )
if err != nil || len ( trex ) != 1 {
return nil , err
}
trexPay := trex [ 0 ] . Payload . ( * mp4 . Trex )
stbl , err := mp4 . ExtractBox ( f , nil , [ ] mp4 . BoxType {
mp4 . BoxTypeMoov ( ) ,
mp4 . BoxTypeTrak ( ) ,
mp4 . BoxTypeMdia ( ) ,
mp4 . BoxTypeMinf ( ) ,
mp4 . BoxTypeStbl ( ) ,
} )
if err != nil || len ( stbl ) != 1 {
return nil , err
}
var extracted * SongInfo
if ! dl_atmos {
enca , err := mp4 . ExtractBoxWithPayload ( f , stbl [ 0 ] , [ ] mp4 . BoxType {
mp4 . BoxTypeStsd ( ) ,
mp4 . BoxTypeEnca ( ) ,
} )
if err != nil {
return nil , err
}
aalac , err := mp4 . ExtractBoxWithPayload ( f , & enca [ 0 ] . Info ,
[ ] mp4 . BoxType { BoxTypeAlac ( ) } )
if err != nil || len ( aalac ) != 1 {
return nil , err
}
extracted = & SongInfo {
r : f ,
alacParam : aalac [ 0 ] . Payload . ( * Alac ) ,
}
} else {
extracted = & SongInfo {
r : f ,
// alacParam: aalac[0].Payload.(*Alac),
}
}
moofs , err := mp4 . ExtractBox ( f , nil , [ ] mp4 . BoxType {
mp4 . BoxTypeMoof ( ) ,
} )
if err != nil || len ( moofs ) <= 0 {
return nil , err
}
mdats , err := mp4 . ExtractBoxWithPayload ( f , nil , [ ] mp4 . BoxType {
mp4 . BoxTypeMdat ( ) ,
} )
if err != nil || len ( mdats ) != len ( moofs ) {
return nil , err
}
for i , moof := range moofs {
tfhd , err := mp4 . ExtractBoxWithPayload ( f , moof , [ ] mp4 . BoxType {
mp4 . BoxTypeTraf ( ) ,
mp4 . BoxTypeTfhd ( ) ,
} )
if err != nil || len ( tfhd ) != 1 {
return nil , err
}
tfhdPay := tfhd [ 0 ] . Payload . ( * mp4 . Tfhd )
index := tfhdPay . SampleDescriptionIndex
if index != 0 {
index --
}
truns , err := mp4 . ExtractBoxWithPayload ( f , moof , [ ] mp4 . BoxType {
mp4 . BoxTypeTraf ( ) ,
mp4 . BoxTypeTrun ( ) ,
} )
if err != nil || len ( truns ) <= 0 {
return nil , err
}
mdat := mdats [ i ] . Payload . ( * mp4 . Mdat ) . Data
for _ , t := range truns {
for _ , en := range t . Payload . ( * mp4 . Trun ) . Entries {
info := SampleInfo { descIndex : index }
switch {
case t . Payload . CheckFlag ( 0x200 ) :
info . data = mdat [ : en . SampleSize ]
mdat = mdat [ en . SampleSize : ]
case tfhdPay . CheckFlag ( 0x10 ) :
info . data = mdat [ : tfhdPay . DefaultSampleSize ]
mdat = mdat [ tfhdPay . DefaultSampleSize : ]
default :
info . data = mdat [ : trexPay . DefaultSampleSize ]
mdat = mdat [ trexPay . DefaultSampleSize : ]
}
switch {
case t . Payload . CheckFlag ( 0x100 ) :
info . duration = en . SampleDuration
case tfhdPay . CheckFlag ( 0x8 ) :
info . duration = tfhdPay . DefaultSampleDuration
default :
info . duration = trexPay . DefaultSampleDuration
}
extracted . samples = append ( extracted . samples , info )
}
}
if len ( mdat ) != 0 {
return nil , errors . New ( "offset mismatch" )
}
}
return extracted , nil
}
func init ( ) {
mp4 . AddBoxDef ( ( * Alac ) ( nil ) )
}
func BoxTypeAlac ( ) mp4 . BoxType { return mp4 . StrToBoxType ( "alac" ) }
type Alac struct {
mp4 . FullBox ` mp4:"extend" `
FrameLength uint32 ` mp4:"size=32" `
CompatibleVersion uint8 ` mp4:"size=8" `
BitDepth uint8 ` mp4:"size=8" `
Pb uint8 ` mp4:"size=8" `
Mb uint8 ` mp4:"size=8" `
Kb uint8 ` mp4:"size=8" `
NumChannels uint8 ` mp4:"size=8" `
MaxRun uint16 ` mp4:"size=16" `
MaxFrameBytes uint32 ` mp4:"size=32" `
AvgBitRate uint32 ` mp4:"size=32" `
SampleRate uint32 ` mp4:"size=32" `
}
func getInfoFromAdam ( adamId string , token string , storefront string ) ( * SongData , error ) {
request , err := http . NewRequest ( "GET" , fmt . Sprintf ( "https://amp-api.music.apple.com/v1/catalog/%s/songs/%s" , storefront , adamId ) , nil )
if err != nil {
return nil , err
}
query := url . Values { }
query . Set ( "extend" , "extendedAssetUrls" )
query . Set ( "include" , "albums" )
request . URL . RawQuery = query . Encode ( )
request . Header . Set ( "Authorization" , fmt . Sprintf ( "Bearer %s" , token ) )
request . Header . Set ( "User-Agent" , "iTunes/12.11.3 (Windows; Microsoft Windows 10 x64 Professional Edition (Build 19041); x64) AppleWebKit/7611.1022.4001.1 (dt:2)" )
request . Header . Set ( "Origin" , "https://music.apple.com" )
do , err := http . DefaultClient . Do ( request )
if err != nil {
return nil , err
}
defer do . Body . Close ( )
if do . StatusCode != http . StatusOK {
return nil , errors . New ( do . Status )
}
obj := new ( ApiResult )
err = json . NewDecoder ( do . Body ) . Decode ( & obj )
if err != nil {
return nil , err
}
for _ , d := range obj . Data {
if d . ID == adamId {
return & d , nil
}
}
return nil , nil
}
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
}
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 iTunesLookup struct {
Results map [ string ] SongResult ` json:"results" `
}
type Meta struct {
Context string ` json:"@context" `
Type string ` json:"@type" `
Name string ` json:"name" `
Description string ` json:"description" `
Tracks [ ] struct {
Type string ` json:"@type" `
Name string ` json:"name" `
Audio struct {
Type string ` json:"@type" `
} ` json:"audio" `
Offers struct {
Type string ` json:"@type" `
Category string ` json:"category" `
Price int ` json:"price" `
} ` json:"offers" `
Duration string ` json:"duration" `
} ` json:"tracks" `
Citation [ ] interface { } ` json:"citation" `
WorkExample [ ] struct {
Type string ` json:"@type" `
Name string ` json:"name" `
URL string ` json:"url" `
Audio struct {
Type string ` json:"@type" `
} ` json:"audio" `
Offers struct {
Type string ` json:"@type" `
Category string ` json:"category" `
Price int ` json:"price" `
} ` json:"offers" `
Duration string ` json:"duration" `
} ` json:"workExample" `
Genre [ ] string ` json:"genre" `
DatePublished time . Time ` json:"datePublished" `
ByArtist struct {
Type string ` json:"@type" `
URL string ` json:"url" `
Name string ` json:"name" `
} ` json:"byArtist" `
}
type AutoGenerated struct {
Data [ ] struct {
ID string ` json:"id" `
Type string ` json:"type" `
Href string ` json:"href" `
Attributes struct {
Artwork struct {
Width int ` json:"width" `
Height int ` json:"height" `
URL string ` json:"url" `
BgColor string ` json:"bgColor" `
TextColor1 string ` json:"textColor1" `
TextColor2 string ` json:"textColor2" `
TextColor3 string ` json:"textColor3" `
TextColor4 string ` json:"textColor4" `
} ` json:"artwork" `
ArtistName string ` json:"artistName" `
IsSingle bool ` json:"isSingle" `
URL string ` json:"url" `
IsComplete bool ` json:"isComplete" `
GenreNames [ ] string ` json:"genreNames" `
TrackCount int ` json:"trackCount" `
IsMasteredForItunes bool ` json:"isMasteredForItunes" `
IsAppleDigitalMaster bool ` json:"isAppleDigitalMaster" `
ContentRating string ` json:"contentRating" `
ReleaseDate string ` json:"releaseDate" `
Name string ` json:"name" `
RecordLabel string ` json:"recordLabel" `
Upc string ` json:"upc" `
AudioTraits [ ] string ` json:"audioTraits" `
Copyright string ` json:"copyright" `
PlayParams struct {
ID string ` json:"id" `
Kind string ` json:"kind" `
} ` json:"playParams" `
IsCompilation bool ` json:"isCompilation" `
EditorialVideo struct {
MotionDetailSquare struct {
Video string ` json:"video" `
} ` json:"motionDetailSquare" `
MotionSquareVideo1x1 struct {
Video string ` json:"video" `
} ` json:"motionSquareVideo1x1" `
} ` json:"editorialVideo" `
} ` json:"attributes" `
Relationships struct {
RecordLabels struct {
Href string ` json:"href" `
Data [ ] interface { } ` json:"data" `
} ` json:"record-labels" `
Artists struct {
Href string ` json:"href" `
Data [ ] struct {
ID string ` json:"id" `
Type string ` json:"type" `
Href string ` json:"href" `
Attributes struct {
Name string ` json:"name" `
Artwork struct {
Url string ` json:"url" `
} ` json:"artwork" `
} ` json:"attributes" `
} ` json:"data" `
} ` json:"artists" `
Tracks struct {
Href string ` json:"href" `
Next string ` json:"next" `
Data [ ] struct {
ID string ` json:"id" `
Type string ` json:"type" `
Href string ` json:"href" `
Attributes struct {
Previews [ ] struct {
URL string ` json:"url" `
} ` json:"previews" `
Artwork struct {
Width int ` json:"width" `
Height int ` json:"height" `
URL string ` json:"url" `
BgColor string ` json:"bgColor" `
TextColor1 string ` json:"textColor1" `
TextColor2 string ` json:"textColor2" `
TextColor3 string ` json:"textColor3" `
TextColor4 string ` json:"textColor4" `
} ` json:"artwork" `
ArtistName string ` json:"artistName" `
URL string ` json:"url" `
DiscNumber int ` json:"discNumber" `
GenreNames [ ] string ` json:"genreNames" `
HasTimeSyncedLyrics bool ` json:"hasTimeSyncedLyrics" `
IsMasteredForItunes bool ` json:"isMasteredForItunes" `
IsAppleDigitalMaster bool ` json:"isAppleDigitalMaster" `
ContentRating string ` json:"contentRating" `
DurationInMillis int ` json:"durationInMillis" `
ReleaseDate string ` json:"releaseDate" `
Name string ` json:"name" `
Isrc string ` json:"isrc" `
AudioTraits [ ] string ` json:"audioTraits" `
HasLyrics bool ` json:"hasLyrics" `
AlbumName string ` json:"albumName" `
PlayParams struct {
ID string ` json:"id" `
Kind string ` json:"kind" `
} ` json:"playParams" `
TrackNumber int ` json:"trackNumber" `
AudioLocale string ` json:"audioLocale" `
ComposerName string ` json:"composerName" `
} ` json:"attributes" `
Relationships struct {
Artists struct {
Href string ` json:"href" `
Data [ ] struct {
ID string ` json:"id" `
Type string ` json:"type" `
Href string ` json:"href" `
Attributes struct {
Name string ` json:"name" `
} ` json:"attributes" `
} ` json:"data" `
} ` json:"artists" `
} ` json:"relationships" `
} ` json:"data" `
} ` json:"tracks" `
} ` json:"relationships" `
} ` json:"data" `
}
type AutoGeneratedTrack struct {
Href string ` json:"href" `
Next string ` json:"next" `
Data [ ] struct {
ID string ` json:"id" `
Type string ` json:"type" `
Href string ` json:"href" `
Attributes struct {
Previews [ ] struct {
URL string ` json:"url" `
} ` json:"previews" `
Artwork struct {
Width int ` json:"width" `
Height int ` json:"height" `
URL string ` json:"url" `
BgColor string ` json:"bgColor" `
TextColor1 string ` json:"textColor1" `
TextColor2 string ` json:"textColor2" `
TextColor3 string ` json:"textColor3" `
TextColor4 string ` json:"textColor4" `
} ` json:"artwork" `
ArtistName string ` json:"artistName" `
URL string ` json:"url" `
DiscNumber int ` json:"discNumber" `
GenreNames [ ] string ` json:"genreNames" `
HasTimeSyncedLyrics bool ` json:"hasTimeSyncedLyrics" `
IsMasteredForItunes bool ` json:"isMasteredForItunes" `
IsAppleDigitalMaster bool ` json:"isAppleDigitalMaster" `
ContentRating string ` json:"contentRating" `
DurationInMillis int ` json:"durationInMillis" `
ReleaseDate string ` json:"releaseDate" `
Name string ` json:"name" `
Isrc string ` json:"isrc" `
AudioTraits [ ] string ` json:"audioTraits" `
HasLyrics bool ` json:"hasLyrics" `
AlbumName string ` json:"albumName" `
PlayParams struct {
ID string ` json:"id" `
Kind string ` json:"kind" `
} ` json:"playParams" `
TrackNumber int ` json:"trackNumber" `
AudioLocale string ` json:"audioLocale" `
ComposerName string ` json:"composerName" `
} ` json:"attributes" `
Relationships struct {
Artists struct {
Href string ` json:"href" `
Data [ ] struct {
ID string ` json:"id" `
Type string ` json:"type" `
Href string ` json:"href" `
Attributes struct {
Name string ` json:"name" `
} ` json:"attributes" `
} ` json:"data" `
} ` json:"artists" `
} ` json:"relationships" `
} ` json:"data" `
}
type AutoGeneratedArtist struct {
Next string ` json:"next" `
Data [ ] struct {
ID string ` json:"id" `
Type string ` json:"type" `
Href string ` json:"href" `
Attributes struct {
Previews [ ] struct {
URL string ` json:"url" `
} ` json:"previews" `
Artwork struct {
Width int ` json:"width" `
Height int ` json:"height" `
URL string ` json:"url" `
BgColor string ` json:"bgColor" `
TextColor1 string ` json:"textColor1" `
TextColor2 string ` json:"textColor2" `
TextColor3 string ` json:"textColor3" `
TextColor4 string ` json:"textColor4" `
} ` json:"artwork" `
ArtistName string ` json:"artistName" `
URL string ` json:"url" `
DiscNumber int ` json:"discNumber" `
GenreNames [ ] string ` json:"genreNames" `
HasTimeSyncedLyrics bool ` json:"hasTimeSyncedLyrics" `
IsMasteredForItunes bool ` json:"isMasteredForItunes" `
IsAppleDigitalMaster bool ` json:"isAppleDigitalMaster" `
ContentRating string ` json:"contentRating" `
DurationInMillis int ` json:"durationInMillis" `
ReleaseDate string ` json:"releaseDate" `
Name string ` json:"name" `
Isrc string ` json:"isrc" `
AudioTraits [ ] string ` json:"audioTraits" `
HasLyrics bool ` json:"hasLyrics" `
AlbumName string ` json:"albumName" `
PlayParams struct {
ID string ` json:"id" `
Kind string ` json:"kind" `
} ` json:"playParams" `
TrackNumber int ` json:"trackNumber" `
AudioLocale string ` json:"audioLocale" `
ComposerName string ` json:"composerName" `
} ` json:"attributes" `
} ` json:"data" `
}
type SongLyrics struct {
Data [ ] struct {
Id string ` json:"id" `
Type string ` json:"type" `
Attributes struct {
Ttml string ` json:"ttml" `
PlayParams struct {
Id string ` json:"id" `
Kind string ` json:"kind" `
CatalogId string ` json:"catalogId" `
DisplayType int ` json:"displayType" `
} ` json:"playParams" `
} ` json:"attributes" `
} ` json:"data" `
}