Get links to other places in user profiles; other small improvements

This commit is contained in:
Laptop
2024-11-30 22:57:01 +02:00
parent 8c92283792
commit 79da855e9e
13 changed files with 198 additions and 76 deletions

View File

@@ -191,6 +191,7 @@ 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. |
| 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. |

View File

@@ -160,4 +160,9 @@ select:focus {
.link {
color: var(--accent);
text-decoration: underline dashed;
}
.link:hover {
text-decoration: underline;
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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)
}
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)
}
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

View File

@@ -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,15 +320,20 @@ func (t *Track) Fix(large bool) {
t.ID = ls[len(ls)-1]
}
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)
}
if fixAuthor {
t.Author.Postfix(prefs)
}
}
func (t Track) FormatDescription() string {
desc := t.Description
@@ -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)}

View File

@@ -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)
}

View File

@@ -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(`<a class="link" href="/%s">%s</a>`, 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(`<a class="link" href="%s" referrerpolicy="no-referrer" rel="external nofollow noopener noreferrer ugc" target="_blank">%s</a>`, href, ent)

View File

@@ -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
}

View File

@@ -28,15 +28,22 @@ templ Base(title string, content templ.Component, head templ.Component) {
</html>
}
templ Description(prefs cfg.Preferences, text string) {
templ Description(prefs cfg.Preferences, text string, injected templ.Component) {
if text != "" || injected != nil {
<details>
<summary>Toggle description</summary>
<p style="white-space: pre-wrap;">
if text != "" {
if *prefs.ParseDescriptions {
@templ.Raw(textparsing.Format(text))
} else {
{ text }
}
}
if injected != nil {
@injected
}
</p>
</details>
}
}

View File

@@ -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) {
<a class="btn" href={ templ.URL("https://soundcloud.com/" + p.Author.Permalink + "/sets/" + p.Permalink) }>view on soundcloud</a>
</div>
<br/>
if p.Description != "" {
@Description(prefs, p.Description)
}
@Description(prefs, p.Description, nil)
<p>{ strconv.FormatInt(p.TrackCount, 10) } tracks</p>
<br/>
<br/>

View File

@@ -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 {
<audio src={ "/_/restream/" + track.Author.Permalink + "/" + track.Permalink } controls></audio>
@@ -38,7 +37,7 @@ templ TrackPlayer(prefs cfg.Preferences, track sc.Track, stream string, displayE
<br/>
JavaScript is disabled! Audio playback may not work without it enabled.
if cfg.Restream {
<br>
<br/>
<a class="link" href="/_/preferences">You can enable Restream player in the preferences. It works without JavaScript.</a>
}
</noscript>
@@ -89,9 +88,7 @@ templ Track(prefs cfg.Preferences, t sc.Track, stream string, displayErr string)
}
</div>
<br/>
if t.Description != "" {
@Description(prefs, t.Description)
}
@Description(prefs, t.Description, nil)
<p>{ strconv.FormatInt(t.Likes, 10) } likes</p>
<p>{ strconv.FormatInt(t.Played, 10) } plays</p>
<p>{ strconv.FormatInt(t.Reposted, 10) } reposts</p>

View File

@@ -32,6 +32,18 @@ templ UserItem(user *sc.User) {
</a>
}
templ UserLinks(links []sc.Link) {
for _, link := range links {
if len(link.URL) > 0 {
if link.URL[0] == '/' {
<p><a class="link" href={ templ.URL(link.URL) }>- { link.Title }</a></p>
} else {
<p><a class="link" href={ templ.URL(link.URL) } referrerpolicy="no-referrer" rel="external nofollow noopener noreferrer" target="_blank">- { link.Title }</a></p>
}
}
}
}
templ UserBase(prefs cfg.Preferences, u sc.User) {
<div>
if u.Avatar != "" {
@@ -45,8 +57,10 @@ templ UserBase(prefs cfg.Preferences, u sc.User) {
<p style="color: var(--accent)">Verified</p>
}
</div>
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)
}
<div>
<p>{ strconv.FormatInt(u.Followers, 10) } followers</p>
@@ -66,7 +80,8 @@ type btn struct {
}
templ UserButtons(current string, user string) {
<div class="btns"> // this part is the tedious one now, because formatting breaks if i space the list out with newlines
<div class="btns">
// 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 {
<a class="btn active">{ b.text }</a>