Skip to content

Commit

Permalink
feat(lyric): add package
Browse files Browse the repository at this point in the history
  • Loading branch information
Topvennie committed Nov 30, 2024
1 parent 0a6eb38 commit 56de0fd
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 0 deletions.
111 changes: 111 additions & 0 deletions internal/pkg/lyrics/lrc.go
Original file line number Diff line number Diff line change
@@ -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
}
31 changes: 31 additions & 0 deletions internal/pkg/lyrics/lyrics.go
Original file line number Diff line number Diff line change
@@ -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)
}
47 changes: 47 additions & 0 deletions internal/pkg/lyrics/plain.go
Original file line number Diff line number Diff line change
@@ -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{}
}

0 comments on commit 56de0fd

Please sign in to comment.