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/beevik/etree"
"gopkg.in/yaml.v2"
"github.com/abema/go-mp4"
"github.com/grafov/m3u8"
)
const (
defaultId = "0"
prefetchKey = "skd://itunes.apple.com/P000000000/s1/e1"
)
var (
forbiddenNames = regexp . MustCompile ( ` [/\\<>:"|?*] ` )
)
type Config struct {
MediaUserToken string ` yaml:"media-user-token" `
SaveLrcFile bool ` yaml:"save-lrc-file" `
EmbedLrc bool ` yaml:"embed-lrc" `
EmbedCover bool ` yaml:"embed-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" `
AtmosMax int ` yaml:"atmos-max" `
UseSongInfoForPlaylist bool ` yaml:"use-songinfo-for-playlist" `
DlAlbumcoverForPlaylist bool ` yaml:"dl-albumcover-for-playlist" `
}
var config Config
var oktrackNum int = 0
var trackTotalnum int = 0
type SampleInfo struct {
data [ ] byte
duration uint32
descIndex uint32
}
type SongInfo struct {
r io . ReadSeeker
alacParam * Alac
samples [ ] SampleInfo
}
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 ( 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 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 { '\251' , 'a' , 'l' , 'b' } , meta . Data [ 0 ] . Attributes . Name )
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 { '\251' , 'w' , 'r' , 't' } , meta . Data [ 0 ] . Relationships . Tracks . Data [ index ] . Attributes . ComposerName )
if err != nil {
return err
}
err = addMeta ( mp4 . BoxType { '\251' , 'd' , 'a' , 'y' } , strings . Split ( meta . Data [ 0 ] . Attributes . ReleaseDate , "-" ) [ 0 ] )
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' } , album . 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 = addExtendedMeta ( "LABEL" , album . Attributes . RecordLabel )
if err != nil {
return err
}
err = addExtendedMeta ( "UPC" , album . Attributes . Upc )
if err != nil {
return err
}
// plID, err := strconv.ParseUint(album.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.Artists.Data) > 0 {
// atID, err := strconv.ParseUint(meta.Data[0].Relationships.Artists.Data[index].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 )
binary . BigEndian . PutUint32 ( trkn , uint32 ( trackNum ) )
binary . BigEndian . PutUint16 ( trkn [ 4 : ] , uint16 ( trackTotal ) )
err = addMeta ( mp4 . BoxType { 't' , 'r' , 'k' , 'n' } , trkn )
if err != nil {
return err
}
// disk := make([]byte, 8)
// binary.BigEndian.PutUint32(disk, uint32(meta.Attributes.DiscNumber))
// 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" , "127.0.0.1:10020" )
if err != nil {
return err
}
defer conn . Close ( )
var decrypted [ ] byte
var lastIndex uint32 = math . MaxUint8
fmt . Println ( "Decrypt start." )
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 ... )
}
_ , _ = conn . Write ( [ ] byte { 0 , 0 , 0 , 0 , 0 } )
fmt . Println ( "Decrypt finished." )
file , err := os . Create ( filename )
if err != nil {
panic ( err )
}
defer file . Close ( )
_ , err = file . 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 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 )
}
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" )
query . Set ( "fields[albums:albums]" , "artistName,artwork,name,releaseDate,url" )
query . Set ( "fields[record-labels]" , "name" )
// 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/lyrics" , storefront , songId ) , 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 )
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 )
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 rip ( albumId string , token string , storefront string , userToken string ) error {
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}" , "" ,
) . Replace ( config . ArtistFolderFormat )
} else if len ( meta . Data [ 0 ] . Relationships . Artists . Data ) > 0 {
singerFoldername = strings . NewReplacer (
"{ArtistName}" , meta . Data [ 0 ] . Attributes . ArtistName ,
"{ArtistId}" , meta . Data [ 0 ] . Relationships . Artists . Data [ 0 ] . ID ,
) . Replace ( config . ArtistFolderFormat )
} else {
singerFoldername = strings . NewReplacer (
"{ArtistName}" , 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 . AtmosSaveFolder , forbiddenNames . ReplaceAllString ( singerFoldername , "_" ) )
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
Quality := fmt . Sprintf ( "%dkbps" , config . AtmosMax - 2000 )
if strings . Contains ( albumId , "pl." ) {
albumFolder = strings . NewReplacer (
"{ArtistName}" , "Apple Music" ,
"{PlaylistName}" , meta . Data [ 0 ] . Attributes . Name ,
"{PlaylistId}" , albumId ,
"{Quality}" , Quality ,
"{Codec}" , "Atmos" ,
"{Tag}" , Tag_string ,
) . Replace ( config . PlaylistFolderFormat )
} else {
albumFolder = strings . NewReplacer (
"{ReleaseDate}" , meta . Data [ 0 ] . Attributes . ReleaseDate ,
"{ReleaseYear}" , meta . Data [ 0 ] . Attributes . ReleaseDate [ : 4 ] ,
"{ArtistName}" , meta . Data [ 0 ] . Attributes . ArtistName ,
"{AlbumName}" , meta . Data [ 0 ] . Attributes . Name ,
"{UPC}" , meta . Data [ 0 ] . Attributes . Upc ,
"{Copyright}" , meta . Data [ 0 ] . Attributes . Copyright ,
"{AlbumId}" , albumId ,
"{Quality}" , Quality ,
"{Codec}" , "Atmos" ,
"{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 )
err = writeCover ( sanAlbumFolder , "cover" , meta . Data [ 0 ] . Attributes . Artwork . URL )
if err != nil {
fmt . Println ( "Failed to write cover." )
}
trackTotal := len ( meta . Data [ 0 ] . Relationships . Tracks . Data )
for trackNum , track := range meta . Data [ 0 ] . Relationships . Tracks . Data {
trackNum ++
trackTotalnum += 1
fmt . Printf ( "Track %d of %d:\n" , trackNum , trackTotal )
manifest , err := getInfoFromAdam ( track . ID , token , storefront )
if err != nil {
fmt . Println ( "Failed to get manifest.\n" , err )
continue
}
if manifest . Attributes . ExtendedAssetUrls . EnhancedHls == "" {
fmt . Println ( "Unavailable in ALAC." )
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}" , track . Attributes . Name ,
"{DiscNumber}" , fmt . Sprintf ( "%0d" , track . Attributes . DiscNumber ) ,
"{TrackNumber}" , fmt . Sprintf ( "%0d" , track . Attributes . TrackNumber ) ,
"{Quality}" , Quality ,
"{Codec}" , "Atmos" ,
"{Tag}" , Tag_string ,
) . Replace ( config . SongFileFormat )
fmt . Println ( songName )
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 )
exists , err := fileExists ( trackPath )
m4aexists , errs := fileExists ( m4atrackPath )
if err != nil {
fmt . Println ( "Failed to check if track exists." )
}
if errs != nil {
fmt . Println ( "Failed to check if m4atrack exists." )
}
if exists || m4aexists {
fmt . Println ( "Track or M4atrack already exists locally." )
oktrackNum += 1
continue
}
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 = ""
}
}
}
}
}
trackUrl , keys , err := extractMedia ( manifest . Attributes . ExtendedAssetUrls . EnhancedHls )
if err != nil {
fmt . Println ( "Failed to extract info from manifest.\n" , err )
continue
}
info , err := extractSong ( trackUrl )
if err != nil {
fmt . Println ( "Failed to extract track." , err )
continue
}
samplesOk := true
for samplesOk {
for _ , i := range info . samples {
if int ( i . descIndex ) >= len ( keys ) {
fmt . Println ( "Decryption size mismatch." )
samplesOk = false
}
}
break
}
if ! samplesOk {
continue
}
err = decryptSong ( info , keys , meta , trackPath , trackNum , trackTotal )
if err != nil {
fmt . Println ( "Failed to decrypt track.\n" , err )
continue
}
index := trackNum - 1
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 ] . 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 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 ) )
}
}
if strings . Contains ( albumId , "pl." ) && ! config . UseSongInfoForPlaylist {
tags = append ( tags , "disk=1/1" )
tags = append ( tags , fmt . Sprintf ( "track=%s" , 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=%s" , 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" )
}
tagsString := strings . Join ( tags , ":" )
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 )
fmt . Printf ( "Encapsulating %s into %s\n" , filepath . Base ( trackPath ) , filepath . Base ( m4atrackPath ) )
if err := cmd . Run ( ) ; err != nil {
fmt . Printf ( "Error encapsulating file: %v\n" , err )
continue
}
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 )
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 )
continue
}
}
fmt . Printf ( "Successfully processed and deleted %s\n" , filepath . Base ( trackPath ) )
oktrackNum += 1
}
return err
}
func main ( ) {
err := loadConfig ( )
if err != nil {
fmt . Printf ( "load config failed: %v" , err )
return
}
token , err := getToken ( )
if err != nil {
fmt . Println ( "Failed to get token." )
return
}
if strings . Contains ( os . Args [ 1 ] , "/artist/" ) {
newArgs , err := checkArtist ( os . Args [ 1 ] , token )
if err != nil {
fmt . Println ( "Failed to get artist." )
return
}
os . Args = append ( [ ] string { os . Args [ 0 ] } , newArgs ... )
}
albumTotal := len ( os . Args [ 1 : ] )
for albumNum , url := range os . Args [ 1 : ] {
fmt . Printf ( "Album %d of %d:\n" , albumNum + 1 , albumTotal )
var storefront , albumId string
if strings . Contains ( url , "/playlist/" ) {
storefront , albumId = checkUrlPlaylist ( url )
} else {
storefront , albumId = checkUrl ( url )
}
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 ( "======= Completed %d/%d ###### %d errors!! =======\n" , oktrackNum , trackTotalnum , trackTotalnum - oktrackNum )
}
func conventTTMLToLRC ( ttml string ) ( string , error ) {
parsedTTML := etree . NewDocument ( )
err := parsedTTML . ReadFromString ( ttml )
if err != nil {
return "" , err
}
var lrcLines [ ] string
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 )
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 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 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
}
}
}
if streamUrl == nil {
return "" , nil , errors . New ( "no ec-3 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 strings . HasSuffix ( match [ 1 ] , "c24" ) || strings . HasSuffix ( match [ 1 ] , "c6" ) {
keys = append ( keys , match [ 1 ] )
}
}
return streamUrl . String ( ) , keys , 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 )
}
rawSong , err := ioutil . ReadAll ( track . Body )
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
}
// 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{mp4.StrToBoxType("dec3")})
// if err != nil || len(aalac) != 1 {
// return nil, err
// }
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" `
} ` 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" `
} ` 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" `
}