di-tui/main.go
2025-11-28 20:16:40 +00:00

236 lines
6.2 KiB
Go

package main
import (
"fmt"
"os"
"strings"
"time"
"github.com/acaloiaro/di-tui/app"
"github.com/acaloiaro/di-tui/config"
"github.com/acaloiaro/di-tui/context"
"github.com/acaloiaro/di-tui/difm"
"github.com/acaloiaro/di-tui/mpris"
"github.com/acaloiaro/di-tui/views"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
var ctx *context.AppContext
const VERSION = "1.12.0"
func main() {
var err error
usernameFlag := pflag.String("username", "", "your di.fm username")
passwordFlag := pflag.String("password", "", "your di.fm password")
versionFlag := pflag.Bool("version", false, "print the current di-tui version")
networkFlag := pflag.String("network", viper.GetString("network.shortname"), "the audioaddict network to connect to")
pflag.Parse()
viper.BindPFlags(pflag.CommandLine)
if *versionFlag {
fmt.Printf("di-tui %s\n", VERSION)
return
}
network, err := difm.GetNetwork(*networkFlag)
if err != nil {
var networks []string
for network := range difm.Networks {
networks = append(networks, network)
}
fmt.Printf("Invalid network: %s \nPlease choose from the following: %s\n", *networkFlag, strings.Join(networks, ", "))
return
}
if *usernameFlag != "" && *passwordFlag != "" {
err = difm.Authenticate(ctx, *usernameFlag, *passwordFlag)
if err != nil {
fmt.Printf("unable to authenticate %s", err)
return
}
}
token := config.GetToken()
if token == "" {
fmt.Println("Authenticate by running: di-tui --username USER --password PASSWORD\n\n" +
"Or, visit https://github.com/acaloiaro/di-tui#authenticate for other options.")
os.Exit(1)
}
ctx = context.CreateAppContext(views.CreateViewContext(network))
ctx.DifmToken = token
run()
}
func run() {
configureEventHandling()
updateScreenLayout()
FetchFavoritesAndChannels()
ctx.View.Keybindings.Bindings = views.GetKeybindings()
err := ctx.View.App.Run()
if err != nil {
panic(err)
}
}
func updateScreenLayout() {
focusView := ctx.View.App.GetFocus()
main := tview.NewFlex()
main.SetDirection(tview.FlexRow)
favsAndChannels := tview.NewFlex().
AddItem(ctx.View.FavoriteList, 0, 1, false).
AddItem(ctx.View.ChannelList, 0, 2, false).
SetDirection(tview.FlexRow)
primaryView := tview.NewFlex()
primaryView.
AddItem(favsAndChannels, 30, 0, false).
AddItem(ctx.View.NowPlaying, 0, 4, false)
if ctx.ShowStatus {
main.AddItem(ctx.View.Status, 4, 0, false)
}
main.
AddItem(primaryView, 0, 3, false).
AddItem(ctx.View.Keybindings, 4, 0, false)
if focusView == nil {
focusView = ctx.View.FavoriteList
}
ctx.View.App.
SetRoot(main, true).
SetFocus(focusView)
}
// configureEventHandling handles key press events, and regular UI updates such as the currently playing track
func configureEventHandling() {
ctx.View.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
favoritesEmpty := len(ctx.FavoriteList) == 0
if favoritesEmpty {
ctx.View.App.SetFocus(ctx.View.ChannelList)
}
focus := ctx.View.App.GetFocus().(*tview.List)
switch event.Key() {
case tcell.KeyEnter:
current := focus.GetCurrentItem()
if focus != ctx.View.ChannelList && current < len(ctx.FavoriteList) {
highlightedFavorite := ctx.FavoriteList[current]
ctx.HighlightedChannel = difm.FavoriteItemChannel(ctx, highlightedFavorite)
} else if current < len(ctx.ChannelList) {
ctx.HighlightedChannel = &ctx.ChannelList[current]
}
app.Play(ctx)
case tcell.KeyRune:
switch event.Rune() {
case 'c':
if focus != ctx.View.ChannelList {
ctx.View.App.SetFocus(ctx.View.ChannelList)
}
case 'f':
if focus != ctx.View.FavoriteList {
ctx.View.App.SetFocus(ctx.View.FavoriteList)
}
case 'q':
ctx.View.App.Stop()
case 'j': // scroll down
current := focus.GetCurrentItem() + 1
focus.SetCurrentItem(current)
case 'k': // scroll up
current := focus.GetCurrentItem()
if current > 0 {
focus.SetCurrentItem(current - 1)
}
case 'p', 32: // tcell has no constant for the space bar rune (32)
app.TogglePause(ctx)
}
}
return event
})
// keep 'now playing' up to date second-by-second
go func() {
c := time.Tick(1 * time.Second)
for range c {
elapsed := time.Since(ctx.View.NowPlaying.Track.StartTime)
// If the current time is past the end of the track, then a new track is playing and the now playing track needs
// to be refreshed.
if ctx.View.NowPlaying.Track.Duration > 0 && ctx.View.NowPlaying.Track.Duration < elapsed.Seconds() {
app.UpdateNowPlaying(ctx, ctx.CurrentChannel)
}
if ctx.CurrentChannel != nil && elapsed.Seconds() > 0 {
ctx.View.NowPlaying.Elapsed = elapsed.Seconds()
}
ctx.View.App.QueueUpdateDraw(func() {})
}
}()
// display the status pane when new status messages arrive
go func() {
for {
status := <-ctx.StatusChannel
ctx.ShowStatus = true
ctx.View.Status.Message = status.Message
updateScreenLayout() // add the status pane to the screen
<-time.Tick(status.Duration)
ctx.ShowStatus = false
updateScreenLayout() // remove the status pane from the screen
}
}()
// Start the mpris server for d-bus support
mpris.Start(ctx)
}
func FetchFavoritesAndChannels() {
ctx.View.ChannelList.Clear()
ctx.View.FavoriteList.Clear()
channels := difm.ListChannels(ctx)
if len(channels) == 0 {
ctx.SetStatusMessage("Unable to get the channel list.")
return
}
for _, chn := range channels {
ctx.View.ChannelList.AddItem(chn.Name, "", 0, func() {
})
}
favorites := difm.ListFavorites(ctx)
for i, favorite := range favorites {
ctx.View.FavoriteList.InsertItem(i, favorite.Name, "", 0, func() {})
}
ctx.ChannelList = channels
ctx.FavoriteList = favorites
if len(channels) == 0 && len(favorites) == 0 {
return
}
if len(favorites) == 0 {
ctx.HighlightedChannel = &channels[0]
return
}
// default the highlighted channel to the first favorite; even before users select a channel manually. This way,
// when di-tui starts and the user presses the "Play" media key, di-tui will start playing the first favorite
// instead of requiring them to choose the channel to be played
highlightedFavorite := ctx.FavoriteList[0]
ctx.HighlightedChannel = difm.FavoriteItemChannel(ctx, highlightedFavorite)
}