2021-02-28 23:19:42 +08:00
|
|
|
|
// Package lyric package download lyrics from different website and embed them into mp3 file.
|
|
|
|
|
// lrc file is used to parse lrc file into subtitle. Similar to subtitles package
|
2021-03-03 13:09:38 +08:00
|
|
|
|
// [al:''Album where the song is from'']
|
|
|
|
|
// [ar:''Lyrics artist'']
|
|
|
|
|
// [by:''Creator of the LRC file'']
|
|
|
|
|
// [offset:''+/- Overall timestamp adjustment in milliseconds, + shifts time up, - shifts down'']
|
|
|
|
|
// [re:''The player or editor that creates LRC file'']
|
|
|
|
|
// [ti:''Lyrics (song) title'']
|
|
|
|
|
// [ve:''version of program'']
|
|
|
|
|
// [ti:Let's Twist Again]
|
|
|
|
|
// [ar:Chubby Checker oppure Beatles, The]
|
|
|
|
|
// [au:Written by Kal Mann / Dave Appell, 1961]
|
|
|
|
|
// [al:Hits Of The 60's - Vol. 2 – Oldies]
|
|
|
|
|
// [00:12.00]Lyrics beginning ...
|
|
|
|
|
// [00:15.30]Some more lyrics ...
|
2021-02-27 23:11:17 +08:00
|
|
|
|
package lyric
|
|
|
|
|
|
|
|
|
|
import (
|
2021-04-13 13:22:17 +08:00
|
|
|
|
"errors"
|
2021-02-27 23:11:17 +08:00
|
|
|
|
"fmt"
|
|
|
|
|
"regexp"
|
|
|
|
|
"runtime"
|
2021-03-18 11:16:23 +08:00
|
|
|
|
"sort"
|
2021-02-27 23:11:17 +08:00
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
2021-03-03 13:09:38 +08:00
|
|
|
|
|
2021-03-26 16:57:19 +08:00
|
|
|
|
"github.com/tramhao/id3v2"
|
2021-03-03 13:09:38 +08:00
|
|
|
|
"github.com/ztrue/tracerr"
|
2021-02-27 23:11:17 +08:00
|
|
|
|
)
|
|
|
|
|
|
2021-03-25 12:01:12 +08:00
|
|
|
|
// Lyric contains UnsyncedCaptions and SyncedCaptions
|
2021-03-02 23:34:38 +08:00
|
|
|
|
type Lyric struct {
|
2021-03-03 13:09:38 +08:00
|
|
|
|
Album string
|
|
|
|
|
Artist string
|
2021-03-15 02:29:41 +08:00
|
|
|
|
ByCreator string // Creator of LRC file
|
|
|
|
|
Offset int32 // positive means delay lyric
|
|
|
|
|
RePlayerEditor string // Player or Editor to create this LRC file
|
2021-03-03 13:09:38 +08:00
|
|
|
|
Title string
|
|
|
|
|
VersionPlayerEditor string // Version of player or editor
|
2021-03-16 00:12:33 +08:00
|
|
|
|
LangExt string
|
2021-03-22 11:40:44 +08:00
|
|
|
|
UnsyncedCaptions []UnsyncedCaption // USLT captions
|
|
|
|
|
SyncedCaptions []id3v2.SyncedText // SYLT captions
|
2021-03-02 23:34:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-03-25 12:01:12 +08:00
|
|
|
|
// UnsyncedCaption is only showing in tageditor
|
2021-03-22 11:40:44 +08:00
|
|
|
|
type UnsyncedCaption struct {
|
2021-03-15 02:29:41 +08:00
|
|
|
|
Timestamp uint32
|
2021-03-13 01:13:45 +08:00
|
|
|
|
Text string
|
2021-03-02 23:34:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-02-27 23:11:17 +08:00
|
|
|
|
// Eol is the end of line characters to use when writing .srt data
|
|
|
|
|
var eol = "\n"
|
|
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
eol = "\r\n"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func looksLikeLRC(s string) bool {
|
2021-02-28 13:52:22 +08:00
|
|
|
|
if s != "" {
|
|
|
|
|
if s[0] == 239 || s[0] == 91 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
2021-02-28 13:26:42 +08:00
|
|
|
|
}
|
|
|
|
|
return false
|
2021-02-27 23:11:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewFromLRC parses a .lrc text into Subtitle, assumes s is a clean utf8 string
|
2021-03-25 16:57:44 +08:00
|
|
|
|
func (lyric *Lyric) NewFromLRC(s string) (err error) {
|
2021-02-28 23:19:42 +08:00
|
|
|
|
s = cleanLRC(s)
|
2021-02-27 23:11:17 +08:00
|
|
|
|
lines := strings.Split(s, "\n")
|
|
|
|
|
|
2021-02-28 13:26:42 +08:00
|
|
|
|
for i := 0; i < len(lines)-1; i++ {
|
2021-02-27 23:11:17 +08:00
|
|
|
|
seq := strings.Trim(lines[i], "\r ")
|
|
|
|
|
if seq == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-03 13:09:38 +08:00
|
|
|
|
if strings.HasPrefix(seq, "[offset") {
|
|
|
|
|
tmpString := strings.TrimPrefix(seq, "[offset:")
|
|
|
|
|
tmpString = strings.TrimSuffix(tmpString, "]")
|
|
|
|
|
tmpString = strings.ReplaceAll(tmpString, " ", "")
|
|
|
|
|
var intOffset int
|
|
|
|
|
intOffset, err = strconv.Atoi(tmpString)
|
|
|
|
|
if err != nil {
|
2021-03-25 16:57:44 +08:00
|
|
|
|
return tracerr.Wrap(err)
|
2021-03-03 13:09:38 +08:00
|
|
|
|
}
|
2021-03-25 16:57:44 +08:00
|
|
|
|
lyric.Offset = int32(intOffset)
|
2021-03-03 13:09:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-03-19 00:40:19 +08:00
|
|
|
|
timestampPattern := regexp.MustCompile(`(?U)^\[[0-9].*\]`)
|
|
|
|
|
matchTimestamp := timestampPattern.FindStringSubmatch(lines[i])
|
2021-02-27 23:11:17 +08:00
|
|
|
|
|
2021-03-19 00:40:19 +08:00
|
|
|
|
if len(matchTimestamp) < 1 {
|
2021-03-15 03:03:12 +08:00
|
|
|
|
// Here we continue to parse the subtitle and ignore the lines have no timestamp
|
2021-02-27 23:11:17 +08:00
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-22 11:40:44 +08:00
|
|
|
|
var o UnsyncedCaption
|
2021-02-27 23:11:17 +08:00
|
|
|
|
|
2021-03-19 00:40:19 +08:00
|
|
|
|
o.Timestamp, err = parseLrcTime(matchTimestamp[0])
|
2021-02-27 23:11:17 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
err = fmt.Errorf("lrc: start error at line %d: %v", i, err)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-28 13:26:42 +08:00
|
|
|
|
r2 := regexp.MustCompile(`^\[.*\]`)
|
|
|
|
|
s2 := r2.ReplaceAllString(lines[i], "$1")
|
2021-03-19 00:40:19 +08:00
|
|
|
|
s3 := strings.Trim(s2, "\r")
|
|
|
|
|
s3 = strings.Trim(s3, "\n")
|
|
|
|
|
s3 = strings.TrimSpace(s3)
|
|
|
|
|
singleSpacePattern := regexp.MustCompile(`\s+`)
|
|
|
|
|
s3 = singleSpacePattern.ReplaceAllString(s3, " ")
|
2021-03-13 01:13:45 +08:00
|
|
|
|
o.Text = s3
|
2021-03-25 16:57:44 +08:00
|
|
|
|
lyric.UnsyncedCaptions = append(lyric.UnsyncedCaptions, o)
|
2021-02-27 23:11:17 +08:00
|
|
|
|
}
|
2021-03-18 14:57:20 +08:00
|
|
|
|
|
|
|
|
|
// we sort the cpations by Timestamp. This is to fix some lyrics downloaded are not sorted
|
2021-03-25 16:57:44 +08:00
|
|
|
|
sort.SliceStable(lyric.UnsyncedCaptions, func(i, j int) bool {
|
|
|
|
|
return lyric.UnsyncedCaptions[i].Timestamp < lyric.UnsyncedCaptions[j].Timestamp
|
2021-03-18 11:16:23 +08:00
|
|
|
|
})
|
2021-03-18 14:57:20 +08:00
|
|
|
|
|
2021-03-25 16:57:44 +08:00
|
|
|
|
lyric.mergeLRC()
|
2021-03-23 22:33:40 +08:00
|
|
|
|
|
|
|
|
|
// add synced lyric by calculating offset of unsynced lyric
|
2021-03-25 16:57:44 +08:00
|
|
|
|
for _, v := range lyric.UnsyncedCaptions {
|
2021-03-22 11:40:44 +08:00
|
|
|
|
var s id3v2.SyncedText
|
|
|
|
|
s.Text = v.Text
|
2021-04-13 13:22:17 +08:00
|
|
|
|
if lyric.Offset <= 0 {
|
|
|
|
|
s.Timestamp = v.Timestamp + uint32(-lyric.Offset)
|
2021-03-22 11:40:44 +08:00
|
|
|
|
} else {
|
2021-04-13 13:22:17 +08:00
|
|
|
|
if v.Timestamp > uint32(lyric.Offset) {
|
|
|
|
|
s.Timestamp = v.Timestamp - uint32(lyric.Offset)
|
2021-03-22 11:40:44 +08:00
|
|
|
|
} else {
|
|
|
|
|
s.Timestamp = 0
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-03-25 16:57:44 +08:00
|
|
|
|
lyric.SyncedCaptions = append(lyric.SyncedCaptions, s)
|
2021-03-22 11:40:44 +08:00
|
|
|
|
}
|
2021-03-18 14:57:20 +08:00
|
|
|
|
|
2021-03-23 22:33:40 +08:00
|
|
|
|
// merge again because timestamp 0 could overlap if offset is negative
|
2021-03-25 16:57:44 +08:00
|
|
|
|
lyric.mergeSyncLRC()
|
2021-02-27 23:11:17 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-15 03:03:12 +08:00
|
|
|
|
// parseLrcTime parses a lrc subtitle time (ms since start of song)
|
2021-03-15 02:29:41 +08:00
|
|
|
|
func parseLrcTime(in string) (uint32, error) {
|
2021-02-27 23:11:17 +08:00
|
|
|
|
in = strings.TrimPrefix(in, "[")
|
|
|
|
|
in = strings.TrimSuffix(in, "]")
|
|
|
|
|
// . and , to :
|
|
|
|
|
in = strings.Replace(in, ",", ":", -1)
|
|
|
|
|
in = strings.Replace(in, ".", ":", -1)
|
|
|
|
|
|
|
|
|
|
if strings.Count(in, ":") == 2 {
|
|
|
|
|
in += ":000"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
r1 := regexp.MustCompile("([0-9]+):([0-9]+):([0-9]+):([0-9]+)")
|
|
|
|
|
matches := r1.FindStringSubmatch(in)
|
|
|
|
|
if len(matches) < 5 {
|
2021-03-15 02:29:41 +08:00
|
|
|
|
return 0, fmt.Errorf("[lrc] Regexp didnt match: %s", in)
|
2021-02-27 23:11:17 +08:00
|
|
|
|
}
|
2021-02-28 13:26:42 +08:00
|
|
|
|
m, err := strconv.Atoi(matches[1])
|
2021-02-27 23:11:17 +08:00
|
|
|
|
if err != nil {
|
2021-03-15 02:29:41 +08:00
|
|
|
|
return 0, err
|
2021-02-27 23:11:17 +08:00
|
|
|
|
}
|
2021-02-28 13:26:42 +08:00
|
|
|
|
s, err := strconv.Atoi(matches[2])
|
2021-02-27 23:11:17 +08:00
|
|
|
|
if err != nil {
|
2021-03-15 02:29:41 +08:00
|
|
|
|
return 0, err
|
2021-02-27 23:11:17 +08:00
|
|
|
|
}
|
2021-02-28 13:26:42 +08:00
|
|
|
|
ms, err := strconv.Atoi(matches[3])
|
2021-02-27 23:11:17 +08:00
|
|
|
|
if err != nil {
|
2021-03-15 02:29:41 +08:00
|
|
|
|
return 0, err
|
2021-02-27 23:11:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-03-15 02:29:41 +08:00
|
|
|
|
timeStamp := m*60*1000 + s*1000 + ms
|
|
|
|
|
if timeStamp < 0 {
|
|
|
|
|
timeStamp = 0
|
|
|
|
|
}
|
2021-02-27 23:11:17 +08:00
|
|
|
|
|
2021-03-15 02:29:41 +08:00
|
|
|
|
return uint32(timeStamp), nil
|
2021-02-27 23:11:17 +08:00
|
|
|
|
}
|
2021-02-28 23:19:42 +08:00
|
|
|
|
|
|
|
|
|
// cleanLRC clean the string download
|
|
|
|
|
func cleanLRC(s string) (cleanLyric string) {
|
|
|
|
|
// Clean ' to '
|
2021-03-03 00:32:07 +08:00
|
|
|
|
s = strings.ToValidUTF8(s, " ")
|
2021-02-28 23:19:42 +08:00
|
|
|
|
s = strings.Replace(s, "'", "'", -1)
|
2021-03-15 02:29:41 +08:00
|
|
|
|
// It's weird that sometimes there are two adjacent ''.
|
2021-02-28 23:19:42 +08:00
|
|
|
|
// Replace it anyway
|
|
|
|
|
cleanLyric = strings.Replace(s, "''", "'", -1)
|
|
|
|
|
|
|
|
|
|
return cleanLyric
|
|
|
|
|
}
|
2021-03-02 23:34:38 +08:00
|
|
|
|
|
2021-03-23 22:33:40 +08:00
|
|
|
|
// mergeLRC merge lyric if the time between two captions is less than 2 seconds
|
2021-03-25 16:57:44 +08:00
|
|
|
|
func (lyric *Lyric) mergeLRC() {
|
2021-03-18 14:57:20 +08:00
|
|
|
|
|
2021-03-22 11:40:44 +08:00
|
|
|
|
lenLyric := len(lyric.UnsyncedCaptions)
|
|
|
|
|
for i := 0; i < lenLyric-1; i++ {
|
|
|
|
|
if lyric.UnsyncedCaptions[i].Timestamp+2000 > lyric.UnsyncedCaptions[i+1].Timestamp && lyric.UnsyncedCaptions[i].Text != "" {
|
|
|
|
|
lyric.UnsyncedCaptions[i].Text = lyric.UnsyncedCaptions[i].Text + " " + lyric.UnsyncedCaptions[i+1].Text
|
2021-03-23 22:33:40 +08:00
|
|
|
|
lyric.UnsyncedCaptions = removeUnsynced(lyric.UnsyncedCaptions, i+1)
|
2021-03-22 11:40:44 +08:00
|
|
|
|
i--
|
|
|
|
|
lenLyric--
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-23 22:33:40 +08:00
|
|
|
|
// mergeSyncLRC merge lyric if the time between two captions is less than 2 seconds
|
|
|
|
|
// this is specially useful when offset is negative and several timestamp 0 in synced lyric
|
2021-03-25 16:57:44 +08:00
|
|
|
|
func (lyric *Lyric) mergeSyncLRC() {
|
2021-03-22 11:40:44 +08:00
|
|
|
|
|
|
|
|
|
lenLyric := len(lyric.SyncedCaptions)
|
2021-03-18 14:57:20 +08:00
|
|
|
|
for i := 0; i < lenLyric-1; i++ {
|
2021-03-22 11:40:44 +08:00
|
|
|
|
if lyric.SyncedCaptions[i].Timestamp+2000 > lyric.SyncedCaptions[i+1].Timestamp && lyric.SyncedCaptions[i].Text != "" {
|
|
|
|
|
lyric.SyncedCaptions[i].Text = lyric.SyncedCaptions[i].Text + " " + lyric.SyncedCaptions[i+1].Text
|
|
|
|
|
lyric.SyncedCaptions = removeSynced(lyric.SyncedCaptions, i+1)
|
2021-03-18 14:57:20 +08:00
|
|
|
|
i--
|
|
|
|
|
lenLyric--
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-23 22:33:40 +08:00
|
|
|
|
func removeUnsynced(slice []UnsyncedCaption, s int) []UnsyncedCaption {
|
2021-03-22 11:40:44 +08:00
|
|
|
|
return append(slice[:s], slice[s+1:]...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func removeSynced(slice []id3v2.SyncedText, s int) []id3v2.SyncedText {
|
2021-03-18 14:57:20 +08:00
|
|
|
|
return append(slice[:s], slice[s+1:]...)
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-05 12:55:12 +08:00
|
|
|
|
// AsLRC renders the sub in .lrc format
|
2021-03-25 16:57:44 +08:00
|
|
|
|
func (lyric *Lyric) AsLRC() (res string) {
|
2021-03-03 13:09:38 +08:00
|
|
|
|
if lyric.Offset != 0 {
|
2021-03-15 02:29:41 +08:00
|
|
|
|
stringOffset := strconv.Itoa(int(lyric.Offset))
|
2021-03-03 13:09:38 +08:00
|
|
|
|
res += "[offset:" + stringOffset + "]" + eol
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-25 16:57:44 +08:00
|
|
|
|
for _, cap := range lyric.UnsyncedCaptions {
|
|
|
|
|
res += cap.asLRC()
|
2021-03-02 23:34:38 +08:00
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-23 22:33:40 +08:00
|
|
|
|
// asLRC renders the caption as one line in lrc
|
|
|
|
|
func (cap UnsyncedCaption) asLRC() string {
|
|
|
|
|
res := "[" + timeLRC(cap.Timestamp) + "]"
|
2021-03-13 01:13:45 +08:00
|
|
|
|
res += cap.Text + eol
|
2021-03-02 23:34:38 +08:00
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-23 22:33:40 +08:00
|
|
|
|
// timeLRC renders a timestamp for use in lrc
|
|
|
|
|
func timeLRC(t uint32) string {
|
2021-03-15 02:29:41 +08:00
|
|
|
|
tDuration := time.Duration(t) * time.Millisecond
|
|
|
|
|
h := tDuration / time.Hour
|
|
|
|
|
tDuration -= h * time.Hour
|
|
|
|
|
m := tDuration / time.Minute
|
|
|
|
|
tDuration -= m * time.Minute
|
|
|
|
|
s := tDuration / time.Second
|
|
|
|
|
tDuration -= s * time.Second
|
|
|
|
|
ms := tDuration / time.Millisecond
|
|
|
|
|
|
|
|
|
|
res := fmt.Sprintf("%02d:%02d.%03d", m, s, ms)
|
2021-03-02 23:34:38 +08:00
|
|
|
|
return res
|
|
|
|
|
}
|
2021-04-13 13:22:17 +08:00
|
|
|
|
|
|
|
|
|
// GetText will fetch lyric by time in seconds and mode, mode=0 means fetch current line,
|
|
|
|
|
// mode =1 means fetch next line, mode=-1 means fetch previous line
|
|
|
|
|
func (lyric *Lyric) GetText(time int) (string, error) {
|
|
|
|
|
if lyric.SyncedCaptions == nil {
|
|
|
|
|
return "", errors.New("no synced lyric found")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for i, v := range lyric.SyncedCaptions {
|
|
|
|
|
// here we want to show lyric 1 second earlier
|
|
|
|
|
if int(v.Timestamp) <= time*1000+1000 {
|
|
|
|
|
if i < len(lyric.SyncedCaptions)-1 {
|
|
|
|
|
next := lyric.SyncedCaptions[i+1]
|
|
|
|
|
if int(next.Timestamp) > time*1000+1000 {
|
|
|
|
|
return v.Text, nil
|
|
|
|
|
} else {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return v.Text, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "", nil
|
|
|
|
|
}
|