diff --git a/README.md b/README.md index d6b793f..b68d800 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Available features: - Player-specific settings: They will only show up if you have selected HLS player currently. - - Proxy streams: Retrieve song pieces through the instance, instead of going to soundcloud's servers for them - - Fully preload track: Fully loads the track when you load the page instead of buffering a small part of it +- - Autoplay next track in playlists: self-explanatory # Contributing @@ -188,29 +189,29 @@ Some notes: - When specifying time, specify it in seconds. -| JSON key | Environment variable | Default value | Description | -| :------------------------ | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| None | SOUNDCLOAK_CONFIG | soundcloak.json | File to load soundcloak config from. If set to `FROM_ENV`, soundcloak loads the config from environment variables. | -| GetWebProfiles | GET_WEB_PROFILES | true | Retrieve links users set in their profile (social media, website, etc) | -| DefaultPreferences | DEFAULT_PREFERENCES | {"Player": "hls", "ProxyStreams": false, "FullyPreloadTrack": false, "ProxyImages": false, "ParseDescriptions": true} | see /_/preferences page, default values adapt to your config (Player: "restream" if Restream, else "hls", ProxyStreams and ProxyImages will be same as respective config values) | -| ProxyImages | PROXY_IMAGES | false | Enables proxying of images (user avatars, track covers etc) | -| ImageCacheControl | IMAGE_CACHE_CONTROL | max-age=600, public, immutable | [Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) header value for proxied images. Cached for 10 minutes by default. | -| ProxyStreams | PROXY_STREAMS | false | Enables proxying of song parts and hls playlist files | -| Restream | RESTREAM | false | Enables Restream Player in settings and the /_/restream/:author/:track endpoint. This player can be used without JavaScript. Restream also enables the button for downloading songs. | -| RestreamCacheControl | RESTREAM_CACHE_CONTROL | max-age=3600, public, immutable | [Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) header value for restreamed songs. Cached for 1 hour by default. | -| ClientIDTTL | CLIENT_ID_TTL | 30 minutes | Time until ClientID cache expires. ClientID is used for authenticating with SoundCloud API | -| UserTTL | USER_TTL | 10 minutes | Time until User profile cache expires | -| UserCacheCleanDelay | USER_CACHE_CLEAN_DELAY | 2.5 minutes | Time between each cleanup of the cache (to remove expired users) | -| TrackTTL | TRACK_TTL | 10 minutes | Time until Track data cache expires | -| TrackCacheCleanDelay | TRACK_CACHE_CLEAN_DELAY | 2.5 minutes | Time between each cleanup of the cache (to remove expired tracks) | -| PlaylistTTL | PLAYLIST_TTL | 10 minutes | Time until Playlist data cache expires | -| PlaylistCacheCleanDelay | PLAYLIST_CACHE_CLEAN_DELAY | 2.5 minutes | Time between each cleanup of the cache (to remove expired playlists) | -| UserAgent | USER_AGENT | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.3 | User-Agent header used for requests to SoundCloud | -| DNSCacheTTL | DNS_CACHE_TTL | 10 minutes | Time until DNS cache expires | -| Addr | ADDR | :4664 | Address and port for soundcloak to listen on | -| Prefork | PREFORK | false | Run multiple instances of soundcloak locally to be able to handle more requests. Each one will be a separate process, so they will have separate cache. | -| TrustedProxyCheck | TRUSTED_PROXY_CHECK | true | Use X-Forwarded-* headers if IP is in TrustedProxies list. When disabled, those headers will blindly be used. | -| TrustedProxies | TRUSTED_PROXIES | [] | List of IPs or IP ranges of trusted proxies | +| JSON key | Environment variable | Default value | Description | +| :------------------------ | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| None | SOUNDCLOAK_CONFIG | soundcloak.json | File to load soundcloak config from. If set to`FROM_ENV`, soundcloak loads the config from environment variables. | +| GetWebProfiles | GET_WEB_PROFILES | true | Retrieve links users set in their profile (social media, website, etc) | +| DefaultPreferences | DEFAULT_PREFERENCES | {"Player": "hls", "ProxyStreams": false, "FullyPreloadTrack": false, "ProxyImages": false, "ParseDescriptions": true, "AutoplayNextTrack": false} | see /_/preferences page, default values adapt to your config (Player: "restream" if Restream, else "hls", ProxyStreams and ProxyImages will be same as respective config values) | +| ProxyImages | PROXY_IMAGES | false | Enables proxying of images (user avatars, track covers etc) | +| ImageCacheControl | IMAGE_CACHE_CONTROL | max-age=600, public, immutable | [Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) header value for proxied images. Cached for 10 minutes by default. | +| ProxyStreams | PROXY_STREAMS | false | Enables proxying of song parts and hls playlist files | +| Restream | RESTREAM | false | Enables Restream Player in settings and the /_/restream/:author/:track endpoint. This player can be used without JavaScript. Restream also enables the button for downloading songs. | +| RestreamCacheControl | RESTREAM_CACHE_CONTROL | max-age=3600, public, immutable | [Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) header value for restreamed songs. Cached for 1 hour by default. | +| ClientIDTTL | CLIENT_ID_TTL | 4 hours | Time until ClientID cache expires. ClientID is used for authenticating with SoundCloud API | +| UserTTL | USER_TTL | 20 minutes | Time until User profile cache expires | +| UserCacheCleanDelay | USER_CACHE_CLEAN_DELAY | 5 minutes | Time between each cleanup of the cache (to remove expired users) | +| TrackTTL | TRACK_TTL | 20 minutes | Time until Track data cache expires | +| TrackCacheCleanDelay | TRACK_CACHE_CLEAN_DELAY | 5 minutes | Time between each cleanup of the cache (to remove expired tracks) | +| PlaylistTTL | PLAYLIST_TTL | 20 minutes | Time until Playlist data cache expires | +| PlaylistCacheCleanDelay | PLAYLIST_CACHE_CLEAN_DELAY | 5 minutes | Time between each cleanup of the cache (to remove expired playlists) | +| UserAgent | USER_AGENT | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.3 | User-Agent header used for requests to SoundCloud | +| DNSCacheTTL | DNS_CACHE_TTL | 60 minutes | Time until DNS cache expires | +| Addr | ADDR | :4664 | Address and port for soundcloak to listen on | +| Prefork | PREFORK | false | Run multiple instances of soundcloak locally to be able to handle more requests. Each one will be a separate process, so they will have separate cache. | +| TrustedProxyCheck | TRUSTED_PROXY_CHECK | true | Use X-Forwarded-* headers if IP is in TrustedProxies list. When disabled, those headers will blindly be used. | +| TrustedProxies | TRUSTED_PROXIES | [] | List of IPs or IP ranges of trusted proxies | diff --git a/assets/player.js b/assets/player.js index 5a5e4ba..e61473d 100644 --- a/assets/player.js +++ b/assets/player.js @@ -3,6 +3,18 @@ if (Hls.isSupported()) { var hls = new Hls(); hls.loadSource(audio.src); hls.attachMedia(audio); + + var volume = audio.getAttribute('volume'); + if (volume) { + audio.volume = parseFloat(volume); + } } else if (!audio.canPlayType('application/vnd.apple.mpegurl')) { alert('HLS is not supported! Audio playback will not work.'); +} + +var next = audio.getAttribute('data-next'); +if (next) { + audio.addEventListener('ended', function() { + location = next + '&volume=' + audio.volume; + }); } \ No newline at end of file diff --git a/assets/player_preload.js b/assets/player_preload.js index e2bd6b1..e15afdd 100644 --- a/assets/player_preload.js +++ b/assets/player_preload.js @@ -3,6 +3,18 @@ if (Hls.isSupported()) { var hls = new Hls({ maxBufferLength: Infinity }); hls.loadSource(audio.src); hls.attachMedia(audio); + + var volume = audio.getAttribute('volume'); + if (volume) { + audio.volume = parseFloat(volume); + } } else if (!audio.canPlayType('application/vnd.apple.mpegurl')) { alert('HLS is not supported! Audio playback will not work.'); +} + +var next = audio.getAttribute('data-next'); +if (next) { + audio.addEventListener('ended', function() { + location = next + '&volume=' + audio.volume; + }); } \ No newline at end of file diff --git a/lib/cfg/init.go b/lib/cfg/init.go index 6598bfe..d3448e1 100644 --- a/lib/cfg/init.go +++ b/lib/cfg/init.go @@ -38,6 +38,9 @@ type Preferences struct { // Highlight @username, https://example.com and email@example.com in text as clickable links ParseDescriptions *bool + + // Automatically play next track in playlists + AutoplayNextTrack *bool } // // config // // @@ -67,22 +70,23 @@ var InstanceInfo = true // time-to-live for clientid cache // larger number will improve performance (no need to recheck everytime) but might make soundcloak briefly unusable for a larger amount of time if the client id is invalidated -var ClientIDTTL = 30 * time.Minute +// I went with 4 hours, since those clientids still remain active for quite some time, even after soundcloud updates it +var ClientIDTTL = 4 * time.Hour // time-to-live for user profile cache -var UserTTL = 10 * time.Minute +var UserTTL = 20 * time.Minute // delay between cleanup of user cache var UserCacheCleanDelay = UserTTL / 4 // time-to-live for track cache -var TrackTTL = 10 * time.Minute +var TrackTTL = 20 * time.Minute // delay between cleanup of track cache var TrackCacheCleanDelay = TrackTTL / 4 // time-to-live for playlist cache -var PlaylistTTL = 10 * time.Minute +var PlaylistTTL = 20 * time.Minute // delay between cleanup of playlist cache var PlaylistCacheCleanDelay = PlaylistTTL / 4 @@ -91,7 +95,7 @@ var PlaylistCacheCleanDelay = PlaylistTTL / 4 var UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.3" // time-to-live for dns cache -var DNSCacheTTL = 10 * time.Minute +var DNSCacheTTL = 60 * time.Minute // // // some webserver configuration, put here to make it easier to configure what you need // // // // more info can be found here: https://docs.gofiber.io/api/fiber#config @@ -118,6 +122,7 @@ var TrustedProxies = []string{} // FullyPreloadTrack: false // ProxyImages: same as ProxyImages in your config (false by default) // ParseDescriptions: true +// AutoplayNextTrack: false func defaultPreferences() { var p string if Restream { @@ -136,6 +141,7 @@ func defaultPreferences() { DefaultPreferences.ProxyImages = &ProxyImages DefaultPreferences.ParseDescriptions = &t + DefaultPreferences.AutoplayNextTrack = &f } func loadDefaultPreferences(loaded Preferences) { @@ -176,6 +182,12 @@ func loadDefaultPreferences(loaded Preferences) { } else { DefaultPreferences.ParseDescriptions = &t } + + if loaded.AutoplayNextTrack != nil { + DefaultPreferences.AutoplayNextTrack = loaded.AutoplayNextTrack + } else { + DefaultPreferences.AutoplayNextTrack = &f + } } func boolean(in string) bool { diff --git a/lib/preferences/init.go b/lib/preferences/init.go index 522d020..5840229 100644 --- a/lib/preferences/init.go +++ b/lib/preferences/init.go @@ -30,6 +30,10 @@ func Defaults(dst *cfg.Preferences) { if dst.ParseDescriptions == nil { dst.ParseDescriptions = cfg.DefaultPreferences.ParseDescriptions } + + if dst.AutoplayNextTrack == nil { + dst.AutoplayNextTrack = cfg.DefaultPreferences.AutoplayNextTrack + } } func Get(c *fiber.Ctx) (cfg.Preferences, error) { @@ -51,6 +55,7 @@ type PrefsForm struct { Player string ProxyStreams string FullyPreloadTrack string + AutoplayNextTrack string } func Load(r fiber.Router) { @@ -85,6 +90,12 @@ func Load(r fiber.Router) { } else if p.ProxyStreams == "" { old.ProxyStreams = &f } + + if p.AutoplayNextTrack == "on" { + old.AutoplayNextTrack = &t + } else { + old.AutoplayNextTrack = &f + } } if p.FullyPreloadTrack == "on" { diff --git a/lib/sc/playlist.go b/lib/sc/playlist.go index 6e2b252..294cdae 100644 --- a/lib/sc/playlist.go +++ b/lib/sc/playlist.go @@ -187,7 +187,7 @@ func (p *Playlist) GetMissingTracks() error { missing := []MissingTrack{} for i, track := range p.Tracks { if track.Title == "" { - missing = append(missing, MissingTrack{ID: track.ID, Index: i}) + missing = append(missing, MissingTrack{ID: strconv.FormatInt(int64(track.IDint), 10), Index: i}) } } @@ -212,3 +212,7 @@ func (p *Playlist) GetMissingTracks() error { return nil } + +func (p Playlist) Href() string { + return "/" + p.Author.Permalink + "/sets/" + p.Permalink +} diff --git a/lib/sc/track.go b/lib/sc/track.go index 18b3008..6a57890 100644 --- a/lib/sc/track.go +++ b/lib/sc/track.go @@ -433,3 +433,7 @@ func (t Track) DownloadImage() ([]byte, string, error) { return data, string(resp.Header.Peek("Content-Type")), nil } + +func (t Track) Href() string { + return "/" + t.Author.Permalink + "/" + t.Permalink +} diff --git a/main.go b/main.go index f2d5dd5..dcabd75 100644 --- a/main.go +++ b/main.go @@ -34,7 +34,7 @@ func main() { app.Use(recover.New()) app.Use(compress.New(compress.Config{Level: compress.LevelBestSpeed})) - app.Static("/", "assets", fiber.Static{Compress: true, MaxAge: 3600}) // 1hour + app.Static("/", "assets", fiber.Static{Compress: true, MaxAge: 7200}) // 2 hours app.Static("/js/hls.js/", "node_modules/hls.js/dist", fiber.Static{Compress: true, MaxAge: 28800}) // 8 hours // Just for easy inspection of cache in development. Since debug is constant, the compiler will just remove the code below if it's set to false, so this has no runtime overhead. @@ -222,6 +222,7 @@ func main() { ProxyImages bool ProxyStreams bool Restream bool + GetWebProfiles bool DefaultPreferences cfg.Preferences } @@ -230,6 +231,7 @@ func main() { ProxyImages: cfg.ProxyImages, ProxyStreams: cfg.ProxyStreams, Restream: cfg.Restream, + GetWebProfiles: cfg.GetWebProfiles, DefaultPreferences: cfg.DefaultPreferences, }) }) @@ -365,8 +367,35 @@ func main() { } } + var playlist *sc.Playlist + var nextTrack *sc.Track + if pl := c.Query("playlist"); pl != "" { + p, err := sc.GetPlaylist(pl) + + if err != nil { + log.Printf("error getting %s playlist (track): %s\n", pl, err) + return err + } + + nextIndex := -1 + for i, t := range p.Tracks { + if t.ID == track.ID { + nextIndex = i + 1 + } + } + + if nextIndex != -1 { + if nextIndex == len(p.Tracks) { + nextIndex = 0 + } + + nextTrack = &p.Tracks[nextIndex] + playlist = &p + } + } + c.Set("Content-Type", "text/html") - return templates.Base(track.Title+" by "+track.Author.Username, templates.Track(prefs, track, stream, displayErr), templates.TrackHeader(prefs, track)).Render(context.Background(), c) + return templates.Base(track.Title+" by "+track.Author.Username, templates.Track(prefs, track, stream, displayErr, c.Query("autoplay") == "true", playlist, nextTrack, c.Query("volume")), templates.TrackHeader(prefs, track)).Render(context.Background(), c) }) app.Get("/:user", func(c *fiber.Ctx) error { diff --git a/templates/featured.templ b/templates/featured.templ index 4a223d0..4cee94b 100644 --- a/templates/featured.templ +++ b/templates/featured.templ @@ -13,7 +13,7 @@ templ FeaturedTracks(p *sc.Paginated[*sc.Track]) {
no more tracks
} else { for _, track := range p.Collection { - @TrackItem(track, true) + @TrackItem(track, true, "") } if p.Next != "" { more tracks diff --git a/templates/playlist.templ b/templates/playlist.templ index 4b2d995..c8779c5 100644 --- a/templates/playlist.templ +++ b/templates/playlist.templ @@ -16,8 +16,12 @@ templ PlaylistHeader(p sc.Playlist) { } +func playlist(t sc.Track, p sc.Playlist) string { + return t.Href() + "?playlist=" + p.Href()[1:] +} + templ PlaylistItem(playlist *sc.Playlist, showUsername bool) { - + if playlist.Artwork != "" {{ t.Genre }
} else {