fixes?; downloading tracks with metadata (restream required)

This commit is contained in:
Laptop
2024-11-29 00:36:21 +02:00
parent 8ef6813018
commit 5f7f2fd0f4
14 changed files with 289 additions and 132 deletions

View File

@@ -189,13 +189,13 @@ Some notes:
| JSON key | Environment variable | Default value | Description | | 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. | | None | SOUNDCLOAK_CONFIG | soundcloak.json | File to load soundcloak config from. If set to`FROM_ENV`, soundcloak loads the config from environment variables. |
| 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) | | 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) | | 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. | | 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 | | 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 and also can be used for downloading songs. | | 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. | | 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 | | 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 | | UserTTL | USER_TTL | 10 minutes | Time until User profile cache expires |

2
go.mod
View File

@@ -4,6 +4,7 @@ go 1.21.3
require ( require (
github.com/a-h/templ v0.2.793 github.com/a-h/templ v0.2.793
github.com/bogem/id3v2/v2 v2.1.4
github.com/gofiber/fiber/v2 v2.52.5 github.com/gofiber/fiber/v2 v2.52.5
github.com/segmentio/encoding v0.4.0 github.com/segmentio/encoding v0.4.0
github.com/valyala/fasthttp v1.57.0 github.com/valyala/fasthttp v1.57.0
@@ -21,4 +22,5 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.27.0 // indirect golang.org/x/sys v0.27.0 // indirect
golang.org/x/text v0.19.0 // indirect
) )

28
go.sum
View File

@@ -2,6 +2,8 @@ github.com/a-h/templ v0.2.793 h1:Io+/ocnfGWYO4VHdR0zBbf39PQlnzVCVVD+wEEs6/qY=
github.com/a-h/templ v0.2.793/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w= github.com/a-h/templ v0.2.793/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/bogem/id3v2/v2 v2.1.4 h1:CEwe+lS2p6dd9UZRlPc1zbFNIha2mb2qzT1cCEoNWoI=
github.com/bogem/id3v2/v2 v2.1.4/go.mod h1:l+gR8MZ6rc9ryPTPkX77smS5Me/36gxkMgDayZ9G1vY=
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
@@ -32,7 +34,33 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -467,3 +467,7 @@ func init() {
defaultPreferences() defaultPreferences()
} }
} }
// 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 ImageCDN = "i1.sndcdn.com"

View File

@@ -11,18 +11,6 @@ import (
var sndcdn = []byte(".sndcdn.com") 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) { func Load(r fiber.Router) {
r.Get("/_/proxy/images", func(c *fiber.Ctx) error { r.Get("/_/proxy/images", func(c *fiber.Ctx) error {
url := c.Query("url") url := c.Query("url")
@@ -42,7 +30,7 @@ func Load(r fiber.Router) {
return fiber.ErrBadRequest return fiber.ErrBadRequest
} }
parsed.SetHost(cdn) parsed.SetHost(cfg.ImageCDN)
req := fasthttp.AcquireRequest() req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req) defer fasthttp.ReleaseRequest(req)
@@ -54,7 +42,7 @@ func Load(r fiber.Router) {
resp := fasthttp.AcquireResponse() resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(resp) defer fasthttp.ReleaseResponse(resp)
err = sc.DoWithRetry(httpc, req, resp) err = sc.DoWithRetry(sc.ImageClient, req, resp)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"io" "io"
"github.com/bogem/id3v2/v2"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/maid-zone/soundcloak/lib/cfg" "github.com/maid-zone/soundcloak/lib/cfg"
"github.com/maid-zone/soundcloak/lib/sc" "github.com/maid-zone/soundcloak/lib/sc"
@@ -126,9 +127,18 @@ func (r *reader) Read(buf []byte) (n int, err error) {
return return
} }
type collector struct {
data []byte
}
func (c *collector) Write(data []byte) (n int, err error) {
c.data = append(c.data, data...)
return len(data), nil
}
func Load(r fiber.Router) { func Load(r fiber.Router) {
r.Get("/_/restream/:author/:track", func(c *fiber.Ctx) error { r.Get("/_/restream/:author/:track", func(c *fiber.Ctx) error {
t, err := sc.GetTrack(stubPrefs, c.Params("author")+"/"+c.Params("track")) t, err := sc.GetTrack(c.Params("author") + "/" + c.Params("track"))
if err != nil { if err != nil {
return err return err
} }
@@ -151,6 +161,30 @@ func Load(r fiber.Router) {
return err return err
} }
if c.Query("metadata") == "true" {
tag := id3v2.NewEmptyTag()
tag.SetArtist(t.Author.Username)
if t.Genre != "" {
tag.SetGenre(t.Genre)
}
tag.SetTitle(t.Title)
if t.Artwork != "" {
data, mime, err := t.DownloadImage()
if err != nil {
return err
}
tag.AddAttachedPicture(id3v2.PictureFrame{MimeType: mime, Picture: data, PictureType: id3v2.PTFrontCover, Encoding: id3v2.EncodingUTF8})
}
var c collector
tag.WriteTo(&c)
r.leftover = c.data
}
return c.SendStream(&r) return c.SendStream(&r)
}) })
} }

View File

@@ -26,7 +26,8 @@ func GetFeaturedTracks(prefs cfg.Preferences, args string) (*Paginated[*Track],
} }
for _, t := range p.Collection { for _, t := range p.Collection {
t.Fix(prefs, false) t.Fix(false)
t.Postfix(prefs)
} }
return &p, nil return &p, nil
@@ -54,6 +55,7 @@ func GetSelections(prefs cfg.Preferences) (*Paginated[*Selection], error) {
func (s *Selection) Fix(prefs cfg.Preferences) { func (s *Selection) Fix(prefs cfg.Preferences) {
for _, p := range s.Items.Collection { for _, p := range s.Items.Collection {
p.Fix(prefs, false) p.Fix(false)
p.Postfix(prefs)
} }
} }

View File

@@ -15,12 +15,14 @@ import (
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
) )
var clientIdCache struct { type clientIdCache struct {
ClientID string ClientID string
Version []byte Version []byte
NextCheck time.Time NextCheck time.Time
} }
var ClientIDCache clientIdCache
const api = "api-v2.soundcloud.com" const api = "api-v2.soundcloud.com"
var httpc = &fasthttp.HostClient{ var httpc = &fasthttp.HostClient{
@@ -28,7 +30,15 @@ var httpc = &fasthttp.HostClient{
IsTLS: true, IsTLS: true,
DialDualStack: true, DialDualStack: true,
Dial: (&fasthttp.TCPDialer{DNSCacheDuration: cfg.DNSCacheTTL}).Dial, Dial: (&fasthttp.TCPDialer{DNSCacheDuration: cfg.DNSCacheTTL}).Dial,
MaxIdleConnDuration: 1<<63 - 1, //improves performance but seems to cause some issues, need more testing MaxIdleConnDuration: 1<<63 - 1,
}
var ImageClient = &fasthttp.HostClient{
Addr: cfg.ImageCDN + ":443",
IsTLS: true,
DialDualStack: true,
Dial: (&fasthttp.TCPDialer{DNSCacheDuration: cfg.DNSCacheTTL}).Dial,
MaxIdleConnDuration: 1<<63 - 1,
} }
var verRegex = regexp.MustCompile(`(?m)^<script>window\.__sc_version="([0-9]{10})"</script>$`) var verRegex = regexp.MustCompile(`(?m)^<script>window\.__sc_version="([0-9]{10})"</script>$`)
@@ -46,15 +56,15 @@ type cached[T any] struct {
// inspired by github.com/imputnet/cobalt (mostly stolen lol) // inspired by github.com/imputnet/cobalt (mostly stolen lol)
func GetClientID() (string, error) { func GetClientID() (string, error) {
if clientIdCache.NextCheck.After(time.Now()) { if ClientIDCache.NextCheck.After(time.Now()) {
return clientIdCache.ClientID, nil return ClientIDCache.ClientID, nil
} }
req := fasthttp.AcquireRequest() req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req) defer fasthttp.ReleaseRequest(req)
req.SetRequestURI("https://soundcloud.com/h") // 404 page req.SetRequestURI("https://soundcloud.com/h") // 404 page
req.Header.Set("User-Agent", cfg.UserAgent) // the connection is stuck with fasthttp useragent lol, maybe randomly select from a list of browser useragents in the future? low priority for now req.Header.Set("User-Agent", cfg.UserAgent)
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd") req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
resp := fasthttp.AcquireResponse() resp := fasthttp.AcquireResponse()
@@ -75,9 +85,9 @@ func GetClientID() (string, error) {
return "", ErrVersionNotFound return "", ErrVersionNotFound
} }
if bytes.Equal(res[1], clientIdCache.Version) { if bytes.Equal(res[1], ClientIDCache.Version) {
clientIdCache.NextCheck = time.Now().Add(cfg.ClientIDTTL) ClientIDCache.NextCheck = time.Now().Add(cfg.ClientIDTTL)
return clientIdCache.ClientID, nil return ClientIDCache.ClientID, nil
} }
ver := res[1] ver := res[1]
@@ -109,15 +119,16 @@ func GetClientID() (string, error) {
continue continue
} }
clientIdCache.ClientID = string(res[1]) ClientIDCache.ClientID = string(res[1])
clientIdCache.Version = ver ClientIDCache.Version = ver
clientIdCache.NextCheck = time.Now().Add(cfg.ClientIDTTL) ClientIDCache.NextCheck = time.Now().Add(cfg.ClientIDTTL)
return clientIdCache.ClientID, nil return ClientIDCache.ClientID, nil
} }
return "", ErrIDNotFound return "", ErrIDNotFound
} }
// Since the http client is setup to always keep connections idle (great for speed, no need to open a new one everytime), those connections may be closed by soundcloud after some time of inactivity, this ensures that we retry those requests that fail due to the connection closing/timing out
func DoWithRetry(httpc *fasthttp.HostClient, 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 < 10; i++ { for i := 0; i < 10; i++ {
err = httpc.Do(req, resp) err = httpc.Do(req, resp)
@@ -264,9 +275,9 @@ func init() {
for range ticker.C { for range ticker.C {
usersCacheLock.Lock() usersCacheLock.Lock()
for key, val := range usersCache { for key, val := range UsersCache {
if val.Expires.Before(time.Now()) { if val.Expires.Before(time.Now()) {
delete(usersCache, key) delete(UsersCache, key)
} }
} }
@@ -279,9 +290,9 @@ func init() {
for range ticker.C { for range ticker.C {
tracksCacheLock.Lock() tracksCacheLock.Lock()
for key, val := range tracksCache { for key, val := range TracksCache {
if val.Expires.Before(time.Now()) { if val.Expires.Before(time.Now()) {
delete(tracksCache, key) delete(TracksCache, key)
} }
} }
@@ -294,9 +305,9 @@ func init() {
for range ticker.C { for range ticker.C {
playlistsCacheLock.Lock() playlistsCacheLock.Lock()
for key, val := range playlistsCache { for key, val := range PlaylistsCache {
if val.Expires.Before(time.Now()) { if val.Expires.Before(time.Now()) {
delete(playlistsCache, key) delete(PlaylistsCache, key)
} }
} }

View File

@@ -10,7 +10,7 @@ import (
"github.com/maid-zone/soundcloak/lib/cfg" "github.com/maid-zone/soundcloak/lib/cfg"
) )
var playlistsCache = map[string]cached[Playlist]{} var PlaylistsCache = map[string]cached[Playlist]{}
var playlistsCacheLock = &sync.RWMutex{} var playlistsCacheLock = &sync.RWMutex{}
// Functions/structures related to playlists // Functions/structures related to playlists
@@ -29,15 +29,15 @@ type Playlist struct {
Type string `json:"set_type"` Type string `json:"set_type"`
Album bool `json:"is_album"` Album bool `json:"is_album"`
Author User `json:"user"` Author User `json:"user"`
Tracks []*Track `json:"tracks"` Tracks []Track `json:"tracks"`
TrackCount int64 `json:"track_count"` TrackCount int64 `json:"track_count"`
MissingTracks string `json:"-"` MissingTracks string `json:"-"`
} }
func GetPlaylist(prefs cfg.Preferences, permalink string) (Playlist, error) { func GetPlaylist(permalink string) (Playlist, error) {
playlistsCacheLock.RLock() playlistsCacheLock.RLock()
if cell, ok := playlistsCache[permalink]; ok && cell.Expires.After(time.Now()) { if cell, ok := PlaylistsCache[permalink]; ok && cell.Expires.After(time.Now()) {
playlistsCacheLock.RUnlock() playlistsCacheLock.RUnlock()
return cell.Value, nil return cell.Value, nil
} }
@@ -53,13 +53,13 @@ func GetPlaylist(prefs cfg.Preferences, permalink string) (Playlist, error) {
return p, ErrKindNotCorrect return p, ErrKindNotCorrect
} }
err = p.Fix(prefs, true) err = p.Fix(true)
if err != nil { if err != nil {
return p, err return p, err
} }
playlistsCacheLock.Lock() playlistsCacheLock.Lock()
playlistsCache[permalink] = cached[Playlist]{Value: p, Expires: time.Now().Add(cfg.PlaylistTTL)} PlaylistsCache[permalink] = cached[Playlist]{Value: p, Expires: time.Now().Add(cfg.PlaylistTTL)}
playlistsCacheLock.Unlock() playlistsCacheLock.Unlock()
return p, nil return p, nil
@@ -78,19 +78,21 @@ func SearchPlaylists(prefs cfg.Preferences, args string) (*Paginated[*Playlist],
} }
for _, p := range p.Collection { for _, p := range p.Collection {
p.Fix(prefs, false) p.Fix(false)
p.Postfix(prefs)
} }
return &p, nil return &p, nil
} }
func (p *Playlist) Fix(prefs cfg.Preferences, cached bool) error { func (p *Playlist) Fix(cached bool) error {
if cached { if cached {
for _, t := range p.Tracks { for i, t := range p.Tracks {
t.Fix(prefs, false) t.Fix(false)
p.Tracks[i] = t
} }
err := p.GetMissingTracks(prefs) err := p.GetMissingTracks()
if err != nil { if err != nil {
return err return err
} }
@@ -100,13 +102,23 @@ func (p *Playlist) Fix(prefs cfg.Preferences, cached bool) error {
p.Artwork = strings.Replace(p.Artwork, "-large.", "-t200x200.", 1) p.Artwork = strings.Replace(p.Artwork, "-large.", "-t200x200.", 1)
} }
p.Author.Fix(false)
return nil
}
func (p *Playlist) Postfix(prefs cfg.Preferences) []Track {
if cfg.ProxyImages && *prefs.ProxyImages && p.Artwork != "" { if cfg.ProxyImages && *prefs.ProxyImages && p.Artwork != "" {
p.Artwork = "/_/proxy/images?url=" + url.QueryEscape(p.Artwork) p.Artwork = "/_/proxy/images?url=" + url.QueryEscape(p.Artwork)
} }
p.Author.Fix(prefs, false) p.Author.Postfix(prefs)
var fixed = make([]Track, len(p.Tracks))
return nil for i, t := range p.Tracks {
t.Postfix(prefs)
fixed[i] = t
}
return fixed
} }
func (p Playlist) FormatDescription() string { func (p Playlist) FormatDescription() string {
@@ -141,28 +153,28 @@ func JoinMissingTracks(missing []MissingTrack) (st string) {
return return
} }
func GetMissingTracks(prefs cfg.Preferences, missing []MissingTrack) (res []*Track, next []MissingTrack, err error) { func GetMissingTracks(missing []MissingTrack) (res []Track, next []MissingTrack, err error) {
if len(missing) > 50 { if len(missing) > 50 {
next = missing[50:] next = missing[50:]
missing = missing[:50] missing = missing[:50]
} }
res, err = GetTracks(prefs, JoinMissingTracks(missing)) res, err = GetTracks(JoinMissingTracks(missing))
return return
} }
func GetNextMissingTracks(prefs cfg.Preferences, raw string) (res []*Track, next []string, err error) { func GetNextMissingTracks(raw string) (res []Track, next []string, err error) {
missing := strings.Split(raw, ",") missing := strings.Split(raw, ",")
if len(missing) > 50 { if len(missing) > 50 {
next = missing[50:] next = missing[50:]
missing = missing[:50] missing = missing[:50]
} }
res, err = GetTracks(prefs, strings.Join(missing, ",")) res, err = GetTracks(strings.Join(missing, ","))
return return
} }
func (p *Playlist) GetMissingTracks(prefs cfg.Preferences) error { func (p *Playlist) GetMissingTracks() error {
missing := []MissingTrack{} missing := []MissingTrack{}
for i, track := range p.Tracks { for i, track := range p.Tracks {
if track.Title == "" { if track.Title == "" {
@@ -174,7 +186,7 @@ func (p *Playlist) GetMissingTracks(prefs cfg.Preferences) error {
return nil return nil
} }
res, next, err := GetMissingTracks(prefs, missing) res, next, err := GetMissingTracks(missing)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -19,7 +19,7 @@ import (
var ErrIncompatibleStream = errors.New("incompatible stream") var ErrIncompatibleStream = errors.New("incompatible stream")
var ErrNoURL = errors.New("no url") var ErrNoURL = errors.New("no url")
var tracksCache = map[string]cached[Track]{} var TracksCache = map[string]cached[Track]{}
var tracksCacheLock = &sync.RWMutex{} var tracksCacheLock = &sync.RWMutex{}
type Track struct { type Track struct {
@@ -90,9 +90,9 @@ func (m Media) SelectCompatible() *Transcoding {
return nil return nil
} }
func GetTrack(prefs cfg.Preferences, permalink string) (Track, error) { func GetTrack(permalink string) (Track, error) {
tracksCacheLock.RLock() tracksCacheLock.RLock()
if cell, ok := tracksCache[permalink]; ok && cell.Expires.After(time.Now()) { if cell, ok := TracksCache[permalink]; ok && cell.Expires.After(time.Now()) {
tracksCacheLock.RUnlock() tracksCacheLock.RUnlock()
return cell.Value, nil return cell.Value, nil
} }
@@ -108,10 +108,10 @@ func GetTrack(prefs cfg.Preferences, permalink string) (Track, error) {
return t, ErrKindNotCorrect return t, ErrKindNotCorrect
} }
t.Fix(prefs, true) t.Fix(true)
tracksCacheLock.Lock() tracksCacheLock.Lock()
tracksCache[permalink] = cached[Track]{Value: t, Expires: time.Now().Add(cfg.TrackTTL)} TracksCache[permalink] = cached[Track]{Value: t, Expires: time.Now().Add(cfg.TrackTTL)}
tracksCacheLock.Unlock() tracksCacheLock.Unlock()
return t, nil return t, nil
@@ -125,12 +125,12 @@ func GetTrack(prefs cfg.Preferences, permalink string) (Track, error) {
// plain permalink/id: // plain permalink/id:
// - <user>/<track> // - <user>/<track>
// - <id> // - <id>
func GetArbitraryTrack(prefs cfg.Preferences, data string) (Track, error) { func GetArbitraryTrack(data string) (Track, error) {
if len(data) > 8 && (data[:8] == "https://" || data[:7] == "http://") { if len(data) > 8 && (data[:8] == "https://" || data[:7] == "http://") {
u, err := url.Parse(data) u, err := url.Parse(data)
if err == nil { if err == nil {
if (u.Host == "api.soundcloud.com" || u.Host == "api-v2.soundcloud.com") && len(u.Path) > 8 && u.Path[:8] == "/tracks/" { if (u.Host == "api.soundcloud.com" || u.Host == "api-v2.soundcloud.com") && len(u.Path) > 8 && u.Path[:8] == "/tracks/" {
return GetTrackByID(prefs, u.Path[8:]) return GetTrackByID(u.Path[8:])
} }
if u.Host == "soundcloud.com" { if u.Host == "soundcloud.com" {
@@ -158,7 +158,7 @@ func GetArbitraryTrack(prefs cfg.Preferences, data string) (Track, error) {
return Track{}, ErrKindNotCorrect return Track{}, ErrKindNotCorrect
} }
return GetTrack(prefs, u.Path) return GetTrack(u.Path)
} }
} else { } else {
return Track{}, err return Track{}, err
@@ -174,7 +174,7 @@ func GetArbitraryTrack(prefs cfg.Preferences, data string) (Track, error) {
} }
if valid { if valid {
return GetTrackByID(prefs, data) return GetTrackByID(data)
} }
// this part should be at the end since it manipulates data // this part should be at the end since it manipulates data
@@ -197,7 +197,7 @@ func GetArbitraryTrack(prefs cfg.Preferences, data string) (Track, error) {
} }
if n == 1 { if n == 1 {
return GetTrack(prefs, data) return GetTrack(data)
} }
// failed to find a data point // failed to find a data point
@@ -217,13 +217,14 @@ func SearchTracks(prefs cfg.Preferences, args string) (*Paginated[*Track], error
} }
for _, t := range p.Collection { for _, t := range p.Collection {
t.Fix(prefs, false) t.Fix(false)
t.Postfix(prefs)
} }
return &p, nil return &p, nil
} }
func GetTracks(prefs cfg.Preferences, ids string) ([]*Track, error) { func GetTracks(ids string) ([]Track, error) {
cid, err := GetClientID() cid, err := GetClientID()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -249,10 +250,11 @@ func GetTracks(prefs cfg.Preferences, ids string) ([]*Track, error) {
data = resp.Body() data = resp.Body()
} }
var res []*Track var res []Track
err = json.Unmarshal(data, &res) err = json.Unmarshal(data, &res)
for _, t := range res { for i, t := range res {
t.Fix(prefs, false) t.Fix(false)
res[i] = t
} }
return res, err return res, err
} }
@@ -304,7 +306,7 @@ func (tr Transcoding) GetStream(prefs cfg.Preferences, authorization string) (st
return s.URL, nil return s.URL, nil
} }
func (t *Track) Fix(prefs cfg.Preferences, large bool) { func (t *Track) Fix(large bool) {
if large { if large {
t.Artwork = strings.Replace(t.Artwork, "-large.", "-t500x500.", 1) t.Artwork = strings.Replace(t.Artwork, "-large.", "-t500x500.", 1)
} else { } else {
@@ -318,11 +320,14 @@ func (t *Track) Fix(prefs cfg.Preferences, large bool) {
t.ID = ls[len(ls)-1] t.ID = ls[len(ls)-1]
} }
t.Author.Fix(false)
}
func (t *Track) Postfix(prefs cfg.Preferences) {
if cfg.ProxyImages && *prefs.ProxyImages && t.Artwork != "" { if cfg.ProxyImages && *prefs.ProxyImages && t.Artwork != "" {
t.Artwork = "/_/proxy/images?url=" + url.QueryEscape(t.Artwork) t.Artwork = "/_/proxy/images?url=" + url.QueryEscape(t.Artwork)
} }
t.Author.Postfix(prefs)
t.Author.Fix(prefs, false)
} }
func (t Track) FormatDescription() string { func (t Track) FormatDescription() string {
@@ -344,14 +349,14 @@ func (t Track) FormatDescription() string {
return desc return desc
} }
func GetTrackByID(prefs cfg.Preferences, id string) (Track, error) { func GetTrackByID(id string) (Track, error) {
cid, err := GetClientID() cid, err := GetClientID()
if err != nil { if err != nil {
return Track{}, err return Track{}, err
} }
tracksCacheLock.RLock() tracksCacheLock.RLock()
for _, cell := range tracksCache { for _, cell := range TracksCache {
if cell.Value.ID == id && cell.Expires.After(time.Now()) { if cell.Value.ID == id && cell.Expires.After(time.Now()) {
tracksCacheLock.RUnlock() tracksCacheLock.RUnlock()
return cell.Value, nil return cell.Value, nil
@@ -389,11 +394,37 @@ func GetTrackByID(prefs cfg.Preferences, id string) (Track, error) {
return t, ErrKindNotCorrect return t, ErrKindNotCorrect
} }
t.Fix(prefs, true) t.Fix(true)
tracksCacheLock.Lock() tracksCacheLock.Lock()
tracksCache[t.Author.Permalink+"/"+t.Permalink] = cached[Track]{Value: t, Expires: time.Now().Add(cfg.TrackTTL)} TracksCache[t.Author.Permalink+"/"+t.Permalink] = cached[Track]{Value: t, Expires: time.Now().Add(cfg.TrackTTL)}
tracksCacheLock.Unlock() tracksCacheLock.Unlock()
return t, nil return t, nil
} }
func (t Track) DownloadImage() ([]byte, string, error) {
req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req)
req.SetRequestURI(t.Artwork)
req.Header.Set("User-Agent", cfg.UserAgent)
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(resp)
err := DoWithRetry(ImageClient, req, resp)
if err != nil {
fmt.Println(t.Artwork)
fmt.Println("hi", err)
return nil, "", err
}
data, err := resp.BodyUncompressed()
if err != nil {
data = resp.Body()
}
return data, string(resp.Header.Peek("Content-Type")), nil
}

View File

@@ -12,7 +12,7 @@ import (
// Functions/structures related to users // Functions/structures related to users
var usersCache = map[string]cached[User]{} var UsersCache = map[string]cached[User]{}
var usersCacheLock = &sync.RWMutex{} var usersCacheLock = &sync.RWMutex{}
type User struct { type User struct {
@@ -52,20 +52,22 @@ func (r *Repost) Fix(prefs cfg.Preferences) {
switch r.Type { switch r.Type {
case TrackRepost: case TrackRepost:
if r.Track != nil { if r.Track != nil {
r.Track.Fix(prefs, false) r.Track.Fix(false)
r.Track.Postfix(prefs)
} }
return return
case PlaylistRepost: case PlaylistRepost:
if r.Playlist != nil { if r.Playlist != nil {
r.Playlist.Fix(prefs, false) // err always nil if cached == false r.Playlist.Fix(false) // err always nil if cached == false
r.Playlist.Postfix(prefs)
} }
return return
} }
} }
func GetUser(prefs cfg.Preferences, permalink string) (User, error) { func GetUser(permalink string) (User, error) {
usersCacheLock.RLock() usersCacheLock.RLock()
if cell, ok := usersCache[permalink]; ok && cell.Expires.After(time.Now()) { if cell, ok := UsersCache[permalink]; ok && cell.Expires.After(time.Now()) {
usersCacheLock.RUnlock() usersCacheLock.RUnlock()
return cell.Value, nil return cell.Value, nil
} }
@@ -83,10 +85,10 @@ func GetUser(prefs cfg.Preferences, permalink string) (User, error) {
return u, err return u, err
} }
u.Fix(prefs, true) u.Fix(true)
usersCacheLock.Lock() usersCacheLock.Lock()
usersCache[permalink] = cached[User]{Value: u, Expires: time.Now().Add(cfg.UserTTL)} UsersCache[permalink] = cached[User]{Value: u, Expires: time.Now().Add(cfg.UserTTL)}
usersCacheLock.Unlock() usersCacheLock.Unlock()
return u, err return u, err
@@ -105,7 +107,8 @@ func SearchUsers(prefs cfg.Preferences, args string) (*Paginated[*User], error)
} }
for _, u := range p.Collection { for _, u := range p.Collection {
u.Fix(prefs, false) u.Fix(false)
u.Postfix(prefs)
} }
return &p, nil return &p, nil
@@ -121,8 +124,9 @@ func (u User) GetTracks(prefs cfg.Preferences, args string) (*Paginated[*Track],
return nil, err return nil, err
} }
for _, u := range p.Collection { for _, t := range p.Collection {
u.Fix(prefs, false) t.Fix(false)
t.Postfix(prefs)
} }
return &p, nil return &p, nil
@@ -151,7 +155,7 @@ func (u User) FormatUsername() string {
return res return res
} }
func (u *User) Fix(prefs cfg.Preferences, large bool) { func (u *User) Fix(large bool) {
if large { if large {
u.Avatar = strings.Replace(u.Avatar, "-large.", "-t500x500.", 1) u.Avatar = strings.Replace(u.Avatar, "-large.", "-t500x500.", 1)
} else { } else {
@@ -163,12 +167,14 @@ func (u *User) Fix(prefs cfg.Preferences, large bool) {
u.Avatar = "" u.Avatar = ""
} }
ls := strings.Split(u.ID, ":")
u.ID = ls[len(ls)-1]
}
func (u *User) Postfix(prefs cfg.Preferences) {
if cfg.ProxyImages && *prefs.ProxyImages && u.Avatar != "" { if cfg.ProxyImages && *prefs.ProxyImages && u.Avatar != "" {
u.Avatar = "/_/proxy/images?url=" + url.QueryEscape(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(prefs cfg.Preferences, args string) (*Paginated[*Playlist], error) { func (u *User) GetPlaylists(prefs cfg.Preferences, args string) (*Paginated[*Playlist], error) {
@@ -182,7 +188,8 @@ func (u *User) GetPlaylists(prefs cfg.Preferences, args string) (*Paginated[*Pla
} }
for _, pl := range p.Collection { for _, pl := range p.Collection {
pl.Fix(prefs, false) pl.Fix(false)
pl.Postfix(prefs)
} }
return &p, nil return &p, nil
@@ -199,7 +206,8 @@ func (u *User) GetAlbums(prefs cfg.Preferences, args string) (*Paginated[*Playli
} }
for _, pl := range p.Collection { for _, pl := range p.Collection {
pl.Fix(prefs, false) pl.Fix(false)
pl.Postfix(prefs)
} }
return &p, nil return &p, nil

52
main.go
View File

@@ -37,6 +37,26 @@ func main() {
app.Static("/", "assets", fiber.Static{Compress: true, MaxAge: 3600}) // 1hour app.Static("/", "assets", fiber.Static{Compress: true, MaxAge: 3600}) // 1hour
app.Static("/js/hls.js/", "node_modules/hls.js/dist", fiber.Static{Compress: true, MaxAge: 28800}) // 8 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.
const debug = false
if debug {
app.Get("/_/cachedump/tracks", func(c *fiber.Ctx) error {
return c.JSON(sc.TracksCache)
})
app.Get("/_/cachedump/playlists", func(c *fiber.Ctx) error {
return c.JSON(sc.PlaylistsCache)
})
app.Get("/_/cachedump/users", func(c *fiber.Ctx) error {
return c.JSON(sc.UsersCache)
})
app.Get("/_/cachedump/clientId", func(c *fiber.Ctx) error {
return c.JSON(sc.ClientIDCache)
})
}
app.Get("/search", func(c *fiber.Ctx) error { app.Get("/search", func(c *fiber.Ctx) error {
prefs, err := preferences.Get(c) prefs, err := preferences.Get(c)
if err != nil { if err != nil {
@@ -127,12 +147,13 @@ func main() {
return err return err
} }
track, err := sc.GetArbitraryTrack(prefs, u) track, err := sc.GetArbitraryTrack(u)
if err != nil { if err != nil {
log.Printf("error getting %s: %s\n", u, err) log.Printf("error getting %s: %s\n", u, err)
return err return err
} }
track.Postfix(prefs)
displayErr := "" displayErr := ""
stream := "" stream := ""
@@ -226,11 +247,12 @@ func main() {
return err return err
} }
user, err := sc.GetUser(prefs, c.Params("user")) user, err := sc.GetUser(c.Params("user"))
if err != nil { if err != nil {
log.Printf("error getting %s (playlists): %s\n", c.Params("user"), err) log.Printf("error getting %s (playlists): %s\n", c.Params("user"), err)
return err return err
} }
user.Postfix(prefs)
pl, err := user.GetPlaylists(prefs, c.Query("pagination", "?limit=20")) pl, err := user.GetPlaylists(prefs, c.Query("pagination", "?limit=20"))
if err != nil { if err != nil {
@@ -248,11 +270,12 @@ func main() {
return err return err
} }
user, err := sc.GetUser(prefs, c.Params("user")) user, err := sc.GetUser(c.Params("user"))
if err != nil { if err != nil {
log.Printf("error getting %s (albums): %s\n", c.Params("user"), err) log.Printf("error getting %s (albums): %s\n", c.Params("user"), err)
return err return err
} }
user.Postfix(prefs)
pl, err := user.GetAlbums(prefs, c.Query("pagination", "?limit=20")) pl, err := user.GetAlbums(prefs, c.Query("pagination", "?limit=20"))
if err != nil { if err != nil {
@@ -270,11 +293,12 @@ func main() {
return err return err
} }
user, err := sc.GetUser(prefs, c.Params("user")) user, err := sc.GetUser(c.Params("user"))
if err != nil { if err != nil {
log.Printf("error getting %s (reposts): %s\n", c.Params("user"), err) log.Printf("error getting %s (reposts): %s\n", c.Params("user"), err)
return err return err
} }
user.Postfix(prefs)
p, err := user.GetReposts(prefs, c.Query("pagination", "?limit=20")) p, err := user.GetReposts(prefs, c.Query("pagination", "?limit=20"))
if err != nil { if err != nil {
@@ -292,11 +316,13 @@ func main() {
return err return err
} }
track, err := sc.GetTrack(prefs, c.Params("user")+"/"+c.Params("track")) track, err := sc.GetTrack(c.Params("user") + "/" + c.Params("track"))
if err != nil { if err != nil {
log.Printf("error getting %s from %s: %s\n", c.Params("track"), c.Params("user"), err) log.Printf("error getting %s from %s: %s\n", c.Params("track"), c.Params("user"), err)
return err return err
} }
track.Postfix(prefs)
displayErr := "" displayErr := ""
stream := "" stream := ""
@@ -327,11 +353,12 @@ func main() {
} }
//h := time.Now() //h := time.Now()
usr, err := sc.GetUser(prefs, c.Params("user")) usr, err := sc.GetUser(c.Params("user"))
if err != nil { if err != nil {
log.Printf("error getting %s: %s\n", c.Params("user"), err) log.Printf("error getting %s: %s\n", c.Params("user"), err)
return err return err
} }
usr.Postfix(prefs)
//fmt.Println("getuser", time.Since(h)) //fmt.Println("getuser", time.Since(h))
//h = time.Now() //h = time.Now()
@@ -352,20 +379,27 @@ func main() {
return err return err
} }
playlist, err := sc.GetPlaylist(prefs, c.Params("user")+"/sets/"+c.Params("playlist")) playlist, err := sc.GetPlaylist(c.Params("user") + "/sets/" + c.Params("playlist"))
if err != nil { if err != nil {
log.Printf("error getting %s playlist from %s: %s\n", c.Params("playlist"), c.Params("user"), err) log.Printf("error getting %s playlist from %s: %s\n", c.Params("playlist"), c.Params("user"), err)
return err return err
} }
// Don't ask why
playlist.Tracks = playlist.Postfix(prefs)
p := c.Query("pagination") p := c.Query("pagination")
if p != "" { if p != "" {
tracks, next, err := sc.GetNextMissingTracks(prefs, p) tracks, next, err := sc.GetNextMissingTracks(p)
if err != nil { if err != nil {
log.Printf("error getting %s playlist tracks from %s: %s\n", c.Params("playlist"), c.Params("user"), err) log.Printf("error getting %s playlist tracks from %s: %s\n", c.Params("playlist"), c.Params("user"), err)
return err return err
} }
for i, track := range tracks {
track.Postfix(prefs)
tracks[i] = track
}
playlist.Tracks = tracks playlist.Tracks = tracks
playlist.MissingTracks = strings.Join(next, ",") playlist.MissingTracks = strings.Join(next, ",")
} }

View File

@@ -51,7 +51,7 @@ templ Playlist(prefs cfg.Preferences, p sc.Playlist) {
<br/> <br/>
<div> <div>
for _, track := range p.Tracks { for _, track := range p.Tracks {
@TrackItem(track, true) @TrackItem(&track, true)
} }
</div> </div>
if len(p.MissingTracks) != 0 { if len(p.MissingTracks) != 0 {

View File

@@ -82,8 +82,11 @@ templ Track(prefs cfg.Preferences, t sc.Track, stream string, displayErr string)
} }
@UserItem(&t.Author) @UserItem(&t.Author)
//<div class="btns"> //<div class="btns">
<div style="display: flex;"> <div style="display: flex; gap: 1rem">
<a class="btn" href={ templ.URL("https://soundcloud.com/" + t.Author.Permalink + "/" + t.Permalink) }>view on soundcloud</a> <a class="btn" href={ templ.URL("https://soundcloud.com/" + t.Author.Permalink + "/" + t.Permalink) }>view on soundcloud</a>
if cfg.Restream {
<a class="btn" href={ templ.URL("/_/restream/" + t.Author.Permalink + "/" + t.Permalink+"?metadata=true") } download={t.Author.Username + " - " + t.Title + ".mp3"}>download</a>
}
</div> </div>
<br/> <br/>
if t.Description != "" { if t.Description != "" {