From 1521c0c4ba12b82bc5b9b99bd27ccae3a0c57cb3 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 16 Dec 2023 13:44:16 -0800 Subject: [PATCH 1/2] add ReleaseTypes to the album model --- .../jellyfin/jellyfinmediaprovider.go | 1 + backend/mediaprovider/model.go | 51 +++++++++++++++---- .../subsonic/subsonicmediaprovider.go | 49 ++++++++++++++++++ go.mod | 2 +- go.sum | 4 +- 5 files changed, 94 insertions(+), 13 deletions(-) diff --git a/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go b/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go index 002fe80..dd862d9 100644 --- a/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go +++ b/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go @@ -440,6 +440,7 @@ func fillAlbum(a *jellyfin.Album, album *mediaprovider.Album) { album.TrackCount = a.ChildCount album.Genres = a.Genres album.Favorite = a.UserData.IsFavorite + album.ReleaseTypes = mediaprovider.ReleaseTypeAlbum } func (j *jellyfinMediaProvider) toPlaylist(p *jellyfin.Playlist) *mediaprovider.Playlist { diff --git a/backend/mediaprovider/model.go b/backend/mediaprovider/model.go index 9e10253..2c1aa81 100644 --- a/backend/mediaprovider/model.go +++ b/backend/mediaprovider/model.go @@ -1,16 +1,47 @@ package mediaprovider +// Bit field flag for the ReleaseTypes property +type ReleaseType = int32 + +// Bit field of release types +type ReleaseTypes = int32 + +// Set of possible release types +// Taken from Picard: +// +// (a) https://picard-docs.musicbrainz.org/en/config/options_releases.html +// (b) https://musicbrainz.org/doc/Release_Group/Type +const ( + ReleaseTypeAlbum ReleaseType = 0x0001 + ReleaseTypeAudiobook ReleaseType = 0x0002 + ReleaseTypeAudioDrama ReleaseType = 0x0004 + ReleaseTypeBroadcast ReleaseType = 0x0008 + ReleaseTypeCompilation ReleaseType = 0x0010 + ReleaseTypeDemo ReleaseType = 0x0020 + ReleaseTypeDJMix ReleaseType = 0x0040 + ReleaseTypeEP ReleaseType = 0x0080 + ReleaseTypeFieldRecording ReleaseType = 0x0100 + ReleaseTypeInterview ReleaseType = 0x0200 + ReleaseTypeLive ReleaseType = 0x0400 + ReleaseTypeMixtape ReleaseType = 0x0800 + ReleaseTypeRemix ReleaseType = 0x1000 + ReleaseTypeSingle ReleaseType = 0x2000 + ReleaseTypeSoundtrack ReleaseType = 0x4000 + ReleaseTypeSpokenWord ReleaseType = 0x8000 +) + type Album struct { - ID string - CoverArtID string - Name string - Duration int - ArtistIDs []string - ArtistNames []string - Year int - Genres []string - TrackCount int - Favorite bool + ID string + CoverArtID string + Name string + Duration int + ArtistIDs []string + ArtistNames []string + Year int + Genres []string + TrackCount int + Favorite bool + ReleaseTypes ReleaseTypes } type AlbumWithTracks struct { diff --git a/backend/mediaprovider/subsonic/subsonicmediaprovider.go b/backend/mediaprovider/subsonic/subsonicmediaprovider.go index 44b51d2..c43a058 100644 --- a/backend/mediaprovider/subsonic/subsonicmediaprovider.go +++ b/backend/mediaprovider/subsonic/subsonicmediaprovider.go @@ -6,6 +6,7 @@ import ( "io" "math" "strconv" + "strings" "sync" "time" @@ -404,6 +405,54 @@ func fillAlbum(subAlbum *subsonic.AlbumID3, album *mediaprovider.Album) { album.TrackCount = subAlbum.SongCount album.Genres = genres album.Favorite = !subAlbum.Starred.IsZero() + album.ReleaseTypes = normalizeReleaseTypes(subAlbum.ReleaseTypes) + if subAlbum.IsCompilation { + album.ReleaseTypes |= mediaprovider.ReleaseTypeCompilation + } +} + +func normalizeReleaseTypes(releaseTypes []string) mediaprovider.ReleaseTypes { + var mpReleaseTypes mediaprovider.ReleaseTypes + for _, t := range releaseTypes { + switch strings.ToLower(strings.ReplaceAll(t, " ", "")) { + case "album": + mpReleaseTypes |= mediaprovider.ReleaseTypeAlbum + case "audiobook": + mpReleaseTypes |= mediaprovider.ReleaseTypeAudiobook + case "audiodrama": + mpReleaseTypes |= mediaprovider.ReleaseTypeAudioDrama + case "broadcast": + mpReleaseTypes |= mediaprovider.ReleaseTypeBroadcast + case "compilation": + mpReleaseTypes |= mediaprovider.ReleaseTypeCompilation + case "demo": + mpReleaseTypes |= mediaprovider.ReleaseTypeDemo + case "djmix": + mpReleaseTypes |= mediaprovider.ReleaseTypeDJMix + case "ep": + mpReleaseTypes |= mediaprovider.ReleaseTypeEP + case "fieldrecording": + mpReleaseTypes |= mediaprovider.ReleaseTypeFieldRecording + case "interview": + mpReleaseTypes |= mediaprovider.ReleaseTypeInterview + case "live": + mpReleaseTypes |= mediaprovider.ReleaseTypeLive + case "mixtape": + mpReleaseTypes |= mediaprovider.ReleaseTypeMixtape + case "remix": + mpReleaseTypes |= mediaprovider.ReleaseTypeRemix + case "single": + mpReleaseTypes |= mediaprovider.ReleaseTypeSingle + case "soundtrack": + mpReleaseTypes |= mediaprovider.ReleaseTypeSoundtrack + case "spokenword": + mpReleaseTypes |= mediaprovider.ReleaseTypeSpokenWord + } + } + if mpReleaseTypes == 0 { + return mediaprovider.ReleaseTypeAlbum + } + return mpReleaseTypes } func toArtistFromID3(ar *subsonic.ArtistID3) *mediaprovider.Artist { diff --git a/go.mod b/go.mod index 543e82e..0f7f008 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1 github.com/dweymouth/go-jellyfin v0.0.0-20231116161116-e800860bdacc github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee - github.com/dweymouth/go-subsonic v0.0.0-20231216015633-3797c9b3d94d + github.com/dweymouth/go-subsonic v0.0.0-20231216190641-c537a36d520c github.com/fsnotify/fsnotify v1.6.0 github.com/godbus/dbus/v5 v5.1.0 github.com/google/uuid v1.3.0 diff --git a/go.sum b/go.sum index 9625e37..35e169d 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ github.com/dweymouth/go-jellyfin v0.0.0-20231116161116-e800860bdacc h1:wJy4U12Ys github.com/dweymouth/go-jellyfin v0.0.0-20231116161116-e800860bdacc/go.mod h1:BMwS4vdjEYf1gmjPGSKCzWP/I6YlI6fkefJ9nsjBjaU= github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee h1:ZGyJ6wp7CAfT31BugypcF/TPKEy2RrGR9JFq1JOjOpY= github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee/go.mod h1:Ov0ieN90M7i+0k3OxhA/g1dozGs+UcPHDsMKqPgRDk0= -github.com/dweymouth/go-subsonic v0.0.0-20231216015633-3797c9b3d94d h1:E9heA09mvlF+i+fH8Cow7NgRpQ+cbJQkdFL0OtFo624= -github.com/dweymouth/go-subsonic v0.0.0-20231216015633-3797c9b3d94d/go.mod h1:OWtcumdQsan8uM6wmx6PqKhldaCthH10CQ+vb+94kzo= +github.com/dweymouth/go-subsonic v0.0.0-20231216190641-c537a36d520c h1:48xbtXN53Rrru1Y3Ywd/FIVohH4NoRMzvxEY4aY1s0M= +github.com/dweymouth/go-subsonic v0.0.0-20231216190641-c537a36d520c/go.mod h1:OWtcumdQsan8uM6wmx6PqKhldaCthH10CQ+vb+94kzo= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= From cb4329cf0849e4f08388877b4a4012374e82a5a3 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 16 Dec 2023 15:03:22 -0800 Subject: [PATCH 2/2] Display release types in album page --- ui/browsing/albumpage.go | 32 +++++++++++++++++------ ui/dialogs/quicksearch.go | 2 +- ui/dialogs/settingsdialog.go | 10 +++----- ui/util/util.go | 49 ++++++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 14 deletions(-) diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index f27e1a3..b6f7c39 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -170,11 +170,13 @@ type AlbumPageHeader struct { page *AlbumPage - cover *widgets.ImagePlaceholder - titleLabel *widget.RichText - artistLabel *widgets.MultiHyperlink - genreLabel *widgets.MultiHyperlink - miscLabel *widget.Label + cover *widgets.ImagePlaceholder + titleLabel *widget.RichText + releaseTypeLabel *widget.RichText + artistLabel *widgets.MultiHyperlink + artistLabelSpace *util.HSpace // TODO: remove when no longer needed + genreLabel *widgets.MultiHyperlink + miscLabel *widget.Label toggleFavButton *widgets.FavoriteButton @@ -196,10 +198,15 @@ func NewAlbumPageHeader(page *AlbumPage) *AlbumPageHeader { a.titleLabel.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{ SizeName: theme.SizeNameHeadingText, } + a.releaseTypeLabel = widget.NewRichText( + &widget.TextSegment{Text: "Album", Style: util.BoldRichTextStyle}, + &widget.TextSegment{Text: " by", Style: widget.RichTextStyle{Inline: true}}, + ) a.artistLabel = widgets.NewMultiHyperlink() a.artistLabel.OnTapped = func(id string) { a.page.contr.NavigateTo(controller.ArtistRoute(id)) } + a.artistLabelSpace = util.NewHSpace(0) // updated in Update a.genreLabel = widgets.NewMultiHyperlink() a.genreLabel.OnTapped = func(genre string) { a.page.contr.NavigateTo(controller.GenreRoute(genre)) @@ -237,13 +244,21 @@ func NewAlbumPageHeader(page *AlbumPage) *AlbumPageHeader { } a.toggleFavButton = widgets.NewFavoriteButton(func() { go a.toggleFavorited() }) - // Todo: there's got to be a way to make this less convoluted. Custom layout? + // TODO: Create a nicer custom layout to set this up properly + // OR once TODO in MultiHyperlink to use RichText as a provider is solved, + // extend MultiHyperlink to support prepending rich text segments and + // don't use two separate widgets here at all. + // n.b. cannot place MultiHyperlink in a HBox or it collapses in width + artistReleaseTypeLine := container.NewStack( + a.releaseTypeLabel, + container.NewBorder(nil, nil, a.artistLabelSpace, nil, a.artistLabel)) + // TODO: there's got to be a way to make this less convoluted. Custom layout? a.container = util.AddHeaderBackground( container.NewBorder(nil, nil, a.cover, nil, container.New(&layouts.VboxCustomPadding{ExtraPad: -10}, a.titleLabel, container.NewVBox( - container.New(&layouts.VboxCustomPadding{ExtraPad: -12}, a.artistLabel, a.genreLabel, a.miscLabel), + container.New(&layouts.VboxCustomPadding{ExtraPad: -12}, artistReleaseTypeLine, a.genreLabel, a.miscLabel), container.NewVBox( container.NewHBox(util.NewHSpace(2), playButton, shuffleBtn, menuBtn), container.NewHBox(util.NewHSpace(2), a.toggleFavButton), @@ -262,6 +277,9 @@ func (a *AlbumPageHeader) Update(album *mediaprovider.AlbumWithTracks, im *backe a.albumID = album.ID a.coverID = album.CoverArtID a.titleLabel.Segments[0].(*widget.TextSegment).Text = album.Name + a.releaseTypeLabel.Segments[0].(*widget.TextSegment).Text = util.DisplayReleaseType(album.ReleaseTypes) + a.releaseTypeLabel.Refresh() // needed so MinSize returns correct width below + a.artistLabelSpace.Width = a.releaseTypeLabel.MinSize().Width - 16 a.artistLabel.BuildSegments(album.ArtistNames, album.ArtistIDs) a.genreLabel.BuildSegments(album.Genres, album.Genres) a.miscLabel.SetText(formatMiscLabelStr(album)) diff --git a/ui/dialogs/quicksearch.go b/ui/dialogs/quicksearch.go index 9dccd74..0a33bab 100644 --- a/ui/dialogs/quicksearch.go +++ b/ui/dialogs/quicksearch.go @@ -74,7 +74,7 @@ func NewQuickSearch(mp mediaprovider.MediaProvider, im util.ImageFetcher) *Quick ) dismissBtn := widget.NewButton("Close", q.onDismiss) - title := widget.NewRichText(&widget.TextSegment{Text: "Quick Search", Style: boldStyle}) + title := widget.NewRichText(&widget.TextSegment{Text: "Quick Search", Style: util.BoldRichTextStyle}) title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter q.content = container.NewBorder( container.NewVBox(title, se), diff --git a/ui/dialogs/settingsdialog.go b/ui/dialogs/settingsdialog.go index ada8961..91477e9 100644 --- a/ui/dialogs/settingsdialog.go +++ b/ui/dialogs/settingsdialog.go @@ -26,8 +26,6 @@ import ( "fyne.io/fyne/v2/widget" ) -var boldStyle = widget.RichTextStyle{TextStyle: fyne.TextStyle{Bold: true}} - type SettingsDialog struct { widget.BaseWidget @@ -236,7 +234,7 @@ func (s *SettingsDialog) createGeneralTab() *container.TabItem { container.NewHBox(systemTrayEnable, closeToTray), s.newSectionSeparator(), - widget.NewRichText(&widget.TextSegment{Text: "Scrobbling", Style: boldStyle}), + widget.NewRichText(&widget.TextSegment{Text: "Scrobbling", Style: util.BoldRichTextStyle}), scrobbleEnabled, container.NewHBox( widget.NewLabel("Scrobble when"), @@ -339,7 +337,7 @@ func (s *SettingsDialog) createPlaybackTab() *container.TabItem { )), s.newSectionSeparator(), - widget.NewRichText(&widget.TextSegment{Text: "ReplayGain", Style: boldStyle}), + widget.NewRichText(&widget.TextSegment{Text: "ReplayGain", Style: util.BoldRichTextStyle}), container.New(layout.NewFormLayout(), widget.NewLabel("ReplayGain mode"), container.NewGridWithColumns(2, replayGainSelect), widget.NewLabel("ReplayGain preamp"), container.NewHBox(preampGain, widget.NewLabel("dB")), @@ -423,10 +421,10 @@ func (s *SettingsDialog) createExperimentalTab(window fyne.Window) *container.Ta return container.NewTabItem("Experimental", container.NewVBox( warningLabel, s.newSectionSeparator(), - widget.NewRichText(&widget.TextSegment{Text: "UI Scaling", Style: boldStyle}), + widget.NewRichText(&widget.TextSegment{Text: "UI Scaling", Style: util.BoldRichTextStyle}), uiScaleRadio, s.newSectionSeparator(), - widget.NewRichText(&widget.TextSegment{Text: "Application Font", Style: boldStyle}), + widget.NewRichText(&widget.TextSegment{Text: "Application Font", Style: util.BoldRichTextStyle}), container.New(layout.NewFormLayout(), widget.NewLabel("Normal font"), container.NewBorder(nil, nil, nil, normalFontBrowse, normalFontEntry), widget.NewLabel("Bold font"), container.NewBorder(nil, nil, nil, boldFontBrowse, boldFontEntry), diff --git a/ui/util/util.go b/ui/util/util.go index 35c3510..643c02e 100644 --- a/ui/util/util.go +++ b/ui/util/util.go @@ -14,11 +14,14 @@ import ( "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" + "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/ui/layouts" myTheme "github.com/dweymouth/supersonic/ui/theme" "golang.org/x/net/html" ) +var BoldRichTextStyle = widget.RichTextStyle{TextStyle: fyne.TextStyle{Bold: true}, Inline: true} + func SecondsToTimeString(s float64) string { if s < 0 { s = 0 @@ -109,6 +112,52 @@ func PlaintextFromHTMLString(s string) string { return text } +func DisplayReleaseType(releaseTypes mediaprovider.ReleaseTypes) string { + baseType := "Album" + switch { + case releaseTypes&mediaprovider.ReleaseTypeAudiobook > 0: + baseType = "Audiobook" + case releaseTypes&mediaprovider.ReleaseTypeAudioDrama > 0: + baseType = "Audio Drama" + case releaseTypes&mediaprovider.ReleaseTypeBroadcast > 0: + baseType = "Broadcast" + case releaseTypes&mediaprovider.ReleaseTypeDJMix > 0: + baseType = "DJ-Mix" + case releaseTypes&mediaprovider.ReleaseTypeEP > 0: + baseType = "EP" + case releaseTypes&mediaprovider.ReleaseTypeFieldRecording > 0: + baseType = "Field Recording" + case releaseTypes&mediaprovider.ReleaseTypeInterview > 0: + baseType = "Interview" + case releaseTypes&mediaprovider.ReleaseTypeMixtape > 0: + baseType = "Mixtape" + case releaseTypes&mediaprovider.ReleaseTypeSingle > 0: + baseType = "Single" + case releaseTypes&mediaprovider.ReleaseTypeSoundtrack > 0: + baseType = "Soundtrack" + } + + var modifiers []string + if releaseTypes&mediaprovider.ReleaseTypeLive > 0 { + modifiers = append(modifiers, "Live") + } + if releaseTypes&mediaprovider.ReleaseTypeDemo > 0 { + modifiers = append(modifiers, "Demo") + } + if releaseTypes&mediaprovider.ReleaseTypeRemix > 0 { + modifiers = append(modifiers, "Remix") + } + if releaseTypes&mediaprovider.ReleaseTypeSpokenWord > 0 { + modifiers = append(modifiers, "Spoken Word") + } + if releaseTypes&mediaprovider.ReleaseTypeCompilation > 0 { + modifiers = append(modifiers, "Compilation") + } + + modifiers = append(modifiers, baseType) + return strings.Join(modifiers, " ") +} + func NewRatingSubmenu(onSetRating func(int)) *fyne.MenuItem { newRatingMenuItem := func(rating int) *fyne.MenuItem { label := "(none)"