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 |
| :------------------------ | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| :------------------------ | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 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) |
| 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. |
| 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. |
| 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 |

2
go.mod
View File

@@ -4,6 +4,7 @@ go 1.21.3
require (
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/segmentio/encoding v0.4.0
github.com/valyala/fasthttp v1.57.0
@@ -21,4 +22,5 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/tcplisten v1.0.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/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
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/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
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/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
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.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/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()
}
}
// 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")
// seems soundcloud has 4 of these (i1, i2, i3, i4)
// they point to the same ip from my observations, and they all serve the same files
const cdn = "i1.sndcdn.com"
var httpc = &fasthttp.HostClient{
Addr: cdn + ":443",
IsTLS: true,
DialDualStack: true,
Dial: (&fasthttp.TCPDialer{DNSCacheDuration: cfg.DNSCacheTTL}).Dial,
MaxIdleConnDuration: 1<<63 - 1,
}
func Load(r fiber.Router) {
r.Get("/_/proxy/images", func(c *fiber.Ctx) error {
url := c.Query("url")
@@ -42,7 +30,7 @@ func Load(r fiber.Router) {
return fiber.ErrBadRequest
}
parsed.SetHost(cdn)
parsed.SetHost(cfg.ImageCDN)
req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req)
@@ -54,7 +42,7 @@ func Load(r fiber.Router) {
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(resp)
err = sc.DoWithRetry(httpc, req, resp)
err = sc.DoWithRetry(sc.ImageClient, req, resp)
if err != nil {
return err
}

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"io"
"github.com/bogem/id3v2/v2"
"github.com/gofiber/fiber/v2"
"github.com/maid-zone/soundcloak/lib/cfg"
"github.com/maid-zone/soundcloak/lib/sc"
@@ -126,9 +127,18 @@ func (r *reader) Read(buf []byte) (n int, err error) {
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) {
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 {
return err
}
@@ -151,6 +161,30 @@ func Load(r fiber.Router) {
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)
})
}

View File

@@ -26,7 +26,8 @@ func GetFeaturedTracks(prefs cfg.Preferences, args string) (*Paginated[*Track],
}
for _, t := range p.Collection {
t.Fix(prefs, false)
t.Fix(false)
t.Postfix(prefs)
}
return &p, nil
@@ -54,6 +55,7 @@ func GetSelections(prefs cfg.Preferences) (*Paginated[*Selection], error) {
func (s *Selection) Fix(prefs cfg.Preferences) {
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"
)
var clientIdCache struct {
type clientIdCache struct {
ClientID string
Version []byte
NextCheck time.Time
}
var ClientIDCache clientIdCache
const api = "api-v2.soundcloud.com"
var httpc = &fasthttp.HostClient{
@@ -28,7 +30,15 @@ var httpc = &fasthttp.HostClient{
IsTLS: true,
DialDualStack: true,
Dial: (&fasthttp.TCPDialer{DNSCacheDuration: cfg.DNSCacheTTL}).Dial,
MaxIdleConnDuration: 1<<63 - 1, //improves performance but seems to cause some issues, need more testing
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>$`)
@@ -46,15 +56,15 @@ type cached[T any] struct {
// inspired by github.com/imputnet/cobalt (mostly stolen lol)
func GetClientID() (string, error) {
if clientIdCache.NextCheck.After(time.Now()) {
return clientIdCache.ClientID, nil
if ClientIDCache.NextCheck.After(time.Now()) {
return ClientIDCache.ClientID, nil
}
req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req)
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")
resp := fasthttp.AcquireResponse()
@@ -75,9 +85,9 @@ func GetClientID() (string, error) {
return "", ErrVersionNotFound
}
if bytes.Equal(res[1], clientIdCache.Version) {
clientIdCache.NextCheck = time.Now().Add(cfg.ClientIDTTL)
return clientIdCache.ClientID, nil
if bytes.Equal(res[1], ClientIDCache.Version) {
ClientIDCache.NextCheck = time.Now().Add(cfg.ClientIDTTL)
return ClientIDCache.ClientID, nil
}
ver := res[1]
@@ -109,15 +119,16 @@ func GetClientID() (string, error) {
continue
}
clientIdCache.ClientID = string(res[1])
clientIdCache.Version = ver
clientIdCache.NextCheck = time.Now().Add(cfg.ClientIDTTL)
return clientIdCache.ClientID, nil
ClientIDCache.ClientID = string(res[1])
ClientIDCache.Version = ver
ClientIDCache.NextCheck = time.Now().Add(cfg.ClientIDTTL)
return ClientIDCache.ClientID, nil
}
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) {
for i := 0; i < 10; i++ {
err = httpc.Do(req, resp)
@@ -264,9 +275,9 @@ func init() {
for range ticker.C {
usersCacheLock.Lock()
for key, val := range usersCache {
for key, val := range UsersCache {
if val.Expires.Before(time.Now()) {
delete(usersCache, key)
delete(UsersCache, key)
}
}
@@ -279,9 +290,9 @@ func init() {
for range ticker.C {
tracksCacheLock.Lock()
for key, val := range tracksCache {
for key, val := range TracksCache {
if val.Expires.Before(time.Now()) {
delete(tracksCache, key)
delete(TracksCache, key)
}
}
@@ -294,9 +305,9 @@ func init() {
for range ticker.C {
playlistsCacheLock.Lock()
for key, val := range playlistsCache {
for key, val := range PlaylistsCache {
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"
)
var playlistsCache = map[string]cached[Playlist]{}
var PlaylistsCache = map[string]cached[Playlist]{}
var playlistsCacheLock = &sync.RWMutex{}
// Functions/structures related to playlists
@@ -29,15 +29,15 @@ type Playlist struct {
Type string `json:"set_type"`
Album bool `json:"is_album"`
Author User `json:"user"`
Tracks []*Track `json:"tracks"`
Tracks []Track `json:"tracks"`
TrackCount int64 `json:"track_count"`
MissingTracks string `json:"-"`
}
func GetPlaylist(prefs cfg.Preferences, permalink string) (Playlist, error) {
func GetPlaylist(permalink string) (Playlist, error) {
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()
return cell.Value, nil
}
@@ -53,13 +53,13 @@ func GetPlaylist(prefs cfg.Preferences, permalink string) (Playlist, error) {
return p, ErrKindNotCorrect
}
err = p.Fix(prefs, true)
err = p.Fix(true)
if err != nil {
return p, err
}
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()
return p, nil
@@ -78,19 +78,21 @@ func SearchPlaylists(prefs cfg.Preferences, args string) (*Paginated[*Playlist],
}
for _, p := range p.Collection {
p.Fix(prefs, false)
p.Fix(false)
p.Postfix(prefs)
}
return &p, nil
}
func (p *Playlist) Fix(prefs cfg.Preferences, cached bool) error {
func (p *Playlist) Fix(cached bool) error {
if cached {
for _, t := range p.Tracks {
t.Fix(prefs, false)
for i, t := range p.Tracks {
t.Fix(false)
p.Tracks[i] = t
}
err := p.GetMissingTracks(prefs)
err := p.GetMissingTracks()
if err != nil {
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.Author.Fix(false)
return nil
}
func (p *Playlist) Postfix(prefs cfg.Preferences) []Track {
if cfg.ProxyImages && *prefs.ProxyImages && p.Artwork != "" {
p.Artwork = "/_/proxy/images?url=" + url.QueryEscape(p.Artwork)
}
p.Author.Fix(prefs, false)
return nil
p.Author.Postfix(prefs)
var fixed = make([]Track, len(p.Tracks))
for i, t := range p.Tracks {
t.Postfix(prefs)
fixed[i] = t
}
return fixed
}
func (p Playlist) FormatDescription() string {
@@ -141,28 +153,28 @@ func JoinMissingTracks(missing []MissingTrack) (st string) {
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 {
next = missing[50:]
missing = missing[:50]
}
res, err = GetTracks(prefs, JoinMissingTracks(missing))
res, err = GetTracks(JoinMissingTracks(missing))
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, ",")
if len(missing) > 50 {
next = missing[50:]
missing = missing[:50]
}
res, err = GetTracks(prefs, strings.Join(missing, ","))
res, err = GetTracks(strings.Join(missing, ","))
return
}
func (p *Playlist) GetMissingTracks(prefs cfg.Preferences) error {
func (p *Playlist) GetMissingTracks() error {
missing := []MissingTrack{}
for i, track := range p.Tracks {
if track.Title == "" {
@@ -174,7 +186,7 @@ func (p *Playlist) GetMissingTracks(prefs cfg.Preferences) error {
return nil
}
res, next, err := GetMissingTracks(prefs, missing)
res, next, err := GetMissingTracks(missing)
if err != nil {
return err
}

View File

@@ -19,7 +19,7 @@ import (
var ErrIncompatibleStream = errors.New("incompatible stream")
var ErrNoURL = errors.New("no url")
var tracksCache = map[string]cached[Track]{}
var TracksCache = map[string]cached[Track]{}
var tracksCacheLock = &sync.RWMutex{}
type Track struct {
@@ -90,9 +90,9 @@ func (m Media) SelectCompatible() *Transcoding {
return nil
}
func GetTrack(prefs cfg.Preferences, permalink string) (Track, error) {
func GetTrack(permalink string) (Track, error) {
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()
return cell.Value, nil
}
@@ -108,10 +108,10 @@ func GetTrack(prefs cfg.Preferences, permalink string) (Track, error) {
return t, ErrKindNotCorrect
}
t.Fix(prefs, true)
t.Fix(true)
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()
return t, nil
@@ -125,12 +125,12 @@ func GetTrack(prefs cfg.Preferences, permalink string) (Track, error) {
// plain permalink/id:
// - <user>/<track>
// - <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://") {
u, err := url.Parse(data)
if err == nil {
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" {
@@ -158,7 +158,7 @@ func GetArbitraryTrack(prefs cfg.Preferences, data string) (Track, error) {
return Track{}, ErrKindNotCorrect
}
return GetTrack(prefs, u.Path)
return GetTrack(u.Path)
}
} else {
return Track{}, err
@@ -174,7 +174,7 @@ func GetArbitraryTrack(prefs cfg.Preferences, data string) (Track, error) {
}
if valid {
return GetTrackByID(prefs, data)
return GetTrackByID(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 {
return GetTrack(prefs, data)
return GetTrack(data)
}
// 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 {
t.Fix(prefs, false)
t.Fix(false)
t.Postfix(prefs)
}
return &p, nil
}
func GetTracks(prefs cfg.Preferences, ids string) ([]*Track, error) {
func GetTracks(ids string) ([]Track, error) {
cid, err := GetClientID()
if err != nil {
return nil, err
@@ -249,10 +250,11 @@ func GetTracks(prefs cfg.Preferences, ids string) ([]*Track, error) {
data = resp.Body()
}
var res []*Track
var res []Track
err = json.Unmarshal(data, &res)
for _, t := range res {
t.Fix(prefs, false)
for i, t := range res {
t.Fix(false)
res[i] = t
}
return res, err
}
@@ -304,7 +306,7 @@ func (tr Transcoding) GetStream(prefs cfg.Preferences, authorization string) (st
return s.URL, nil
}
func (t *Track) Fix(prefs cfg.Preferences, large bool) {
func (t *Track) Fix(large bool) {
if large {
t.Artwork = strings.Replace(t.Artwork, "-large.", "-t500x500.", 1)
} else {
@@ -318,11 +320,14 @@ func (t *Track) Fix(prefs cfg.Preferences, large bool) {
t.ID = ls[len(ls)-1]
}
t.Author.Fix(false)
}
func (t *Track) Postfix(prefs cfg.Preferences) {
if cfg.ProxyImages && *prefs.ProxyImages && t.Artwork != "" {
t.Artwork = "/_/proxy/images?url=" + url.QueryEscape(t.Artwork)
}
t.Author.Fix(prefs, false)
t.Author.Postfix(prefs)
}
func (t Track) FormatDescription() string {
@@ -344,14 +349,14 @@ func (t Track) FormatDescription() string {
return desc
}
func GetTrackByID(prefs cfg.Preferences, id string) (Track, error) {
func GetTrackByID(id string) (Track, error) {
cid, err := GetClientID()
if err != nil {
return Track{}, err
}
tracksCacheLock.RLock()
for _, cell := range tracksCache {
for _, cell := range TracksCache {
if cell.Value.ID == id && cell.Expires.After(time.Now()) {
tracksCacheLock.RUnlock()
return cell.Value, nil
@@ -389,11 +394,37 @@ func GetTrackByID(prefs cfg.Preferences, id string) (Track, error) {
return t, ErrKindNotCorrect
}
t.Fix(prefs, true)
t.Fix(true)
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()
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
var usersCache = map[string]cached[User]{}
var UsersCache = map[string]cached[User]{}
var usersCacheLock = &sync.RWMutex{}
type User struct {
@@ -52,20 +52,22 @@ func (r *Repost) Fix(prefs cfg.Preferences) {
switch r.Type {
case TrackRepost:
if r.Track != nil {
r.Track.Fix(prefs, false)
r.Track.Fix(false)
r.Track.Postfix(prefs)
}
return
case PlaylistRepost:
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
}
}
func GetUser(prefs cfg.Preferences, permalink string) (User, error) {
func GetUser(permalink string) (User, error) {
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()
return cell.Value, nil
}
@@ -83,10 +85,10 @@ func GetUser(prefs cfg.Preferences, permalink string) (User, error) {
return u, err
}
u.Fix(prefs, true)
u.Fix(true)
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()
return u, err
@@ -105,7 +107,8 @@ func SearchUsers(prefs cfg.Preferences, args string) (*Paginated[*User], error)
}
for _, u := range p.Collection {
u.Fix(prefs, false)
u.Fix(false)
u.Postfix(prefs)
}
return &p, nil
@@ -121,8 +124,9 @@ func (u User) GetTracks(prefs cfg.Preferences, args string) (*Paginated[*Track],
return nil, err
}
for _, u := range p.Collection {
u.Fix(prefs, false)
for _, t := range p.Collection {
t.Fix(false)
t.Postfix(prefs)
}
return &p, nil
@@ -151,7 +155,7 @@ func (u User) FormatUsername() string {
return res
}
func (u *User) Fix(prefs cfg.Preferences, large bool) {
func (u *User) Fix(large bool) {
if large {
u.Avatar = strings.Replace(u.Avatar, "-large.", "-t500x500.", 1)
} else {
@@ -163,12 +167,14 @@ func (u *User) Fix(prefs cfg.Preferences, large bool) {
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 != "" {
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) {
@@ -182,7 +188,8 @@ func (u *User) GetPlaylists(prefs cfg.Preferences, args string) (*Paginated[*Pla
}
for _, pl := range p.Collection {
pl.Fix(prefs, false)
pl.Fix(false)
pl.Postfix(prefs)
}
return &p, nil
@@ -199,7 +206,8 @@ func (u *User) GetAlbums(prefs cfg.Preferences, args string) (*Paginated[*Playli
}
for _, pl := range p.Collection {
pl.Fix(prefs, false)
pl.Fix(false)
pl.Postfix(prefs)
}
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("/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 {
prefs, err := preferences.Get(c)
if err != nil {
@@ -127,12 +147,13 @@ func main() {
return err
}
track, err := sc.GetArbitraryTrack(prefs, u)
track, err := sc.GetArbitraryTrack(u)
if err != nil {
log.Printf("error getting %s: %s\n", u, err)
return err
}
track.Postfix(prefs)
displayErr := ""
stream := ""
@@ -226,11 +247,12 @@ func main() {
return err
}
user, err := sc.GetUser(prefs, c.Params("user"))
user, err := sc.GetUser(c.Params("user"))
if err != nil {
log.Printf("error getting %s (playlists): %s\n", c.Params("user"), err)
return err
}
user.Postfix(prefs)
pl, err := user.GetPlaylists(prefs, c.Query("pagination", "?limit=20"))
if err != nil {
@@ -248,11 +270,12 @@ func main() {
return err
}
user, err := sc.GetUser(prefs, c.Params("user"))
user, err := sc.GetUser(c.Params("user"))
if err != nil {
log.Printf("error getting %s (albums): %s\n", c.Params("user"), err)
return err
}
user.Postfix(prefs)
pl, err := user.GetAlbums(prefs, c.Query("pagination", "?limit=20"))
if err != nil {
@@ -270,11 +293,12 @@ func main() {
return err
}
user, err := sc.GetUser(prefs, c.Params("user"))
user, err := sc.GetUser(c.Params("user"))
if err != nil {
log.Printf("error getting %s (reposts): %s\n", c.Params("user"), err)
return err
}
user.Postfix(prefs)
p, err := user.GetReposts(prefs, c.Query("pagination", "?limit=20"))
if err != nil {
@@ -292,11 +316,13 @@ func main() {
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 {
log.Printf("error getting %s from %s: %s\n", c.Params("track"), c.Params("user"), err)
return err
}
track.Postfix(prefs)
displayErr := ""
stream := ""
@@ -327,11 +353,12 @@ func main() {
}
//h := time.Now()
usr, err := sc.GetUser(prefs, c.Params("user"))
usr, err := sc.GetUser(c.Params("user"))
if err != nil {
log.Printf("error getting %s: %s\n", c.Params("user"), err)
return err
}
usr.Postfix(prefs)
//fmt.Println("getuser", time.Since(h))
//h = time.Now()
@@ -352,20 +379,27 @@ func main() {
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 {
log.Printf("error getting %s playlist from %s: %s\n", c.Params("playlist"), c.Params("user"), err)
return err
}
// Don't ask why
playlist.Tracks = playlist.Postfix(prefs)
p := c.Query("pagination")
if p != "" {
tracks, next, err := sc.GetNextMissingTracks(prefs, p)
tracks, next, err := sc.GetNextMissingTracks(p)
if err != nil {
log.Printf("error getting %s playlist tracks from %s: %s\n", c.Params("playlist"), c.Params("user"), err)
return err
}
for i, track := range tracks {
track.Postfix(prefs)
tracks[i] = track
}
playlist.Tracks = tracks
playlist.MissingTracks = strings.Join(next, ",")
}

View File

@@ -51,7 +51,7 @@ templ Playlist(prefs cfg.Preferences, p sc.Playlist) {
<br/>
<div>
for _, track := range p.Tracks {
@TrackItem(track, true)
@TrackItem(&track, true)
}
</div>
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)
//<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>
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>
<br/>
if t.Description != "" {