diff --git a/internal/pkg/lyrics/lrc.go b/internal/pkg/lyrics/lrc.go new file mode 100644 index 0000000..7c6b3a2 --- /dev/null +++ b/internal/pkg/lyrics/lrc.go @@ -0,0 +1,111 @@ +package lyrics + +import ( + "regexp" + "strconv" + "strings" + "time" + + "github.com/zeusWPI/scc/internal/pkg/db/dto" +) + +// LRC represents synced lyrics +type LRC struct { + song dto.Song + lyrics []Lyric + i int +} + +func newLRC(song *dto.Song) Lyrics { + return &LRC{song: *song, lyrics: parseLRC(song.Lyrics, time.Duration(song.DurationMS)), i: 0} +} + +// GetSong returns the song associated to the lyrics +func (l *LRC) GetSong() dto.Song { + return l.song +} + +// Previous provides the previous `amount` of lyrics without affecting the current lyric +func (l *LRC) Previous(amount int) []Lyric { + lyrics := make([]Lyric, 0, amount) + + for i := 1; i <= amount; i++ { + if l.i-i < 0 { + break + } + + lyrics = append(lyrics, l.lyrics[l.i-1]) + } + + return lyrics +} + +// Next provides the next lyric if any. +// If the song is finished the boolean is set to false +func (l *LRC) Next() (Lyric, bool) { + if l.i >= len(l.lyrics) { + return Lyric{}, false + } + + l.i++ + return l.lyrics[l.i-1], true +} + +// Upcoming provides the next `amount` lyrics without affecting the current lyric +func (l *LRC) Upcoming(amount int) []Lyric { + lyrics := make([]Lyric, 0, amount) + + for i := 1; i <= amount; i++ { + if i+l.i >= len(l.lyrics) { + break + } + + lyrics = append(lyrics, l.lyrics[i+l.i]) + } + + return lyrics +} + +func parseLRC(text string, totalDuration time.Duration) []Lyric { + lines := strings.Split(text, "\n") + + lyrics := make([]Lyric, 0, len(lines)) + var previousTimestamp time.Duration + + re, err := regexp.Compile(`^\[(\d{2}):(\d{2})\.(\d{2})\] (.+)$`) + if err != nil { + return lyrics + } + + // Add first lyric (no text) + lyrics = append(lyrics, Lyric{Text: ""}) + previousTimestamp = time.Duration(0) + + for i, line := range lines { + match := re.FindStringSubmatch(line) + if match == nil { + continue + } + + // Construct timestamp + minutes, _ := strconv.Atoi(match[1]) + seconds, _ := strconv.Atoi(match[2]) + hundredths, _ := strconv.Atoi(match[3]) + timestamp := time.Duration(minutes)*time.Minute + + time.Duration(seconds)*time.Second + + time.Duration(hundredths)*10*time.Millisecond + + t := match[4] + + lyrics = append(lyrics, Lyric{Text: t}) + + // Set duration of previous lyric + lyrics[i-1].Duration = timestamp - previousTimestamp + previousTimestamp = timestamp + } + + // Set duration of last lyric + lyrics[len(lines)-1].Duration = totalDuration - previousTimestamp + + return lyrics +} diff --git a/internal/pkg/lyrics/lyrics.go b/internal/pkg/lyrics/lyrics.go new file mode 100644 index 0000000..f66cbd3 --- /dev/null +++ b/internal/pkg/lyrics/lyrics.go @@ -0,0 +1,31 @@ +// Package lyrics provides a way to work with both synced and plain lyrics +package lyrics + +import ( + "time" + + "github.com/zeusWPI/scc/internal/pkg/db/dto" +) + +// Lyrics is the common interface for different lyric types +type Lyrics interface { + GetSong() dto.Song + Previous(int) []Lyric + Next() (Lyric, bool) + Upcoming(int) []Lyric +} + +// Lyric represents a single lyric line. +type Lyric struct { + Text string + Duration time.Duration +} + +// New returns a new object that implements the Lyrics interface +func New(song *dto.Song) Lyrics { + if song.LyricsType == "synced" { + return newLRC(song) + } + + return newPlain(song) +} diff --git a/internal/pkg/lyrics/plain.go b/internal/pkg/lyrics/plain.go new file mode 100644 index 0000000..4afaa48 --- /dev/null +++ b/internal/pkg/lyrics/plain.go @@ -0,0 +1,47 @@ +package lyrics + +import ( + "time" + + "github.com/zeusWPI/scc/internal/pkg/db/dto" +) + +// Plain represents lyrics that don't have timestamps or songs without lyrics +type Plain struct { + song dto.Song + lyrics Lyric + given bool +} + +func newPlain(song *dto.Song) Lyrics { + lyric := Lyric{ + Text: song.Lyrics, + Duration: time.Duration(song.DurationMS), + } + return &Plain{song: *song, lyrics: lyric, given: false} +} + +// GetSong returns the song associated to the lyrics +func (p *Plain) GetSong() dto.Song { + return p.song +} + +// Previous provides the previous `amount` of lyrics without affecting the current lyric +func (p *Plain) Previous(amount int) []Lyric { + return []Lyric{} +} + +// Next provides the next lyric. +// If the lyrics are finished the boolean is set to false +func (p *Plain) Next() (Lyric, bool) { + if p.given { + return Lyric{}, false + } + + return p.lyrics, true +} + +// Upcoming provides the next `amount` lyrics without affecting the current lyric +func (p *Plain) Upcoming(amount int) []Lyric { + return []Lyric{} +}