package main

import (
	"bytes"
	"encoding/binary"
	"encoding/json"
	"gopkg.in/yaml.v2"
	"errors"
	"fmt"
	"github.com/beevik/etree"
	"io"
	"io/ioutil"
	"math"
	"net"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/abema/go-mp4"
	"github.com/grafov/m3u8"
	"bufio"
)

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"`
}

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
		}
		fmt.Printf("Sample size: %d\n", len(sp.data))

		_, 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 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, url string) error {
	covPath := filepath.Join(sanAlbumFolder, "cover." + 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, albumURL string) error {
	meta, err := getMeta(albumId, token, storefront)
	if err != nil {
		fmt.Println("Failed to get album metadata.\n")
		return err
	}
	singerFoldername := fmt.Sprintf("%s", meta.Data[0].Attributes.ArtistName)
	if strings.HasSuffix(singerFoldername, ".") {
		singerFoldername = strings.ReplaceAll(singerFoldername, ".", "")
	}
	singerFoldername = strings.TrimSpace(singerFoldername)
	singerFolder := filepath.Join(config.AtmosSaveFolder, forbiddenNames.ReplaceAllString(singerFoldername, "_"))
	albumFolder := fmt.Sprintf("%s [Dolby Atmos]", meta.Data[0].Attributes.Name)
	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(singerFoldername)
	fmt.Println(albumFolder)
	// New functionality: write the album URL to album_link.txt
    if err := writeAlbumURL(sanAlbumFolder, albumURL); err != nil { 
        fmt.Println("Error saving album URL:", err)
        // Decide if you want to return the error and halt or just log it and continue
    }
	err = writeCover(sanAlbumFolder, 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
		}

		trackUrl, keys, codecSuffix, selectedCodec, err := extractMedia(manifest.Attributes.ExtendedAssetUrls.EnhancedHls)
		if err != nil {
			fmt.Println("Failed to extract info from manifest.\n", err)
			continue
		}

		// selectedVariant := strings.ReplaceAll(codecSuffix, ".m3u8", "")
		// New lines to extract the desired codec part and construct filenames 
		newCodecSuffix := strings.SplitN(codecSuffix, "_", 3)[2]  // This will extract the desired codec suffix part
		fmt.Println("newCodecSuffix: ", newCodecSuffix)
		filename := fmt.Sprintf("%02d. %s [%s].ec3", trackNum, forbiddenNames.ReplaceAllString(track.Attributes.Name, ""), newCodecSuffix)
		// m4afilename := fmt.Sprintf("%02d. %s [%s].m4a", trackNum, forbiddenNames.ReplaceAllString(track.Attributes.Name, ""), newCodecSuffix)
		lrcFilename := fmt.Sprintf("%02d. %s.lrc", trackNum, forbiddenNames.ReplaceAllString(track.Attributes.Name, "_"))
		trackPath := filepath.Join(sanAlbumFolder, filename)
		// m4atrackPath := filepath.Join(sanAlbumFolder, m4afilename)
		m4atrackPath := strings.Replace(trackPath, "."+strings.Split(selectedCodec, ".")[0], ".m4a", 1) // Generate m4a path
		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 = ""
						}
					}
				}
			}
		}
		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
		}
		// Check if the codec is ec-3 for further processing
		if selectedCodec == "ec-3" {
			// Decrypt the song and encapsulate/decrypt
			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("album=%s", meta.Data[0].Attributes.Name),
				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("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),
				fmt.Sprintf("track=%d/%d", trackNum, trackTotal),
			}
			if config.EmbedCover {
				tags = append(tags, fmt.Sprintf("cover=%s/cover.%s", sanAlbumFolder, config.CoverFormat))
			}
			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
			}
			fmt.Printf("Successfully processed and deleted %s\n", filepath.Base(trackPath))
		} else {
			// If codec is not the ec-3:
			resp, err := http.Get(trackUrl)
			if err != nil {
				fmt.Println("Failed to download track.")
				continue
			}
			defer resp.Body.Close()
			
			f, err := os.Create(m4atrackPath)
			if err != nil {
				fmt.Printf("Error creating file %s: %v\n", m4atrackPath, err)
				continue
			}
			defer f.Close()
			
			_, err = io.Copy(f, resp.Body)
			if err != nil {
				fmt.Printf("Error saving file %s: %v\n", m4atrackPath, err)
				continue
			}
			fmt.Println("Successfully downloaded and saved track as m4a.")
		}
		oktrackNum += 1
	}
	return err
}

func writeAlbumURL(sanAlbumFolder string, url string) error {
    albumLinkFile := filepath.Join(sanAlbumFolder, "apple_music_link.txt")
    err := ioutil.WriteFile(albumLinkFile, []byte(url), 0644)
    if err != nil {
        fmt.Println("Failed to write album URL to file")
        return err
    }
    fmt.Printf("Apple Music URL saved to %s\n", albumLinkFile)
    return nil
}

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
	}
	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, url)
		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, 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 selectedVariant *m3u8.Variant
	variants := master.Variants

	// Display available codecs to the user
	fmt.Println("Available audio streams:")
	for i, variant := range variants {
		fmt.Printf("[%d] Codec: %s, Average Bandwidth: %d, URI: %s\n", i, variant.Codecs, variant.AverageBandwidth, variant.URI)
	}

	reader := bufio.NewReader(os.Stdin)
	fmt.Print("Enter the number of the audio stream you want to download: ")
	input, err := reader.ReadString('\n')
	if err != nil {
		return "", nil, "", "", err
	}
	input = strings.TrimSpace(input)
	selectedIndex, err := strconv.Atoi(input)
	if err != nil {
		return "", nil, "", "", err
	}
	if selectedIndex < 0 || selectedIndex >= len(variants) {
		return "", nil, "", "", errors.New("Invalid selection")
	}
	selectedVariant = variants[selectedIndex]

	selectedCodec := selectedVariant.Codecs
	streamUrl, err := masterUrl.Parse(selectedVariant.URI)
	if err != nil {
		return "", nil, "", "", err
	}

	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])
		}
	}

	// Generate the suffix based on the selected codec
	// codecSuffix := selectedVariant.Codecs
	// segments := strings.Split(selectedVariant.URI, "")
	// var codecSuffix string
	// if len(segments) > 0 && strings.HasSuffix(selectedVariant.Codecs, ".m3u8") {
	// if len(segments) > 0 {
		// codecSuffix = strings.TrimSuffix(segments[len(segments)-1], ".m3u8")
	// }
	codecSuffix := strings.TrimSuffix(filepath.Base(selectedVariant.URI), ".m3u8")
	fmt.Println("codecSuffix:", codecSuffix)
	// codecSuffix = strings.SplitN(codecSuffix, "", 3)[2]

	return streamUrl.String(), keys, codecSuffix, selectedCodec, 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
	}

	fmt.Println("Extracting samples.")
	for i, moof := range moofs {
		tfhd, err := mp4.ExtractBoxWithPayload(f, moof, []mp4.BoxType{
			mp4.BoxTypeTraf(),
			mp4.BoxTypeTfhd(),
		})
		if err != nil || len(tfhd) != 1 {
			fmt.Printf("Error extracting tfhd for moof %d: %v\n", i, err)
			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 {
			fmt.Printf("Error extracting trun for moof %d: %v\n", i, err)
			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 {
			fmt.Println("Offset mismatch detected.")
			return nil, errors.New("offset mismatch")
		}
	}
	fmt.Println("Samples extracted successfully.")
	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"`
	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"`
	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"`
			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"`
						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"`
			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 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"`
}


// code:
/*
ec-3 not working because condition if selectedCodec == "ec-3" is always false 
so, I am using index not codecSelection.

output:

(myenv) D:\ALAC features(enabled)\apple-music-alac-atmos-downloader-main>go run atms.go https://music.apple.com/us/album/happiness-begins/1461478261?ls
Album 1 of 1:
Jonas Brothers
Happiness Begins [Dolby Atmos]
Apple Music URL saved to AM-DL-Atmos downloads\Jonas Brothers\Happiness Begins [Dolby Atmos]\apple_music_link.txt
Track 1 of 14:
Available audio streams:
[0] Codec: mp4a.40.2, Average Bandwidth: 254249, URI: P856765898_A1461478389_audio_en_gr256_mp4a-40-2_dm.m3u8
[1] Codec: mp4a.40.2, Average Bandwidth: 254549, URI: P856765898_A1461478389_audio_en_gr256_mp4a-40-2_bm.m3u8
[2] Codec: mp4a.40.2, Average Bandwidth: 253930, URI: P856765898_A1461478389_audio_en_gr256_mp4a-40-2.m3u8
[3] Codec: ec-3, Average Bandwidth: 770164, URI: P856765898_A1461478389_audio_en_gr2768_mp4a-A6.m3u8
[4] Codec: ec-3, Average Bandwidth: 450164, URI: P856765898_A1461478389_audio_en_gr2448_mp4a-A6.m3u8
[5] Codec: alac, Average Bandwidth: 1653738, URI: P856765898_A1461478389_audio_en_gr2116_alac.m3u8
[6] Codec: mp4a.40.2, Average Bandwidth: 129194, URI: P856765898_A1461478389_audio_en_gr128_mp4a-40-2_dm.m3u8
[7] Codec: mp4a.40.2, Average Bandwidth: 128819, URI: P856765898_A1461478389_audio_en_gr128_mp4a-40-2.m3u8
[8] Codec: mp4a.40.2, Average Bandwidth: 129754, URI: P856765898_A1461478389_audio_en_gr128_mp4a-40-2_bm.m3u8
[9] Codec: mp4a.40.5, Average Bandwidth: 69196, URI: P856765898_A1461478389_audio_en_gr64_mp4a-40-2_bm.m3u8
[10] Codec: mp4a.40.5, Average Bandwidth: 69520, URI: P856765898_A1461478389_audio_en_gr64_mp4a-40-2_dm.m3u8
[11] Codec: mp4a.40.5, Average Bandwidth: 69418, URI: P856765898_A1461478389_audio_en_gr64_mp4a-40-2.m3u8
Enter the number of the audio stream you want to download: 3
codecSuffix: P856765898_A1461478389_audio_en_gr2768_mp4a-A6
newCodecSuffix:  audio_en_gr2768_mp4a-A6
Downloading...
Downloaded.
Extracting samples.
Samples extracted successfully.
Decrypt start.
Sample size: 3072
Sample size: 3072
Decrypt finished.
Encapsulating 01. Sucker [audio_en_gr2768_mp4a-A6].ec3 into 01. Sucker [audio_en_gr2768_mp4a-A6].ec3
Deleting original EC3 file: 01. Sucker [audio_en_gr2768_mp4a-A6].ec3
Successfully processed and deleted 01. Sucker [audio_en_gr2768_mp4a-A6].ec3

ec3 is lost here and m4a is not converting................. from ec3 to m4a - convert here
like encapsulate Encapsulating 01. Sucker [audio_en_gr2768_mp4a-A6].ec3 into 01. Sucker [audio_en_gr2768_mp4a-A6].m4a - like this



output:

Track 2 of 14:
Available audio streams:
[0] Codec: mp4a.40.2, Average Bandwidth: 257622, URI: P856766089_A1461478401_audio_en_gr256_mp4a-40-2.m3u8
[1] Codec: mp4a.40.2, Average Bandwidth: 260453, URI: P856766089_A1461478401_audio_en_gr256_mp4a-40-2_dm.m3u8
[2] Codec: mp4a.40.2, Average Bandwidth: 257826, URI: P856766089_A1461478401_audio_en_gr256_mp4a-40-2_bm.m3u8
[3] Codec: ec-3, Average Bandwidth: 770148, URI: P856766089_A1461478401_audio_en_gr2768_mp4a-A6.m3u8
[4] Codec: ec-3, Average Bandwidth: 450148, URI: P856766089_A1461478401_audio_en_gr2448_mp4a-A6.m3u8
[5] Codec: alac, Average Bandwidth: 1674736, URI: P856766089_A1461478401_audio_en_gr2116_alac.m3u8
[6] Codec: mp4a.40.2, Average Bandwidth: 131505, URI: P856766089_A1461478401_audio_en_gr128_mp4a-40-2_dm.m3u8
[7] Codec: mp4a.40.2, Average Bandwidth: 130805, URI: P856766089_A1461478401_audio_en_gr128_mp4a-40-2_bm.m3u8
[8] Codec: mp4a.40.2, Average Bandwidth: 130527, URI: P856766089_A1461478401_audio_en_gr128_mp4a-40-2.m3u8
[9] Codec: mp4a.40.5, Average Bandwidth: 69489, URI: P856766089_A1461478401_audio_en_gr64_mp4a-40-2_dm.m3u8
[10] Codec: mp4a.40.5, Average Bandwidth: 68934, URI: P856766089_A1461478401_audio_en_gr64_mp4a-40-2.m3u8
[11] Codec: mp4a.40.5, Average Bandwidth: 69257, URI: P856766089_A1461478401_audio_en_gr64_mp4a-40-2_bm.m3u8
Enter the number of the audio stream you want to download: 5
codecSuffix: P856766089_A1461478401_audio_en_gr2116_alac
newCodecSuffix:  audio_en_gr2116_alac
Downloading...
Downloaded.
Extracting samples.
Samples extracted successfully.
Successfully downloaded and saved track as m4a.


its even downloaded as ec3 , but i need in m4a
another thing is it downloads when i select other index like 5 , it still embeds into ec3 which is not ec3 and decrypt doesnt take place and i need in m4a