diff --git a/internal/common/album.go b/internal/common/album.go new file mode 100644 index 0000000..1ce2ea7 --- /dev/null +++ b/internal/common/album.go @@ -0,0 +1,189 @@ +package common + +import ( + "crypto/md5" + "fmt" + "io" + "path/filepath" + "slices" + "strings" + "time" + + "suruatoel.xyz/mcg/internal/i18n" +) + +type Album struct { + title string + songs []*Song + artists []string + pathes []string + dates []string + length uint64 + lastModified time.Time +} + +func NewAlbum(title string) *Album { + album := Album{ + title: title, + } + + return &album +} + +func (a *Album) Title() string { + return a.title +} + +func (a *Album) AddSong(song *Song) { + a.songs = append(a.songs, song) + a.length += song.Length() + + for _, artist := range song.AlbumArtists() { + if !slices.Contains(a.artists, artist) { + a.artists = append(a.artists, artist) + } + } + + if !slices.Contains(a.dates, song.Date()) { + a.dates = append(a.dates, song.Date()) + } + + path := filepath.Base(song.File()) + if !slices.Contains(a.pathes, path) { + a.pathes = append(a.pathes, path) + } + + if !song.LastModified().After(a.lastModified) { + a.lastModified = song.LastModified() + } +} + +func (a *Album) Songs() []*Song { + return a.songs +} + +func (a *Album) Artists() []string { + return a.artists +} + +func (a *Album) Dates() []string { + return a.dates +} + +func (a *Album) Length() uint64 { + return a.length +} + +func (a *Album) LastModified() time.Time { + return a.lastModified +} + +func (a *Album) ID() string { + return generateAlbumId(a.title) +} + +func (a *Album) Tooltip() string { + artists := strings.Join(a.Artists(), ", ") + dates := strings.Join(a.dates, ", ") + minutes := a.length / 60 + seconds := a.length - minutes*60 + length := fmt.Sprintf(i18n.T("%d:%d minutes"), minutes, seconds) + + return strings.Join( + []string{ + a.title, + dates, + artists, + length, + }, + "\n", + ) +} + +type PlaylistAlbum struct { + *Album + songs []*PlaylistSong + length uint64 +} + +func NewPlaylistAlbum(album *Album) *PlaylistAlbum { + playlistAlbum := PlaylistAlbum{ + Album: album, + } + + return &playlistAlbum +} + +func (a *PlaylistAlbum) AddSong(playlistSong *PlaylistSong) { + a.songs = append(a.songs, playlistSong) + + song := playlistSong.Song + a.length += song.Length() + + for _, artist := range song.AlbumArtists() { + if !slices.Contains(a.artists, artist) { + a.artists = append(a.artists, artist) + } + } + + if !slices.Contains(a.dates, song.Date()) { + a.dates = append(a.dates, song.Date()) + } + + path := filepath.Base(song.File()) + if !slices.Contains(a.pathes, path) { + a.pathes = append(a.pathes, path) + } +} + +func (a *PlaylistAlbum) Songs() []*PlaylistSong { + return a.songs +} + +func (a *PlaylistAlbum) Length() uint64 { + return a.length +} + +func (a *Album) IsMatch(query string) bool { + if query == "" { + return true + } + + for keyword := range strings.SplitSeq(query, " ") { + if !a.isKeywordMatch(keyword) { + return false + } + } + + return true +} + +func (a *Album) isKeywordMatch(keyword string) bool { + if strings.Contains(strings.ToLower(a.title), keyword) { + return true + } + for _, artist := range a.artists { + if strings.Contains(strings.ToLower(artist), keyword) { + return true + } + } + for _, song := range a.songs { + if strings.Contains(strings.ToLower(song.Title()), keyword) { + return true + } + if strings.Contains(strings.ToLower(song.File()), keyword) { + return true + } + } + + return false +} + +func generateAlbumId(values ...string) string { + h := md5.New() + for _, value := range values { + io.WriteString(h, value) + } + + return fmt.Sprintf("%x", h.Sum(nil)) +} diff --git a/internal/common/outputdevice.go b/internal/common/outputdevice.go new file mode 100644 index 0000000..df6aa49 --- /dev/null +++ b/internal/common/outputdevice.go @@ -0,0 +1,14 @@ +package common + +type OutputDevice struct { + ID string + Name string + Enabled bool +} + +func NewOutputDevice(id string, name string) *OutputDevice { + return &OutputDevice{ + ID: id, + Name: name, + } +} diff --git a/internal/common/song.go b/internal/common/song.go new file mode 100644 index 0000000..1b81806 --- /dev/null +++ b/internal/common/song.go @@ -0,0 +1,122 @@ +package common + +import ( + "slices" + "time" +) + +type Song struct { + artists []string + title string + file string + albumartists []string + track string + length uint64 + date string + lastModified time.Time +} + +func NewSong(artist string, title string, file string) *Song { + song := Song{ + artists: []string{artist}, + title: title, + file: file, + } + + return &song +} + +func (s *Song) AddArtist(artist string) { + s.artists = append(s.artists, artist) +} + +func (s *Song) AddAlbumartist(artist string) { + s.albumartists = append(s.albumartists, artist) +} + +func (s *Song) SongArtists() []string { + if len(s.albumartists) > 0 { + songArtists := []string{} + for _, artist := range s.artists { + if !slices.Contains(s.albumartists, artist) { + songArtists = append(songArtists, artist) + } + } + + return songArtists + } + + return []string{} +} + +func (s *Song) AlbumArtists() []string { + if len(s.albumartists) > 0 { + return s.albumartists + } + + return s.artists +} + +func (s *Song) Title() string { + return s.title +} + +func (s *Song) File() string { + return s.file +} + +func (s *Song) SetTrack(track string) { + s.track = track +} + +func (s *Song) Track() string { + return s.track +} + +func (s *Song) SetLength(length uint64) { + s.length = length +} + +func (s *Song) Length() uint64 { + return s.length +} + +func (s *Song) SetDate(date string) { + s.date = date +} + +func (s *Song) Date() string { + return s.date +} + +func (s *Song) SetLastModified(lastModified time.Time) { + s.lastModified = lastModified +} + +func (s *Song) LastModified() time.Time { + return s.lastModified +} + +type PlaylistSong struct { + *Song + id string + pos uint64 +} + +func NewPlaylistSong(song *Song, id string, pos uint64) *PlaylistSong { + playlistSong := &PlaylistSong{ + Song: song, + id: id, + pos: pos, + } + + return playlistSong +} + +func (t *PlaylistSong) ID() string { + return t.id +} + +func (t *PlaylistSong) Pos() uint64 { + return t.pos +} diff --git a/internal/common/sort.go b/internal/common/sort.go new file mode 100644 index 0000000..7c62117 --- /dev/null +++ b/internal/common/sort.go @@ -0,0 +1,89 @@ +package common + +import "sort" + +type albumLessFunc func(p1, p2 *Album) bool + +type albumSorter struct { + albums []*Album + less []albumLessFunc +} + +func (s *albumSorter) Len() int { + return len(s.albums) +} + +func (s *albumSorter) Swap(i, j int) { + s.albums[i], s.albums[j] = s.albums[j], s.albums[i] +} + +func (s *albumSorter) Less(i, j int) bool { + p, q := s.albums[i], s.albums[j] + + var k int + for k = 0; k < len(s.less)-1; k++ { + less := s.less[k] + switch { + case less(p, q): + return true + case less(q, p): + return false + } + } + + return s.less[k](p, q) +} + +func (s *albumSorter) Sort(albums []*Album) { + s.albums = albums + sort.Sort(s) +} + +func OrderAlbumBy(less ...albumLessFunc) *albumSorter { + return &albumSorter{ + less: less, + } +} + +func SortAlbums(album []*Album, field string) { + title := orderAlbumByTitle + artists := orderAlbumByArtists + years := orderAlbumByYears + modified := orderAlbumByModified + + var sorter *albumSorter + switch field { + case "artist": + sorter = OrderAlbumBy(artists, title, years, modified) + case "year": + sorter = OrderAlbumBy(years, title, artists, modified) + case "modified": + sorter = OrderAlbumBy(modified, title, artists, years) + default: + sorter = OrderAlbumBy(title, artists, years, modified) + } + + sorter.Sort(album) +} + +func orderAlbumByTitle(c1, c2 *Album) bool { + return c1.Title() < c2.Title() +} + +func orderAlbumByArtists(left, right *Album) bool { + if len(left.Artists()) > 0 && len(right.Artists()) > 0 { + return left.Artists()[0] < right.Artists()[0] + } + return false +} + +func orderAlbumByYears(c1, c2 *Album) bool { + if len(c1.Dates()) > 0 && len(c2.Dates()) > 0 { + return c1.Dates()[0] < c2.Dates()[0] + } + return false +} + +func orderAlbumByModified(c1, c2 *Album) bool { + return c1.LastModified().Before(c2.LastModified()) +}