mirror of
https://git.maid.zone/stuff/soundcloak.git
synced 2025-12-10 05:39:38 +05:00
API and many small fixes
This commit is contained in:
46
docs/API.md
Normal file
46
docs/API.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Check enabled features
|
||||
|
||||
Just go to `/_/info` endpoint
|
||||
|
||||
# API
|
||||
|
||||
To make use of it, instance must have API enabled of course. Currently, there is only one functionality present. If you are working on some cool project and wanna have more functionality here, let me know
|
||||
|
||||
## Searching
|
||||
|
||||
You can search with endpoint `/_/api/search`. Query parameters are:
|
||||
- `q`: the query
|
||||
- `type`: `users`, `tracks`, or `playlists`. Required
|
||||
- `pagination`: raw parameters to pass into soundcloud's api
|
||||
|
||||
The response is in JSON format. To go to next page, take the `next_href` from result, strip away everything until `?`, and pass that as `pagination` parameter.
|
||||
|
||||
For example: `/_/api/search?q=test&type=tracks` to search for `tracks` named `test`
|
||||
|
||||
# Other automation
|
||||
|
||||
Doesn't require API to be enabled
|
||||
|
||||
## Download songs
|
||||
|
||||
Restream must be enabled in the instance. The endpoint is `/_/restream/<author permalink>/<track permalink>`. Query parameters are:
|
||||
- `metadata`: `true` or `false`. If `true`, soundcloak will inject metadata (author, track cover, track title, etc) into the audio file, but this may take a little bit more time
|
||||
- `audio`: `best`, `aac`, `opus`, or `mpeg`. [Read more here](AUDIO_PRESETS.md)
|
||||
|
||||
For example: `/_/restream/lucybedroque/speakers?metadata=true&audio=opus` to get the `opus` audio with `metadata` for song `speakers` by author `lucybedroque`
|
||||
|
||||
## Get search suggestions
|
||||
|
||||
The endpoint is `/_/searchSuggestions`. Query parameters are:
|
||||
- `q`: the query
|
||||
|
||||
The response is a list of search suggestions as strings in JSON format.
|
||||
|
||||
For example: `/_/searchSuggestions?q=hi` to get search suggestions for `hi`
|
||||
|
||||
## Proxy images
|
||||
|
||||
ProxyImages must be enabled in the instance.
|
||||
|
||||
Endpoint for images: `/_/proxy/images`. Put image url into `url` query parameter. Of course, this only proxies images from soundcloud cdn
|
||||
|
||||
@@ -117,9 +117,10 @@ Some notes:
|
||||
| PlaylistCacheCleanDelay | PLAYLIST_CACHE_CLEAN_DELAY | 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 |
|
||||
| DNSCacheTTL | DNS_CACHE_TTL | 60 minutes | Time until DNS cache expires |
|
||||
| EnableAPI | ENABLE_API | false | Should [API](API.md) be enabled? |
|
||||
| Network | NETWORK | tcp4 | Network to listen on. Can be tcp4, tcp6 or unix |
|
||||
| Addr | ADDR | :4664 | Address and port (or socket path) for soundcloak to listen on |
|
||||
| UnixSocketPerms | UNIX_SOCKET_PERMS | 0775 | Permissions for unix socket (Network must be set to unix) on |
|
||||
| UnixSocketPerms | UNIX_SOCKET_PERMS | 0775 | Permissions for unix socket (Network must be set to unix) |
|
||||
| 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. |
|
||||
| TrustedProxies | TRUSTED_PROXIES | [] | List of IPs or IP ranges of trusted proxies |
|
||||
@@ -170,7 +171,7 @@ To get listed on [the instance list](https://maid.zone/soundcloak/instances.html
|
||||
Basic rules:
|
||||
|
||||
1. Do not collect user information (either yourself, or by including 3rd party tooling which does that)
|
||||
2. If you are modifying the source code, publish those changes somewhere. Even if it's just static files, it would be best to publish those changes somewhere.
|
||||
2. If you are modifying the source code, publish those changes somewhere.
|
||||
|
||||
Also, keep in mind that the instance list will periodically hit the `/_/info` endpoint on your instance (usually each 10 minutes) in order to display the instance settings. If you do not want this to happen, state it in your discussion/message, and I will exclude your instance from this checking.
|
||||
|
||||
|
||||
58
lib/api/init.go
Normal file
58
lib/api/init.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/url"
|
||||
|
||||
"git.maid.zone/stuff/soundcloak/lib/cfg"
|
||||
"git.maid.zone/stuff/soundcloak/lib/preferences"
|
||||
"git.maid.zone/stuff/soundcloak/lib/sc"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func Load(r *fiber.App) {
|
||||
r.Get("/_/api/search", func(c fiber.Ctx) error {
|
||||
prefs, err := preferences.Get(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q := cfg.B2s(c.RequestCtx().QueryArgs().Peek("q"))
|
||||
t := cfg.B2s(c.RequestCtx().QueryArgs().Peek("type"))
|
||||
args := cfg.B2s(c.RequestCtx().QueryArgs().Peek("pagination"))
|
||||
if args == "" {
|
||||
args = "?q=" + url.QueryEscape(q)
|
||||
}
|
||||
|
||||
switch t {
|
||||
case "tracks":
|
||||
p, err := sc.SearchTracks("", prefs, args)
|
||||
if err != nil {
|
||||
log.Printf("[API] error getting tracks for %s: %s\n", q, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(p)
|
||||
|
||||
case "users":
|
||||
p, err := sc.SearchUsers("", prefs, args)
|
||||
if err != nil {
|
||||
log.Printf("[API] error getting users for %s: %s\n", q, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(p)
|
||||
|
||||
case "playlists":
|
||||
p, err := sc.SearchPlaylists("", prefs, args)
|
||||
if err != nil {
|
||||
log.Printf("[API] error getting playlists for %s: %s\n", q, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(p)
|
||||
}
|
||||
|
||||
return c.SendStatus(404)
|
||||
})
|
||||
}
|
||||
@@ -69,6 +69,9 @@ var UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (K
|
||||
// time-to-live for dns cache
|
||||
var DNSCacheTTL = 60 * time.Minute
|
||||
|
||||
// enab;e api
|
||||
var EnableAPI = false
|
||||
|
||||
// // // some webserver configuration, put here to make it easier to configure what you need // // //
|
||||
// more info can be found here: https://docs.gofiber.io/api/fiber#config
|
||||
|
||||
@@ -387,6 +390,11 @@ func fromEnv() error {
|
||||
DNSCacheTTL = time.Duration(num) * time.Second
|
||||
}
|
||||
|
||||
env = os.Getenv("ENABLE_API")
|
||||
if env != "" {
|
||||
EnableAPI = boolean(env)
|
||||
}
|
||||
|
||||
env = os.Getenv("NETWORK")
|
||||
if env != "" {
|
||||
Network = env
|
||||
@@ -480,6 +488,7 @@ func init() {
|
||||
PlaylistCacheCleanDelay *time.Duration
|
||||
UserAgent *string
|
||||
DNSCacheTTL *time.Duration
|
||||
EnableAPI *bool
|
||||
Network *string
|
||||
Addr *string
|
||||
UnixSocketPerms *string
|
||||
@@ -547,6 +556,9 @@ func init() {
|
||||
if config.DNSCacheTTL != nil {
|
||||
DNSCacheTTL = *config.DNSCacheTTL * time.Second
|
||||
}
|
||||
if config.EnableAPI != nil {
|
||||
EnableAPI = *config.EnableAPI
|
||||
}
|
||||
if config.Network != nil {
|
||||
Network = *config.Network
|
||||
}
|
||||
|
||||
@@ -54,7 +54,6 @@ func Load(r *fiber.App) {
|
||||
|
||||
req.SetURI(parsed)
|
||||
req.Header.SetUserAgent(cfg.UserAgent)
|
||||
//req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd") images not big enough to be compressed
|
||||
|
||||
resp := fasthttp.AcquireResponse()
|
||||
//defer fasthttp.ReleaseResponse(resp) moved to proxyreader!!!
|
||||
|
||||
@@ -42,7 +42,6 @@ func Load(a *fiber.App) {
|
||||
|
||||
req.SetURI(parsed)
|
||||
req.Header.SetUserAgent(cfg.UserAgent)
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
|
||||
resp := fasthttp.AcquireResponse()
|
||||
//defer fasthttp.ReleaseResponse(resp)
|
||||
@@ -81,7 +80,6 @@ func Load(a *fiber.App) {
|
||||
|
||||
req.SetURI(parsed)
|
||||
req.Header.SetUserAgent(cfg.UserAgent)
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
|
||||
resp := fasthttp.AcquireResponse()
|
||||
|
||||
@@ -119,7 +117,6 @@ func Load(a *fiber.App) {
|
||||
|
||||
req.SetURI(parsed)
|
||||
req.Header.SetUserAgent(cfg.UserAgent)
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
|
||||
resp := fasthttp.AcquireResponse()
|
||||
defer fasthttp.ReleaseResponse(resp)
|
||||
@@ -170,7 +167,6 @@ func Load(a *fiber.App) {
|
||||
|
||||
req.SetURI(parsed)
|
||||
req.Header.SetUserAgent(cfg.UserAgent)
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
|
||||
resp := fasthttp.AcquireResponse()
|
||||
defer fasthttp.ReleaseResponse(resp)
|
||||
@@ -180,12 +176,7 @@ func Load(a *fiber.App) {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := resp.BodyUncompressed()
|
||||
if err != nil {
|
||||
data = resp.Body()
|
||||
}
|
||||
|
||||
var sp = bytes.Split(data, newline)
|
||||
var sp = bytes.Split(resp.Body(), newline)
|
||||
for i, l := range sp {
|
||||
if len(l) == 0 {
|
||||
continue
|
||||
|
||||
@@ -48,14 +48,19 @@ func Load(r *fiber.App) {
|
||||
}
|
||||
|
||||
var isDownload = string(c.RequestCtx().QueryArgs().Peek("metadata")) == "true"
|
||||
var quality *string
|
||||
if isDownload {
|
||||
quality = p.DownloadAudio
|
||||
var forcedQuality = c.RequestCtx().QueryArgs().Peek("audio")
|
||||
var quality string
|
||||
if len(forcedQuality) != 0 {
|
||||
quality = cfg.B2s(forcedQuality)
|
||||
} else {
|
||||
quality = p.RestreamAudio
|
||||
if isDownload {
|
||||
quality = *p.DownloadAudio
|
||||
} else {
|
||||
quality = *p.RestreamAudio
|
||||
}
|
||||
}
|
||||
|
||||
tr, audio := t.Media.SelectCompatible(*quality, true)
|
||||
tr, audio := t.Media.SelectCompatible(quality, true)
|
||||
if tr == nil {
|
||||
return fiber.ErrExpectationFailed
|
||||
}
|
||||
@@ -92,7 +97,6 @@ func Load(r *fiber.App) {
|
||||
|
||||
if t.Artwork != "" {
|
||||
r.req.SetRequestURI(t.Artwork)
|
||||
r.req.Header.Del("Accept-Encoding")
|
||||
|
||||
err := sc.DoWithRetry(misc.ImageClient, r.req, r.resp)
|
||||
if err != nil {
|
||||
@@ -100,7 +104,6 @@ func Load(r *fiber.App) {
|
||||
}
|
||||
|
||||
tag.AddAttachedPicture(id3v2.PictureFrame{MimeType: cfg.B2s(r.req.Header.ContentType()), Picture: r.req.Body(), PictureType: id3v2.PTFrontCover, Encoding: id3v2.EncodingUTF8})
|
||||
r.req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
}
|
||||
|
||||
var col collector
|
||||
@@ -115,17 +118,13 @@ func Load(r *fiber.App) {
|
||||
|
||||
req.SetRequestURI(u)
|
||||
req.Header.SetUserAgent(cfg.UserAgent)
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
|
||||
err := sc.DoWithRetry(misc.HlsClient, req, resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := resp.BodyUncompressed()
|
||||
if err != nil {
|
||||
data = resp.Body()
|
||||
}
|
||||
data := resp.Body()
|
||||
|
||||
res := make([]byte, 0, 1024*1024*1)
|
||||
for _, s := range bytes.Split(data, []byte{'\n'}) {
|
||||
@@ -139,10 +138,7 @@ func Load(r *fiber.App) {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err = resp.BodyUncompressed()
|
||||
if err != nil {
|
||||
data = resp.Body()
|
||||
}
|
||||
data = resp.Body()
|
||||
|
||||
res = append(res, data...)
|
||||
}
|
||||
@@ -161,7 +157,6 @@ func Load(r *fiber.App) {
|
||||
|
||||
if t.Artwork != "" {
|
||||
req.SetRequestURI(t.Artwork)
|
||||
req.Header.Del("Accept-Encoding")
|
||||
|
||||
err := sc.DoWithRetry(misc.ImageClient, req, resp)
|
||||
if err != nil {
|
||||
@@ -207,7 +202,6 @@ func Load(r *fiber.App) {
|
||||
|
||||
if t.Artwork != "" {
|
||||
r.req.SetRequestURI(t.Artwork)
|
||||
r.req.Header.Del("Accept-Encoding")
|
||||
|
||||
err := sc.DoWithRetry(misc.ImageClient, r.req, r.resp)
|
||||
if err != nil {
|
||||
@@ -221,7 +215,6 @@ func Load(r *fiber.App) {
|
||||
}
|
||||
|
||||
tag.SetCoverArt(&parsed)
|
||||
r.req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
}
|
||||
|
||||
var col collector
|
||||
|
||||
@@ -66,7 +66,6 @@ func (r *reader) Setup(url string, aac bool, duration *uint32) error {
|
||||
|
||||
r.req.SetRequestURI(url)
|
||||
r.req.Header.SetUserAgent(cfg.UserAgent)
|
||||
r.req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
|
||||
if aac {
|
||||
r.client = misc.HlsAacClient
|
||||
@@ -80,11 +79,6 @@ func (r *reader) Setup(url string, aac bool, duration *uint32) error {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := r.resp.BodyUncompressed()
|
||||
if err != nil {
|
||||
data = r.resp.Body()
|
||||
}
|
||||
|
||||
if r.parts == nil {
|
||||
misc.Log("make() r.parts")
|
||||
r.parts = make([][]byte, 0, defaultPartsCapacity)
|
||||
@@ -93,7 +87,7 @@ func (r *reader) Setup(url string, aac bool, duration *uint32) error {
|
||||
}
|
||||
if aac {
|
||||
// clone needed to mitigate memory skill issues here
|
||||
for _, s := range bytes.Split(data, []byte{'\n'}) {
|
||||
for _, s := range bytes.Split(r.resp.Body(), []byte{'\n'}) {
|
||||
if len(s) == 0 {
|
||||
continue
|
||||
}
|
||||
@@ -108,7 +102,7 @@ func (r *reader) Setup(url string, aac bool, duration *uint32) error {
|
||||
r.parts = append(r.parts, clone(s))
|
||||
}
|
||||
} else {
|
||||
for _, s := range bytes.Split(data, []byte{'\n'}) {
|
||||
for _, s := range bytes.Split(r.resp.Body(), []byte{'\n'}) {
|
||||
if len(s) == 0 || s[0] == '#' {
|
||||
continue
|
||||
}
|
||||
@@ -169,11 +163,7 @@ func (r *reader) Read(buf []byte) (n int, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := r.resp.BodyUncompressed()
|
||||
if err != nil {
|
||||
data = r.resp.Body()
|
||||
}
|
||||
|
||||
data := r.resp.Body()
|
||||
if r.index == 0 && r.duration != nil {
|
||||
fixDuration(data, r.duration) // I'm guessing that mvhd will always be in first part
|
||||
}
|
||||
|
||||
26
main.go
26
main.go
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.maid.zone/stuff/soundcloak/lib/api"
|
||||
"git.maid.zone/stuff/soundcloak/lib/misc"
|
||||
"github.com/a-h/templ"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
@@ -347,11 +348,16 @@ Disallow: /`)
|
||||
return err
|
||||
}
|
||||
|
||||
q := c.Query("q")
|
||||
t := c.Query("type")
|
||||
q := cfg.B2s(c.RequestCtx().QueryArgs().Peek("q"))
|
||||
t := cfg.B2s(c.RequestCtx().QueryArgs().Peek("type"))
|
||||
args := cfg.B2s(c.RequestCtx().QueryArgs().Peek("pagination"))
|
||||
if args == "" {
|
||||
args = "?q=" + url.QueryEscape(q)
|
||||
}
|
||||
|
||||
switch t {
|
||||
case "tracks":
|
||||
p, err := sc.SearchTracks("", prefs, c.Query("pagination", "?q="+url.QueryEscape(q)))
|
||||
p, err := sc.SearchTracks("", prefs, args)
|
||||
if err != nil {
|
||||
log.Printf("error getting tracks for %s: %s\n", q, err)
|
||||
return err
|
||||
@@ -360,7 +366,7 @@ Disallow: /`)
|
||||
return r(c, "tracks: "+q, templates.SearchTracks(p), nil)
|
||||
|
||||
case "users":
|
||||
p, err := sc.SearchUsers("", prefs, c.Query("pagination", "?q="+url.QueryEscape(q)))
|
||||
p, err := sc.SearchUsers("", prefs, args)
|
||||
if err != nil {
|
||||
log.Printf("error getting users for %s: %s\n", q, err)
|
||||
return err
|
||||
@@ -369,9 +375,9 @@ Disallow: /`)
|
||||
return r(c, "users: "+q, templates.SearchUsers(p), nil)
|
||||
|
||||
case "playlists":
|
||||
p, err := sc.SearchPlaylists("", prefs, c.Query("pagination", "?q="+url.QueryEscape(q)))
|
||||
p, err := sc.SearchPlaylists("", prefs, args)
|
||||
if err != nil {
|
||||
log.Printf("error getting users for %s: %s\n", q, err)
|
||||
log.Printf("error getting playlists for %s: %s\n", q, err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -417,8 +423,6 @@ Disallow: /`)
|
||||
return fiber.ErrNotFound
|
||||
}
|
||||
|
||||
//fmt.Println(c.Hostname(), c.Protocol(), c.IPs())
|
||||
|
||||
u, err := url.Parse(cfg.B2s(loc))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -544,6 +548,10 @@ Disallow: /`)
|
||||
proxystreams.Load(app)
|
||||
}
|
||||
|
||||
if cfg.EnableAPI {
|
||||
api.Load(app)
|
||||
}
|
||||
|
||||
if cfg.InstanceInfo {
|
||||
type info struct {
|
||||
Commit string
|
||||
@@ -553,6 +561,7 @@ Disallow: /`)
|
||||
Restream bool
|
||||
GetWebProfiles bool
|
||||
DefaultPreferences cfg.Preferences
|
||||
EnableAPI bool
|
||||
}
|
||||
|
||||
inf, err := json.Marshal(info{
|
||||
@@ -563,6 +572,7 @@ Disallow: /`)
|
||||
Restream: cfg.Restream,
|
||||
GetWebProfiles: cfg.GetWebProfiles,
|
||||
DefaultPreferences: cfg.DefaultPreferences,
|
||||
EnableAPI: cfg.EnableAPI,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalln("failed to marshal info: ", err)
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"git.maid.zone/stuff/soundcloak/lib/sc"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
templ UserHeader(u sc.User) {
|
||||
@@ -115,7 +114,7 @@ templ User(prefs cfg.Preferences, u sc.User, p *sc.Paginated[*sc.Track]) {
|
||||
}
|
||||
</div>
|
||||
if p.Next != "" && len(p.Collection) != int(u.Tracks) {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(strings.Split(p.Next, "/tracks")[1])) } rel="noreferrer">more tracks</a>
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.Next[sc.H+len("/users/")+len(u.ID)+len("/tracks"):])) } rel="noreferrer">more tracks</a>
|
||||
}
|
||||
} else {
|
||||
<span>no more tracks</span>
|
||||
@@ -133,7 +132,7 @@ templ UserPlaylists(prefs cfg.Preferences, u sc.User, p *sc.Paginated[*sc.Playli
|
||||
}
|
||||
</div>
|
||||
if p.Next != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(strings.Split(p.Next, "/playlists_without_albums")[1])) } rel="noreferrer">more playlists</a>
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.Next[sc.H+len("/users/")+len(u.ID)+len("/playlists_without_albums"):])) } rel="noreferrer">more playlists</a>
|
||||
}
|
||||
} else {
|
||||
<span>no more playlists</span>
|
||||
@@ -151,7 +150,7 @@ templ UserAlbums(prefs cfg.Preferences, u sc.User, p *sc.Paginated[*sc.Playlist]
|
||||
}
|
||||
</div>
|
||||
if p.Next != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(strings.Split(p.Next, "/albums")[1])) } rel="noreferrer">more albums</a>
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.Next[sc.H+len("/users/")+len(u.ID)+len("/albums"):])) } rel="noreferrer">more albums</a>
|
||||
}
|
||||
} else {
|
||||
<span>no more albums</span>
|
||||
@@ -173,7 +172,7 @@ templ UserReposts(prefs cfg.Preferences, u sc.User, p *sc.Paginated[*sc.Repost])
|
||||
}
|
||||
</div>
|
||||
if p.Next != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(strings.Split(p.Next, "/reposts")[1])) } rel="noreferrer">more reposts</a>
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.Next[sc.H+len("/stream/users/")+len(u.ID)+len("/reposts"):])) } rel="noreferrer">more reposts</a>
|
||||
}
|
||||
} else {
|
||||
<span>no more reposts</span>
|
||||
@@ -195,7 +194,7 @@ templ UserLikes(prefs cfg.Preferences, u sc.User, p *sc.Paginated[*sc.Like]) {
|
||||
}
|
||||
</div>
|
||||
if p.Next != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(strings.Split(p.Next, "/likes")[1])) } rel="noreferrer">more likes</a>
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.Next[sc.H+len("/users/")+len(u.ID)+len("/likes"):])) } rel="noreferrer">more likes</a>
|
||||
}
|
||||
} else {
|
||||
<span>no more likes</span>
|
||||
@@ -243,7 +242,7 @@ templ UserFollowers(prefs cfg.Preferences, u sc.User, p *sc.Paginated[*sc.User])
|
||||
}
|
||||
</div>
|
||||
if p.Next != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(strings.Split(p.Next, "/followers")[1])) } rel="noreferrer">more users</a>
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.Next[sc.H+len("/users/")+len(u.ID)+len("/followers"):])) } rel="noreferrer">more users</a>
|
||||
}
|
||||
} else {
|
||||
<span>no more users</span>
|
||||
@@ -261,7 +260,7 @@ templ UserFollowing(prefs cfg.Preferences, u sc.User, p *sc.Paginated[*sc.User])
|
||||
}
|
||||
</div>
|
||||
if p.Next != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(strings.Split(p.Next, "/following")[1])) } rel="noreferrer">more users</a>
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.Next[sc.H+len("/users/")+len(u.ID)+len("/followings"):])) } rel="noreferrer">more users</a>
|
||||
}
|
||||
} else {
|
||||
<span>no more users</span>
|
||||
@@ -281,7 +280,7 @@ templ SearchUsers(p *sc.Paginated[*sc.User]) {
|
||||
@UserItem(user)
|
||||
}
|
||||
if p.Next != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?type=users&pagination=" + url.QueryEscape(strings.Split(p.Next, "/users")[1])) } rel="noreferrer">more users</a>
|
||||
<a class="btn" href={ templ.SafeURL("?type=users&pagination=" + url.QueryEscape(p.Next[sc.H+len("/search/users"):])) } rel="noreferrer">more users</a>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user