<|start_filename|>ui/menu_artist_detail.go<|end_filename|>
package ui

import "fmt"

type ArtistDetailMenu struct {
    menus    []MenuItem
    artistId int64
}

func NewArtistDetailMenu(artistId int64) *ArtistDetailMenu {
    artistMenu := new(ArtistDetailMenu)
    artistMenu.menus = []MenuItem{
        {Title: "热门歌曲"},
        {Title: "热门专辑"},
    }
    artistMenu.artistId = artistId

    return artistMenu
}

func (m *ArtistDetailMenu) MenuData() interface{} {
    return nil
}

func (m *ArtistDetailMenu) BeforeBackMenuHook() Hook {
    return nil
}

func (m *ArtistDetailMenu) IsPlayable() bool {
    return false
}

func (m *ArtistDetailMenu) ResetPlaylistWhenPlay() bool {
    return false
}

func (m *ArtistDetailMenu) GetMenuKey() string {
    return fmt.Sprintf("artist_detail_%d", m.artistId)
}

func (m *ArtistDetailMenu) MenuViews() []MenuItem {
    return m.menus
}

func (m *ArtistDetailMenu) SubMenu(_ *NeteaseModel, index int) IMenu {
    switch index {
    case 0:
        return NewArtistSongMenu(m.artistId)
    case 1:
        return NewArtistAlbumMenu(m.artistId)
    }

    return nil
}

func (m *ArtistDetailMenu) BeforePrePageHook() Hook {
    // Nothing to do
    return nil
}

func (m *ArtistDetailMenu) BeforeNextPageHook() Hook {
    // Nothing to do
    return nil
}

func (m *ArtistDetailMenu) BeforeEnterMenuHook() Hook {
    // Nothing to do
    return nil
}

func (m *ArtistDetailMenu) BottomOutHook() Hook {
    // Nothing to do
    return nil
}

func (m *ArtistDetailMenu) TopOutHook() Hook {
    // Nothing to do
    return nil
}


<|start_filename|>ui/menu_help.go<|end_filename|>
package ui

import (
    "go-musicfox/constants"
    "go-musicfox/utils"
)

type HelpMenu struct {
    menus []MenuItem
}

func NewHelpMenu() *HelpMenu {
    menu := new(HelpMenu)
    menu.menus = []MenuItem{
        {Title: "Enter进来给个star吧, 谢谢~v~"},
        {Title: "r/R", Subtitle: "重新渲染UI"},
        {Title: "h/H/LEFT", Subtitle: "左"},
        {Title: "l/L/RIGHT", Subtitle: "右"},
        {Title: "j/J/DOWN", Subtitle: "下"},
        {Title: "q/Q", Subtitle: "退出"},
        {Title: "SPACE", Subtitle: "播放/暂停"},
        {Title: "[", Subtitle: "上一首"},
        {Title: "]", Subtitle: "下一首"},
        {Title: "-", Subtitle: "减小音量"},
        {Title: "=", Subtitle: "加大音量"},
        {Title: "n/N/ENTER", Subtitle: "进入"},
        {Title: "b/B/ESC", Subtitle: "返回"},
        {Title: "w/W", Subtitle: "注销并退出"},
        {Title: "p", Subtitle: "切换播放模式"},
        {Title: "P", Subtitle: "心动模式"},
        {Title: ",", Subtitle: "喜欢播放中歌曲"},
        {Title: "<", Subtitle: "喜欢选中歌曲"},
        {Title: ".", Subtitle: "取消喜欢播放中歌曲"},
        {Title: ">", Subtitle: "取消喜欢选中歌曲"},
        {Title: "/", Subtitle: "标记播放中歌曲为不喜欢"},
        {Title: "?", Subtitle: "标记选中歌曲为不喜欢"},
    }

    return menu
}

func (m *HelpMenu) MenuData() interface{} {
    return nil
}

func (m *HelpMenu) IsPlayable() bool {
    return false
}

func (m *HelpMenu) ResetPlaylistWhenPlay() bool {
    return false
}

func (m *HelpMenu) GetMenuKey() string {
    return "help_menu"
}

func (m *HelpMenu) MenuViews() []MenuItem {
    return m.menus
}

func (m *HelpMenu) SubMenu(_ *NeteaseModel, index int) IMenu {
    if index == 0 {
        _ = utils.OpenUrl(constants.AppGithubUrl)
    }
    return nil
}

func (m *HelpMenu) BeforePrePageHook() Hook {
    // Nothing to do
    return nil
}

func (m *HelpMenu) BeforeNextPageHook() Hook {
    // Nothing to do
    return nil
}

func (m *HelpMenu) BeforeEnterMenuHook() Hook {
    // Nothing to do
    return nil
}

func (m *HelpMenu) BeforeBackMenuHook() Hook {
    // Nothing to do
    return nil
}

func (m *HelpMenu) BottomOutHook() Hook {
    // Nothing to do
    return nil
}

func (m *HelpMenu) TopOutHook() Hook {
    // Nothing to do
    return nil
}


<|start_filename|>utils/notifier.go<|end_filename|>
package utils

import (
    "github.com/anhoder/notificator"
    "go-musicfox/config"
    "io/ioutil"
    "os"
)

func Notify(title, text, url string) {
    if !config.ConfigRegistry.MainShowNotify {
        return
    }

    notify := notificator.New(notificator.Options{
        AppName: "musicfox",
    })

    localDir := GetLocalDataDir()
    iconPath := localDir + "/logo.png"
    if _, err := os.Stat(iconPath); os.IsNotExist(err) {

        // 写入logo文件
        logoContent, _ := static.ReadFile("static/logo.png")
        _ = ioutil.WriteFile(iconPath, logoContent, 0644)

    }

    _ = notify.Push(notificator.UrNormal, title, text, iconPath, url)
}


<|start_filename|>ui/menu_dj_top.go<|end_filename|>
package ui

import (
    "github.com/anhoder/netease-music/service"
    "go-musicfox/ds"
    "go-musicfox/utils"
)

type DjHotType string

const (
    DjHot    DjHotType = "hot"
    DjNotHot           = "not_hot"
)

type DjHotMenu struct {
    menus   []MenuItem
    radios  []ds.DjRadio
    hotType DjHotType
}

func NewDjHotMenu(hotType DjHotType) *DjHotMenu {
    return &DjHotMenu{
        hotType: hotType,
    }
}

func (m *DjHotMenu) MenuData() interface{} {
    return nil
}

func (m *DjHotMenu) BeforeBackMenuHook() Hook {
    return nil
}

func (m *DjHotMenu) IsPlayable() bool {
    return false
}

func (m *DjHotMenu) ResetPlaylistWhenPlay() bool {
    return false
}

func (m *DjHotMenu) GetMenuKey() string {
    return "dj_hot"
}

func (m *DjHotMenu) MenuViews() []MenuItem {
    return m.menus
}

func (m *DjHotMenu) SubMenu(_ *NeteaseModel, index int) IMenu {
    if index >= len(m.radios) {
        return nil
    }

    return NewDjRadioDetailMenu(m.radios[index].Id)
}

func (m *DjHotMenu) BeforePrePageHook() Hook {
    // Nothing to do
    return nil
}

func (m *DjHotMenu) BeforeNextPageHook() Hook {
    // Nothing to do
    return nil
}

func (m *DjHotMenu) BeforeEnterMenuHook() Hook {
    return func(model *NeteaseModel) bool {

        // 不重复请求
        if len(m.menus) > 0 && len(m.radios) > 0 {
            return true
        }

        djTopService := service.DjToplistService{
            Type:   string(m.hotType),
        }
        code, response := djTopService.DjToplist()
        codeType := utils.CheckCode(code)
        if codeType != utils.Success {
            return false
        }

        m.radios = utils.GetDjRadiosOfTopDj(response)
        m.menus = GetViewFromDjRadios(m.radios)

        return true
    }
}

func (m *DjHotMenu) BottomOutHook() Hook {
    return nil
}

func (m *DjHotMenu) TopOutHook() Hook {
    // Nothing to do
    return nil
}


<|start_filename|>db/model.go<|end_filename|>
package db

type Model interface {
	GetDbName()    string
	GetTableName() string
}

// IDSetter set id
type IDSetter interface {
	SetID(ID uint64)
}

<|start_filename|>db/play_mode.go<|end_filename|>
package db

import "go-musicfox/constants"

type PlayMode struct {}

func (p PlayMode) GetDbName() string {
	return constants.AppDBName
}

func (p PlayMode) GetTableName() string {
	return "default_bucket"
}

func (p PlayMode) GetKey() string {
	return "play_mode"
}

<|start_filename|>ui/menu_album_new_area.go<|end_filename|>
package ui

type AlbumNewAreaMenu struct {
    menus    []MenuItem
}

func NewAlbumNewAreaMenu() *AlbumNewAreaMenu {
    areaMenu := new(AlbumNewAreaMenu)
    areaMenu.menus = []MenuItem{
        {Title: "全部"},
        {Title: "华语"},
        {Title: "欧美"},
        {Title: "韩国"},
        {Title: "日本"},
    }

    return areaMenu
}

func (m *AlbumNewAreaMenu) MenuData() interface{} {
    return nil
}

func (m *AlbumNewAreaMenu) BeforeBackMenuHook() Hook {
    return nil
}

func (m *AlbumNewAreaMenu) IsPlayable() bool {
    return false
}

func (m *AlbumNewAreaMenu) ResetPlaylistWhenPlay() bool {
    return false
}

func (m *AlbumNewAreaMenu) GetMenuKey() string {
    return "album_new_area"
}

func (m *AlbumNewAreaMenu) MenuViews() []MenuItem {
    return m.menus
}

func (m *AlbumNewAreaMenu) SubMenu(_ *NeteaseModel, index int) IMenu {
    areaValueMapping := []string{
        "ALL",
        "ZH",
        "EA",
        "KR",
        "JP",
    }

    return NewAlbumNewMenu(areaValueMapping[index])
}

func (m *AlbumNewAreaMenu) BeforePrePageHook() Hook {
    // Nothing to do
    return nil
}

func (m *AlbumNewAreaMenu) BeforeNextPageHook() Hook {
    // Nothing to do
    return nil
}

func (m *AlbumNewAreaMenu) BeforeEnterMenuHook() Hook {
    // Nothing to do
    return nil
}

func (m *AlbumNewAreaMenu) BottomOutHook() Hook {
    // Nothing to do
    return nil
}

func (m *AlbumNewAreaMenu) TopOutHook() Hook {
    // Nothing to do
    return nil
}



<|start_filename|>ui/menu_playlist_detail.go<|end_filename|>
package ui

import (
	"fmt"
	"github.com/anhoder/netease-music/service"
	"go-musicfox/ds"
	"go-musicfox/utils"
	"strconv"
)

type PlaylistDetailMenu struct {
	menus 	   []MenuItem
	songs      []ds.Song
	PlaylistId int64
}

func NewPlaylistDetailMenu(playlistId int64) *PlaylistDetailMenu {
	return &PlaylistDetailMenu{
		PlaylistId: playlistId,
	}
}

func (m *PlaylistDetailMenu) MenuData() interface{} {
	return m.songs
}

func (m *PlaylistDetailMenu) BeforeBackMenuHook() Hook {
	return nil
}

func (m *PlaylistDetailMenu) IsPlayable() bool {
	return true
}

func (m *PlaylistDetailMenu) ResetPlaylistWhenPlay() bool {
	return false
}

func (m *PlaylistDetailMenu) GetMenuKey() string {
	return fmt.Sprintf("playlist_detail_%d", m.PlaylistId)
}

func (m *PlaylistDetailMenu) MenuViews() []MenuItem {
	return m.menus
}

func (m *PlaylistDetailMenu) SubMenu(_ *NeteaseModel, _ int) IMenu {
	return nil
}

func (m *PlaylistDetailMenu) BeforePrePageHook() Hook {
	// Nothing to do
	return nil
}

func (m *PlaylistDetailMenu) BeforeNextPageHook() Hook {
	// Nothing to do
	return nil
}

func (m *PlaylistDetailMenu) BeforeEnterMenuHook() Hook {
	return func(model *NeteaseModel) bool {

		playlistDetail := service.PlaylistDetailService{Id: strconv.FormatInt(m.PlaylistId, 10), S: "0"}	// 最近S个收藏者，设为0
		code, response := playlistDetail.PlaylistDetail()
		codeType := utils.CheckCode(code)
		if codeType == utils.NeedLogin {
			NeedLoginHandle(model, enterMenu)
			return false
		} else if codeType != utils.Success {
			return false
		}
		m.songs = utils.GetSongsOfPlaylist(response)
		m.menus = GetViewFromSongs(m.songs)

		return true
	}
}

func (m *PlaylistDetailMenu) BottomOutHook() Hook {
	// Nothing to do
	return nil
}

func (m *PlaylistDetailMenu) TopOutHook() Hook {
	// Nothing to do
	return nil
}



<|start_filename|>db/player_snapshot.go<|end_filename|>
package db

import (
    "go-musicfox/constants"
    "go-musicfox/ds"
)

type PlayerSnapshot struct {
    CurSongIndex   int       `json:"cur_song_index"`
    Playlist       []ds.Song `json:"playlist"`
    //PlayingMenuKey string    `json:"playing_menu_key"`
}

func (p PlayerSnapshot) GetDbName() string {
    return constants.AppDBName
}

func (p PlayerSnapshot) GetTableName() string {
    return "default_bucket"
}

func (p PlayerSnapshot) GetKey() string {
    return "playlist_snapshot"
}


<|start_filename|>utils/utils.go<|end_filename|>
package utils

import (
    "bytes"
    "embed"
    "encoding/binary"
    "errors"
    "fmt"
    "github.com/buger/jsonparser"
    "go-musicfox/config"
    "go-musicfox/constants"
    "go-musicfox/ds"
    "io/ioutil"
    "net/http"
    "os"
    "os/exec"
    "os/user"
    "runtime"
    "strings"
)

//go:embed static
var static embed.FS

// GetLocalDataDir 获取本地数据存储目录
func GetLocalDataDir() string {
    // Home目录
    homeDir, err := Home()
    if nil != err {
        panic("未获取到用户Home目录: " + err.Error())
    }

    projectDir := fmt.Sprintf("%s/%s", homeDir, constants.AppLocalDataDir)

    if _, err := os.Stat(projectDir); os.IsNotExist(err) {
        _ = os.Mkdir(projectDir, os.ModePerm)
    }

    return projectDir
}

// Home 获取当前用户的Home目录
func Home() (string, error) {
    curUser, err := user.Current()
    if nil == err {
        return curUser.HomeDir, nil
    }

    // cross compile support
    if "windows" == runtime.GOOS {
        return homeWindows()
    }

    // Unix-like system, so just assume Unix
    return homeUnix()
}

func homeUnix() (string, error) {
    // First prefer the HOME environmental variable
    if home := os.Getenv("HOME"); home != "" {
        return home, nil
    }

    // If that fails, try the shell
    var stdout bytes.Buffer
    cmd := exec.Command("sh", "-c", "eval echo ~$USER")
    cmd.Stdout = &stdout
    if err := cmd.Run(); err != nil {
        return "", err
    }

    result := strings.TrimSpace(stdout.String())
    if result == "" {
        return "", errors.New("blank output when reading home directory")
    }

    return result, nil
}

func homeWindows() (string, error) {
    drive := os.Getenv("HOMEDRIVE")
    path := os.Getenv("HOMEPATH")
    home := drive + path
    if drive == "" || path == "" {
        home = os.Getenv("USERPROFILE")
    }
    if home == "" {
        return "", errors.New("HOMEDRIVE, HOMEPATH, and USERPROFILE are blank")
    }

    return home, nil
}

// IDToBin convert autoincrement ID to []byte
func IDToBin(ID uint64) []byte {
    b := make([]byte, 8)
    binary.BigEndian.PutUint64(b, ID)
    return b
}

// BinToID convert []byte to autoincrement ID
func BinToID(bin []byte) uint64 {
    ID := binary.BigEndian.Uint64(bin)

    return ID
}

type ResCode uint8
const (
    Success ResCode = iota
    UnknownError
    NetworkError
    NeedLogin
    PasswordError
)

// CheckCode 验证响应码
func CheckCode(code float64) ResCode {
    switch code {
    case 301, 302:
        return NeedLogin
    case 520:
        return NetworkError
    case 200:
        return Success
    }

    return PasswordError
}

// CheckUserInfo 验证用户信息
func CheckUserInfo(user *ds.User) ResCode {
    if user == nil || user.UserId == 0 {
        return NeedLogin
    }

    return Success
}

// ReplaceSpecialStr 替换特殊字符
func ReplaceSpecialStr(str string) string {
    replaceStr := map[string]string{
        "“": "\"",
        "”": "\"",
        "·": ".",
    }
    for oldStr, newStr := range replaceStr {
        str = strings.ReplaceAll(str, oldStr, newStr)
    }

    return str
}

// GetDailySongs 获取每日歌曲列表
func GetDailySongs(data []byte) (list []ds.Song) {
    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
       if song, err := ds.NewSongFromDailySongsJson(value); err == nil {
           list = append(list, song)
       }

    }, "data", "dailySongs")

    return
}

// GetDailyPlaylists 获取播放列表
func GetDailyPlaylists(data []byte) (list []ds.Playlist) {

    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        if playlist, err := ds.NewPlaylistFromJson(value); err == nil {
            list = append(list, playlist)
        }
    }, "recommend")

    return
}

// GetSongsOfPlaylist 获取播放列表的歌曲
func GetSongsOfPlaylist(data []byte) (list []ds.Song) {
    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        if song, err := ds.NewSongFromPlaylistSongsJson(value); err == nil {
            list = append(list, song)
        }

    }, "playlist", "tracks")

    return
}

// GetSongsOfAlbum 获取专辑的歌曲
func GetSongsOfAlbum(data []byte) (list []ds.Song) {
    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        if song, err := ds.NewSongFromAlbumSongsJson(value); err == nil {
            list = append(list, song)
        }

    }, "songs")

    return
}

// GetPlaylists 获取播放列表
func GetPlaylists(data []byte) (list []ds.Playlist) {

    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        if playlist, err := ds.NewPlaylistFromJson(value); err == nil {
            list = append(list, playlist)
        }
    }, "playlist")

    return
}

// GetPlaylistsFromHighQuality 获取精品歌单
func GetPlaylistsFromHighQuality(data []byte) (list []ds.Playlist) {

    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        if playlist, err := ds.NewPlaylistFromJson(value); err == nil {
            list = append(list, playlist)
        }
    }, "playlists")

    return
}

// GetFmSongs 获取每日歌曲列表
func GetFmSongs(data []byte) (list []ds.Song) {
    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        if song, err := ds.NewSongFromFmJson(value); err == nil {
            list = append(list, song)
        }

    }, "data")

    return
}

// GetIntelligenceSongs 获取心动模式歌曲列表
func GetIntelligenceSongs(data []byte) (list []ds.Song) {
    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        if song, err := ds.NewSongFromIntelligenceJson(value); err == nil {
            list = append(list, song)
        }

    }, "data")

    return
}

// GetNewAlbums 获取最新专辑列表
func GetNewAlbums(data []byte) (albums []ds.Album) {
    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {

        if album, err := ds.NewAlbumFromAlbumJson(value); err == nil {
            albums = append(albums, album)
        }

    }, "albums")

    return
}

// GetTopAlbums 获取专辑列表
func GetTopAlbums(data []byte) (albums []ds.Album) {
    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {

        if album, err := ds.NewAlbumFromAlbumJson(value); err == nil {
            albums = append(albums, album)
        }

    }, "monthData")

    return
}

// GetArtistHotAlbums 获取歌手热门专辑列表
func GetArtistHotAlbums(data []byte) (albums []ds.Album) {
    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {

        if album, err := ds.NewAlbumFromAlbumJson(value); err == nil {
            albums = append(albums, album)
        }

    }, "hotAlbums")

    return
}

// GetSongsOfSearchResult 获取搜索结果的歌曲
func GetSongsOfSearchResult(data []byte) (list []ds.Song) {
    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        if song, err := ds.NewSongFromSearchResultJson(value); err == nil {
            list = append(list, song)
        }

    }, "result", "songs")

    return
}


// GetAlbumsOfSearchResult 获取搜索结果的专辑
func GetAlbumsOfSearchResult(data []byte) (list []ds.Album) {
    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        if album, err := ds.NewAlbumFromAlbumJson(value); err == nil {
            list = append(list, album)
        }

    }, "result", "albums")

    return
}

// GetPlaylistsOfSearchResult 获取搜索结果的歌单
func GetPlaylistsOfSearchResult(data []byte) (list []ds.Playlist) {
    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        if playlist, err := ds.NewPlaylistFromJson(value); err == nil {
            list = append(list, playlist)
        }

    }, "result", "playlists")

    return
}

// GetArtistsOfSearchResult 获取搜索结果的歌手
func GetArtistsOfSearchResult(data []byte) (list []ds.Artist) {
    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        if artist, err := ds.NewArtist(value); err == nil {
            list = append(list, artist)
        }

    }, "result", "artists")

    return
}

// GetArtistsOfTopArtists 获取热门歌手
func GetArtistsOfTopArtists(data []byte) (list []ds.Artist) {
    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        if artist, err := ds.NewArtist(value); err == nil {
            list = append(list, artist)
        }

    }, "artists")

    return
}

// GetSongsOfArtist 获取歌手的歌曲
func GetSongsOfArtist(data []byte) (list []ds.Song) {
    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        if song, err := ds.NewSongFromArtistSongsJson(value); err == nil {
            list = append(list, song)
        }

    }, "songs")

    return
}

// GetUsersOfSearchResult 从搜索结果中获取用户列表
func GetUsersOfSearchResult(data []byte) (list []ds.User) {
    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        if song, err := ds.NewUserFromSearchResultJson(value); err == nil {
            list = append(list, song)
        }

    }, "result", "userprofiles")

    return
}

// GetDjRadiosOfSearchResult 从搜索结果中获取电台列表
func GetDjRadiosOfSearchResult(data []byte) (list []ds.DjRadio) {
    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        if radio, err := ds.NewDjRadioFromJson(value); err == nil {
            list = append(list, radio)
        }

    }, "result", "djRadios")

    return
}

// GetDjRadios 获取电台列表
func GetDjRadios(data []byte) (list []ds.DjRadio) {
    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        if radio, err := ds.NewDjRadioFromJson(value); err == nil {
            list = append(list, radio)
        }

    }, "djRadios")

    return
}

// GetDjRadiosOfToday 获取今日优选电台列表
func GetDjRadiosOfToday(data []byte) (list []ds.DjRadio) {
    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        if radio, err := ds.NewDjRadioFromJson(value); err == nil {
            list = append(list, radio)
        }

    }, "data")

    return
}

// GetDjRadiosOfTopDj 获取热门电台列表
func GetDjRadiosOfTopDj(data []byte) (list []ds.DjRadio) {
    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        if radio, err := ds.NewDjRadioFromJson(value); err == nil {
            list = append(list, radio)
        }

    }, "toplist")

    return
}

// GetSongsOfDjRadio 获取电台节目列表的歌曲
func GetSongsOfDjRadio(data []byte) (list []ds.Song) {
    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        if song, err := ds.NewSongFromDjRadioProgramJson(value); err == nil {
           list = append(list, song)
        }
    }, "programs")

    return
}

// GetSongsOfDjRank 获取电台节目排行榜列表的歌曲
func GetSongsOfDjRank(data []byte) (list []ds.Song) {
    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        if song, err := ds.NewSongFromDjRankProgramJson(value); err == nil {
            list = append(list, song)
        }
    }, "toplist")

    return
}

// GetSongsOfDjHoursRank 获取电台节目24小时排行榜列表的歌曲
func GetSongsOfDjHoursRank(data []byte) (list []ds.Song) {
    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        if song, err := ds.NewSongFromDjRankProgramJson(value); err == nil {
            list = append(list, song)
        }
    }, "data", "list")

    return
}

// GetRanks 获取排行榜
func GetRanks(data []byte) (list []ds.Rank) {

    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        if rank, err := ds.NewRankFromJson(value); err == nil {
            list = append(list, rank)
        }
    }, "list")

    return
}

// GetSongsOfCloud 获取云盘的歌曲
func GetSongsOfCloud(data []byte) (list []ds.Song) {
    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        if song, err := ds.NewSongFromCloudJson(value); err == nil {
            list = append(list, song)
        }
    }, "data")

    return
}

// GetDjCategory 获取电台分类
func GetDjCategory(data []byte) (list []ds.DjCategory) {
    _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        if cate, err := ds.NewDjCategoryFromJson(value); err == nil {
            list = append(list, cate)
        }

    }, "categories")

    return
}

// OpenUrl 打开链接
func OpenUrl(url string) error {
    commands := map[string]string{
        "windows": "start",
        "darwin":  "open",
        "linux":   "xdg-open",
    }

    run, ok := commands[runtime.GOOS]
    if !ok {
        return errors.New(fmt.Sprintf("don't know how to open things on %s platform", runtime.GOOS))
    }

    var cmd *exec.Cmd
    if runtime.GOOS == "windows" {
        cmd = exec.Command("cmd", "/c", run, url)
    } else {
        cmd = exec.Command(run, url)
    }
    return cmd.Start()
}

// LoadIniConfig 加载ini配置信息
func LoadIniConfig() *config.Registry {
    projectDir := GetLocalDataDir()
    return config.NewRegistryFromIniFile(fmt.Sprintf("%s/%s", projectDir, constants.AppIniFile))
}

func CheckUpdate() bool {
    response, err := http.Get(constants.AppCheckUpdateUrl)
    if err != nil {
        return false
    }
    defer response.Body.Close()

    jsonBytes, err := ioutil.ReadAll(response.Body)
    if err != nil {
        return false
    }

    tag, err := jsonparser.GetString(jsonBytes, "tag_name")
    if err != nil {
        return false
    }

    curTagArr := strings.Split(constants.AppVersion, ".")
    tagArr := strings.Split(strings.Trim(tag, "v"), ".")
    if len(tagArr) >= 1 && len(curTagArr) >= 1 {
        if tagArr[0] > curTagArr[0] {
            return true
        }

        if tagArr[0] < curTagArr[0] {
            return false
        }

    }

    if len(tagArr) >= 2 && len(curTagArr) >= 2 {
        if tagArr[1] > curTagArr[1] {
            return true
        }

        if tagArr[1] < curTagArr[1] {
            return false
        }

    }

    if len(tagArr) >= 3 && len(curTagArr) >= 3 {
        if tagArr[2] > curTagArr[2] {
            return true
        }

        if tagArr[2] < curTagArr[2] {
            return false
        }

    }

    return false
}

<|start_filename|>ds/user.go<|end_filename|>
package ds

import (
	"errors"
	"github.com/buger/jsonparser"
)

type User struct {
	UserId    int64  `json:"user_id"`
	Nickname  string `json:"nickname"`
	AvatarUrl string `json:"avatar_url"`
	AccountId int64  `json:"account_id"`
}

func NewUserFromLocalJson(json []byte) (User, error) {
	var user User
	if len(json) == 0 {
		return user, errors.New("json is empty")
	}

	userId, err := jsonparser.GetInt(json, "user_id")
	if err != nil {
		return user, err
	}
	user.UserId = userId

	if nickname, err := jsonparser.GetString(json, "nickname"); err == nil {
		user.Nickname = nickname
	}

	if avatarUrl, err := jsonparser.GetString(json, "avatar_url"); err == nil {
		user.AvatarUrl = avatarUrl
	}

	if accountId, err := jsonparser.GetInt(json, "account_id"); err == nil {
		user.AccountId = accountId
	}

	return user, nil
}

func NewUserFromJson(json []byte) (User, error) {
	var user User
	if len(json) == 0 {
		return user, errors.New("json is empty")
	}

	userId, err := jsonparser.GetInt(json, "profile", "userId")
	if err != nil {
		return user, err
	}
	user.UserId = userId

	if nickname, err := jsonparser.GetString(json, "profile", "nickname"); err == nil {
		user.Nickname = nickname
	}

	if avatarUrl, err := jsonparser.GetString(json, "profile", "avatarUrl"); err == nil {
		user.AvatarUrl = avatarUrl
	}

	if accountId, err := jsonparser.GetInt(json, "account", "id"); err == nil {
		user.AccountId = accountId
	}

	return user, nil
}


// NewUserFromSearchResultJson 从搜索结果json中获取用户信息
func NewUserFromSearchResultJson(json []byte) (User, error) {
	var user User
	if len(json) == 0 {
		return user, errors.New("json is empty")
	}

	userId, err := jsonparser.GetInt(json, "userId")
	if err != nil {
		return user, err
	}
	user.UserId = userId

	if nickname, err := jsonparser.GetString(json, "nickname"); err == nil {
		user.Nickname = nickname
	}

	if avatarUrl, err := jsonparser.GetString(json, "avatarUrl"); err == nil {
		user.AvatarUrl = avatarUrl
	}

	return user, nil
}

<|start_filename|>db/table.go<|end_filename|>
package db

import (
	"encoding/json"
	"errors"
	"fmt"
	"github.com/boltdb/bolt"
	"go-musicfox/utils"
)

type IteratorCallback func(k, v []byte) error

type table struct {}

// NewTable 创建table
func NewTable() *table {
	return &table{}
}

// AllMap traverse all data
func (table *table) AllMap(model Model, callback IteratorCallback) (err error) {
	localDB, err := DBManager.GetDBFromCache(model)
	if err != nil {
		return
	}

	err = localDB.View(func(tx *bolt.Tx) (err error) {
		bucketName := model.GetTableName()
		bucket := tx.Bucket([]byte(bucketName))
		if err = checkBucket(bucket, bucketName); err != nil {
			return err
		}

		err = bucket.ForEach(func(k, v []byte) error {
			if err = callback(k, v); err != nil {
				return err
			}

			return nil
		})

		return
	})

	return
}

// IncrAdd add one line, return increment id
func (table *table) IncrAdd(model Model, data IDSetter) (id uint64, err error) {
	localDB, err := DBManager.GetDBFromCache(model)
	if err != nil {
		return
	}

	err = localDB.Update(func(tx *bolt.Tx) error {
		bucketName := model.GetTableName()
		bucket, err := tx.CreateBucketIfNotExists([]byte(bucketName))
		if err != nil {
			return err
		}

		id, err = bucket.NextSequence()
		if err != nil {
			return err
		}
		data.SetID(id)

		buf, err := json.Marshal(data)
		if err != nil {
			return err
		}

		// Persist bytes to users bucket.
		return bucket.Put(utils.IDToBin(id), buf)
	})

	if err != nil {
		return
	}

	return id, nil
}

// Set edit one line
func (table *table) Set(model Model, key []byte, data interface{}) (err error) {
	localDB, err := DBManager.GetDBFromCache(model)
	if err != nil {
		return
	}

	err = localDB.Update(func(tx *bolt.Tx) error {
		bucketName := model.GetTableName()
		bucket, err := tx.CreateBucketIfNotExists([]byte(bucketName))
		if err != nil {
			return err
		}

		buf, err := json.Marshal(data)
		if err != nil {
			return err
		}

		// Persist bytes to users bucket.
		return bucket.Put(key, buf)
	})

	return
}

// SetByID edit one line by ID
func (table *table) SetByID(model Model, ID uint64, data interface{}) error {
	return table.Set(model, utils.IDToBin(ID), data)
}

// SetByKVModel edit one line by KVModel
func (table *table) SetByKVModel(model KVModel, data interface{}) error {
	return table.Set(model, []byte(model.GetKey()), data)
}

// Delete delete one line
func (table *table) Delete(model Model, key []byte) (err error) {
	localDB, err := DBManager.GetDBFromCache(model)
	if err != nil {
		return
	}

	err = localDB.Update(func(tx *bolt.Tx) error {
		bucketName := model.GetTableName()
		bucket, err := tx.CreateBucketIfNotExists([]byte(bucketName))
		if err != nil {
			return err
		}

		return bucket.Delete(key)
	})

	return
}

// DeleteByID delete one line by ID
func (table *table) DeleteByID(model Model, ID uint64) error {
	return table.Delete(model, utils.IDToBin(ID))
}

// DeleteByKVModel delete one line by KVModel
func (table *table) DeleteByKVModel(model KVModel) error {
	return table.Delete(model, []byte(model.GetKey()))
}

// Get 通过key获取value
func (table *table) Get(model Model, key []byte) (value []byte, err error) {
	db, err := DBManager.GetDBFromCache(model)
	if err != nil {
		return
	}

	err = db.View(func(tx *bolt.Tx) error {
		bucketName := model.GetTableName()
		bucket := tx.Bucket([]byte(bucketName))
		if err = checkBucket(bucket, bucketName); err != nil {
			return err
		}

		value = bucket.Get(key)
		return nil
	})

	return
}

// GetByID 通过ID获取value
func (table *table) GetByID(model Model, ID uint64) ([]byte, error) {
	return table.Get(model, utils.IDToBin(ID))
}

// GetByKVModel 通过KVModel获取value
func (table *table) GetByKVModel(model KVModel) ([]byte, error) {
	return table.Get(model, []byte(model.GetKey()))
}

func checkBucket(bucket *bolt.Bucket, bucketName string) error {
	if bucket == nil {
		return errors.New(fmt.Sprintf("Bucket(%s) not exists!", bucketName))
	}

	return nil
}



<|start_filename|>db/local_db.go<|end_filename|>
package db

import (
	"errors"
	"fmt"
	"github.com/boltdb/bolt"
	"go-musicfox/utils"
	"os"
	"time"
)

type LocalDB struct {
	*bolt.DB
}

// NewLocalDB 创建本地数据库
func NewLocalDB(dbName string) (*LocalDB, error) {
	projectPath := utils.GetLocalDataDir()

	dbDir := fmt.Sprintf("%s/db", projectPath)
	if _, err := os.Stat(dbDir); err != nil {
		err = os.MkdirAll(dbDir, 0755)
	}
	path := fmt.Sprintf("%s/%s.db", dbDir, dbName)

	options := bolt.DefaultOptions
	options.Timeout = 500 * time.Millisecond
	boltDB, err := bolt.Open(path, 0600, options)
	if err != nil {
		return nil, err
	}

	db := &LocalDB{
		DB: boltDB,
	}

	return db, err
}

var DBManager *LocalDBManager

type LocalDBManager struct {
	localDBs map[string]*LocalDB
}

// GetDBFromCache 从缓存中获取 LocalDB
func (dm *LocalDBManager) GetDBFromCache(db interface{}) (localDB *LocalDB, err error) {
	var dbName string
	switch dbWithType := db.(type) {
	case []byte:
		dbName = string(dbWithType)
	case string:
		dbName = dbWithType
	case Model:
		dbName = dbWithType.GetDbName()
	default:
		return nil, errors.New("param(db) expect a string or db.Model")
	}

	if dm.localDBs == nil {
		dm.localDBs = map[string]*LocalDB{}
	}

	localDB, ok := dm.localDBs[dbName]
	if !ok {
		localDB, err = NewLocalDB(dbName)
		if err != nil {
			return nil, err
		}
		dm.localDBs[dbName] = localDB
	}

	return localDB, nil
}

<|start_filename|>ui/loading.go<|end_filename|>
package ui

import (
    "fmt"
    "github.com/muesli/termenv"
    "go-musicfox/config"
    "strings"
    "unicode/utf8"
)

type Loading struct {
    model  *NeteaseModel
    curLen int
}

func NewLoading(m *NeteaseModel) *Loading {
    return &Loading{
        model: m,
    }
}

// 开始
func (loading *Loading)start() {
    termenv.MoveCursor(loading.model.menuTitleStartRow, 0)

    loading.curLen = utf8.RuneCountInString(loading.model.menuTitle) + utf8.RuneCountInString(" " + config.ConfigRegistry.MainLoadingText)

    var repeatSpace string
    if loading.model.menuTitleStartColumn > 0 {
        repeatSpace = strings.Repeat(" ", loading.model.menuTitleStartColumn)
    }
    fmt.Printf("%s%s%s",
        repeatSpace,
        SetFgStyle(loading.model.menuTitle, termenv.ANSIBrightGreen),
        SetFgStyle(" " + config.ConfigRegistry.MainLoadingText, termenv.ANSIBrightBlack))

    termenv.MoveCursor(0, 0)
}

// 完成
func (loading *Loading)complete() {
    termenv.MoveCursor(loading.model.menuTitleStartRow, 0)

    spaceLen := loading.curLen - utf8.RuneCountInString(loading.model.menuTitle)
    if spaceLen < 0 {
        spaceLen = 0
    }

    var repeatSpace string
    if loading.model.menuTitleStartColumn > 0 {
        repeatSpace = strings.Repeat(" ", loading.model.menuTitleStartColumn)
    }

    fmt.Printf("%s%s%s",
        repeatSpace,
        SetFgStyle(loading.model.menuTitle, termenv.ANSIBrightGreen),
        strings.Repeat("　", spaceLen))

    termenv.MoveCursor(0, 0)
}

<|start_filename|>ui/menu_dj_category.go<|end_filename|>
package ui

import (
    "github.com/anhoder/netease-music/service"
    "go-musicfox/ds"
    "go-musicfox/utils"
)

type DjCategoryMenu struct {
    menus      []MenuItem
    categories []ds.DjCategory
}

func NewDjCategoryMenu() *DjCategoryMenu {
    return &DjCategoryMenu{}
}

func (m *DjCategoryMenu) MenuData() interface{} {
    return nil
}

func (m *DjCategoryMenu) BeforeBackMenuHook() Hook {
    return nil
}

func (m *DjCategoryMenu) IsPlayable() bool {
    return false
}

func (m *DjCategoryMenu) ResetPlaylistWhenPlay() bool {
    return false
}

func (m *DjCategoryMenu) GetMenuKey() string {
    return "dj_category"
}

func (m *DjCategoryMenu) MenuViews() []MenuItem {
    return m.menus
}

func (m *DjCategoryMenu) SubMenu(_ *NeteaseModel, index int) IMenu {
    if index >= len(m.categories) {
        return nil
    }

    return NewDjCategoryDetailMenu(m.categories[index].Id)
}

func (m *DjCategoryMenu) BeforePrePageHook() Hook {
    // Nothing to do
    return nil
}

func (m *DjCategoryMenu) BeforeNextPageHook() Hook {
    // Nothing to do
    return nil
}

func (m *DjCategoryMenu) BeforeEnterMenuHook() Hook {
    return func(model *NeteaseModel) bool {

        // 不重复请求
        if len(m.menus) > 0 && len(m.categories) > 0 {
            return true
        }

        djCateService := service.DjCatelistService{}
        code, response := djCateService.DjCatelist()
        codeType := utils.CheckCode(code)
        if codeType != utils.Success {
            return false
        }

        m.categories = utils.GetDjCategory(response)
        m.menus = GetViewFromDjCate(m.categories)

        return true
    }
}

func (m *DjCategoryMenu) BottomOutHook() Hook {
    return nil
}

func (m *DjCategoryMenu) TopOutHook() Hook {
    // Nothing to do
    return nil
}


<|start_filename|>ui/menu_album_detail.go<|end_filename|>
package ui

import (
	"fmt"
	"github.com/anhoder/netease-music/service"
	"go-musicfox/ds"
	"go-musicfox/utils"
	"strconv"
)

type AlbumDetailMenu struct {
	menus   []MenuItem
	songs   []ds.Song
	AlbumId int64
}

func NewAlbumDetailMenu(albumId int64) *AlbumDetailMenu {
	return &AlbumDetailMenu{
		AlbumId: albumId,
	}
}

func (m *AlbumDetailMenu) MenuData() interface{} {
	return m.songs
}

func (m *AlbumDetailMenu) BeforeBackMenuHook() Hook {
	return nil
}

func (m *AlbumDetailMenu) IsPlayable() bool {
	return true
}

func (m *AlbumDetailMenu) ResetPlaylistWhenPlay() bool {
	return false
}

func (m *AlbumDetailMenu) GetMenuKey() string {
	return fmt.Sprintf("album_detail_%d", m.AlbumId)
}

func (m *AlbumDetailMenu) MenuViews() []MenuItem {
	return m.menus
}

func (m *AlbumDetailMenu) SubMenu(_ *NeteaseModel, _ int) IMenu {
	return nil
}

func (m *AlbumDetailMenu) BeforePrePageHook() Hook {
	// Nothing to do
	return nil
}

func (m *AlbumDetailMenu) BeforeNextPageHook() Hook {
	// Nothing to do
	return nil
}

func (m *AlbumDetailMenu) BeforeEnterMenuHook() Hook {
	return func(model *NeteaseModel) bool {

		albumService := service.AlbumService{
			ID: strconv.FormatInt(m.AlbumId, 10),
		}
		code, response := albumService.Album()
		codeType := utils.CheckCode(code)
		if codeType == utils.NeedLogin {
			NeedLoginHandle(model, enterMenu)
			return false
		} else if codeType != utils.Success {
			return false
		}

		m.songs = utils.GetSongsOfAlbum(response)
		m.menus = GetViewFromSongs(m.songs)

		return true
	}
}

func (m *AlbumDetailMenu) BottomOutHook() Hook {
	// Nothing to do
	return nil
}

func (m *AlbumDetailMenu) TopOutHook() Hook {
	// Nothing to do
	return nil
}


<|start_filename|>ui/menu_daily_recommend_songs.go<|end_filename|>
package ui

import (
	"github.com/anhoder/netease-music/service"
	"go-musicfox/ds"
	"go-musicfox/utils"
)

type DailyRecommendSongsMenu struct {
	menus []MenuItem
	songs []ds.Song
}

func NewDailyRecommendSongsMenu() *DailyRecommendSongsMenu {
	return new(DailyRecommendSongsMenu)
}

func (m *DailyRecommendSongsMenu) MenuData() interface{} {
	return m.songs
}

func (m *DailyRecommendSongsMenu) BeforeBackMenuHook() Hook {
	return nil
}

func (m *DailyRecommendSongsMenu) IsPlayable() bool {
	return true
}

func (m *DailyRecommendSongsMenu) ResetPlaylistWhenPlay() bool {
	return false
}

func (m *DailyRecommendSongsMenu) GetMenuKey() string {
	return "daily_songs"
}

func (m *DailyRecommendSongsMenu) MenuViews() []MenuItem {
	return m.menus
}

func (m *DailyRecommendSongsMenu) SubMenu(*NeteaseModel, int) IMenu {
	return nil
}

func (m *DailyRecommendSongsMenu) BeforePrePageHook() Hook {
	// Nothing to do
	return nil
}

func (m *DailyRecommendSongsMenu) BeforeNextPageHook() Hook {
	// Nothing to do
	return nil
}

func (m *DailyRecommendSongsMenu) BeforeEnterMenuHook() Hook {
	return func(model *NeteaseModel) bool {
		if utils.CheckUserInfo(model.user) == utils.NeedLogin {
			NeedLoginHandle(model, enterMenu)
			return false
		}

		// 不重复请求
		if len(m.menus) > 0 && len(m.songs) > 0 {
			return true
		}

		recommendSongs := service.RecommendSongsService{}
		code, response := recommendSongs.RecommendSongs()
		codeType := utils.CheckCode(code)
		if codeType == utils.NeedLogin {
			NeedLoginHandle(model, enterMenu)
			return false
		} else if codeType != utils.Success {
			return false
		}
		m.songs = utils.GetDailySongs(response)
		m.menus = GetViewFromSongs(m.songs)

		return true
	}
}

func (m *DailyRecommendSongsMenu) BottomOutHook() Hook {
	// Nothing to do
	return nil
}

func (m *DailyRecommendSongsMenu) TopOutHook() Hook {
	// Nothing to do
	return nil
}



<|start_filename|>ui/menu_radio_dj_type.go<|end_filename|>
package ui

type RadioDjTypeMenu struct {
    menus    []MenuItem
    menuList []IMenu
}

func NewRadioDjTypeMenu() *RadioDjTypeMenu {
    menu := new(RadioDjTypeMenu)
    menu.menus = []MenuItem{
        {Title: "我的订阅"},
        {Title: "推荐电台"},
        {Title: "今日优选"},
        {Title: "热门电台"},
        {Title: "新晋电台"},
        {Title: "电台分类"},
        {Title: "节目榜单"},
        {Title: "24小时节目榜"},
    }
    menu.menuList = []IMenu{
        NewDjSubListMenu(),
        NewDjRecommendMenu(),
        NewDjTodayRecommendMenu(),
        NewDjHotMenu(DjHot),
        NewDjHotMenu(DjNotHot),
        NewDjCategoryMenu(),
        NewDjProgramRankMenu(),
        NewDjProgramHoursRankMenu(),
    }

    return menu
}

func (m *RadioDjTypeMenu) MenuData() interface{} {
    return nil
}

func (m *RadioDjTypeMenu) BeforeBackMenuHook() Hook {
    return nil
}

func (m *RadioDjTypeMenu) IsPlayable() bool {
    return false
}

func (m *RadioDjTypeMenu) ResetPlaylistWhenPlay() bool {
    return false
}

func (m *RadioDjTypeMenu) GetMenuKey() string {
    return "radio_dj_type"
}

func (m *RadioDjTypeMenu) MenuViews() []MenuItem {
    return m.menus
}

func (m *RadioDjTypeMenu) SubMenu(_ *NeteaseModel, index int) IMenu {
    if index >= len(m.menuList) {
        return nil
    }

    return m.menuList[index]
}

func (m *RadioDjTypeMenu) BeforePrePageHook() Hook {
    // Nothing to do
    return nil
}

func (m *RadioDjTypeMenu) BeforeNextPageHook() Hook {
    // Nothing to do
    return nil
}

func (m *RadioDjTypeMenu) BeforeEnterMenuHook() Hook {
    // Nothing to do
    return nil
}

func (m *RadioDjTypeMenu) BottomOutHook() Hook {
    // Nothing to do
    return nil
}

func (m *RadioDjTypeMenu) TopOutHook() Hook {
    // Nothing to do
    return nil
}



<|start_filename|>ui/netease.go<|end_filename|>
package ui

import (
    "encoding/json"
    tea "github.com/anhoder/bubbletea"
    "github.com/anhoder/netease-music/service"
    "github.com/anhoder/netease-music/util"
    "github.com/mattn/go-runewidth"
    "github.com/telanflow/cookiejar"
    "go-musicfox/config"
    "go-musicfox/constants"
    "go-musicfox/db"
    "go-musicfox/ds"
    "go-musicfox/utils"
    "strconv"
    "time"
)

type NeteaseModel struct {
    WindowWidth    int
    WindowHeight   int
    isListeningKey bool
    program        *tea.Program
    user           *ds.User

    // startup
    *startupModel

    // main ui
    *mainUIModel

    // login
    loginModel *LoginModel

    // search
    searchModel *SearchModel
}

// NewNeteaseModel get netease model
func NewNeteaseModel(loadingDuration time.Duration) (m *NeteaseModel) {
    m = new(NeteaseModel)
    m.isListeningKey = !config.ConfigRegistry.StartupShow

    // startup
    m.startupModel = NewStartup()
    m.TotalDuration = loadingDuration

    // main menu
    m.mainUIModel = NewMainUIModel(m)

    // login
    m.loginModel = NewLogin()

    // search
    m.searchModel = NewSearch()

    // 东亚
    runewidth.EastAsianWidth = false

    return
}

func (m *NeteaseModel) Init() tea.Cmd {

    projectDir := utils.GetLocalDataDir()

    // 全局文件Jar
    cookieJar, _ := cookiejar.NewFileJar(projectDir+"/cookie", nil)
    util.SetGlobalCookieJar(cookieJar)

    // DBManager初始化
    db.DBManager = new(db.LocalDBManager)

    // 获取用户信息
    go func() {
        table := db.NewTable()

        // 获取用户信息
        if jsonStr, err := table.GetByKVModel(db.User{}); err == nil {
            if user, err := ds.NewUserFromLocalJson(jsonStr); err == nil {
                m.user = &user
            }
        }

        // 获取播放模式
        if jsonStr, err := table.GetByKVModel(db.PlayMode{}); err == nil && len(jsonStr) > 0 {
            var playMode PlayMode
            if err = json.Unmarshal(jsonStr, &playMode); err == nil {
                m.player.mode = playMode
            }
        }

        // 获取播放歌曲信息
        if jsonStr, err := table.GetByKVModel(db.PlayerSnapshot{}); err == nil && len(jsonStr) > 0 {
            var snapshot db.PlayerSnapshot
            if err = json.Unmarshal(jsonStr, &snapshot); err == nil {
                m.player.curSongIndex = snapshot.CurSongIndex
                m.player.playlist = snapshot.Playlist
                m.player.curSong = m.player.playlist[m.player.curSongIndex]
                //m.player.playingMenuKey = snapshot.PlayingMenuKey
                m.player.playingMenuKey = "from_local_db" // 启动后，重置菜单Key，避免很多问题
            }
        }

        // 签到
        if config.ConfigRegistry.StartupSignIn {
            var lastSignIn int
            if jsonStr, err := table.GetByKVModel(db.LastSignIn{}); err == nil && len(jsonStr) > 0 {
                _ = json.Unmarshal(jsonStr, &lastSignIn)
            }
            today, err := strconv.Atoi(time.Now().Format("20060102"))
            if m.user != nil && err == nil && lastSignIn != today {
                // 手机签到
                signInService := service.DailySigninService{}
                signInService.Type = "0"
                signInService.DailySignin()
                // PC签到
                signInService.Type = "1"
                signInService.DailySignin()

                _ = table.SetByKVModel(db.LastSignIn{}, today)

                utils.Notify("签到成功", "今日手机、PC端签到成功~", constants.AppGithubUrl)
            }
        }

        // 检查更新
        if config.ConfigRegistry.StartupCheckUpdate && utils.CheckUpdate() {
            utils.Notify("发现新版本", "点击去看看吧~", constants.AppLatestReleases)
        }
    }()

    if config.ConfigRegistry.StartupShow {
        return tickStartup(time.Nanosecond)
    }

    return tickMainUI(time.Nanosecond)
}

func (m *NeteaseModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    // Make sure these keys always quit
    switch msgWithType := msg.(type) {
    case tea.KeyMsg:
        k := msgWithType.String()
        // 登录界面输入q不退出
        if m.modelType == MtMain && (k == "q" || k == "Q" || k == "ctrl+c") {
            m.quitting = true
            return m, tea.Quit
        }

    case tea.WindowSizeMsg:
        m.WindowHeight = msgWithType.Height
        m.WindowWidth = msgWithType.Width

    }

    // Hand off the message and model to the approprate update function for the
    // appropriate view based on the current state.
    if config.ConfigRegistry.StartupShow && !m.loaded {
        if _, ok := msg.(tea.WindowSizeMsg); ok {
            updateMainUI(msg, m)
        }
        return updateStartup(msg, m)
    }

    switch m.modelType {
    case MtLogin:
        return updateLogin(msg, m)
    case MtSearch:
        return updateSearch(msg, m)
    }

    return updateMainUI(msg, m)
}

func (m *NeteaseModel) View() string {
    if m.quitting || m.WindowWidth <= 0 || m.WindowHeight <= 0 {
        return ""
    }

    if config.ConfigRegistry.StartupShow && !m.loaded {
        return startupView(m)
    }

    switch m.modelType {
    case MtLogin:
        return loginView(m)
    case MtSearch:
        return searchView(m)
    }

    return mainUIView(m)
}

func (m *NeteaseModel) BindProgram(program *tea.Program) {
    m.program = program
}

func (m *NeteaseModel) Rerender() {
    if m.program != nil {
        m.program.Rerender(m.View())
    }
}


<|start_filename|>ui/menu_ranks.go<|end_filename|>
package ui

import (
    "fmt"
    "github.com/anhoder/netease-music/service"
    "go-musicfox/ds"
    "go-musicfox/utils"
)

type RanksMenu struct {
    menus []MenuItem
    ranks []ds.Rank
}

func NewRanksMenu() *RanksMenu {
    return new(RanksMenu)
}

func (m *RanksMenu) MenuData() interface{} {
    return nil
}

func (m *RanksMenu) BeforeBackMenuHook() Hook {
    return nil
}

func (m *RanksMenu) IsPlayable() bool {
    return false
}

func (m *RanksMenu) ResetPlaylistWhenPlay() bool {
    return false
}

func (m *RanksMenu) GetMenuKey() string {
    return "ranks"
}

func (m *RanksMenu) MenuViews() []MenuItem {
    return m.menus
}

func (m *RanksMenu) SubMenu(_ *NeteaseModel, index int) IMenu {
    if index >= len(m.ranks) {
        return nil
    }

    return NewPlaylistDetailMenu(m.ranks[index].Id)
}

func (m *RanksMenu) BeforePrePageHook() Hook {
    // Nothing to do
    return nil
}

func (m *RanksMenu) BeforeNextPageHook() Hook {
    // Nothing to do
    return nil
}

func (m *RanksMenu) BeforeEnterMenuHook() Hook {
    return func(model *NeteaseModel) bool {
        if len(m.menus) > 0 && len(m.ranks) > 0 {
            return true
        }

        rankListService := service.ToplistService{}
        code, response := rankListService.Toplist()
        codeType := utils.CheckCode(code)
        if codeType != utils.Success {
            return false
        }

        var menus []MenuItem
        m.ranks = utils.GetRanks(response)
        for _, rank := range m.ranks {
            frequency := fmt.Sprintf("[%s]", rank.Frequency)
            menus = append(menus, MenuItem{utils.ReplaceSpecialStr(rank.Name), frequency})
        }
        m.menus = menus

        return true
    }
}

func (m *RanksMenu) BottomOutHook() Hook {
    // Nothing to do
    return nil
}

func (m *RanksMenu) TopOutHook() Hook {
    // Nothing to do
    return nil
}


<|start_filename|>ui/menu_daily_recommend_playlists.go<|end_filename|>
package ui

import (
	"github.com/anhoder/netease-music/service"
	"go-musicfox/ds"
	"go-musicfox/utils"
)

type DailyRecommendPlaylistsMenu struct {
	menus     []MenuItem
	playlists []ds.Playlist
}

func NewDailyRecommendPlaylistMenu() *DailyRecommendPlaylistsMenu {
	return new(DailyRecommendPlaylistsMenu)
}

func (m *DailyRecommendPlaylistsMenu) MenuData() interface{} {
	return m.playlists
}

func (m *DailyRecommendPlaylistsMenu) BeforeBackMenuHook() Hook {
	return nil
}

func (m *DailyRecommendPlaylistsMenu) IsPlayable() bool {
	return false
}

func (m *DailyRecommendPlaylistsMenu) ResetPlaylistWhenPlay() bool {
	return false
}

func (m *DailyRecommendPlaylistsMenu) GetMenuKey() string {
	return "daily_playlists"
}

func (m *DailyRecommendPlaylistsMenu) MenuViews() []MenuItem {
	return m.menus
}

func (m *DailyRecommendPlaylistsMenu) SubMenu(_ *NeteaseModel, index int) IMenu {
	if index >= len(m.playlists) {
		return nil
	}
	return NewPlaylistDetailMenu(m.playlists[index].Id)
}

func (m *DailyRecommendPlaylistsMenu) BeforePrePageHook() Hook {
	// Nothing to do
	return nil
}

func (m *DailyRecommendPlaylistsMenu) BeforeNextPageHook() Hook {
	// Nothing to do
	return nil
}

func (m *DailyRecommendPlaylistsMenu) BeforeEnterMenuHook() Hook {
	return func(model *NeteaseModel) bool {
		if utils.CheckUserInfo(model.user) == utils.NeedLogin {
			NeedLoginHandle(model, enterMenu)
			return false
		}

		// 不重复请求
		if len(m.menus) > 0 && len(m.playlists) > 0 {
			return true
		}

		recommendPlaylists := service.RecommendResourceService{}
		code, response := recommendPlaylists.RecommendResource()
		codeType := utils.CheckCode(code)
		if codeType == utils.NeedLogin {
			NeedLoginHandle(model, enterMenu)
			return false
		} else if codeType != utils.Success {
			return false
		}
		m.playlists = utils.GetDailyPlaylists(response)
		for _, playlist := range m.playlists {
			m.menus = append(m.menus, MenuItem{utils.ReplaceSpecialStr(playlist.Name), ""})
		}

		return true
	}
}

func (m *DailyRecommendPlaylistsMenu) BottomOutHook() Hook {
	// Nothing to do
	return nil
}

func (m *DailyRecommendPlaylistsMenu) TopOutHook() Hook {
	// Nothing to do
	return nil
}



<|start_filename|>ui/menu_dj_radio_detail.go<|end_filename|>
package ui

import (
    "fmt"
    "github.com/anhoder/netease-music/service"
    "go-musicfox/ds"
    "go-musicfox/utils"
    "strconv"
)

type DjRadioDetailMenu struct {
    menus     []MenuItem
    songs     []ds.Song
    djRadioId int64
    limit     int
    offset    int
}

func NewDjRadioDetailMenu(djRadioId int64) *DjRadioDetailMenu {
    return &DjRadioDetailMenu{
        djRadioId: djRadioId,
        limit:     50,
        offset:    0,
    }
}

func (m *DjRadioDetailMenu) MenuData() interface{} {
    return m.songs
}

func (m *DjRadioDetailMenu) BeforeBackMenuHook() Hook {
    return nil
}

func (m *DjRadioDetailMenu) IsPlayable() bool {
    return true
}

func (m *DjRadioDetailMenu) ResetPlaylistWhenPlay() bool {
    return false
}

func (m *DjRadioDetailMenu) GetMenuKey() string {
    return fmt.Sprintf("dj_radio_detail_%d", m.djRadioId)
}

func (m *DjRadioDetailMenu) MenuViews() []MenuItem {
    return m.menus
}

func (m *DjRadioDetailMenu) SubMenu(_ *NeteaseModel, _ int) IMenu {
    return nil
}

func (m *DjRadioDetailMenu) BeforePrePageHook() Hook {
    // Nothing to do
    return nil
}

func (m *DjRadioDetailMenu) BeforeNextPageHook() Hook {
    // Nothing to do
    return nil
}

func (m *DjRadioDetailMenu) BeforeEnterMenuHook() Hook {
    return func(model *NeteaseModel) bool {

        djProgramService := service.DjProgramService{
            RID:    strconv.FormatInt(m.djRadioId, 10),
            Limit:  strconv.Itoa(m.limit),
            Offset: strconv.Itoa(m.offset),
        }
        code, response := djProgramService.DjProgram()
        codeType := utils.CheckCode(code)
        if codeType != utils.Success {
            return false
        }
        m.songs = utils.GetSongsOfDjRadio(response)
        m.menus = GetViewFromSongs(m.songs)

        return true
    }
}

func (m *DjRadioDetailMenu) BottomOutHook() Hook {
    // Nothing to do
    return nil
}

func (m *DjRadioDetailMenu) TopOutHook() Hook {
    // Nothing to do
    return nil
}


<|start_filename|>db/user.go<|end_filename|>
package db

import "go-musicfox/constants"

type User struct {}

func (u User) GetDbName() string {
	return constants.AppDBName
}

func (u User) GetTableName() string {
	return "default_bucket"
}

func (u User) GetKey() string {
	return "cur_user"
}

<|start_filename|>ui/menu_personal_fm.go<|end_filename|>
package ui

import (
	"github.com/anhoder/netease-music/service"
	"go-musicfox/ds"
	"go-musicfox/utils"
)

type PersonalFmMenu struct {
	menus []MenuItem
	songs []ds.Song
}

func NewPersonalFmMenu() *PersonalFmMenu {
	return new(PersonalFmMenu)
}

func (m *PersonalFmMenu) BeforeBackMenuHook() Hook {
	return nil
}

func (m *PersonalFmMenu) IsPlayable() bool {
	return true
}

func (m *PersonalFmMenu) ResetPlaylistWhenPlay() bool {
	return true
}

func (m *PersonalFmMenu) GetMenuKey() string {
	return "personal_fm"
}

func (m *PersonalFmMenu) MenuViews() []MenuItem {
	return m.menus
}

func (m *PersonalFmMenu) MenuData() interface{} {
	return m.songs
}

func (m *PersonalFmMenu) SubMenu(*NeteaseModel, int) IMenu {
	return nil
}

func (m *PersonalFmMenu) BeforePrePageHook() Hook {
	// Nothing to do
	return nil
}

func (m *PersonalFmMenu) BeforeNextPageHook() Hook {
	// Nothing to do
	return nil
}

func (m *PersonalFmMenu) BeforeEnterMenuHook() Hook {
	return func(model *NeteaseModel) bool {
		// 已有数据
		if len(m.menus) > 0 && len(m.songs) > 0 {
			return true
		}

		personalFm := service.PersonalFmService{}
		code, response := personalFm.PersonalFm()
		codeType := utils.CheckCode(code)
		if codeType != utils.Success {
			return false
		}

		// 响应中获取数据
		m.songs = utils.GetFmSongs(response)
		m.menus = GetViewFromSongs(m.songs)

		return true
	}
}

func (m *PersonalFmMenu) BottomOutHook() Hook {
	return func(model *NeteaseModel) bool {
		personalFm := service.PersonalFmService{}
		code, response := personalFm.PersonalFm()
		codeType := utils.CheckCode(code)
		if codeType != utils.Success {
			return false
		}
		songs := utils.GetFmSongs(response)
		menus := GetViewFromSongs(songs)

		m.menus = append(m.menus, menus...)
		m.songs = append(m.songs, songs...)
		model.player.playlist = m.songs

		return true
	}
}

func (m *PersonalFmMenu) TopOutHook() Hook {
	// Nothing to do
	return nil
}



<|start_filename|>ui/menu_dj_cate_detail.go<|end_filename|>
package ui

import (
    "fmt"
    "github.com/anhoder/netease-music/service"
    "go-musicfox/ds"
    "go-musicfox/utils"
    "strconv"
)

type DjCategoryDetailMenu struct {
    menus      []MenuItem
    radios     []ds.DjRadio
    categoryId int64
}

func NewDjCategoryDetailMenu(categoryId int64) *DjCategoryDetailMenu {
    return &DjCategoryDetailMenu{
        categoryId: categoryId,
    }
}

func (m *DjCategoryDetailMenu) MenuData() interface{} {
    return nil
}

func (m *DjCategoryDetailMenu) BeforeBackMenuHook() Hook {
    return nil
}

func (m *DjCategoryDetailMenu) IsPlayable() bool {
    return false
}

func (m *DjCategoryDetailMenu) ResetPlaylistWhenPlay() bool {
    return false
}

func (m *DjCategoryDetailMenu) GetMenuKey() string {
    return fmt.Sprintf("dj_category_detail_%d", m.categoryId)
}

func (m *DjCategoryDetailMenu) MenuViews() []MenuItem {
    return m.menus
}

func (m *DjCategoryDetailMenu) SubMenu(_ *NeteaseModel, index int) IMenu {
    if index >= len(m.radios) {
        return nil
    }

    return NewDjRadioDetailMenu(m.radios[index].Id)
}

func (m *DjCategoryDetailMenu) BeforePrePageHook() Hook {
    // Nothing to do
    return nil
}

func (m *DjCategoryDetailMenu) BeforeNextPageHook() Hook {
    // Nothing to do
    return nil
}

func (m *DjCategoryDetailMenu) BeforeEnterMenuHook() Hook {
    return func(model *NeteaseModel) bool {

        // 不重复请求
        if len(m.menus) > 0 && len(m.radios) > 0 {
            return true
        }

        cateDetailService := service.DjRecommendTypeService{
            CateId: strconv.FormatInt(m.categoryId, 10),
        }
        code, response := cateDetailService.DjRecommendType()
        codeType := utils.CheckCode(code)
        if codeType != utils.Success {
            return false
        }

        m.radios = utils.GetDjRadios(response)
        m.menus = GetViewFromDjRadios(m.radios)

        return true
    }
}

func (m *DjCategoryDetailMenu) BottomOutHook() Hook {
    return nil
}

func (m *DjCategoryDetailMenu) TopOutHook() Hook {
    // Nothing to do
    return nil
}


<|start_filename|>ds/dj_radio.go<|end_filename|>
package ds

import (
    "errors"
    "github.com/buger/jsonparser"
)

type DjRadio struct {
    Id     int64  `json:"id"`
    Name   string `json:"name"`
    PicUrl string `json:"pic_url"`
    Dj     User   `json:"dj"`
}

// NewDjRadioFromJson 从Json中初始化 DjRadio
func NewDjRadioFromJson(json []byte) (DjRadio, error) {
    var radio DjRadio
    if len(json) == 0 {
        return radio, errors.New("json is empty")
    }

    radioId, err := jsonparser.GetInt(json, "id")
    if err != nil {
        return radio, err
    }
    radio.Id = radioId

    if name, err := jsonparser.GetString(json, "name"); err == nil {
        radio.Name = name
    }

    if picUrl, err := jsonparser.GetString(json, "picUrl"); err == nil {
        radio.PicUrl = picUrl
    }

    if djUserId, err := jsonparser.GetInt(json, "dj", "userId"); err == nil {
        radio.Dj.UserId = djUserId
    }

    if djName, err := jsonparser.GetString(json, "dj", "nickname"); err == nil {
        radio.Dj.Nickname = djName
    }

    if djAvatar, err := jsonparser.GetString(json, "dj", "avatarUrl"); err == nil {
        radio.Dj.AvatarUrl = djAvatar
    }

    return radio, nil
}

<|start_filename|>db/ky_model.go<|end_filename|>
package db

type KVModel interface {
	Model
	GetKey() string
}

<|start_filename|>utils/queue.go<|end_filename|>
package utils

type QNode struct {
    value interface{}
    next  *QNode
}

type Queue struct {
    head *QNode
    tail *QNode
    len  int
}

func (q *Queue) Len() int {
    return q.len
}

func (q *Queue) Peek() interface{} {
    if q.head == nil {
        return nil
    }
    return q.head.value
}

func (q *Queue) Enqueue(value interface{}) {
    newNode := &QNode{value, nil}
    if q.tail == nil {
        q.head = newNode
        q.tail = newNode
    } else {
        q.tail.next = newNode
        q.tail = newNode
    }
    q.len++
    newNode = nil
}

func (q *Queue) Dequeue() interface{} {
    if q.head == nil {
        return nil
    }
    first := q.head
    value := first.value

    q.head = first.next
    first.next = nil
    first.value = nil
    q.len--
    first = nil

    return value
}

<|start_filename|>ui/menu_album_new.go<|end_filename|>
package ui

import (
	"fmt"
	"github.com/anhoder/netease-music/service"
	"github.com/buger/jsonparser"
	"go-musicfox/ds"
	"go-musicfox/utils"
	"strconv"
	"strings"
)

type AlbumNewMenu struct {
	menus  []MenuItem
	albums []ds.Album
	area   string
	offset int
	limit  int
	total  int
}

func NewAlbumNewMenu(area string) *AlbumNewMenu {
	return &AlbumNewMenu{
		area:   area,
		offset: 0,
		limit:  50,
		total:  -1,
	}
}

func (m *AlbumNewMenu) MenuData() interface{} {
	return m.albums
}

func (m *AlbumNewMenu) BeforeBackMenuHook() Hook {
	return nil
}

func (m *AlbumNewMenu) IsPlayable() bool {
	return false
}

func (m *AlbumNewMenu) ResetPlaylistWhenPlay() bool {
	return false
}

func (m *AlbumNewMenu) GetMenuKey() string {
	return fmt.Sprintf("album_new_%s", m.area)
}

func (m *AlbumNewMenu) MenuViews() []MenuItem {
	return m.menus
}

func (m *AlbumNewMenu) SubMenu(_ *NeteaseModel, index int) IMenu {
	if len(m.albums) < index {
		return nil
	}

	return NewAlbumDetailMenu(m.albums[index].Id)
}

func (m *AlbumNewMenu) BeforePrePageHook() Hook {
	// Nothing to do
	return nil
}

func (m *AlbumNewMenu) BeforeNextPageHook() Hook {
	// Nothing to do
	return nil
}

func (m *AlbumNewMenu) BeforeEnterMenuHook() Hook {
	return func(model *NeteaseModel) bool {

		if len(m.menus) > 0 && len(m.albums) > 0 {
			return true
		}

		newAlbumService := service.AlbumNewService{
			Area:   m.area,
			Limit:  strconv.Itoa(m.limit),
			Offset: strconv.Itoa(m.offset),
		}
		code, response := newAlbumService.AlbumNew()
		codeType := utils.CheckCode(code)
		if codeType != utils.Success {
			return false
		}

		// 总数量
		if total, err := jsonparser.GetInt(response, "total"); err == nil {
			m.total = int(total)
		}

		m.albums = utils.GetNewAlbums(response)

		for _, album := range m.albums {
			var artists []string
			for _, artist := range album.Artists {
				artists = append(artists, artist.Name)
			}
			artistsStr := fmt.Sprintf("[%s]", strings.Join(artists, ","))
			m.menus = append(m.menus, MenuItem{utils.ReplaceSpecialStr(album.Name), utils.ReplaceSpecialStr(artistsStr)})
		}

		return true
	}
}

func (m *AlbumNewMenu) BottomOutHook() Hook {
	if m.total != -1 && m.offset < m.total  {
		return nil
	}
	return func(model *NeteaseModel) bool {
		m.offset = m.offset + len(m.menus)
		newAlbumService := service.AlbumNewService{
			Area:   m.area,
			Limit:  strconv.Itoa(m.limit),
			Offset: strconv.Itoa(m.offset),
		}
		code, response := newAlbumService.AlbumNew()
		codeType := utils.CheckCode(code)
		if codeType != utils.Success {
			return false
		}

		// 总数量
		if total, err := jsonparser.GetInt(response, "total"); err == nil {
			m.total = int(total)
		}

		albums := utils.GetNewAlbums(response)

		for _, album := range albums {
			var artists []string
			for _, artist := range album.Artists {
				artists = append(artists, artist.Name)
			}
			artistsStr := fmt.Sprintf("[%s]", strings.Join(artists, ","))
			m.menus = append(m.menus, MenuItem{utils.ReplaceSpecialStr(album.Name), utils.ReplaceSpecialStr(artistsStr)})
		}

		m.albums = append(m.albums, albums...)

		return true
	}
}

func (m *AlbumNewMenu) TopOutHook() Hook {
	// Nothing to do
	return nil
}


<|start_filename|>lyric/lrc.go<|end_filename|>
package lyric

import (
	"bufio"
	"errors"
	"fmt"
	"io"
	"math"
	"os"
	"sort"
	"strconv"
	"strings"
	"time"
)

// Parses an LRC file
// https://en.wikipedia.org/wiki/LRC_(file_format)

type LRCFile struct {
	fragments []LRCFragment
}

type LRCFragment struct {
	StartTimeMs int64
	Content     string
}

func OpenLRCFile(filePath string) (lrcFile *LRCFile, err error) {
	file, err := os.Open(filePath)
	if err != nil {
		return
	}
	defer file.Close()
	lrcFile, err = ReadLRC(file)
	return
}

func ReadLRC(reader io.Reader) (lrcFile *LRCFile, err error) {
	var fragments []LRCFragment

	lineNo := 1
	scanner := bufio.NewScanner(reader)
	for scanner.Scan() {
		var lineFragments []LRCFragment
		lineFragments, err = readLRCLine(scanner.Text(), lineNo)
		if err != nil {
			continue
		}
		for _, fragment := range lineFragments {
			fragments = append(fragments, fragment)
		}
		lineNo++
	}

	sort.Slice(fragments, func(i, j int) bool {
		return fragments[i].StartTimeMs < fragments[j].StartTimeMs
	})

	lrcFile = &LRCFile{
		fragments: fragments,
	}
	return
}

func readLRCLine(line string, lineNo int) (fragments []LRCFragment, err error) {
	line = strings.TrimSpace(line)
	if line == "" {
		return
	}

	tm, err := parseLRCTime(line, "[", "]")
	if err != nil {
		err = fmt.Errorf("Error on line %d: %v", lineNo, err)
		return
	}

	closeIndex := strings.Index(line, "]")
	line = line[closeIndex+1:]

	var extraTms []time.Duration
	for {
		extraTm, tmErr := parseLRCTime(line, "[", "]")
		if tmErr != nil {
			break
		}
		extraTms = append(extraTms, extraTm)
		line = line[closeIndex+1:]
	}

	line = strings.TrimSpace(line)
	if len(extraTms) > 0 {
		fragments = append(fragments, LRCFragment{
			StartTimeMs: tm.Milliseconds(),
			Content:     line,
		})

		for _, extraTm := range extraTms {
			fragments = append(fragments, LRCFragment{
				StartTimeMs: extraTm.Milliseconds(),
				Content:     line,
			})
		}
		return
	}

	lineFragments, err := parseContentLine(line, tm)
	for _, fragment := range lineFragments {
		fragments = append(fragments, fragment)
	}
	return
}

func parseLRCTime(line, openChar, closeChar string) (tm time.Duration, err error) {

	var left = strings.Index(line, openChar)
	var right = strings.Index(line, closeChar)
	if left < 0 && right < 0 {
		err = errors.New("brackets missing")
		return
	}
	timeStr := line[left + 1:right]
	t := strings.Split(timeStr, ":")
	if len(t) > 1 && t[0] != "" && t[1] != "" {
		minutes, err1 := strconv.Atoi(t[0])
		seconds, err2 := strconv.ParseFloat(t[1], 64)
		if err1 != nil || err2 != nil {
			err = errors.New("format error")
			return
		}
		var milliseconds = minutes * 60000 + int(math.Floor(seconds * 1000))
		tm = time.Duration(milliseconds) * time.Millisecond
		return
	}

	err = errors.New("brackets missing")
	return
}

func parseContentLine(line string, tm time.Duration) (fragments []LRCFragment, err error) {
	if !strings.Contains(line, "<") {
		fragments = append(fragments, LRCFragment{
			StartTimeMs: tm.Milliseconds(),
			Content:     line,
		})
		return
	}

	previousTm := tm
	startIndex := 0
	lastIndex := 0
	for {
		idx := strings.Index(line[lastIndex:], "<")
		closeIndex := strings.Index(line[lastIndex:], ">")
		if idx < 0 || closeIndex < 0 {
			break
		}
		idx += lastIndex
		closeIndex += lastIndex

		splitTm, tmErr := parseLRCTime(line[idx:], "<", ">")
		if tmErr == nil {
			fragments = append(fragments, LRCFragment{
				StartTimeMs: previousTm.Milliseconds(),
				Content:     strings.TrimSpace(line[startIndex:idx]),
			})
			startIndex = closeIndex + 1
			previousTm = splitTm
		}
		lastIndex = closeIndex + 1
	}

	fragments = append(fragments, LRCFragment{
		StartTimeMs: previousTm.Milliseconds(),
		Content:     strings.TrimSpace(line[startIndex:]),
	})
	return
}


<|start_filename|>ui/operate.go<|end_filename|>
package ui

import (
	"fmt"
	"github.com/anhoder/netease-music/service"
	"github.com/muesli/termenv"
	"go-musicfox/constants"
	"go-musicfox/db"
	"go-musicfox/ds"
	"go-musicfox/utils"
	"math"
	"strconv"
)

type menuStackItem struct {
	menuList      []MenuItem
	selectedIndex int
	menuCurPage   int
	menuTitle     string
	menu          IMenu
}

// 上移
func moveUp(m *NeteaseModel) {
	topHook := m.menu.TopOutHook()
	if m.doubleColumn {
		if m.selectedIndex-2 < 0 && topHook != nil {
			loading := NewLoading(m)
			loading.start()
			if res := topHook(m); !res {
				loading.complete()
				return
			}
			// 更新菜单UI
			m.menuList = m.menu.MenuViews()
			loading.complete()
		}
		if m.selectedIndex-2 < 0 {
			return
		}
		m.selectedIndex -= 2
	} else {
		if m.selectedIndex-1 < 0 && topHook != nil {
			loading := NewLoading(m)
			loading.start()
			if res := topHook(m); !res {
				loading.complete()
				return
			}
			m.menuList = m.menu.MenuViews()
			loading.complete()
		}
		if m.selectedIndex-1 < 0 {
			return
		}
		m.selectedIndex--
	}
	if m.selectedIndex < (m.menuCurPage-1)*m.menuPageSize {
		prePage(m)
	}
}

// 下移
func moveDown(m *NeteaseModel) {
	bottomHook := m.menu.BottomOutHook()
	if m.doubleColumn {
		if m.selectedIndex+2 > len(m.menuList)-1 && bottomHook != nil {
			loading := NewLoading(m)
			loading.start()
			if res := bottomHook(m); !res {
				loading.complete()
				return
			}
			m.menuList = m.menu.MenuViews()
			loading.complete()
		}
		if m.selectedIndex+2 > len(m.menuList)-1 {
			return
		}
		m.selectedIndex += 2
	} else {
		if m.selectedIndex+1 > len(m.menuList)-1 && bottomHook != nil {
			loading := NewLoading(m)
			loading.start()
			if res := bottomHook(m); !res {
				loading.complete()
				return
			}
			m.menuList = m.menu.MenuViews()
			loading.complete()
		}
		if m.selectedIndex+1 > len(m.menuList)-1 {
			return
		}
		m.selectedIndex++
	}
	if m.selectedIndex >= m.menuCurPage*m.menuPageSize {
		nextPage(m)
	}
}

// 左移
func moveLeft(m *NeteaseModel) {
	if !m.doubleColumn || m.selectedIndex%2 == 0 || m.selectedIndex-1 < 0 {
		return
	}
	m.selectedIndex--
}

// 右移
func moveRight(m *NeteaseModel) {
	if !m.doubleColumn || m.selectedIndex%2 != 0 {
		return
	}
	if bottomHook := m.menu.BottomOutHook(); m.selectedIndex >= len(m.menuList)-1 && bottomHook != nil {
		loading := NewLoading(m)
		loading.start()
		if res := bottomHook(m); !res {
			loading.complete()
			return
		}
		m.menuList = m.menu.MenuViews()
		loading.complete()
	}
	if m.selectedIndex >= len(m.menuList)-1 {
		return
	}
	m.selectedIndex++
}

// 切换到上一页
func prePage(m *NeteaseModel) {
	m.isListeningKey = false
	defer func() {
		m.isListeningKey = true
	}()

	if prePageHook := m.menu.BeforePrePageHook(); prePageHook != nil {
		loading := NewLoading(m)
		loading.start()
		if res := prePageHook(m); !res {
			loading.complete()
			return
		}
		loading.complete()
	}

	if m.menuCurPage <= 1 {
		return
	}
	m.menuCurPage--
}

// 切换到下一页
func nextPage(m *NeteaseModel) {
	m.isListeningKey = false
	defer func() {
		m.isListeningKey = true
	}()

	if nextPageHook := m.menu.BeforeNextPageHook(); nextPageHook != nil {
		loading := NewLoading(m)
		loading.start()
		if res := nextPageHook(m); !res {
			loading.complete()
			return
		}
		loading.complete()
	}
	if m.menuCurPage >= int(math.Ceil(float64(len(m.menuList))/float64(m.menuPageSize))) {
		return
	}

	m.menuCurPage++
}

// 进入菜单
func enterMenu(m *NeteaseModel) {
	m.isListeningKey = false
	defer func() {
		m.isListeningKey = true
	}()

	if m.selectedIndex >= len(m.menuList) {
		return
	}

	newTitle := m.menuList[m.selectedIndex]
	stackItem := &menuStackItem{
		menuList:      m.menuList,
		selectedIndex: m.selectedIndex,
		menuCurPage:   m.menuCurPage,
		menuTitle:     m.menuTitle,
		menu:          m.menu,
	}
	m.menuStack.Push(stackItem)

	menu := m.menu.SubMenu(m, m.selectedIndex)
	if menu == nil {
		m.menuStack.Pop()
		return
	}

	if enterMenuHook := menu.BeforeEnterMenuHook(); enterMenuHook != nil {
		loading := NewLoading(m)
		loading.start()
		if res := enterMenuHook(m); !res {
			loading.complete()
			m.menuStack.Pop() // 压入的重新弹出
			return
		}

		// 如果位于正在播放的菜单中，更新播放列表
		if menu.GetMenuKey() == m.player.playingMenuKey {
			if songs, ok := menu.MenuData().([]ds.Song); ok {
				m.player.playlist = songs
			}
		}

		loading.complete()
	}

	menuList := menu.MenuViews()

	m.menu = menu
	m.menuList = menuList
	m.menuTitle = fmt.Sprintf("%s %s", newTitle.Title, SetFgStyle(newTitle.Subtitle, termenv.ANSIBrightBlack))
	m.selectedIndex = 0
	m.menuCurPage = 1
}

// 菜单返回
func backMenu(m *NeteaseModel) {
	m.isListeningKey = false
	defer func() {
		m.isListeningKey = true
	}()

	if m.menuStack.Len() <= 0 {
		return
	}

	stackItem := m.menuStack.Pop()
	if backMenuHook := m.menu.BeforeBackMenuHook(); backMenuHook != nil {
		loading := NewLoading(m)
		loading.start()
		if res := backMenuHook(m); !res {
			loading.complete()
			m.menuStack.Push(stackItem) // 弹出的重新压入
			return
		}
		loading.complete()
	}

	stackMenu, ok := stackItem.(*menuStackItem)
	if !ok {
		return
	}

	m.menuList = stackMenu.menuList
	m.menu = stackMenu.menu
	m.menuTitle = stackMenu.menuTitle
	m.selectedIndex = stackMenu.selectedIndex
	m.menuCurPage = stackMenu.menuCurPage
}

// 空格监听
func spaceKeyHandle(m *NeteaseModel) {
	var (
		songs         []ds.Song
		inPlayingMenu = m.player.InPlayingMenu()
	)
	if inPlayingMenu && !m.menu.ResetPlaylistWhenPlay() {
		songs = m.player.playlist
	} else {
		if data, ok := m.menu.MenuData().([]ds.Song); ok {
			songs = data
		}
	}

	selectedIndex := m.selectedIndex
	if !m.menu.IsPlayable() || len(songs) == 0 || m.selectedIndex > len(songs)-1 {
		if m.player.curSongIndex > len(m.player.playlist)-1 {
			return
		}

		switch m.player.State {
		case utils.Paused:
			m.player.Resume()
		case utils.Playing:
			m.player.Paused()
		case utils.Stopped:
			_ = m.player.PlaySong(m.player.playlist[m.player.curSongIndex], DurationNext)
		}

		return
	}

	if inPlayingMenu && songs[selectedIndex].Id == m.player.playlist[m.player.curSongIndex].Id {
		switch m.player.State {
		case utils.Paused:
			m.player.Resume()
		case utils.Playing:
			m.player.Paused()
		}
	} else {
		m.player.curSongIndex = selectedIndex
		m.player.playingMenuKey = m.menu.GetMenuKey()
		m.player.playingMenu = m.menu
		m.player.playlist = songs
		if m.player.mode == PmIntelligent {
			m.player.SetPlayMode("")
		}
		_ = m.player.PlaySong(songs[selectedIndex], DurationNext)
	}

}

// likePlayingSong like/unlike playing song
func likePlayingSong(m *NeteaseModel, isLike bool) {
	loading := NewLoading(m)
	loading.start()
	defer loading.complete()

	if m.player.curSongIndex >= len(m.player.playlist) {
		return
	}

	if utils.CheckUserInfo(m.user) == utils.NeedLogin {
		NeedLoginHandle(m, func(m *NeteaseModel) {
			likePlayingSong(m, isLike)
		})
		return
	}

	likeService := service.LikeService{
		ID: strconv.FormatInt(m.player.playlist[m.player.curSongIndex].Id, 10),
		L: strconv.FormatBool(isLike),
	}
	likeService.Like()

	if isLike {
		utils.Notify("已添加到我喜欢的歌曲", m.player.playlist[m.player.curSongIndex].Name, constants.AppGithubUrl)
	} else {
		utils.Notify("已从我喜欢的歌曲移除", m.player.playlist[m.player.curSongIndex].Name, constants.AppGithubUrl)
	}
}

// logout 登出
func logout() {
	table := db.NewTable()
	_ = table.DeleteByKVModel(db.User{})
	utils.Notify("登出成功", "已清理用户信息", constants.AppGithubUrl)
}

// likeSelectedSong like/unlike selected song
func likeSelectedSong(m *NeteaseModel, isLike bool) {
	loading := NewLoading(m)
	loading.start()
	defer loading.complete()

	songs, ok := m.menu.MenuData().([]ds.Song)
	if !ok || m.selectedIndex >= len(songs) {
		return
	}

	if utils.CheckUserInfo(m.user) == utils.NeedLogin {
		NeedLoginHandle(m, func(m *NeteaseModel) {
			likeSelectedSong(m, isLike)
		})
		return
	}

	likeService := service.LikeService{
		ID: strconv.FormatInt(songs[m.selectedIndex].Id, 10),
		L: strconv.FormatBool(isLike),
	}
	likeService.Like()

	if isLike {
		utils.Notify("已添加到我喜欢的歌曲", songs[m.selectedIndex].Name, constants.AppGithubUrl)
	} else {
		utils.Notify("已从我喜欢的歌曲移除", songs[m.selectedIndex].Name, constants.AppGithubUrl)
	}
}

// trashPlayingSong 标记为不喜欢
func trashPlayingSong(m *NeteaseModel) {
	loading := NewLoading(m)
	loading.start()
	defer loading.complete()

	if m.player.curSongIndex >= len(m.player.playlist) {
		return
	}

	if utils.CheckUserInfo(m.user) == utils.NeedLogin {
		NeedLoginHandle(m, func(m *NeteaseModel) {
			trashPlayingSong(m)
		})
		return
	}

	trashService := service.FmTrashService{
		SongID: strconv.FormatInt(m.player.playlist[m.player.curSongIndex].Id, 10),
	}
	trashService.FmTrash()

	utils.Notify("已标记为不喜欢", m.player.playlist[m.player.curSongIndex].Name, constants.AppGithubUrl)
}

// trashSelectedSong 标记为不喜欢
func trashSelectedSong(m *NeteaseModel) {
	loading := NewLoading(m)
	loading.start()
	defer loading.complete()

	songs, ok := m.menu.MenuData().([]ds.Song)
	if !ok || m.selectedIndex >= len(songs) {
		return
	}

	if utils.CheckUserInfo(m.user) == utils.NeedLogin {
		NeedLoginHandle(m, func(m *NeteaseModel) {
			trashSelectedSong(m)
		})
		return
	}

	trashService := service.FmTrashService{
		SongID: strconv.FormatInt(songs[m.selectedIndex].Id, 10),
	}
	trashService.FmTrash()

	utils.Notify("已标记为不喜欢", songs[m.selectedIndex].Name, constants.AppGithubUrl)
}

<|start_filename|>ds/artist.go<|end_filename|>
package ds

import (
	"errors"
	"github.com/buger/jsonparser"
)

type Artist struct {
	Id   int64 `json:"id"`
	Name string `json:"name"`
}

func NewArtist(json []byte) (Artist, error) {
	var artist Artist

	if len(json) == 0 {
		return artist, errors.New("json is empty")
	}

	arId, err := jsonparser.GetInt(json, "id")
	if err != nil {
		return artist, err
	}
	artist.Id = arId

	if arName, err := jsonparser.GetString(json, "name"); err == nil {
		artist.Name = arName
	}

	return artist, nil
}

<|start_filename|>commands/netease.go<|end_filename|>
package commands

import (
    tea "github.com/anhoder/bubbletea"
    "github.com/gookit/gcli/v2"
    "go-musicfox/config"
    "go-musicfox/ui"
)

func NewPlayerCommand() *gcli.Command {

    return &gcli.Command{
        Name: "netease",
        // allow color tag and {$cmd} will be replace to 'demo'
        UseFor: "Command line player for Netease Cloud Music",
        Func: runPlayer,
    }
}

func runPlayer(cmd *gcli.Command, args []string) error {
    neteaseModel := ui.NewNeteaseModel(config.ConfigRegistry.StartupLoadingDuration)
    program := tea.NewProgram(neteaseModel)
    neteaseModel.BindProgram(program)
    program.EnterAltScreen()
    defer program.ExitAltScreen()
    if err := program.Start(); err != nil {
        return err
    }

    return nil
}

<|start_filename|>ui/menu_search_type.go<|end_filename|>
package ui

type SearchTypeMenu struct {
    menus    []MenuItem
}

func NewSearchTypeMenu() *SearchTypeMenu {
    typeMenu := new(SearchTypeMenu)
    typeMenu.menus = []MenuItem{
        {Title: "按单曲"},
        {Title: "按专辑"},
        {Title: "按歌手"},
        {Title: "按歌单"},
        {Title: "按用户"},
        {Title: "按歌词"},
        {Title: "按电台"},
    }

    return typeMenu
}

func (m *SearchTypeMenu) MenuData() interface{} {
    return nil
}

func (m *SearchTypeMenu) BeforeBackMenuHook() Hook {
    return nil
}

func (m *SearchTypeMenu) IsPlayable() bool {
    return false
}

func (m *SearchTypeMenu) ResetPlaylistWhenPlay() bool {
    return false
}

func (m *SearchTypeMenu) GetMenuKey() string {
    return "search_type"
}

func (m *SearchTypeMenu) MenuViews() []MenuItem {
    return m.menus
}

func (m *SearchTypeMenu) SubMenu(_ *NeteaseModel, index int) IMenu {

    typeArr := []SearchType{
        StSingleSong,
        StAlbum,
        StSinger,
        StPlaylist,
        StUser,
        StLyric,
        StRadio,
    }

    if index >= len(typeArr) {
        return nil
    }

    return NewSearchResultMenu(typeArr[index])
}

func (m *SearchTypeMenu) BeforePrePageHook() Hook {
    // Nothing to do
    return nil
}

func (m *SearchTypeMenu) BeforeNextPageHook() Hook {
    // Nothing to do
    return nil
}

func (m *SearchTypeMenu) BeforeEnterMenuHook() Hook {
    // Nothing to do
    return nil
}

func (m *SearchTypeMenu) BottomOutHook() Hook {
    // Nothing to do
    return nil
}

func (m *SearchTypeMenu) TopOutHook() Hook {
    // Nothing to do
    return nil
}



<|start_filename|>ui/menu_dj_sub_list.go<|end_filename|>
package ui

import (
    "github.com/anhoder/netease-music/service"
    "github.com/buger/jsonparser"
    "go-musicfox/ds"
    "go-musicfox/utils"
    "strconv"
)

type DjSubListMenu struct {
    menus  []MenuItem
    radios []ds.DjRadio
    limit  int
    offset int
    total  int
}

func NewDjSubListMenu() *DjSubListMenu {
    return &DjSubListMenu{
        limit:  50,
        offset: 0,
        total:  -1,
    }
}

func (m *DjSubListMenu) MenuData() interface{} {
    return nil
}

func (m *DjSubListMenu) BeforeBackMenuHook() Hook {
    return nil
}

func (m *DjSubListMenu) IsPlayable() bool {
    return false
}

func (m *DjSubListMenu) ResetPlaylistWhenPlay() bool {
    return false
}

func (m *DjSubListMenu) GetMenuKey() string {
    return "dj_sub"
}

func (m *DjSubListMenu) MenuViews() []MenuItem {
    return m.menus
}

func (m *DjSubListMenu) SubMenu(_ *NeteaseModel, index int) IMenu {
    if index >= len(m.radios) {
        return nil
    }

    return NewDjRadioDetailMenu(m.radios[index].Id)
}

func (m *DjSubListMenu) BeforePrePageHook() Hook {
    // Nothing to do
    return nil
}

func (m *DjSubListMenu) BeforeNextPageHook() Hook {
    // Nothing to do
    return nil
}

func (m *DjSubListMenu) BeforeEnterMenuHook() Hook {
    return func(model *NeteaseModel) bool {

        if utils.CheckUserInfo(model.user) == utils.NeedLogin {
            NeedLoginHandle(model, enterMenu)
            return false
        }

        // 不重复请求
        if len(m.menus) > 0 && len(m.radios) > 0 {
            return true
        }

        djSublistService := service.DjSublistService{
            Limit:  strconv.Itoa(m.limit),
            Offset: strconv.Itoa(m.offset),
        }
        code, response := djSublistService.DjSublist()
        codeType := utils.CheckCode(code)
        if codeType == utils.NeedLogin {
            NeedLoginHandle(model, enterMenu)
            return false
        } else if codeType != utils.Success {
            return false
        }

        if total, err := jsonparser.GetInt(response, "count"); err != nil {
            m.total = int(total)
        }

        m.radios = utils.GetDjRadios(response)
        m.menus = GetViewFromDjRadios(m.radios)

        return true
    }
}

func (m *DjSubListMenu) BottomOutHook() Hook {
    if len(m.radios) >= m.total {
        return nil
    }

    return func(model *NeteaseModel) bool {
        m.offset += m.limit

        if utils.CheckUserInfo(model.user) == utils.NeedLogin {
            NeedLoginHandle(model, enterMenu)
            return false
        }

        djSublistService := service.DjSublistService{
            Limit:  strconv.Itoa(m.limit),
            Offset: strconv.Itoa(m.offset),
        }
        code, response := djSublistService.DjSublist()
        codeType := utils.CheckCode(code)
        if codeType == utils.NeedLogin {
            NeedLoginHandle(model, enterMenu)
            return false
        } else if codeType != utils.Success {
            return false
        }

        if total, err := jsonparser.GetInt(response, "count"); err != nil {
            m.total = int(total)
        }

        radios := utils.GetDjRadios(response)
        menus := GetViewFromDjRadios(radios)

        m.radios = append(m.radios, radios...)
        m.menus = append(m.menus, menus...)

        return true
    }
}

func (m *DjSubListMenu) TopOutHook() Hook {
    // Nothing to do
    return nil
}


<|start_filename|>db/last_signin_date.go<|end_filename|>
package db

import "go-musicfox/constants"

type LastSignIn struct {}

func (p LastSignIn) GetDbName() string {
	return constants.AppDBName
}

func (p LastSignIn) GetTableName() string {
	return "default_bucket"
}

func (p LastSignIn) GetKey() string {
	return "last_sign_in"
}

<|start_filename|>ui/menu_artist_album.go<|end_filename|>
package ui

import (
    "fmt"
    "github.com/anhoder/netease-music/service"
    "go-musicfox/ds"
    "go-musicfox/utils"
    "strconv"
)

type ArtistAlbumMenu struct {
    menus    []MenuItem
    albums   []ds.Album
    artistId int64
}

func NewArtistAlbumMenu(artistId int64) *ArtistAlbumMenu {
    return &ArtistAlbumMenu{
        artistId: artistId,
    }
}

func (m *ArtistAlbumMenu) MenuData() interface{} {
    return m.albums
}

func (m *ArtistAlbumMenu) BeforeBackMenuHook() Hook {
    return nil
}

func (m *ArtistAlbumMenu) IsPlayable() bool {
    return false
}

func (m *ArtistAlbumMenu) ResetPlaylistWhenPlay() bool {
    return false
}

func (m *ArtistAlbumMenu) GetMenuKey() string {
    return fmt.Sprintf("artist_album_%d", m.artistId)
}

func (m *ArtistAlbumMenu) MenuViews() []MenuItem {
    return m.menus
}

func (m *ArtistAlbumMenu) SubMenu(_ *NeteaseModel, index int) IMenu {
    if len(m.albums) < index {
        return nil
    }

    return NewAlbumDetailMenu(m.albums[index].Id)
}

func (m *ArtistAlbumMenu) BeforePrePageHook() Hook {
    // Nothing to do
    return nil
}

func (m *ArtistAlbumMenu) BeforeNextPageHook() Hook {
    // Nothing to do
    return nil
}

func (m *ArtistAlbumMenu) BeforeEnterMenuHook() Hook {
    return func(model *NeteaseModel) bool {

        artistAlbumService := service.ArtistAlbumService{
            ID:     strconv.FormatInt(m.artistId, 10),
            Offset: "0",
            Limit:  "50",
        }
        code, response := artistAlbumService.ArtistAlbum()
        codeType := utils.CheckCode(code)
        if codeType != utils.Success {
            return false
        }

        m.albums = utils.GetArtistHotAlbums(response)
        m.menus = GetViewFromAlbums(m.albums)

        return true
    }
}

func (m *ArtistAlbumMenu) BottomOutHook() Hook {
    return nil
}

func (m *ArtistAlbumMenu) TopOutHook() Hook {
    // Nothing to do
    return nil
}


<|start_filename|>ui/menu_album_top_area.go<|end_filename|>
package ui

type AlbumTopAreaMenu struct {
    menus    []MenuItem
}

func NewAlbumTopAreaMenu() *AlbumTopAreaMenu {
    areaMenu := new(AlbumTopAreaMenu)
    areaMenu.menus = []MenuItem{
        {Title: "全部"},
        {Title: "华语"},
        {Title: "欧美"},
        {Title: "韩国"},
        {Title: "日本"},
    }

    return areaMenu
}

func (m *AlbumTopAreaMenu) MenuData() interface{} {
    return nil
}

func (m *AlbumTopAreaMenu) BeforeBackMenuHook() Hook {
    return nil
}

func (m *AlbumTopAreaMenu) IsPlayable() bool {
    return false
}

func (m *AlbumTopAreaMenu) ResetPlaylistWhenPlay() bool {
    return false
}

func (m *AlbumTopAreaMenu) GetMenuKey() string {
    return "album_top_area"
}

func (m *AlbumTopAreaMenu) MenuViews() []MenuItem {
    return m.menus
}

func (m *AlbumTopAreaMenu) SubMenu(_ *NeteaseModel, index int) IMenu {
    areaValueMapping := []string{
        "ALL",
        "ZH",
        "EA",
        "KR",
        "JP",
    }

    return NewAlbumTopMenu(areaValueMapping[index])
}

func (m *AlbumTopAreaMenu) BeforePrePageHook() Hook {
    // Nothing to do
    return nil
}

func (m *AlbumTopAreaMenu) BeforeNextPageHook() Hook {
    // Nothing to do
    return nil
}

func (m *AlbumTopAreaMenu) BeforeEnterMenuHook() Hook {
    // Nothing to do
    return nil
}

func (m *AlbumTopAreaMenu) BottomOutHook() Hook {
    // Nothing to do
    return nil
}

func (m *AlbumTopAreaMenu) TopOutHook() Hook {
    // Nothing to do
    return nil
}



<|start_filename|>ui/menu_hot_artists.go<|end_filename|>
package ui

import (
    "github.com/anhoder/netease-music/service"
    "go-musicfox/ds"
    "go-musicfox/utils"
)

type HotArtistsMenu struct {
    menus   []MenuItem
    artists []ds.Artist
}

func NewHotArtistsMenu() *HotArtistsMenu {
    return new(HotArtistsMenu)
}

func (m *HotArtistsMenu) MenuData() interface{} {
    return m.artists
}

func (m *HotArtistsMenu) BeforeBackMenuHook() Hook {
    return nil
}

func (m *HotArtistsMenu) IsPlayable() bool {
    return false
}

func (m *HotArtistsMenu) ResetPlaylistWhenPlay() bool {
    return false
}

func (m *HotArtistsMenu) GetMenuKey() string {
    return "hot_artists"
}

func (m *HotArtistsMenu) MenuViews() []MenuItem {
    return m.menus
}

func (m *HotArtistsMenu) SubMenu(_ *NeteaseModel, index int) IMenu {
    if index >= len(m.artists) {
        return nil
    }
    return NewArtistDetailMenu(m.artists[index].Id)
}

func (m *HotArtistsMenu) BeforePrePageHook() Hook {
    // Nothing to do
    return nil
}

func (m *HotArtistsMenu) BeforeNextPageHook() Hook {
    // Nothing to do
    return nil
}

func (m *HotArtistsMenu) BeforeEnterMenuHook() Hook {
    return func(model *NeteaseModel) bool {
        // 不重复请求
        if len(m.menus) > 0 && len(m.artists) > 0 {
            return true
        }

        artistService := service.TopArtistsService{
            Limit: "80",
        }
        code, response := artistService.TopArtists()
        codeType := utils.CheckCode(code)
        if codeType != utils.Success {
            return false
        }
        m.artists = utils.GetArtistsOfTopArtists(response)
        for _, artist := range m.artists {
            m.menus = append(m.menus, MenuItem{utils.ReplaceSpecialStr(artist.Name), ""})
        }

        return true
    }
}

func (m *HotArtistsMenu) BottomOutHook() Hook {
    // Nothing to do
    return nil
}

func (m *HotArtistsMenu) TopOutHook() Hook {
    // Nothing to do
    return nil
}


<|start_filename|>ui/menu_album_newest.go<|end_filename|>
package ui

import (
	"fmt"
	"github.com/anhoder/netease-music/service"
	"go-musicfox/ds"
	"go-musicfox/utils"
	"strings"
)

type AlbumNewestMenu struct {
	menus  []MenuItem
	albums []ds.Album
}

func NewAlbumNewestMenu() *AlbumNewestMenu {
	return new(AlbumNewestMenu)
}

func (m *AlbumNewestMenu) MenuData() interface{} {
	return m.albums
}

func (m *AlbumNewestMenu) BeforeBackMenuHook() Hook {
	return nil
}

func (m *AlbumNewestMenu) IsPlayable() bool {
	return false
}

func (m *AlbumNewestMenu) ResetPlaylistWhenPlay() bool {
	return false
}

func (m *AlbumNewestMenu) GetMenuKey() string {
	return "album_new_hot"
}

func (m *AlbumNewestMenu) MenuViews() []MenuItem {
	return m.menus
}

func (m *AlbumNewestMenu) SubMenu(_ *NeteaseModel, index int) IMenu {
	if len(m.albums) < index {
		return nil
	}

	return NewAlbumDetailMenu(m.albums[index].Id)
}

func (m *AlbumNewestMenu) BeforePrePageHook() Hook {
	// Nothing to do
	return nil
}

func (m *AlbumNewestMenu) BeforeNextPageHook() Hook {
	// Nothing to do
	return nil
}

func (m *AlbumNewestMenu) BeforeEnterMenuHook() Hook {
	return func(model *NeteaseModel) bool {

		if len(m.menus) > 0 && len(m.albums) > 0 {
			return true
		}

		albumService := service.AlbumNewestService{}
		code, response := albumService.AlbumNewest()
		codeType := utils.CheckCode(code)
		if codeType != utils.Success {
			return false
		}

		m.albums = utils.GetNewAlbums(response)

		for _, album := range m.albums {
			var artists []string
			for _, artist := range album.Artists {
				artists = append(artists, artist.Name)
			}
			artistsStr := fmt.Sprintf("[%s]", strings.Join(artists, ","))
			m.menus = append(m.menus, MenuItem{utils.ReplaceSpecialStr(album.Name), utils.ReplaceSpecialStr(artistsStr)})
		}

		return true
	}
}

func (m *AlbumNewestMenu) BottomOutHook() Hook {
	// Nothing to do
	return nil
}

func (m *AlbumNewestMenu) TopOutHook() Hook {
	// Nothing to do
	return nil
}


<|start_filename|>ui/menu_album_list.go<|end_filename|>
package ui

type AlbumListMenu struct {
    menus    []MenuItem
    menuList []IMenu
}

func NewAlbumListMenu() *AlbumListMenu {
    albumMenu := new(AlbumListMenu)
    albumMenu.menus = []MenuItem{
        {Title: "全部新碟"},
        {Title: "新碟上架"},
        {Title: "最新专辑"},
    }
    albumMenu.menuList = []IMenu{
        NewAlbumNewAreaMenu(),
        NewAlbumTopAreaMenu(),
        NewAlbumNewestMenu(),
    }

    return albumMenu
}

func (m *AlbumListMenu) MenuData() interface{} {
    return nil
}

func (m *AlbumListMenu) IsPlayable() bool {
    return false
}

func (m *AlbumListMenu) ResetPlaylistWhenPlay() bool {
    return false
}

func (m *AlbumListMenu) GetMenuKey() string {
    return "album_menu"
}

func (m *AlbumListMenu) MenuViews() []MenuItem {
    return m.menus
}

func (m *AlbumListMenu) SubMenu(_ *NeteaseModel, index int) IMenu {

    if index >= len(m.menuList) {
        return nil
    }

    return m.menuList[index]
}

func (m *AlbumListMenu) BeforePrePageHook() Hook {
    // Nothing to do
    return nil
}

func (m *AlbumListMenu) BeforeNextPageHook() Hook {
    // Nothing to do
    return nil
}

func (m *AlbumListMenu) BeforeEnterMenuHook() Hook {
    // Nothing to do
    return nil
}

func (m *AlbumListMenu) BeforeBackMenuHook() Hook {
    // Nothing to do
    return nil
}

func (m *AlbumListMenu) BottomOutHook() Hook {
    // Nothing to do
    return nil
}

func (m *AlbumListMenu) TopOutHook() Hook {
    // Nothing to do
    return nil
}



<|start_filename|>ui/menu_search_result.go<|end_filename|>
package ui

import (
    "fmt"
    "go-musicfox/ds"
)

type SearchResultMenu struct {
    menus      []MenuItem
    offset     int
    limit      int
    searchType SearchType
    keyword    string
    result     interface{}
}

func NewSearchResultMenu(searchType SearchType) *SearchResultMenu {
    return &SearchResultMenu{
        offset:     0,
        limit:      100,
        searchType: searchType,
    }
}

func (m *SearchResultMenu) MenuData() interface{} {
    return m.result
}

func (m *SearchResultMenu) BeforeBackMenuHook() Hook {
    return func(model *NeteaseModel) bool {
        if model.searchModel.wordsInput.Value() != "" {
            model.searchModel.wordsInput.SetValue("")
        }

        return true
    }
}

func (m *SearchResultMenu) IsPlayable() bool {
    playableMap := map[SearchType]bool{
        StSingleSong: true,
        StAlbum:      false,
        StSinger:     false,
        StPlaylist:   false,
        StUser:       false,
        StLyric:      true,
        StRadio:      false,
    }

    if playable, ok := playableMap[m.searchType]; ok {
        return playable
    }

    return false
}

func (m *SearchResultMenu) ResetPlaylistWhenPlay() bool {
    return false
}

func (m *SearchResultMenu) GetMenuKey() string {
    return fmt.Sprintf("search_result_%d_%s", m.searchType, m.keyword)
}

func (m *SearchResultMenu) MenuViews() []MenuItem {
    return m.menus
}

func (m *SearchResultMenu) SubMenu(_ *NeteaseModel, index int) IMenu {
    switch resultWithType := m.result.(type) {
    case []ds.Song:
        return nil
    case []ds.Album:
        if index >= len(resultWithType) {
            return nil
        }
        return NewAlbumDetailMenu(resultWithType[index].Id)
    case []ds.Playlist:
        if index >= len(resultWithType) {
            return nil
        }
        return NewPlaylistDetailMenu(resultWithType[index].Id)
    case []ds.Artist:
        if index >= len(resultWithType) {
            return nil
        }
        return NewArtistDetailMenu(resultWithType[index].Id)
    case []ds.User:
        if index >= len(resultWithType) {
            return nil
        }
        return NewUserPlaylistMenu(resultWithType[index].UserId)
    case []ds.DjRadio:
       if index >= len(resultWithType) {
           return nil
       }
       return NewDjRadioDetailMenu(resultWithType[index].Id)
    }

    return nil
}

func (m *SearchResultMenu) BeforePrePageHook() Hook {
    // Nothing to do
    return nil
}

func (m *SearchResultMenu) BeforeNextPageHook() Hook {
    // Nothing to do
    return nil
}

func (m *SearchResultMenu) BeforeEnterMenuHook() Hook {
    return func(model *NeteaseModel) bool {
        if model.searchModel.wordsInput.Value() == "" {
            // 显示搜索页面
            SearchHandle(model, m.searchType)
            return false
        }

        m.result = model.searchModel.result

        switch resultWithType := m.result.(type) {
        case []ds.Song:
            m.menus = GetViewFromSongs(resultWithType)
        case []ds.Album:
            m.menus = GetViewFromAlbums(resultWithType)
        case []ds.Playlist:
            m.menus = GetViewFromPlaylists(resultWithType)
        case []ds.Artist:
            m.menus = GetViewFromArtists(resultWithType)
        case []ds.User:
            m.menus = GetViewFromUsers(resultWithType)
        case []ds.DjRadio:
            m.menus = GetViewFromDjRadios(resultWithType)
        }

        return true
    }
}

func (m *SearchResultMenu) BottomOutHook() Hook {
    // 暂时只取前100
    return nil
}

func (m *SearchResultMenu) TopOutHook() Hook {
    // Nothing to do
    return nil
}


<|start_filename|>ui/menu_check_update.go<|end_filename|>
package ui

import (
    "go-musicfox/constants"
    "go-musicfox/utils"
)

type CheckUpdateMenu struct {
    hasUpdate bool
}

func NewCheckUpdateMenu() *CheckUpdateMenu {
    return &CheckUpdateMenu{}
}

func (m *CheckUpdateMenu) MenuData() interface{} {
    return nil
}

func (m *CheckUpdateMenu) BeforeBackMenuHook() Hook {
    return nil
}

func (m *CheckUpdateMenu) IsPlayable() bool {
    return false
}

func (m *CheckUpdateMenu) ResetPlaylistWhenPlay() bool {
    return false
}

func (m *CheckUpdateMenu) GetMenuKey() string {
    return "check_update"
}

func (m *CheckUpdateMenu) MenuViews() []MenuItem {
    if m.hasUpdate {
        return []MenuItem{
            {Title: "检查到新版本，回车查看~", Subtitle: "ENTER"},
        }
    }

    return []MenuItem{
        {Title: "已是最新版本"},
    }
}

func (m *CheckUpdateMenu) SubMenu(_ *NeteaseModel, _ int) IMenu {
    if m.hasUpdate {
        _ = utils.OpenUrl(constants.AppGithubUrl)
    }
    return nil
}

func (m *CheckUpdateMenu) BeforePrePageHook() Hook {
    // Nothing to do
    return nil
}

func (m *CheckUpdateMenu) BeforeNextPageHook() Hook {
    // Nothing to do
    return nil
}

func (m *CheckUpdateMenu) BeforeEnterMenuHook() Hook {
    return func(model *NeteaseModel) bool {
        m.hasUpdate = utils.CheckUpdate()
        return true
    }
}

func (m *CheckUpdateMenu) BottomOutHook() Hook {
    // Nothing to do
    return nil
}

func (m *CheckUpdateMenu) TopOutHook() Hook {
    // Nothing to do
    return nil
}


<|start_filename|>ui/menu_user_playlist.go<|end_filename|>
package ui

import (
    "fmt"
    "github.com/anhoder/netease-music/service"
    "github.com/buger/jsonparser"
    "go-musicfox/ds"
    "go-musicfox/utils"
    "strconv"
)

const CurUser int64 = 0

type UserPlaylistMenu struct {
    menus     []MenuItem
    playlists []ds.Playlist
    userId    int64
    offset    int
    limit     int
    hasMore   bool
}

func NewUserPlaylistMenu(userId int64) *UserPlaylistMenu {
    return &UserPlaylistMenu{
        userId: userId,
        offset: 0,
        limit:  100,
    }
}

func (m *UserPlaylistMenu) MenuData() interface{} {
    return m.playlists
}

func (m *UserPlaylistMenu) BeforeBackMenuHook() Hook {
    return nil
}

func (m *UserPlaylistMenu) IsPlayable() bool {
    return false
}

func (m *UserPlaylistMenu) ResetPlaylistWhenPlay() bool {
    return false
}

func (m *UserPlaylistMenu) GetMenuKey() string {
    return fmt.Sprintf("user_playlist_%d", m.userId)
}

func (m *UserPlaylistMenu) MenuViews() []MenuItem {
    return m.menus
}

func (m *UserPlaylistMenu) SubMenu(_ *NeteaseModel, index int) IMenu {
    if len(m.playlists) < index {
        return nil
    }
    return NewPlaylistDetailMenu(m.playlists[index].Id)
}

func (m *UserPlaylistMenu) BeforePrePageHook() Hook {
    // Nothing to do
    return nil
}

func (m *UserPlaylistMenu) BeforeNextPageHook() Hook {
    // Nothing to do
    return nil
}

func (m *UserPlaylistMenu) BeforeEnterMenuHook() Hook {
	return func(model *NeteaseModel) bool {
	    // 等于0，获取当前用户歌单
		if m.userId == CurUser && utils.CheckUserInfo(model.user) == utils.NeedLogin {
			NeedLoginHandle(model, enterMenu)
			return false
		}

		userId := m.userId
		if m.userId == CurUser {
            // 等于0，获取当前用户歌单
		    userId = model.user.UserId
        }

        if len(m.menus) > 0 && len(m.playlists) > 0 {
            return true
        }

        userPlaylists := service.UserPlaylistService{
            Uid:    strconv.FormatInt(userId, 10),
            Limit:  strconv.Itoa(m.limit),
            Offset: strconv.Itoa(m.offset),
        }
        code, response := userPlaylists.UserPlaylist()
        codeType := utils.CheckCode(code)
        if codeType == utils.NeedLogin {
            NeedLoginHandle(model, enterMenu)
            return false
        } else if codeType != utils.Success {
            return false
        }

        var menus []MenuItem
        m.playlists = utils.GetPlaylists(response)
        for _, playlist := range m.playlists {
            menus = append(menus, MenuItem{utils.ReplaceSpecialStr(playlist.Name), ""})
        }
        m.menus = menus

        // 是否有更多
        if hasMore, err := jsonparser.GetBoolean(response, "more"); err == nil {
            m.hasMore = hasMore
        }

        return true
    }
}

func (m *UserPlaylistMenu) BottomOutHook() Hook {
    if !m.hasMore {
        return nil
    }
    return func(model *NeteaseModel) bool {
        userId := m.userId
        if m.userId == CurUser {
            // 等于0，获取当前用户歌单
            userId = model.user.UserId
        }

        m.offset = m.offset + len(m.menus)
        userPlaylists := service.UserPlaylistService{
            Uid:    strconv.FormatInt(userId, 10),
            Limit:  strconv.Itoa(m.limit),
            Offset: strconv.Itoa(m.offset),
        }
        code, response := userPlaylists.UserPlaylist()
        codeType := utils.CheckCode(code)
        if codeType == utils.NeedLogin {
            NeedLoginHandle(model, nil)
            return false
        } else if codeType != utils.Success {
            return false
        }

        list := utils.GetPlaylists(response)
        for _, playlist := range list {
            m.menus = append(m.menus, MenuItem{utils.ReplaceSpecialStr(playlist.Name), ""})
        }

        m.playlists = append(m.playlists, list...)

        // 是否有更多
        if hasMore, err := jsonparser.GetBoolean(response, "more"); err == nil {
            m.hasMore = hasMore
        }

        return true
    }
}

func (m *UserPlaylistMenu) TopOutHook() Hook {
    // Nothing to do
    return nil
}


<|start_filename|>ui/menu.go<|end_filename|>
package ui

type Hook func(m *NeteaseModel) bool

// IMenu menu interface
type IMenu interface {
    // IsPlayable 当前菜单是否可播放？
    IsPlayable() bool

    // ResetPlaylistWhenPlay 空格播放时，是否重置播放列表？
    ResetPlaylistWhenPlay() bool

    // GetMenuKey 菜单唯一Key
    GetMenuKey() string

    // MenuViews 获取子菜单View
    MenuViews() []MenuItem

    // MenuData 获取当前菜单的数据
    MenuData() interface{}

    // SubMenu 根据下标获取菜单Model
    SubMenu(model *NeteaseModel, index int) IMenu

    // BeforePrePageHook 切换上一页前的Hook
    BeforePrePageHook() Hook

    // BeforeNextPageHook 切换下一页前的Hook
    BeforeNextPageHook() Hook

    // BeforeEnterMenuHook 进入菜单项前的Hook
    BeforeEnterMenuHook() Hook

    // BeforeBackMenuHook 菜单返回前的Hook
    BeforeBackMenuHook() Hook

    // BottomOutHook 触底的Hook
    BottomOutHook() Hook

    // TopOutHook 触顶Hook
    TopOutHook() Hook
}

// IDjMenu dj menu interface
type IDjMenu interface {
    IMenu
}

<|start_filename|>ui/menu_dj_program_hours_rank.go<|end_filename|>
package ui

import (
    "github.com/anhoder/netease-music/service"
    "go-musicfox/ds"
    "go-musicfox/utils"
)

type DjProgramHoursRankMenu struct {
    menus     []MenuItem
    songs     []ds.Song
}

func NewDjProgramHoursRankMenu() *DjProgramHoursRankMenu {
    return &DjProgramHoursRankMenu{}
}

func (m *DjProgramHoursRankMenu) MenuData() interface{} {
    return m.songs
}

func (m *DjProgramHoursRankMenu) BeforeBackMenuHook() Hook {
    return nil
}

func (m *DjProgramHoursRankMenu) IsPlayable() bool {
    return true
}

func (m *DjProgramHoursRankMenu) ResetPlaylistWhenPlay() bool {
    return false
}

func (m *DjProgramHoursRankMenu) GetMenuKey() string {
    return "dj_program_hour_rank"
}

func (m *DjProgramHoursRankMenu) MenuViews() []MenuItem {
    return m.menus
}

func (m *DjProgramHoursRankMenu) SubMenu(_ *NeteaseModel, _ int) IMenu {
    return nil
}

func (m *DjProgramHoursRankMenu) BeforePrePageHook() Hook {
    // Nothing to do
    return nil
}

func (m *DjProgramHoursRankMenu) BeforeNextPageHook() Hook {
    // Nothing to do
    return nil
}

func (m *DjProgramHoursRankMenu) BeforeEnterMenuHook() Hook {
    return func(model *NeteaseModel) bool {

        djProgramService := service.DjProgramToplistHoursService{
            Limit:  "100",
        }
        code, response := djProgramService.DjProgramToplistHours()
        codeType := utils.CheckCode(code)
        if codeType != utils.Success {
            return false
        }
        m.songs = utils.GetSongsOfDjHoursRank(response)
        m.menus = GetViewFromSongs(m.songs)

        return true
    }
}

func (m *DjProgramHoursRankMenu) BottomOutHook() Hook {
    // Nothing to do
    return nil
}

func (m *DjProgramHoursRankMenu) TopOutHook() Hook {
    // Nothing to do
    return nil
}


<|start_filename|>ui/menu_artist_song.go<|end_filename|>
package ui

import (
	"fmt"
	"github.com/anhoder/netease-music/service"
	"go-musicfox/ds"
	"go-musicfox/utils"
	"strconv"
)

type ArtistSongMenu struct {
	menus 	   []MenuItem
	songs      []ds.Song
	artistId int64
}

func NewArtistSongMenu(artistId int64) *ArtistSongMenu {
	return &ArtistSongMenu{
		artistId: artistId,
	}
}

func (m *ArtistSongMenu) MenuData() interface{} {
	return m.songs
}

func (m *ArtistSongMenu) BeforeBackMenuHook() Hook {
	return nil
}

func (m *ArtistSongMenu) IsPlayable() bool {
	return true
}

func (m *ArtistSongMenu) ResetPlaylistWhenPlay() bool {
	return false
}

func (m *ArtistSongMenu) GetMenuKey() string {
	return fmt.Sprintf("artist_song_%d", m.artistId)
}

func (m *ArtistSongMenu) MenuViews() []MenuItem {
	return m.menus
}

func (m *ArtistSongMenu) SubMenu(_ *NeteaseModel, _ int) IMenu {
	return nil
}

func (m *ArtistSongMenu) BeforePrePageHook() Hook {
	// Nothing to do
	return nil
}

func (m *ArtistSongMenu) BeforeNextPageHook() Hook {
	// Nothing to do
	return nil
}

func (m *ArtistSongMenu) BeforeEnterMenuHook() Hook {
	return func(model *NeteaseModel) bool {

		artistSongService := service.ArtistTopSongService{Id: strconv.FormatInt(m.artistId, 10)}
		code, response := artistSongService.ArtistTopSong()
		codeType := utils.CheckCode(code)
		if codeType != utils.Success {
			return false
		}
		m.songs = utils.GetSongsOfArtist(response)
		m.menus = GetViewFromSongs(m.songs)

		return true
	}
}

func (m *ArtistSongMenu) BottomOutHook() Hook {
	// Nothing to do
	return nil
}

func (m *ArtistSongMenu) TopOutHook() Hook {
	// Nothing to do
	return nil
}



<|start_filename|>ui/menu_high_quality_playlists.go<|end_filename|>
package ui

import (
    "github.com/anhoder/netease-music/service"
    "go-musicfox/ds"
    "go-musicfox/utils"
)

type HighQualityPlaylistsMenu struct {
    menus     []MenuItem
    playlists []ds.Playlist
}

func NewHighQualityPlaylistsMenu() *HighQualityPlaylistsMenu {
    return new(HighQualityPlaylistsMenu)
}

func (m *HighQualityPlaylistsMenu) MenuData() interface{} {
    return m.playlists
}

func (m *HighQualityPlaylistsMenu) BeforeBackMenuHook() Hook {
    return nil
}

func (m *HighQualityPlaylistsMenu) IsPlayable() bool {
    return false
}

func (m *HighQualityPlaylistsMenu) ResetPlaylistWhenPlay() bool {
    return false
}

func (m *HighQualityPlaylistsMenu) GetMenuKey() string {
    return "high_quality_playlists"
}

func (m *HighQualityPlaylistsMenu) MenuViews() []MenuItem {
    return m.menus
}

func (m *HighQualityPlaylistsMenu) SubMenu(_ *NeteaseModel, index int) IMenu {
    if index >= len(m.playlists) {
        return nil
    }
    return NewPlaylistDetailMenu(m.playlists[index].Id)
}

func (m *HighQualityPlaylistsMenu) BeforePrePageHook() Hook {
    // Nothing to do
    return nil
}

func (m *HighQualityPlaylistsMenu) BeforeNextPageHook() Hook {
    // Nothing to do
    return nil
}

func (m *HighQualityPlaylistsMenu) BeforeEnterMenuHook() Hook {
    return func(model *NeteaseModel) bool {
        // 不重复请求
        if len(m.menus) > 0 && len(m.playlists) > 0 {
            return true
        }

        highQualityPlaylists := service.TopPlaylistHighqualityService{
            Limit: "80",
        }
        code, response := highQualityPlaylists.TopPlaylistHighquality()
        codeType := utils.CheckCode(code)
        if codeType != utils.Success {
            return false
        }
        m.playlists = utils.GetPlaylistsFromHighQuality(response)
        for _, playlist := range m.playlists {
            m.menus = append(m.menus, MenuItem{utils.ReplaceSpecialStr(playlist.Name), ""})
        }

        return true
    }
}

func (m *HighQualityPlaylistsMenu) BottomOutHook() Hook {
    // Nothing to do
    return nil
}

func (m *HighQualityPlaylistsMenu) TopOutHook() Hook {
    // Nothing to do
    return nil
}



<|start_filename|>ds/rank.go<|end_filename|>
package ds

import (
    "encoding/json"
    "errors"
)

type Rank struct {
    Id        int64  `json:"id"`
    Name      string `json:"name"`
    Frequency string `json:"updateFrequency"` // 更新频率
}

// NewRankFromJson 获取排行榜信息
func NewRankFromJson(jsonBytes []byte) (Rank, error) {
    var rank Rank
    if len(jsonBytes) == 0 {
        return rank, errors.New("json is empty")
    }

    err := json.Unmarshal(jsonBytes, &rank)
    if err != nil {
        return rank, err
    }

    return rank, nil
}

<|start_filename|>ui/menu_dj_recommend.go<|end_filename|>
package ui

import (
    "github.com/anhoder/netease-music/service"
    "go-musicfox/ds"
    "go-musicfox/utils"
)

type DjRecommendMenu struct {
    menus  []MenuItem
    radios []ds.DjRadio
}

func NewDjRecommendMenu() *DjRecommendMenu {
    return &DjRecommendMenu{}
}

func (m *DjRecommendMenu) MenuData() interface{} {
    return nil
}

func (m *DjRecommendMenu) BeforeBackMenuHook() Hook {
    return nil
}

func (m *DjRecommendMenu) IsPlayable() bool {
    return false
}

func (m *DjRecommendMenu) ResetPlaylistWhenPlay() bool {
    return false
}

func (m *DjRecommendMenu) GetMenuKey() string {
    return "dj_recommend"
}

func (m *DjRecommendMenu) MenuViews() []MenuItem {
    return m.menus
}

func (m *DjRecommendMenu) SubMenu(_ *NeteaseModel, index int) IMenu {
    if index >= len(m.radios) {
        return nil
    }

    return NewDjRadioDetailMenu(m.radios[index].Id)
}

func (m *DjRecommendMenu) BeforePrePageHook() Hook {
    // Nothing to do
    return nil
}

func (m *DjRecommendMenu) BeforeNextPageHook() Hook {
    // Nothing to do
    return nil
}

func (m *DjRecommendMenu) BeforeEnterMenuHook() Hook {
    return func(model *NeteaseModel) bool {

        // 不重复请求
        if len(m.menus) > 0 && len(m.radios) > 0 {
            return true
        }

        djRecommendService := service.DjRecommendService{}
        code, response := djRecommendService.DjRecommend()
        codeType := utils.CheckCode(code)
        if codeType != utils.Success {
            return false
        }

        m.radios = utils.GetDjRadios(response)
        m.menus = GetViewFromDjRadios(m.radios)

        return true
    }
}

func (m *DjRecommendMenu) BottomOutHook() Hook {
    return nil
}

func (m *DjRecommendMenu) TopOutHook() Hook {
    // Nothing to do
    return nil
}


<|start_filename|>ds/playlist.go<|end_filename|>
package ds

import (
	"errors"
	"github.com/buger/jsonparser"
)

type Playlist struct {
	Id   int64
	Name string
}

// NewPlaylistFromJson 获取歌单信息
func NewPlaylistFromJson(json []byte) (Playlist, error) {
	var playlist Playlist
	if len(json) == 0 {
		return playlist, errors.New("json is empty")
	}

	id, err := jsonparser.GetInt(json, "id")
	if err != nil {
		return playlist, err
	}
	playlist.Id = id

	if name, err := jsonparser.GetString(json, "name"); err == nil {
		playlist.Name = name
	}

	return playlist, nil
}


<|start_filename|>ui/menu_dj_program_rank.go<|end_filename|>
package ui

import (
    "github.com/anhoder/netease-music/service"
    "go-musicfox/ds"
    "go-musicfox/utils"
)

type DjProgramRankMenu struct {
    menus     []MenuItem
    songs     []ds.Song
}

func NewDjProgramRankMenu() *DjProgramRankMenu {
    return &DjProgramRankMenu{}
}

func (m *DjProgramRankMenu) MenuData() interface{} {
    return m.songs
}

func (m *DjProgramRankMenu) BeforeBackMenuHook() Hook {
    return nil
}

func (m *DjProgramRankMenu) IsPlayable() bool {
    return true
}

func (m *DjProgramRankMenu) ResetPlaylistWhenPlay() bool {
    return false
}

func (m *DjProgramRankMenu) GetMenuKey() string {
    return "dj_program_rank"
}

func (m *DjProgramRankMenu) MenuViews() []MenuItem {
    return m.menus
}

func (m *DjProgramRankMenu) SubMenu(_ *NeteaseModel, _ int) IMenu {
    return nil
}

func (m *DjProgramRankMenu) BeforePrePageHook() Hook {
    // Nothing to do
    return nil
}

func (m *DjProgramRankMenu) BeforeNextPageHook() Hook {
    // Nothing to do
    return nil
}

func (m *DjProgramRankMenu) BeforeEnterMenuHook() Hook {
    return func(model *NeteaseModel) bool {

        djProgramService := service.DjProgramToplistService{
            Limit:  "100",
        }
        code, response := djProgramService.DjProgramToplist()
        codeType := utils.CheckCode(code)
        if codeType != utils.Success {
            return false
        }
        m.songs = utils.GetSongsOfDjRank(response)
        m.menus = GetViewFromSongs(m.songs)

        return true
    }
}

func (m *DjProgramRankMenu) BottomOutHook() Hook {
    // Nothing to do
    return nil
}

func (m *DjProgramRankMenu) TopOutHook() Hook {
    // Nothing to do
    return nil
}


<|start_filename|>ds/album.go<|end_filename|>
package ds

import (
	"errors"
	"github.com/buger/jsonparser"
)

type Album struct {
	Id      int64    `json:"id"`
	Name    string   `json:"name"`
	PicUrl  string   `json:"pic_url"`
	Artists []Artist `json:"artists"`
}

// NewAlbumFromJson 获取歌曲列表的专辑信息
func NewAlbumFromJson(json []byte) (Album, error) {
	var album Album
	if len(json) == 0 {
		return album, errors.New("json is empty")
	}

	alId, err := jsonparser.GetInt(json, "al", "id")
	if err != nil {
		return album, err
	}
	album.Id = alId

	if alName, err := jsonparser.GetString(json, "al", "name"); err == nil {
		album.Name = alName
	}

	if alPic, err := jsonparser.GetString(json, "al", "picUrl"); err == nil {
		album.PicUrl = alPic
	}

	return album, nil
}

// NewAlbumFromAlbumJson 从Album列表获取专辑信息
func NewAlbumFromAlbumJson(json []byte) (Album, error) {
	var album Album
	if len(json) == 0 {
		return album, errors.New("json is empty")
	}

	alId, err := jsonparser.GetInt(json, "id")
	if err != nil {
		return album, err
	}
	album.Id = alId

	if alName, err := jsonparser.GetString(json, "name"); err == nil {
		album.Name = alName
	}

	if alPic, err := jsonparser.GetString(json, "picUrl"); err == nil {
		album.PicUrl = alPic
	}

	_, _ = jsonparser.ArrayEach(json, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
		artist, err := NewArtist(value)

		if err == nil {
			album.Artists = append(album.Artists, artist)
		}

	}, "artists")

	return album, nil
}

<|start_filename|>ui/main_ui.go<|end_filename|>
package ui

import (
    "fmt"
    tea "github.com/anhoder/bubbletea"
    "github.com/mattn/go-runewidth"
    "github.com/muesli/termenv"
    "go-musicfox/config"
    "go-musicfox/constants"
    "go-musicfox/utils"
    "math"
    "strings"
    "time"
    "unicode/utf8"
)

// ModelType 显示模型的类型
type ModelType uint8

const (
    MtMain   ModelType = iota // 主页面
    MtLogin                   // 登录页面
    MtSearch                  // 搜索页面
)

type MenuItem struct {
    Title    string
    Subtitle string
}

type mainUIModel struct {
    doubleColumn bool // 是否双列显示

    menuTitle            string // 菜单标题
    menuTitleStartRow    int    // 菜单标题开始行
    menuTitleStartColumn int    // 菜单标题开始列

    menuStartRow    int // 菜单开始行
    menuStartColumn int // 菜单开始列
    menuBottomRow   int // 菜单最底部所在行

    menuCurPage  int // 菜单当前页
    menuPageSize int // 菜单每页大小

    menuList      []MenuItem   // 菜单列表
    menuStack     *utils.Stack // 菜单栈
    selectedIndex int          // 当前选中的菜单index

    showLogin bool      // 显示登陆
    modelType ModelType // 显示的页面类型

    menu   IMenu   // 菜单
    player *Player // 播放器
}

func NewMainUIModel(parentModel *NeteaseModel) (m *mainUIModel) {
    m = new(mainUIModel)
    m.menuTitle = "网易云音乐"
    m.player = NewPlayer(parentModel)
    m.menu = NewMainMenu()
    m.menuList = m.menu.MenuViews()
    m.menuStack = new(utils.Stack)
    m.menuCurPage = 1
    m.menuPageSize = 10
    m.selectedIndex = 0

    return
}

// update main ui
func updateMainUI(msg tea.Msg, m *NeteaseModel) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {

    case tea.KeyMsg:
        return keyMsgHandle(msg, m)

    case tea.ClearScreenMsg:
        return m, tickMainUI(time.Nanosecond)

    case tickMainUIMsg:
        return m, nil

    case tea.WindowSizeMsg:
        m.doubleColumn = msg.Width >= 75

        // 菜单开始行、列
        m.menuStartRow = msg.Height / 3
        if !config.ConfigRegistry.MainShowTitle && m.menuStartRow > 1 {
            m.menuStartRow--
        }
        if m.doubleColumn {
            m.menuStartColumn = (msg.Width - 60) / 2
            m.menuBottomRow = m.menuStartRow + int(math.Ceil(float64(m.menuPageSize)/2)) - 1
        } else {
            m.menuStartColumn = (msg.Width - 20) / 2
            m.menuBottomRow = m.menuStartRow + m.menuPageSize - 1
        }

        // 菜单标题开始行、列
        m.menuTitleStartColumn = m.menuStartColumn
        if config.ConfigRegistry.MainShowTitle && m.menuStartRow > 2 {
            if m.menuStartRow > 4 {
                m.menuTitleStartRow = m.menuStartRow - 3
            } else {
                m.menuTitleStartRow = 2
            }
        } else if !config.ConfigRegistry.MainShowTitle && m.menuStartRow > 1 {
            if m.menuStartRow > 3 {
                m.menuTitleStartRow = m.menuStartRow - 3
            } else {
                m.menuTitleStartRow = 2
            }
        }

        // 播放器歌词
        spaceHeight := m.WindowHeight - 4 - m.menuBottomRow
        if spaceHeight < 4 {
            // 不显示歌词
            m.player.showLyric = false
        } else {
            m.player.showLyric = true

            if spaceHeight > 6 {
                // 5行歌词
                m.player.lyricStartRow = (m.WindowHeight-3+m.menuBottomRow)/2 - 3
                m.player.lyricLines = 5
            } else {
                // 3行歌词
                m.player.lyricStartRow = (m.WindowHeight-3+m.menuBottomRow)/2 - 2
                m.player.lyricLines = 3
            }

        }

    }

    return m, nil
}

// get main ui view
func mainUIView(m *NeteaseModel) string {
    if m.WindowWidth <= 0 || m.WindowHeight <= 0 {
        return ""
    }

    var builder strings.Builder

    // 距离顶部的行数
    top := 0

    // title
    if config.ConfigRegistry.MainShowTitle {
        builder.WriteString(titleView(m, &top))
    } else {
        top++
    }

    // menu title
    builder.WriteString(menuTitleView(m, &top, ""))

    // menu list
    builder.WriteString(menuListView(m, &top))

    // player view
    builder.WriteString(m.player.playerView(&top))

    if top < m.WindowHeight {
        builder.WriteString(strings.Repeat("\n", m.WindowHeight-top-1))
    }

    return builder.String()
}

// title view
func titleView(m *NeteaseModel, top *int) string {
    var titleBuilder strings.Builder
    titleLen := utf8.RuneCountInString(constants.AppName) + 2
    prefixLen := (m.WindowWidth - titleLen) / 2
    suffixLen := m.WindowWidth - prefixLen - titleLen
    if prefixLen > 0 {
        titleBuilder.WriteString(strings.Repeat("─", prefixLen))
    }
    titleBuilder.WriteString(" ")
    titleBuilder.WriteString(constants.AppName)
    titleBuilder.WriteString(" ")
    if suffixLen > 0 {
        titleBuilder.WriteString(strings.Repeat("─", suffixLen))
    }

    *top++

    return SetFgStyle(titleBuilder.String(), GetPrimaryColor())
}

// menu title
func menuTitleView(m *NeteaseModel, top *int, menuTitle string) string {
    var (
        menuTitleBuilder strings.Builder
        title            string
        maxLen           = 50
    )
    if maxLen > m.WindowWidth-m.menuTitleStartColumn {
        maxLen = m.WindowWidth - m.menuTitleStartColumn
    }

    if len(menuTitle) <= 0 {
        menuTitle = m.menuTitle
    }

    if runewidth.StringWidth(menuTitle) > maxLen {
        title = runewidth.Truncate(menuTitle, maxLen, "")
    } else {
        title = runewidth.FillRight(menuTitle, maxLen)
    }

    if m.menuTitleStartRow-*top > 0 {
        menuTitleBuilder.WriteString(strings.Repeat("\n", m.menuTitleStartRow-*top))
    }
    if m.menuTitleStartColumn > 0 {
        menuTitleBuilder.WriteString(strings.Repeat(" ", m.menuTitleStartColumn))
    }
    menuTitleBuilder.WriteString(SetFgStyle(title, termenv.ANSIBrightGreen))

    *top = m.menuTitleStartRow

    return menuTitleBuilder.String()
}

// 菜单列表
func menuListView(m *NeteaseModel, top *int) string {
    var menuListBuilder strings.Builder
    menus := getCurPageMenus(m)
    var lines, maxLines int
    if m.doubleColumn {
        lines = int(math.Ceil(float64(len(menus)) / 2))
        maxLines = int(math.Ceil(float64(m.menuPageSize) / 2))
    } else {
        lines = len(menus)
        maxLines = m.menuPageSize
    }

    if m.menuStartRow > *top {
        menuListBuilder.WriteString(strings.Repeat("\n", m.menuStartRow-*top))
    }

    var str string
    for i := 0; i < lines; i++ {
        str = menuLineView(m, i)
        menuListBuilder.WriteString(str)
        menuListBuilder.WriteString("\n")
    }

    // 补全空白
    if maxLines > lines {
        if m.WindowWidth-m.menuStartColumn > 0 {
            menuListBuilder.WriteString(strings.Repeat(" ", m.WindowWidth-m.menuStartColumn))
        }
        menuListBuilder.WriteString(strings.Repeat("\n", maxLines-lines))
    }

    *top = m.menuBottomRow

    return menuListBuilder.String()
}

// 菜单Line
func menuLineView(m *NeteaseModel, line int) string {
    var menuLineBuilder strings.Builder
    var index int
    if m.doubleColumn {
        index = line*2 + (m.menuCurPage-1)*m.menuPageSize
    } else {
        index = line + (m.menuCurPage-1)*m.menuPageSize
    }
    if index > len(m.menuList)-1 {
        index = len(m.menuList) - 1
    }
    if m.menuStartColumn > 4 {
        menuLineBuilder.WriteString(strings.Repeat(" ", m.menuStartColumn-4))
    }
    menuLineBuilder.WriteString(menuItemView(m, index))
    if m.doubleColumn {
        if index < len(m.menuList)-1 {
            menuLineBuilder.WriteString(menuItemView(m, index+1))
        } else {
            menuLineBuilder.WriteString("    ")
        }
    }

    return menuLineBuilder.String()
}

// 菜单Item
func menuItemView(m *NeteaseModel, index int) string {
    var (
        menuItemBuilder strings.Builder
        menuTitle       string
        itemMaxLen      int
        menuName        string
    )

    if index == m.selectedIndex {
        menuTitle = fmt.Sprintf(" => %d. %s", index, m.menuList[index].Title)
    } else {
        menuTitle = fmt.Sprintf("    %d. %s", index, m.menuList[index].Title)
    }
    if len(m.menuList[index].Subtitle) != 0 {
        menuTitle += " "
    }

    if m.doubleColumn {
        if m.WindowWidth <= 88 {
            itemMaxLen = (m.WindowWidth - m.menuStartColumn - 4) / 2
        } else {
            if index%2 == 0 {
                itemMaxLen = 44
            } else {
                itemMaxLen = m.WindowWidth - m.menuStartColumn - 44
            }
        }
    } else {
        itemMaxLen = m.WindowWidth - m.menuStartColumn
    }

    menuTitleLen := runewidth.StringWidth(menuTitle)
    menuSubtitleLen := runewidth.StringWidth(m.menuList[index].Subtitle)

    var tmp string
    if menuTitleLen > itemMaxLen {
        tmp = runewidth.Truncate(menuTitle, itemMaxLen, "")
        tmp = runewidth.FillRight(tmp, itemMaxLen) // fix: 切割中文后缺少字符导致未对齐
        if index == m.selectedIndex {
            menuName = SetFgStyle(tmp, GetPrimaryColor())
        } else {
            menuName = SetNormalStyle(tmp)
        }
    } else if menuTitleLen+menuSubtitleLen > itemMaxLen {
        tmp = runewidth.Truncate(m.menuList[index].Subtitle, itemMaxLen-menuTitleLen, "")
        tmp = runewidth.FillRight(tmp, itemMaxLen-menuTitleLen)
        if index == m.selectedIndex {
            menuName = fmt.Sprintf("%s%s", SetFgStyle(menuTitle, GetPrimaryColor()), SetFgStyle(tmp, termenv.ANSIBrightBlack))
        } else {
            menuName = fmt.Sprintf("%s%s", SetNormalStyle(menuTitle), SetFgStyle(tmp, termenv.ANSIBrightBlack))
        }
    } else {
        tmp = runewidth.FillRight(m.menuList[index].Subtitle, itemMaxLen-menuTitleLen)
        if index == m.selectedIndex {
            menuName = fmt.Sprintf("%s%s", SetFgStyle(menuTitle, GetPrimaryColor()), SetFgStyle(tmp, termenv.ANSIBrightBlack))
        } else {
            menuName = fmt.Sprintf("%s%s", SetNormalStyle(menuTitle), SetFgStyle(tmp, termenv.ANSIBrightBlack))
        }
    }

    menuItemBuilder.WriteString(menuName)

    return menuItemBuilder.String()
}

// 获取当前页的菜单
func getCurPageMenus(m *NeteaseModel) []MenuItem {
    start := (m.menuCurPage - 1) * m.menuPageSize
    end := int(math.Min(float64(len(m.menuList)), float64(m.menuCurPage*m.menuPageSize)))

    return m.menuList[start:end]
}

// key handle
func keyMsgHandle(msg tea.KeyMsg, m *NeteaseModel) (tea.Model, tea.Cmd) {
    if !m.isListeningKey {
        return m, nil
    }
    switch msg.String() {
    case "j", "J", "down":
        moveDown(m)
    case "k", "K", "up":
        moveUp(m)
    case "h", "H", "left":
        moveLeft(m)
    case "l", "L", "right":
        moveRight(m)
    case "n", "N", "enter":
        enterMenu(m)
    case "b", "B", "esc":
        backMenu(m)
    case " ":
        spaceKeyHandle(m)
    case "[":
        m.player.PreSong()
    case "]":
        m.player.NextSong()
    case "p":
        m.player.SetPlayMode("")
    case "P":
        m.player.Intelligence(false)
    case ",":
        // like playing song
        likePlayingSong(m, true)
    case ".":
        // unlike playing song
        likePlayingSong(m, false)
    case "w", "W":
        // logout and quit
        logout()
        m.quitting = true
        return m, tea.Quit
    case "-":
        m.player.DownVolume()
    case "=":
        m.player.UpVolume()
    case "/":
        // trash playing song
        trashPlayingSong(m)
    case "<":
        // like selected song
        likeSelectedSong(m, true)
    case ">":
        // unlike selected song
        likeSelectedSong(m, false)
    case "?":
        // trash selected song
        trashSelectedSong(m)
    case "r", "R":
        // rerender
        return m, func() tea.Msg {
            return tea.ClearScreenMsg{}
        }
    }

    return m, tickMainUI(time.Nanosecond)
}


<|start_filename|>ui/menu_cloud.go<|end_filename|>
package ui

import (
    "github.com/anhoder/netease-music/service"
    "github.com/buger/jsonparser"
    "go-musicfox/ds"
    "go-musicfox/utils"
    "strconv"
)

type CloudMenu struct {
    menus   []MenuItem
    songs   []ds.Song
    limit   int
    offset  int
    hasMore bool
}

func NewCloudMenu() *CloudMenu {
    return &CloudMenu{
        limit:  100,
        offset: 0,
    }
}

func (m *CloudMenu) MenuData() interface{} {
    return m.songs
}

func (m *CloudMenu) BeforeBackMenuHook() Hook {
    return nil
}

func (m *CloudMenu) IsPlayable() bool {
    return true
}

func (m *CloudMenu) ResetPlaylistWhenPlay() bool {
    return false
}

func (m *CloudMenu) GetMenuKey() string {
    return "could"
}

func (m *CloudMenu) MenuViews() []MenuItem {
    return m.menus
}

func (m *CloudMenu) SubMenu(*NeteaseModel, int) IMenu {
    return nil
}

func (m *CloudMenu) BeforePrePageHook() Hook {
    // Nothing to do
    return nil
}

func (m *CloudMenu) BeforeNextPageHook() Hook {
    // Nothing to do
    return nil
}

func (m *CloudMenu) BeforeEnterMenuHook() Hook {
    return func(model *NeteaseModel) bool {
        if utils.CheckUserInfo(model.user) == utils.NeedLogin {
            NeedLoginHandle(model, enterMenu)
            return false
        }

        // 不重复请求
        if len(m.menus) > 0 && len(m.songs) > 0 {
            return true
        }

        cloudService := service.UserCloudService{
            Offset: strconv.Itoa(m.offset),
            Limit:  strconv.Itoa(m.limit),
        }
        code, response := cloudService.UserCloud()
        codeType := utils.CheckCode(code)
        if codeType == utils.NeedLogin {
            NeedLoginHandle(model, enterMenu)
            return false
        } else if codeType != utils.Success {
            return false
        }

        if hasMore, err := jsonparser.GetBoolean(response, "hasMore"); err == nil {
            m.hasMore = hasMore
        }

        m.songs = utils.GetSongsOfCloud(response)
        m.menus = GetViewFromSongs(m.songs)

        return true
    }
}

func (m *CloudMenu) BottomOutHook() Hook {
    if !m.hasMore {
        return nil
    }
    return func(model *NeteaseModel) bool {
        m.offset += m.limit

        // 不重复请求
        if len(m.menus) > 0 && len(m.songs) > 0 {
            return true
        }

        cloudService := service.UserCloudService{
            Offset: strconv.Itoa(m.offset),
            Limit:  strconv.Itoa(m.limit),
        }
        code, response := cloudService.UserCloud()
        codeType := utils.CheckCode(code)
        if codeType == utils.NeedLogin {
            NeedLoginHandle(model, enterMenu)
            return false
        } else if codeType != utils.Success {
            return false
        }

        if hasMore, err := jsonparser.GetBoolean(response, "hasMore"); err == nil {
            m.hasMore = hasMore
        }

        songs := utils.GetSongsOfCloud(response)
        menus := GetViewFromSongs(songs)

        m.songs = append(m.songs, songs...)
        m.menus = append(m.menus, menus...)

        return true
    }
}

func (m *CloudMenu) TopOutHook() Hook {
    // Nothing to do
    return nil
}


<|start_filename|>ui/tick_msg.go<|end_filename|>
package ui

import (
    tea "github.com/anhoder/bubbletea"
    "time"
)

// startup tick
type tickStartupMsg struct{}

func tickStartup(duration time.Duration) tea.Cmd {
    return tea.Tick(duration, func(time.Time) tea.Msg {
        return tickStartupMsg{}
    })
}

// main ui tick
type tickMainUIMsg struct{}

func tickMainUI(duration time.Duration) tea.Cmd {
    return tea.Tick(duration, func(time.Time) tea.Msg {
        return tickMainUIMsg{}
    })
}

// login tick
type tickLoginMsg struct{}

func tickLogin(duration time.Duration) tea.Cmd {
    return tea.Tick(duration, func(t time.Time) tea.Msg {
        return tickLoginMsg{}
    })
}

// search tick
type tickSearchMsg struct{}

func tickSearch(duration time.Duration) tea.Cmd {
    return tea.Tick(duration, func(t time.Time) tea.Msg {
        return tickSearchMsg{}
    })
}

<|start_filename|>ui/login.go<|end_filename|>
package ui

import (
    "github.com/anhoder/bubbles/textinput"
    tea "github.com/anhoder/bubbletea"
    "github.com/anhoder/netease-music/service"
    "github.com/mattn/go-runewidth"
    "github.com/muesli/termenv"
    "go-musicfox/config"
    "go-musicfox/db"
    "go-musicfox/ds"
    "go-musicfox/utils"
    "strings"
    "time"
)

type LoginModel struct {
    index         int
    accountInput  textinput.Model
    passwordInput textinput.Model
    submitButton  string
    tips          string
    AfterLogin    func (m *NeteaseModel)
}

func NewLogin() (login *LoginModel) {
    login = new(LoginModel)
    login.accountInput = textinput.NewModel()
    login.accountInput.Placeholder = " 手机号或邮箱"
    login.accountInput.Focus()
    login.accountInput.Prompt = GetFocusedPrompt()
    login.accountInput.TextColor = primaryColorStr
    login.accountInput.CharLimit = 32

    login.passwordInput = textinput.NewModel()
    login.passwordInput.Placeholder = " 密码"
    login.passwordInput.Prompt = "> "
    login.passwordInput.EchoMode = textinput.EchoPassword
    login.passwordInput.EchoCharacter = '•'
    login.passwordInput.CharLimit = 32

    login.submitButton = GetBlurredSubmitButton()

    return
}

// update main ui
func updateLogin(msg tea.Msg, m *NeteaseModel) (tea.Model, tea.Cmd) {

    switch msg := msg.(type) {
    case tickLoginMsg:
        return m, nil
    case tea.KeyMsg:
        switch msg.String() {

        case "esc":
            m.modelType = MtMain
            m.loginModel.tips = ""
            return m, tickMainUI(time.Nanosecond)

        // Cycle between inputs
        case "tab", "shift+tab", "enter", "up", "down":

            inputs := []textinput.Model{
                m.loginModel.accountInput,
                m.loginModel.passwordInput,
            }

            s := msg.String()

            // Did the user press enter while the submit button was focused?
            // If so, exit.
            if s == "enter" && m.loginModel.index == len(inputs) {
                if len(m.loginModel.accountInput.Value()) <= 0 || len(m.loginModel.passwordInput.Value()) <= 0 {
                    m.loginModel.tips = SetFgStyle("请输入账号或密码", termenv.ANSIBrightRed)
                    return m, nil
                }
                var (
                	code     float64
                	response []byte
                )
                if strings.ContainsRune(m.loginModel.accountInput.Value(), '@') {
                    loginService := service.LoginEmailService{
                        Email: m.loginModel.accountInput.Value(),
                        Password: <PASSWORD>(),
                    }
                    code, response = loginService.LoginEmail()
                } else {
                    loginService := service.LoginCellphoneService{
                        Phone: m.loginModel.accountInput.Value(),
                        Password: <PASSWORD>(),
                    }
                    code, response = loginService.LoginCellphone()
                }

                codeType := utils.CheckCode(code)
                switch codeType {
                case utils.UnknownError:
                    m.loginModel.tips = SetFgStyle("未知错误，请稍后再试~", termenv.ANSIBrightRed)
                    return m, tickLogin(time.Nanosecond)
                case utils.NetworkError:
                    m.loginModel.tips = SetFgStyle("网络异常，请稍后再试~", termenv.ANSIBrightRed)
                    return m, tickLogin(time.Nanosecond)
                case utils.Success:
                    if user, err := ds.NewUserFromJson(response); err == nil {
                        m.user = &user

                        // 写入本地数据库
                        table := db.NewTable()
                        _ = table.SetByKVModel(db.User{}, user)

                        if m.loginModel.AfterLogin != nil {
                            m.loginModel.AfterLogin(m)
                        }
                    }
                default:
                    m.loginModel.tips = SetFgStyle("你是个好人，但我们不合适(╬▔皿▔)凸 ", termenv.ANSIBrightRed) +
                        SetFgStyle("(账号或密码错误)", termenv.ANSIBrightBlack)
                    return m, tickLogin(time.Nanosecond)
                }

                m.modelType = MtMain
                m.loginModel.tips = ""
                return m, tickMainUI(time.Nanosecond)
            }

            // Cycle indexes
            if s == "up" || s == "shift+tab" {
                m.loginModel.index--
            } else {
                m.loginModel.index++
            }

            if m.loginModel.index > len(inputs) {
                m.loginModel.index = 0
            } else if m.loginModel.index < 0 {
                m.loginModel.index = len(inputs)
            }

            for i := 0; i <= len(inputs)-1; i++ {
                if i == m.loginModel.index {
                    // Set focused state
                    inputs[i].Focus()
                    inputs[i].Prompt = GetFocusedPrompt()
                    inputs[i].TextColor = primaryColorStr
                    continue
                }
                // Remove focused state
                inputs[i].Blur()
                inputs[i].Prompt = GetBlurredPrompt()
                inputs[i].TextColor = ""
            }

            m.loginModel.accountInput = inputs[0]
            m.loginModel.passwordInput = inputs[1]

            if m.loginModel.index == len(inputs) {
                m.loginModel.submitButton = GetFocusedSubmitButton()
            } else {
                m.loginModel.submitButton = GetBlurredSubmitButton()
            }

            return m, nil
        }
    }

    // Handle character input and blinks
    return updateLoginInputs(msg, m)
}

func updateLoginInputs(msg tea.Msg, m *NeteaseModel) (tea.Model, tea.Cmd) {
    var (
        cmd  tea.Cmd
        cmds []tea.Cmd
    )

    m.loginModel.accountInput, cmd = m.loginModel.accountInput.Update(msg)
    cmds = append(cmds, cmd)

    m.loginModel.passwordInput, cmd = m.loginModel.passwordInput.Update(msg)
    cmds = append(cmds, cmd)

    return m, tea.Batch(cmds...)
}

func loginView(m *NeteaseModel) string {

    var builder strings.Builder

    // 距离顶部的行数
    top := 0

    // title
    if config.ConfigRegistry.MainShowTitle {

        builder.WriteString(titleView(m, &top))
    } else {
        top++
    }

    // menu title
    builder.WriteString(menuTitleView(m, &top, "用户登录 (手机号或邮箱)"))
    builder.WriteString("\n\n\n")
    top += 2

    inputs := []textinput.Model{
        m.loginModel.accountInput,
        m.loginModel.passwordInput,
    }

    for i, input := range inputs {
        if m.menuStartColumn > 0 {
            builder.WriteString(strings.Repeat(" ", m.menuStartColumn))
        }

        builder.WriteString(input.View())

        var valueLen int
        if input.Value() == "" {
            valueLen = runewidth.StringWidth(input.Placeholder)
        } else {
            valueLen = runewidth.StringWidth(input.Value())
        }
        if spaceLen := m.WindowWidth-m.menuStartColumn-valueLen-3; spaceLen > 0 {
           builder.WriteString(strings.Repeat(" ", spaceLen))
        }

        top++

        if i < len(inputs)-1 {
            builder.WriteString("\n\n")
            top++
        }
    }

    builder.WriteString("\n\n")
    top++
    if m.menuStartColumn > 0 {
        builder.WriteString(strings.Repeat(" ", m.menuStartColumn))
    }
    builder.WriteString(m.loginModel.tips)
    builder.WriteString("\n\n")
    top++
    if m.menuStartColumn > 0 {
        builder.WriteString(strings.Repeat(" ", m.menuStartColumn))
    }
    builder.WriteString(m.loginModel.submitButton)
    builder.WriteString("\n")

    if m.WindowHeight > top+3 {
        builder.WriteString(strings.Repeat("\n", m.WindowHeight-top-3))
    }

    return builder.String()
}

// NeedLoginHandle 需要登录的处理
func NeedLoginHandle(model *NeteaseModel, callback func (m *NeteaseModel)) {
    model.modelType = MtLogin
    model.loginModel.AfterLogin = callback
}

<|start_filename|>ui/menu_dj_today_recommend.go<|end_filename|>
package ui

import (
    "github.com/anhoder/netease-music/service"
    "go-musicfox/ds"
    "go-musicfox/utils"
)

type DjTodayRecommendMenu struct {
    menus  []MenuItem
    radios []ds.DjRadio
}

func NewDjTodayRecommendMenu() *DjTodayRecommendMenu {
    return &DjTodayRecommendMenu{}
}

func (m *DjTodayRecommendMenu) MenuData() interface{} {
    return nil
}

func (m *DjTodayRecommendMenu) BeforeBackMenuHook() Hook {
    return nil
}

func (m *DjTodayRecommendMenu) IsPlayable() bool {
    return false
}

func (m *DjTodayRecommendMenu) ResetPlaylistWhenPlay() bool {
    return false
}

func (m *DjTodayRecommendMenu) GetMenuKey() string {
    return "dj_today_recommend"
}

func (m *DjTodayRecommendMenu) MenuViews() []MenuItem {
    return m.menus
}

func (m *DjTodayRecommendMenu) SubMenu(_ *NeteaseModel, index int) IMenu {
    if index >= len(m.radios) {
        return nil
    }

    return NewDjRadioDetailMenu(m.radios[index].Id)
}

func (m *DjTodayRecommendMenu) BeforePrePageHook() Hook {
    // Nothing to do
    return nil
}

func (m *DjTodayRecommendMenu) BeforeNextPageHook() Hook {
    // Nothing to do
    return nil
}

func (m *DjTodayRecommendMenu) BeforeEnterMenuHook() Hook {
    return func(model *NeteaseModel) bool {

        // 不重复请求
        if len(m.menus) > 0 && len(m.radios) > 0 {
            return true
        }

        djTodayService := service.DjTodayPerferedService{}
        code, response := djTodayService.DjTodayPerfered()
        codeType := utils.CheckCode(code)
        if codeType != utils.Success {
            return false
        }

        m.radios = utils.GetDjRadiosOfToday(response)
        m.menus = GetViewFromDjRadios(m.radios)

        return true
    }
}

func (m *DjTodayRecommendMenu) BottomOutHook() Hook {
    return func(model *NeteaseModel) bool {

        djTodayService := service.DjTodayPerferedService{}
        code, response := djTodayService.DjTodayPerfered()
        codeType := utils.CheckCode(code)
        if codeType != utils.Success {
            return false
        }

        radios := utils.GetDjRadiosOfToday(response)
        menus := GetViewFromDjRadios(radios)

        m.radios = append(m.radios, radios...)
        m.menus = append(m.menus, menus...)

        return true
    }
}

func (m *DjTodayRecommendMenu) TopOutHook() Hook {
    // Nothing to do
    return nil
}


<|start_filename|>config/registry.go<|end_filename|>
package config

import (
    "github.com/gookit/ini/v2"
    "go-musicfox/constants"
    "time"
)

var ConfigRegistry *Registry

type Registry struct {
    StartupShow              bool          // 显示启动页
    StartupProgressOutBounce bool          // 是否启动页进度条回弹效果
    StartupLoadingDuration   time.Duration // 启动页加载时长
    StartupWelcome           string        // 启动页欢迎语
    StartupSignIn            bool          // 每天启动时自动签到
    StartupCheckUpdate       bool          // 启动检查更新

    ProgressFullChar  rune // 进度条已加载字符
    ProgressEmptyChar rune // 进度条未加载字符

    MainShowTitle    bool   // 主界面是否显示标题
    MainLoadingText  string // 主页面加载中提示
    MainPlayerSongBr int64  // 歌曲br设置
    MainPrimaryColor string // 主题色
    MainShowLyric    bool   // 显示歌词
    MainShowNotify   bool   // 显示通知
}

func NewRegistryWithDefault() *Registry {
    registry := &Registry{
        StartupShow:              constants.AppShowStartup,
        StartupProgressOutBounce: constants.StartupProgressOutBounce,
        StartupLoadingDuration:   time.Second * constants.StartupLoadingSeconds,
        StartupWelcome:           constants.AppName,
        StartupSignIn:            constants.StartupSignIn,
        StartupCheckUpdate:       constants.StartupCheckUpdate,

        ProgressFullChar:  rune(constants.ProgressFullChar[0]),
        ProgressEmptyChar: rune(constants.ProgressEmptyChar[0]),

        MainShowTitle:    constants.MainShowTitle,
        MainLoadingText:  constants.MainLoadingText,
        MainPlayerSongBr: constants.PlayerSongBr,
        MainPrimaryColor: constants.AppPrimaryRandom,
        MainShowLyric:    constants.MainShowLyric,
        MainShowNotify:   constants.MainShowNotify,
    }

    return registry
}

func NewRegistryFromIniFile(filepath string) *Registry {
    registry := NewRegistryWithDefault()

    if err := ini.LoadExists(filepath); err != nil {
        return registry
    }

    registry.StartupShow = ini.Bool("startup.show", constants.AppShowStartup)
    registry.StartupProgressOutBounce = ini.Bool("startup.progressOutBounce", constants.AppShowStartup)
    registry.StartupLoadingDuration = time.Second * time.Duration(ini.Int("startup.loadingSeconds", constants.StartupLoadingSeconds))
    registry.StartupWelcome = ini.String("startup.welcome", constants.AppName)
    registry.StartupSignIn = ini.Bool("startup.signIn", constants.StartupSignIn)
    registry.StartupCheckUpdate = ini.Bool("startup.checkUpdate", constants.StartupCheckUpdate)

    fullChar := ini.String("progress.fullChar", constants.ProgressFullChar)
    if len(fullChar) > 0 {
        registry.ProgressFullChar = rune(fullChar[0])
    } else {
        registry.ProgressFullChar = rune(constants.ProgressFullChar[0])
    }
    emptyChar := ini.String("progress.emptyChar", constants.ProgressEmptyChar)
    if len(emptyChar) > 0 {
        registry.ProgressEmptyChar = rune(emptyChar[0])
    } else {
        registry.ProgressEmptyChar = rune(constants.ProgressEmptyChar[0])
    }

    registry.MainShowTitle = ini.Bool("main.showTitle", constants.MainShowTitle)
    registry.MainLoadingText = ini.String("main.loadingText", constants.MainLoadingText)
    registry.MainPlayerSongBr = ini.Int64("main.songBr", constants.PlayerSongBr)
    primaryColor := ini.String("main.primaryColor", constants.AppPrimaryRandom)
    if primaryColor != "" {
        registry.MainPrimaryColor = primaryColor
    } else {
        registry.MainPrimaryColor = constants.AppPrimaryRandom
    }
    registry.MainShowLyric = ini.Bool("main.showLyric", constants.MainShowLyric)
    registry.MainShowNotify = ini.Bool("main.showNotify", constants.MainShowNotify)

    return registry
}


<|start_filename|>utils/player.go<|end_filename|>
package utils

import (
    "github.com/faiface/beep"
    "github.com/faiface/beep/effects"
    "github.com/faiface/beep/flac"
    "github.com/faiface/beep/mp3"
    "github.com/faiface/beep/speaker"
    "github.com/faiface/beep/vorbis"
    "github.com/faiface/beep/wav"
    "net/http"
    "time"
)

// State 播放器状态
type State uint8

const (
    Stopped State = iota
    Paused
    Playing
)

// SongType 歌曲类型
type SongType uint8

const (
    Mp3 SongType = iota
    Wav
    Ogg
    Flac
)

type UrlMusic struct {
    Url      string
    Type     SongType
    Duration time.Duration
}

type Player struct {
    State    State
    Progress int
    CurMusic UrlMusic
    ctrl     *beep.Ctrl
    volume   *effects.Volume
    Timer    *Timer
    timeChan chan time.Duration
    done     chan struct{}

    musicChan chan UrlMusic
}

func NewPlayer() *Player {
    player := new(Player)
    player.timeChan = make(chan time.Duration)
    player.done = make(chan struct{})
    player.musicChan = make(chan UrlMusic)
    player.ctrl = &beep.Ctrl{
        Paused: false,
    }
    player.volume = &effects.Volume{
        Base:   2,
        Silent: false,
    }

    go func() {
        player.listen()
    }()

    return player
}

// listen 开始监听
func (p *Player) listen() {
    var sampleRate beep.SampleRate = 44100
    err := speaker.Init(sampleRate, sampleRate.N(time.Millisecond*200))
    if err != nil {
        panic(err)
    }
    done := make(chan bool)

    var (
        streamer, oldStreamer beep.StreamSeekCloser
    )

    for {
        select {
        case <-done:
            p.State = Stopped
            p.pushDone()
            break
        case p.CurMusic = <-p.musicChan:
            var (
                resp   *http.Response
                format beep.Format
            )

            speaker.Clear()

            // 关闭旧响应body
            if resp != nil {
                _ = resp.Body.Close()
            }

            // 关闭旧计时器
            if p.Timer != nil {
                p.Timer.Stop()
            }
            p.Progress = 0

            resp, err = http.Get(p.CurMusic.Url)
            if err != nil {
                p.pushDone()
                break
            }

            oldStreamer = streamer
            switch p.CurMusic.Type {
            case Mp3:
                streamer, format, err = mp3.Decode(resp.Body)
            case Wav:
                streamer, format, err = wav.Decode(resp.Body)
            case Ogg:
                streamer, format, err = vorbis.Decode(resp.Body)
            case Flac:
                streamer, format, err = flac.Decode(resp.Body)
            default:
                p.pushDone()
                break
            }
            if err != nil {
                p.pushDone()
                break
            }

            p.State = Playing
            newStreamer := beep.Resample(3, format.SampleRate, sampleRate, streamer)
            p.ctrl.Streamer = beep.Seq(newStreamer, beep.Callback(func() {
                done <- true
            }))
            p.volume.Streamer = p.ctrl
            p.ctrl.Paused = false
            speaker.Play(p.volume)

            // 启动计时器
            p.Timer = New(Options{
                Duration:       24 * time.Hour,
                TickerInternal: 200 * time.Millisecond,
                OnRun:          func(started bool) {},
                OnPaused:       func() {},
                OnDone:         func(stopped bool) {},
                OnTick: func() {
                    if p.CurMusic.Duration > 0 {
                        p.Progress = int(p.Timer.Passed() * 100 / p.CurMusic.Duration)
                    }
                    select {
                    case p.timeChan <- p.Timer.Passed():
                    default:
                    }
                },
            })
            go p.Timer.Run()

            // 关闭旧Streamer，避免协程泄漏
            if oldStreamer != nil {
                _ = oldStreamer.Close()
            }
        }

    }

}

// Play 播放音乐
func (p *Player) Play(songType SongType, url string, duration time.Duration) {
    music := UrlMusic{
        url,
        songType,
        duration,
    }
    select {
    case p.musicChan <- music:
    default:
    }
}

func (p *Player) pushDone() {
    select {
    case p.done <- struct{}{}:
    default:
    }
}

// TimeChan 获取定时器
func (p *Player) TimeChan() <-chan time.Duration {
    return p.timeChan
}

// Done done chan, 如果播放完成往chan中写入struct{}
func (p *Player) Done() <-chan struct{} {
    return p.done
}

// UpVolume 调大音量
func (p *Player) UpVolume() {
    if p.volume.Volume > 0 {
        return
    }

    speaker.Lock()
    p.volume.Silent = false
    p.volume.Volume += 0.5
    speaker.Unlock()
}

// DownVolume 调小音量
func (p *Player) DownVolume() {
    if p.volume.Volume <= -5 {
        speaker.Lock()
        p.volume.Silent = true
        speaker.Unlock()
        return
    }

    speaker.Lock()
    p.volume.Volume -= 0.5
    speaker.Unlock()
}

// Paused 暂停播放
func (p *Player) Paused() {
    if p.State == Paused {
        return
    }

    speaker.Lock()
    p.ctrl.Paused = true
    speaker.Unlock()
    p.State = Paused
    p.Timer.Pause()
}

// Resume 继续播放
func (p *Player) Resume() {
    if p.State == Playing {
        return
    }

    speaker.Lock()
    p.ctrl.Paused = false
    speaker.Unlock()
    p.State = Playing
    go p.Timer.Run()
}

// Close 关闭
func (p *Player) Close() {
    p.Timer.Stop()
    speaker.Clear()
}


<|start_filename|>utils/stack.go<|end_filename|>
package utils

type SNode struct {
	value interface{}
	pre   *SNode
	next  *SNode
}

type Stack struct {
	tail *SNode
	len  int
}

func (s *Stack) Len() int {
	return s.len
}

func (s *Stack) Peek() interface{} {
	if s.tail == nil {
		return nil
	}
	return s.tail.value
}

func (s *Stack) Push(value interface{}) {
	newNode := &SNode{value, nil, nil}
	if s.tail == nil {
		s.tail = newNode
	} else {
		newNode.pre = s.tail
		s.tail.next = newNode
		s.tail = newNode
	}
	s.len++
	newNode = nil
}

func (s *Stack) Pop() interface{} {
	if s.tail == nil {
		return nil
	}
	last := s.tail
	value := last.value

	s.tail = last.pre
	last.pre = nil
	last.next = nil
	last.value = nil
	s.len--
	last = nil

	return value
}

<|start_filename|>lyric/lrc_timer.go<|end_filename|>
package lyric

import "time"

type Listener func(startTimeMs int64, content string, last bool, index int)

type LRCTimer struct {
	file      *LRCFile
	timer     chan time.Duration
	stop      chan struct{}
	listeners []Listener
}

func NewLRCTimer(file *LRCFile) (timer *LRCTimer) {
	timer = &LRCTimer{
		file: file,
		timer: make(chan time.Duration),
	}
	return
}

func (t *LRCTimer) Timer() chan<- time.Duration {
	return t.timer
}

func (t *LRCTimer) AddListener(l Listener) {
	t.listeners = append(t.listeners, l)
}

func (t *LRCTimer) Start() {
	fragments := t.file.fragments

	if len(fragments) < 1 {
		return
	}

	currentIdx := 0
	current := fragments[0]
	t.stop = make(chan struct{})
	for {
		select {
		case duration := <-t.timer:
			if duration < time.Duration(fragments[currentIdx].StartTimeMs) * time.Millisecond {
				continue
			}

			last := currentIdx >= len(fragments)-1

			for _, l := range t.listeners {
				go l(current.StartTimeMs, current.Content, last, currentIdx)
			}

			if last {
				break
			}

			currentIdx++
			current = fragments[currentIdx]
		case <- t.stop:
			return
		}

	}
}

func (t LRCTimer) IsStarted() bool {
	return t.timer != nil
}

func (t LRCTimer) Stop() {
	t.stop <- struct{}{}
	t.timer = nil
}

func (t LRCTimer) GetLRCFragment(index int) *LRCFragment {
	if nil == t.file || index >= len(t.file.fragments) || index < 0 {
		return nil
	}

	return &t.file.fragments[index]
}

func (t LRCTimer) IsEmpty() bool {
	return nil == t.file || len(t.file.fragments) == 0
}

<|start_filename|>ui/startup.go<|end_filename|>
package ui

import (
    "fmt"
    tea "github.com/anhoder/bubbletea"
    "github.com/fogleman/ease"
    "github.com/muesli/termenv"
    "go-musicfox/config"
    "go-musicfox/constants"
    "go-musicfox/utils"
    "math"
    "strings"
    "time"
    "unicode/utf8"
)

var (
    progressRamp       []string
    progressLastWidth  float64
    progressStartColor string
    progressEndColor   string
)

type startupModel struct {
    TotalDuration  time.Duration
    loadedDuration time.Duration
    loadedPercent  float64
    loaded         bool
    quitting       bool
}

func NewStartup() (m *startupModel) {
    m = new(startupModel)
    m.TotalDuration = time.Second * 2 // 默认

    return
}

// startup func
func updateStartup(msg tea.Msg, m *NeteaseModel) (tea.Model, tea.Cmd) {
    switch msg.(type) {

    case tickStartupMsg:
        if m.loadedDuration >= m.TotalDuration {
            m.loaded = true
            m.isListeningKey = true
            return m, tea.ClearScreen
        }
        m.loadedDuration += constants.StartupTickDuration
        m.loadedPercent = float64(m.loadedDuration) / float64(m.TotalDuration)
        if config.ConfigRegistry.StartupProgressOutBounce {
            m.loadedPercent = ease.OutBounce(m.loadedPercent)
        }
        return m, tickStartup(constants.StartupTickDuration)

    case tea.ClearScreenMsg:
        return m, tickStartup(time.Nanosecond)
    }

    return m, nil
}

// startup view
func startupView(m *NeteaseModel) string {

    if m.WindowWidth <= 0 || m.WindowHeight <= 0 {
        return ""
    }

    blankLine := 1
    tipsHeight := 1
    progressHeight := 1
    height := utils.AsciiHeight + blankLine + tipsHeight + blankLine + progressHeight
    var top, bottom int
    if m.WindowHeight - height > 0 {
        top = (m.WindowHeight - height) / 2
    }
    if m.WindowHeight - top - height > 0 {
        bottom = m.WindowHeight - top - height
    }

    var uiBuilder strings.Builder
    if top > 1 {
        uiBuilder.WriteString(strings.Repeat("\n", top-1))
    }
    uiBuilder.WriteString(logoView(m))
    uiBuilder.WriteString("\n")
    if top != 0 && bottom != 0 {
        uiBuilder.WriteString("\n")
    }
    uiBuilder.WriteString(tipsView(m))
    uiBuilder.WriteString("\n")
    if top != 0 && bottom != 0 {
        uiBuilder.WriteString("\n")
    }
    uiBuilder.WriteString(progressView(m))
    uiBuilder.WriteString(strings.Repeat("\n", bottom))

    return uiBuilder.String()
}

// get logo
func logoView(m *NeteaseModel) string {
    if m.WindowWidth <= 0 || m.WindowHeight <= 0 {
        return ""
    }

    originLogo := utils.GetAlphaAscii(config.ConfigRegistry.StartupWelcome)
    var logoWidth int
    if logoArr := strings.Split(originLogo, "\n"); len(logoArr) > 1 {
        logoWidth = utf8.RuneCountInString(logoArr[1])
    }

    var left int
    if m.WindowWidth - logoWidth > 0 {
        left = (m.WindowWidth - logoWidth) / 2
    }

    var logoBuilder strings.Builder
    leftSpace := strings.Repeat(" ", left)
    lines := strings.Split(originLogo, "\n")
    for _, line := range lines {
        logoBuilder.WriteString(leftSpace)
        logoBuilder.WriteString(line)
        logoBuilder.WriteString("\n")
    }
    return SetFgStyle(logoBuilder.String(), GetPrimaryColor())
}

// get tips
func tipsView(m *NeteaseModel) string {
    example := "Enter after 11.1 seconds..."
    var left int
    if m.WindowWidth - len(example) > 0 {
        left = (m.WindowWidth - len(example)) / 2
    }
    tips := fmt.Sprintf("%sEnter after %.1f seconds...",
        strings.Repeat(" ", left),
        float64(m.TotalDuration - m.loadedDuration)/float64(time.Second))

    return SetFgStyle(tips, termenv.ANSIBrightBlack)
}

// get progress
func progressView(m *NeteaseModel) string {
    width := float64(m.WindowWidth)

    if progressStartColor == "" || progressEndColor == "" {
        progressStartColor, progressEndColor = GetRandomRgbColor(true)
    }
    if width != progressLastWidth {
        progressRamp = makeRamp(progressStartColor, progressEndColor, width)
        progressLastWidth = width
    }

    fullSize := int(math.Round(width*m.loadedPercent))
    var fullCells string
    for i := 0; i < fullSize && i < len(progressRamp); i++ {
        fullCells += termenv.String(string(config.ConfigRegistry.ProgressFullChar)).Foreground(termProfile.Color(progressRamp[i])).String()
    }

    emptySize := 0
    if int(width) - fullSize > 0 {
        emptySize = int(width) - fullSize
    }
    emptyCells := strings.Repeat(string(config.ConfigRegistry.ProgressEmptyChar), emptySize)

    return fmt.Sprintf("%s%s", fullCells, emptyCells)
}

<|start_filename|>ui/ui_helper.go<|end_filename|>
package ui

import (
    "fmt"
    "github.com/lucasb-eyer/go-colorful"
    "github.com/muesli/termenv"
    "go-musicfox/config"
    "go-musicfox/constants"
    "go-musicfox/ds"
    "go-musicfox/utils"
    "math/rand"
    "strconv"
    "strings"
    "time"
)

var (
    termProfile     = termenv.ColorProfile()
    primaryColor    termenv.Color
    primaryColorStr string
)

// GetPrimaryColor get random color
func GetPrimaryColor() termenv.Color {
    if primaryColor != nil {
        return primaryColor
    }

    if config.ConfigRegistry.MainPrimaryColor == "" || config.ConfigRegistry.MainPrimaryColor == constants.AppPrimaryRandom {
        rand.Seed(time.Now().UnixNano())
        primaryColorStr = strconv.Itoa(rand.Intn(228-17) + 17)
    } else {
        primaryColorStr = config.ConfigRegistry.MainPrimaryColor
    }
    primaryColor = termProfile.Color(primaryColorStr)

    return primaryColor
}

// GetPrimaryColorStr get random color string
func GetPrimaryColorStr() string {
    if primaryColorStr != "" {
        return primaryColorStr
    }
    if config.ConfigRegistry.MainPrimaryColor == "" || config.ConfigRegistry.MainPrimaryColor == constants.AppPrimaryRandom {
        rand.Seed(time.Now().UnixNano())
        primaryColorStr = strconv.Itoa(rand.Intn(228-17) + 17)
    } else {
        primaryColorStr = config.ConfigRegistry.MainPrimaryColor
    }
    primaryColor = termProfile.Color(primaryColorStr)

    return primaryColorStr
}

// GetRandomRgbColor get random rgb color
func GetRandomRgbColor(isRange bool) (string, string) {
    rand.Seed(time.Now().UnixNano())
    r := 255 - rand.Intn(100)
    rand.Seed(time.Now().UnixNano() / 2)
    g := 255 - rand.Intn(100)
    rand.Seed(time.Now().UnixNano() / 3)
    b := 255 - rand.Intn(100)

    startColor := fmt.Sprintf("#%x%x%x", r, g, b)
    if !isRange {
        return startColor, ""
    }

    rand.Seed(time.Now().UnixNano() / 5)
    rEnd := 50 + rand.Intn(100)
    rand.Seed(time.Now().UnixNano() / 7)
    gEnd := 50 + rand.Intn(100)
    rand.Seed(time.Now().UnixNano() / 11)
    bEnd := 50 + rand.Intn(100)
    endColor := fmt.Sprintf("#%x%x%x", rEnd, gEnd, bEnd)

    return startColor, endColor
}

// SetFgStyle Return a function that will colorize the foreground of a given string.
func SetFgStyle(content string, color termenv.Color) string {
    return termenv.Style{}.Foreground(color).Styled(content)
}

// SetFgBgStyle Color a string's foreground and background with the given value.
func SetFgBgStyle(content string, fg, bg termenv.Color) string {
    return termenv.Style{}.Foreground(fg).Background(bg).Styled(content)
}

// SetNormalStyle dont set any style
func SetNormalStyle(content string) string {
    seq := strings.Join([]string{"0"}, ";")
    return fmt.Sprintf("%s%sm%s%sm", termenv.CSI, seq, content, termenv.CSI+termenv.ResetSeq)
}

// Generate a blend of colors.
func makeRamp(colorA, colorB string, steps float64) (s []string) {
    cA, _ := colorful.Hex(colorA)
    cB, _ := colorful.Hex(colorB)

    for i := 0.0; i < steps; i++ {
        c := cA.BlendLuv(cB, i/steps)
        s = append(s, colorToHex(c))
    }
    return
}

// Convert a colorful.Color to a hexidecimal format compatible with termenv.
func colorToHex(c colorful.Color) string {
    return fmt.Sprintf("#%s%s%s", colorFloatToHex(c.R), colorFloatToHex(c.G), colorFloatToHex(c.B))
}

// Helper function for converting colors to hex. Assumes a value between 0 and 1.
func colorFloatToHex(f float64) (s string) {
    s = strconv.FormatInt(int64(f*255), 16)
    if len(s) == 1 {
        s = "0" + s
    }
    return
}

// GetViewFromSongs 从歌曲列表获取View
func GetViewFromSongs(songs []ds.Song) []MenuItem {
    var menus []MenuItem
    for _, song := range songs {
        var artists []string
        for _, artist := range song.Artists {
            artists = append(artists, artist.Name)
        }
        menus = append(menus, MenuItem{utils.ReplaceSpecialStr(song.Name), utils.ReplaceSpecialStr(strings.Join(artists, ","))})
    }

    return menus
}

// GetViewFromAlbums 从歌曲列表获取View
func GetViewFromAlbums(albums []ds.Album) []MenuItem {
    var menus []MenuItem
    for _, album := range albums {
        var artists []string
        for _, artist := range album.Artists {
            artists = append(artists, artist.Name)
        }
        artistsStr := fmt.Sprintf("[%s]", strings.Join(artists, ","))
        menus = append(menus, MenuItem{utils.ReplaceSpecialStr(album.Name), utils.ReplaceSpecialStr(artistsStr)})
    }

    return menus
}

// GetViewFromPlaylists 从歌单列表获取View
func GetViewFromPlaylists(playlists []ds.Playlist) []MenuItem {
    var menus []MenuItem
    for _, playlist := range playlists {
        menus = append(menus, MenuItem{utils.ReplaceSpecialStr(playlist.Name), ""})
    }

    return menus
}

// GetViewFromArtists 从歌手列表获取View
func GetViewFromArtists(artists []ds.Artist) []MenuItem {
    var menus []MenuItem
    for _, artist := range artists {
        menus = append(menus, MenuItem{utils.ReplaceSpecialStr(artist.Name), ""})
    }

    return menus
}

// GetViewFromUsers 用户列表获取View
func GetViewFromUsers(users []ds.User) []MenuItem {
    var menus []MenuItem
    for _, user := range users {
        menus = append(menus, MenuItem{utils.ReplaceSpecialStr(user.Nickname), ""})
    }

    return menus
}

// GetViewFromDjRadios DjRadio列表获取View
func GetViewFromDjRadios(radios []ds.DjRadio) []MenuItem {
    var menus []MenuItem
    for _, radio := range radios {
        var dj string
        if radio.Dj.Nickname != "" {
            dj = fmt.Sprintf("[%s]", radio.Dj.Nickname)
        }
        menus = append(menus, MenuItem{utils.ReplaceSpecialStr(radio.Name), utils.ReplaceSpecialStr(dj)})
    }

    return menus
}

// GetViewFromDjCate 分类列表获取View
func GetViewFromDjCate(categories []ds.DjCategory) []MenuItem {
    var menus []MenuItem
    for _, category := range categories {
        menus = append(menus, MenuItem{utils.ReplaceSpecialStr(category.Name), ""})
    }

    return menus
}


<|start_filename|>constants/constants.go<|end_filename|>
package constants

import (
    "time"
)

const AppName = "musicfox"
const AppVersion = "2.1.0"
const AppVersionInt = 20100
const AppDescription = "<cyan>Musicfox - 命令行版网易云音乐</>"
const AppShowStartup = true
const AppGithubUrl = "https://github.com/anhoder/go-musicfox"
const AppLatestReleases = "https://github.com/anhoder/go-musicfox/releases/latest"
const AppCheckUpdateUrl = "https://api.github.com/repos/anhoder/go-musicfox/releases/latest"
const ProgressFullChar = "#"
const ProgressEmptyChar = " "
const StartupProgressOutBounce = true
const StartupLoadingSeconds = 2
const StartupTickDuration = time.Millisecond * 16
const StartupSignIn = true
const StartupCheckUpdate = true

const AppLocalDataDir = ".go-musicfox"
const AppDBName = "musicfox"
const AppIniFile = "go-musicfox.ini"
const AppPrimaryRandom = "random"

const PlayerSongBr = 320000 // 999000

const MainShowTitle = true
const MainLoadingText = "[加载中...]"
const MainShowLyric = true
const MainShowNotify = true

const AppHelpTemplate = `%s

{{.Description}} (Version: <info>{{.Version}}</>)

<comment>Usage:</>
  {$binName} [Global Options...] <info>{command}</> [--option ...] [argument ...]

<comment>Global Options:</>
{{.GOpts}}
<comment>Available Commands:</>{{range $module, $cs := .Cs}}{{if $module}}
<comment> {{ $module }}</>{{end}}{{ range $cs }}
  <info>{{.Name | paddingName }}</> {{.UseFor}}{{if .Aliases}} (alias: <cyan>{{ join .Aliases ","}}</>){{end}}{{end}}{{end}}

  <info>{{ paddingName "help" }}</> Display help information

Use "<cyan>{$binName} {COMMAND} -h</>" for more information about a command
`


<|start_filename|>ui/menu_album_top.go<|end_filename|>
package ui

import (
	"fmt"
	"github.com/anhoder/netease-music/service"
	"github.com/buger/jsonparser"
	"go-musicfox/ds"
	"go-musicfox/utils"
	"strconv"
	"strings"
)

type AlbumTopMenu struct {
	menus   []MenuItem
	albums  []ds.Album
	area    string
	offset  int
	limit   int
	hasMore bool
}

func NewAlbumTopMenu(area string) *AlbumTopMenu {
	return &AlbumTopMenu{
		area:   area,
		offset: 0,
		limit:  50,
	}
}

func (m *AlbumTopMenu) MenuData() interface{} {
	return m.albums
}

func (m *AlbumTopMenu) BeforeBackMenuHook() Hook {
	return nil
}

func (m *AlbumTopMenu) IsPlayable() bool {
	return false
}

func (m *AlbumTopMenu) ResetPlaylistWhenPlay() bool {
	return false
}

func (m *AlbumTopMenu) GetMenuKey() string {
	return fmt.Sprintf("album_top_%s", m.area)
}

func (m *AlbumTopMenu) MenuViews() []MenuItem {
	return m.menus
}

func (m *AlbumTopMenu) SubMenu(_ *NeteaseModel, index int) IMenu {
	if len(m.albums) < index {
		return nil
	}

	return NewAlbumDetailMenu(m.albums[index].Id)
}

func (m *AlbumTopMenu) BeforePrePageHook() Hook {
	// Nothing to do
	return nil
}

func (m *AlbumTopMenu) BeforeNextPageHook() Hook {
	// Nothing to do
	return nil
}

func (m *AlbumTopMenu) BeforeEnterMenuHook() Hook {
	return func(model *NeteaseModel) bool {

		if len(m.menus) > 0 && len(m.albums) > 0 {
			return true
		}

		topAlbumService := service.TopAlbumService{
			Area:   m.area,
			Limit:  strconv.Itoa(m.limit),
			Offset: strconv.Itoa(m.offset),
		}
		code, response := topAlbumService.TopAlbum()
		codeType := utils.CheckCode(code)
		if codeType != utils.Success {
			return false
		}

		// 是否有更多数据
		if hasMore, err := jsonparser.GetBoolean(response, "hasMore"); err == nil {
			m.hasMore = hasMore
		}

		m.albums = utils.GetTopAlbums(response)

		for _, album := range m.albums {
			var artists []string
			for _, artist := range album.Artists {
				artists = append(artists, artist.Name)
			}
			artistsStr := fmt.Sprintf("[%s]", strings.Join(artists, ","))
			m.menus = append(m.menus, MenuItem{utils.ReplaceSpecialStr(album.Name), utils.ReplaceSpecialStr(artistsStr)})
		}

		return true
	}
}

func (m *AlbumTopMenu) BottomOutHook() Hook {
	if !m.hasMore {
		return nil
	}
	return func(model *NeteaseModel) bool {
		m.offset = m.offset + len(m.menus)
		topAlbumService := service.TopAlbumService{
			Area:   m.area,
			Limit:  strconv.Itoa(m.limit),
			Offset: strconv.Itoa(m.offset),
		}
		code, response := topAlbumService.TopAlbum()
		codeType := utils.CheckCode(code)
		if codeType != utils.Success {
			return false
		}

		// 是否有更多数据
		if hasMore, err := jsonparser.GetBoolean(response, "hasMore"); err == nil {
			m.hasMore = hasMore
		}

		albums := utils.GetTopAlbums(response)

		for _, album := range albums {
			var artists []string
			for _, artist := range album.Artists {
				artists = append(artists, artist.Name)
			}
			artistsStr := fmt.Sprintf("[%s]", strings.Join(artists, ","))
			m.menus = append(m.menus, MenuItem{utils.ReplaceSpecialStr(album.Name), utils.ReplaceSpecialStr(artistsStr)})
		}

		m.albums = append(m.albums, albums...)

		return true
	}
}

func (m *AlbumTopMenu) TopOutHook() Hook {
	// Nothing to do
	return nil
}


<|start_filename|>ds/song.go<|end_filename|>
package ds

import (
    "errors"
    "github.com/buger/jsonparser"
    "time"
)

type Song struct {
    Id       int64         `json:"id"`
    Name     string        `json:"name"`
    Duration time.Duration `json:"duration"`
    Artists  []Artist      `json:"artists"`
    Album    `json:"album"`
}

// NewSongFromDailySongsJson 从每日推荐歌曲获取数据
func NewSongFromDailySongsJson(json []byte) (Song, error) {
    var song Song
    if len(json) == 0 {
        return song, errors.New("json is empty")
    }

    id, err := jsonparser.GetInt(json, "id")
    if err != nil {
        return song, err
    }
    song.Id = id

    if name, err := jsonparser.GetString(json, "name"); err == nil {
        song.Name = name
    }
    if duration, err := jsonparser.GetInt(json, "dt"); err == nil {
        song.Duration = time.Millisecond * time.Duration(duration)
    }

    album, err := NewAlbumFromJson(json)
    if err == nil {
        song.Album = album
    }

    _, _ = jsonparser.ArrayEach(json, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        artist, err := NewArtist(value)

        if err == nil {
            song.Artists = append(song.Artists, artist)
        }
    }, "ar")

    return song, nil
}

// NewSongFromPlaylistSongsJson 从歌单获取数据
func NewSongFromPlaylistSongsJson(json []byte) (Song, error) {
    var song Song
    if len(json) == 0 {
        return song, errors.New("json is empty")
    }

    id, err := jsonparser.GetInt(json, "id")
    if err != nil {
        return song, err
    }
    song.Id = id

    if name, err := jsonparser.GetString(json, "name"); err == nil {
        song.Name = name
    }
    if duration, err := jsonparser.GetInt(json, "dt"); err == nil {
        song.Duration = time.Millisecond * time.Duration(duration)
    }
    if alId, err := jsonparser.GetInt(json, "al", "id"); err == nil {
        song.Album.Id = alId
    }
    if alName, err := jsonparser.GetString(json, "al", "name"); err == nil {
        song.Album.Name = alName
    }
    if alPic, err := jsonparser.GetString(json, "al", "picUrl"); err == nil {
        song.Album.PicUrl = alPic
    }

    _, _ = jsonparser.ArrayEach(json, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        artist, err := NewArtist(value)

        if err == nil {
            song.Artists = append(song.Artists, artist)
        }
    }, "ar")

    return song, nil
}

// NewSongFromFmJson 从私人FM获取数据
func NewSongFromFmJson(json []byte) (Song, error) {
    var song Song
    if len(json) == 0 {
        return song, errors.New("json is empty")
    }

    id, err := jsonparser.GetInt(json, "id")
    if err != nil {
        return song, err
    }
    song.Id = id

    if name, err := jsonparser.GetString(json, "name"); err == nil {
        song.Name = name
    }
    if duration, err := jsonparser.GetInt(json, "duration"); err == nil {
        song.Duration = time.Millisecond * time.Duration(duration)
    }
    if alId, err := jsonparser.GetInt(json, "album", "id"); err == nil {
        song.Album.Id = alId
    }
    if alName, err := jsonparser.GetString(json, "album", "name"); err == nil {
        song.Album.Name = alName
    }
    if alPic, err := jsonparser.GetString(json, "album", "picUrl"); err == nil {
        song.Album.PicUrl = alPic
    }

    _, _ = jsonparser.ArrayEach(json, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        artist, err := NewArtist(value)

        if err == nil {
            song.Artists = append(song.Artists, artist)
        }
    }, "artists")

    return song, nil
}

// NewSongFromIntelligenceJson 心动模式获取数据
func NewSongFromIntelligenceJson(json []byte) (Song, error) {
    var song Song
    if len(json) == 0 {
        return song, errors.New("json is empty")
    }

    id, err := jsonparser.GetInt(json, "songInfo", "id")
    if err != nil {
        return song, err
    }
    song.Id = id

    if name, err := jsonparser.GetString(json, "songInfo", "name"); err == nil {
        song.Name = name
    }
    if duration, err := jsonparser.GetInt(json, "songInfo", "dt"); err == nil {
        song.Duration = time.Millisecond * time.Duration(duration)
    }
    if alId, err := jsonparser.GetInt(json, "songInfo", "al", "id"); err == nil {
        song.Album.Id = alId
    }
    if alName, err := jsonparser.GetString(json, "songInfo", "al", "name"); err == nil {
        song.Album.Name = alName
    }
    if alPic, err := jsonparser.GetString(json, "songInfo", "al", "picUrl"); err == nil {
        song.Album.PicUrl = alPic
    }

    _, _ = jsonparser.ArrayEach(json, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        artist, err := NewArtist(value)

        if err == nil {
            song.Artists = append(song.Artists, artist)
        }
    }, "songInfo", "ar")

    return song, nil
}

// NewSongFromAlbumSongsJson 从专辑获取数据
func NewSongFromAlbumSongsJson(json []byte) (Song, error) {
    return NewSongFromPlaylistSongsJson(json)
}

// NewSongFromArtistSongsJson 从专辑获取数据
func NewSongFromArtistSongsJson(json []byte) (Song, error) {
    return NewSongFromPlaylistSongsJson(json)
}

// NewSongFromSearchResultJson 从专辑获取数据
func NewSongFromSearchResultJson(json []byte) (Song, error) {
    return NewSongFromFmJson(json)
}

// NewSongFromDjRadioProgramJson 从DjRadio节目中获取数据
func NewSongFromDjRadioProgramJson(json []byte) (Song, error) {
    var song Song
    if len(json) == 0 {
        return song, errors.New("json is empty")
    }

    id, err := jsonparser.GetInt(json, "mainSong", "id")
    if err != nil {
        return song, err
    }
    song.Id = id

    if name, err := jsonparser.GetString(json, "mainSong", "name"); err == nil {
        song.Name = name
    }
    if duration, err := jsonparser.GetInt(json, "mainSong", "duration"); err == nil {
        song.Duration = time.Millisecond * time.Duration(duration)
    }
    if alId, err := jsonparser.GetInt(json, "mainSong", "album", "id"); err == nil {
        song.Album.Id = alId
    }
    if alName, err := jsonparser.GetString(json, "mainSong", "album", "name"); err == nil {
        song.Album.Name = alName
    }
    if alPic, err := jsonparser.GetString(json, "mainSong", "album", "picUrl"); err == nil {
        song.Album.PicUrl = alPic
    }

    var artist Artist
    if arName, err := jsonparser.GetString(json, "dj", "nickname"); err == nil {
        artist.Name = arName
    }
    song.Artists = append(song.Artists, artist)


    return song, nil
}

// NewSongFromCloudJson 从DjRadio节目中获取数据
func NewSongFromCloudJson(json []byte) (Song, error) {
    var song Song
    if len(json) == 0 {
        return song, errors.New("json is empty")
    }

    id, err := jsonparser.GetInt(json, "songId")
    if err != nil {
        return song, err
    }
    song.Id = id

    if name, err := jsonparser.GetString(json, "songName"); err == nil {
        song.Name = name
    }
    if duration, err := jsonparser.GetInt(json, "simpleSong", "dt"); err == nil {
        song.Duration = time.Millisecond * time.Duration(duration)
    }
    if alId, err := jsonparser.GetInt(json, "simpleSong", "al", "id"); err == nil {
        song.Album.Id = alId
    }
    if alName, err := jsonparser.GetString(json, "simpleSong", "al", "name"); err == nil {
        song.Album.Name = alName
    }
    if alPic, err := jsonparser.GetString(json, "simpleSong", "al", "picUrl"); err == nil {
        song.Album.PicUrl = alPic
    }

    _, _ = jsonparser.ArrayEach(json, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        artist, err := NewArtist(value)

        if err == nil {
            song.Artists = append(song.Artists, artist)
        }
    }, "simpleSong", "ar")

    return song, nil
}

// NewSongFromDjRankProgramJson 从DjRadio节目中获取数据
func NewSongFromDjRankProgramJson(json []byte) (Song, error) {
    var song Song
    if len(json) == 0 {
        return song, errors.New("json is empty")
    }

    id, err := jsonparser.GetInt(json, "program", "mainSong", "id")
    if err != nil {
        return song, err
    }
    song.Id = id

    if name, err := jsonparser.GetString(json, "program", "mainSong", "name"); err == nil {
        song.Name = name
    }
    if duration, err := jsonparser.GetInt(json, "program", "mainSong", "duration"); err == nil {
        song.Duration = time.Millisecond * time.Duration(duration)
    }
    if alId, err := jsonparser.GetInt(json, "program", "mainSong", "album", "id"); err == nil {
        song.Album.Id = alId
    }
    if alName, err := jsonparser.GetString(json, "program", "mainSong", "album", "name"); err == nil {
        song.Album.Name = alName
    }
    if alPic, err := jsonparser.GetString(json, "program", "mainSong", "album", "picUrl"); err == nil {
        song.Album.PicUrl = alPic
    }

    _, _ = jsonparser.ArrayEach(json, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
        artist, err := NewArtist(value)

        if err == nil {
            song.Artists = append(song.Artists, artist)
        }
    }, "program", "mainSong", "artists")

    return song, nil
}

<|start_filename|>ui/input.go<|end_filename|>
package ui

import (
    "fmt"
    "github.com/muesli/termenv"
)

var (
    focusedPrompt,
    blurredPrompt,
    focusedSubmitButton,
    blurredSubmitButton string
)

func GetFocusedPrompt() string {
    if focusedPrompt != "" {
        return focusedPrompt
    }

    focusedPrompt = termenv.String("> ").Foreground(GetPrimaryColor()).String()

    return focusedPrompt
}

func GetBlurredPrompt() string {
    if blurredPrompt != "" {
        return blurredPrompt
    }

    blurredPrompt = "> "

    return blurredPrompt
}

func GetFocusedSubmitButton() string {
    if focusedSubmitButton != "" {
        return focusedSubmitButton
    }

    focusedSubmitButton = fmt.Sprintf("[ %s ]", termenv.String("确认").Foreground(GetPrimaryColor()).String())

    return focusedSubmitButton
}

func GetBlurredSubmitButton() string {
    if blurredSubmitButton != "" {
        return blurredSubmitButton
    }

    blurredSubmitButton = fmt.Sprintf("[ %s ]", termenv.String("确认").Foreground(termenv.ANSIBrightBlack).String())

    return blurredSubmitButton
}

<|start_filename|>ui/search.go<|end_filename|>
package ui

import (
    "github.com/anhoder/bubbles/textinput"
    tea "github.com/anhoder/bubbletea"
    "github.com/anhoder/netease-music/service"
    "github.com/mattn/go-runewidth"
    "github.com/muesli/termenv"
    "go-musicfox/config"
    "go-musicfox/utils"
    "strconv"
    "strings"
    "time"
)

type SearchType uint32

const (
    StNull       SearchType = 0
    StSingleSong SearchType = 1
    StAlbum      SearchType = 10
    StSinger     SearchType = 100
    StPlaylist   SearchType = 1000
    StUser       SearchType = 1002
    StLyric      SearchType = 1006
    StRadio      SearchType = 1009
)

type SearchModel struct {
    index        int
    wordsInput   textinput.Model
    submitButton string
    tips         string
    searchType   SearchType
    result       interface{}
}

func (s *SearchModel) Reset() {
    s.tips = ""
    s.wordsInput.SetValue("")
    s.wordsInput.Focus()
}

func NewSearch() (search *SearchModel) {
    search = new(SearchModel)
    search.wordsInput = textinput.NewModel()
    search.wordsInput.Placeholder = " 输入关键词"
    search.wordsInput.Focus()
    search.wordsInput.Prompt = GetFocusedPrompt()
    search.wordsInput.TextColor = primaryColorStr
    search.wordsInput.CharLimit = 32

    search.submitButton = GetBlurredSubmitButton()

    return
}

// update search
func updateSearch(msg tea.Msg, m *NeteaseModel) (tea.Model, tea.Cmd) {

    switch msg := msg.(type) {
    case tickSearchMsg:
        return m, nil
    case tea.KeyMsg:
        switch msg.String() {

        case "esc":
            m.modelType = MtMain
            m.searchModel.Reset()
            return m, tickMainUI(time.Nanosecond)

        // Cycle between inputs
        case "tab", "shift+tab", "enter", "up", "down":

            if m.searchModel.searchType == StNull {
                return m, nil
            }

            inputs := []textinput.Model{
                m.searchModel.wordsInput,
            }

            s := msg.String()

            // Did the user press enter while the submit button was focused?
            // If so, exit.
            if s == "enter" && m.searchModel.index == len(inputs) {
                if len(m.searchModel.wordsInput.Value()) <= 0 {
                    m.searchModel.tips = SetFgStyle("关键词不得为空", termenv.ANSIBrightRed)
                    return m, nil
                }
                var (
                    code     float64
                    response []byte
                )
                searchService := service.SearchService{
                    S:    m.searchModel.wordsInput.Value(),
                    Type: strconv.Itoa(int(m.searchModel.searchType)),
                }
                code, response = searchService.Search()

                codeType := utils.CheckCode(code)
                switch codeType {
                case utils.UnknownError:
                    m.searchModel.tips = SetFgStyle("未知错误，请稍后再试~", termenv.ANSIBrightRed)
                    return m, tickSearch(time.Nanosecond)
                case utils.NetworkError:
                    m.searchModel.tips = SetFgStyle("网络异常，请稍后再试~", termenv.ANSIBrightRed)
                    return m, tickSearch(time.Nanosecond)
                case utils.Success:
                    m.searchModel.result = response

                    switch m.searchModel.searchType {
                    case StSingleSong:
                        m.searchModel.result = utils.GetSongsOfSearchResult(response)
                    case StAlbum:
                        m.searchModel.result = utils.GetAlbumsOfSearchResult(response)
                    case StSinger:
                        m.searchModel.result = utils.GetArtistsOfSearchResult(response)
                    case StPlaylist:
                        m.searchModel.result = utils.GetPlaylistsOfSearchResult(response)
                    case StUser:
                        m.searchModel.result = utils.GetUsersOfSearchResult(response)
                    case StLyric:
                        m.searchModel.result = utils.GetSongsOfSearchResult(response)
                    case StRadio:
                        m.searchModel.result = utils.GetDjRadiosOfSearchResult(response)
                    }

                    enterMenu(m)
                }

                m.modelType = MtMain
                m.searchModel.Reset()
                return m, tickMainUI(time.Nanosecond)
            }

            // Cycle indexes
            if s == "up" || s == "shift+tab" {
                m.searchModel.index--
            } else {
                m.searchModel.index++
            }

            if m.searchModel.index > len(inputs) {
                m.searchModel.index = 0
            } else if m.searchModel.index < 0 {
                m.searchModel.index = len(inputs)
            }

            for i := 0; i <= len(inputs)-1; i++ {
                if i == m.searchModel.index {
                    // Set focused state
                    inputs[i].Focus()
                    inputs[i].Prompt = GetFocusedPrompt()
                    inputs[i].TextColor = primaryColorStr
                    continue
                }
                // Remove focused state
                inputs[i].Blur()
                inputs[i].Prompt = GetBlurredPrompt()
                inputs[i].TextColor = ""
            }

            m.searchModel.wordsInput = inputs[0]

            if m.searchModel.index == len(inputs) {
                m.searchModel.submitButton = GetFocusedSubmitButton()
            } else {
                m.searchModel.submitButton = GetBlurredSubmitButton()
            }

            return m, nil
        }
    }

    // Handle character input and blinks
    return updateSearchInputs(msg, m)
}

func updateSearchInputs(msg tea.Msg, m *NeteaseModel) (tea.Model, tea.Cmd) {
    var (
        cmd  tea.Cmd
        cmds []tea.Cmd
    )

    m.searchModel.wordsInput, cmd = m.searchModel.wordsInput.Update(msg)
    cmds = append(cmds, cmd)

    return m, tea.Batch(cmds...)
}

func searchView(m *NeteaseModel) string {

    var builder strings.Builder

    // 距离顶部的行数
    top := 0

    // title
    if config.ConfigRegistry.MainShowTitle {

        builder.WriteString(titleView(m, &top))
    } else {
        top++
    }

    // menu title
    menuViews := m.menu.MenuViews()
    var menuTitle string
    if m.selectedIndex < len(menuViews) {
        menuTitle = menuViews[m.selectedIndex].Title
    }
    builder.WriteString(menuTitleView(m, &top, menuTitle))
    builder.WriteString("\n\n\n")
    top += 2

    inputs := []textinput.Model{
        m.searchModel.wordsInput,
    }

    for i, input := range inputs {
        if m.menuStartColumn > 0 {
            builder.WriteString(strings.Repeat(" ", m.menuStartColumn))
        }

        builder.WriteString(input.View())

        var valueLen int
        if input.Value() == "" {
            valueLen = runewidth.StringWidth(input.Placeholder)
        } else {
            valueLen = runewidth.StringWidth(input.Value())
        }
        if spaceLen := m.WindowWidth - m.menuStartColumn - valueLen - 3; spaceLen > 0 {
            builder.WriteString(strings.Repeat(" ", spaceLen))
        }

        top++

        if i < len(inputs)-1 {
            builder.WriteString("\n\n")
            top++
        }
    }

    builder.WriteString("\n\n")
    top++
    if m.menuStartColumn > 0 {
        builder.WriteString(strings.Repeat(" ", m.menuStartColumn))
    }
    builder.WriteString(m.searchModel.tips)
    builder.WriteString("\n\n")
    top++
    if m.menuStartColumn > 0 {
        builder.WriteString(strings.Repeat(" ", m.menuStartColumn))
    }
    builder.WriteString(m.searchModel.submitButton)
    builder.WriteString("\n")

    if m.WindowHeight > top+3 {
        builder.WriteString(strings.Repeat("\n", m.WindowHeight-top-3))
    }

    return builder.String()
}

// SearchHandle 搜索
func SearchHandle(model *NeteaseModel, searchType SearchType) {
    model.modelType = MtSearch
    model.searchModel.searchType = searchType
}


<|start_filename|>ds/dj_category.go<|end_filename|>
package ds

import (
    "errors"
    "github.com/buger/jsonparser"
)

type DjCategory struct {
    Id   int64
    Name string
}

// NewDjCategoryFromJson 初始化分类
func NewDjCategoryFromJson(json []byte) (DjCategory, error) {
    var category DjCategory

    if len(json) == 0 {
        return category, errors.New("json is empty")
    }

    id, err := jsonparser.GetInt(json, "id")
    if err != nil {
        return category, err
    }
    category.Id = id

    if name, err := jsonparser.GetString(json, "name"); err == nil {
        category.Name = name
    }

    return category, nil
}

<|start_filename|>main.go<|end_filename|>
package main

import (
    "fmt"
    "github.com/gookit/gcli/v2"
    "go-musicfox/commands"
    "go-musicfox/config"
    "go-musicfox/constants"
    "go-musicfox/ui"
    "go-musicfox/utils"
)

func main() {
    app := gcli.NewApp()
    app.Name = constants.AppName
    app.Version = constants.AppVersion
    app.Description = constants.AppDescription

    // 加载config
    config.ConfigRegistry = utils.LoadIniConfig()

    logo := utils.GetAlphaAscii(app.Name)
    randomColor := ui.GetPrimaryColor()
    logoColorful := ui.SetFgStyle(logo, randomColor)

    gcli.AppHelpTemplate = fmt.Sprintf(constants.AppHelpTemplate, logoColorful)
    app.Logo.Text = logoColorful

    playerCommand := commands.NewPlayerCommand()
    app.Add(playerCommand)
    app.DefaultCommand(playerCommand.Name)

    app.Run()
}