image proxying

This commit is contained in:
Laptop
2024-10-26 11:08:05 +03:00
parent 4eae862bd6
commit 9416f14ef7
11 changed files with 143 additions and 30 deletions

View File

@@ -15,6 +15,10 @@ wip alternative frontend for soundcloud
The UI isn't really done yet. All parameters other than url are unsupported. You can also specify track without the `soundcloud.com` part: `https://sc.maid.zone/w/player/?url=<id>` or `https://sc.maid.zone/w/player/?url=<user>/<track>`
- Content proxying
Image proxying is already implemented, track proxying is not done yet
# Contributing
Contributions are appreciated! This includes feedback, feature requests, issues, pull requests and etc.
Feedback and feature requests are especially needed, since I (laptopcat) don't really know what to prioritize

16
go.mod
View File

@@ -3,24 +3,24 @@ module github.com/maid-zone/soundcloak
go 1.21.3
require (
github.com/a-h/templ v0.2.747
github.com/a-h/templ v0.2.778
github.com/gofiber/fiber/v2 v2.52.5
github.com/json-iterator/go v1.1.12
github.com/valyala/fasthttp v1.55.0
github.com/valyala/fasthttp v1.56.0
)
require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/sys v0.26.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

17
go.sum
View File

@@ -1,7 +1,11 @@
github.com/a-h/templ v0.2.747 h1:D0dQ2lxC3W7Dxl6fxQ/1zZHBQslSkTSvl5FxP/CfdKg=
github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2yop4=
github.com/a-h/templ v0.2.778 h1:VzhOuvWECrwOec4790lcLlZpP4Iptt5Q4K9aFxQmtaM=
github.com/a-h/templ v0.2.778/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -16,6 +20,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@@ -23,14 +29,20 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
@@ -39,12 +51,17 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
github.com/valyala/fasthttp v1.56.0 h1:bEZdJev/6LCBlpdORfrLu/WOZXXxvrUQSiyniuaoW8U=
github.com/valyala/fasthttp v1.56.0/go.mod h1:sReBt3XZVnudxuLOx4J/fMrJVorWRiWY2koQKgABiVI=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -11,6 +11,10 @@ import (
// if the stream isn't fully loaded before it expires - you'll need to reload the page
const FullyPreloadTrack = false
// proxy images (user avatars, track/playlist covers)
const ProxyImages = false
const ImageCacheControl = "max-age=600; public" // 10 minutes by default, only used for proxied images
// 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
const ClientIDTTL = 30 * time.Minute

66
lib/proxy_images/init.go Normal file
View File

@@ -0,0 +1,66 @@
package proxyimages
import (
"bytes"
"github.com/gofiber/fiber/v2"
"github.com/maid-zone/soundcloak/lib/cfg"
"github.com/maid-zone/soundcloak/lib/sc"
"github.com/valyala/fasthttp"
)
var sndcdn = []byte(".sndcdn.com")
// seems soundcloud has 4 of these (i1, i2, i3, i4)
// they point to the same ip from my observations, and they all serve the same files
const cdn = "i1.sndcdn.com"
var httpc = &fasthttp.HostClient{
Addr: cdn + ":443",
IsTLS: true,
DialDualStack: true,
Dial: (&fasthttp.TCPDialer{DNSCacheDuration: cfg.DNSCacheTTL}).Dial,
MaxIdleConnDuration: 1<<63 - 1,
}
func Load(r fiber.Router) {
r.Get("/proxy/images", func(c *fiber.Ctx) error {
url := c.Query("url")
if url == "" {
return fiber.ErrBadRequest
}
parsed := fasthttp.AcquireURI()
defer fasthttp.ReleaseURI(parsed)
err := parsed.Parse(nil, []byte(url))
if err != nil {
return err
}
if !bytes.HasSuffix(parsed.Host(), sndcdn) {
return fiber.ErrBadRequest
}
parsed.SetHost(cdn)
req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req)
req.SetURI(parsed)
req.Header.Set("User-Agent", cfg.UserAgent)
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(resp)
err = sc.DoWithRetry(httpc, req, resp)
if err != nil {
return err
}
c.Response().Header.SetBytesV("Content-Type", resp.Header.Peek("Content-Type"))
c.Set("Cache-Control", cfg.ImageCacheControl)
return c.Send(resp.Body())
})
}

View File

@@ -21,12 +21,12 @@ var clientIdCache struct {
const api = "api-v2.soundcloud.com"
var httpc = fasthttp.HostClient{
Addr: api + ":443",
IsTLS: true,
DialDualStack: true,
Dial: (&fasthttp.TCPDialer{DNSCacheDuration: cfg.DNSCacheTTL}).Dial,
//MaxIdleConnDuration: 1<<63 - 1, //seems to cause some issues
var httpc = &fasthttp.HostClient{
Addr: api + ":443",
IsTLS: true,
DialDualStack: true,
Dial: (&fasthttp.TCPDialer{DNSCacheDuration: cfg.DNSCacheTTL}).Dial,
MaxIdleConnDuration: 1<<63 - 1, //improves performance but seems to cause some issues, need more testing
}
var verRegex = regexp.MustCompile(`(?m)^<script>window\.__sc_version="([0-9]{10})"</script>$`)
@@ -116,7 +116,7 @@ func GetClientID() (string, error) {
return "", ErrIDNotFound
}
func DoWithRetry(req *fasthttp.Request, resp *fasthttp.Response) (err error) {
func DoWithRetry(httpc *fasthttp.HostClient, req *fasthttp.Request, resp *fasthttp.Response) (err error) {
for i := 0; i < 5; i++ {
err = httpc.Do(req, resp)
if err == nil {
@@ -147,7 +147,7 @@ func Resolve(path string, out any) error {
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(resp)
err = DoWithRetry(req, resp)
err = DoWithRetry(httpc, req, resp)
if err != nil {
return err
}
@@ -187,7 +187,7 @@ func (p *Paginated[T]) Proceed() error {
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(resp)
err = DoWithRetry(req, resp)
err = DoWithRetry(httpc, req, resp)
if err != nil {
return err
}

View File

@@ -1,6 +1,7 @@
package sc
import (
"net/url"
"strconv"
"strings"
"sync"
@@ -99,6 +100,10 @@ func (p *Playlist) Fix(cached bool) error {
p.Artwork = strings.Replace(p.Artwork, "-large.", "-t200x200.", 1)
}
if cfg.ProxyImages && p.Artwork != "" {
p.Artwork = "/proxy/images?url=" + url.QueryEscape(p.Artwork)
}
p.Author.Fix(false)
return nil

View File

@@ -230,7 +230,7 @@ func GetTracks(ids string) ([]*Track, error) {
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(resp)
err = DoWithRetry(req, resp)
err = DoWithRetry(httpc, req, resp)
if err != nil {
return nil, err
}
@@ -269,7 +269,7 @@ func (t Track) GetStream() (string, error) {
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(resp)
err = DoWithRetry(req, resp)
err = DoWithRetry(httpc, req, resp)
if err != nil {
return "", err
}
@@ -302,6 +302,7 @@ func (t *Track) Fix(large bool) {
} else {
t.Artwork = strings.Replace(t.Artwork, "-large.", "-t200x200.", 1)
}
if t.ID == "" {
t.ID = strconv.FormatInt(t.IDint, 10)
} else {
@@ -309,6 +310,10 @@ func (t *Track) Fix(large bool) {
t.ID = ls[len(ls)-1]
}
if cfg.ProxyImages && t.Artwork != "" {
t.Artwork = "/proxy/images?url=" + url.QueryEscape(t.Artwork)
}
t.Author.Fix(false)
}
@@ -357,7 +362,7 @@ func GetTrackByID(id string) (Track, error) {
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(resp)
err = DoWithRetry(req, resp)
err = DoWithRetry(httpc, req, resp)
if err != nil {
return t, err
}

View File

@@ -1,6 +1,7 @@
package sc
import (
"net/url"
"strconv"
"strings"
"sync"
@@ -80,8 +81,8 @@ func SearchUsers(args string) (*Paginated[*User], error) {
return &p, nil
}
func (u User) GetTracks(args string) (*Paginated[Track], error) {
p := Paginated[Track]{
func (u User) GetTracks(args string) (*Paginated[*Track], error) {
p := Paginated[*Track]{
Next: "https://" + api + "/users/" + u.ID + "/tracks" + args,
}
@@ -126,12 +127,17 @@ func (u *User) Fix(large bool) {
} else {
u.Avatar = strings.Replace(u.Avatar, "-large.", "-t200x200.", 1)
}
if cfg.ProxyImages && u.Avatar != "" {
u.Avatar = "/proxy/images?url=" + url.QueryEscape(u.Avatar)
}
ls := strings.Split(u.ID, ":")
u.ID = ls[len(ls)-1]
}
func (u *User) GetPlaylists(args string) (*Paginated[Playlist], error) {
p := Paginated[Playlist]{
func (u *User) GetPlaylists(args string) (*Paginated[*Playlist], error) {
p := Paginated[*Playlist]{
Next: "https://" + api + "/users/" + u.ID + "/playlists_without_albums" + args,
}
@@ -147,8 +153,8 @@ func (u *User) GetPlaylists(args string) (*Paginated[Playlist], error) {
return &p, nil
}
func (u *User) GetAlbums(args string) (*Paginated[Playlist], error) {
p := Paginated[Playlist]{
func (u *User) GetAlbums(args string) (*Paginated[*Playlist], error) {
p := Paginated[*Playlist]{
Next: "https://" + api + "/users/" + u.ID + "/albums" + args,
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/valyala/fasthttp"
"github.com/maid-zone/soundcloak/lib/cfg"
proxyimages "github.com/maid-zone/soundcloak/lib/proxy_images"
"github.com/maid-zone/soundcloak/lib/sc"
"github.com/maid-zone/soundcloak/templates"
)
@@ -26,11 +27,12 @@ func main() {
EnableTrustedProxyCheck: cfg.TrustedProxyCheck,
TrustedProxies: cfg.TrustedProxies,
})
app.Use(compress.New())
app.Use(recover.New())
if cfg.EarlyData {
app.Use(earlydata.New())
}
app.Use(compress.New(compress.Config{Level: compress.LevelBestSpeed}))
app.Static("/", "assets", fiber.Static{Compress: true, MaxAge: 3600})
app.Static("/js/hls.js/", "node_modules/hls.js/dist", fiber.Static{Compress: true, MaxAge: 14400})
@@ -131,6 +133,10 @@ func main() {
return templates.TrackEmbed(track, stream).Render(context.Background(), c)
})
if cfg.ProxyImages {
proxyimages.Load(app)
}
app.Get("/:user/sets", func(c *fiber.Ctx) error {
user, err := sc.GetUser(c.Params("user"))
if err != nil {

View File

@@ -45,7 +45,7 @@ templ UserBase(u sc.User) {
</div>
}
templ User(u sc.User, p *sc.Paginated[sc.Track]) {
templ User(u sc.User, p *sc.Paginated[*sc.Track]) {
@UserBase(u)
// kinda tedious but whatever, might make it more flexible in the future
<div class="btns">
@@ -77,7 +77,7 @@ templ User(u sc.User, p *sc.Paginated[sc.Track]) {
}
}
templ UserPlaylists(u sc.User, p *sc.Paginated[sc.Playlist]) {
templ UserPlaylists(u sc.User, p *sc.Paginated[*sc.Playlist]) {
@UserBase(u)
<div class="btns">
<a class="btn" href={ templ.URL("/" + u.Permalink) }>songs</a>
@@ -108,7 +108,7 @@ templ UserPlaylists(u sc.User, p *sc.Paginated[sc.Playlist]) {
}
}
templ UserAlbums(u sc.User, p *sc.Paginated[sc.Playlist]) {
templ UserAlbums(u sc.User, p *sc.Paginated[*sc.Playlist]) {
@UserBase(u)
<div class="btns">
<a class="btn" href={ templ.URL("/" + u.Permalink) }>songs</a>