RSS feeds for users

This commit is contained in:
Laptop
2025-10-26 10:03:42 +02:00
parent e8c6912cbd
commit a415d883f8
10 changed files with 165 additions and 13 deletions

5
.gitignore vendored
View File

@@ -22,4 +22,7 @@ regexp2_codegen.go
# built binary
main
!.gitkeep
!.gitkeep
# for development/testing
Caddyfile

View File

@@ -1,4 +1,4 @@
ARG GO_VERSION=1.25.2
ARG GO_VERSION=1.25.3
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build
ARG TARGETOS

View File

@@ -7,6 +7,7 @@ Frontend for SoundCloud
- Works without JavaScript (for most of the functionality)
- Pretty configurable. Both for instance maintainers, and for regular users
- URL scheme similar to SoundCloud's in most places
- RSS Feeds available to follow artists
- It's also possible to download tracks, together with metadata
# Screenshots

View File

@@ -26,6 +26,12 @@ soundcloak tries to keep the URL schemes same to SoundCloud's, so you can just r
To automatically redirect, you can use [LibRedirect](https://libredirect.github.io/) extension. Soundcloak is supported
# Following artists
There is no accounts system yet (maybe in the future?), but you can use [RSS](https://en.wikipedia.org/wiki/RSS) feeds to keep up with new tracks.
You can subscribe to `<instance>/_/rss/<username>` in your feed reader program. Thunderbird works well for this
# Extra notes
If you find music that you like, make sure to download it! Stuff that's on there may be deleted or changed at any moment, without any warning or ability to experience it again, unless you download it for yourself. Download button is available if `Restream` is enabled in backend config. You can configure audio preset for downloading in preferences page. For easily and quickly downloading entire users or playlists, you can use my tool [scrip](https://git.maid.zone/laptop/scrip)

9
go.mod
View File

@@ -1,16 +1,17 @@
module git.maid.zone/stuff/soundcloak
go 1.25.2
go 1.25.3
require (
github.com/a-h/templ v0.3.943
github.com/a-h/templ v0.3.960
github.com/bogem/id3v2/v2 v2.1.4
github.com/dlclark/regexp2 v1.11.5-0.20240806004527-5bbbed8ea10b
github.com/gcottom/mp4meta v0.0.5
github.com/gcottom/oggmeta v0.0.8
github.com/goccy/go-json v0.10.5
github.com/gofiber/fiber/v3 v3.0.0-rc.2
github.com/valyala/fasthttp v1.67.0
github.com/gorilla/feeds v1.2.0
github.com/valyala/fasthttp v1.68.0
)
require (
@@ -27,7 +28,7 @@ require (
github.com/gofiber/schema v1.6.0 // indirect
github.com/gofiber/utils/v2 v2.0.0-rc.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/natefinch/atomic v1.0.1 // indirect

20
go.sum
View File

@@ -2,8 +2,8 @@ git.maid.zone/stuff/soundcloakctl v0.0.0-20250828150155-48445273aad6 h1:PlLbk5bP
git.maid.zone/stuff/soundcloakctl v0.0.0-20250828150155-48445273aad6/go.mod h1:6+m2N2tJFsrHMZ2GH7h50oAbtECoPrJr12YkZdYucuA=
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
github.com/a-h/templ v0.3.943 h1:o+mT/4yqhZ33F3ootBiHwaY4HM5EVaOJfIshvd5UNTY=
github.com/a-h/templ v0.3.943/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
github.com/a-h/templ v0.3.960 h1:trshEpGa8clF5cdI39iY4ZrZG8Z/QixyzEyUnA7feTM=
github.com/a-h/templ v0.3.960/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
github.com/abema/go-mp4 v1.4.1 h1:YoS4VRqd+pAmddRPLFf8vMk74kuGl6ULSjzhsIqwr6M=
github.com/abema/go-mp4 v1.4.1/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=
github.com/aler9/writerseeker v1.1.0 h1:t+Sm3tjp8scNlqyoa8obpeqwciMNOvdvsxjxEb3Sx3g=
@@ -47,12 +47,18 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc=
github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -67,6 +73,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/shamaton/msgpack/v2 v2.3.1 h1:R3QNLIGA/tbdczNMZ5PCRxrXvy+fnzsIaHG4kKMgWYo=
github.com/shamaton/msgpack/v2 v2.3.1/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -82,8 +90,8 @@ github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8=
github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.67.0 h1:tqKlJMUP6iuNG8hGjK/s9J4kadH7HLV4ijEcPGsezac=
github.com/valyala/fasthttp v1.67.0/go.mod h1:qYSIpqt/0XNmShgo/8Aq8E3UYWVVwNS2QYmzd8WIEPM=
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=

20
lib/sc/rss.templ Normal file
View File

@@ -0,0 +1,20 @@
package sc
import "git.maid.zone/stuff/soundcloak/lib/cfg"
import "git.maid.zone/stuff/soundcloak/lib/textparsing"
templ TrackDescription(prefs cfg.Preferences, t *Track, href string) {
if t.Artwork != "" {
<img src={ t.Artwork } width="300px"/>
}
<h1><a href={ templ.SafeURL(href) }>{ t.Title }</a></h1>
<p style="white-space: pre-wrap;">
if t.Description != "" {
if *prefs.ParseDescriptions {
@templ.Raw(textparsing.Format(t.Description))
} else {
{ t.Description }
}
}
</p>
}

View File

@@ -1,7 +1,10 @@
package sc
import (
"context"
"encoding/xml"
"fmt"
"log"
"net/url"
"strconv"
"strings"
@@ -11,6 +14,7 @@ import (
"git.maid.zone/stuff/soundcloak/lib/cfg"
"git.maid.zone/stuff/soundcloak/lib/textparsing"
"github.com/goccy/go-json"
"github.com/gorilla/feeds"
"github.com/valyala/fasthttp"
)
@@ -404,3 +408,70 @@ func (u User) GetFollowing(cid string, prefs cfg.Preferences, args string) (*Pag
return &p, nil
}
func t(s string) string {
parsed, err := time.Parse(time.RFC3339, s)
if err == nil {
return parsed.Format(time.RFC1123Z)
}
return ""
}
// TODO: maybe add option for caching generated feeds? could benefit when many people follow same artists
func (u *User) GenerateFeed(ctx context.Context, cid string, prefs cfg.Preferences, base string) ([]byte, error) {
tracks, err := u.GetTracks(cid, prefs, "?limit=20")
if err != nil {
return nil, err
}
f := feeds.RssFeed{
Title: "Tracks from " + u.Username,
Link: base + "/" + u.Permalink,
ManagingEditor: u.Username + " (@" + u.Permalink + ")",
Category: "Music",
Generator: "soundcloak",
Ttl: int(cfg.UserTTL / time.Second),
}
f.Description = "Recently released tracks by " + f.ManagingEditor
if len(tracks.Collection) != 0 {
f.LastBuildDate = t(tracks.Collection[0].LastModified)
for _, track := range tracks.Collection {
item := feeds.RssItem{
Title: track.Title,
Link: base + "/" + u.Permalink + "/" + track.Permalink,
Category: track.Genre,
Guid: &feeds.RssGuid{Id: string(track.ID), IsPermaLink: "false"},
PubDate: t(track.LastModified),
}
if cfg.ProxyImages && *prefs.ProxyImages {
track.Artwork = base + track.Artwork
}
track.Artwork = strings.Replace(track.Artwork, "-t200x200.", "-original.", 1)
buf := strings.Builder{}
err = TrackDescription(prefs, track, item.Link).Render(ctx, &buf)
if err != nil {
log.Printf("error generating %s (%s) feed: %s\n", u.Permalink, track.Permalink, err)
continue
}
item.Description = buf.String()
f.Items = append(f.Items, &item)
}
} else {
f.LastBuildDate = t(u.LastModified)
}
f.PubDate = f.LastBuildDate
return xml.Marshal(feeds.RssFeedXml{
Version: "2.0",
Channel: &f,
ContentNamespace: "http://purl.org/rss/1.0/modules/content/",
})
}

42
main.go
View File

@@ -40,6 +40,14 @@ func boolean(b bool) string {
return "Disabled"
}
func boolean2(s []byte) *bool {
if string(s) == "true" {
return &cfg.True
} else {
return &cfg.False
}
}
type compressionMap = map[string][][]byte
func parseCompressionMap(path string, filesystem fs.FS) compressionMap {
@@ -948,6 +956,40 @@ Disallow: /`)
return render(c, templates.Comments(comm))
})
app.Get("/_/rss/:user", func(c fiber.Ctx) error {
prefs, err := preferences.Get(c)
if err != nil {
return err
}
if cfg.ProxyImages {
q := c.RequestCtx().QueryArgs()
b := q.Peek("proxy_images")
if b != nil {
prefs.ProxyImages = boolean2(b)
}
}
cid, err := sc.GetClientID()
if err != nil {
return err
}
usr, err := sc.GetUser(cid, c.Params("user"))
if err != nil {
log.Printf("error getting %s (rss): %s\n", c.Params("user"), err)
return err
}
feed, err := usr.GenerateFeed(c.RequestCtx(), cid, prefs, c.BaseURL())
if err != nil {
return err
}
c.RequestCtx().SetContentType("application/rss+xml")
return c.Send(feed)
})
app.Get("/:user", func(c fiber.Ctx) error {
prefs, err := preferences.Get(c)
if err != nil {

View File

@@ -95,7 +95,7 @@ type btn struct {
templ UserButtons(current string, user sc.User) {
<div class="btns">
// this part is the tedious one now, because formatting breaks if i space the list out with newlines
for _, b := range [...]btn{{"tracks", "", false, false},{"popular tracks", "/popular-tracks", false, false},{"playlists", "/sets", false, false},{"albums", "/albums", false, false},{"reposts","/reposts", false, false},{"related", "/_/related", false, false},{"user station", "/discover/sets/"+user.Station, true, false},{"view on soundcloud", "https://soundcloud.com/"+user.Permalink, true, true}} {
for _, b := range [...]btn{{"tracks", "", false, false},{"popular tracks", "/popular-tracks", false, false},{"playlists", "/sets", false, false},{"albums", "/albums", false, false},{"reposts","/reposts", false, false},{"related", "/_/related", false, false},{"user station", "/discover/sets/"+user.Station, true, false},{"rss feed", "/_/rss/"+user.Permalink, true, false},{"view on soundcloud", "https://soundcloud.com/"+user.Permalink, true, true}} {
<a
if b.text == current {
class="btn active"