From 959112113135be0c468c1055e8395bbbcc2ee0e3 Mon Sep 17 00:00:00 2001 From: itouakirai Date: Tue, 25 Feb 2025 00:14:01 +0800 Subject: [PATCH] style: Lyrics modularity --- config.yaml | 4 +- main.go | 241 +++------------------------------------ utils/lyrics/lyrics.go | 253 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 268 insertions(+), 230 deletions(-) create mode 100644 utils/lyrics/lyrics.go diff --git a/config.yaml b/config.yaml index 5551435..81cf4ad 100644 --- a/config.yaml +++ b/config.yaml @@ -3,7 +3,7 @@ authorization-token: "your-authorization-token" #You don't need to change it; it language: "" #supportedLanguage by each storefront --> https://gist.github.com/itouakirai/c8ba9df9dc65bd300094103b058731d0 lrc-type: "lyrics" #lyrics or syllable-lyrics lrc-format: "lrc" #lrc or ttml -embed-lrc: true #Unable to embed ttml lyrics +embed-lrc: true save-lrc-file: false save-artist-cover: false save-animated-artwork: false # If enabled, requires ffmpeg @@ -43,4 +43,4 @@ use-songinfo-for-playlist: false #if set true,will download album cover for playlist dl-albumcover-for-playlist: false mv-audio-type: atmos #atmos ac3 aac -mv-max: 2160 \ No newline at end of file +mv-max: 2160 diff --git a/main.go b/main.go index 0584297..d6a47cf 100644 --- a/main.go +++ b/main.go @@ -23,8 +23,8 @@ import ( "main/utils/runv2" "main/utils/runv3" "main/utils/structs" + "main/utils/lyrics" - "github.com/beevik/etree" "github.com/fatih/color" "github.com/grafov/m3u8" "github.com/olekukonko/tablewriter" @@ -393,31 +393,6 @@ func getMeta(albumId string, token string, storefront string) (*structs.AutoGene return obj, nil } -func getSongLyrics(songId string, storefront string, token string, userToken string) (string, error) { - req, err := http.NewRequest("GET", - fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/songs/%s/%s?l=%s", storefront, songId, Config.LrcType, Config.Language), nil) - if err != nil { - return "", err - } - req.Header.Set("Origin", "https://music.apple.com") - req.Header.Set("Referer", "https://music.apple.com/") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - cookie := http.Cookie{Name: "media-user-token", Value: userToken} - req.AddCookie(&cookie) - do, err := http.DefaultClient.Do(req) - if err != nil { - return "", err - } - defer do.Body.Close() - obj := new(structs.SongLyrics) - _ = json.NewDecoder(do.Body).Decode(&obj) - if obj.Data != nil { - return obj.Data[0].Attributes.Ttml, nil - } else { - return "", errors.New("failed to get lyrics") - } -} - func writeCover(sanAlbumFolder, name string, url string) (string, error) { covPath := filepath.Join(sanAlbumFolder, name+"."+Config.CoverFormat) if Config.CoverFormat == "original" { @@ -601,36 +576,23 @@ func downloadTrack(trackNum int, trackTotal int, meta *structs.AutoGenerated, tr //get lrc var lrc string = "" - if len(mediaUserToken) > 50 { - ttml, err := getSongLyrics(track.ID, storefront, token, mediaUserToken) + if Config.EmbedLrc || Config.SaveLrcFile { + lrcStr, err := lyrics.Get(storefront, track.ID, Config.LrcType, Config.Language, Config.LrcFormat, token, mediaUserToken) if err != nil { - fmt.Println("Failed to get lyrics") - } else if Config.LrcFormat == "ttml" { + fmt.Println(err) + } else { if Config.SaveLrcFile { - lrc = ttml - err := writeLyrics(sanAlbumFolder, lrcFilename, lrc) + err := writeLyrics(sanAlbumFolder, lrcFilename, lrcStr) if err != nil { fmt.Printf("Failed to write lyrics") } - lrc = "" } - } else { - lrc, err = conventTTMLToLRC(ttml) - if err != nil { - fmt.Printf("Failed to parse lyrics: %s \n", err) - } else { - if Config.SaveLrcFile { - err := writeLyrics(sanAlbumFolder, lrcFilename, lrc) - if err != nil { - fmt.Printf("Failed to write lyrics") - } - if !Config.EmbedLrc { - lrc = "" - } - } + if Config.EmbedLrc { + lrc = lrcStr } } } + exists, err := fileExists(trackPath) if err != nil { fmt.Println("Failed to check if track exists.") @@ -671,7 +633,7 @@ func downloadTrack(trackNum int, trackTotal int, meta *structs.AutoGenerated, tr tags := []string{ "tool=", fmt.Sprintf("artist=%s", meta.Data[0].Attributes.ArtistName), - fmt.Sprintf("lyrics=%s", lrc), + //fmt.Sprintf("lyrics=%s", lrc), } var trackCovPath string if Config.EmbedCover { @@ -699,7 +661,7 @@ func downloadTrack(trackNum int, trackTotal int, meta *structs.AutoGenerated, tr return } } - err = writeMP4Tags(trackPath, meta, trackNum, trackTotal) + err = writeMP4Tags(trackPath, lrc, meta, trackNum, trackTotal) if err != nil { fmt.Println("\u26A0 Failed to write tags in media:", err) counter.Unavailable++ @@ -1103,7 +1065,7 @@ func rip(albumId string, token string, storefront string, mediaUserToken string, return nil } -func writeMP4Tags(trackPath string, meta *structs.AutoGenerated, trackNum, trackTotal int) error { +func writeMP4Tags(trackPath, lrc string, meta *structs.AutoGenerated, trackNum, trackTotal int) error { index := trackNum - 1 t := &mp4tag.MP4Tags{ @@ -1124,6 +1086,7 @@ func writeMP4Tags(trackPath string, meta *structs.AutoGenerated, trackNum, track CustomGenre: meta.Data[0].Relationships.Tracks.Data[index].Attributes.GenreNames[0], Copyright: meta.Data[0].Attributes.Copyright, Publisher: meta.Data[0].Attributes.RecordLabel, + Lyrics: lrc, } if !strings.Contains(meta.Data[0].ID, "pl.") { @@ -1563,184 +1526,6 @@ func extractMvAudio(c string) (string, error) { return audioStreams[0].URL, nil } -func conventSyllableTTMLToLRC(ttml string) (string, error) { - parsedTTML := etree.NewDocument() - err := parsedTTML.ReadFromString(ttml) - if err != nil { - return "", err - } - var lrcLines []string - parseTime := func(timeValue string) (string, error) { - var h, m, s, ms int - if strings.Contains(timeValue, ":") { - _, err = fmt.Sscanf(timeValue, "%d:%d:%d.%d", &h, &m, &s, &ms) - if err != nil { - _, err = fmt.Sscanf(timeValue, "%d:%d.%d", &m, &s, &ms) - h = 0 - } - } else { - _, err = fmt.Sscanf(timeValue, "%d.%d", &s, &ms) - h, m = 0, 0 - } - if err != nil { - return "", err - } - m += h * 60 - ms = ms / 10 - return fmt.Sprintf("[%02d:%02d.%02d]", m, s, ms), nil - } - divs := parsedTTML.FindElement("tt").FindElement("body").FindElements("div") - //get trans - if len(parsedTTML.FindElement("tt").FindElements("head")) > 0 { - if len(parsedTTML.FindElement("tt").FindElement("head").FindElements("metadata")) > 0 { - Metadata := parsedTTML.FindElement("tt").FindElement("head").FindElement("metadata") - if len(Metadata.FindElements("iTunesMetadata")) > 0 { - iTunesMetadata := Metadata.FindElement("iTunesMetadata") - if len(iTunesMetadata.FindElements("translations")) > 0 { - if len(iTunesMetadata.FindElement("translations").FindElements("translation")) > 0 { - divs = iTunesMetadata.FindElement("translations").FindElements("translation") - } - } - } - } - } - for _, div := range divs { - for _, item := range div.ChildElements() { - var lrcSyllables []string - var i int = 0 - var endTime string - for _, lyrics := range item.Child { - if _, ok := lyrics.(*etree.CharData); ok { - if i > 0 { - lrcSyllables = append(lrcSyllables, " ") - continue - } - continue - } - lyric := lyrics.(*etree.Element) - if lyric.SelectAttr("begin") == nil { - continue - } - beginTime, err := parseTime(lyric.SelectAttr("begin").Value) - if err != nil { - return "", err - } - endTime, err = parseTime(lyric.SelectAttr("end").Value) - if err != nil { - return "", err - } - var text string - if lyric.SelectAttr("text") == nil { - var textTmp []string - for _, span := range lyric.Child { - if _, ok := span.(*etree.CharData); ok { - textTmp = append(textTmp, span.(*etree.CharData).Data) - } else { - textTmp = append(textTmp, span.(*etree.Element).Text()) - } - } - text = strings.Join(textTmp, "") - } else { - text = lyric.SelectAttr("text").Value - } - lrcSyllables = append(lrcSyllables, fmt.Sprintf("%s%s", beginTime, text)) - i += 1 - } - //endTime, err := parseTime(item.SelectAttr("end").Value) - //if err != nil { - // return "", err - //} - lrcLines = append(lrcLines, strings.Join(lrcSyllables, "")+endTime) - } - } - return strings.Join(lrcLines, "\n"), nil -} - -func conventTTMLToLRC(ttml string) (string, error) { - parsedTTML := etree.NewDocument() - err := parsedTTML.ReadFromString(ttml) - if err != nil { - return "", err - } - - var lrcLines []string - timingAttr := parsedTTML.FindElement("tt").SelectAttr("itunes:timing") - if timingAttr != nil { - if timingAttr.Value == "Word" { - lrc, err := conventSyllableTTMLToLRC(ttml) - return lrc, err - } - if timingAttr.Value == "None" { - for _, p := range parsedTTML.FindElements("//p") { - line := p.Text() - line = strings.TrimSpace(line) - if line != "" { - lrcLines = append(lrcLines, line) - } - } - return strings.Join(lrcLines, "\n"), nil - } - } - - for _, item := range parsedTTML.FindElement("tt").FindElement("body").ChildElements() { - for _, lyric := range item.ChildElements() { - var h, m, s, ms int - if lyric.SelectAttr("begin") == nil { - return "", errors.New("no synchronised lyrics") - } - if strings.Contains(lyric.SelectAttr("begin").Value, ":") { - _, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d:%d:%d.%d", &h, &m, &s, &ms) - if err != nil { - _, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d:%d.%d", &m, &s, &ms) - if err != nil { - _, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d:%d", &m, &s) - } - h = 0 - } - } else { - _, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d.%d", &s, &ms) - h, m = 0, 0 - } - if err != nil { - return "", err - } - var text string - //GET trans - if len(parsedTTML.FindElement("tt").FindElements("head")) > 0 { - if len(parsedTTML.FindElement("tt").FindElement("head").FindElements("metadata")) > 0 { - Metadata := parsedTTML.FindElement("tt").FindElement("head").FindElement("metadata") - if len(Metadata.FindElements("iTunesMetadata")) > 0 { - iTunesMetadata := Metadata.FindElement("iTunesMetadata") - if len(iTunesMetadata.FindElements("translations")) > 0 { - if len(iTunesMetadata.FindElement("translations").FindElements("translation")) > 0 { - xpath := fmt.Sprintf("//text[@for='%s']", lyric.SelectAttr("itunes:key").Value) - trans := iTunesMetadata.FindElement("translations").FindElement("translation").FindElement(xpath) - lyric = trans - } - } - } - } - } - if lyric.SelectAttr("text") == nil { - var textTmp []string - for _, span := range lyric.Child { - if _, ok := span.(*etree.CharData); ok { - textTmp = append(textTmp, span.(*etree.CharData).Data) - } else { - textTmp = append(textTmp, span.(*etree.Element).Text()) - } - } - text = strings.Join(textTmp, "") - } else { - text = lyric.SelectAttr("text").Value - } - m += h * 60 - ms = ms / 10 - lrcLines = append(lrcLines, fmt.Sprintf("[%02d:%02d.%02d]%s", m, s, ms, text)) - } - } - return strings.Join(lrcLines, "\n"), nil -} func checkM3u8(b string, f string) (string, error) { var EnhancedHls string diff --git a/utils/lyrics/lyrics.go b/utils/lyrics/lyrics.go new file mode 100644 index 0000000..af9fde9 --- /dev/null +++ b/utils/lyrics/lyrics.go @@ -0,0 +1,253 @@ +package lyrics + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/beevik/etree" +) + +type SongLyrics struct { + Data []struct { + Id string `json:"id"` + Type string `json:"type"` + Attributes struct { + Ttml string `json:"ttml"` + PlayParams struct { + Id string `json:"id"` + Kind string `json:"kind"` + CatalogId string `json:"catalogId"` + DisplayType int `json:"displayType"` + } `json:"playParams"` + } `json:"attributes"` + } `json:"data"` +} + + +func Get(storefront, songId, lrcType, language, lrcFormat, token, mediaUserToken string) (string, error) { + if len(mediaUserToken) < 50 { + return "", errors.New("MediaUserToken not set") + } + + ttml, err := getSongLyrics(songId, storefront, token, mediaUserToken, lrcType, language) + if err != nil { + return "", err + } + + if lrcFormat == "ttml" { + return ttml, nil + } + + lrc, err := TtmlToLrc(ttml) + if err != nil { + return "", err + } + + return lrc, nil +} +func getSongLyrics(songId string, storefront string, token string, userToken string, lrcType string, language string) (string, error) { + req, err := http.NewRequest("GET", + fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/songs/%s/%s?l=%s", storefront, songId, lrcType, language), nil) + if err != nil { + return "", err + } + req.Header.Set("Origin", "https://music.apple.com") + req.Header.Set("Referer", "https://music.apple.com/") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + cookie := http.Cookie{Name: "media-user-token", Value: userToken} + req.AddCookie(&cookie) + do, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer do.Body.Close() + obj := new(SongLyrics) + _ = json.NewDecoder(do.Body).Decode(&obj) + if obj.Data != nil { + return obj.Data[0].Attributes.Ttml, nil + } else { + return "", errors.New("failed to get lyrics") + } +} + +func TtmlToLrc(ttml string) (string, error) { + parsedTTML := etree.NewDocument() + err := parsedTTML.ReadFromString(ttml) + if err != nil { + return "", err + } + + var lrcLines []string + timingAttr := parsedTTML.FindElement("tt").SelectAttr("itunes:timing") + if timingAttr != nil { + if timingAttr.Value == "Word" { + lrc, err := conventSyllableTTMLToLRC(ttml) + return lrc, err + } + if timingAttr.Value == "None" { + for _, p := range parsedTTML.FindElements("//p") { + line := p.Text() + line = strings.TrimSpace(line) + if line != "" { + lrcLines = append(lrcLines, line) + } + } + return strings.Join(lrcLines, "\n"), nil + } + } + + for _, item := range parsedTTML.FindElement("tt").FindElement("body").ChildElements() { + for _, lyric := range item.ChildElements() { + var h, m, s, ms int + if lyric.SelectAttr("begin") == nil { + return "", errors.New("no synchronised lyrics") + } + if strings.Contains(lyric.SelectAttr("begin").Value, ":") { + _, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d:%d:%d.%d", &h, &m, &s, &ms) + if err != nil { + _, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d:%d.%d", &m, &s, &ms) + if err != nil { + _, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d:%d", &m, &s) + } + h = 0 + } + } else { + _, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d.%d", &s, &ms) + h, m = 0, 0 + } + if err != nil { + return "", err + } + var text string + //GET trans + if len(parsedTTML.FindElement("tt").FindElements("head")) > 0 { + if len(parsedTTML.FindElement("tt").FindElement("head").FindElements("metadata")) > 0 { + Metadata := parsedTTML.FindElement("tt").FindElement("head").FindElement("metadata") + if len(Metadata.FindElements("iTunesMetadata")) > 0 { + iTunesMetadata := Metadata.FindElement("iTunesMetadata") + if len(iTunesMetadata.FindElements("translations")) > 0 { + if len(iTunesMetadata.FindElement("translations").FindElements("translation")) > 0 { + xpath := fmt.Sprintf("//text[@for='%s']", lyric.SelectAttr("itunes:key").Value) + trans := iTunesMetadata.FindElement("translations").FindElement("translation").FindElement(xpath) + lyric = trans + } + } + } + } + } + if lyric.SelectAttr("text") == nil { + var textTmp []string + for _, span := range lyric.Child { + if _, ok := span.(*etree.CharData); ok { + textTmp = append(textTmp, span.(*etree.CharData).Data) + } else { + textTmp = append(textTmp, span.(*etree.Element).Text()) + } + } + text = strings.Join(textTmp, "") + } else { + text = lyric.SelectAttr("text").Value + } + m += h * 60 + ms = ms / 10 + lrcLines = append(lrcLines, fmt.Sprintf("[%02d:%02d.%02d]%s", m, s, ms, text)) + } + } + return strings.Join(lrcLines, "\n"), nil +} + +func conventSyllableTTMLToLRC(ttml string) (string, error) { + parsedTTML := etree.NewDocument() + err := parsedTTML.ReadFromString(ttml) + if err != nil { + return "", err + } + var lrcLines []string + parseTime := func(timeValue string) (string, error) { + var h, m, s, ms int + if strings.Contains(timeValue, ":") { + _, err = fmt.Sscanf(timeValue, "%d:%d:%d.%d", &h, &m, &s, &ms) + if err != nil { + _, err = fmt.Sscanf(timeValue, "%d:%d.%d", &m, &s, &ms) + h = 0 + } + } else { + _, err = fmt.Sscanf(timeValue, "%d.%d", &s, &ms) + h, m = 0, 0 + } + if err != nil { + return "", err + } + m += h * 60 + ms = ms / 10 + return fmt.Sprintf("[%02d:%02d.%02d]", m, s, ms), nil + } + divs := parsedTTML.FindElement("tt").FindElement("body").FindElements("div") + //get trans + if len(parsedTTML.FindElement("tt").FindElements("head")) > 0 { + if len(parsedTTML.FindElement("tt").FindElement("head").FindElements("metadata")) > 0 { + Metadata := parsedTTML.FindElement("tt").FindElement("head").FindElement("metadata") + if len(Metadata.FindElements("iTunesMetadata")) > 0 { + iTunesMetadata := Metadata.FindElement("iTunesMetadata") + if len(iTunesMetadata.FindElements("translations")) > 0 { + if len(iTunesMetadata.FindElement("translations").FindElements("translation")) > 0 { + divs = iTunesMetadata.FindElement("translations").FindElements("translation") + } + } + } + } + } + for _, div := range divs { + for _, item := range div.ChildElements() { + var lrcSyllables []string + var i int = 0 + var endTime string + for _, lyrics := range item.Child { + if _, ok := lyrics.(*etree.CharData); ok { + if i > 0 { + lrcSyllables = append(lrcSyllables, " ") + continue + } + continue + } + lyric := lyrics.(*etree.Element) + if lyric.SelectAttr("begin") == nil { + continue + } + beginTime, err := parseTime(lyric.SelectAttr("begin").Value) + if err != nil { + return "", err + } + endTime, err = parseTime(lyric.SelectAttr("end").Value) + if err != nil { + return "", err + } + var text string + if lyric.SelectAttr("text") == nil { + var textTmp []string + for _, span := range lyric.Child { + if _, ok := span.(*etree.CharData); ok { + textTmp = append(textTmp, span.(*etree.CharData).Data) + } else { + textTmp = append(textTmp, span.(*etree.Element).Text()) + } + } + text = strings.Join(textTmp, "") + } else { + text = lyric.SelectAttr("text").Value + } + lrcSyllables = append(lrcSyllables, fmt.Sprintf("%s%s", beginTime, text)) + i += 1 + } + //endTime, err := parseTime(item.SelectAttr("end").Value) + //if err != nil { + // return "", err + //} + lrcLines = append(lrcLines, strings.Join(lrcSyllables, "")+endTime) + } + } + return strings.Join(lrcLines, "\n"), nil +}