589 lines
16 KiB
Go
589 lines
16 KiB
Go
package browsing
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"log"
|
|
"net/url"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cenkalti/dominantcolor"
|
|
"github.com/dweymouth/supersonic/backend"
|
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
|
"github.com/dweymouth/supersonic/backend/player"
|
|
"github.com/dweymouth/supersonic/backend/player/mpv"
|
|
"github.com/dweymouth/supersonic/sharedutil"
|
|
"github.com/dweymouth/supersonic/ui/controller"
|
|
"github.com/dweymouth/supersonic/ui/layouts"
|
|
myTheme "github.com/dweymouth/supersonic/ui/theme"
|
|
"github.com/dweymouth/supersonic/ui/util"
|
|
"github.com/dweymouth/supersonic/ui/widgets"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/canvas"
|
|
"fyne.io/fyne/v2/container"
|
|
"fyne.io/fyne/v2/lang"
|
|
"fyne.io/fyne/v2/layout"
|
|
"fyne.io/fyne/v2/theme"
|
|
"fyne.io/fyne/v2/widget"
|
|
)
|
|
|
|
type NowPlayingPage struct {
|
|
widget.BaseWidget
|
|
|
|
nowPlayingPageState
|
|
|
|
// volatile state
|
|
nowPlaying mediaprovider.MediaItem
|
|
nowPlayingID string
|
|
curLyricsID string // id of track currently shown in lyrics
|
|
curRelatedID string // id of track currrently used to populate related list
|
|
totalTime float64
|
|
lastPlayPos float64
|
|
queue []mediaprovider.MediaItem
|
|
related []*mediaprovider.Track
|
|
alreadyLoaded bool
|
|
|
|
// widgets for render
|
|
background *canvas.LinearGradient
|
|
queueList *widgets.PlayQueueList
|
|
relatedList *widgets.PlayQueueList
|
|
lyricsViewer *widgets.LyricsViewer
|
|
card *widgets.LargeNowPlayingCard
|
|
statusLabel *widget.Label
|
|
tabs *container.AppTabs
|
|
lyricsLoading *widgets.LoadingDots
|
|
relatedLoading *widgets.LoadingDots
|
|
container *fyne.Container
|
|
|
|
// cancel funcs for background fetch tasks
|
|
lyricFetchCancel context.CancelFunc
|
|
imageLoadCancel context.CancelFunc
|
|
relatedFetchCancel context.CancelFunc
|
|
}
|
|
|
|
type nowPlayingPageState struct {
|
|
conf *backend.NowPlayingPageConfig
|
|
contr *controller.Controller
|
|
pool *util.WidgetPool
|
|
sm *backend.ServerManager
|
|
pm *backend.PlaybackManager
|
|
im *backend.ImageManager
|
|
mp mediaprovider.MediaProvider
|
|
canRate bool
|
|
canShare bool
|
|
lrcFetch *backend.LrcLibFetcher
|
|
}
|
|
|
|
func NewNowPlayingPage(
|
|
conf *backend.NowPlayingPageConfig,
|
|
contr *controller.Controller,
|
|
pool *util.WidgetPool,
|
|
sm *backend.ServerManager,
|
|
im *backend.ImageManager,
|
|
pm *backend.PlaybackManager,
|
|
mp mediaprovider.MediaProvider,
|
|
canRate bool,
|
|
canShare bool,
|
|
lrcLibFetcher *backend.LrcLibFetcher,
|
|
) *NowPlayingPage {
|
|
state := nowPlayingPageState{
|
|
conf: conf, contr: contr, pool: pool, sm: sm, im: im, pm: pm, mp: mp, canRate: canRate, canShare: canShare, lrcFetch: lrcLibFetcher,
|
|
}
|
|
if page, ok := pool.Obtain(util.WidgetTypeNowPlayingPage).(*NowPlayingPage); ok && page != nil {
|
|
page.nowPlayingPageState = state
|
|
page.Reload()
|
|
return page
|
|
}
|
|
a := &NowPlayingPage{nowPlayingPageState: state}
|
|
a.ExtendBaseWidget(a)
|
|
|
|
doFmtStatus := func() { fyne.Do(a.formatStatusLine) }
|
|
pm.OnPaused(doFmtStatus)
|
|
pm.OnPlaying(doFmtStatus)
|
|
pm.OnStopped(doFmtStatus)
|
|
|
|
a.card = widgets.NewLargeNowPlayingCard()
|
|
a.card.OnAlbumNameTapped = func() {
|
|
contr.NavigateTo(controller.AlbumRoute(a.nowPlaying.Metadata().AlbumID))
|
|
}
|
|
a.card.OnArtistNameTapped = func(artistID string) {
|
|
contr.NavigateTo(controller.ArtistRoute(artistID))
|
|
}
|
|
a.card.OnRadioURLTapped = func(urlText string) {
|
|
if u, err := url.Parse(urlText); err == nil {
|
|
fyne.CurrentApp().OpenURL(u)
|
|
}
|
|
}
|
|
a.card.OnSetFavorite = func(fav bool) {
|
|
a.contr.SetTrackFavorites([]string{a.nowPlayingID}, fav)
|
|
}
|
|
a.card.OnSetRating = func(rating int) {
|
|
a.contr.SetTrackRatings([]string{a.nowPlayingID}, rating)
|
|
}
|
|
|
|
a.queueList = widgets.NewPlayQueueList(a.im, false)
|
|
a.relatedList = widgets.NewPlayQueueList(a.im, true)
|
|
a.queueList.Reorderable = true
|
|
|
|
a.contr.ConnectPlayQueuelistActions(a.queueList)
|
|
// override OnSetRating and Favorite so we can also update the
|
|
// LargeNowPlayingCard's displayed rating and favorite
|
|
a.queueList.OnSetRating = func(trackIDs []string, rating int) {
|
|
contr.SetTrackRatings(trackIDs, rating)
|
|
if slices.Contains(trackIDs, a.nowPlayingID) {
|
|
a.card.SetDisplayedRating(rating)
|
|
}
|
|
}
|
|
a.queueList.OnSetFavorite = func(trackIDs []string, fav bool) {
|
|
contr.SetTrackFavorites(trackIDs, fav)
|
|
if slices.Contains(trackIDs, a.nowPlayingID) {
|
|
a.card.SetDisplayedFavorite(fav)
|
|
}
|
|
}
|
|
|
|
a.relatedList.OnDownload = a.queueList.OnDownload
|
|
a.relatedList.OnShare = a.queueList.OnShare
|
|
a.relatedList.OnAddToPlaylist = a.queueList.OnAddToPlaylist
|
|
a.relatedList.OnShowArtistPage = a.queueList.OnShowArtistPage
|
|
a.relatedList.OnSetRating = a.queueList.OnSetRating
|
|
a.relatedList.OnSetFavorite = a.queueList.OnSetFavorite
|
|
a.relatedList.OnPlayItemAt = func(idx int) {
|
|
a.pm.LoadTracks(a.related, backend.Replace, false)
|
|
a.pm.PlayTrackAt(idx)
|
|
}
|
|
a.relatedList.OnAddToQueue = func(items []mediaprovider.MediaItem) {
|
|
a.pm.LoadItems(items, backend.Append, false)
|
|
}
|
|
a.relatedList.OnPlaySelection = func(items []mediaprovider.MediaItem, shuffle bool) {
|
|
a.pm.LoadItems(items, backend.Replace, shuffle)
|
|
a.pm.PlayFromBeginning()
|
|
}
|
|
a.relatedList.OnPlaySelectionNext = func(items []mediaprovider.MediaItem) {
|
|
a.pm.LoadItems(items, backend.InsertNext, false)
|
|
}
|
|
a.relatedList.OnPlaySongRadio = func(track *mediaprovider.Track) {
|
|
go func() {
|
|
if err := a.pm.PlaySimilarSongs(track.ID); err != nil {
|
|
fyne.Do(func() {
|
|
a.contr.ToastProvider.ShowErrorToast(lang.L("Unable to play song radio"))
|
|
})
|
|
}
|
|
}()
|
|
}
|
|
a.relatedList.OnShowTrackInfo = func(track *mediaprovider.Track) {
|
|
a.contr.ShowTrackInfoDialog(track)
|
|
}
|
|
a.lyricsViewer = widgets.NewLyricsViewer()
|
|
a.statusLabel = widget.NewLabel(lang.L("Stopped"))
|
|
|
|
a.Reload()
|
|
return a
|
|
}
|
|
|
|
func (a *NowPlayingPage) CreateRenderer() fyne.WidgetRenderer {
|
|
if a.container == nil {
|
|
initialTab := 0
|
|
if a.conf.InitialView == "Lyrics" {
|
|
initialTab = 1
|
|
} else if a.conf.InitialView == "Related" {
|
|
initialTab = 2
|
|
}
|
|
_ = initialTab
|
|
paddedLayout := &layouts.PercentPadLayout{
|
|
LeftRightObjectPercent: .8,
|
|
TopBottomObjectPercent: .8,
|
|
}
|
|
a.lyricsLoading = widgets.NewLoadingDots()
|
|
a.relatedLoading = widgets.NewLoadingDots()
|
|
a.tabs = container.NewAppTabs(
|
|
container.NewTabItem(lang.L("Play Queue"),
|
|
container.NewBorder(layout.NewSpacer(), nil, nil, nil, a.queueList)),
|
|
container.NewTabItem(lang.L("Lyrics"), container.NewStack(
|
|
a.lyricsViewer,
|
|
container.NewCenter(a.lyricsLoading))),
|
|
container.NewTabItem(lang.L("Related"), container.NewStack(
|
|
a.relatedList,
|
|
container.NewCenter(a.relatedLoading))),
|
|
)
|
|
a.tabs.SelectIndex(initialTab)
|
|
a.tabs.OnSelected = func(*container.TabItem) {
|
|
idx := a.tabs.SelectedIndex()
|
|
a.saveSelectedTab(idx)
|
|
if idx == 1 /*lyrics*/ {
|
|
a.updateLyrics()
|
|
} else if idx == 2 /*related*/ {
|
|
a.updateRelatedList()
|
|
}
|
|
}
|
|
if initialTab == 1 /*lyrics*/ {
|
|
a.updateLyrics()
|
|
} else if initialTab == 2 /*related*/ {
|
|
a.updateRelatedList()
|
|
}
|
|
c := theme.Color(myTheme.ColorNamePageBackground)
|
|
a.background = canvas.NewLinearGradient(c, c, 0)
|
|
|
|
mainContent := container.NewGridWithColumns(2,
|
|
container.New(paddedLayout, a.card),
|
|
container.New(paddedLayout,
|
|
util.AddHeaderBackgroundWithColorName(
|
|
a.tabs, myTheme.ColorNameNowPlayingPanel)))
|
|
a.container = container.NewStack(
|
|
a.background,
|
|
mainContent,
|
|
container.NewVBox(
|
|
layout.NewSpacer(),
|
|
container.NewBorder(nil, nil, util.NewHSpace(1), util.NewHSpace(1),
|
|
myTheme.NewThemedRectangle(theme.ColorNameInputBorder)),
|
|
a.statusLabel,
|
|
),
|
|
)
|
|
}
|
|
return widget.NewSimpleRenderer(a.container)
|
|
}
|
|
|
|
func (a *NowPlayingPage) Save() SavedPage {
|
|
if a.imageLoadCancel != nil {
|
|
a.imageLoadCancel()
|
|
}
|
|
a.alreadyLoaded = false
|
|
nps := a.nowPlayingPageState
|
|
a.pool.Release(util.WidgetTypeNowPlayingPage, a)
|
|
return &nps
|
|
}
|
|
|
|
func (a *NowPlayingPage) Route() controller.Route {
|
|
return controller.NowPlayingRoute()
|
|
}
|
|
|
|
var _ Scrollable = (*NowPlayingPage)(nil)
|
|
|
|
func (a *NowPlayingPage) Scroll(delta float32) {
|
|
switch a.tabs.SelectedIndex() {
|
|
case 0: /*play queue*/
|
|
a.queueList.Scroll(delta)
|
|
case 1: /*lyrics*/
|
|
case 2: /*related*/
|
|
a.relatedList.Scroll(delta)
|
|
}
|
|
}
|
|
|
|
var _ CanShowNowPlaying = (*NowPlayingPage)(nil)
|
|
|
|
func (a *NowPlayingPage) OnSongChange(song mediaprovider.MediaItem, lastScrobbledIfAny *mediaprovider.Track) {
|
|
a.nowPlaying = song
|
|
if a.imageLoadCancel != nil {
|
|
a.imageLoadCancel()
|
|
}
|
|
a.nowPlayingID = sharedutil.MediaItemIDOrEmptyStr(song)
|
|
a.queueList.SetNowPlaying(a.nowPlayingID)
|
|
if !a.alreadyLoaded {
|
|
a.queueList.ScrollToNowPlaying()
|
|
a.alreadyLoaded = true
|
|
}
|
|
a.relatedList.SetNowPlaying(a.nowPlayingID)
|
|
|
|
a.card.Update(song)
|
|
if song == nil {
|
|
a.card.SetCoverImage(nil)
|
|
} else {
|
|
a.imageLoadCancel = a.im.GetFullSizeCoverArtAsync(song.Metadata().CoverArtID, a.onImageLoaded)
|
|
}
|
|
|
|
if a.tabs != nil && a.tabs.SelectedIndex() == 1 /*lyrics*/ {
|
|
a.updateLyrics()
|
|
} else if a.tabs != nil && a.tabs.SelectedIndex() == 2 /*related*/ {
|
|
a.updateRelatedList()
|
|
}
|
|
}
|
|
|
|
func (a *NowPlayingPage) onImageLoaded(img image.Image, err error) {
|
|
if err != nil {
|
|
log.Printf("error loading cover art: %v\n", err)
|
|
}
|
|
|
|
fyne.Do(func() { a.card.SetCoverImage(img) })
|
|
if img == nil {
|
|
return
|
|
}
|
|
|
|
c := dominantcolor.Find(img)
|
|
if c == a.background.StartColor {
|
|
return
|
|
}
|
|
|
|
// Fyne animation starting is currently thread-safe,
|
|
// despite not being marked as such
|
|
// TODO: if this changes, use fyne.Do
|
|
anim := canvas.NewColorRGBAAnimation(
|
|
a.background.StartColor, c, 75*time.Millisecond, func(c color.Color) {
|
|
a.background.StartColor = c
|
|
a.background.Refresh()
|
|
})
|
|
anim.Start()
|
|
}
|
|
|
|
func (a *NowPlayingPage) updateLyrics() {
|
|
if a.lyricFetchCancel != nil {
|
|
a.lyricFetchCancel()
|
|
}
|
|
|
|
if a.nowPlayingID == a.curLyricsID {
|
|
if a.nowPlayingID != "" {
|
|
// just need to sync the current time
|
|
a.lyricsViewer.OnSeeked(a.lastPlayPos)
|
|
return
|
|
}
|
|
}
|
|
if a.nowPlaying == nil || a.nowPlaying.Metadata().Type == mediaprovider.MediaItemTypeRadioStation {
|
|
a.lyricsViewer.SetLyrics(nil)
|
|
a.curLyricsID = ""
|
|
return
|
|
}
|
|
a.curLyricsID = a.nowPlayingID
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
a.lyricFetchCancel = cancel
|
|
a.lyricsLoading.Start()
|
|
// set the widget to an empty (not nil) lyric during fetch
|
|
// to keep it from showing "Lyrics not available"
|
|
a.lyricsViewer.SetLyrics(&mediaprovider.Lyrics{Synced: true,
|
|
Lines: []mediaprovider.LyricLine{{Text: ""}}})
|
|
tr, _ := a.nowPlaying.(*mediaprovider.Track)
|
|
go a.fetchLyrics(ctx, tr)
|
|
}
|
|
|
|
func (a *NowPlayingPage) fetchLyrics(ctx context.Context, song *mediaprovider.Track) {
|
|
var lyrics *mediaprovider.Lyrics
|
|
var err error
|
|
if lp, ok := a.sm.Server.(mediaprovider.LyricsProvider); ok {
|
|
if lyrics, err = lp.GetLyrics(song); err != nil {
|
|
log.Printf("Error fetching lyrics: %v", err)
|
|
}
|
|
}
|
|
if lyrics == nil && a.lrcFetch != nil {
|
|
lyrics, err = a.lrcFetch.FetchLrcLibLyrics(song.Title, song.ArtistNames[0], song.Album, song.Duration)
|
|
if err != nil {
|
|
log.Println(err.Error())
|
|
}
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
fyne.Do(func() {
|
|
a.lyricsLoading.Stop()
|
|
a.lyricsViewer.SetLyrics(lyrics)
|
|
if lyrics != nil {
|
|
a.lyricsViewer.OnSeeked(a.lastPlayPos)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func (a *NowPlayingPage) updateRelatedList() {
|
|
if a.relatedFetchCancel != nil {
|
|
a.relatedFetchCancel()
|
|
}
|
|
if a.curRelatedID == a.nowPlayingID {
|
|
return
|
|
}
|
|
a.curRelatedID = a.nowPlayingID
|
|
if a.nowPlayingID == "" {
|
|
a.relatedList.SetTracks(nil)
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
a.relatedFetchCancel = cancel
|
|
a.relatedLoading.Start()
|
|
a.relatedList.SetTracks(nil)
|
|
go func(ctx context.Context) {
|
|
related, err := a.mp.GetSongRadio(a.nowPlayingID, 30)
|
|
if err != nil {
|
|
log.Printf("failed to get similar tracks: %s", err.Error())
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
fyne.Do(func() {
|
|
a.related = related
|
|
a.relatedList.SetTracks(a.related)
|
|
a.relatedLoading.Stop()
|
|
})
|
|
}
|
|
}(ctx)
|
|
}
|
|
|
|
func (a *NowPlayingPage) OnPlayQueueChange() {
|
|
a.Reload()
|
|
}
|
|
|
|
func (a *NowPlayingPage) Reload() {
|
|
a.card.DisableRating = !a.canRate
|
|
a.queueList.DisableRating = !a.canRate
|
|
a.queueList.DisableSharing = !a.canShare
|
|
a.relatedList.DisableRating = !a.canRate
|
|
a.relatedList.DisableSharing = !a.canShare
|
|
|
|
a.queue = a.pm.GetPlayQueue()
|
|
a.queueList.SetItems(a.queue)
|
|
a.totalTime = 0.0
|
|
for _, tr := range a.queue {
|
|
a.totalTime += float64(tr.Metadata().Duration)
|
|
}
|
|
a.formatStatusLine()
|
|
|
|
if a.tabs == nil {
|
|
return
|
|
}
|
|
switch a.tabs.SelectedIndex() {
|
|
case 1: /*lyrics*/
|
|
a.lastPlayPos = a.pm.PlayerStatus().TimePos
|
|
a.updateLyrics()
|
|
case 2: /*related*/
|
|
a.updateRelatedList()
|
|
}
|
|
}
|
|
|
|
func (s *nowPlayingPageState) Restore() Page {
|
|
return NewNowPlayingPage(s.conf, s.contr, s.pool, s.sm, s.im, s.pm, s.mp, s.canRate, s.canShare, s.lrcFetch)
|
|
}
|
|
|
|
var _ CanShowPlayTime = (*NowPlayingPage)(nil)
|
|
|
|
func (a *NowPlayingPage) OnPlayTimeUpdate(curTime, _ float64, seeked bool) {
|
|
a.lastPlayPos = curTime
|
|
a.formatStatusLine()
|
|
if a.tabs == nil || a.tabs.SelectedIndex() != 1 /*lyrics*/ {
|
|
return
|
|
}
|
|
if seeked {
|
|
a.lyricsViewer.OnSeeked(curTime)
|
|
} else {
|
|
a.lyricsViewer.UpdatePlayPos(curTime)
|
|
}
|
|
}
|
|
|
|
func (a *NowPlayingPage) currentTracklistOrNil() *widgets.PlayQueueList {
|
|
if a.tabs != nil {
|
|
|
|
switch a.tabs.SelectedIndex() {
|
|
case 0: /*queue*/
|
|
return a.queueList
|
|
case 2: /*related*/
|
|
return a.relatedList
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var _ CanSelectAll = (*NowPlayingPage)(nil)
|
|
|
|
func (a *NowPlayingPage) SelectAll() {
|
|
if l := a.currentTracklistOrNil(); l != nil {
|
|
l.SelectAll()
|
|
}
|
|
}
|
|
|
|
func (a *NowPlayingPage) UnselectAll() {
|
|
if l := a.currentTracklistOrNil(); l != nil {
|
|
l.UnselectAll()
|
|
}
|
|
}
|
|
|
|
func (a *NowPlayingPage) Refresh() {
|
|
if a.background != nil {
|
|
c := theme.Color(myTheme.ColorNamePageBackground)
|
|
if c != a.background.EndColor {
|
|
a.background.EndColor = c
|
|
a.background.Refresh()
|
|
}
|
|
}
|
|
a.BaseWidget.Refresh()
|
|
}
|
|
|
|
func (a *NowPlayingPage) doSetNewTrackOrder(idxs []int, insertPos int) {
|
|
newTracks := sharedutil.ReorderItems(a.queue, idxs, insertPos)
|
|
a.pm.UpdatePlayQueue(newTracks)
|
|
}
|
|
|
|
func (a *NowPlayingPage) saveSelectedTab(tabNum int) {
|
|
var tabName string
|
|
switch tabNum {
|
|
case 0:
|
|
tabName = "Play Queue"
|
|
case 1:
|
|
tabName = "Lyrics"
|
|
case 2:
|
|
tabName = "Related"
|
|
}
|
|
a.conf.InitialView = tabName
|
|
}
|
|
|
|
func (a *NowPlayingPage) formatStatusLine() {
|
|
curPlayer := a.pm.CurrentPlayer()
|
|
playerStats := a.pm.PlayerStatus()
|
|
lastStatus := a.statusLabel.Text
|
|
stopped := lang.L("Stopped")
|
|
state := stopped
|
|
switch playerStats.State {
|
|
case player.Paused:
|
|
state = lang.L("Paused")
|
|
case player.Playing:
|
|
state = lang.L("Playing")
|
|
}
|
|
|
|
dur := 0.0
|
|
if np := a.pm.NowPlaying(); np != nil {
|
|
dur = float64(np.Metadata().Duration)
|
|
}
|
|
statusSuffix := ""
|
|
trackNum := 0
|
|
if state != stopped {
|
|
trackNum = a.pm.NowPlayingIndex() + 1
|
|
statusSuffix = fmt.Sprintf(" %s/%s",
|
|
util.SecondsToMMSS(playerStats.TimePos),
|
|
util.SecondsToMMSS(dur))
|
|
}
|
|
status := fmt.Sprintf("%s (%d/%d)%s", state, trackNum,
|
|
len(a.queue), statusSuffix)
|
|
|
|
mediaInfo := ""
|
|
if state != stopped {
|
|
mediaInfo = a.formatMediaInfoStr(curPlayer)
|
|
}
|
|
if mediaInfo != "" {
|
|
mediaInfo = " · " + mediaInfo
|
|
}
|
|
|
|
a.statusLabel.Text = fmt.Sprintf("%s%s | %s: %s", status, mediaInfo, lang.L("Total time"), util.SecondsToTimeString(a.totalTime))
|
|
if lastStatus != a.statusLabel.Text {
|
|
a.statusLabel.Refresh()
|
|
}
|
|
}
|
|
|
|
func (a *NowPlayingPage) formatMediaInfoStr(player player.BasePlayer) string {
|
|
mpv, ok := player.(*mpv.Player)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
audioInfo, err := mpv.GetMediaInfo()
|
|
if err != nil {
|
|
log.Printf("error getting playback status: %s", err.Error())
|
|
return ""
|
|
}
|
|
codec := audioInfo.Codec
|
|
if len(codec) <= 4 && !strings.EqualFold(codec, "opus") {
|
|
codec = strings.ToUpper(codec) // FLAC, MP3, AAC, etc
|
|
}
|
|
|
|
// Note: bit depth intentionally omitted since MPV reports the decoded bit depth
|
|
// i.e. 24 bit files get reported as 32 bit. Also b/c bit depth isn't meaningful for lossy.
|
|
return fmt.Sprintf("%s %g kHz, %d kbps", codec, float64(audioInfo.Samplerate)/1000, audioInfo.Bitrate/1000)
|
|
}
|