KeepPlayerFocus pref, rework prefs page, always autoplay if you click on the next track

This commit is contained in:
Laptop
2025-02-26 22:48:26 +02:00
parent 3718ef7e66
commit 7a044a2dec
7 changed files with 170 additions and 132 deletions

View File

@@ -1,22 +1,16 @@
# Preferences
| Name | Key | Default | Possible values | Description |
| :--------------------------------- | --------------------- | ------------------------------------------------------------------------ | ------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Parse descriptions | ParseDescriptions | true | true, false | Turn @mentions, external links (https://example.org) and emails (hello@example.org) inside descriptions into clickable links |
| Show current audio | ShowAudio | false | true, false | Show what [audio preset](AUDIO_PRESETS.md) is being streamed below the audio player |
| Proxy images | ProxyImages | same as ProxyImages in backend config | true, false | Proxy images through the backend. ProxyImages must be enabled on the backend |
| Download audio | DownloadAudio | "mpeg" | "mpeg", "opus", "aac", "best" | What [audio preset](AUDIO_PRESETS.md) should be loaded when downloading audio with metadata. Restream must be enabled on the backend |
| Autoplay next track in playlists | AutoplayNextTrack | false | true, false | Automatically start playlist playback when you open a track from the playlist. Requires JS |
| Default autoplay mode (in playlists) | DefaultAutoplayMode | "normal" | "normal", "random" | Default mode for playlist autoplay. Normal - play songs in order. Random - play random song next |
| Autoplay next related track | AutoplayNextRelatedTrack | false | true, false | Automatically play a related track next. Requires JS
| Fetch search suggestions | SearchSuggestions | false | true, false | Load search suggestions on main page when you type. Requires JS |
| Dynamically load comments | DynamicLoadComments | false | true, false | Dynamically load track comments, without leaving the page. Requires JS |
# Player preferences
| Name | Key | Default | Possible values | Description |
| :--------------------------------- | --------------------- | ------------------------------------------------------------------------ | ------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Player | Player | "restream" if Restream is enabled in backend config, otherwise - "hls" | "restream", "hls", "none" | Method used to play the track in the frontend. HLS - requires JavaScript, loads the track in pieces. Restream - works without JavaScript, loads entire track through the backend right away. None - don't play the track |
## Player-specific preferences
### HLS Player
## HLS Player
| Name | Key | Default | Possible values | Description |
| :-------------------- | ------------------- | ---------------------------------------- | ----------------- | :------------------------------------------------------------------------------------ |
@@ -24,9 +18,30 @@
| Fully preload track | FullyPreloadTrack | false | true, false | Fully load track when the page is loaded (track stream expires in ~5 minutes) |
| Streaming audio | HLSAudio | "mpeg" | "mpeg", "aac" | What [audio preset](AUDIO_PRESETS.md) should be loaded when streaming audio |
### Restream Player
## Restream Player
| Name | Key | Default | Possible values | Description |
| :---------------- | --------------- | --------- | ------------------------------- | :--------------------------------------------------------------------------- |
| Streaming audio | RestreamAudio | "mpeg" | "mpeg", "opus", "aac", "best" | What [audio preset](AUDIO_PRESETS.md) should be loaded when streaming audio |
# Frontend enhancements
| Name | Key | Default | Possible values | Description |
| :--------------------------------- | --------------------- | ------------------------------------------------------------------------ | ------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Proxy images | ProxyImages | same as ProxyImages in backend config | true, false | Proxy images through the backend. ProxyImages must be enabled on the backend |
| Parse descriptions | ParseDescriptions | true | true, false | Turn @mentions, external links (https://example.org) and emails (hello@example.org) inside descriptions into clickable links |
| Show current audio | ShowAudio | false | true, false | Show what [audio preset](AUDIO_PRESETS.md) is being streamed below the audio player |
| Fetch search suggestions | SearchSuggestions | false | true, false | Load search suggestions on main page when you type. Requires JS |
| Dynamically load comments | DynamicLoadComments | false | true, false | Dynamically load track comments, without leaving the page. Requires JS
| Keep player focus | KeepPlayerFocus | false | true, false | Always keep track element in focus, so you can control it with keyboard. Requires JS |
## Autoplay
*Requires JS. You also need to allow autoplay from this domain*
| Name | Key | Default | Possible values | Description |
| :--------------------------------- | --------------------- | ------------------------------------------------------------------------ | ------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Autoplay next track in playlists | AutoplayNextTrack | false | true, false | Automatically start playlist playback when you open a track from the playlist. |
| Default autoplay mode (in playlists) | DefaultAutoplayMode | "normal" | "normal", "random" | Default mode for playlist autoplay. Normal - play songs in order. Random - play random song next |
| Autoplay next related track | AutoplayNextRelatedTrack | false | true, false | Automatically play a related track next.

View File

@@ -1,7 +1,6 @@
package cfg
import (
"fmt"
"log"
"os"
"strconv"
@@ -144,6 +143,7 @@ func defaultPreferences() {
DefaultPreferences.SearchSuggestions = &False
DefaultPreferences.DynamicLoadComments = &False
DefaultPreferences.KeepPlayerFocus = &False
}
func loadDefaultPreferences(loaded Preferences) {
@@ -238,21 +238,18 @@ func loadDefaultPreferences(loaded Preferences) {
} else {
DefaultPreferences.DynamicLoadComments = &False
}
if loaded.KeepPlayerFocus != nil {
DefaultPreferences.KeepPlayerFocus = loaded.KeepPlayerFocus
} else {
DefaultPreferences.KeepPlayerFocus = &False
}
}
func boolean(in string) bool {
return strings.Trim(strings.ToLower(in), " ") == "true"
}
type wrappedError struct {
err error
fault string
}
func (w wrappedError) Error() string {
return fmt.Sprintf("error loading %s: %s", w.fault, w.err)
}
func fromEnv() error {
env := os.Getenv("GET_WEB_PROFILES")
if env != "" {
@@ -264,7 +261,7 @@ func fromEnv() error {
var p Preferences
err := json.Unmarshal(S2b(env), &p)
if err != nil {
return wrappedError{err, "DEFAULT_PREFERENCES"}
return err
}
loadDefaultPreferences(p)
@@ -306,7 +303,7 @@ func fromEnv() error {
if env != "" {
num, err := strconv.ParseInt(env, 10, 64)
if err != nil {
return wrappedError{err, "CLIENT_ID_TTL"}
return err
}
ClientIDTTL = time.Duration(num) * time.Second
@@ -316,7 +313,7 @@ func fromEnv() error {
if env != "" {
num, err := strconv.ParseInt(env, 10, 64)
if err != nil {
return wrappedError{err, "USER_TTL"}
return err
}
UserTTL = time.Duration(num) * time.Second
@@ -326,7 +323,7 @@ func fromEnv() error {
if env != "" {
num, err := strconv.ParseInt(env, 10, 64)
if err != nil {
return wrappedError{err, "USER_CACHE_CLEAN_DELAY"}
return err
}
UserCacheCleanDelay = time.Duration(num) * time.Second
@@ -336,7 +333,7 @@ func fromEnv() error {
if env != "" {
num, err := strconv.ParseInt(env, 10, 64)
if err != nil {
return wrappedError{err, "TRACK_TTL"}
return err
}
TrackTTL = time.Duration(num) * time.Second
@@ -346,7 +343,7 @@ func fromEnv() error {
if env != "" {
num, err := strconv.ParseInt(env, 10, 64)
if err != nil {
return wrappedError{err, "TRACK_CACHE_CLEAN_DELAY"}
return err
}
TrackCacheCleanDelay = time.Duration(num) * time.Second
@@ -356,7 +353,7 @@ func fromEnv() error {
if env != "" {
num, err := strconv.ParseInt(env, 10, 64)
if err != nil {
return wrappedError{err, "PLAYLIST_TTL"}
return err
}
PlaylistTTL = time.Duration(num) * time.Second
@@ -366,7 +363,7 @@ func fromEnv() error {
if env != "" {
num, err := strconv.ParseInt(env, 10, 64)
if err != nil {
return wrappedError{err, "PLAYLIST_CACHE_CLEAN_DELAY"}
return err
}
PlaylistCacheCleanDelay = time.Duration(num) * time.Second
@@ -381,7 +378,7 @@ func fromEnv() error {
if env != "" {
num, err := strconv.ParseInt(env, 10, 64)
if err != nil {
return wrappedError{err, "DNS_CACHE_TTL"}
return err
}
DNSCacheTTL = time.Duration(num) * time.Second
@@ -412,7 +409,7 @@ func fromEnv() error {
var p []string
err := json.Unmarshal(S2b(env), &p)
if err != nil {
return wrappedError{err, "TRUSTED_PROXIES"}
return err
}
TrustedProxies = p
@@ -436,14 +433,10 @@ func init() {
if env := os.Getenv("SOUNDCLOAK_CONFIG"); env == "FROM_ENV" {
err := fromEnv()
if err != nil {
// So we only set default preferences if it fails to load that in
if err.(wrappedError).fault == "DEFAULT_PREFERENCES" {
log.Println("Warning: failed to load config from environment:", err)
defaultPreferences()
}
log.Println("failed to load config from environment:", err)
}
return
} else if env != "" {
filename = env
@@ -451,7 +444,7 @@ func init() {
data, err := os.ReadFile(filename)
if err != nil {
log.Printf("failed to load config from %s: %s\n", filename, err)
log.Printf("Warning: failed to load config from %s: %s\n", filename, err)
defaultPreferences()
return
}

View File

@@ -80,6 +80,8 @@ type Preferences struct {
SearchSuggestions *bool // load search suggestions on main page
DynamicLoadComments *bool // dynamic comments loader without leaving track page
KeepPlayerFocus *bool // keep player element in focus
}
func B2s(b []byte) string {

View File

@@ -11,6 +11,8 @@ import (
"github.com/gofiber/fiber/v3"
)
const on = "on"
func Defaults(dst *cfg.Preferences) {
if dst.Player == nil {
dst.Player = cfg.DefaultPreferences.Player
@@ -67,6 +69,10 @@ func Defaults(dst *cfg.Preferences) {
if dst.DynamicLoadComments == nil {
dst.DynamicLoadComments = cfg.DefaultPreferences.DynamicLoadComments
}
if dst.KeepPlayerFocus == nil {
dst.KeepPlayerFocus = cfg.DefaultPreferences.KeepPlayerFocus
}
}
func Get(c fiber.Ctx) (cfg.Preferences, error) {
@@ -97,6 +103,7 @@ type PrefsForm struct {
ShowAudio string
SearchSuggestions string
DynamicLoadComments string
KeepPlayerFocus string
}
type Export struct {
@@ -130,19 +137,19 @@ func Load(r *fiber.App) {
old.DefaultAutoplayMode = &p.DefaultAutoplayMode
}
if p.AutoplayNextTrack == "on" {
if p.AutoplayNextTrack == on {
old.AutoplayNextTrack = &cfg.True
} else {
old.AutoplayNextTrack = &cfg.False
}
if p.AutoplayNextRelatedTrack == "on" {
if p.AutoplayNextRelatedTrack == on {
old.AutoplayNextRelatedTrack = &cfg.True
} else {
old.AutoplayNextRelatedTrack = &cfg.False
}
if p.ShowAudio == "on" {
if p.ShowAudio == on {
old.ShowAudio = &cfg.True
} else {
old.ShowAudio = &cfg.False
@@ -150,14 +157,14 @@ func Load(r *fiber.App) {
if *old.Player == cfg.HLSPlayer {
if cfg.ProxyStreams {
if p.ProxyStreams == "on" {
if p.ProxyStreams == on {
old.ProxyStreams = &cfg.True
} else if p.ProxyStreams == "" {
old.ProxyStreams = &cfg.False
}
}
if p.FullyPreloadTrack == "on" {
if p.FullyPreloadTrack == on {
old.FullyPreloadTrack = &cfg.True
} else if p.FullyPreloadTrack == "" {
old.FullyPreloadTrack = &cfg.False
@@ -175,31 +182,37 @@ func Load(r *fiber.App) {
}
if cfg.ProxyImages {
if p.ProxyImages == "on" {
if p.ProxyImages == on {
old.ProxyImages = &cfg.True
} else if p.ProxyImages == "" {
old.ProxyImages = &cfg.False
}
}
if p.ParseDescriptions == "on" {
if p.ParseDescriptions == on {
old.ParseDescriptions = &cfg.True
} else {
old.ParseDescriptions = &cfg.False
}
if p.SearchSuggestions == "on" {
if p.SearchSuggestions == on {
old.SearchSuggestions = &cfg.True
} else {
old.SearchSuggestions = &cfg.False
}
if p.DynamicLoadComments == "on" {
if p.DynamicLoadComments == on {
old.DynamicLoadComments = &cfg.True
} else {
old.DynamicLoadComments = &cfg.False
}
if p.KeepPlayerFocus == on {
old.KeepPlayerFocus = &cfg.True
} else {
old.KeepPlayerFocus = &cfg.False
}
old.Player = &p.Player
data, err := json.Marshal(old)

View File

@@ -0,0 +1,9 @@
var audio = document.getElementById('track');
audio.onblur = function (e) {
if (e.target != e.relatedTarget) {
setTimeout(function() {
e.target.focus({preventScroll: true, focusVisible: false});
})
}
}
audio.focus({focusVisible: false});

View File

@@ -42,55 +42,13 @@ templ sel_audio(name string, selected string, noOpus bool) {
templ Preferences(prefs cfg.Preferences) {
<h1>Preferences</h1>
<form method="post" autocomplete="off">
<label>
Parse descriptions:
@checkbox("ParseDescriptions", *prefs.ParseDescriptions)
</label>
<label>
Show current audio:
@checkbox("ShowAudio", *prefs.ShowAudio)
</label>
if cfg.ProxyImages {
<label>
Proxy images:
@checkbox("ProxyImages", *prefs.ProxyImages)
</label>
}
if cfg.Restream {
<label>
Download audio:
@sel_audio("DownloadAudio", *prefs.DownloadAudio, false)
</label>
}
<label>
Autoplay next track in playlists:
@checkbox("AutoplayNextTrack", *prefs.AutoplayNextTrack)
(requires JS; you need to allow autoplay from this domain!!)
</label>
if *prefs.AutoplayNextTrack {
<label>
Default autoplay mode (in playlists):
@sel("DefaultAutoplayMode", []option{
{"normal", "Normal (play songs in order)", false},
{"random", "Random (play random song)", false},
}, *prefs.DefaultAutoplayMode)
</label>
}
<label>
Autoplay next related track:
@checkbox("AutoplayNextRelatedTrack", *prefs.AutoplayNextRelatedTrack)
(requires JS; you need to allow autoplay from this domain!!)
</label>
<label>
Fetch search suggestions:
@checkbox("SearchSuggestions", *prefs.SearchSuggestions)
(requires JS)
</label>
<label>
Dynamically load comments:
@checkbox("DynamicLoadComments", *prefs.DynamicLoadComments)
(requires JS)
</label>
<h1>Player preferences</h1>
<label>
Player:
@sel("Player", []option{
@@ -101,7 +59,6 @@ templ Preferences(prefs cfg.Preferences) {
</label>
switch *prefs.Player {
case cfg.HLSPlayer:
<h1>Player-specific preferences</h1>
if cfg.ProxyStreams {
<label>
Proxy song streams:
@@ -117,12 +74,60 @@ templ Preferences(prefs cfg.Preferences) {
@sel_audio("HLSAudio", *prefs.HLSAudio, true)
</label>
case cfg.RestreamPlayer:
<h1>Player-specific preferences</h1>
<label>
Streaming audio:
@sel_audio("RestreamAudio", *prefs.RestreamAudio, false)
</label>
}
<h1>Frontend enhancements</h1>
if cfg.ProxyImages {
<label>
Proxy images:
@checkbox("ProxyImages", *prefs.ProxyImages)
</label>
}
<label>
Parse descriptions:
@checkbox("ParseDescriptions", *prefs.ParseDescriptions)
</label>
<label>
Show current audio:
@checkbox("ShowAudio", *prefs.ShowAudio)
</label>
<label>
Fetch search suggestions:
@checkbox("SearchSuggestions", *prefs.SearchSuggestions)
(requires JS)
</label>
<label>
Dynamically load comments:
@checkbox("DynamicLoadComments", *prefs.DynamicLoadComments)
(requires JS)
</label>
<label>
Keep player focus:
@checkbox("KeepPlayerFocus", *prefs.KeepPlayerFocus)
(requires JS)
</label>
<h2 style="margin-bottom: .35rem">Autoplay</h2>
<i>Requires JS. You also need to allow autoplay from this domain</i>
<label style="margin-top: 1rem">
Autoplay next track in playlists:
@checkbox("AutoplayNextTrack", *prefs.AutoplayNextTrack)
</label>
if *prefs.AutoplayNextTrack {
<label>
Default autoplay mode (in playlists):
@sel("DefaultAutoplayMode", []option{
{"normal", "Normal (play songs in order)", false},
{"random", "Random (play random song)", false},
}, *prefs.DefaultAutoplayMode)
</label>
}
<label>
Autoplay next related track:
@checkbox("AutoplayNextRelatedTrack", *prefs.AutoplayNextRelatedTrack)
</label>
<input type="submit" value="Update" class="btn" style="margin-top: 1rem;"/>
<p>These preferences get saved in a cookie.</p>
</form>
@@ -137,6 +142,5 @@ templ Preferences(prefs cfg.Preferences) {
<input class="btn" type="file" autocomplete="off" name="prefs"/>
<input type="submit" value="Import" class="btn"/>
</form>
<style>label{display:flex;gap:.5rem;align-items:center;margin-bottom:.35rem}</style>
}

View File

@@ -47,38 +47,36 @@ templ TrackHeader(prefs cfg.Preferences, t sc.Track, needPlayer bool) {
<meta name="og:description" content={ t.FormatDescription() }/>
<meta name="og:image" content={ t.Artwork }/>
<link rel="icon" type="image/x-icon" href={ t.Artwork }/>
if needPlayer && *prefs.Player == cfg.HLSPlayer {
if needPlayer {
if *prefs.Player == cfg.HLSPlayer {
<script src="/_/static/js/hls.light.min.js"></script>
}
}
}
func next(c *sc.Track, t *sc.Track, p *sc.Playlist, autoplay bool, mode string, volume string) string {
func next(c *sc.Track, t *sc.Track, p *sc.Playlist, mode string, volume string) string {
r := t.Href()
if p != nil {
r += "?playlist=" + p.Href()[1:]
}
if autoplay {
if p == nil {
r += "?"
} else {
r += "&"
}
r += "autoplay=true"
}
if mode != "" {
r += "&mode=" + mode
}
r += "&"
} else {
r += "?"
if c != nil {
r += "prev=" + string(c.ID) + "&"
}
}
r += "autoplay=true"
if volume != "" {
r += "&volume=" + volume
}
if p == nil {
r += "&prev=" + string(c.ID)
}
return r
}
@@ -87,19 +85,22 @@ templ TrackPlayer(prefs cfg.Preferences, track sc.Track, stream string, displayE
{{ return }}
}
if displayErr == "" {
{{ var audioPref string }}
{{ var audioPref *string }}
if cfg.Restream && *prefs.Player == cfg.RestreamPlayer {
{{ audioPref = *prefs.RestreamAudio }}
{{ audioPref = prefs.RestreamAudio }}
if nextTrack != nil {
<audio id="track" src={ "/_/restream" + track.Href() } controls autoplay?={ autoplay } data-next={ next(&track, nextTrack, playlist, true, mode, "") } volume={ volume }></audio>
<audio id="track" src={ "/_/restream" + track.Href() } controls autoplay?={ autoplay } data-next={ next(&track, nextTrack, playlist, mode, "") } volume={ volume }></audio>
<script async src="/_/static/restream.js"></script>
} else {
<audio src={ "/_/restream" + track.Href() } controls autoplay?={ autoplay }></audio>
<audio id="track" src={ "/_/restream" + track.Href() } controls autoplay?={ autoplay }></audio>
}
if *prefs.KeepPlayerFocus {
<script async src="/_/static/keepfocus.js"></script>
}
} else if stream != "" {
{{ audioPref = *prefs.HLSAudio }}
{{ audioPref = prefs.HLSAudio }}
if nextTrack != nil {
<audio id="track" src={ stream } controls autoplay?={ autoplay } data-next={ next(&track, nextTrack, playlist, true, mode, "") } volume={ volume }></audio>
<audio id="track" src={ stream } controls autoplay?={ autoplay } data-next={ next(&track, nextTrack, playlist, mode, "") } volume={ volume }></audio>
} else {
<audio id="track" src={ stream } controls autoplay?={ autoplay }></audio>
}
@@ -108,6 +109,9 @@ templ TrackPlayer(prefs cfg.Preferences, track sc.Track, stream string, displayE
} else {
<script async src="/_/static/player.js"></script>
}
if *prefs.KeepPlayerFocus {
<script async src="/_/static/keepfocus.js"></script>
}
<noscript>
<br/>
JavaScript is disabled! Audio playback may not work without it enabled.
@@ -124,7 +128,7 @@ templ TrackPlayer(prefs cfg.Preferences, track sc.Track, stream string, displayE
}
if *prefs.ShowAudio {
<div>
if audioPref == cfg.AudioBest {
if *audioPref == cfg.AudioBest {
<p>Audio: best ({ audio })</p>
} else {
<p>Audio: { audio }</p>
@@ -178,21 +182,19 @@ templ Track(prefs cfg.Preferences, t sc.Track, stream string, displayErr string,
if nextTrack != nil {
<details open style="margin-bottom: 1rem;">
<summary>Playback info</summary>
if playlist != nil {
<h2>In playlist:</h2>
@PlaylistItem(playlist, true)
}
<h2>Next track:</h2>
@TrackItem(nextTrack, true, next(&t, nextTrack, playlist, true, mode, volume))
@TrackItem(nextTrack, true, next(&t, nextTrack, playlist, mode, volume))
<div style="display: flex; gap: 1rem">
if playlist != nil {
<a href={ templ.SafeURL(t.Href()) } class="btn">Stop playlist playback</a>
if mode != cfg.AutoplayRandom {
<a href={ templ.SafeURL(next(nil, &t, playlist, false, cfg.AutoplayRandom, volume)) } class="btn">Switch to random mode</a>
<a href={ templ.SafeURL(next(nil, &t, playlist, cfg.AutoplayRandom, volume)) } class="btn">Switch to random mode</a>
} else {
<a href={ templ.SafeURL(next(nil, &t, playlist, false, cfg.AutoplayNormal, volume)) } class="btn">Switch to normal mode</a>
<a href={ templ.SafeURL(next(nil, &t, playlist, cfg.AutoplayNormal, volume)) } class="btn">Switch to normal mode</a>
}
} else {
<a href={ templ.SafeURL(t.Href() + "?playRelated=false") } class="btn">Stop playback</a>