From 79da855e9e0fd635eac69109124be67a5eb1aad4 Mon Sep 17 00:00:00 2001 From: Laptop Date: Sat, 30 Nov 2024 22:57:01 +0200 Subject: [PATCH] Get links to other places in user profiles; other small improvements --- README.md | 3 +- assets/global.css | 5 ++ lib/cfg/init.go | 14 +++++- lib/sc/featured.go | 8 +-- lib/sc/playlist.go | 31 +++++++----- lib/sc/track.go | 23 +++++---- lib/sc/user.go | 105 ++++++++++++++++++++++++++++++++------- lib/textparsing/init.go | 10 +++- main.go | 8 +-- templates/base.templ | 29 +++++++---- templates/playlist.templ | 6 +-- templates/track.templ | 9 ++-- templates/user.templ | 23 +++++++-- 13 files changed, 198 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 1ef1796..d6b793f 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,8 @@ 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. | +| None | SOUNDCLOAK_CONFIG | soundcloak.json | File to load soundcloak config from. If set to `FROM_ENV`, soundcloak loads the config from environment variables. | +| GetWebProfiles | GET_WEB_PROFILES | true | Retrieve links users set in their profile (social media, website, etc) | | 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. | diff --git a/assets/global.css b/assets/global.css index 0b6f978..b765933 100644 --- a/assets/global.css +++ b/assets/global.css @@ -160,4 +160,9 @@ select:focus { .link { color: var(--accent); + text-decoration: underline dashed; +} + +.link:hover { + text-decoration: underline; } \ No newline at end of file diff --git a/lib/cfg/init.go b/lib/cfg/init.go index 8b9500d..6598bfe 100644 --- a/lib/cfg/init.go +++ b/lib/cfg/init.go @@ -42,6 +42,9 @@ type Preferences struct { // // config // // +// Retrieve links users set in their profile (social media, website, etc) +var GetWebProfiles = true + // Default preferences. You can override those preferences in the config file, otherwise they default to values depending on your config // (so, if you have ProxyStreams enabled - it will be enabled for the user by default and etc, or if you enabled Restream, the default player will be RestreamPlayer instead of HLSPlayer) var DefaultPreferences Preferences @@ -189,7 +192,12 @@ func (w wrappedError) Error() string { } func fromEnv() error { - env := os.Getenv("DEFAULT_PREFERENCES") + env := os.Getenv("GET_WEB_PROFILES") + if env != "" { + GetWebProfiles = boolean(env) + } + + env = os.Getenv("DEFAULT_PREFERENCES") if env != "" { var p Preferences err := json.Unmarshal([]byte(env), &p) @@ -372,6 +380,7 @@ func init() { } var config struct { + GetWebProfiles *bool DefaultPreferences *Preferences ProxyImages *bool ImageCacheControl *string @@ -403,6 +412,9 @@ func init() { // tedious // i've decided to fully override to make it easier to change default config later on + if config.GetWebProfiles != nil { + GetWebProfiles = *config.GetWebProfiles + } if config.ProxyImages != nil { ProxyImages = *config.ProxyImages } diff --git a/lib/sc/featured.go b/lib/sc/featured.go index 7525e97..d692662 100644 --- a/lib/sc/featured.go +++ b/lib/sc/featured.go @@ -26,8 +26,8 @@ func GetFeaturedTracks(prefs cfg.Preferences, args string) (*Paginated[*Track], } for _, t := range p.Collection { - t.Fix(false) - t.Postfix(prefs) + t.Fix(false, false) + t.Postfix(prefs, false) } return &p, nil @@ -55,7 +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(false) - p.Postfix(prefs, false) + p.Fix(false, false) + p.Postfix(prefs, false, false) } } diff --git a/lib/sc/playlist.go b/lib/sc/playlist.go index 7a61f22..6e2b252 100644 --- a/lib/sc/playlist.go +++ b/lib/sc/playlist.go @@ -53,7 +53,7 @@ func GetPlaylist(permalink string) (Playlist, error) { return p, ErrKindNotCorrect } - err = p.Fix(true) + err = p.Fix(true, true) if err != nil { return p, err } @@ -78,45 +78,50 @@ func SearchPlaylists(prefs cfg.Preferences, args string) (*Paginated[*Playlist], } for _, p := range p.Collection { - p.Fix(false) - p.Postfix(prefs, false) + p.Fix(false, false) + p.Postfix(prefs, false, false) } return &p, nil } -func (p *Playlist) Fix(cached bool) error { +func (p *Playlist) Fix(cached bool, fixAuthor bool) error { if cached { - for i, t := range p.Tracks { - t.Fix(false) - p.Tracks[i] = t - } - err := p.GetMissingTracks() if err != nil { return err } + for i, t := range p.Tracks { + t.Fix(false, false) + p.Tracks[i] = t + } + p.Artwork = strings.Replace(p.Artwork, "-large.", "-t500x500.", 1) } else { p.Artwork = strings.Replace(p.Artwork, "-large.", "-t200x200.", 1) } - p.Author.Fix(false) + if fixAuthor { + p.Author.Fix(false) + } return nil } -func (p *Playlist) Postfix(prefs cfg.Preferences, fixTracks bool) []Track { +func (p *Playlist) Postfix(prefs cfg.Preferences, fixTracks bool, fixAuthor bool) []Track { if cfg.ProxyImages && *prefs.ProxyImages && p.Artwork != "" { p.Artwork = "/_/proxy/images?url=" + url.QueryEscape(p.Artwork) } - p.Author.Postfix(prefs) + if fixAuthor { + p.Author.Postfix(prefs) + } + if fixTracks { var fixed = make([]Track, len(p.Tracks)) for i, t := range p.Tracks { - t.Postfix(prefs) + t.Postfix(prefs, false) fixed[i] = t } return fixed diff --git a/lib/sc/track.go b/lib/sc/track.go index 2d10b28..18b3008 100644 --- a/lib/sc/track.go +++ b/lib/sc/track.go @@ -108,7 +108,7 @@ func GetTrack(permalink string) (Track, error) { return t, ErrKindNotCorrect } - t.Fix(true) + t.Fix(true, true) tracksCacheLock.Lock() TracksCache[permalink] = cached[Track]{Value: t, Expires: time.Now().Add(cfg.TrackTTL)} @@ -217,8 +217,8 @@ func SearchTracks(prefs cfg.Preferences, args string) (*Paginated[*Track], error } for _, t := range p.Collection { - t.Fix(false) - t.Postfix(prefs) + t.Fix(false, false) + t.Postfix(prefs, false) } return &p, nil @@ -253,7 +253,7 @@ func GetTracks(ids string) ([]Track, error) { var res []Track err = json.Unmarshal(data, &res) for i, t := range res { - t.Fix(false) + t.Fix(false, false) res[i] = t } return res, err @@ -306,7 +306,7 @@ func (tr Transcoding) GetStream(prefs cfg.Preferences, authorization string) (st return s.URL, nil } -func (t *Track) Fix(large bool) { +func (t *Track) Fix(large bool, fixAuthor bool) { if large { t.Artwork = strings.Replace(t.Artwork, "-large.", "-t500x500.", 1) } else { @@ -320,14 +320,19 @@ func (t *Track) Fix(large bool) { t.ID = ls[len(ls)-1] } - t.Author.Fix(false) + if fixAuthor { + t.Author.Fix(false) + } } -func (t *Track) Postfix(prefs cfg.Preferences) { +func (t *Track) Postfix(prefs cfg.Preferences, fixAuthor bool) { if cfg.ProxyImages && *prefs.ProxyImages && t.Artwork != "" { t.Artwork = "/_/proxy/images?url=" + url.QueryEscape(t.Artwork) } - t.Author.Postfix(prefs) + + if fixAuthor { + t.Author.Postfix(prefs) + } } func (t Track) FormatDescription() string { @@ -394,7 +399,7 @@ func GetTrackByID(id string) (Track, error) { return t, ErrKindNotCorrect } - t.Fix(true) + t.Fix(true, true) tracksCacheLock.Lock() TracksCache[t.Author.Permalink+"/"+t.Permalink] = cached[Track]{Value: t, Expires: time.Now().Add(cfg.TrackTTL)} diff --git a/lib/sc/user.go b/lib/sc/user.go index ecc08c3..d9efb8e 100644 --- a/lib/sc/user.go +++ b/lib/sc/user.go @@ -1,6 +1,7 @@ package sc import ( + "fmt" "net/url" "strconv" "strings" @@ -8,6 +9,9 @@ import ( "time" "github.com/maid-zone/soundcloak/lib/cfg" + "github.com/maid-zone/soundcloak/lib/textparsing" + "github.com/segmentio/encoding/json" + "github.com/valyala/fasthttp" ) // Functions/structures related to users @@ -31,6 +35,13 @@ type User struct { ID string `json:"urn"` Username string `json:"username"` Verified bool `json:"verified"` + + WebProfiles []Link +} + +type Link struct { + URL string `json:"url"` + Title string `json:"title"` } type RepostType string @@ -52,14 +63,14 @@ func (r Repost) Fix(prefs cfg.Preferences) { switch r.Type { case TrackRepost: if r.Track != nil { - r.Track.Fix(false) - r.Track.Postfix(prefs) + r.Track.Fix(false, false) + r.Track.Postfix(prefs, false) } return case PlaylistRepost: if r.Playlist != nil { - r.Playlist.Fix(false) // err always nil if cached == false - r.Playlist.Postfix(prefs, false) + r.Playlist.Fix(false, false) // err always nil if cached == false + r.Playlist.Postfix(prefs, false, false) } return } @@ -73,11 +84,11 @@ type Like struct { func (l Like) Fix(prefs cfg.Preferences) { if l.Track != nil { - l.Track.Fix(false) - l.Track.Postfix(prefs) + l.Track.Fix(false, false) + l.Track.Postfix(prefs, false) } else if l.Playlist != nil { - l.Playlist.Fix(false) - l.Playlist.Postfix(prefs, false) + l.Playlist.Fix(false, false) + l.Playlist.Postfix(prefs, false, false) } } func GetUser(permalink string) (User, error) { @@ -100,6 +111,12 @@ func GetUser(permalink string) (User, error) { return u, err } + if cfg.GetWebProfiles { + err = u.GetWebProfiles() + if err != nil { + return u, err + } + } u.Fix(true) usersCacheLock.Lock() @@ -140,8 +157,8 @@ func (u User) GetTracks(prefs cfg.Preferences, args string) (*Paginated[*Track], } for _, t := range p.Collection { - t.Fix(false) - t.Postfix(prefs) + t.Fix(false, false) + t.Postfix(prefs, false) } return &p, nil @@ -184,6 +201,25 @@ func (u *User) Fix(large bool) { ls := strings.Split(u.ID, ":") u.ID = ls[len(ls)-1] + + for i, l := range u.WebProfiles { + if textparsing.IsEmail(l.URL) { + l.URL = "mailto:" + l.URL + u.WebProfiles[i] = l + } else { + parsed, err := url.Parse(l.URL) + if err == nil { + if parsed.Host == "soundcloud.com" || strings.HasSuffix(parsed.Host, ".soundcloud.com") { + l.URL = "/" + strings.Join(strings.Split(l.URL, "/")[3:], "/") + if parsed.Host == "on.soundcloud.com" { + l.URL = "/on" + l.URL + } + + u.WebProfiles[i] = l + } + } + } + } } func (u *User) Postfix(prefs cfg.Preferences) { @@ -192,7 +228,7 @@ func (u *User) Postfix(prefs cfg.Preferences) { } } -func (u *User) GetPlaylists(prefs cfg.Preferences, args string) (*Paginated[*Playlist], error) { +func (u User) GetPlaylists(prefs cfg.Preferences, args string) (*Paginated[*Playlist], error) { p := Paginated[*Playlist]{ Next: "https://" + api + "/users/" + u.ID + "/playlists_without_albums" + args, } @@ -203,14 +239,14 @@ func (u *User) GetPlaylists(prefs cfg.Preferences, args string) (*Paginated[*Pla } for _, pl := range p.Collection { - pl.Fix(false) - pl.Postfix(prefs, false) + pl.Fix(false, false) + pl.Postfix(prefs, false, false) } return &p, nil } -func (u *User) GetAlbums(prefs cfg.Preferences, args string) (*Paginated[*Playlist], error) { +func (u User) GetAlbums(prefs cfg.Preferences, args string) (*Paginated[*Playlist], error) { p := Paginated[*Playlist]{ Next: "https://" + api + "/users/" + u.ID + "/albums" + args, } @@ -221,14 +257,14 @@ func (u *User) GetAlbums(prefs cfg.Preferences, args string) (*Paginated[*Playli } for _, pl := range p.Collection { - pl.Fix(false) - pl.Postfix(prefs, false) + pl.Fix(false, false) + pl.Postfix(prefs, false, false) } return &p, nil } -func (u *User) GetReposts(prefs cfg.Preferences, args string) (*Paginated[*Repost], error) { +func (u User) GetReposts(prefs cfg.Preferences, args string) (*Paginated[*Repost], error) { p := Paginated[*Repost]{ Next: "https://" + api + "/stream/users/" + u.ID + "/reposts" + args, } @@ -245,7 +281,7 @@ func (u *User) GetReposts(prefs cfg.Preferences, args string) (*Paginated[*Repos return &p, nil } -func (u *User) GetLikes(prefs cfg.Preferences, args string) (*Paginated[*Like], error) { +func (u User) GetLikes(prefs cfg.Preferences, args string) (*Paginated[*Like], error) { p := Paginated[*Like]{ Next: "https://" + api + "/users/" + u.ID + "/likes" + args, } @@ -261,3 +297,36 @@ func (u *User) GetLikes(prefs cfg.Preferences, args string) (*Paginated[*Like], return &p, nil } + +func (u *User) GetWebProfiles() error { + cid, err := GetClientID() + if err != nil { + return err + } + + req := fasthttp.AcquireRequest() + defer fasthttp.ReleaseRequest(req) + + req.SetRequestURI("https://" + api + "/users/" + u.ID + "/web-profiles?client_id=" + cid) + 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(httpc, req, resp) + if err != nil { + return err + } + + if resp.StatusCode() != 200 { + return fmt.Errorf("getwebprofiles: got status code %d", resp.StatusCode()) + } + + data, err := resp.BodyUncompressed() + if err != nil { + data = resp.Body() + } + + return json.Unmarshal(data, &u.WebProfiles) +} diff --git a/lib/textparsing/init.go b/lib/textparsing/init.go index ec636df..b3c1411 100644 --- a/lib/textparsing/init.go +++ b/lib/textparsing/init.go @@ -11,10 +11,15 @@ import ( //var wordre = regexp.MustCompile(`\S+`) // var urlre = regexp.MustCompile(`https?:\/\/[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{1,6}[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*`) -// var emailre = regexp.MustCompile(`[-a-zA-Z0-9%._\+~#=]+@[-a-zA-Z0-9%._\+~=&]{2,256}\.[a-z]{1,6}`) +var emailre = regexp.MustCompile(`^[-a-zA-Z0-9%._\+~#=]+@[-a-zA-Z0-9%._\+~=&]{2,256}\.[a-z]{1,6}$`) + // var usernamere = regexp.MustCompile(`@[a-zA-Z0-9\-]+`) var theregex = regexp.MustCompile(`@[a-zA-Z0-9\-]+|(?:https?:\/\/[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{1,6}[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)|(?:[-a-zA-Z0-9%._\+~#=]+@[-a-zA-Z0-9%._\+~=&]{2,256}\.[a-z]{1,6})`) +func IsEmail(s string) bool { + return emailre.MatchString(s) +} + func replacer(ent string) string { if strings.HasPrefix(ent, "@") { return fmt.Sprintf(`%s`, ent[1:], ent) @@ -27,6 +32,9 @@ func replacer(ent string) string { href := ent if parsed.Host == "soundcloud.com" || strings.HasSuffix(parsed.Host, ".soundcloud.com") { href = "/" + strings.Join(strings.Split(ent, "/")[3:], "/") + if parsed.Host == "on.soundcloud.com" { + href = "/on" + href + } } return fmt.Sprintf(`%s`, href, ent) diff --git a/main.go b/main.go index 44bd00a..f2d5dd5 100644 --- a/main.go +++ b/main.go @@ -152,7 +152,7 @@ func main() { log.Printf("error getting %s: %s\n", u, err) return err } - track.Postfix(prefs) + track.Postfix(prefs, true) displayErr := "" stream := "" @@ -344,7 +344,7 @@ func main() { log.Printf("error getting %s from %s: %s\n", c.Params("track"), c.Params("user"), err) return err } - track.Postfix(prefs) + track.Postfix(prefs, true) displayErr := "" stream := "" @@ -408,7 +408,7 @@ func main() { return err } // Don't ask why - playlist.Tracks = playlist.Postfix(prefs, true) + playlist.Tracks = playlist.Postfix(prefs, true, true) p := c.Query("pagination") if p != "" { @@ -419,7 +419,7 @@ func main() { } for i, track := range tracks { - track.Postfix(prefs) + track.Postfix(prefs, false) tracks[i] = track } diff --git a/templates/base.templ b/templates/base.templ index 3ffdd94..4a17f8a 100644 --- a/templates/base.templ +++ b/templates/base.templ @@ -28,15 +28,22 @@ templ Base(title string, content templ.Component, head templ.Component) { } -templ Description(prefs cfg.Preferences, text string) { -
- Toggle description -

- if *prefs.ParseDescriptions { - @templ.Raw(textparsing.Format(text)) - } else { - { text } - } -

-
+templ Description(prefs cfg.Preferences, text string, injected templ.Component) { + if text != "" || injected != nil { +
+ Toggle description +

+ if text != "" { + if *prefs.ParseDescriptions { + @templ.Raw(textparsing.Format(text)) + } else { + { text } + } + } + if injected != nil { + @injected + } +

+
+ } } diff --git a/templates/playlist.templ b/templates/playlist.templ index 33f4a2c..4b2d995 100644 --- a/templates/playlist.templ +++ b/templates/playlist.templ @@ -1,11 +1,11 @@ package templates import ( + "github.com/maid-zone/soundcloak/lib/cfg" "github.com/maid-zone/soundcloak/lib/sc" "net/url" "strconv" "strings" - "github.com/maid-zone/soundcloak/lib/cfg" ) templ PlaylistHeader(p sc.Playlist) { @@ -43,9 +43,7 @@ templ Playlist(prefs cfg.Preferences, p sc.Playlist) { view on soundcloud
- if p.Description != "" { - @Description(prefs, p.Description) - } + @Description(prefs, p.Description, nil)

{ strconv.FormatInt(p.TrackCount, 10) } tracks



diff --git a/templates/track.templ b/templates/track.templ index f50c5d8..d60688e 100644 --- a/templates/track.templ +++ b/templates/track.templ @@ -23,7 +23,6 @@ templ TrackPlayer(prefs cfg.Preferences, track sc.Track, stream string, displayE if *prefs.Player == cfg.NonePlayer { {{ return }} } - if displayErr == "" { if cfg.Restream && *prefs.Player == cfg.RestreamPlayer { @@ -38,7 +37,7 @@ templ TrackPlayer(prefs cfg.Preferences, track sc.Track, stream string, displayE
JavaScript is disabled! Audio playback may not work without it enabled. if cfg.Restream { -
+
You can enable Restream player in the preferences. It works without JavaScript. } @@ -85,13 +84,11 @@ templ Track(prefs cfg.Preferences, t sc.Track, stream string, displayErr string)
view on soundcloud if cfg.Restream { - download + download }

- if t.Description != "" { - @Description(prefs, t.Description) - } + @Description(prefs, t.Description, nil)

{ strconv.FormatInt(t.Likes, 10) } likes

{ strconv.FormatInt(t.Played, 10) } plays

{ strconv.FormatInt(t.Reposted, 10) } reposts

diff --git a/templates/user.templ b/templates/user.templ index afd48d2..c999d0e 100644 --- a/templates/user.templ +++ b/templates/user.templ @@ -32,6 +32,18 @@ templ UserItem(user *sc.User) { } +templ UserLinks(links []sc.Link) { + for _, link := range links { + if len(link.URL) > 0 { + if link.URL[0] == '/' { +

- { link.Title }

+ } else { +

- { link.Title }

+ } + } + } +} + templ UserBase(prefs cfg.Preferences, u sc.User) {
if u.Avatar != "" { @@ -45,8 +57,10 @@ templ UserBase(prefs cfg.Preferences, u sc.User) {

Verified

}
- if u.Description != "" { - @Description(prefs, u.Description) + if len(u.WebProfiles) != 0 { + @Description(prefs, u.Description, UserLinks(u.WebProfiles)) + } else { + @Description(prefs, u.Description, nil) }

{ strconv.FormatInt(u.Followers, 10) } followers

@@ -66,10 +80,11 @@ type btn struct { } templ UserButtons(current string, user string) { -
// this part is the tedious one now, because formatting breaks if i space the list out with newlines +
+ // this part is the tedious one now, because formatting breaks if i space the list out with newlines for _, b := range [6]btn{{"tracks", "", false},{"playlists", "/sets",false},{"albums", "/albums", false},{"reposts","/reposts", false},{"likes", "/likes", false},{"view on soundcloud", "https://soundcloud.com/"+user, true}} { if b.text == current { - {b.text} + { b.text } } else { if b.external { { b.text }