mirror of
https://git.maid.zone/stuff/soundcloak.git
synced 2025-12-10 05:39:38 +05:00
RSS feeds for users
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -22,4 +22,7 @@ regexp2_codegen.go
|
||||
# built binary
|
||||
main
|
||||
|
||||
!.gitkeep
|
||||
!.gitkeep
|
||||
|
||||
# for development/testing
|
||||
Caddyfile
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
9
go.mod
@@ -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
20
go.sum
@@ -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
20
lib/sc/rss.templ
Normal 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>
|
||||
}
|
||||
@@ -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
42
main.go
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user