mirror of
https://git.maid.zone/stuff/soundcloak.git
synced 2025-12-10 13:49:39 +05:00
fixes?; downloading tracks with metadata (restream required)
This commit is contained in:
44
README.md
44
README.md
@@ -188,28 +188,28 @@ Some notes:
|
|||||||
- When specifying time, specify it in seconds.
|
- When specifying time, specify it in seconds.
|
||||||
|
|
||||||
|
|
||||||
| 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 |
|
||||||
| UserCacheCleanDelay | USER_CACHE_CLEAN_DELAY | 2.5 minutes | Time between each cleanup of the cache (to remove expired users) |
|
| 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 |
|
| 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) |
|
| 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 |
|
| 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) |
|
| 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 |
|
| 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 |
|
| DNSCacheTTL | DNS_CACHE_TTL | 10 minutes | Time until DNS cache expires |
|
||||||
| Addr | ADDR | :4664 | Address and port for soundcloak to listen on |
|
| 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. |
|
| 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. |
|
| 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 |
|
| TrustedProxies | TRUSTED_PROXIES | [] | List of IPs or IP ranges of trusted proxies |
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -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
28
go.sum
@@ -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=
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -24,20 +24,20 @@ type Playlist struct {
|
|||||||
Likes int64 `json:"likes_count"`
|
Likes int64 `json:"likes_count"`
|
||||||
Permalink string `json:"permalink"`
|
Permalink string `json:"permalink"`
|
||||||
//ReleaseDate string `json:"release_date"`
|
//ReleaseDate string `json:"release_date"`
|
||||||
TagList string `json:"tag_list"`
|
TagList string `json:"tag_list"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
52
main.go
@@ -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, ",")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 != "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user