diff --git a/config.yaml b/config.yaml index d5cfab7..a8a778a 100644 --- a/config.yaml +++ b/config.yaml @@ -45,3 +45,5 @@ use-songinfo-for-playlist: false dl-albumcover-for-playlist: false mv-audio-type: atmos #atmos ac3 aac mv-max: 2160 +storefront: "enter your storefront" #country-code that is shown in urls i.e (us, ca, jp etc..) + diff --git a/go.mod b/go.mod index c7565be..611204b 100644 --- a/go.mod +++ b/go.mod @@ -46,6 +46,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/klauspost/pgzip v1.2.6 // indirect @@ -54,6 +55,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mholt/acmez/v3 v3.0.0 // indirect github.com/mholt/archives v0.1.0 // indirect github.com/miekg/dns v1.1.62 // indirect @@ -95,6 +97,7 @@ require ( ) require ( + github.com/AlecAivazis/survey/v2 v2.3.7 github.com/beevik/etree v1.3.0 github.com/fatih/color v1.18.0 github.com/olekukonko/tablewriter v0.0.5 diff --git a/go.sum b/go.sum index 45eff7a..f14132c 100644 --- a/go.sum +++ b/go.sum @@ -15,10 +15,14 @@ cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= +github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Eyevinn/mp4ff v0.46.0 h1:A8oJA4A3C9fDbX38jEw/26utjNdvmRmrO37tVI5pDk0= github.com/Eyevinn/mp4ff v0.46.0/go.mod h1:hJNUUqOBryLAzUW9wpCJyw2HaI+TCd2rUPhafoS5lgg= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU= github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY= github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg= @@ -48,6 +52,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= +github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -143,12 +149,16 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= @@ -167,13 +177,18 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mholt/acmez/v3 v3.0.0 h1:r1NcjuWR0VaKP2BTjDK9LRFBw/WvURx3jlaEUl9Ht8E= github.com/mholt/acmez/v3 v3.0.0/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= github.com/mholt/archives v0.1.0 h1:FacgJyrjiuyomTuNA92X5GyRBRZjE43Y/lrzKIlF35Q= @@ -224,6 +239,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -365,6 +381,7 @@ golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -408,6 +425,7 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= diff --git a/main.go b/main.go index 4abe38a..e0cd4dc 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,7 @@ import ( "main/utils/structs" "main/utils/task" + "github.com/AlecAivazis/survey/v2" "github.com/fatih/color" "github.com/grafov/m3u8" "github.com/olekukonko/tablewriter" @@ -54,16 +55,17 @@ var ( ) func loadConfig() error { - // 读取config.yaml文件内容 data, err := os.ReadFile("config.yaml") if err != nil { return err } - // 将yaml解析到config变量中 err = yaml.Unmarshal(data, &Config) if err != nil { return err } + if Config.Storefront == "" { + Config.Storefront = "us" + } return nil } @@ -243,11 +245,7 @@ func checkArtist(artistUrl string, token string, relationship string) ([]string, } else if relationship == "music-videos" { table.SetHeader([]string{"", "MV Name", "Date", "MV ID"}) } - //table.SetFooter([]string{"", "", "Total", "$146.93"}) - //table.SetAutoMergeCells(true) - //table.SetAutoMergeCellsByColumnIndex([]int{1,2,3}) table.SetRowLine(false) - //table.AppendBulk(options) table.SetHeaderColor(tablewriter.Colors{}, tablewriter.Colors{tablewriter.FgRedColor, tablewriter.Bold}, tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor}, @@ -273,29 +271,26 @@ func checkArtist(artistUrl string, token string, relationship string) ([]string, cyanColor.Print("Enter your choice: ") input, _ := reader.ReadString('\n') - // Remove newline and whitespace input = strings.TrimSpace(input) if input == "all" { fmt.Println("You have selected all options:") return urls, nil } - // Split input into string slices selectedOptions := [][]string{} parts := strings.Split(input, ",") for _, part := range parts { - if strings.Contains(part, "-") { // Range setting + if strings.Contains(part, "-") { rangeParts := strings.Split(part, "-") selectedOptions = append(selectedOptions, rangeParts) - } else { // Single option + } else { selectedOptions = append(selectedOptions, []string{part}) } } - // Print selected options fmt.Println("You have selected the following options:") for _, opt := range selectedOptions { - if len(opt) == 1 { // Single option + if len(opt) == 1 { num, err := strconv.Atoi(opt[0]) if err != nil { fmt.Println("Invalid option:", opt[0]) @@ -307,7 +302,7 @@ func checkArtist(artistUrl string, token string, relationship string) ([]string, } else { fmt.Println("Option out of range:", opt[0]) } - } else if len(opt) == 2 { // Range + } else if len(opt) == 2 { start, err1 := strconv.Atoi(opt[0]) end, err2 := strconv.Atoi(opt[1]) if err1 != nil || err2 != nil { @@ -402,6 +397,215 @@ func contains(slice []string, item string) bool { return false } +// START: New functions for search functionality + +// SearchResultItem is a unified struct to hold search results for display. +type SearchResultItem struct { + Type string + Name string + Detail string + URL string + ID string +} + +// QualityOption holds information about a downloadable quality. +type QualityOption struct { + ID string + Description string +} + +// setDlFlags configures the global download flags based on the user's quality selection. +func setDlFlags(quality string) { + dl_atmos = false + dl_aac = false + + switch quality { + case "atmos": + dl_atmos = true + fmt.Println("Quality set to: Dolby Atmos") + case "aac": + dl_aac = true + *aac_type = "aac" + fmt.Println("Quality set to: High-Quality (AAC)") + case "alac": + fmt.Println("Quality set to: Lossless (ALAC)") + } +} + +// promptForQuality asks the user to select a download quality for the chosen media. +func promptForQuality(item SearchResultItem, token string) (string, error) { + if item.Type == "Artist" { + fmt.Println("Artist selected. Proceeding to list all albums/videos.") + return "default", nil + } + + fmt.Printf("\nFetching available qualities for: %s\n", item.Name) + + qualities := []QualityOption{ + {ID: "alac", Description: "Lossless (ALAC)"}, + {ID: "aac", Description: "High-Quality (AAC)"}, + {ID: "atmos", Description: "Dolby Atmos"}, + } + qualityOptions := []string{} + for _, q := range qualities { + qualityOptions = append(qualityOptions, q.Description) + } + + prompt := &survey.Select{ + Message: "Select a quality to download:", + Options: qualityOptions, + PageSize: 5, + } + + selectedIndex := 0 + err := survey.AskOne(prompt, &selectedIndex) + if err != nil { + // This can happen if the user presses Ctrl+C + return "", nil + } + + return qualities[selectedIndex].ID, nil +} + +// handleSearch manages the entire interactive search process. +func handleSearch(searchType string, queryParts []string, token string) (string, error) { + query := strings.Join(queryParts, " ") + validTypes := map[string]bool{"album": true, "song": true, "artist": true} + if !validTypes[searchType] { + return "", fmt.Errorf("invalid search type: %s. Use 'album', 'song', or 'artist'", searchType) + } + + fmt.Printf("Searching for %ss: \"%s\" in storefront \"%s\"\n", searchType, query, Config.Storefront) + + offset := 0 + limit := 15 // Increased limit for better navigation + + apiSearchType := searchType + "s" + + for { + searchResp, err := ampapi.Search(Config.Storefront, query, apiSearchType, Config.Language, token, limit, offset) + if err != nil { + return "", fmt.Errorf("error fetching search results: %w", err) + } + + var items []SearchResultItem + var displayOptions []string + hasNext := false + + // Special options for navigation + const prevPageOpt = "⬅️ Previous Page" + const nextPageOpt = "➡️ Next Page" + + // Add previous page option if applicable + if offset > 0 { + displayOptions = append(displayOptions, prevPageOpt) + } + + switch searchType { + case "album": + if searchResp.Results.Albums != nil { + for _, item := range searchResp.Results.Albums.Data { + year := "" + if len(item.Attributes.ReleaseDate) >= 4 { + year = item.Attributes.ReleaseDate[:4] + } + trackInfo := fmt.Sprintf("%d tracks", item.Attributes.TrackCount) + detail := fmt.Sprintf("%s (%s, %s)", item.Attributes.ArtistName, year, trackInfo) + displayOptions = append(displayOptions, fmt.Sprintf("%s - %s", item.Attributes.Name, detail)) + items = append(items, SearchResultItem{Type: "Album", URL: item.Attributes.URL, ID: item.ID}) + } + hasNext = searchResp.Results.Albums.Next != "" + } + case "song": + if searchResp.Results.Songs != nil { + for _, item := range searchResp.Results.Songs.Data { + detail := fmt.Sprintf("%s (%s)", item.Attributes.ArtistName, item.Attributes.AlbumName) + displayOptions = append(displayOptions, fmt.Sprintf("%s - %s", item.Attributes.Name, detail)) + items = append(items, SearchResultItem{Type: "Song", URL: item.Attributes.URL, ID: item.ID}) + } + hasNext = searchResp.Results.Songs.Next != "" + } + case "artist": + if searchResp.Results.Artists != nil { + for _, item := range searchResp.Results.Artists.Data { + detail := "" + if len(item.Attributes.GenreNames) > 0 { + detail = strings.Join(item.Attributes.GenreNames, ", ") + } + displayOptions = append(displayOptions, fmt.Sprintf("%s (%s)", item.Attributes.Name, detail)) + items = append(items, SearchResultItem{Type: "Artist", URL: item.Attributes.URL, ID: item.ID}) + } + hasNext = searchResp.Results.Artists.Next != "" + } + } + + if len(items) == 0 && offset == 0 { + fmt.Println("No results found.") + return "", nil + } + + // Add next page option if applicable + if hasNext { + displayOptions = append(displayOptions, nextPageOpt) + } + + prompt := &survey.Select{ + Message: "Use arrow keys to navigate, Enter to select:", + Options: displayOptions, + PageSize: limit, // Show a full page of results + } + + selectedIndex := 0 + err = survey.AskOne(prompt, &selectedIndex) + if err != nil { + // User pressed Ctrl+C + return "", nil + } + + selectedOption := displayOptions[selectedIndex] + + // Handle pagination + if selectedOption == nextPageOpt { + offset += limit + continue + } + if selectedOption == prevPageOpt { + offset -= limit + continue + } + + // Adjust index to match the `items` slice if "Previous Page" was an option + itemIndex := selectedIndex + if offset > 0 { + itemIndex-- + } + + selectedItem := items[itemIndex] + + // Automatically set single song download flag + if selectedItem.Type == "Song" { + dl_song = true + } + + quality, err := promptForQuality(selectedItem, token) + if err != nil { + return "", fmt.Errorf("could not process quality selection: %w", err) + } + if quality == "" { // User cancelled quality selection + fmt.Println("Selection cancelled.") + return "", nil + } + + if quality != "default" { + setDlFlags(quality) + } + + return selectedItem.URL, nil + } +} + +// END: New functions for search functionality + func ripTrack(track *task.Track, token string, mediaUserToken string) { var err error counter.Total++ @@ -573,7 +777,6 @@ func ripTrack(track *task.Track, token string, mediaUserToken string) { tags := []string{ "tool=", fmt.Sprintf("artist=%s", track.Resp.Attributes.ArtistName), - //fmt.Sprintf("lyrics=%s", lrc), } if Config.EmbedCover { if (strings.Contains(track.PreID, "pl.") || strings.Contains(track.PreID, "ra.")) && Config.DlAlbumcoverForPlaylist { @@ -627,7 +830,6 @@ func ripStation(albumId string, token string, storefront string, mediaUserToken Codec = "ALAC" } station.Codec = Codec - // Get Artist Folder var singerFoldername string if Config.ArtistFolderFormat != "" { singerFoldername = strings.NewReplacer( @@ -648,10 +850,9 @@ func ripStation(albumId string, token string, storefront string, mediaUserToken if dl_aac { singerFolder = filepath.Join(Config.AacSaveFolder, forbiddenNames.ReplaceAllString(singerFoldername, "_")) } - os.MkdirAll(singerFolder, os.ModePerm) // Create artist folder + os.MkdirAll(singerFolder, os.ModePerm) station.SaveDir = singerFolder - //Get Playlist Folder Name playlistFolder := strings.NewReplacer( "{ArtistName}", "Apple Music Station", "{PlaylistName}", LimitString(station.Name), @@ -668,19 +869,16 @@ func ripStation(albumId string, token string, storefront string, mediaUserToken os.MkdirAll(playlistFolderPath, os.ModePerm) station.SaveName = playlistFolder fmt.Println(playlistFolder) - //先省略封面相关的获取 - //get playlist cover + covPath, err := writeCover(playlistFolderPath, "cover", meta.Data[0].Attributes.Artwork.URL) if err != nil { fmt.Println("Failed to write cover.") } station.CoverPath = covPath - //get animated artwork if Config.SaveAnimatedArtwork && meta.Data[0].Attributes.EditorialVideo.MotionSquare.Video != "" { fmt.Println("Found Animation Artwork.") - // Download square version motionvideoUrlSquare, err := extractVideo(meta.Data[0].Attributes.EditorialVideo.MotionSquare.Video) if err != nil { fmt.Println("no motion video square.\n", err) @@ -703,14 +901,12 @@ func ripStation(albumId string, token string, storefront string, mediaUserToken } if Config.EmbyAnimatedArtwork { - // Convert square version to gif cmd3 := exec.Command("ffmpeg", "-i", filepath.Join(playlistFolderPath, "square_animated_artwork.mp4"), "-vf", "scale=440:-1", "-r", "24", "-f", "gif", filepath.Join(playlistFolderPath, "folder.jpg")) if err := cmd3.Run(); err != nil { fmt.Printf("animated artwork square to gif err: %v\n", err) } } } - // 处理stream类型的station if station.Type == "stream" { counter.Total++ if isInArray(okDict[station.ID], 1) { @@ -744,7 +940,6 @@ func ripStation(albumId string, token string, storefront string, mediaUserToken return err } trackM3U8 := strings.ReplaceAll(assetsUrl, "index.m3u8", "256/prog_index.m3u8") - //testM3U8 := "https://itsliveradio.apple.com/bb/aod/exp_aod/jpopnowradio/AkinaNakamori/cmaf/256/prog_index.m3u8" keyAndUrls, _ := runv3.Run(station.ID, trackM3U8, token, mediaUserToken, true) err = runv3.ExtMvData(keyAndUrls, trackPath) if err != nil { @@ -752,7 +947,6 @@ func ripStation(albumId string, token string, storefront string, mediaUserToken counter.Error++ return err } - //tags tags := []string{ "tool=", "disk=1/1", @@ -761,7 +955,6 @@ func ripStation(albumId string, token string, storefront string, mediaUserToken fmt.Sprintf("artist=%s", "Apple Music Station"), fmt.Sprintf("performer=%s", "Apple Music Station"), fmt.Sprintf("album_artist=%s", "Apple Music Station"), - fmt.Sprintf("artist=%s", "Apple Music Station"), fmt.Sprintf("album=%s", station.Name), fmt.Sprintf("title=%s", station.Name), } @@ -794,18 +987,10 @@ func ripStation(albumId string, token string, storefront string, mediaUserToken if true { selected = arr } - //Download tracks for i := range station.Tracks { i++ - // if isInArray(okDict[playlistId], i) { - // //fmt.Println("已完成直接跳过.\n") - // counter.Total++ - // counter.Success++ - // continue - // } if isInArray(selected, i) { ripTrack(&station.Tracks[i-1], token, mediaUserToken) - //downloadTrack(trackNum, trackTotal, meta, track, albumId, token, storefront, mediaUserToken, sanAlbumFolder, Codec, covPath) } } return nil @@ -819,9 +1004,7 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st return err } meta := album.Resp - //debug mode if debug_mode { - // Print album info fmt.Println(meta.Data[0].Attributes.ArtistName) fmt.Println(meta.Data[0].Attributes.Name) @@ -837,11 +1020,9 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st } var m3u8Url string - //Web端m3u8 if manifest.Data[0].Attributes.ExtendedAssetUrls.EnhancedHls != "" { m3u8Url = manifest.Data[0].Attributes.ExtendedAssetUrls.EnhancedHls } - //设备端满血m3u8 needCheck := false if Config.GetM3u8Mode == "all" { needCheck = true @@ -863,9 +1044,8 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st continue } } - return nil // Return directly without showing statistics + return nil } - // Get Codec var Codec string if dl_atmos { Codec = "ATMOS" @@ -875,7 +1055,6 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st Codec = "ALAC" } album.Codec = Codec - // Get Artist Folder var singerFoldername string if Config.ArtistFolderFormat != "" { if len(meta.Data[0].Relationships.Artists.Data) > 0 { @@ -904,9 +1083,8 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st if dl_aac { singerFolder = filepath.Join(Config.AacSaveFolder, forbiddenNames.ReplaceAllString(singerFoldername, "_")) } - os.MkdirAll(singerFolder, os.ModePerm) // Create artist folder + os.MkdirAll(singerFolder, os.ModePerm) album.SaveDir = singerFolder - //Get Quality var Quality string if strings.Contains(Config.AlbumFolderFormat, "Quality") { if dl_atmos { @@ -921,7 +1099,6 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st if manifest1.Data[0].Attributes.ExtendedAssetUrls.EnhancedHls == "" { Codec = "AAC" Quality = "256Kbps" - //fmt.Println("Unavailable.\n") } else { needCheck := false @@ -945,7 +1122,6 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st } } } - //Set Album Folder Tags stringsToJoin := []string{} if meta.Data[0].Attributes.IsAppleDigitalMaster || meta.Data[0].Attributes.IsMasteredForItunes { if Config.AppleMasterChoice != "" { @@ -963,7 +1139,6 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st } } Tag_string := strings.Join(stringsToJoin, " ") - //Get Album Folder Name var albumFolderName string albumFolderName = strings.NewReplacer( "{ReleaseDate}", meta.Data[0].Attributes.ReleaseDate, @@ -984,10 +1159,9 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st } albumFolderName = strings.TrimSpace(albumFolderName) albumFolderPath := filepath.Join(singerFolder, forbiddenNames.ReplaceAllString(albumFolderName, "_")) - os.MkdirAll(albumFolderPath, os.ModePerm) // Create album folder + os.MkdirAll(albumFolderPath, os.ModePerm) album.SaveName = albumFolderName fmt.Println(albumFolderName) - //get artist cover if Config.SaveArtistCover { if len(meta.Data[0].Relationships.Artists.Data) > 0 { _, err = writeCover(singerFolder, "folder", meta.Data[0].Relationships.Artists.Data[0].Attributes.Artwork.Url) @@ -996,16 +1170,13 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st } } } - //get playlist cover covPath, err := writeCover(albumFolderPath, "cover", meta.Data[0].Attributes.Artwork.URL) if err != nil { fmt.Println("Failed to write cover.") } - //get animated artwork if Config.SaveAnimatedArtwork && meta.Data[0].Attributes.EditorialVideo.MotionDetailSquare.Video != "" { fmt.Println("Found Animation Artwork.") - // Download square version motionvideoUrlSquare, err := extractVideo(meta.Data[0].Attributes.EditorialVideo.MotionDetailSquare.Video) if err != nil { fmt.Println("no motion video square.\n", err) @@ -1028,14 +1199,12 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st } if Config.EmbyAnimatedArtwork { - // Convert square version to gif cmd3 := exec.Command("ffmpeg", "-i", filepath.Join(albumFolderPath, "square_animated_artwork.mp4"), "-vf", "scale=440:-1", "-r", "24", "-f", "gif", filepath.Join(albumFolderPath, "folder.jpg")) if err := cmd3.Run(); err != nil { fmt.Printf("animated artwork square to gif err: %v\n", err) } } - // Download tall version motionvideoUrlTall, err := extractVideo(meta.Data[0].Attributes.EditorialVideo.MotionDetailTall.Video) if err != nil { fmt.Println("no motion video tall.\n", err) @@ -1057,13 +1226,11 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st } } } - //填充子track信息 for i := range album.Tracks { album.Tracks[i].CoverPath = covPath album.Tracks[i].SaveDir = albumFolderPath album.Tracks[i].Codec = Codec } - //Get selected tracks trackTotal := len(meta.Data[0].Relationships.Tracks.Data) arr := make([]int, trackTotal) for i := 0; i < trackTotal; i++ { @@ -1072,13 +1239,10 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st if dl_song { if urlArg_i == "" { - //fmt.Println("URL does not contain parameter 'i'. Please ensure the URL includes 'i' or use another mode.") - //return nil } else { for i := range album.Tracks { if urlArg_i == album.Tracks[i].ID { ripTrack(&album.Tracks[i], token, mediaUserToken) - //downloadTrack(trackNum, trackTotal, meta, track, albumId, token, storefront, mediaUserToken, albumFolderPath, Codec, covPath) return nil } } @@ -1091,18 +1255,15 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st } else { selected = album.ShowSelect() } - //Download tracks for i := range album.Tracks { i++ if isInArray(okDict[albumId], i) { - //fmt.Println("已完成直接跳过.\n") counter.Total++ counter.Success++ continue } if isInArray(selected, i) { ripTrack(&album.Tracks[i-1], token, mediaUserToken) - //downloadTrack(trackNum, trackTotal, meta, track, albumId, token, storefront, mediaUserToken, sanAlbumFolder, Codec, covPath) } } return nil @@ -1117,7 +1278,6 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo } meta := playlist.Resp if debug_mode { - // Print album info fmt.Println(meta.Data[0].Attributes.ArtistName) fmt.Println(meta.Data[0].Attributes.Name) @@ -1133,11 +1293,9 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo } var m3u8Url string - //Web端m3u8 if manifest.Data[0].Attributes.ExtendedAssetUrls.EnhancedHls != "" { m3u8Url = manifest.Data[0].Attributes.ExtendedAssetUrls.EnhancedHls } - //设备端满血m3u8 needCheck := false if Config.GetM3u8Mode == "all" { needCheck = true @@ -1159,7 +1317,7 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo continue } } - return nil // Return directly without showing statistics + return nil } var Codec string if dl_atmos { @@ -1170,7 +1328,6 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo Codec = "ALAC" } playlist.Codec = Codec - // Get Artist Folder var singerFoldername string if Config.ArtistFolderFormat != "" { singerFoldername = strings.NewReplacer( @@ -1191,9 +1348,8 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo if dl_aac { singerFolder = filepath.Join(Config.AacSaveFolder, forbiddenNames.ReplaceAllString(singerFoldername, "_")) } - os.MkdirAll(singerFolder, os.ModePerm) // Create artist folder + os.MkdirAll(singerFolder, os.ModePerm) playlist.SaveDir = singerFolder - //Get Quality var Quality string if strings.Contains(Config.AlbumFolderFormat, "Quality") { @@ -1209,7 +1365,6 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo if manifest1.Data[0].Attributes.ExtendedAssetUrls.EnhancedHls == "" { Codec = "AAC" Quality = "256Kbps" - //fmt.Println("Unavailable.\n") } else { needCheck := false @@ -1233,7 +1388,6 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo } } } - //Set Playlist Folder Tags stringsToJoin := []string{} if meta.Data[0].Attributes.IsAppleDigitalMaster || meta.Data[0].Attributes.IsMasteredForItunes { if Config.AppleMasterChoice != "" { @@ -1251,7 +1405,6 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo } } Tag_string := strings.Join(stringsToJoin, " ") - //Get Playlist Folder Name playlistFolder := strings.NewReplacer( "{ArtistName}", "Apple Music", "{PlaylistName}", LimitString(meta.Data[0].Attributes.Name), @@ -1268,8 +1421,6 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo os.MkdirAll(playlistFolderPath, os.ModePerm) playlist.SaveName = playlistFolder fmt.Println(playlistFolder) - //先省略封面相关的获取 - //get playlist cover covPath, err := writeCover(playlistFolderPath, "cover", meta.Data[0].Attributes.Artwork.URL) if err != nil { fmt.Println("Failed to write cover.") @@ -1281,11 +1432,9 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo playlist.Tracks[i].Codec = Codec } - //get animated artwork if Config.SaveAnimatedArtwork && meta.Data[0].Attributes.EditorialVideo.MotionDetailSquare.Video != "" { fmt.Println("Found Animation Artwork.") - // Download square version motionvideoUrlSquare, err := extractVideo(meta.Data[0].Attributes.EditorialVideo.MotionDetailSquare.Video) if err != nil { fmt.Println("no motion video square.\n", err) @@ -1308,14 +1457,12 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo } if Config.EmbyAnimatedArtwork { - // Convert square version to gif cmd3 := exec.Command("ffmpeg", "-i", filepath.Join(playlistFolderPath, "square_animated_artwork.mp4"), "-vf", "scale=440:-1", "-r", "24", "-f", "gif", filepath.Join(playlistFolderPath, "folder.jpg")) if err := cmd3.Run(); err != nil { fmt.Printf("animated artwork square to gif err: %v\n", err) } } - // Download tall version motionvideoUrlTall, err := extractVideo(meta.Data[0].Attributes.EditorialVideo.MotionDetailTall.Video) if err != nil { fmt.Println("no motion video tall.\n", err) @@ -1349,18 +1496,15 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo } else { selected = playlist.ShowSelect() } - //Download tracks for i := range playlist.Tracks { i++ if isInArray(okDict[playlistId], i) { - //fmt.Println("已完成直接跳过.\n") counter.Total++ counter.Success++ continue } if isInArray(selected, i) { ripTrack(&playlist.Tracks[i-1], token, mediaUserToken) - //downloadTrack(trackNum, trackTotal, meta, track, albumId, token, storefront, mediaUserToken, sanAlbumFolder, Codec, covPath) } } return nil @@ -1381,15 +1525,12 @@ func writeMP4Tags(track *task.Track, lrc string) error { }, Composer: track.Resp.Attributes.ComposerName, ComposerSort: track.Resp.Attributes.ComposerName, - //Date: meta.Data[0].Attributes.ReleaseDate, - CustomGenre: track.Resp.Attributes.GenreNames[0], - //Copyright: meta.Data[0].Attributes.Copyright, - //Publisher: meta.Data[0].Attributes.RecordLabel, - Lyrics: lrc, - TrackNumber: int16(track.Resp.Attributes.TrackNumber), - DiscNumber: int16(track.Resp.Attributes.DiscNumber), - Album: track.Resp.Attributes.AlbumName, - AlbumSort: track.Resp.Attributes.AlbumName, + CustomGenre: track.Resp.Attributes.GenreNames[0], + Lyrics: lrc, + TrackNumber: int16(track.Resp.Attributes.TrackNumber), + DiscNumber: int16(track.Resp.Attributes.DiscNumber), + Album: track.Resp.Attributes.AlbumName, + AlbumSort: track.Resp.Attributes.AlbumName, } if track.PreType == "albums" { @@ -1473,7 +1614,8 @@ func main() { return } } - // Define command-line flags + var search_type string + pflag.StringVar(&search_type, "search", "", "Search for 'album', 'song', or 'artist'. Provide query after flags.") pflag.BoolVar(&dl_atmos, "atmos", false, "Enable atmos download mode") pflag.BoolVar(&dl_aac, "aac", false, "Enable adm-aac download mode") pflag.BoolVar(&dl_select, "select", false, "Enable selective download") @@ -1486,14 +1628,13 @@ func main() { mv_audio_type = pflag.String("mv-audio-type", Config.MVAudioType, "Select MV audio type, atmos ac3 aac") mv_max = pflag.Int("mv-max", Config.MVMax, "Specify the max quality for download MV") - // Custom usage message for help pflag.Usage = func() { - fmt.Fprintf(os.Stderr, "Usage: %s [options] url1 url2 ...\n", "[main | main.exe | go run main.go]") - fmt.Println("Options:") + fmt.Fprintf(os.Stderr, "Usage: %s [options] [url1 url2 ...]\n", "[main | main.exe | go run main.go]") + fmt.Fprintf(os.Stderr, "Search Usage: %s --search [album|song|artist] [query]\n", "[main | main.exe | go run main.go]") + fmt.Println("\nOptions:") pflag.PrintDefaults() } - // Parse the flag arguments pflag.Parse() Config.AlacMax = *alac_max Config.AtmosMax = *atmos_max @@ -1502,19 +1643,38 @@ func main() { Config.MVMax = *mv_max args := pflag.Args() - if len(args) == 0 { - fmt.Println("No URLs provided. Please provide at least one URL.") - pflag.Usage() - return + + if search_type != "" { + if len(args) == 0 { + fmt.Println("Error: --search flag requires a query.") + pflag.Usage() + return + } + selectedUrl, err := handleSearch(search_type, args, token) + if err != nil { + fmt.Printf("\nSearch process failed: %v\n", err) + return + } + if selectedUrl == "" { + fmt.Println("\nExiting.") + return + } + os.Args = []string{selectedUrl} + } else { + if len(args) == 0 { + fmt.Println("No URLs provided. Please provide at least one URL.") + pflag.Usage() + return + } + os.Args = args } - os.Args = args + if strings.Contains(os.Args[0], "/artist/") { urlArtistName, urlArtistID, err := getUrlArtistName(os.Args[0], token) if err != nil { fmt.Println("Failed to get artistname.") return } - //fmt.Println("get artistname:", urlArtistName) Config.ArtistFolderFormat = strings.NewReplacer( "{UrlArtistName}", LimitString(urlArtistName), "{ArtistId}", urlArtistID, @@ -1527,7 +1687,6 @@ func main() { mvArgs, err := checkArtist(os.Args[0], token, "music-videos") if err != nil { fmt.Println("Failed to get artist music-videos.") - //return } os.Args = append(albumArgs, mvArgs...) } @@ -1537,7 +1696,6 @@ func main() { fmt.Printf("Queue %d of %d: ", albumNum+1, albumTotal) var storefront, albumId string - //mv dl dev if strings.Contains(urlRaw, "/music-video/") { fmt.Println("Music Video") if debug_mode { @@ -1576,11 +1734,14 @@ func main() { } if strings.Contains(urlRaw, "/song/") { fmt.Printf("Song->") - urlRaw, err = getUrlSong(urlRaw, token) - dl_song = true - if err != nil { - fmt.Println("Failed to get Song info.") + // When dl_song is true from search, we don't need to re-fetch the album URL + if !dl_song { + urlRaw, err = getUrlSong(urlRaw, token) + if err != nil { + fmt.Println("Failed to get Song info.") + } } + dl_song = true } parse, err := url.Parse(urlRaw) if err != nil { @@ -1663,16 +1824,13 @@ func mvDownloader(adamID string, saveDir string, token string, storefront string } os.MkdirAll(saveDir, os.ModePerm) - //video videom3u8url, _ := extractVideo(mvm3u8url) videokeyAndUrls, _ := runv3.Run(adamID, videom3u8url, token, mediaUserToken, true) _ = runv3.ExtMvData(videokeyAndUrls, vidPath) - //audio audiom3u8url, _ := extractMvAudio(mvm3u8url) audiokeyAndUrls, _ := runv3.Run(adamID, audiom3u8url, token, mediaUserToken, true) _ = runv3.ExtMvData(audiokeyAndUrls, audPath) - //tags tags := []string{ "tool=", fmt.Sprintf("artist=%s", MVInfo.Data[0].Attributes.ArtistName), @@ -1682,7 +1840,6 @@ func mvDownloader(adamID string, saveDir string, token string, storefront string fmt.Sprintf("ISRC=%s", MVInfo.Data[0].Attributes.Isrc), } - // ContentRating tag if MVInfo.Data[0].Attributes.ContentRating == "explicit" { tags = append(tags, "rating=1") } else if MVInfo.Data[0].Attributes.ContentRating == "clean" { @@ -1691,7 +1848,6 @@ func mvDownloader(adamID string, saveDir string, token string, storefront string tags = append(tags, "rating=0") } - //根据情况额外添加可使用的tags if track != nil { if track.PreType == "playlists" && !Config.UseSongInfoForPlaylist { tags = append(tags, "disk=1/1") @@ -1700,8 +1856,6 @@ func mvDownloader(adamID string, saveDir string, token string, storefront string tags = append(tags, fmt.Sprintf("tracknum=%d/%d", track.TaskNum, track.TaskTotal)) tags = append(tags, fmt.Sprintf("album_artist=%s", track.PlaylistData.Attributes.ArtistName)) tags = append(tags, fmt.Sprintf("performer=%s", track.Resp.Attributes.ArtistName)) - //tags = append(tags, fmt.Sprintf("copyright=%s", track.PlaylistData.Attributes.Copyright)) - //tags = append(tags, fmt.Sprintf("UPC=%s", track.PlaylistData.Attributes.Upc)) } else if track.PreType == "playlists" && Config.UseSongInfoForPlaylist { tags = append(tags, fmt.Sprintf("album=%s", track.AlbumData.Attributes.Name)) tags = append(tags, fmt.Sprintf("disk=%d/%d", track.Resp.Attributes.DiscNumber, track.DiscTotal)) @@ -1726,31 +1880,21 @@ func mvDownloader(adamID string, saveDir string, token string, storefront string tags = append(tags, fmt.Sprintf("disk=%d", MVInfo.Data[0].Attributes.DiscNumber)) tags = append(tags, fmt.Sprintf("track=%d", MVInfo.Data[0].Attributes.TrackNumber)) tags = append(tags, fmt.Sprintf("tracknum=%d", MVInfo.Data[0].Attributes.TrackNumber)) - //tags = append(tags, fmt.Sprintf("album_artist=%s", MVInfo.Data[0].Attributes.ArtistName)) tags = append(tags, fmt.Sprintf("performer=%s", MVInfo.Data[0].Attributes.ArtistName)) } - // Extract and save thumbnail if enabled var covPath string - //强制嵌入封面 if true { - // Get the highest quality thumbnail URL from the MV info thumbURL := MVInfo.Data[0].Attributes.Artwork.URL - - // Generate base name without extension baseThumbName := forbiddenNames.ReplaceAllString(mvSaveName, "_") + "_thumbnail" - - // Download and save thumbnail covPath, err = writeCover(saveDir, baseThumbName, thumbURL) if err != nil { fmt.Println("Failed to save MV thumbnail:", err) } else { - //fmt.Println("MV thumbnail saved successfully") tags = append(tags, fmt.Sprintf("cover=%s", covPath)) } } - //mux and add tag tagsString := strings.Join(tags, ":") muxCmd := exec.Command("MP4Box", "-itags", tagsString, "-quiet", "-add", vidPath, "-add", audPath, "-keep-utc", "-new", mvOutPath) fmt.Printf("MV Remuxing...") @@ -1858,11 +2002,9 @@ func checkM3u8(b string, f string) (string, error) { fmt.Println("Connected to device") } - // Send the length of adamID and the adamID itself adamIDBuffer := []byte(adamID) lengthBuffer := []byte{byte(len(adamIDBuffer))} - // Write length and adamID to the connection _, err = conn.Write(lengthBuffer) if err != nil { fmt.Println("Error writing length to device:", err) @@ -1875,15 +2017,12 @@ func checkM3u8(b string, f string) (string, error) { return "none", err } - // Read the response (URL) from the device response, err := bufio.NewReader(conn).ReadBytes('\n') if err != nil { fmt.Println("Error reading response from device:", err) return "none", err } - // Trim any newline characters from the response - response = bytes.TrimSpace(response) if len(response) > 0 { if f == "song" { @@ -1936,14 +2075,10 @@ func extractMedia(b string, more_mode bool) (string, string, error) { var data [][]string for _, variant := range master.Variants { data = append(data, []string{variant.Codecs, variant.Audio, fmt.Sprint(variant.Bandwidth)}) - //fmt.Printf("Codec: %s, Audio: %s, Bandwidth: %d\n", - //variant.Codecs, variant.Audio, variant.Bandwidth) } table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"Codec", "Audio", "Bandwidth"}) - //table.SetFooter([]string{"", "", "Total", "$146.93"}) table.SetAutoMergeCells(true) - //table.SetAutoMergeCellsByColumnIndex([]int{1,2,3}) table.SetRowLine(true) table.AppendBulk(data) table.Render() @@ -1951,7 +2086,6 @@ func extractMedia(b string, more_mode bool) (string, string, error) { var hasAAC, hasLossless, hasHiRes, hasAtmos, hasDolbyAudio bool var aacQuality, losslessQuality, hiResQuality, atmosQuality, dolbyAudioQuality string - // Check for all formats for _, variant := range master.Variants { if variant.Codecs == "mp4a.40.2" { // AAC hasAAC = true @@ -1973,7 +2107,6 @@ func extractMedia(b string, more_mode bool) (string, string, error) { split := strings.Split(variant.Audio, "-") if len(split) > 0 { bitrateStr := split[len(split)-1] - // Remove leading "2" if present in "2768" if len(bitrateStr) == 4 && bitrateStr[0] == '2' { bitrateStr = bitrateStr[1:] } @@ -2175,4 +2308,4 @@ func extractVideo(c string) (string, error) { } return streamUrl.String(), nil -} +} \ No newline at end of file diff --git a/utils/ampapi/search.go b/utils/ampapi/search.go new file mode 100644 index 0000000..9e216d5 --- /dev/null +++ b/utils/ampapi/search.go @@ -0,0 +1,95 @@ +package ampapi + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" +) + +// SearchResp represents the top-level response from the search API. +type SearchResp struct { + Results SearchResults `json:"results"` +} + +// SearchResults contains the different types of search results. +type SearchResults struct { + Songs *SongResults `json:"songs,omitempty"` + Albums *AlbumResults `json:"albums,omitempty"` + Artists *ArtistResults `json:"artists,omitempty"` +} + +// SongResults contains a list of song search results. +type SongResults struct { + Href string `json:"href"` + Next string `json:"next"` + Data []SongRespData `json:"data"` +} + +// AlbumResults contains a list of album search results. +type AlbumResults struct { + Href string `json:"href"` + Next string `json:"next"` + Data []AlbumRespData `json:"data"` +} + +// ArtistResults contains a list of artist search results. +type ArtistResults struct { + Href string `json:"href"` + Next string `json:"next"` + Data []struct { + ID string `json:"id"` + Type string `json:"type"` + Href string `json:"href"` + Attributes struct { + Name string `json:"name"` + GenreNames []string `json:"genreNames"` + URL string `json:"url"` + } `json:"attributes"` + } `json:"data"` +} + +// Search performs a search query against the Apple Music API. +func Search(storefront, term, types, language, token string, limit, offset int) (*SearchResp, error) { + var err error + if token == "" { + token, err = GetToken() + if err != nil { + return nil, err + } + } + + req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/search", storefront), nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") + req.Header.Set("Origin", "https://music.apple.com") + + query := url.Values{} + query.Set("term", term) + query.Set("types", types) + query.Set("limit", fmt.Sprintf("%d", limit)) + query.Set("offset", fmt.Sprintf("%d", offset)) + query.Set("l", language) + req.URL.RawQuery = query.Encode() + + do, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer do.Body.Close() + + if do.StatusCode != http.StatusOK { + return nil, fmt.Errorf("API request failed with status: %s", do.Status) + } + + obj := new(SearchResp) + err = json.NewDecoder(do.Body).Decode(&obj) + if err != nil { + return nil, err + } + + return obj, nil +} diff --git a/utils/structs/structs.go b/utils/structs/structs.go index 237ef61..f882428 100644 --- a/utils/structs/structs.go +++ b/utils/structs/structs.go @@ -1,6 +1,7 @@ package structs type ConfigSet struct { + Storefront string `yaml:"storefront"` MediaUserToken string `yaml:"media-user-token"` AuthorizationToken string `yaml:"authorization-token"` Language string `yaml:"language"` @@ -92,4 +93,4 @@ type AutoGeneratedArtist struct { ComposerName string `json:"composerName"` } `json:"attributes"` } `json:"data"` -} +} \ No newline at end of file