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

@@ -8,7 +8,8 @@
| 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 |
| 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 |

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
}
@@ -85,6 +89,7 @@ type PrefsForm struct {
ProxyStreams string
FullyPreloadTrack string
AutoplayNextTrack string
AutoplayNextRelatedTrack string
DefaultAutoplayMode string
HLSAudio string
RestreamAudio string
@@ -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,19 +175,27 @@ 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>
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>