From 7fafb886b9b5ddba4cb89f3383b037b24af006a4 Mon Sep 17 00:00:00 2001 From: itouakirai Date: Thu, 9 Jan 2025 21:54:21 +0800 Subject: [PATCH] add utils --- .gitignore | 2 +- utils/runv2/LICENSE | 21 ++ utils/runv2/runv2.go | 681 +++++++++++++++++++++++++++++++++++++++ utils/structs/structs.go | 430 ++++++++++++++++++++++++ 4 files changed, 1133 insertions(+), 1 deletion(-) create mode 100644 utils/runv2/LICENSE create mode 100644 utils/runv2/runv2.go create mode 100644 utils/structs/structs.go diff --git a/.gitignore b/.gitignore index 20818e5..32d457c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,4 @@ !go.sum !main.go !README.md -!/utils +!utils/* diff --git a/utils/runv2/LICENSE b/utils/runv2/LICENSE new file mode 100644 index 0000000..fa8d9f1 --- /dev/null +++ b/utils/runv2/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Sendy McSenderson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/utils/runv2/runv2.go b/utils/runv2/runv2.go new file mode 100644 index 0000000..1b1e8f2 --- /dev/null +++ b/utils/runv2/runv2.go @@ -0,0 +1,681 @@ +package runv2 + +import ( + "bufio" + "bytes" + "context" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + "time" + + "github.com/Eyevinn/mp4ff/mp4" + "github.com/grafov/m3u8" + + "encoding/binary" + "github.com/schollz/progressbar/v3" + + "main/utils/structs" +) +const prefetchKey = "skd://itunes.apple.com/P000000000/s1/e1" +var ErrTimeout = errors.New("response timed out") + +type TimedResponseBody struct { + timeout time.Duration + timer *time.Timer + threshold int + body io.Reader +} + +func (b *TimedResponseBody) Read(p []byte) (int, error) { + n, err := b.body.Read(p) + if err != nil { + return n, err + } + // fmt.Printf("Read %d bytes, buffer size %d bytes", n, len(p)) + if n >= b.threshold { + b.timer.Reset(b.timeout) + } + return n, err +} + + +func Run(adamId string, playlistUrl string, outfile string, Config structs.ConfigSet) error { + var err error + var optstimeout uint + optstimeout = 20000 + timeout := time.Duration(optstimeout * uint(time.Millisecond)) + header := make(http.Header) + + // request media playlist + req, err := http.NewRequest("GET", playlistUrl, nil) + if err != nil { + return err + } + req.Header = header + // requesting an HLS playlist should be relatively fast, so we set the timeout directly on the client + do, err := (&http.Client{Timeout: timeout}).Do(req) + if err != nil { + return err + } + + // parse m3u8 + segments, err := parseMediaPlaylist(do.Body) + if err != nil { + return err + } + segment := segments[0] + if segment == nil { + return errors.New("no segments extracted from playlist") + } + if segment.Limit <= 0 { + return errors.New("non-byterange playlists are currently unsupported") + } + + // get URL to the actual file + parsedUrl, err := url.Parse(playlistUrl) + if err != nil { + return err + } + fileUrl, err := parsedUrl.Parse(segment.URI) + if err != nil { + return err + } + + // request mp4 + ctx, cancel := context.WithCancelCause(context.Background()) + defer cancel(nil) + req, err = http.NewRequestWithContext(ctx, "GET", fileUrl.String(), nil) + if err != nil { + return err + } + req.Header = header + + var body io.Reader + client := &http.Client{Timeout: timeout} + if optstimeout > 0 { + // create the timer before calling Do so that the timeout covers TCP handshake, + // TLS handshake, sending the request and receiving HTTP headers + timer := time.AfterFunc(timeout, func() { cancel(ErrTimeout) }) + do, err = client.Do(req) + if err != nil { + return err + } + defer do.Body.Close() + body = &TimedResponseBody{ + timeout: timeout, + timer: timer, + threshold: 256, + body: do.Body, + } + } else { + do, err = client.Do(req) + if err != nil { + return err + } + defer do.Body.Close() + body = do.Body + } + + var totalLen int64 + totalLen = do.ContentLength + + + + // connect to decryptor + //addr := fmt.Sprintf("127.0.0.1:10020") + addr := Config.DecryptM3u8Port + conn, err := net.Dial("tcp", addr) + if err != nil { + return err + } + fmt.Print("Downloading...\n") + defer Close(conn) + + err = downloadAndDecryptFile(conn, body, outfile, adamId, segments, totalLen, Config) + if err != nil { + return err + } + fmt.Print("Decryption finished\n") + // create output file + // ofh, err := os.Create(outfile) + // if err != nil { + // return err + // } + // defer ofh.Close() + // + // _, err = ofh.Write(buffer.Bytes()) + // if err != nil { + // return err + // } + + return nil +} + +func downloadAndDecryptFile(conn io.ReadWriter, in io.Reader, outfile string, + adamId string, playlistSegments []*m3u8.MediaSegment, totalLen int64, Config structs.ConfigSet) error { + var buffer bytes.Buffer + var outBuf *bufio.Writer + MaxMemorySize := int64(Config.MaxMemoryLimit * 1024 * 1024) + inBuf := bufio.NewReader(in) + if totalLen <= MaxMemorySize { + outBuf = bufio.NewWriter(&buffer) + } else { + ofh, err := os.Create(outfile) + if err != nil { + return err + } + defer ofh.Close() + outBuf = bufio.NewWriter(ofh) + } + init, offset, err := ReadInitSegment(inBuf) + if err != nil { + return err + } + if init == nil { + return errors.New("no init segment found") + } + + tracks, err := TransformInit(init) + if err != nil { + return err + } + err = sanitizeInit(init) + if err != nil { + // errors returned by sanitizeInit are non-fatal + fmt.Printf("Warning: unable to sanitize init completely: %s\n", err) + } + err = init.Encode(outBuf) + if err != nil { + return err + } + + // 'segment' in m3u8 == 'fragment' in mp4ff + //fmt.Println("Starting decryption...") + bar := progressbar.NewOptions64(totalLen, + progressbar.OptionClearOnFinish(), + progressbar.OptionSetElapsedTime(false), + progressbar.OptionSetPredictTime(false), + progressbar.OptionShowElapsedTimeOnFinish(), + progressbar.OptionShowCount(), + progressbar.OptionEnableColorCodes(true), + progressbar.OptionShowBytes(true), + //progressbar.OptionSetDescription("Decrypting..."), + progressbar.OptionSetTheme(progressbar.Theme{ + Saucer: "", + SaucerHead: "", + SaucerPadding: "", + BarStart: "", + BarEnd: "", + }), + ) + bar.Add64(int64(offset)) + rw := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)) + for i := 0; ; i++ { + var frag *mp4.Fragment + rawoffset := offset + frag, offset, err = ReadNextFragment(inBuf, offset) + rawoffset = offset - rawoffset + if err != nil { + return err + } + if frag == nil { + // check offset against Content-Length? + break + } + // print progress + + // if totalLen > 0 { + // fmt.Printf("%.2f%% of %d bytes\n", 100*float32(offset)/float32(totalLen), totalLen) + // } + segment := playlistSegments[i] + if segment == nil { + return errors.New("segment number out of sync") + } + key := segment.Key + if key != nil { + if i != 0 { + SwitchKeys(rw) + } + if key.URI == prefetchKey { + SendString(rw, "0") + } else { + SendString(rw, adamId) + } + SendString(rw, key.URI) + } + // flushes the buffer + err = DecryptFragment(frag, tracks, rw) + if err != nil { + return fmt.Errorf("decryptFragment: %w", err) + } + err = frag.Encode(outBuf) + if err != nil { + return err + } + bar.Add64(int64(rawoffset)) + } + err = outBuf.Flush() + if err != nil { + return err + } + if totalLen <= MaxMemorySize { + // create output file + ofh, err := os.Create(outfile) + if err != nil { + return err + } + defer ofh.Close() + + _, err = ofh.Write(buffer.Bytes()) + if err != nil { + return err + } + } + return nil +} + +// Remove boxes in the init segment that are known to cause compatibility issues +func sanitizeInit(init *mp4.InitSegment) error { + traks := init.Moov.Traks + if len(traks) > 1 { + return errors.New("more than 1 track found") + } + // Remove duplicate ec-3 or alac boxes in stsd since some programs (e.g. cuetools) don't + // like it when there's more than 1 entry in stsd. + // Every audio track contains two of these boxes because two IVs are needed to decrypt the + // track. The two boxes become identical after removing encryption info. + stsd := traks[0].Mdia.Minf.Stbl.Stsd + if stsd.SampleCount == 1 { + return nil + } + if stsd.SampleCount > 2 { + return fmt.Errorf("expected only 1 or 2 entries in stsd, got %d", stsd.SampleCount) + } + children := stsd.Children + if children[0].Type() != children[1].Type() { + return errors.New("children in stsd are not of the same type") + } + stsd.Children = children[:1] + stsd.SampleCount = 1 + return nil +} + +// Workaround for m3u8 not supporting multiple keys - remove +// PlayReady and Widevine +func filterResponse(f io.Reader) (*bytes.Buffer, error) { + buf := &bytes.Buffer{} + scanner := bufio.NewScanner(f) + + prefix := []byte("#EXT-X-KEY:") + keyFormat := []byte("streamingkeydelivery") + for scanner.Scan() { + lineBytes := scanner.Bytes() + if bytes.HasPrefix(lineBytes, prefix) && !bytes.Contains(lineBytes, keyFormat) { + continue + } + _, err := buf.Write(lineBytes) + if err != nil { + return nil, err + } + _, err = buf.WriteString("\n") + if err != nil { + return nil, err + } + } + if err := scanner.Err(); err != nil { + return nil, err + } + return buf, nil +} + +func parseMediaPlaylist(r io.ReadCloser) ([]*m3u8.MediaSegment, error) { + defer r.Close() + playlistBuf, err := filterResponse(r) + if err != nil { + return nil, err + } + + playlist, listType, err := m3u8.Decode(*playlistBuf, true) + if err != nil { + return nil, err + } + + if listType != m3u8.MEDIA { + return nil, errors.New("m3u8 not of media type") + } + + mediaPlaylist := playlist.(*m3u8.MediaPlaylist) + return mediaPlaylist.Segments, nil +} + +//pasing +func ReadInitSegment(r io.Reader) (*mp4.InitSegment, uint64, error) { + var offset uint64 = 0 + init := mp4.NewMP4Init() + for i := 0; i < 2; i++ { + box, err := mp4.DecodeBox(offset, r) + if err != nil { + return nil, offset, err + } + boxType := box.Type() + if boxType != "ftyp" && boxType != "moov" { + return nil, offset, fmt.Errorf("unexpected box type %s, should be ftyp or moov", boxType) + } + init.AddChild(box) + offset += box.Size() + } + return init, offset, nil +} + +// Get the next fragment. Returns nil and no error on EOF +func ReadNextFragment(r io.Reader, offset uint64) (*mp4.Fragment, uint64, error) { + frag := mp4.NewFragment() + for { + box, err := mp4.DecodeBox(offset, r) + if err == io.EOF { + return nil, offset, nil + } + if err != nil { + return nil, offset, err + } + boxType := box.Type() + // fmt.Printf("processing %s, box starts @ offset %d\n", boxType, offset) + offset += box.Size() + if boxType == "moof" || boxType == "emsg" || boxType == "prft" { + frag.AddChild(box) + continue + } + if boxType == "mdat" { + frag.AddChild(box) + break + } + fmt.Printf("ignoring a %s box found mid-stream", boxType) + } + // only 1 mdat box in fragment, meaning that the box doesn't have a preceding moof box + if frag.Moof == nil { + return nil, offset, fmt.Errorf("more than one mdat box in fragment (box ends @ offset %d)", offset) + } + return frag, offset, nil +} + +// Return a new slice of boxes with encryption-related sbgp and sgpd removed, +// and the total number of bytes removed. +// Non-encryption-related ones such as 'roll' are left untouched. +func FilterSbgpSgpd(children []mp4.Box) ([]mp4.Box, uint64) { + var bytesRemoved uint64 = 0 + remainingChildren := make([]mp4.Box, 0, len(children)) + for _, child := range children { + switch box := child.(type) { + case *mp4.SbgpBox: + if box.GroupingType == "seam" || box.GroupingType == "seig" { + bytesRemoved += child.Size() + continue + } + case *mp4.SgpdBox: + if box.GroupingType == "seam" || box.GroupingType == "seig" { + bytesRemoved += child.Size() + continue + } + } + remainingChildren = append(remainingChildren, child) + } + return remainingChildren, bytesRemoved +} + +// Get decryption info for tracks from init segment and remove encryption-related boxes +func TransformInit(init *mp4.InitSegment) (map[uint32]mp4.DecryptTrackInfo, error) { + di, err := mp4.DecryptInit(init) + tracks := make(map[uint32]mp4.DecryptTrackInfo, len(di.TrackInfos)) + for _, ti := range di.TrackInfos { + tracks[ti.TrackID] = ti + } + if err != nil { + return tracks, err + } + // remove encryption-related sbgp and sgpd + for _, trak := range init.Moov.Traks { + stbl := trak.Mdia.Minf.Stbl + stbl.Children, _ = FilterSbgpSgpd(stbl.Children) + } + return tracks, nil +} +//remote +// Reset the loops on the script's end and close the connection +func Close(conn io.WriteCloser) error { + defer conn.Close() + _, err := conn.Write([]byte{0, 0, 0, 0, 0}) + return err +} + +func SwitchKeys(conn io.Writer) error { + _, err := conn.Write([]byte{0, 0, 0, 0}) + return err +} + +// Send id or keyUri +func SendString(conn io.Writer, uri string) error { + _, err := conn.Write([]byte{byte(len(uri))}) + if err != nil { + return err + } + _, err = io.WriteString(conn, uri) + return err +} + + + +func cbcsFullSubsampleDecrypt(data []byte, conn *bufio.ReadWriter) error { + // Drops 4 last bits -> multiple of 16 + // It wouldn't hurt to send the remaining bytes also because the decryption + // function would just return them as-is, but we're truncating the data here + // for clarity and interoperability + truncatedLen := len(data) & ^0xf + // send the whole chunk at once + err := binary.Write(conn, binary.LittleEndian, uint32(truncatedLen)) + if err != nil { + return err + } + _, err = conn.Write(data[:truncatedLen]) + if err != nil { + return err + } + err = conn.Flush() + if err != nil { + return err + } + _, err = io.ReadFull(conn, data[:truncatedLen]) + return err +} + +func cbcsStripeDecrypt(data []byte, conn *bufio.ReadWriter, decryptBlockLen, skipBlockLen int) error { + size := len(data) + + // block too small, ignore + if size < decryptBlockLen { + return nil + } + + // number of encrypted blocks in this sample + count := ((size - decryptBlockLen) / (decryptBlockLen + skipBlockLen)) + 1 + totalLen := count * decryptBlockLen + + err := binary.Write(conn, binary.LittleEndian, uint32(totalLen)) + if err != nil { + return err + } + + pos := 0 + for { + if size-pos < decryptBlockLen { // Leave the rest + break + } + _, err = conn.Write(data[pos : pos+decryptBlockLen]) + if err != nil { + return err + } + pos += decryptBlockLen + if size-pos < skipBlockLen { + break + } + pos += skipBlockLen + } + err = conn.Flush() + if err != nil { + return err + } + + pos = 0 + for { + if size-pos < decryptBlockLen { + break + } + _, err = io.ReadFull(conn, data[pos:pos+decryptBlockLen]) + if err != nil { + return err + } + pos += decryptBlockLen + if size-pos < skipBlockLen { + break + } + pos += skipBlockLen + } + return nil +} + +// Decryption function dispatcher +func cbcsDecryptRaw(data []byte, conn *bufio.ReadWriter, decryptBlockLen, skipBlockLen int) error { + if skipBlockLen == 0 { + // Full encryption of subsamples + // e.g. Apple Music ALAC + return cbcsFullSubsampleDecrypt(data, conn) + } else { + // Pattern (stripe) encryption of subsamples + // e.g. most AVC and HEVC applications + return cbcsStripeDecrypt(data, conn, decryptBlockLen, skipBlockLen) + } +} + +// Decrypt a cbcs-encrypted sample in-place +func cbcsDecryptSample(sample []byte, conn *bufio.ReadWriter, + subSamplePatterns []mp4.SubSamplePattern, tenc *mp4.TencBox) error { + + decryptBlockLen := int(tenc.DefaultCryptByteBlock) * 16 + skipBlockLen := int(tenc.DefaultSkipByteBlock) * 16 + var pos uint32 = 0 + + // Full sample encryption + if len(subSamplePatterns) == 0 { + return cbcsDecryptRaw(sample, conn, decryptBlockLen, skipBlockLen) + } + + // Has subsamples + for j := 0; j < len(subSamplePatterns); j++ { + ss := subSamplePatterns[j] + pos += uint32(ss.BytesOfClearData) + + // Nothing to decrypt! + if ss.BytesOfProtectedData <= 0 { + continue + } + + err := cbcsDecryptRaw(sample[pos:pos+ss.BytesOfProtectedData], + conn, decryptBlockLen, skipBlockLen) + if err != nil { + return err + } + pos += ss.BytesOfProtectedData + } + + return nil +} + +// Decrypt an array of cbcs-encrypted samples in-place +func cbcsDecryptSamples(samples []mp4.FullSample, conn *bufio.ReadWriter, + tenc *mp4.TencBox, senc *mp4.SencBox) error { + + for i := range samples { + var subSamplePatterns []mp4.SubSamplePattern + if len(senc.SubSamples) != 0 { + subSamplePatterns = senc.SubSamples[i] + } + err := cbcsDecryptSample(samples[i].Data, conn, subSamplePatterns, tenc) + if err != nil { + return err + } + } + return nil +} + +func DecryptFragment(frag *mp4.Fragment, tracks map[uint32]mp4.DecryptTrackInfo, conn *bufio.ReadWriter) error { + moof := frag.Moof + var bytesRemoved uint64 = 0 + var sxxxBytesRemoved uint64 + + for _, traf := range moof.Trafs { + ti, ok := tracks[traf.Tfhd.TrackID] + if !ok { + return fmt.Errorf("could not find decryption info for track %d", traf.Tfhd.TrackID) + } + if ti.Sinf == nil { + // unencrypted track + continue + } + + schemeType := ti.Sinf.Schm.SchemeType + if schemeType != "cbcs" { + return fmt.Errorf("scheme type %s not supported", schemeType) + } + hasSenc, isParsed := traf.ContainsSencBox() + if !hasSenc { + return fmt.Errorf("no senc box in traf") + } + + var senc *mp4.SencBox + if traf.Senc != nil { + senc = traf.Senc + } else { + senc = traf.UUIDSenc.Senc + } + + if !isParsed { + // simply ignore sbgp and sgpd + // "Sample To Group Box ('sbgp') and Sample Group Description Box ('sgpd') + // of type 'seig' are used to indicate the KID applied to each sample, and changes + // to KIDs over time (i.e. 'key rotation')" + // (ref: https://dashif.org/docs/DASH-IF-IOP-v3.2.pdf) + err := senc.ParseReadBox(ti.Sinf.Schi.Tenc.DefaultPerSampleIVSize, traf.Saiz) + if err != nil { + return err + } + } + + samples, err := frag.GetFullSamples(ti.Trex) + if err != nil { + return err + } + + err = cbcsDecryptSamples(samples, conn, ti.Sinf.Schi.Tenc, senc) + if err != nil { + return err + } + + bytesRemoved += traf.RemoveEncryptionBoxes() + // remove sbgp and sgpd + traf.Children, sxxxBytesRemoved = FilterSbgpSgpd(traf.Children) + bytesRemoved += sxxxBytesRemoved + } + _, psshBytesRemoved := moof.RemovePsshs() + bytesRemoved += psshBytesRemoved + for _, traf := range moof.Trafs { + for _, trun := range traf.Truns { + trun.DataOffset -= int32(bytesRemoved) + } + } + + return nil +} diff --git a/utils/structs/structs.go b/utils/structs/structs.go new file mode 100644 index 0000000..3a59a1f --- /dev/null +++ b/utils/structs/structs.go @@ -0,0 +1,430 @@ +package structs + +type ConfigSet struct { + MediaUserToken string `yaml:"media-user-token"` + AuthorizationToken string `yaml:"authorization-token"` + Language string `yaml:"language"` + SaveLrcFile bool `yaml:"save-lrc-file"` + LrcType string `yaml:"lrc-type"` + LrcFormat string `yaml:"lrc-format"` + SaveAnimatedArtwork bool `yaml:"save-animated-artwork"` + EmbyAnimatedArtwork bool `yaml:"emby-animated-artwork"` + EmbedLrc bool `yaml:"embed-lrc"` + EmbedCover bool `yaml:"embed-cover"` + SaveArtistCover bool `yaml:"save-artist-cover"` + CoverSize string `yaml:"cover-size"` + CoverFormat string `yaml:"cover-format"` + AlacSaveFolder string `yaml:"alac-save-folder"` + AtmosSaveFolder string `yaml:"atmos-save-folder"` + AlbumFolderFormat string `yaml:"album-folder-format"` + PlaylistFolderFormat string `yaml:"playlist-folder-format"` + ArtistFolderFormat string `yaml:"artist-folder-format"` + SongFileFormat string `yaml:"song-file-format"` + ExplicitChoice string `yaml:"explicit-choice"` + CleanChoice string `yaml:"clean-choice"` + AppleMasterChoice string `yaml:"apple-master-choice"` + MaxMemoryLimit int `yaml:"max-memory-limit"` + DecryptM3u8Port string `yaml:"decrypt-m3u8-port"` + GetM3u8Port string `yaml:"get-m3u8-port"` + GetM3u8Mode string `yaml:"get-m3u8-mode"` + GetM3u8FromDevice bool `yaml:"get-m3u8-from-device"` + AlacMax int `yaml:"alac-max"` + AtmosMax int `yaml:"atmos-max"` + LimitMax int `yaml:"limit-max"` + UseSongInfoForPlaylist bool `yaml:"use-songinfo-for-playlist"` + DlAlbumcoverForPlaylist bool `yaml:"dl-albumcover-for-playlist"` +} + +type Counter struct { + Unavailable int + NotSong int + Error int + Success int + Total int +} + +type ApiResult struct { + Data []SongData `json:"data"` +} + +type SongAttributes struct { + ArtistName string `json:"artistName"` + DiscNumber int `json:"discNumber"` + GenreNames []string `json:"genreNames"` + ExtendedAssetUrls struct { + EnhancedHls string `json:"enhancedHls"` + } `json:"extendedAssetUrls"` + IsMasteredForItunes bool `json:"isMasteredForItunes"` + IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` + ContentRating string `json:"contentRating"` + ReleaseDate string `json:"releaseDate"` + Name string `json:"name"` + Isrc string `json:"isrc"` + AlbumName string `json:"albumName"` + TrackNumber int `json:"trackNumber"` + ComposerName string `json:"composerName"` +} + +type AlbumAttributes struct { + ArtistName string `json:"artistName"` + IsSingle bool `json:"isSingle"` + IsComplete bool `json:"isComplete"` + GenreNames []string `json:"genreNames"` + TrackCount int `json:"trackCount"` + IsMasteredForItunes bool `json:"isMasteredForItunes"` + IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` + ContentRating string `json:"contentRating"` + ReleaseDate string `json:"releaseDate"` + Name string `json:"name"` + RecordLabel string `json:"recordLabel"` + Upc string `json:"upc"` + Copyright string `json:"copyright"` + IsCompilation bool `json:"isCompilation"` +} + +type SongData struct { + ID string `json:"id"` + Attributes SongAttributes `json:"attributes"` + Relationships struct { + Albums struct { + Data []struct { + ID string `json:"id"` + Type string `json:"type"` + Href string `json:"href"` + Attributes AlbumAttributes `json:"attributes"` + } `json:"data"` + } `json:"albums"` + Artists struct { + Href string `json:"href"` + Data []struct { + ID string `json:"id"` + Type string `json:"type"` + Href string `json:"href"` + } `json:"data"` + } `json:"artists"` + } `json:"relationships"` +} + +type SongResult struct { + Artwork struct { + Width int `json:"width"` + URL string `json:"url"` + Height int `json:"height"` + TextColor3 string `json:"textColor3"` + TextColor2 string `json:"textColor2"` + TextColor4 string `json:"textColor4"` + HasAlpha bool `json:"hasAlpha"` + TextColor1 string `json:"textColor1"` + BgColor string `json:"bgColor"` + HasP3 bool `json:"hasP3"` + SupportsLayeredImage bool `json:"supportsLayeredImage"` + } `json:"artwork"` + ArtistName string `json:"artistName"` + CollectionID string `json:"collectionId"` + DiscNumber int `json:"discNumber"` + GenreNames []string `json:"genreNames"` + ID string `json:"id"` + DurationInMillis int `json:"durationInMillis"` + ReleaseDate string `json:"releaseDate"` + ContentRatingsBySystem struct { + } `json:"contentRatingsBySystem"` + Name string `json:"name"` + Composer struct { + Name string `json:"name"` + URL string `json:"url"` + } `json:"composer"` + EditorialArtwork struct { + } `json:"editorialArtwork"` + CollectionName string `json:"collectionName"` + AssetUrls struct { + Plus string `json:"plus"` + Lightweight string `json:"lightweight"` + SuperLightweight string `json:"superLightweight"` + LightweightPlus string `json:"lightweightPlus"` + EnhancedHls string `json:"enhancedHls"` + } `json:"assetUrls"` + AudioTraits []string `json:"audioTraits"` + Kind string `json:"kind"` + Copyright string `json:"copyright"` + ArtistID string `json:"artistId"` + Genres []struct { + GenreID string `json:"genreId"` + Name string `json:"name"` + URL string `json:"url"` + MediaType string `json:"mediaType"` + } `json:"genres"` + TrackNumber int `json:"trackNumber"` + AudioLocale string `json:"audioLocale"` + Offers []struct { + ActionText struct { + Short string `json:"short"` + Medium string `json:"medium"` + Long string `json:"long"` + Downloaded string `json:"downloaded"` + Downloading string `json:"downloading"` + } `json:"actionText"` + Type string `json:"type"` + PriceFormatted string `json:"priceFormatted"` + Price float64 `json:"price"` + BuyParams string `json:"buyParams"` + Variant string `json:"variant,omitempty"` + Assets []struct { + Flavor string `json:"flavor"` + Preview struct { + Duration int `json:"duration"` + URL string `json:"url"` + } `json:"preview"` + Size int `json:"size"` + Duration int `json:"duration"` + } `json:"assets"` + } `json:"offers"` +} + +type AutoGenerated struct { + Data []struct { + ID string `json:"id"` + Type string `json:"type"` + Href string `json:"href"` + Attributes struct { + Artwork struct { + Width int `json:"width"` + Height int `json:"height"` + URL string `json:"url"` + BgColor string `json:"bgColor"` + TextColor1 string `json:"textColor1"` + TextColor2 string `json:"textColor2"` + TextColor3 string `json:"textColor3"` + TextColor4 string `json:"textColor4"` + } `json:"artwork"` + ArtistName string `json:"artistName"` + IsSingle bool `json:"isSingle"` + URL string `json:"url"` + IsComplete bool `json:"isComplete"` + GenreNames []string `json:"genreNames"` + TrackCount int `json:"trackCount"` + IsMasteredForItunes bool `json:"isMasteredForItunes"` + IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` + ContentRating string `json:"contentRating"` + ReleaseDate string `json:"releaseDate"` + Name string `json:"name"` + RecordLabel string `json:"recordLabel"` + Upc string `json:"upc"` + AudioTraits []string `json:"audioTraits"` + Copyright string `json:"copyright"` + PlayParams struct { + ID string `json:"id"` + Kind string `json:"kind"` + } `json:"playParams"` + IsCompilation bool `json:"isCompilation"` + EditorialVideo struct { + MotionDetailSquare struct { + Video string `json:"video"` + } `json:"motionDetailSquare"` + MotionSquareVideo1x1 struct { + Video string `json:"video"` + } `json:"motionSquareVideo1x1"` + } `json:"editorialVideo"` + } `json:"attributes"` + Relationships struct { + RecordLabels struct { + Href string `json:"href"` + Data []interface{} `json:"data"` + } `json:"record-labels"` + Artists struct { + Href string `json:"href"` + Data []struct { + ID string `json:"id"` + Type string `json:"type"` + Href string `json:"href"` + Attributes struct { + Name string `json:"name"` + Artwork struct { + Url string `json:"url"` + } `json:"artwork"` + } `json:"attributes"` + } `json:"data"` + } `json:"artists"` + Tracks struct { + Href string `json:"href"` + Next string `json:"next"` + Data []struct { + ID string `json:"id"` + Type string `json:"type"` + Href string `json:"href"` + Attributes struct { + Previews []struct { + URL string `json:"url"` + } `json:"previews"` + Artwork struct { + Width int `json:"width"` + Height int `json:"height"` + URL string `json:"url"` + BgColor string `json:"bgColor"` + TextColor1 string `json:"textColor1"` + TextColor2 string `json:"textColor2"` + TextColor3 string `json:"textColor3"` + TextColor4 string `json:"textColor4"` + } `json:"artwork"` + ArtistName string `json:"artistName"` + URL string `json:"url"` + DiscNumber int `json:"discNumber"` + GenreNames []string `json:"genreNames"` + HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"` + IsMasteredForItunes bool `json:"isMasteredForItunes"` + IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` + ContentRating string `json:"contentRating"` + DurationInMillis int `json:"durationInMillis"` + ReleaseDate string `json:"releaseDate"` + Name string `json:"name"` + Isrc string `json:"isrc"` + AudioTraits []string `json:"audioTraits"` + HasLyrics bool `json:"hasLyrics"` + AlbumName string `json:"albumName"` + PlayParams struct { + ID string `json:"id"` + Kind string `json:"kind"` + } `json:"playParams"` + TrackNumber int `json:"trackNumber"` + AudioLocale string `json:"audioLocale"` + ComposerName string `json:"composerName"` + } `json:"attributes"` + Relationships struct { + Artists struct { + Href string `json:"href"` + Data []struct { + ID string `json:"id"` + Type string `json:"type"` + Href string `json:"href"` + Attributes struct { + Name string `json:"name"` + } `json:"attributes"` + } `json:"data"` + } `json:"artists"` + } `json:"relationships"` + } `json:"data"` + } `json:"tracks"` + } `json:"relationships"` + } `json:"data"` +} + +type AutoGeneratedTrack struct { + Href string `json:"href"` + Next string `json:"next"` + Data []struct { + ID string `json:"id"` + Type string `json:"type"` + Href string `json:"href"` + Attributes struct { + Previews []struct { + URL string `json:"url"` + } `json:"previews"` + Artwork struct { + Width int `json:"width"` + Height int `json:"height"` + URL string `json:"url"` + BgColor string `json:"bgColor"` + TextColor1 string `json:"textColor1"` + TextColor2 string `json:"textColor2"` + TextColor3 string `json:"textColor3"` + TextColor4 string `json:"textColor4"` + } `json:"artwork"` + ArtistName string `json:"artistName"` + URL string `json:"url"` + DiscNumber int `json:"discNumber"` + GenreNames []string `json:"genreNames"` + HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"` + IsMasteredForItunes bool `json:"isMasteredForItunes"` + IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` + ContentRating string `json:"contentRating"` + DurationInMillis int `json:"durationInMillis"` + ReleaseDate string `json:"releaseDate"` + Name string `json:"name"` + Isrc string `json:"isrc"` + AudioTraits []string `json:"audioTraits"` + HasLyrics bool `json:"hasLyrics"` + AlbumName string `json:"albumName"` + PlayParams struct { + ID string `json:"id"` + Kind string `json:"kind"` + } `json:"playParams"` + TrackNumber int `json:"trackNumber"` + AudioLocale string `json:"audioLocale"` + ComposerName string `json:"composerName"` + } `json:"attributes"` + Relationships struct { + Artists struct { + Href string `json:"href"` + Data []struct { + ID string `json:"id"` + Type string `json:"type"` + Href string `json:"href"` + Attributes struct { + Name string `json:"name"` + } `json:"attributes"` + } `json:"data"` + } `json:"artists"` + } `json:"relationships"` + } `json:"data"` +} + +type AutoGeneratedArtist struct { + Next string `json:"next"` + Data []struct { + ID string `json:"id"` + Type string `json:"type"` + Href string `json:"href"` + Attributes struct { + Previews []struct { + URL string `json:"url"` + } `json:"previews"` + Artwork struct { + Width int `json:"width"` + Height int `json:"height"` + URL string `json:"url"` + BgColor string `json:"bgColor"` + TextColor1 string `json:"textColor1"` + TextColor2 string `json:"textColor2"` + TextColor3 string `json:"textColor3"` + TextColor4 string `json:"textColor4"` + } `json:"artwork"` + ArtistName string `json:"artistName"` + URL string `json:"url"` + DiscNumber int `json:"discNumber"` + GenreNames []string `json:"genreNames"` + HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"` + IsMasteredForItunes bool `json:"isMasteredForItunes"` + IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` + ContentRating string `json:"contentRating"` + DurationInMillis int `json:"durationInMillis"` + ReleaseDate string `json:"releaseDate"` + Name string `json:"name"` + Isrc string `json:"isrc"` + AudioTraits []string `json:"audioTraits"` + HasLyrics bool `json:"hasLyrics"` + AlbumName string `json:"albumName"` + PlayParams struct { + ID string `json:"id"` + Kind string `json:"kind"` + } `json:"playParams"` + TrackNumber int `json:"trackNumber"` + AudioLocale string `json:"audioLocale"` + ComposerName string `json:"composerName"` + } `json:"attributes"` + } `json:"data"` +} + +type SongLyrics struct { + Data []struct { + Id string `json:"id"` + Type string `json:"type"` + Attributes struct { + Ttml string `json:"ttml"` + PlayParams struct { + Id string `json:"id"` + Kind string `json:"kind"` + CatalogId string `json:"catalogId"` + DisplayType int `json:"displayType"` + } `json:"playParams"` + } `json:"attributes"` + } `json:"data"` +}