feat: Support the whole audio addict network
This commit is contained in:
parent
f2886123b4
commit
36c412afe1
10 changed files with 843 additions and 215 deletions
4
.github/workflows/goreleaser.yml
vendored
4
.github/workflows/goreleaser.yml
vendored
|
|
@ -25,8 +25,8 @@ jobs:
|
|||
git config --global user.email "actions@github.com"
|
||||
git config --global user.name "Github Actions"
|
||||
go install git.sr.ht/~jcmuller/semver-bumper@latest
|
||||
OLD_TAG=$(git tag --list 'v*' | semver-bumper -s)
|
||||
NEW_TAG=$(git tag --list 'v*' | semver-bumper --increment minor)
|
||||
OLD_TAG=$(git tag --list '[0-9]*.[0-9]*.[0-9]*' | semver-bumper -s)
|
||||
NEW_TAG=$(git tag --list '[0-9]*.[0-9]*.[0-9]*' | semver-bumper --increment minor)
|
||||
sed -i "s/$OLD_TAG/$NEW_TAG/g" default.nix
|
||||
git add default.nix
|
||||
git commit -m "Bump version in default.nix" --allow-empty
|
||||
|
|
|
|||
76
README.md
76
README.md
|
|
@ -1,27 +1,18 @@
|
|||
# di-tui
|
||||
|
||||
A simple terminal UI player for [di.fm Premium](http://di.fm)
|
||||
|
||||

|
||||
|
||||
## Dependencies
|
||||
This app began as di.fm player, but now supports the whole Audio Addict network
|
||||
|
||||
### PulseAudio
|
||||
- Classical Radio
|
||||
- DI.fm
|
||||
- Radio Tunes
|
||||
- Rock Radio
|
||||
- Jazz Radio
|
||||
- Zen Radio
|
||||
|
||||
Both linux and MacOS depend on pulseaudio to be running.
|
||||
|
||||
#### MacOS
|
||||
|
||||
By default, pulseaudio on MacOS runs as "root", which is not ideal. PulseAudio is best run by non-root users. By symbolically linking the pulseaudio plist file into your user's `~/Library/LaunchAgents/`, it runs as your user.
|
||||
|
||||
```
|
||||
brew install pulseaudio
|
||||
ln -s $(brew info pulseaudio | grep "/usr/local/Cellar" | awk '{print $1}')//homebrew.mxcl.pulseaudio.plist ~/Library/LaunchAgents
|
||||
brew services start pulseaudio
|
||||
```
|
||||
|
||||
#### Debian / Ubuntu
|
||||
|
||||
`apt install pulseaudio`
|
||||
|
||||
## Install
|
||||
|
||||
|
|
@ -43,17 +34,29 @@ source ~/.bashrc
|
|||
nix run github:acaloiaro/di-tui
|
||||
```
|
||||
|
||||
## Authenticate
|
||||
## Usage
|
||||
|
||||
### Authenticate
|
||||
|
||||
There are two authentication options
|
||||
|
||||
**NOTE** di.fm has started using Cloudflare on di.fm, which prevents `di-tui` from logging in with username and password. This also prevents adding/removing favorites within di-tui, since that functionality requires a credential that's separate from di.fm's "listen key", and only available by logging in with username/password. Use your Listen Key if you're a new `di-tui` user. If you've previously logged in with username and password, you'll remain logged in as long as `~/.config/di-tui/config.yml` remains in place.
|
||||
|
||||
|
||||
- Enter your username and password directly into `di-tui`
|
||||
- Enter your username and password directly into `di-tui` with the `--username` and `--password` switches
|
||||
- If you're justifiably uncomfortable with entering your username/password into this application, copy your "Listen Key" from (https://www.di.fm/settings) and create the following file:
|
||||
|
||||
### ~/.config/di-tui/config.yml
|
||||
### Choose a network
|
||||
|
||||
DI.fm is the default network, but other audio addict networks can be chosen with the `--network` switch.
|
||||
|
||||
| switch value | network |
|
||||
| --- | ----------- |
|
||||
| classicalradio | Classical Radio [https://classicalradio.com](https://classicalradio.com) |
|
||||
| di | DI.fm [https://di.fm](https://di.fm) |
|
||||
| radiotunes | Radio Tunes [https://radiotunes.com](https://radiotunes.com) |
|
||||
| rockradio | Rock Radio [https://rockradio.com](https://rockradio.com)|
|
||||
| jazzradio | Jazz Radio [https://jazzradio.com](https://jazzradio.com)|
|
||||
| zenradio | Zen Radio [https://zenradio.com](https://zenradio.com)|
|
||||
|
||||
#### ~/.config/di-tui/config.yml
|
||||
```yml
|
||||
token: <YOUR LISTEN KEY>
|
||||
album_art: <BOOLEAN>
|
||||
|
|
@ -61,14 +64,34 @@ album_art: <BOOLEAN>
|
|||
|
||||
| key | description |
|
||||
| --- | ----------- |
|
||||
| token | Your di.fm authentication "Listen Key" found at https://www.di.fm/settings |
|
||||
| album_art | Turn album art on or off |
|
||||
| token | **string** Your di.fm authentication "Listen Key" found at https://www.di.fm/settings |
|
||||
| album_art | **boolean** Enable/disable audio art |
|
||||
|
||||
## Dependencies
|
||||
|
||||
### PulseAudio
|
||||
|
||||
Both linux and MacOS depend on pulseaudio to be running.
|
||||
|
||||
#### MacOS
|
||||
|
||||
By default, pulseaudio on MacOS runs as "root", which is not ideal. PulseAudio is best run by non-root users. By symbolically linking the pulseaudio plist file into your user's `~/Library/LaunchAgents/`, it runs as your user.
|
||||
|
||||
```
|
||||
brew install pulseaudio
|
||||
ln -s $(brew info pulseaudio | grep "/usr/local/Cellar" | awk '{print $1}')//homebrew.mxcl.pulseaudio.plist ~/Library/LaunchAgents
|
||||
brew services start pulseaudio
|
||||
```
|
||||
|
||||
#### Debian / Ubuntu
|
||||
|
||||
`apt install pulseaudio`
|
||||
|
||||
## MPRIS/D-bus support
|
||||
|
||||
MPRIS is a D-Bus specification allowing media players to be controlled in a standardized way, e.g. with `playerctl`.
|
||||
|
||||
`di-tui` supports a very limited set of MPRIS commands. The limited set is to to the fact that `di-tui` is a streaming audio player, and it doesn't make sense to support `next`, `previous`, `seek`, etc., because audio streams have no next or previous track; or the ability to seek forward.
|
||||
`di-tui` supports a very limited set of MPRIS commands. The limited set is due to the fact that `di-tui` is a streaming audio player, and it doesn't make sense to support `next`, `previous`, `seek`, etc., because audio streams have no next or previous track; or the ability to seek forward.
|
||||
|
||||
### Supported MPRIS commands
|
||||
|
||||
|
|
@ -84,7 +107,6 @@ MPRIS is a D-Bus specification allowing media players to be controlled in a stan
|
|||
|
||||
`playerName` The name of the player: `di-tui`
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
### Themes
|
||||
|
|
|
|||
|
|
@ -4,6 +4,13 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
// Network is an audio addict network
|
||||
type Network struct {
|
||||
ListenURL string
|
||||
Name string
|
||||
ShortName string // the name used in API URL paths
|
||||
}
|
||||
|
||||
// ChannelItem contains di.fm channel metadata
|
||||
type ChannelItem struct {
|
||||
ID int64 `json:"id"`
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/acaloiaro/di-tui/components"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
|
|
@ -24,6 +25,7 @@ func init() {
|
|||
viper.AddConfigPath(".")
|
||||
|
||||
viper.SetDefault("album_art", true)
|
||||
viper.SetDefault("network.shortname", "di")
|
||||
|
||||
err := viper.ReadInConfig()
|
||||
if err != nil {
|
||||
|
|
@ -89,6 +91,20 @@ func SaveListenToken(token string) {
|
|||
saveConfig()
|
||||
}
|
||||
|
||||
// SaveAPIKey saves the user's API key
|
||||
func SaveAPIKey(key string) {
|
||||
viper.Set("api_key", key)
|
||||
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
// SaveNetwork saves the user's default network
|
||||
func SaveNetwork(network *components.Network) {
|
||||
viper.Set("network", network)
|
||||
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
// HasTheme returns true if the di-tui config has a Theme set
|
||||
func HasTheme() bool {
|
||||
if C.Theme["primary_color"] != "" {
|
||||
|
|
|
|||
|
|
@ -21,23 +21,25 @@ func CreateAppContext(view *views.ViewContext) *AppContext {
|
|||
|
||||
// AppContext is a shared context to be shared across the application
|
||||
// AudioStream - The raw audio stream from which audio is streamed by the player
|
||||
// CurrentChannel - The ChannelItem representing the currently playing di.fm channel
|
||||
// DfmToken - The token used to authenticate to di.fm
|
||||
// CurrentChannel - The ChannelItem representing the currently playing on the network
|
||||
// DfmToken - The token used to authenticate to the network
|
||||
// IsPlaying - Is there audio playing?
|
||||
// Netowrk - the network to connect to
|
||||
// View - The view context
|
||||
// Status - Gets and sets current application status messages
|
||||
type AppContext struct {
|
||||
AudioStream io.ReadCloser
|
||||
Player *pulse.PlaybackStream
|
||||
ChannelList []components.ChannelItem
|
||||
CurrentChannel *components.ChannelItem
|
||||
DifmToken string
|
||||
FavoriteList []components.FavoriteItem
|
||||
Network *components.Network
|
||||
HighlightedChannel *components.ChannelItem
|
||||
IsPlaying bool
|
||||
Player *pulse.PlaybackStream
|
||||
ShowStatus bool // The status pane will be visible when true
|
||||
StatusChannel chan components.StatusMessage
|
||||
View *views.ViewContext
|
||||
ChannelList []components.ChannelItem
|
||||
FavoriteList []components.FavoriteItem
|
||||
HighlightedChannel *components.ChannelItem
|
||||
}
|
||||
|
||||
// SetStatusMessage sets the application's status message for five seconds.
|
||||
|
|
|
|||
224
difm/difm.go
224
difm/difm.go
|
|
@ -8,83 +8,78 @@ import (
|
|||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/acaloiaro/di-tui/context"
|
||||
"github.com/acaloiaro/di-tui/player"
|
||||
|
||||
"github.com/acaloiaro/di-tui/components"
|
||||
"github.com/acaloiaro/di-tui/config"
|
||||
"github.com/acaloiaro/di-tui/context"
|
||||
"github.com/acaloiaro/di-tui/player"
|
||||
"github.com/bradfitz/iter"
|
||||
ini "gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
type ApplicationMetadata struct {
|
||||
User struct {
|
||||
ID int64 `json:"id"`
|
||||
AudioToken string `json:"audio_token"`
|
||||
SessionKey string `json:"session_key"`
|
||||
} `json:"user"`
|
||||
CsrfToken string
|
||||
var Networks = map[string]*components.Network{
|
||||
"classicalradio": {
|
||||
Name: "Classical Radio",
|
||||
ListenURL: "http://listen.classicalradio.com",
|
||||
ShortName: "classicalradio",
|
||||
},
|
||||
"di": {
|
||||
Name: "DI.fm",
|
||||
ListenURL: "http://listen.di.fm",
|
||||
ShortName: "di",
|
||||
},
|
||||
"radiotunes": {
|
||||
Name: "RadioTunes",
|
||||
ListenURL: "http://listen.radiotunes.com",
|
||||
ShortName: "radiotunes",
|
||||
},
|
||||
"rockradio": {
|
||||
Name: "ROCKRADIO.COM",
|
||||
ListenURL: "http://listen.rockradio.com",
|
||||
ShortName: "rockradio",
|
||||
},
|
||||
"jazzradio": {
|
||||
Name: "JazzRadio",
|
||||
ListenURL: "http://listen.jazzradio.com",
|
||||
ShortName: "jazzradio",
|
||||
},
|
||||
"zenradio": {
|
||||
Name: "Zen Radio",
|
||||
ListenURL: "http://listen.zenradio.com",
|
||||
ShortName: "zenradio",
|
||||
},
|
||||
}
|
||||
|
||||
func GetNetwork(name string) (network *components.Network, err error) {
|
||||
var ok bool
|
||||
if network, ok = Networks[name]; !ok {
|
||||
return nil, fmt.Errorf("network does not exist: %s", network)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type authResponse struct {
|
||||
ListenKey string `json:"listen_key"`
|
||||
APIKey string `json:"api_key"`
|
||||
}
|
||||
|
||||
// Authenticate authenticates to the di.fm API with a username and password
|
||||
//
|
||||
// Note: There is a more API-friendly way of authenticating to the audioaddict API. However, because only the "web
|
||||
// player" allows interactions such as adding/removing favorite channels, we're obligated to authenticate in the way
|
||||
// that the web player authenticates.
|
||||
|
||||
// login workflow
|
||||
// 1. GET www.di.fm/login to get the CSRF token
|
||||
// 2. POST www.di.fm/login (with CSRF token and other appropriate headers)
|
||||
// 3. GET www.di.fm/ to retrieve two key pieces of information
|
||||
// - an "audio_token"
|
||||
// - a "session_key"
|
||||
//
|
||||
// Both of which are required to perform advanced player features such as adding/removing favorites.
|
||||
//
|
||||
// One added benefit of logging in this way is that di-tui may use the audio token to stream "on-demand" audio in the
|
||||
// future, which would allow the player to buffer content. Currently, no buffering is performed because di-tui uses the
|
||||
// streaming API.
|
||||
//
|
||||
// However, until a developer-friendly, go-native AAC decoder is available, this player will
|
||||
// continue using the MP3 "streaming" API.
|
||||
func Authenticate(ctx *context.AppContext, username, password string) (token string) {
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Got error while creating cookie jar %s", err.Error())
|
||||
}
|
||||
client := &http.Client{Jar: jar}
|
||||
|
||||
// 1. GET www.di.fm/login to get the CSRF token
|
||||
meta, _ := getApplicationMetadata(ctx, client)
|
||||
authURL := "https://www.di.fm/login"
|
||||
// Authenticate authenticates to the audio addict API
|
||||
func Authenticate(ctx *context.AppContext, username, password string) (err error) {
|
||||
authURL := fmt.Sprintf("https://api.audioaddict.com/v1/%s/members/authenticate", ctx.Network.ShortName)
|
||||
log.Println(authURL)
|
||||
client := &http.Client{}
|
||||
data := url.Values{}
|
||||
data.Set("member_session[username]", username)
|
||||
data.Set("member_session[password]", password)
|
||||
|
||||
data.Set("username", username)
|
||||
data.Set("password", password)
|
||||
encodedData := data.Encode()
|
||||
|
||||
// 2. POST www.di.fm/login (with CSRF token and other appropriate headers)
|
||||
req, _ := http.NewRequest("POST", authURL, strings.NewReader(encodedData))
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(encodedData)))
|
||||
req.Header.Add("Origin", "https://www.di.fm")
|
||||
req.Header.Add("Referrer", "https://www.di.fm")
|
||||
req.Header.Add("Sec-Fetch-Mode", "cors")
|
||||
req.Header.Add("Sec-Fetch-Dest", "empty")
|
||||
req.Header.Add("Sec-Fetch-Site", "same-origin")
|
||||
req.Header.Add("X-Requested-With", "XMLHttpRequest")
|
||||
req.Header.Add("X-CSRF-Token", meta.CsrfToken)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
|
|
@ -94,69 +89,19 @@ func Authenticate(ctx *context.AppContext, username, password string) (token str
|
|||
|
||||
var res authResponse
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
fmt.Println("Unable to authenticate to di.fm. Status code:", resp.StatusCode)
|
||||
os.Exit(1)
|
||||
if err != nil {
|
||||
log.Fatal("unable to authenticate", err.Error())
|
||||
}
|
||||
|
||||
json.Unmarshal(body, &res)
|
||||
err = json.Unmarshal([]byte(body), &res)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to reason API response: %v", err)
|
||||
}
|
||||
|
||||
config.SaveListenToken(res.ListenKey)
|
||||
|
||||
// 3. GET www.di.fm/ to retrieve two key pieces of information
|
||||
meta, err = getApplicationMetadata(ctx, client)
|
||||
if err != nil {
|
||||
fmt.Println("Unable to fetch audio token and session key")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
config.SaveAudioToken(meta.User.AudioToken)
|
||||
config.SaveSessionKey(meta.User.SessionKey)
|
||||
config.SaveUserID(meta.User.ID)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetApplicationMetadata fetches the application's metadata (the di.fm player application) from www.di.fm
|
||||
func getApplicationMetadata(ctx *context.AppContext, client *http.Client) (appMeta ApplicationMetadata, err error) {
|
||||
var req *http.Request
|
||||
req, err = http.NewRequest("GET", "https://www.di.fm", nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Add("Sec-Fetch-Mode", "cors")
|
||||
req.Header.Add("Sec-Fetch-Dest", "empty")
|
||||
req.Header.Add("Sec-Fetch-Site", "same-origin")
|
||||
|
||||
var resp *http.Response
|
||||
resp, err = client.Do(req)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var body []byte
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
return
|
||||
}
|
||||
|
||||
bodyStr := string(body)
|
||||
re := regexp.MustCompile(`.*di\.app\.start\((.*)\);.*`)
|
||||
matches := re.FindStringSubmatch(bodyStr)
|
||||
if len(matches) > 0 {
|
||||
appMeta = ApplicationMetadata{}
|
||||
err = json.Unmarshal([]byte(matches[1]), &appMeta)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
re = regexp.MustCompile(`.*meta name="csrf-token" content="(.*?)"/>`)
|
||||
matches = re.FindStringSubmatch(bodyStr)
|
||||
if len(matches) > 0 {
|
||||
appMeta.CsrfToken = matches[1]
|
||||
}
|
||||
config.SaveAPIKey(res.APIKey)
|
||||
config.SaveNetwork(ctx.Network)
|
||||
|
||||
return
|
||||
}
|
||||
|
|
@ -179,14 +124,14 @@ func GetStreamURL(data []byte, ctx *context.AppContext) (streamURL string, ok bo
|
|||
// GetCurrentlyPlaying fetches the list of all currently playing tracks site-side
|
||||
func GetCurrentlyPlaying(ctx *context.AppContext) (currentlyPlaying components.CurrentlyPlaying) {
|
||||
client := &http.Client{}
|
||||
req, _ := http.NewRequest("GET", "https://api.audioaddict.com/v1/di/currently_playing", nil)
|
||||
url := fmt.Sprintf("https://api.audioaddict.com/v1/%s/currently_playing", ctx.Network.ShortName)
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
ctx.SetStatusMessage("Unable to fetch currently playing track info")
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
ctx.SetStatusMessage("Unable to fetch currently playing track info.")
|
||||
|
|
@ -209,7 +154,8 @@ func GetCurrentlyPlaying(ctx *context.AppContext) (currentlyPlaying components.C
|
|||
// ListChannels lists all premium MP3 channels
|
||||
func ListChannels(ctx *context.AppContext) (channels []components.ChannelItem) {
|
||||
client := &http.Client{}
|
||||
req, _ := http.NewRequest("GET", "http://listen.di.fm/premium_high", nil)
|
||||
url := fmt.Sprintf("%s/premium_high", ctx.Network.ListenURL)
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
ctx.SetStatusMessage("Unable to fetch the list of channels")
|
||||
|
|
@ -235,7 +181,7 @@ func ListChannels(ctx *context.AppContext) (channels []components.ChannelItem) {
|
|||
// ListFavorites lists a user's favorite channels
|
||||
func ListFavorites(ctx *context.AppContext) (favorites []components.FavoriteItem) {
|
||||
client := &http.Client{}
|
||||
url := fmt.Sprintf("%s?%s", "http://listen.di.fm/premium_high/favorites.pls", ctx.DifmToken)
|
||||
url := fmt.Sprintf("%s%s?%s", ctx.Network.ListenURL, "/premium_high/favorites.pls", ctx.DifmToken)
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
|
|
@ -265,41 +211,6 @@ func ListFavorites(ctx *context.AppContext) (favorites []components.FavoriteItem
|
|||
return
|
||||
}
|
||||
|
||||
// ToggleFavorite adds/removes the currentlly selected channel to/from the user's favorites
|
||||
func ToggleFavorite(ctx *context.AppContext) {
|
||||
if config.GetSessionKey() == "" {
|
||||
ctx.SetStatusMessage("Unfortunately you must log in to di-tui with a username and password to change favorites.")
|
||||
return
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
url := fmt.Sprintf("https://api.audioaddict.com/v1/di/members/%d/favorites/channel/%d", config.GetUserID(), ctx.HighlightedChannel.ID)
|
||||
|
||||
requestMethod := "POST"
|
||||
focusedView := ctx.View.App.GetFocus()
|
||||
// if the user is currently viewing favorites, then the request is to remove the channel from the favorite list
|
||||
if focusedView == ctx.View.FavoriteList {
|
||||
requestMethod = "DELETE"
|
||||
}
|
||||
|
||||
url = fmt.Sprintf("%s?audio_token=%s", url, config.GetAudioToken())
|
||||
|
||||
var jsonStr = []byte(fmt.Sprintf(`{"id": %d}`, ctx.HighlightedChannel.ID))
|
||||
req, _ := http.NewRequest(requestMethod, url, bytes.NewBuffer(jsonStr))
|
||||
|
||||
req.Header.Add("Sec-Fetch-Mode", "cors")
|
||||
req.Header.Add("Sec-Fetch-Dest", "empty")
|
||||
req.Header.Add("Sec-Fetch-Site", "same-origin")
|
||||
req.Header.Add("X-Session-Key", config.GetSessionKey())
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil || (resp.StatusCode != 204 && resp.StatusCode != 200) {
|
||||
ctx.SetStatusMessage("There was a problem updating channel favorites")
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
// Stream streams the provided URL using the given di.fm premium token
|
||||
func Stream(url string, ctx *context.AppContext) {
|
||||
client := http.DefaultClient
|
||||
|
|
@ -346,10 +257,11 @@ func Stream(url string, ctx *context.AppContext) {
|
|||
|
||||
// FavoriteItemChannel identifies the ChannelItem that corresponds with a FavoriteItem
|
||||
func FavoriteItemChannel(ctx *context.AppContext, favorite components.FavoriteItem) (channel *components.ChannelItem) {
|
||||
// favorites are prefixed with "<NETWORK-NAME> - <CHANNEL NAME>", e.g. "DI.fm - Liquid Trap"
|
||||
// shave it off before comparing
|
||||
prefix := fmt.Sprintf("%s - ", ctx.Network.Name)
|
||||
for _, chn := range ctx.ChannelList {
|
||||
// favorites are prefixed with "DI.fm - <CHANNEL NAME>", shave it off before comparing
|
||||
// TODO: this feels a bit hacky -- consider doing something else.
|
||||
if chn.Name == favorite.Name[8:len(favorite.Name)] {
|
||||
if chn.Name == favorite.Name[len(prefix):len(favorite.Name)] {
|
||||
channel = &chn
|
||||
return
|
||||
}
|
||||
|
|
|
|||
658
flake.lock
658
flake.lock
|
|
@ -1,5 +1,30 @@
|
|||
{
|
||||
"nodes": {
|
||||
"cachix": {
|
||||
"inputs": {
|
||||
"devenv": "devenv_3",
|
||||
"flake-compat": "flake-compat_3",
|
||||
"nixpkgs": [
|
||||
"semver-bumper",
|
||||
"devenv",
|
||||
"nixpkgs"
|
||||
],
|
||||
"pre-commit-hooks": "pre-commit-hooks_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710475558,
|
||||
"narHash": "sha256-egKrPCKjy/cE+NqCj4hg2fNX/NwLCf0bRDInraYXDgs=",
|
||||
"owner": "cachix",
|
||||
"repo": "cachix",
|
||||
"rev": "661bbb7f8b55722a0406456b15267b5426a3bda6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "cachix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"devenv": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
|
|
@ -21,6 +46,61 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"devenv_2": {
|
||||
"inputs": {
|
||||
"cachix": "cachix",
|
||||
"flake-compat": "flake-compat_5",
|
||||
"nix": "nix_3",
|
||||
"nixpkgs": "nixpkgs_4",
|
||||
"pre-commit-hooks": "pre-commit-hooks_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1711371145,
|
||||
"narHash": "sha256-abuKk8NiXO+tMTOtt+4QbSaFsSmN+Vo4UGLDO4+Ncjg=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"rev": "e0440c5c88ae2a56813402f32377140840203c52",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"devenv_3": {
|
||||
"inputs": {
|
||||
"flake-compat": [
|
||||
"semver-bumper",
|
||||
"devenv",
|
||||
"cachix",
|
||||
"flake-compat"
|
||||
],
|
||||
"nix": "nix_2",
|
||||
"nixpkgs": "nixpkgs_3",
|
||||
"poetry2nix": "poetry2nix",
|
||||
"pre-commit-hooks": [
|
||||
"semver-bumper",
|
||||
"devenv",
|
||||
"cachix",
|
||||
"pre-commit-hooks"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1708704632,
|
||||
"narHash": "sha256-w+dOIW60FKMaHI1q5714CSibk99JfYxm0CzTinYWr+Q=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"rev": "2ee4450b0f4b95a1b90f2eb5ffea98b90e48c196",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"ref": "python-rewrite",
|
||||
"repo": "devenv",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
|
|
@ -37,6 +117,86 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat_2": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1673956053,
|
||||
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat_3": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat_4": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat_5": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat_6": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1673956053,
|
||||
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
|
|
@ -73,6 +233,78 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_3": {
|
||||
"inputs": {
|
||||
"systems": "systems_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1689068808,
|
||||
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_4": {
|
||||
"inputs": {
|
||||
"systems": "systems_4"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_5": {
|
||||
"inputs": {
|
||||
"systems": "systems_5"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_6": {
|
||||
"inputs": {
|
||||
"systems": "systems_6"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1694529238,
|
||||
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
|
|
@ -95,6 +327,53 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore_2": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"semver-bumper",
|
||||
"devenv",
|
||||
"cachix",
|
||||
"pre-commit-hooks",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1703887061,
|
||||
"narHash": "sha256-gGPa9qWNc6eCXT/+Z5/zMkyYOuRZqeFZBDbopNZQkuY=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "43e1aa1308018f37118e34d3a9cb4f5e75dc11d5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore_3": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"semver-bumper",
|
||||
"devenv",
|
||||
"pre-commit-hooks",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1703887061,
|
||||
"narHash": "sha256-gGPa9qWNc6eCXT/+Z5/zMkyYOuRZqeFZBDbopNZQkuY=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "43e1aa1308018f37118e34d3a9cb4f5e75dc11d5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gomod2nix": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
|
|
@ -116,6 +395,28 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gomod2nix_2": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_6",
|
||||
"nixpkgs": [
|
||||
"semver-bumper",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710154385,
|
||||
"narHash": "sha256-4c3zQ2YY4BZOufaBJB4v9VBBeN2dH7iVdoJw8SDNCfI=",
|
||||
"owner": "nix-community",
|
||||
"repo": "gomod2nix",
|
||||
"rev": "872b63ddd28f318489c929d25f1f0a3c6039c971",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "gomod2nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"lowdown-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
|
|
@ -156,6 +457,83 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-github-actions": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"semver-bumper",
|
||||
"devenv",
|
||||
"cachix",
|
||||
"devenv",
|
||||
"poetry2nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1688870561,
|
||||
"narHash": "sha256-4UYkifnPEw1nAzqqPOTL2MvWtm3sNGw1UTYTalkTcGY=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nix-github-actions",
|
||||
"rev": "165b1650b753316aa7f1787f3005a8d2da0f5301",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nix-github-actions",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix_2": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat_2",
|
||||
"nixpkgs": [
|
||||
"semver-bumper",
|
||||
"devenv",
|
||||
"cachix",
|
||||
"devenv",
|
||||
"nixpkgs"
|
||||
],
|
||||
"nixpkgs-regression": "nixpkgs-regression_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1708577783,
|
||||
"narHash": "sha256-92xq7eXlxIT5zFNccLpjiP7sdQqQI30Gyui2p/PfKZM=",
|
||||
"owner": "domenkozar",
|
||||
"repo": "nix",
|
||||
"rev": "ecd0af0c1f56de32cbad14daa1d82a132bf298f8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "domenkozar",
|
||||
"ref": "devenv-2.21",
|
||||
"repo": "nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix_3": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat_6",
|
||||
"nixpkgs": [
|
||||
"semver-bumper",
|
||||
"devenv",
|
||||
"nixpkgs"
|
||||
],
|
||||
"nixpkgs-regression": "nixpkgs-regression_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710500156,
|
||||
"narHash": "sha256-zvCqeUO2GLOm7jnU23G4EzTZR7eylcJN+HJ5svjmubI=",
|
||||
"owner": "domenkozar",
|
||||
"repo": "nix",
|
||||
"rev": "c5bbf14ecbd692eeabf4184cc8d50f79c2446549",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "domenkozar",
|
||||
"ref": "devenv-2.21",
|
||||
"repo": "nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1678875422,
|
||||
|
|
@ -188,6 +566,38 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-regression_2": {
|
||||
"locked": {
|
||||
"lastModified": 1643052045,
|
||||
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-regression_3": {
|
||||
"locked": {
|
||||
"lastModified": 1643052045,
|
||||
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-stable": {
|
||||
"locked": {
|
||||
"lastModified": 1685801374,
|
||||
|
|
@ -204,6 +614,38 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-stable_2": {
|
||||
"locked": {
|
||||
"lastModified": 1704874635,
|
||||
"narHash": "sha256-YWuCrtsty5vVZvu+7BchAxmcYzTMfolSPP5io8+WYCg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3dc440faeee9e889fe2d1b4d25ad0f430d449356",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-23.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-stable_3": {
|
||||
"locked": {
|
||||
"lastModified": 1704874635,
|
||||
"narHash": "sha256-YWuCrtsty5vVZvu+7BchAxmcYzTMfolSPP5io8+WYCg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3dc440faeee9e889fe2d1b4d25ad0f430d449356",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-23.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1708751719,
|
||||
|
|
@ -220,6 +662,64 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1692808169,
|
||||
"narHash": "sha256-x9Opq06rIiwdwGeK2Ykj69dNc2IvUH1fY55Wm7atwrE=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9201b5ff357e781bf014d0330d18555695df7ba8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1710236354,
|
||||
"narHash": "sha256-vWrciFdq49vve43g4pbi7NjmL4cwG1ifXnQx+dU3T5E=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv-nixpkgs",
|
||||
"rev": "829e73affeadfb4198a7105cbe3a03153d13edc9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"ref": "rolling",
|
||||
"repo": "devenv-nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"poetry2nix": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_3",
|
||||
"nix-github-actions": "nix-github-actions",
|
||||
"nixpkgs": [
|
||||
"semver-bumper",
|
||||
"devenv",
|
||||
"cachix",
|
||||
"devenv",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1692876271,
|
||||
"narHash": "sha256-IXfZEkI0Mal5y1jr6IRWMqK8GW2/f28xJenZIPQqkY0=",
|
||||
"owner": "nix-community",
|
||||
"repo": "poetry2nix",
|
||||
"rev": "d5006be9c2c2417dafb2e2e5034d83fabd207ee3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "poetry2nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"pre-commit-hooks": {
|
||||
"inputs": {
|
||||
"flake-compat": [
|
||||
|
|
@ -248,12 +748,93 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"pre-commit-hooks_2": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat_4",
|
||||
"flake-utils": "flake-utils_4",
|
||||
"gitignore": "gitignore_2",
|
||||
"nixpkgs": [
|
||||
"semver-bumper",
|
||||
"devenv",
|
||||
"cachix",
|
||||
"nixpkgs"
|
||||
],
|
||||
"nixpkgs-stable": "nixpkgs-stable_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1708018599,
|
||||
"narHash": "sha256-M+Ng6+SePmA8g06CmUZWi1AjG2tFBX9WCXElBHEKnyM=",
|
||||
"owner": "cachix",
|
||||
"repo": "pre-commit-hooks.nix",
|
||||
"rev": "5df5a70ad7575f6601d91f0efec95dd9bc619431",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "pre-commit-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"pre-commit-hooks_3": {
|
||||
"inputs": {
|
||||
"flake-compat": [
|
||||
"semver-bumper",
|
||||
"devenv",
|
||||
"flake-compat"
|
||||
],
|
||||
"flake-utils": "flake-utils_5",
|
||||
"gitignore": "gitignore_3",
|
||||
"nixpkgs": [
|
||||
"semver-bumper",
|
||||
"devenv",
|
||||
"nixpkgs"
|
||||
],
|
||||
"nixpkgs-stable": "nixpkgs-stable_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1708018599,
|
||||
"narHash": "sha256-M+Ng6+SePmA8g06CmUZWi1AjG2tFBX9WCXElBHEKnyM=",
|
||||
"owner": "cachix",
|
||||
"repo": "pre-commit-hooks.nix",
|
||||
"rev": "5df5a70ad7575f6601d91f0efec95dd9bc619431",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "pre-commit-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"devenv": "devenv",
|
||||
"gomod2nix": "gomod2nix",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"systems": "systems_3"
|
||||
"semver-bumper": "semver-bumper",
|
||||
"systems": "systems_8"
|
||||
}
|
||||
},
|
||||
"semver-bumper": {
|
||||
"inputs": {
|
||||
"devenv": "devenv_2",
|
||||
"gomod2nix": "gomod2nix_2",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"systems": "systems_7"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1711378352,
|
||||
"narHash": "sha256-f+fJ+FIs7jABDGoj6ZasAqstAMGeP4RVWvy/rwZIJcc=",
|
||||
"owner": "~jcmuller",
|
||||
"repo": "semver-bumper",
|
||||
"rev": "eb01c170dcbaf759b9f5bdc9d53ca2c489f5a465",
|
||||
"type": "sourcehut"
|
||||
},
|
||||
"original": {
|
||||
"owner": "~jcmuller",
|
||||
"repo": "semver-bumper",
|
||||
"type": "sourcehut"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
|
|
@ -300,6 +881,81 @@
|
|||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_4": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_5": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_6": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_7": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_8": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@
|
|||
url = "github:nix-community/gomod2nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
semver-bumper = {
|
||||
url = "sourcehut:~jcmuller/semver-bumper";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = {
|
||||
|
|
@ -16,6 +21,7 @@
|
|||
devenv,
|
||||
systems,
|
||||
gomod2nix,
|
||||
semver-bumper,
|
||||
...
|
||||
} @ inputs: let
|
||||
forEachSystem = nixpkgs.lib.genAttrs (import systems);
|
||||
|
|
@ -43,6 +49,7 @@
|
|||
gomod2nix.legacyPackages.${system}.gomod2nix
|
||||
golangci-lint
|
||||
pre-commit
|
||||
semver-bumper.packages.${system}.default
|
||||
];
|
||||
|
||||
pre-commit.hooks.gomod2nix = {
|
||||
|
|
|
|||
51
main.go
51
main.go
|
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/acaloiaro/di-tui/app"
|
||||
|
|
@ -20,28 +21,35 @@ import (
|
|||
var ctx *context.AppContext
|
||||
|
||||
func main() {
|
||||
pflag.String("username", "", "your di.fm username")
|
||||
pflag.String("password", "", "your di.fm password")
|
||||
|
||||
ctx = context.CreateAppContext(views.CreateViewContext())
|
||||
var err error
|
||||
username := pflag.String("username", "", "your di.fm username")
|
||||
password := pflag.String("password", "", "your di.fm password")
|
||||
network := pflag.String("network", viper.GetString("network.shortname"), "the audioaddict network to connect to")
|
||||
pflag.Parse()
|
||||
viper.BindPFlags(pflag.CommandLine)
|
||||
|
||||
username := viper.GetString("username")
|
||||
password := viper.GetString("password")
|
||||
|
||||
var token string
|
||||
if len(username) > 0 && len(password) > 0 {
|
||||
difm.Authenticate(ctx, username, password)
|
||||
ctx.Network, err = difm.GetNetwork(*network)
|
||||
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", *network, strings.Join(networks, ", "))
|
||||
return
|
||||
}
|
||||
|
||||
token = config.GetToken()
|
||||
if *username != "" && *password != "" {
|
||||
difm.Authenticate(ctx, *username, *password)
|
||||
}
|
||||
|
||||
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())
|
||||
ctx.DifmToken = token
|
||||
|
||||
run()
|
||||
|
|
@ -96,8 +104,12 @@ func updateScreenLayout() {
|
|||
// 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 {
|
||||
focus := ctx.View.App.GetFocus().(*tview.List)
|
||||
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()
|
||||
|
|
@ -118,16 +130,6 @@ func configureEventHandling() {
|
|||
if focus != ctx.View.FavoriteList {
|
||||
ctx.View.App.SetFocus(ctx.View.FavoriteList)
|
||||
}
|
||||
case 'F':
|
||||
current := focus.GetCurrentItem()
|
||||
if focus == ctx.View.ChannelList {
|
||||
ctx.HighlightedChannel = &ctx.ChannelList[current]
|
||||
} else {
|
||||
highlightedFavorite := ctx.FavoriteList[current]
|
||||
ctx.HighlightedChannel = difm.FavoriteItemChannel(ctx, highlightedFavorite)
|
||||
}
|
||||
difm.ToggleFavorite(ctx)
|
||||
FetchFavoritesAndChannels()
|
||||
case 'q':
|
||||
ctx.View.App.Stop()
|
||||
case 'j': // scroll down
|
||||
|
|
@ -200,6 +202,11 @@ func FetchFavoritesAndChannels() {
|
|||
ctx.ChannelList = channels
|
||||
ctx.FavoriteList = favorites
|
||||
|
||||
if len(favorites) == 0 {
|
||||
ctx.HighlightedChannel = &ctx.ChannelList[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
|
||||
|
|
|
|||
|
|
@ -242,7 +242,6 @@ func GetKeybindings() (bindings []UIKeybinding) {
|
|||
bindings = []UIKeybinding{
|
||||
{Shortcut: "c", Description: "Channels", Func: func() {}},
|
||||
{Shortcut: "f", Description: "Favorites", Func: func() {}},
|
||||
{Shortcut: "F", Description: "Toggle Favorite", Func: func() {}},
|
||||
{Shortcut: "j", Description: "Scroll Down", Func: func() {}},
|
||||
{Shortcut: "k", Description: "Scroll Up", Func: func() {}},
|
||||
{Shortcut: "q", Description: "Quit", Func: func() {}},
|
||||
|
|
|
|||
Loading…
Reference in a new issue