mirror of
https://git.maid.zone/stuff/soundcloak.git
synced 2025-12-10 05:39:38 +05:00
Highlight @username, links and emails in descriptions; and some fixes
This commit is contained in:
@@ -5,11 +5,12 @@ wip alternative frontend for soundcloud
|
||||
|
||||
# Already implemented
|
||||
- Searching for songs, users, playlists
|
||||
- Basic user overview (songs, playlists, albums, metadata)
|
||||
- Basic user overview (songs, playlists, albums, reposts, metadata)
|
||||
- Basic song overview (author, metadata) & streaming (requires some JS if instance has `Restream` disabled)
|
||||
- Basic playlist/set/album overview (songs list, author, metadata)
|
||||
- Resolving shortened links (`https://on.soundcloud.com/boiKDP46fayYDoVK9` -> `https://sc.maid.zone/on/boiKDP46fayYDoVK9`)
|
||||
- Content proxying (images, audio)
|
||||
- View featured tracks, playlists
|
||||
- Users can change their preferences (should proxying be enabled, what method of playing the song should be used etc)
|
||||
|
||||
## In the works
|
||||
@@ -25,6 +26,7 @@ An easier way is to navigate to `<instance>/_/preferences`.
|
||||
If some features are disabled by the instance, they won't show up there.
|
||||
|
||||
Available features:
|
||||
- ParseDescriptions: Highlight `@username`, `https://example.com` and `email@example.com` in text as clickable links
|
||||
- Proxy images: Retrieve images through the instance, instead of going to soundcloud's servers for them
|
||||
- Player: In what way should the track be streamed. Can be Restream (does not require JS, better compatibility, can be a bit buggy client-side) or HLS (requires JS, more stable, less good compatibility (you'll be ok unless you are using a very outdated browser))
|
||||
- Player-specific settings: They will only show up if you have selected HLS player currently.
|
||||
|
||||
@@ -156,4 +156,8 @@ select:focus {
|
||||
display: grid;
|
||||
grid-template: auto / auto auto auto;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: var(--accent);
|
||||
}
|
||||
@@ -32,6 +32,9 @@ type Preferences struct {
|
||||
FullyPreloadTrack *bool
|
||||
|
||||
ProxyImages *bool
|
||||
|
||||
// Highlight @username, https://example.com and email@example.com in text as clickable links
|
||||
ParseDescriptions *bool
|
||||
}
|
||||
|
||||
// // config // //
|
||||
@@ -218,8 +221,10 @@ func init() {
|
||||
// ProxyStreams: same as ProxyStreams in your config (false by default)
|
||||
// FullyPreloadTrack: false
|
||||
// ProxyImages: same as ProxyImages in your config (false by default)
|
||||
// ParseDescriptions: true
|
||||
if config.DefaultPreferences != nil {
|
||||
var f bool
|
||||
var t = true
|
||||
if config.DefaultPreferences.Player != nil {
|
||||
DefaultPreferences.Player = config.DefaultPreferences.Player
|
||||
} else {
|
||||
@@ -249,6 +254,12 @@ func init() {
|
||||
} else {
|
||||
DefaultPreferences.ProxyImages = &ProxyImages
|
||||
}
|
||||
|
||||
if config.DefaultPreferences.ParseDescriptions != nil {
|
||||
DefaultPreferences.ParseDescriptions = config.DefaultPreferences.ParseDescriptions
|
||||
} else {
|
||||
DefaultPreferences.ParseDescriptions = &t
|
||||
}
|
||||
} else {
|
||||
var p string
|
||||
if Restream {
|
||||
@@ -261,8 +272,11 @@ func init() {
|
||||
DefaultPreferences.ProxyStreams = &ProxyStreams
|
||||
|
||||
var f bool
|
||||
var t = true
|
||||
DefaultPreferences.FullyPreloadTrack = &f
|
||||
|
||||
DefaultPreferences.ProxyImages = &ProxyImages
|
||||
|
||||
DefaultPreferences.ParseDescriptions = &t
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ func Defaults(dst *cfg.Preferences) {
|
||||
if dst.ProxyImages == nil {
|
||||
dst.ProxyImages = cfg.DefaultPreferences.ProxyImages
|
||||
}
|
||||
|
||||
if dst.ParseDescriptions == nil {
|
||||
dst.ParseDescriptions = cfg.DefaultPreferences.ParseDescriptions
|
||||
}
|
||||
}
|
||||
|
||||
func Get(c *fiber.Ctx) (cfg.Preferences, error) {
|
||||
@@ -43,6 +47,7 @@ func Get(c *fiber.Ctx) (cfg.Preferences, error) {
|
||||
|
||||
type PrefsForm struct {
|
||||
ProxyImages string
|
||||
ParseDescriptions string
|
||||
Player string
|
||||
ProxyStreams string
|
||||
FullyPreloadTrack string
|
||||
@@ -96,6 +101,12 @@ func Load(r fiber.Router) {
|
||||
old.ProxyImages = &f
|
||||
}
|
||||
}
|
||||
|
||||
if p.ParseDescriptions == "on" {
|
||||
old.ParseDescriptions = &t
|
||||
} else {
|
||||
old.ParseDescriptions = &f
|
||||
}
|
||||
old.Player = &p.Player
|
||||
|
||||
data, err := json.Marshal(old)
|
||||
|
||||
@@ -119,13 +119,19 @@ func GetClientID() (string, error) {
|
||||
}
|
||||
|
||||
func DoWithRetry(httpc *fasthttp.HostClient, req *fasthttp.Request, resp *fasthttp.Response) (err error) {
|
||||
for i := 0; i < 5; i++ {
|
||||
for i := 0; i < 10; i++ {
|
||||
err = httpc.Do(req, resp)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != fasthttp.ErrTimeout && err != fasthttp.ErrConnectionClosed && !os.IsTimeout(err) && !errors.Is(err, syscall.EPIPE) { // EPIPE is "broken pipe" error
|
||||
if err != fasthttp.ErrTimeout &&
|
||||
err != fasthttp.ErrDialTimeout &&
|
||||
err != fasthttp.ErrTLSHandshakeTimeout &&
|
||||
err != fasthttp.ErrConnectionClosed &&
|
||||
!os.IsTimeout(err) &&
|
||||
!errors.Is(err, syscall.EPIPE) && // EPIPE is "broken pipe" error
|
||||
err.Error() != "timeout" {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
42
lib/textparsing/init.go
Normal file
42
lib/textparsing/init.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package textparsing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//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 usernamere = regexp.MustCompile(`@[a-z0-9\-]+`)
|
||||
var theregex = regexp.MustCompile(`@[a-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 replacer(ent string) string {
|
||||
if strings.HasPrefix(ent, "@") {
|
||||
return fmt.Sprintf(`<a class="link" href="/%s">%s</a>`, ent[1:], ent)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(ent, "https://") || strings.HasPrefix(ent, "http://") {
|
||||
ent = html.UnescapeString(ent)
|
||||
parsed, err := url.Parse(ent)
|
||||
if err == nil {
|
||||
href := ent
|
||||
if parsed.Host == "soundcloud.com" || strings.HasSuffix(parsed.Host, ".soundcloud.com") {
|
||||
href = "/" + strings.Join(strings.Split(ent, "/")[3:], "/")
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`<a class="link" href="%s" referrerpolicy="no-referrer" rel="external nofollow noopener noreferrer ugc" target="_blank">%s</a>`, href, ent)
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, it can only be an email
|
||||
return fmt.Sprintf(`<a class="link" href="mailto:%s">%s</a>`, ent, ent)
|
||||
}
|
||||
|
||||
func Format(text string) string {
|
||||
return theregex.ReplaceAllStringFunc(html.EscapeString(text), replacer)
|
||||
}
|
||||
10
main.go
10
main.go
@@ -243,7 +243,7 @@ func main() {
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base(user.Username, templates.UserPlaylists(user, pl), templates.UserHeader(user)).Render(context.Background(), c)
|
||||
return templates.Base(user.Username, templates.UserPlaylists(prefs, user, pl), templates.UserHeader(user)).Render(context.Background(), c)
|
||||
})
|
||||
|
||||
app.Get("/:user/albums", func(c *fiber.Ctx) error {
|
||||
@@ -265,7 +265,7 @@ func main() {
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base(user.Username, templates.UserAlbums(user, pl), templates.UserHeader(user)).Render(context.Background(), c)
|
||||
return templates.Base(user.Username, templates.UserAlbums(prefs, user, pl), templates.UserHeader(user)).Render(context.Background(), c)
|
||||
})
|
||||
|
||||
app.Get("/:user/reposts", func(c *fiber.Ctx) error {
|
||||
@@ -287,7 +287,7 @@ func main() {
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base(user.Username, templates.UserReposts(user, p), templates.UserHeader(user)).Render(context.Background(), c)
|
||||
return templates.Base(user.Username, templates.UserReposts(prefs, user, p), templates.UserHeader(user)).Render(context.Background(), c)
|
||||
})
|
||||
|
||||
app.Get("/:user/:track", func(c *fiber.Ctx) error {
|
||||
@@ -347,7 +347,7 @@ func main() {
|
||||
//fmt.Println("gettracks", time.Since(h))
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base(usr.Username, templates.User(usr, p), templates.UserHeader(usr)).Render(context.Background(), c)
|
||||
return templates.Base(usr.Username, templates.User(prefs, usr, p), templates.UserHeader(usr)).Render(context.Background(), c)
|
||||
})
|
||||
|
||||
app.Get("/:user/sets/:playlist", func(c *fiber.Ctx) error {
|
||||
@@ -375,7 +375,7 @@ func main() {
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base(playlist.Title+" by "+playlist.Author.Username, templates.Playlist(playlist), templates.PlaylistHeader(playlist)).Render(context.Background(), c)
|
||||
return templates.Base(playlist.Title+" by "+playlist.Author.Username, templates.Playlist(prefs, playlist), templates.PlaylistHeader(playlist)).Render(context.Background(), c)
|
||||
})
|
||||
|
||||
log.Fatal(app.Listen(cfg.Addr))
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"github.com/maid-zone/soundcloak/lib/cfg"
|
||||
"github.com/maid-zone/soundcloak/lib/textparsing"
|
||||
)
|
||||
|
||||
templ Base(title string, content templ.Component, head templ.Component) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@@ -22,3 +27,16 @@ templ Base(title string, content templ.Component, head templ.Component) {
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
templ Description(prefs cfg.Preferences, text string) {
|
||||
<details>
|
||||
<summary>Toggle description</summary>
|
||||
<p style="white-space: pre-wrap;">
|
||||
if *prefs.ParseDescriptions {
|
||||
@templ.Raw(textparsing.Format(text))
|
||||
} else {
|
||||
{ text }
|
||||
}
|
||||
</p>
|
||||
</details>
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"github.com/maid-zone/soundcloak/lib/cfg"
|
||||
)
|
||||
|
||||
templ PlaylistHeader(p sc.Playlist) {
|
||||
@@ -32,7 +33,7 @@ templ PlaylistItem(playlist *sc.Playlist, showUsername bool) {
|
||||
</a>
|
||||
}
|
||||
|
||||
templ Playlist(p sc.Playlist) {
|
||||
templ Playlist(prefs cfg.Preferences, p sc.Playlist) {
|
||||
if p.Artwork != "" {
|
||||
<img src={ p.Artwork } width="300px"/>
|
||||
}
|
||||
@@ -43,10 +44,7 @@ templ Playlist(p sc.Playlist) {
|
||||
</div>
|
||||
<br/>
|
||||
if p.Description != "" {
|
||||
<details>
|
||||
<summary>Toggle description</summary>
|
||||
<p style="white-space: pre-wrap">{ p.Description }</p>
|
||||
</details>
|
||||
@Description(prefs, p.Description)
|
||||
}
|
||||
<p>{ strconv.FormatInt(p.TrackCount, 10) } tracks</p>
|
||||
<br/>
|
||||
|
||||
@@ -11,8 +11,8 @@ templ checkbox(name string, checked bool) {
|
||||
}
|
||||
|
||||
type option struct {
|
||||
value string
|
||||
desc string
|
||||
value string
|
||||
desc string
|
||||
disabled bool
|
||||
}
|
||||
|
||||
@@ -36,6 +36,11 @@ templ sel(name string, options []option, selected string) {
|
||||
templ Preferences(prefs cfg.Preferences) {
|
||||
<h1>Preferences</h1>
|
||||
<form method="post">
|
||||
<label>
|
||||
Parse descriptions:
|
||||
@checkbox("ParseDescriptions", *prefs.ParseDescriptions)
|
||||
</label>
|
||||
<br/>
|
||||
if cfg.ProxyImages {
|
||||
<label>
|
||||
Proxy images:
|
||||
@@ -68,9 +73,8 @@ templ Preferences(prefs cfg.Preferences) {
|
||||
<br/>
|
||||
}
|
||||
<input type="submit" value="Update" class="btn" style="margin-top: 1rem;"/>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
<br/>
|
||||
<br/>
|
||||
<p>These preferences get saved in a cookie.</p>
|
||||
</form>
|
||||
}
|
||||
|
||||
@@ -87,10 +87,7 @@ templ Track(prefs cfg.Preferences, t sc.Track, stream string, displayErr string)
|
||||
</div>
|
||||
<br/>
|
||||
if t.Description != "" {
|
||||
<details>
|
||||
<summary>Toggle description</summary>
|
||||
<p style="white-space: pre-wrap">{ t.Description }</p>
|
||||
</details>
|
||||
@Description(prefs, t.Description)
|
||||
}
|
||||
<p>{ strconv.FormatInt(t.Likes, 10) } likes</p>
|
||||
<p>{ strconv.FormatInt(t.Played, 10) } plays</p>
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"github.com/maid-zone/soundcloak/lib/cfg"
|
||||
)
|
||||
|
||||
templ UserHeader(u sc.User) {
|
||||
@@ -31,7 +32,7 @@ templ UserItem(user *sc.User) {
|
||||
</a>
|
||||
}
|
||||
|
||||
templ UserBase(u sc.User) {
|
||||
templ UserBase(prefs cfg.Preferences, u sc.User) {
|
||||
<div>
|
||||
if u.Avatar != "" {
|
||||
<img src={ u.Avatar } width="300px"/>
|
||||
@@ -45,10 +46,7 @@ templ UserBase(u sc.User) {
|
||||
}
|
||||
</div>
|
||||
if u.Description != "" {
|
||||
<details>
|
||||
<summary>Toggle description</summary>
|
||||
<p style="white-space: pre-wrap">{ u.Description }</p>
|
||||
</details>
|
||||
@Description(prefs, u.Description)
|
||||
}
|
||||
<div>
|
||||
<p>{ strconv.FormatInt(u.Followers, 10) } followers</p>
|
||||
@@ -61,15 +59,15 @@ templ UserBase(u sc.User) {
|
||||
</div>
|
||||
}
|
||||
|
||||
templ User(u sc.User, p *sc.Paginated[*sc.Track]) {
|
||||
@UserBase(u)
|
||||
templ User(prefs cfg.Preferences, u sc.User, p *sc.Paginated[*sc.Track]) {
|
||||
@UserBase(prefs, u)
|
||||
// kinda tedious but whatever, might make it more flexible in the future
|
||||
<div class="btns">
|
||||
<a class="btn active">tracks</a>
|
||||
<a class="btn" href={ templ.URL("/" + u.Permalink + "/sets") }>playlists</a>
|
||||
<a class="btn" href={ templ.URL("/" + u.Permalink + "/albums") }>albums</a>
|
||||
<a class="btn" href={ templ.URL("/" + u.Permalink + "/reposts") }>reposts</a>
|
||||
<a class="btn" href={ templ.URL("https://soundcloud.com/" + u.Permalink) }>view on soundcloud</a>
|
||||
<a class="btn" href={ templ.URL("https://soundcloud.com/" + u.Permalink) } referrerpolicy="no-referrer" rel="external nofollow noopener noreferrer" target="_blank">view on soundcloud</a>
|
||||
</div>
|
||||
<br/>
|
||||
if len(p.Collection) != 0 {
|
||||
@@ -86,14 +84,14 @@ templ User(u sc.User, p *sc.Paginated[*sc.Track]) {
|
||||
}
|
||||
}
|
||||
|
||||
templ UserPlaylists(u sc.User, p *sc.Paginated[*sc.Playlist]) {
|
||||
@UserBase(u)
|
||||
templ UserPlaylists(prefs cfg.Preferences, u sc.User, p *sc.Paginated[*sc.Playlist]) {
|
||||
@UserBase(prefs, u)
|
||||
<div class="btns">
|
||||
<a class="btn" href={ templ.URL("/" + u.Permalink) }>tracks</a>
|
||||
<a class="btn active">playlists</a>
|
||||
<a class="btn" href={ templ.URL("/" + u.Permalink + "/albums") }>albums</a>
|
||||
<a class="btn" href={ templ.URL("/" + u.Permalink + "/reposts") }>reposts</a>
|
||||
<a class="btn" href={ templ.URL("https://soundcloud.com/" + u.Permalink) }>view on soundcloud</a>
|
||||
<a class="btn" href={ templ.URL("https://soundcloud.com/" + u.Permalink) } referrerpolicy="no-referrer" rel="external nofollow noopener noreferrer" target="_blank">view on soundcloud</a>
|
||||
</div>
|
||||
<br/>
|
||||
if len(p.Collection) != 0 {
|
||||
@@ -110,14 +108,14 @@ templ UserPlaylists(u sc.User, p *sc.Paginated[*sc.Playlist]) {
|
||||
}
|
||||
}
|
||||
|
||||
templ UserAlbums(u sc.User, p *sc.Paginated[*sc.Playlist]) {
|
||||
@UserBase(u)
|
||||
templ UserAlbums(prefs cfg.Preferences, u sc.User, p *sc.Paginated[*sc.Playlist]) {
|
||||
@UserBase(prefs, u)
|
||||
<div class="btns">
|
||||
<a class="btn" href={ templ.URL("/" + u.Permalink) }>tracks</a>
|
||||
<a class="btn" href={ templ.URL("/" + u.Permalink + "/sets") }>playlists</a>
|
||||
<a class="btn active">albums</a>
|
||||
<a class="btn" href={ templ.URL("/" + u.Permalink + "/reposts") }>reposts</a>
|
||||
<a class="btn" href={ templ.URL("https://soundcloud.com/" + u.Permalink) }>view on soundcloud</a>
|
||||
<a class="btn" href={ templ.URL("https://soundcloud.com/" + u.Permalink) } referrerpolicy="no-referrer" rel="external nofollow noopener noreferrer" target="_blank">view on soundcloud</a>
|
||||
</div>
|
||||
<br/>
|
||||
if len(p.Collection) != 0 {
|
||||
@@ -134,14 +132,14 @@ templ UserAlbums(u sc.User, p *sc.Paginated[*sc.Playlist]) {
|
||||
}
|
||||
}
|
||||
|
||||
templ UserReposts(u sc.User, p *sc.Paginated[*sc.Repost]) {
|
||||
@UserBase(u)
|
||||
templ UserReposts(prefs cfg.Preferences, u sc.User, p *sc.Paginated[*sc.Repost]) {
|
||||
@UserBase(prefs, u)
|
||||
<div class="btns">
|
||||
<a class="btn" href={ templ.URL("/" + u.Permalink) }>tracks</a>
|
||||
<a class="btn" href={ templ.URL("/" + u.Permalink + "/sets") }>playlists</a>
|
||||
<a class="btn" href={ templ.URL("/" + u.Permalink + "/albums") }>albums</a>
|
||||
<a class="btn active">reposts</a>
|
||||
<a class="btn" href={ templ.URL("https://soundcloud.com/" + u.Permalink) }>view on soundcloud</a>
|
||||
<a class="btn" href={ templ.URL("https://soundcloud.com/" + u.Permalink) } referrerpolicy="no-referrer" rel="external nofollow noopener noreferrer" target="_blank">view on soundcloud</a>
|
||||
</div>
|
||||
<br/>
|
||||
if len(p.Collection) != 0 {
|
||||
|
||||
Reference in New Issue
Block a user