package main import ( "bufio" "fmt" "os" "os/exec" "strconv" "strings" ) func readLine(reader *bufio.Reader) string { line, _ := reader.ReadString('\n') return strings.TrimSpace(line) } func promptChoice(reader *bufio.Reader, max int) int { for { s := readLine(reader) n, err := strconv.Atoi(s) if err == nil && n >= 0 && n <= max { return n } fmt.Printf(" 请输入 0-%d: ", max) } } func promptReleaseSelection(reader *bufio.Reader, max int) []int { for { s := readLine(reader) if strings.ToLower(s) == "all" { result := make([]int, max) for i := range result { result[i] = i + 1 } return result } parts := strings.Split(s, ",") var result []int valid := true for _, p := range parts { p = strings.TrimSpace(p) n, err := strconv.Atoi(p) if err != nil || n < 1 || n > max { valid = false break } result = append(result, n) } if valid && len(result) > 0 { return result } fmt.Printf(" 请输入 1-%d (逗号分隔) 或 'all': ", max) } } func promptCategory(reader *bufio.Reader) string { fmt.Println("\n分类:") for i, cat := range cfg.Categories { fmt.Printf(" [%d] %s\n", i+1, cat) } fmt.Printf("\n> 选择分类 [1]: ") s := readLine(reader) if s == "" { return cfg.Categories[0] } n, err := strconv.Atoi(s) if err != nil || n < 1 || n > len(cfg.Categories) { return cfg.Categories[0] } return cfg.Categories[n-1] } func promptArtistFolder(reader *bufio.Reader, defaultName string) string { fmt.Printf("> 艺术家文件夹名 [%s]: ", defaultName) s := readLine(reader) if s == "" { return defaultName } return s } func printHelp() { fmt.Println(`YTM - YouTube Music Downloader 从 YouTube Music 搜索并下载音乐到 Navidrome 音乐库 使用方法: 1. 输入关键词搜索艺术家或专辑 2. 选择搜索模式(艺术家/专辑) 3. 从列表中选择要下载的项目 4. 选择音乐分类和文件夹名 5. 等待下载完成 命令: help 显示此帮助信息 q 退出程序 输入格式: 数字 选择单项 (如: 1) 逗号分隔 选择多项 (如: 1,3,5) all 选择全部 回车 接受默认值 [方括号内]`) fmt.Printf("\n下载目录: %s{分类}/{艺术家}/{专辑}/\n", cfg.MusicRoot) fmt.Printf("分类: %s\n", strings.Join(cfg.Categories, " | ")) fmt.Println("单曲保存到: {艺术家}/Singles/") fmt.Println() } func fetchTrackCounts(releases []Release) { fmt.Println("获取曲目信息...") for i := range releases { tracks, err := ListPlaylistTracks(releases[i].OLAKID) if err == nil { releases[i].TrackCount = len(tracks) releases[i].Tracks = tracks } else { releases[i].TrackCount = 0 } } } func displayReleases(label string, releases []Release, showArtist bool) { fmt.Printf("\n%s:\n", label) for i, r := range releases { trackLabel := "track" if r.TrackCount != 1 { trackLabel = "tracks" } if showArtist && r.Artist != "" { fmt.Printf(" [%d] %s - %s (%d %s)\n", i+1, r.Artist, r.Title, r.TrackCount, trackLabel) } else { fmt.Printf(" [%d] %s (%d %s)\n", i+1, r.Title, r.TrackCount, trackLabel) } } } func downloadReleases(reader *bufio.Reader, releases []Release, defaultArtist string) { fmt.Printf("\n> 选择要下载的 (逗号分隔, 或 'all'): ") selections := promptReleaseSelection(reader, len(releases)) category := promptCategory(reader) artistFolder := promptArtistFolder(reader, defaultArtist) artistFolder = sanitizePath(artistFolder) basePath := cfg.MusicRoot + category + "/" + artistFolder fmt.Printf("\n开始下载到 %s/ ...\n", basePath) totalTracks := 0 for idx, sel := range selections { r := releases[sel-1] fmt.Printf(" [%d/%d] %s (%d tracks) ", idx+1, len(selections), r.Title, r.TrackCount) var dlErr error if r.TrackCount == 1 && len(r.Tracks) == 1 { singlesDir := basePath + "/Singles" os.MkdirAll(singlesDir, 0755) dlErr = DownloadSingle(r.Tracks[0].VideoID, singlesDir) } else { albumDir := basePath + "/" + sanitizePath(r.Title) os.MkdirAll(albumDir, 0755) dlErr = DownloadPlaylist(r.OLAKID, albumDir) } if dlErr != nil { fmt.Printf("x\n 错误: %v\n", dlErr) } else { fmt.Println("OK") totalTracks += r.TrackCount } } fmt.Printf("\n下载完成! 共 %d 首曲目\n\n", totalTracks) } func main() { // Set Windows console to UTF-8 exec.Command("cmd", "/C", "chcp", "65001").Run() cfg = loadConfig() initHTTPClient() printHelp() reader := bufio.NewReader(os.Stdin) for { fmt.Print("> 输入搜索关键词 (q 退出): ") query := readLine(reader) if query == "" { continue } if query == "q" || query == "Q" { fmt.Println("再见!") return } if strings.ToLower(query) == "help" { printHelp() continue } // Choose search mode fmt.Println("\n搜索模式:") fmt.Println(" [1] 按艺术家搜索") fmt.Println(" [2] 按专辑搜索") fmt.Printf("\n> 选择模式 [1]: ") modeStr := readLine(reader) mode := 1 if modeStr == "2" { mode = 2 } if mode == 2 { // Album search fmt.Printf("\n搜索专辑 \"%s\" ...\n", query) releases, err := SearchAlbums(query) if err != nil { fmt.Printf("搜索失败: %v\n\n", err) continue } if len(releases) == 0 { fmt.Println("未找到专辑\n") continue } fetchTrackCounts(releases) displayReleases("搜索结果", releases, true) // Use artist from first release as default defaultArtist := "" if len(releases) > 0 { defaultArtist = releases[0].Artist } downloadReleases(reader, releases, defaultArtist) } else { // Artist search fmt.Printf("\n搜索艺术家 \"%s\" ...\n", query) artists, err := SearchArtists(query) if err != nil { fmt.Printf("搜索失败: %v\n\n", err) continue } if len(artists) == 0 { fmt.Println("未找到艺术家\n") continue } fmt.Println("\n搜索结果:") for i, a := range artists { fmt.Printf(" [%d] %s (%s) - %d releases\n", i+1, a.Name, a.ID, a.ReleaseCount) } fmt.Println(" [0] 重新搜索") fmt.Printf("\n> 选择艺术家 [1]: ") choice := promptChoice(reader, len(artists)) if choice == 0 { continue } artist := artists[choice-1] // Browse channel to get MPRE IDs fmt.Printf("\n获取 %s 的发行列表...\n", artist.Name) _, mpreIDs, err := BrowseChannel(artist.ID) if err != nil { fmt.Printf("获取发行列表失败: %v\n\n", err) continue } releases := ResolveAllReleases(mpreIDs) if len(releases) == 0 { fmt.Println("未找到可用发行\n") continue } fetchTrackCounts(releases) displayReleases(artist.Name+" 的发行列表", releases, false) downloadReleases(reader, releases, artist.Name) } } }