option to autoplay a related track

This commit is contained in:
Laptop
2025-02-24 23:27:58 +02:00
parent 6daeac4638
commit 3718ef7e66
7 changed files with 124 additions and 64 deletions

View File

@@ -3,15 +3,16 @@
| 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 | DefaultAutoplayMode | "normal" | "normal", "random" | Default mode for autoplay. Normal - play songs in order. Random - play random song next |
| 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 | 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 |
| 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 | 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

View File

@@ -130,6 +130,7 @@ func defaultPreferences() {
DefaultPreferences.ParseDescriptions = &True
DefaultPreferences.AutoplayNextTrack = &False
DefaultPreferences.AutoplayNextRelatedTrack = &False
p2 := AutoplayNormal
DefaultPreferences.DefaultAutoplayMode = &p2
@@ -188,6 +189,12 @@ func loadDefaultPreferences(loaded Preferences) {
DefaultPreferences.AutoplayNextTrack = &False
}
if loaded.AutoplayNextRelatedTrack != nil {
DefaultPreferences.AutoplayNextRelatedTrack = loaded.AutoplayNextRelatedTrack
} else {
DefaultPreferences.AutoplayNextRelatedTrack = &False
}
if loaded.DefaultAutoplayMode != nil {
DefaultPreferences.DefaultAutoplayMode = loaded.DefaultAutoplayMode
} else {

View File

@@ -64,6 +64,9 @@ type Preferences struct {
// Automatically play next track in playlists
AutoplayNextTrack *bool
// Automatically play next related track
AutoplayNextRelatedTrack *bool
DefaultAutoplayMode *string // "normal" or "random"
// Check above for more info

View File

@@ -36,6 +36,10 @@ func Defaults(dst *cfg.Preferences) {
dst.AutoplayNextTrack = cfg.DefaultPreferences.AutoplayNextTrack
}
if dst.AutoplayNextRelatedTrack == nil {
dst.AutoplayNextRelatedTrack = cfg.DefaultPreferences.AutoplayNextRelatedTrack
}
if dst.DefaultAutoplayMode == nil {
dst.DefaultAutoplayMode = cfg.DefaultPreferences.DefaultAutoplayMode
}
@@ -79,19 +83,20 @@ func Get(c fiber.Ctx) (cfg.Preferences, error) {
}
type PrefsForm struct {
ProxyImages string
ParseDescriptions string
Player string
ProxyStreams string
FullyPreloadTrack string
AutoplayNextTrack string
DefaultAutoplayMode string
HLSAudio string
RestreamAudio string
DownloadAudio string
ShowAudio string
SearchSuggestions string
DynamicLoadComments string
ProxyImages string
ParseDescriptions string
Player string
ProxyStreams string
FullyPreloadTrack string
AutoplayNextTrack string
AutoplayNextRelatedTrack string
DefaultAutoplayMode string
HLSAudio string
RestreamAudio string
DownloadAudio string
ShowAudio string
SearchSuggestions string
DynamicLoadComments string
}
type Export struct {
@@ -131,6 +136,12 @@ func Load(r *fiber.App) {
old.AutoplayNextTrack = &cfg.False
}
if p.AutoplayNextRelatedTrack == "on" {
old.AutoplayNextRelatedTrack = &cfg.True
} else {
old.AutoplayNextRelatedTrack = &cfg.False
}
if p.ShowAudio == "on" {
old.ShowAudio = &cfg.True
} else {

67
main.go
View File

@@ -52,8 +52,7 @@ func (osfs) Open(name string) (fs.File, error) {
return f, err
}
type staticfs struct {
}
type staticfs struct{}
func (staticfs) Open(name string) (fs.File, error) {
misc.Log("staticfs:", name)
@@ -150,7 +149,7 @@ func main() {
return err
}
c.Set("Content-Type", "text/html")
c.Response().Header.SetContentType("text/html")
return templates.Base("", templates.MainPage(prefs), templates.MainPageHead()).Render(c.RequestCtx(), c)
}
@@ -158,7 +157,6 @@ func main() {
app.Get("/index.html", mainPageHandler)
}
const AssetsCacheControl = "public, max-age=28800" // 8hrs
if cfg.EmbedFiles {
misc.Log("using embedded files")
ServeFromFS(app, staticfs{})
@@ -171,7 +169,7 @@ func main() {
// try to load favicon from default location,
// and this path loads the user "favicon" by default
app.Get("favicon.ico", func(c fiber.Ctx) error {
return c.Redirect().To("/_/static/favicon.ico")
return c.Redirect().Status(fiber.StatusPermanentRedirect).To("/_/static/favicon.ico")
})
app.Get("robots.txt", func(c fiber.Ctx) error {
@@ -195,7 +193,7 @@ Disallow: /`)
return err
}
c.Set("Content-Type", "text/html")
c.Response().Header.SetContentType("text/html")
return templates.Base("tracks: "+q, templates.SearchTracks(p), nil).Render(c.RequestCtx(), c)
case "users":
@@ -205,7 +203,7 @@ Disallow: /`)
return err
}
c.Set("Content-Type", "text/html")
c.Response().Header.SetContentType("text/html")
return templates.Base("users: "+q, templates.SearchUsers(p), nil).Render(c.RequestCtx(), c)
case "playlists":
@@ -215,7 +213,7 @@ Disallow: /`)
return err
}
c.Set("Content-Type", "text/html")
c.Response().Header.SetContentType("text/html")
return templates.Base("playlists: "+q, templates.SearchPlaylists(p), nil).Render(c.RequestCtx(), c)
}
@@ -310,7 +308,7 @@ Disallow: /`)
}
}
c.Set("Content-Type", "text/html")
c.Response().Header.SetContentType("text/html")
return templates.TrackEmbed(prefs, track, stream, displayErr).Render(c.RequestCtx(), c)
})
@@ -332,7 +330,7 @@ Disallow: /`)
return err
}
c.Set("Content-Type", "text/html")
c.Response().Header.SetContentType("text/html")
return templates.Base("Recent tracks tagged "+tag, templates.RecentTracks(tag, p), nil).Render(c.RequestCtx(), c)
})
@@ -354,7 +352,7 @@ Disallow: /`)
return err
}
c.Set("Content-Type", "text/html")
c.Response().Header.SetContentType("text/html")
return templates.Base("Popular tracks tagged "+tag, templates.PopularTracks(tag, p), nil).Render(c.RequestCtx(), c)
})
@@ -377,7 +375,7 @@ Disallow: /`)
return err
}
c.Set("Content-Type", "text/html")
c.Response().Header.SetContentType("text/html")
return templates.Base("Playlists tagged "+tag, templates.TaggedPlaylists(tag, p), nil).Render(c.RequestCtx(), c)
})
@@ -393,7 +391,7 @@ Disallow: /`)
return err
}
c.Set("Content-Type", "text/html")
c.Response().Header.SetContentType("text/html")
return templates.Base("Featured Tracks", templates.FeaturedTracks(tracks), nil).Render(c.RequestCtx(), c)
})
@@ -409,7 +407,7 @@ Disallow: /`)
return err
}
c.Set("Content-Type", "text/html")
c.Response().Header.SetContentType("text/html")
return templates.Base("Discover", templates.Discover(selections), nil).Render(c.RequestCtx(), c)
})
@@ -446,7 +444,7 @@ Disallow: /`)
}
app.Get("/_/info", func(c fiber.Ctx) error {
c.Set("Content-Type", "application/json")
c.Response().Header.SetContentType("application/json")
return c.Send(inf)
})
}
@@ -500,7 +498,7 @@ Disallow: /`)
return err
}
c.Set("Content-Type", "text/html")
c.Response().Header.SetContentType("text/html")
return templates.Base(user.Username, templates.UserPlaylists(prefs, user, pl), templates.UserHeader(user)).Render(c.RequestCtx(), c)
})
@@ -528,7 +526,7 @@ Disallow: /`)
return err
}
c.Set("Content-Type", "text/html")
c.Response().Header.SetContentType("text/html")
return templates.Base(user.Username, templates.UserAlbums(prefs, user, pl), templates.UserHeader(user)).Render(c.RequestCtx(), c)
})
@@ -556,7 +554,7 @@ Disallow: /`)
return err
}
c.Set("Content-Type", "text/html")
c.Response().Header.SetContentType("text/html")
return templates.Base(user.Username, templates.UserReposts(prefs, user, p), templates.UserHeader(user)).Render(c.RequestCtx(), c)
})
@@ -584,7 +582,7 @@ Disallow: /`)
return err
}
c.Set("Content-Type", "text/html")
c.Response().Header.SetContentType("text/html")
return templates.Base(user.Username, templates.UserLikes(prefs, user, p), templates.UserHeader(user)).Render(c.RequestCtx(), c)
})
@@ -612,7 +610,7 @@ Disallow: /`)
return err
}
c.Set("Content-Type", "text/html")
c.Response().Header.SetContentType("text/html")
return templates.Base(user.Username, templates.UserTopTracks(prefs, user, p), templates.UserHeader(user)).Render(c.RequestCtx(), c)
})
@@ -640,7 +638,7 @@ Disallow: /`)
return err
}
c.Set("Content-Type", "text/html")
c.Response().Header.SetContentType("text/html")
return templates.Base(user.Username, templates.UserFollowers(prefs, user, p), templates.UserHeader(user)).Render(c.RequestCtx(), c)
})
@@ -668,7 +666,7 @@ Disallow: /`)
return err
}
c.Set("Content-Type", "text/html")
c.Response().Header.SetContentType("text/html")
return templates.Base(user.Username, templates.UserFollowing(prefs, user, p), templates.UserHeader(user)).Render(c.RequestCtx(), c)
})
@@ -762,6 +760,17 @@ Disallow: /`)
}
}
if *prefs.AutoplayNextRelatedTrack && nextTrack == nil && string(c.RequestCtx().QueryArgs().Peek("playRelated")) != "false" {
rel, err := track.GetRelated(cid, prefs, "?limit=4")
if err == nil && len(rel.Collection) != 0 {
prev := c.RequestCtx().QueryArgs().Peek("prev")
nextTrack = &track
for i := len(rel.Collection) - 1; i >= 0 && (string(nextTrack.ID) == string(track.ID) || string(nextTrack.ID) == string(prev)); i-- {
nextTrack = rel.Collection[i]
}
}
}
var comments *sc.Paginated[*sc.Comment]
if q := c.Query("pagination"); q != "" {
comments, err = track.GetComments(cid, prefs, q)
@@ -777,7 +786,7 @@ Disallow: /`)
downloadAudio = &audio
}
c.Set("Content-Type", "text/html")
c.Response().Header.SetContentType("text/html")
return templates.Base(track.Title+" by "+track.Author.Username, templates.Track(prefs, track, stream, displayErr, string(c.RequestCtx().QueryArgs().Peek("autoplay")) == "true", playlist, nextTrack, c.Query("volume"), mode, audio, downloadAudio, comments), templates.TrackHeader(prefs, track, true)).Render(c.RequestCtx(), c)
})
@@ -836,7 +845,7 @@ Disallow: /`)
return err
}
c.Set("Content-Type", "text/html")
c.Response().Header.SetContentType("text/html")
return templates.Base(usr.Username, templates.User(prefs, usr, p), templates.UserHeader(usr)).Render(c.RequestCtx(), c)
})
@@ -876,7 +885,7 @@ Disallow: /`)
playlist.MissingTracks = strings.Join(next, ",")
}
c.Set("Content-Type", "text/html")
c.Response().Header.SetContentType("text/html")
return templates.Base(playlist.Title+" by "+playlist.Author.Username, templates.Playlist(prefs, playlist), templates.PlaylistHeader(playlist)).Render(c.RequestCtx(), c)
})
@@ -904,7 +913,7 @@ Disallow: /`)
return err
}
c.Set("Content-Type", "text/html")
c.Response().Header.SetContentType("text/html")
return templates.Base(user.Username, templates.UserRelated(prefs, user, r), templates.UserHeader(user)).Render(c.RequestCtx(), c)
})
@@ -933,7 +942,7 @@ Disallow: /`)
return err
}
c.Set("Content-Type", "text/html")
c.Response().Header.SetContentType("text/html")
return templates.Base(track.Title+" by "+track.Author.Username, templates.RelatedTracks(track, r), templates.TrackHeader(prefs, track, false)).Render(c.RequestCtx(), c)
})
@@ -961,7 +970,7 @@ Disallow: /`)
return err
}
c.Set("Content-Type", "text/html")
c.Response().Header.SetContentType("text/html")
return templates.Base(track.Title+" by "+track.Author.Username, templates.TrackInPlaylists(track, p), templates.TrackHeader(prefs, track, false)).Render(c.RequestCtx(), c)
})
@@ -989,7 +998,7 @@ Disallow: /`)
return err
}
c.Set("Content-Type", "text/html")
c.Response().Header.SetContentType("text/html")
return templates.Base(track.Title+" by "+track.Author.Username, templates.TrackInAlbums(track, p), templates.TrackHeader(prefs, track, false)).Render(c.RequestCtx(), c)
})

View File

@@ -69,13 +69,18 @@ templ Preferences(prefs cfg.Preferences) {
</label>
if *prefs.AutoplayNextTrack {
<label>
Default autoplay mode:
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)

View File

@@ -52,17 +52,33 @@ templ TrackHeader(prefs cfg.Preferences, t sc.Track, needPlayer bool) {
}
}
func next(t *sc.Track, p *sc.Playlist, autoplay bool, mode string, volume string) string {
r := t.Href() + "?playlist=" + p.Href()[1:]
if autoplay {
r += "&autoplay=true"
func next(c *sc.Track, t *sc.Track, p *sc.Playlist, autoplay bool, 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
}
if volume != "" {
r += "&volume=" + volume
}
if p == nil {
r += "&prev=" + string(c.ID)
}
return r
}
@@ -75,7 +91,7 @@ templ TrackPlayer(prefs cfg.Preferences, track sc.Track, stream string, displayE
if cfg.Restream && *prefs.Player == cfg.RestreamPlayer {
{{ audioPref = *prefs.RestreamAudio }}
if nextTrack != nil {
<audio id="track" src={ "/_/restream" + track.Href() } controls autoplay?={ autoplay } data-next={ next(nextTrack, playlist, true, mode, "") } volume={ volume }></audio>
<audio id="track" src={ "/_/restream" + track.Href() } controls autoplay?={ autoplay } data-next={ next(&track, nextTrack, playlist, true, mode, "") } volume={ volume }></audio>
<script async src="/_/static/restream.js"></script>
} else {
<audio src={ "/_/restream" + track.Href() } controls autoplay?={ autoplay }></audio>
@@ -83,7 +99,7 @@ templ TrackPlayer(prefs cfg.Preferences, track sc.Track, stream string, displayE
} else if stream != "" {
{{ audioPref = *prefs.HLSAudio }}
if nextTrack != nil {
<audio id="track" src={ stream } controls autoplay?={ autoplay } data-next={ next(nextTrack, playlist, true, mode, "") } volume={ volume }></audio>
<audio id="track" src={ stream } controls autoplay?={ autoplay } data-next={ next(&track, nextTrack, playlist, true, mode, "") } volume={ volume }></audio>
} else {
<audio id="track" src={ stream } controls autoplay?={ autoplay }></audio>
}
@@ -159,20 +175,28 @@ templ Track(prefs cfg.Preferences, t sc.Track, stream string, displayErr string,
if t.Genre != "" {
<a href={ templ.SafeURL("/tags/" + t.Genre) }><p class="tag">{ t.Genre }</p></a>
}
if playlist != nil {
if nextTrack != nil {
<details open style="margin-bottom: 1rem;">
<summary>Playback info</summary>
<h2>In playlist:</h2>
@PlaylistItem(playlist, true)
if playlist != nil {
<h2>In playlist:</h2>
@PlaylistItem(playlist, true)
}
<h2>Next track:</h2>
@TrackItem(nextTrack, true, next(nextTrack, playlist, true, mode, volume))
@TrackItem(nextTrack, true, next(&t, nextTrack, playlist, true, 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(&t, playlist, false, cfg.AutoplayRandom, volume)) } class="btn">Switch to random mode</a>
<a href={ templ.SafeURL(next(nil, &t, playlist, false, cfg.AutoplayRandom, volume)) } class="btn">Switch to random mode</a>
} else {
<a href={ templ.SafeURL(next(&t, playlist, false, cfg.AutoplayNormal, volume)) } class="btn">Switch to normal mode</a>
<a href={ templ.SafeURL(next(nil, &t, playlist, false, cfg.AutoplayNormal, volume)) } class="btn">Switch to normal mode</a>
}
} else {
<a href={ templ.SafeURL(t.Href() + "?playRelated=false") } class="btn">Stop playback</a>
}
</div>
</details>
}