mirror of
https://git.maid.zone/stuff/soundcloak.git
synced 2026-03-22 00:28:12 +05:00
it's so simple
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
ARG GO_VERSION=1.25.5
|
||||
ARG GO_VERSION=1.25.6
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build
|
||||
ARG TARGETOS
|
||||
|
||||
28
go.mod
28
go.mod
@@ -1,18 +1,16 @@
|
||||
module git.maid.zone/stuff/soundcloak
|
||||
|
||||
go 1.25.5
|
||||
go 1.25.6
|
||||
|
||||
require (
|
||||
github.com/a-h/templ v0.3.960
|
||||
github.com/a-h/templ v0.3.977
|
||||
github.com/bogem/id3v2/v2 v2.1.4
|
||||
github.com/dlclark/regexp2 v1.11.5-0.20240806004527-5bbbed8ea10b
|
||||
github.com/gcottom/mp4meta v0.0.5
|
||||
github.com/gcottom/oggmeta v0.0.8
|
||||
github.com/goccy/go-json v0.10.5
|
||||
github.com/goccy/go-json v0.10.6-0.20251028001429-e4877d51d546
|
||||
github.com/gofiber/fiber/v3 v3.0.0-rc.3
|
||||
github.com/gorilla/feeds v1.2.0
|
||||
github.com/valyala/fasthttp v1.68.0
|
||||
golang.org/x/net v0.48.0
|
||||
github.com/valyala/fasthttp v1.69.0
|
||||
golang.org/x/net v0.49.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -27,23 +25,23 @@ require (
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gofiber/schema v1.6.0 // indirect
|
||||
github.com/gofiber/utils/v2 v2.0.0-rc.4 // indirect
|
||||
github.com/gofiber/utils/v2 v2.0.0-rc.6 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/natefinch/atomic v1.0.1 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/sunfish-shogi/bufseekio v0.1.0 // indirect
|
||||
github.com/tinylib/msgp v1.6.1 // indirect
|
||||
github.com/tinylib/msgp v1.6.3 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
)
|
||||
|
||||
tool (
|
||||
|
||||
58
go.sum
58
go.sum
@@ -2,8 +2,8 @@ git.maid.zone/stuff/soundcloakctl v0.0.0-20251212193418-f85f457a0df1 h1:Ksv7MuRY
|
||||
git.maid.zone/stuff/soundcloakctl v0.0.0-20251212193418-f85f457a0df1/go.mod h1:teXAWdTDQ3CXlXy8Co+U3Jai2LuxFLgjPy7LnguYoiY=
|
||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
|
||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
|
||||
github.com/a-h/templ v0.3.960 h1:trshEpGa8clF5cdI39iY4ZrZG8Z/QixyzEyUnA7feTM=
|
||||
github.com/a-h/templ v0.3.960/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
||||
github.com/a-h/templ v0.3.977 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg=
|
||||
github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
||||
github.com/abema/go-mp4 v1.4.1 h1:YoS4VRqd+pAmddRPLFf8vMk74kuGl6ULSjzhsIqwr6M=
|
||||
github.com/abema/go-mp4 v1.4.1/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=
|
||||
github.com/aler9/writerseeker v1.1.0 h1:t+Sm3tjp8scNlqyoa8obpeqwciMNOvdvsxjxEb3Sx3g=
|
||||
@@ -32,33 +32,25 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gcottom/mp4meta v0.0.5 h1:pZZAMwRMisx7RaewO7MvjuD3t+tHCYRujpJkQI2yVHU=
|
||||
github.com/gcottom/mp4meta v0.0.5/go.mod h1:Dxt8rM1fJDl9sOJCfsnQuprE3gWtmE/oXTJHA/g5WHY=
|
||||
github.com/gcottom/oggmeta v0.0.8 h1:cai8PX7k4/6coKaYCeBZI5GD2f+bgzrCHbcFM5l5Vms=
|
||||
github.com/gcottom/oggmeta v0.0.8/go.mod h1:as5q4K3n3GHJIuWKoJPjiroxPEtqDCNB52+sYzBSaGQ=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-json v0.10.6-0.20251028001429-e4877d51d546 h1:FcZnXbn+88+xAIR8RLuIeIdT3Vnn5WSpQk+gzdRNlg8=
|
||||
github.com/goccy/go-json v0.10.6-0.20251028001429-e4877d51d546/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/gofiber/fiber/v3 v3.0.0-rc.3 h1:h0KXuRHbivSslIpoHD1R/XjUsjcGwt+2vK0avFiYonA=
|
||||
github.com/gofiber/fiber/v3 v3.0.0-rc.3/go.mod h1:LNBPuS/rGoUFlOyy03fXsWAeWfdGoT1QytwjRVNSVWo=
|
||||
github.com/gofiber/schema v1.6.0 h1:rAgVDFwhndtC+hgV7Vu5ItQCn7eC2mBA4Eu1/ZTiEYY=
|
||||
github.com/gofiber/schema v1.6.0/go.mod h1:WNZWpQx8LlPSK7ZaX0OqOh+nQo/eW2OevsXs1VZfs/s=
|
||||
github.com/gofiber/utils/v2 v2.0.0-rc.4 h1:CDjwPwtwwj1OTIf6v3iRk+D2wcdjUzwk91Ghu2TMNbE=
|
||||
github.com/gofiber/utils/v2 v2.0.0-rc.4/go.mod h1:gXins5o7up+BQFiubmO8aUJc/+Mhd7EKXIiAK5GBomI=
|
||||
github.com/gofiber/utils/v2 v2.0.0-rc.6 h1:pBAbppiFMR+BpdEwjnZDMpnH0rBreDUPWjolUVe6BVY=
|
||||
github.com/gofiber/utils/v2 v2.0.0-rc.6/go.mod h1:8PuWXERC3IoTmoD2Fp/X7amJntq928Fa2yTHI5Orj2M=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc=
|
||||
github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
@@ -73,8 +65,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/shamaton/msgpack/v2 v2.4.0 h1:O5Z08MRmbo0lA9o2xnQ4TXx6teJbPqEurqcCOQ8Oi/4=
|
||||
github.com/shamaton/msgpack/v2 v2.4.0/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -86,12 +76,12 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD
|
||||
github.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I=
|
||||
github.com/sunfish-shogi/bufseekio v0.1.0 h1:zu38kFbv0KuuiwZQeuYeS02U9AM14j0pVA9xkHOCJ2A=
|
||||
github.com/sunfish-shogi/bufseekio v0.1.0/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I=
|
||||
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
|
||||
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
|
||||
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
||||
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
@@ -99,16 +89,16 @@ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3i
|
||||
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/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
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/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
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/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
@@ -120,21 +110,21 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
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.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
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/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -2,12 +2,12 @@ package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/url"
|
||||
|
||||
"git.maid.zone/stuff/soundcloak/lib/cfg"
|
||||
"git.maid.zone/stuff/soundcloak/lib/sc"
|
||||
json "github.com/goccy/go-json"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
func Load(a *fiber.App) {
|
||||
@@ -15,18 +15,18 @@ func Load(a *fiber.App) {
|
||||
|
||||
prefs := cfg.Preferences{ProxyImages: &cfg.False}
|
||||
r.Get("/search", func(c fiber.Ctx) error {
|
||||
q := cfg.B2s(c.RequestCtx().QueryArgs().Peek("q"))
|
||||
q := 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)
|
||||
args := c.RequestCtx().QueryArgs().Peek("pagination")
|
||||
if len(args) == 0 {
|
||||
args = fasthttp.AppendQuotedArg([]byte("q="), 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)
|
||||
log.Printf("[API] error getting tracks for %s: %s\n", cfg.B2s(q), err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func Load(a *fiber.App) {
|
||||
case "users":
|
||||
p, err := sc.SearchUsers("", prefs, args)
|
||||
if err != nil {
|
||||
log.Printf("[API] error getting users for %s: %s\n", q, err)
|
||||
log.Printf("[API] error getting users for %s: %s\n", cfg.B2s(q), err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func Load(a *fiber.App) {
|
||||
case "playlists":
|
||||
p, err := sc.SearchPlaylists("", prefs, args)
|
||||
if err != nil {
|
||||
log.Printf("[API] error getting playlists for %s: %s\n", q, err)
|
||||
log.Printf("[API] error getting playlists for %s: %s\n", cfg.B2s(q), err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ var PlaylistTTL = 20 * time.Minute
|
||||
var PlaylistCacheCleanDelay = PlaylistTTL / 4
|
||||
|
||||
// default fasthttp one was causing connections to be stuck? todo make it cycle browser useragents or just choose random at startup
|
||||
var UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36"
|
||||
var UserAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:146.0) Gecko/20100101 Firefox/146.0"
|
||||
|
||||
// override the extractor
|
||||
var ClientID = ""
|
||||
@@ -72,41 +72,7 @@ var ClientID = ""
|
||||
// enab;e api
|
||||
var EnableAPI = false
|
||||
|
||||
// Dear soundcloud workers who are for sure reading this:
|
||||
// please keep this image in your mind
|
||||
// . ,+@#@=--__, __+##@@@#==,
|
||||
// . @* @#m, __==#@ #
|
||||
// . @ @#m_, __+#@ %
|
||||
// . @ #@*@#* %
|
||||
// . @ __--##@@@#---__ @
|
||||
// . #, m#@@* *%@, ,#
|
||||
// . @ ___,@ *@+---___ @
|
||||
// . %#__==#@ ,* %@, *==%,
|
||||
// . ,=@ @ @ *@=,
|
||||
// . ,#@ ,% ,_ @, %,
|
||||
// ,@* @* ,m#@ ,@ @, *# @,
|
||||
// # __---@ ,@ @ .@ @, *@---__ #
|
||||
// @ .@ .@ --____@ .%_____-- @ @. @
|
||||
// .@ # @ @, @ @. @ #
|
||||
// . % :@ @ @, @ # @: ,@
|
||||
// . @, @ ,* @@@****----- *@-----****@@@ % @: ,%
|
||||
// . *_, @ @ @ @ @ @ @ @ @ @ ,@
|
||||
// . *+# @ @** @ @** @ @ @+*^
|
||||
// . @, @ # # # @ @ .#
|
||||
// . # #: *@__@* *@__#* ,# @
|
||||
// . %*, %. @ ,#@
|
||||
// . @ @, @_ m m _* @ @
|
||||
// . @ @ *-___# ^+___+^ #___+#* #
|
||||
// . #__+* @ @ %, , @ %
|
||||
// . @ # @_, ,_@* # # @
|
||||
// . *, @ *@##--___--##@* ,* @ *-__#
|
||||
// . ^--=* @ @ @ @ @ ,#
|
||||
// . :@ ,+@@@+, @: #==-
|
||||
// . ==@ @ @ @==
|
||||
// . ,# @ @ @,
|
||||
// . @ % % %
|
||||
// . *==* @==#*
|
||||
// cirno day everyday
|
||||
// yeah i doubt they will be reading this lolol
|
||||
var SoundcloudApiProxy = ""
|
||||
|
||||
var DialDualStack = false
|
||||
|
||||
@@ -27,10 +27,14 @@ var Repo = "unknown"
|
||||
var CommitURL = "unknown"
|
||||
|
||||
const (
|
||||
// Downloads the HLS stream on the backend, and restreams it to frontend as a file. Requires no JS, but less stable client-side
|
||||
// Downloads the HLS stream on the backend, and restreams it to frontend as a file.
|
||||
// If requested MP3 preset, it uses Progressive protocol (so just proxying a file, maybe adding metadata if you need it)
|
||||
// Requires no JS, but less stable client-side (browser likes to randomly unload the audio if you listen to shit on repeat xd)
|
||||
RestreamPlayer string = "restream"
|
||||
// Downloads the HLS stream on the frontend (proxying can be enabled). Requires JS, more stable client-side
|
||||
HLSPlayer string = "hls"
|
||||
// Just proxies the file given from Progressive stream, only available when restream is not, also just MP3 preset
|
||||
ProgressivePlayer string = "progressive"
|
||||
// Disables the song player
|
||||
NonePlayer string = "none"
|
||||
)
|
||||
@@ -56,6 +60,9 @@ const (
|
||||
AudioMP3 string = "mpeg"
|
||||
)
|
||||
|
||||
// for taking ptrs :)
|
||||
var MP3 = AudioMP3
|
||||
|
||||
type Preferences struct {
|
||||
Player *string
|
||||
ProxyStreams *bool
|
||||
|
||||
@@ -45,27 +45,16 @@ func Log(what ...any) {
|
||||
}
|
||||
}
|
||||
|
||||
var ImageClient *fasthttp.HostClient
|
||||
var HlsClient *fasthttp.HostClient
|
||||
var HlsStreamingOnlyClient *fasthttp.HostClient
|
||||
var HlsAacClient *fasthttp.HostClient
|
||||
|
||||
func init() {
|
||||
if cfg.Restream || cfg.ProxyImages {
|
||||
ImageClient = &fasthttp.HostClient{
|
||||
Addr: cfg.ImageCDN + ":443",
|
||||
IsTLS: true,
|
||||
MaxIdleConnDuration: cfg.MaxIdleConnDuration,
|
||||
StreamResponseBody: true,
|
||||
DialDualStack: cfg.DialDualStack,
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Restream || cfg.ProxyStreams {
|
||||
HlsClient = &fasthttp.HostClient{
|
||||
Addr: cfg.HLSCDN + ":443",
|
||||
IsTLS: true,
|
||||
MaxIdleConnDuration: cfg.MaxIdleConnDuration,
|
||||
StreamResponseBody: true,
|
||||
DialDualStack: cfg.DialDualStack,
|
||||
}
|
||||
|
||||
@@ -73,7 +62,15 @@ func init() {
|
||||
Addr: cfg.HLSAACCDN + ":443",
|
||||
IsTLS: true,
|
||||
MaxIdleConnDuration: cfg.MaxIdleConnDuration,
|
||||
DialDualStack: cfg.DialDualStack,
|
||||
}
|
||||
|
||||
HlsStreamingOnlyClient = &fasthttp.HostClient{
|
||||
Addr: cfg.HLSCDN + ":443",
|
||||
IsTLS: true,
|
||||
MaxIdleConnDuration: cfg.MaxIdleConnDuration,
|
||||
StreamResponseBody: true,
|
||||
MaxResponseBodySize: 1,
|
||||
DialDualStack: cfg.DialDualStack,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
encodingjson "encoding/json"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/valyala/fasthttp"
|
||||
|
||||
@@ -175,7 +177,7 @@ func Load(r *fiber.App) {
|
||||
old.ShowAudio = &cfg.False
|
||||
}
|
||||
|
||||
if *old.Player == cfg.HLSPlayer {
|
||||
if *old.Player == cfg.HLSPlayer || *old.Player == cfg.ProgressivePlayer {
|
||||
if cfg.ProxyStreams {
|
||||
switch p.ProxyStreams {
|
||||
case on:
|
||||
@@ -184,7 +186,9 @@ func Load(r *fiber.App) {
|
||||
old.ProxyStreams = &cfg.False
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if *old.Player == cfg.HLSPlayer {
|
||||
switch p.FullyPreloadTrack {
|
||||
case on:
|
||||
old.FullyPreloadTrack = &cfg.True
|
||||
@@ -265,7 +269,15 @@ func Load(r *fiber.App) {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(Export{Preferences: &p})
|
||||
data, err := encodingjson.Marshal(Export{Preferences: &p})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Response().Header.SetContentType("application/json")
|
||||
|
||||
return c.Send(data)
|
||||
// go-json seems to crash for this one :p
|
||||
})
|
||||
|
||||
r.Post("/_/preferences/import", func(c fiber.Ctx) error {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package proxyimages
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"git.maid.zone/stuff/soundcloak/lib/cfg"
|
||||
"git.maid.zone/stuff/soundcloak/lib/misc"
|
||||
"git.maid.zone/stuff/soundcloak/lib/sc"
|
||||
@@ -11,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
var al_httpc *fasthttp.HostClient
|
||||
var sndcdn = []byte(".sndcdn.com")
|
||||
var streaming_httpc *fasthttp.HostClient
|
||||
|
||||
func Load(r *fiber.App) {
|
||||
|
||||
@@ -20,6 +18,15 @@ func Load(r *fiber.App) {
|
||||
IsTLS: true,
|
||||
MaxIdleConnDuration: cfg.MaxIdleConnDuration,
|
||||
StreamResponseBody: true,
|
||||
MaxResponseBodySize: 1,
|
||||
}
|
||||
|
||||
streaming_httpc = &fasthttp.HostClient{
|
||||
Addr: cfg.ImageCDN + ":443",
|
||||
IsTLS: true,
|
||||
MaxIdleConnDuration: cfg.MaxIdleConnDuration,
|
||||
StreamResponseBody: true,
|
||||
MaxResponseBodySize: 1,
|
||||
DialDualStack: cfg.DialDualStack,
|
||||
}
|
||||
|
||||
@@ -37,14 +44,15 @@ func Load(r *fiber.App) {
|
||||
return err
|
||||
}
|
||||
|
||||
if !bytes.HasSuffix(parsed.Host(), sndcdn) {
|
||||
const x = ".sndcdn.com"
|
||||
if h := parsed.Host(); len(h) > len(x) && string(h[len(h)-len(x):]) != x {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
var cl *fasthttp.HostClient
|
||||
if parsed.Host()[0] == 'i' {
|
||||
parsed.SetHost(cfg.ImageCDN)
|
||||
cl = misc.ImageClient
|
||||
cl = streaming_httpc
|
||||
} else if string(parsed.Host()[:2]) == "al" {
|
||||
cl = al_httpc
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package proxystreams
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/url"
|
||||
|
||||
"git.maid.zone/stuff/soundcloak/lib/cfg"
|
||||
"git.maid.zone/stuff/soundcloak/lib/misc"
|
||||
@@ -11,12 +10,23 @@ import (
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
var sndcdn = []byte(".sndcdn.com")
|
||||
var soundcloudcloud = []byte(".soundcloud.cloud")
|
||||
const sndcdn = ".sndcdn.com"
|
||||
const soundcloudcloud = "soundcloud.cloud"
|
||||
|
||||
var newline = []byte{'\n'}
|
||||
var extxmap = []byte(`#EXT-X-MAP:URI="`)
|
||||
|
||||
var hls_aac_streaming_httpc *fasthttp.HostClient
|
||||
|
||||
func Load(a *fiber.App) {
|
||||
hls_aac_streaming_httpc = &fasthttp.HostClient{
|
||||
Addr: cfg.HLSAACCDN + ":443",
|
||||
IsTLS: true,
|
||||
MaxIdleConnDuration: cfg.MaxIdleConnDuration,
|
||||
StreamResponseBody: true,
|
||||
MaxResponseBodySize: 1,
|
||||
DialDualStack: cfg.DialDualStack,
|
||||
}
|
||||
|
||||
r := a.Group("/_/proxy/streams")
|
||||
|
||||
r.Get("/", func(c fiber.Ctx) error {
|
||||
@@ -33,7 +43,7 @@ func Load(a *fiber.App) {
|
||||
return err
|
||||
}
|
||||
|
||||
if !bytes.HasSuffix(parsed.Host(), sndcdn) {
|
||||
if h := parsed.Host(); len(h) > len(sndcdn) && string(h[len(h)-len(sndcdn):]) != sndcdn {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
@@ -46,7 +56,7 @@ func Load(a *fiber.App) {
|
||||
resp := fasthttp.AcquireResponse()
|
||||
//defer fasthttp.ReleaseResponse(resp)
|
||||
|
||||
err = sc.DoWithRetry(misc.HlsClient, req, resp)
|
||||
err = sc.DoWithRetry(misc.HlsStreamingOnlyClient, req, resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -71,7 +81,7 @@ func Load(a *fiber.App) {
|
||||
return err
|
||||
}
|
||||
|
||||
if !bytes.HasSuffix(parsed.Host(), soundcloudcloud) {
|
||||
if h := parsed.Host(); len(h) > len(soundcloudcloud) && string(h[len(h)-len(soundcloudcloud):]) != soundcloudcloud {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
@@ -83,7 +93,7 @@ func Load(a *fiber.App) {
|
||||
|
||||
resp := fasthttp.AcquireResponse()
|
||||
|
||||
err = sc.DoWithRetry(misc.HlsAacClient, req, resp)
|
||||
err = sc.DoWithRetry(hls_aac_streaming_httpc, req, resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -108,7 +118,8 @@ func Load(a *fiber.App) {
|
||||
return err
|
||||
}
|
||||
|
||||
if !bytes.HasSuffix(parsed.Host(), sndcdn) {
|
||||
const x = ".sndcdn.com"
|
||||
if h := parsed.Host(); len(h) > len(x) && string(h[len(h)-len(x):]) != x {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
@@ -126,22 +137,19 @@ func Load(a *fiber.App) {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := resp.BodyUncompressed()
|
||||
if err != nil {
|
||||
data = resp.Body()
|
||||
}
|
||||
|
||||
var sp = bytes.Split(data, newline)
|
||||
for i, l := range sp {
|
||||
for l := range bytes.SplitSeq(resp.Body(), newline) {
|
||||
if len(l) == 0 || l[0] == '#' {
|
||||
c.Response().AppendBody(l)
|
||||
c.Response().AppendBody(newline)
|
||||
continue
|
||||
}
|
||||
|
||||
l = []byte("/_/proxy/streams?url=" + url.QueryEscape(cfg.B2s(l)))
|
||||
sp[i] = l
|
||||
c.Response().AppendBodyString("/_/proxy/streams?url=")
|
||||
c.Response().AppendBody(fasthttp.AppendQuotedArg(nil, l))
|
||||
c.Response().AppendBody(newline)
|
||||
}
|
||||
|
||||
return c.Send(bytes.Join(sp, newline))
|
||||
return nil
|
||||
})
|
||||
|
||||
r.Get("/playlist/aac", func(c fiber.Ctx) error {
|
||||
@@ -158,7 +166,8 @@ func Load(a *fiber.App) {
|
||||
return err
|
||||
}
|
||||
|
||||
if !bytes.HasSuffix(parsed.Host(), soundcloudcloud) {
|
||||
const x = ".soundcloud.cloud"
|
||||
if h := parsed.Host(); len(h) > len(x) && string(h[len(h)-len(x):]) != x {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
@@ -176,25 +185,32 @@ func Load(a *fiber.App) {
|
||||
return err
|
||||
}
|
||||
|
||||
var sp = bytes.Split(resp.Body(), newline)
|
||||
for i, l := range sp {
|
||||
for l := range bytes.SplitSeq(resp.Body(), newline) {
|
||||
if len(l) == 0 {
|
||||
c.Response().AppendBody(l)
|
||||
c.Response().AppendBody(newline)
|
||||
continue
|
||||
}
|
||||
|
||||
if l[0] == '#' {
|
||||
if bytes.HasPrefix(l, extxmap) {
|
||||
l = []byte(`#EXT-X-MAP:URI="/_/proxy/streams/aac?url=` + url.QueryEscape(cfg.B2s(l[16:len(l)-1])) + `"`)
|
||||
sp[i] = l
|
||||
// #EXT-X-MAP:URI="..."
|
||||
const x = `#EXT-X-MAP:URI="`
|
||||
if len(l) > len(x) && string(l[:len(x)]) == x {
|
||||
c.Response().AppendBodyString(`#EXT-X-MAP:URI="/_/proxy/streams/aac?url=`)
|
||||
c.Response().AppendBody(fasthttp.AppendQuotedArg(nil, l[16:len(l)-1]))
|
||||
c.Response().AppendBodyString(`"`)
|
||||
} else {
|
||||
c.Response().AppendBody(l)
|
||||
}
|
||||
|
||||
c.Response().AppendBody(newline)
|
||||
continue
|
||||
}
|
||||
|
||||
l = []byte("/_/proxy/streams/aac?url=" + url.QueryEscape(cfg.B2s(l)))
|
||||
sp[i] = l
|
||||
c.Response().AppendBodyString("/_/proxy/streams/aac?url=")
|
||||
c.Response().AppendBody(fasthttp.AppendQuotedArg(nil, l))
|
||||
c.Response().AppendBody(newline)
|
||||
}
|
||||
|
||||
return c.Send(bytes.Join(sp, newline))
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package restream
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"image"
|
||||
"strings"
|
||||
|
||||
@@ -14,21 +16,33 @@ import (
|
||||
"git.maid.zone/stuff/soundcloak/lib/sc"
|
||||
"github.com/bogem/id3v2/v2"
|
||||
"github.com/gcottom/mp4meta"
|
||||
"github.com/gcottom/oggmeta"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
var image_httpc *fasthttp.HostClient
|
||||
var crcTable [256]uint32
|
||||
|
||||
func Load(r *fiber.App) {
|
||||
for i := range crcTable {
|
||||
r := uint32(i) << 24
|
||||
for j := 0; j < 8; j++ {
|
||||
if r&0x80000000 != 0 {
|
||||
r = (r << 1) ^ 0x04c11db7
|
||||
} else {
|
||||
r <<= 1
|
||||
}
|
||||
}
|
||||
crcTable[i] = r
|
||||
}
|
||||
|
||||
image_httpc = &fasthttp.HostClient{
|
||||
Addr: cfg.ImageCDN + ":443",
|
||||
IsTLS: true,
|
||||
MaxIdleConnDuration: cfg.MaxIdleConnDuration,
|
||||
DialDualStack: cfg.DialDualStack,
|
||||
}
|
||||
|
||||
r.Get("/_/restream/:author/:track", func(c fiber.Ctx) error {
|
||||
p, err := preferences.Get(c)
|
||||
if err != nil {
|
||||
@@ -60,7 +74,20 @@ func Load(r *fiber.App) {
|
||||
}
|
||||
}
|
||||
|
||||
tr, audio := t.Media.SelectCompatible(quality, true)
|
||||
if isDownload {
|
||||
var s []byte
|
||||
if s = c.RequestCtx().QueryArgs().Peek("title"); len(s) > 0 {
|
||||
t.Title = cfg.B2s(s)
|
||||
}
|
||||
if s = c.RequestCtx().QueryArgs().Peek("genre"); len(s) > 0 {
|
||||
t.Genre = cfg.B2s(s)
|
||||
}
|
||||
if s = c.RequestCtx().QueryArgs().Peek("author"); len(s) > 0 {
|
||||
t.Author.Username = cfg.B2s(s)
|
||||
}
|
||||
}
|
||||
|
||||
tr, audio := t.Media.SelectCompatible(quality, true, true)
|
||||
if tr == nil {
|
||||
return fiber.ErrExpectationFailed
|
||||
}
|
||||
@@ -72,6 +99,7 @@ func Load(r *fiber.App) {
|
||||
|
||||
c.Response().Header.SetContentType(tr.Format.MimeType)
|
||||
c.Set("Cache-Control", cfg.RestreamCacheControl)
|
||||
c.Set("Content-Disposition", `attachment; filename="`+t.Permalink+"."+sc.ToExt(audio)+`"`)
|
||||
|
||||
if isDownload {
|
||||
if t.Artwork != "" {
|
||||
@@ -80,11 +108,10 @@ func Load(r *fiber.App) {
|
||||
|
||||
switch audio {
|
||||
case cfg.AudioMP3:
|
||||
r := acquireReader()
|
||||
err := r.Setup(u, false, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := fasthttp.AcquireRequest()
|
||||
resp := fasthttp.AcquireResponse()
|
||||
|
||||
req.Header.SetUserAgent(cfg.UserAgent)
|
||||
|
||||
tag := id3v2.NewEmptyTag()
|
||||
|
||||
@@ -95,80 +122,254 @@ func Load(r *fiber.App) {
|
||||
|
||||
tag.SetTitle(t.Title)
|
||||
|
||||
if t.Artwork != "" {
|
||||
r.req.SetRequestURI(t.Artwork)
|
||||
|
||||
err := sc.DoWithRetry(misc.ImageClient, r.req, r.resp)
|
||||
if err == nil && r.resp.StatusCode() == 200 {
|
||||
tag.AddAttachedPicture(id3v2.PictureFrame{MimeType: cfg.B2s(r.req.Header.ContentType()), Picture: r.req.Body(), PictureType: id3v2.PTFrontCover, Encoding: id3v2.EncodingUTF8})
|
||||
}
|
||||
}
|
||||
|
||||
var col collector
|
||||
tag.WriteTo(&col)
|
||||
r.leftover = col.data
|
||||
|
||||
return c.SendStream(r)
|
||||
case cfg.AudioOpus: // might try to fuck around with metadata injection. Dynamically injecting metadata for opus wasn't really good idea as it breaks some things :P
|
||||
req := fasthttp.AcquireRequest()
|
||||
resp := fasthttp.AcquireResponse()
|
||||
|
||||
defer fasthttp.ReleaseRequest(req)
|
||||
defer fasthttp.ReleaseResponse(resp)
|
||||
|
||||
req.SetRequestURI(u)
|
||||
req.Header.SetUserAgent(cfg.UserAgent)
|
||||
|
||||
err := sc.DoWithRetry(misc.HlsClient, req, resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parts := make([][]byte, 0, defaultPartsCapacity)
|
||||
for _, s := range bytes.Split(resp.Body(), newline) {
|
||||
if len(s) == 0 || s[0] == '#' {
|
||||
continue
|
||||
}
|
||||
|
||||
parts = append(parts, clone(s))
|
||||
}
|
||||
|
||||
res := make([]byte, 0, 1024*1024*1)
|
||||
for _, s := range parts {
|
||||
req.SetRequestURIBytes(s)
|
||||
err := sc.DoWithRetry(misc.HlsClient, req, resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res = append(res, resp.Body()...)
|
||||
}
|
||||
|
||||
tag, err := oggmeta.ReadOGG(bytes.NewReader(res))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tag.SetArtist(t.Author.Username)
|
||||
if t.Genre != "" {
|
||||
tag.SetGenre(t.Genre)
|
||||
}
|
||||
|
||||
tag.SetTitle(t.Title)
|
||||
|
||||
if t.Artwork != "" {
|
||||
req.SetRequestURI(t.Artwork)
|
||||
|
||||
err := sc.DoWithRetry(misc.ImageClient, req, resp)
|
||||
err := sc.DoWithRetry(image_httpc, req, resp)
|
||||
if err == nil && resp.StatusCode() == 200 {
|
||||
parsed, _, err := image.Decode(resp.BodyStream())
|
||||
if err == nil {
|
||||
tag.SetCoverArt(&parsed)
|
||||
}
|
||||
//fmt.Println(string(resp.Header.ContentType()), string(resp.Header.Peek("Content-Encoding")), len(resp.Body()))
|
||||
tag.AddAttachedPicture(id3v2.PictureFrame{MimeType: cfg.B2s(resp.Header.ContentType()), Picture: resp.Body(), PictureType: id3v2.PTFrontCover, Encoding: id3v2.EncodingUTF8})
|
||||
}
|
||||
}
|
||||
|
||||
return tag.Save(c.Response().BodyWriter())
|
||||
if tr.Format.Protocol == sc.ProtocolProgressive {
|
||||
r := acquireInjector()
|
||||
tag.WriteTo(r) // write out tag first because the buffers will be overwritten if you reuse the req/resp
|
||||
|
||||
req.SetRequestURI(u)
|
||||
// enforce streaming here!!
|
||||
err := sc.DoWithRetry(misc.HlsStreamingOnlyClient, req, resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.reader = resp.BodyStream()
|
||||
r.resp = resp
|
||||
return c.SendStream(r)
|
||||
}
|
||||
|
||||
r := acquireReader()
|
||||
tag.WriteTo(r)
|
||||
r.req = req
|
||||
r.resp = resp
|
||||
err := r.Setup(u, false, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.SendStream(r)
|
||||
case cfg.AudioOpus:
|
||||
r := acquireReader()
|
||||
err := r.Setup(u, false, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.req.SetRequestURIBytes(r.parts[0])
|
||||
err = sc.DoWithRetry(r.client, r.req, r.resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.index++
|
||||
res := r.resp.Body()
|
||||
// purpose is to inject OpusTags metadata into ogg files
|
||||
const until_hdr = len("OggS") + 1 /* ver */ + 0
|
||||
const until_seq = until_hdr + 1 /* hdr */ + 8 /* granule */ + 4 /* bitstream */ + 0
|
||||
const until_checksum = until_seq + 4 /* seq */ + 0
|
||||
const until_segments = until_checksum + 4 /* checksum */ + 1 /* segments num */ + 0
|
||||
|
||||
// this expects first page to only have 1 segment
|
||||
second_page := until_segments + 1 + int(res[until_segments])
|
||||
|
||||
r.leftover = append(r.leftover, res[:second_page]...)
|
||||
|
||||
const opustags_prelude = "OpusTags\x04\x00\x00\x00maid" // "OpusTags", uint32 for length, then <length> bytes for vendor string
|
||||
const artist = "ARTIST="
|
||||
const title = "TITLE="
|
||||
const genre = "GENRE="
|
||||
// we need to put actual content somewhere else to segment it properly
|
||||
var leftover []byte
|
||||
{
|
||||
ln := len(opustags_prelude) + // opustags hdr
|
||||
4 + // number of fields
|
||||
4 + len(artist) + len(t.Author.Username) + // ARTIST=...
|
||||
4 + len(title) + len(t.Title) // TITLE=...
|
||||
|
||||
if t.Genre != "" {
|
||||
ln += 4 + len(genre) + len(t.Genre)
|
||||
}
|
||||
leftover = make([]byte, 0, ln)
|
||||
}
|
||||
|
||||
// here come the metadata
|
||||
leftover = append(leftover, opustags_prelude...)
|
||||
// number of fields
|
||||
const default_num = 2
|
||||
num := uint32(default_num)
|
||||
leftover = append(leftover, default_num, 0, 0, 0)
|
||||
// each field in the format of SOME_KEY=SOME_VALUE
|
||||
// same approach here, first the field length, then the field itself
|
||||
leftover = binary.LittleEndian.AppendUint32(leftover, uint32(len(artist)+len(t.Author.Username)))
|
||||
leftover = append(leftover, artist...)
|
||||
leftover = append(leftover, t.Author.Username...)
|
||||
|
||||
leftover = binary.LittleEndian.AppendUint32(leftover, uint32(len(title)+len(t.Title)))
|
||||
leftover = append(leftover, title...)
|
||||
leftover = append(leftover, t.Title...)
|
||||
if t.Genre != "" {
|
||||
leftover = binary.LittleEndian.AppendUint32(leftover, uint32(len(genre)+len(t.Genre)))
|
||||
leftover = append(leftover, genre...)
|
||||
leftover = append(leftover, t.Genre...)
|
||||
num++
|
||||
}
|
||||
|
||||
if t.Artwork != "" {
|
||||
r.req.SetRequestURI(t.Artwork)
|
||||
|
||||
err := sc.DoWithRetry(image_httpc, r.req, r.resp)
|
||||
if err == nil && r.resp.StatusCode() == 200 {
|
||||
// METADATA_BLOCK_PICTURE comes from the FLAC format (https://www.rfc-editor.org/rfc/rfc9639.html#section-8.8), but base64 encoded
|
||||
const picture = "METADATA_BLOCK_PICTURE="
|
||||
|
||||
const emptyshits2 = "\x00\x00\x00\x00" + // desc length
|
||||
"\x00\x00\x00\x00" + // width
|
||||
"\x00\x00\x00\x00" + // height
|
||||
"\x00\x00\x00\x00" + // color depth
|
||||
"\x00\x00\x00\x00" // indexed color count
|
||||
pic := make([]byte, 0, 4+ // picture type
|
||||
4+ // mime len
|
||||
len(r.resp.Header.ContentType())+ // mime
|
||||
len(emptyshits2)+ // blah blah look above
|
||||
4+ // body len
|
||||
len(r.resp.Body()))
|
||||
pic = append(pic, 0, 0, 0, 3) // picture type (3, Front cover)
|
||||
pic = binary.BigEndian.AppendUint32(pic, uint32(len(r.resp.Header.ContentType())))
|
||||
pic = append(pic, r.resp.Header.ContentType()...)
|
||||
pic = pic[:len(pic)+len(emptyshits2)]
|
||||
pic = binary.BigEndian.AppendUint32(pic, uint32(len(r.resp.Body())))
|
||||
pic = append(pic, r.resp.Body()...)
|
||||
|
||||
newLen := base64.StdEncoding.EncodedLen(len(pic))
|
||||
// alloc for the picture
|
||||
if n := 4 + len(picture) + newLen - (cap(leftover) - len(leftover)); n > 0 {
|
||||
leftover = append(leftover[:cap(leftover)], make([]byte, n)...)[:len(leftover)]
|
||||
}
|
||||
leftover = binary.LittleEndian.AppendUint32(leftover, uint32(len(picture)+newLen))
|
||||
leftover = append(leftover, picture...)
|
||||
base64.StdEncoding.Encode(leftover[len(leftover):len(leftover)+newLen], pic)
|
||||
leftover = leftover[:len(leftover)+newLen]
|
||||
num++
|
||||
}
|
||||
}
|
||||
|
||||
if num != default_num {
|
||||
binary.LittleEndian.PutUint32(leftover[len(opustags_prelude):], num)
|
||||
}
|
||||
|
||||
const max_possible_page = 255 * 255 // 255 segments, each can have 255 bytes
|
||||
if len(leftover) <= max_possible_page { // happy path :) it all fits in one page
|
||||
// allocate hdr
|
||||
if n := until_segments - (cap(r.leftover) - len(r.leftover)); n > 0 {
|
||||
r.leftover = append(r.leftover[:cap(r.leftover)], make([]byte, n)...)[:len(r.leftover)]
|
||||
}
|
||||
|
||||
ptr := len(r.leftover)
|
||||
r.leftover = append(r.leftover, res[second_page:second_page+until_checksum]...)
|
||||
r.leftover = append(r.leftover, 0, 0, 0, 0) // checksum, to be filled in
|
||||
|
||||
// lets segment it using ceil division
|
||||
segments_num := (len(leftover) + 254) / 255
|
||||
r.leftover = append(r.leftover, byte(segments_num))
|
||||
// grow the slice before we add allat
|
||||
if n := segments_num - (cap(r.leftover) - len(r.leftover)); n > 0 {
|
||||
r.leftover = append(r.leftover[:cap(r.leftover)], make([]byte, n)...)[:len(r.leftover)+segments_num]
|
||||
}
|
||||
|
||||
for i := range segments_num - 1 {
|
||||
r.leftover[len(r.leftover)-i-2] = 255
|
||||
}
|
||||
|
||||
if n := byte(len(leftover) % 255); n != 0 {
|
||||
r.leftover[len(r.leftover)-1] = n
|
||||
} else {
|
||||
r.leftover[len(r.leftover)-1] = 255
|
||||
}
|
||||
|
||||
r.leftover = append(r.leftover, leftover...)
|
||||
|
||||
// checksum is calculated for entire page including header
|
||||
var crc uint32
|
||||
for _, b := range r.leftover[ptr:] {
|
||||
crc = (crc << 8) ^ crcTable[(crc>>24)^uint32(b)]
|
||||
}
|
||||
|
||||
binary.LittleEndian.PutUint32(r.leftover[ptr+until_checksum:], crc)
|
||||
} else { // sad path :(
|
||||
pages_num := (len(leftover) + max_possible_page - 1) / max_possible_page
|
||||
|
||||
// allocate exactly as much as we need
|
||||
if n :=
|
||||
pages_num*until_segments + // headers
|
||||
((len(leftover) + 254) / 255) + // needed segments
|
||||
len(leftover) - // data itself
|
||||
|
||||
(cap(r.leftover) - len(r.leftover)); n > 0 {
|
||||
r.leftover = append(r.leftover[:cap(r.leftover)], make([]byte, n)...)[:len(r.leftover)]
|
||||
}
|
||||
for i := range pages_num {
|
||||
ptr := len(r.leftover)
|
||||
r.leftover = append(r.leftover, res[second_page:second_page+until_checksum]...)
|
||||
binary.LittleEndian.PutUint32(r.leftover[ptr+until_seq:], uint32(i))
|
||||
r.leftover = append(r.leftover, 0, 0, 0, 0) // checksum, to be filled in
|
||||
|
||||
const (
|
||||
Continuation byte = 0x01
|
||||
BOS byte = 0x02
|
||||
EOS byte = 0x04
|
||||
)
|
||||
var segments_num int
|
||||
var sl []byte
|
||||
if i+1 == pages_num {
|
||||
r.leftover[ptr+until_hdr] = EOS
|
||||
sl = leftover[i*max_possible_page:]
|
||||
segments_num = (len(sl) + 254) / 255
|
||||
} else {
|
||||
if i != 0 {
|
||||
r.leftover[ptr+until_hdr] = Continuation
|
||||
} else {
|
||||
r.leftover[ptr+until_hdr] = BOS
|
||||
}
|
||||
segments_num = 255
|
||||
sl = leftover[i*max_possible_page : (i+1)*max_possible_page]
|
||||
}
|
||||
r.leftover = append(r.leftover, byte(segments_num))
|
||||
|
||||
r.leftover = r.leftover[:len(r.leftover)+segments_num]
|
||||
for i := range segments_num - 1 {
|
||||
r.leftover[len(r.leftover)-i-2] = 255
|
||||
}
|
||||
|
||||
if n := byte(len(sl) % 255); n != 0 {
|
||||
r.leftover[len(r.leftover)-1] = n
|
||||
} else {
|
||||
r.leftover[len(r.leftover)-1] = 255
|
||||
}
|
||||
|
||||
r.leftover = append(r.leftover, sl...)
|
||||
|
||||
// checksum is calculated for entire page including header
|
||||
var crc uint32
|
||||
for _, b := range r.leftover[ptr:] {
|
||||
crc = (crc << 8) ^ crcTable[(crc>>24)^uint32(b)]
|
||||
}
|
||||
|
||||
binary.LittleEndian.PutUint32(r.leftover[ptr+until_checksum:], crc)
|
||||
}
|
||||
}
|
||||
// dump the rest after original 2nd page
|
||||
r.leftover = append(r.leftover, res[second_page+until_segments+int(res[second_page+until_segments])])
|
||||
return c.SendStream(r)
|
||||
case cfg.AudioAAC:
|
||||
r := acquireReader()
|
||||
err := r.Setup(u, true, nil)
|
||||
@@ -198,7 +399,7 @@ func Load(r *fiber.App) {
|
||||
if t.Artwork != "" {
|
||||
r.req.SetRequestURI(t.Artwork)
|
||||
|
||||
err := sc.DoWithRetry(misc.ImageClient, r.req, r.resp)
|
||||
err := sc.DoWithRetry(image_httpc, r.req, r.resp)
|
||||
if err == nil && r.resp.StatusCode() == 200 {
|
||||
parsed, _, err := image.Decode(r.resp.BodyStream())
|
||||
r.resp.CloseBodyStream()
|
||||
@@ -208,15 +409,36 @@ func Load(r *fiber.App) {
|
||||
}
|
||||
}
|
||||
|
||||
var col collector
|
||||
tag.Save(&col)
|
||||
fixDuration(col.data, &t.Duration)
|
||||
r.leftover = col.data
|
||||
tag.Save(r)
|
||||
fixDuration(r.leftover, &t.Duration)
|
||||
|
||||
return c.SendStream(r)
|
||||
}
|
||||
}
|
||||
|
||||
// just the audio file itself, means less processing overhead for us :)
|
||||
if tr.Format.Protocol == sc.ProtocolProgressive {
|
||||
misc.Log("use progressive")
|
||||
req := fasthttp.AcquireRequest()
|
||||
defer fasthttp.ReleaseRequest(req)
|
||||
|
||||
resp := fasthttp.AcquireResponse()
|
||||
|
||||
req.SetRequestURI(u)
|
||||
req.Header.SetUserAgent(cfg.UserAgent)
|
||||
|
||||
// enforce streaming here!!
|
||||
err := sc.DoWithRetry(misc.HlsStreamingOnlyClient, req, resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r := misc.AcquireProxyReader()
|
||||
r.Reader = resp.BodyStream()
|
||||
r.Resp = resp
|
||||
return c.SendStream(r)
|
||||
}
|
||||
|
||||
r := acquireReader()
|
||||
if audio == cfg.AudioAAC {
|
||||
err = r.Setup(u, true, &t.Duration)
|
||||
|
||||
60
lib/restream/injector.go
Normal file
60
lib/restream/injector.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package restream
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// inject some bytes before the real reader
|
||||
type injector struct {
|
||||
leftover []byte
|
||||
reader io.Reader
|
||||
resp *fasthttp.Response
|
||||
}
|
||||
|
||||
var injectorpool = sync.Pool{
|
||||
New: func() any {
|
||||
return &injector{}
|
||||
},
|
||||
}
|
||||
|
||||
func acquireInjector() *injector {
|
||||
return injectorpool.Get().(*injector)
|
||||
}
|
||||
|
||||
func (r *injector) Read(buf []byte) (n int, err error) {
|
||||
if len(r.leftover) != 0 {
|
||||
h := min(len(buf), len(r.leftover))
|
||||
|
||||
n = copy(buf, r.leftover[:h])
|
||||
|
||||
if n > len(r.leftover) {
|
||||
r.leftover = nil
|
||||
} else {
|
||||
r.leftover = r.leftover[n:]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return r.reader.Read(buf)
|
||||
}
|
||||
|
||||
func (r *injector) Close() error {
|
||||
r.resp.CloseBodyStream()
|
||||
fasthttp.ReleaseResponse(r.resp)
|
||||
|
||||
r.reader = nil
|
||||
r.resp = nil
|
||||
r.leftover = r.leftover[:0]
|
||||
|
||||
injectorpool.Put(r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *injector) Write(data []byte) (n int, err error) {
|
||||
c.leftover = append(c.leftover, data...)
|
||||
return len(data), nil
|
||||
}
|
||||
@@ -88,13 +88,15 @@ func (r *reader) Setup(url string, aac bool, duration *uint32) error {
|
||||
}
|
||||
// clone needed to mitigate memory skill issues smh
|
||||
if aac {
|
||||
for _, s := range bytes.Split(r.resp.Body(), newline) {
|
||||
for s := range bytes.SplitSeq(r.resp.Body(), newline) {
|
||||
if len(s) == 0 {
|
||||
continue
|
||||
}
|
||||
if s[0] == '#' {
|
||||
if bytes.HasPrefix(s, []byte(`#EXT-X-MAP:URI="`)) {
|
||||
r.parts = append(r.parts, clone(s[16:len(s)-1]))
|
||||
// #EXT-X-MAP:URI="..."
|
||||
const x = `#EXT-X-MAP:URI="`
|
||||
if len(s) > len(x) && string(s[:len(x)]) == x {
|
||||
r.parts = append(r.parts, clone(s[len(x):len(s)-1]))
|
||||
}
|
||||
|
||||
continue
|
||||
@@ -103,7 +105,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(r.resp.Body(), newline) {
|
||||
for s := range bytes.SplitSeq(r.resp.Body(), newline) {
|
||||
if len(s) == 0 || s[0] == '#' {
|
||||
continue
|
||||
}
|
||||
@@ -120,7 +122,7 @@ func (r *reader) Close() error {
|
||||
r.req.Reset()
|
||||
r.resp.Reset()
|
||||
|
||||
r.leftover = nil
|
||||
r.leftover = r.leftover[:0]
|
||||
r.index = 0
|
||||
r.parts = r.parts[:0]
|
||||
|
||||
@@ -132,10 +134,7 @@ func (r *reader) Close() error {
|
||||
func (r *reader) Read(buf []byte) (n int, err error) {
|
||||
misc.Log("we read")
|
||||
if len(r.leftover) != 0 {
|
||||
h := len(buf)
|
||||
if h > len(r.leftover) {
|
||||
h = len(r.leftover)
|
||||
}
|
||||
h := min(len(buf), len(r.leftover))
|
||||
|
||||
n = copy(buf, r.leftover[:h])
|
||||
|
||||
@@ -184,3 +183,8 @@ func (r *reader) Read(buf []byte) (n int, err error) {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *reader) Write(data []byte) (n int, err error) {
|
||||
c.leftover = append(c.leftover, data...)
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
@@ -1,70 +1,24 @@
|
||||
package sc
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"git.maid.zone/stuff/soundcloak/lib/cfg"
|
||||
)
|
||||
|
||||
// Functions/structures related to featured/suggested content
|
||||
|
||||
type PlaylistOrUser struct {
|
||||
Kind string `json:"kind"` // "playlist" or "system-playlist" or "user"
|
||||
Permalink string `json:"permalink"`
|
||||
|
||||
// User-specific
|
||||
Avatar string `json:"avatar_url"`
|
||||
Username string `json:"username"`
|
||||
FullName string `json:"full_name"`
|
||||
|
||||
// Playlist-specific
|
||||
Title string `json:"title"`
|
||||
Author struct {
|
||||
Permalink string `string:"permalink"`
|
||||
} `json:"user"`
|
||||
Artwork string `json:"artwork_url"`
|
||||
TrackCount int64 `json:"track_count"`
|
||||
}
|
||||
|
||||
func (p PlaylistOrUser) Href() string {
|
||||
switch p.Kind {
|
||||
case "system-playlist":
|
||||
return "/discover/sets/" + p.Permalink
|
||||
case "playlist":
|
||||
return "/" + p.Author.Permalink + "/sets/" + p.Permalink
|
||||
default:
|
||||
return "/" + p.Permalink
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PlaylistOrUser) Fix(prefs cfg.Preferences) {
|
||||
switch p.Kind {
|
||||
case "user":
|
||||
if p.Avatar == "https://a1.sndcdn.com/images/default_avatar_large.png" {
|
||||
p.Avatar = ""
|
||||
} else {
|
||||
p.Avatar = strings.Replace(p.Avatar, "-large.", "-t200x200.", 1)
|
||||
}
|
||||
default:
|
||||
if p.Artwork != "" {
|
||||
p.Artwork = strings.Replace(p.Artwork, "-large.", "-t200x200.", 1)
|
||||
if cfg.ProxyImages && *prefs.ProxyImages {
|
||||
p.Artwork = "/_/proxy/images?url=" + url.QueryEscape(p.Artwork)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Selection struct {
|
||||
Title string `json:"title"`
|
||||
Kind string `json:"kind"` // should always be "selection"!
|
||||
Items Paginated[*PlaylistOrUser] `json:"items"` // ?? why
|
||||
Items Paginated[*UserPlaylistTrack] `json:"items"` // ?? why
|
||||
}
|
||||
|
||||
func GetSelections(cid string, prefs cfg.Preferences) (*Paginated[*Selection], error) {
|
||||
uri := baseUri()
|
||||
uri.SetPath("/mixed-selections")
|
||||
uri.QueryArgs().Set("limit", "20")
|
||||
|
||||
// There is no pagination
|
||||
p := Paginated[*Selection]{Next: "https://" + api + "/mixed-selections?limit=20"}
|
||||
p := Paginated[*Selection]{Next: uri}
|
||||
err := p.Proceed(cid, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
59
lib/sc/feeds.go
Normal file
59
lib/sc/feeds.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package sc
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// vendored type definitions from github.com/gorilla/feeds
|
||||
// the only thing used from that module so why not drop some dead weight :)
|
||||
|
||||
type RssFeedXml struct {
|
||||
XMLName xml.Name `xml:"rss"`
|
||||
Version string `xml:"version,attr"`
|
||||
ContentNamespace string `xml:"xmlns:content,attr"`
|
||||
Channel *RssFeed
|
||||
}
|
||||
|
||||
type RssFeed struct {
|
||||
XMLName xml.Name `xml:"channel"`
|
||||
Title string `xml:"title"` // required
|
||||
Link string `xml:"link"` // required
|
||||
Description string `xml:"description"` // required
|
||||
//Language string `xml:"language,omitempty"`
|
||||
//Copyright string `xml:"copyright,omitempty"`
|
||||
ManagingEditor string `xml:"managingEditor,omitempty"` // Author used
|
||||
//WebMaster string `xml:"webMaster,omitempty"`
|
||||
PubDate string `xml:"pubDate,omitempty"` // created or updated
|
||||
LastBuildDate string `xml:"lastBuildDate,omitempty"` // updated used
|
||||
Category string `xml:"category,omitempty"`
|
||||
Generator string `xml:"generator,omitempty"`
|
||||
//Docs string `xml:"docs,omitempty"`
|
||||
//Cloud string `xml:"cloud,omitempty"`
|
||||
Ttl int `xml:"ttl,omitempty"`
|
||||
//Rating string `xml:"rating,omitempty"`
|
||||
//SkipHours string `xml:"skipHours,omitempty"`
|
||||
//SkipDays string `xml:"skipDays,omitempty"`
|
||||
//Image *RssImage
|
||||
//TextInput *RssTextInput
|
||||
Items []*RssItem `xml:"item"`
|
||||
}
|
||||
|
||||
type RssItem struct {
|
||||
XMLName xml.Name `xml:"item"`
|
||||
Title string `xml:"title"` // required
|
||||
Link string `xml:"link"` // required
|
||||
Description string `xml:"description"` // required
|
||||
//Content *RssContent
|
||||
//Author string `xml:"author,omitempty"`
|
||||
Category string `xml:"category,omitempty"`
|
||||
//Comments string `xml:"comments,omitempty"`
|
||||
//Enclosure *RssEnclosure
|
||||
Guid *RssGuid // Id used
|
||||
PubDate string `xml:"pubDate,omitempty"` // created or updated
|
||||
//Source string `xml:"source,omitempty"`
|
||||
}
|
||||
|
||||
type RssGuid struct {
|
||||
//RSS 2.0 <guid isPermaLink="true">http://inessential.com/2002/09/01.php#a2</guid>
|
||||
XMLName xml.Name `xml:"guid"`
|
||||
Id string `xml:",chardata"`
|
||||
IsPermaLink string `xml:"isPermaLink,attr,omitempty"` // "true", "false", or an empty string
|
||||
}
|
||||
242
lib/sc/init.go
242
lib/sc/init.go
@@ -2,6 +2,7 @@ package sc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -22,12 +23,21 @@ import (
|
||||
)
|
||||
|
||||
var ProxyErr = errors.New("could not connect to proxy")
|
||||
var parsedproxy string
|
||||
|
||||
// don't jus leak the proxy like that lol
|
||||
func scrub(err error) error {
|
||||
if cfg.SoundcloudApiProxy != "" && err != nil {
|
||||
if parsedproxy == "" {
|
||||
u, err := url.Parse(cfg.SoundcloudApiProxy)
|
||||
if err == nil {
|
||||
parsedproxy = u.Host
|
||||
} else {
|
||||
parsedproxy = cfg.SoundcloudApiProxy
|
||||
}
|
||||
}
|
||||
s := err.Error()
|
||||
if strings.HasPrefix(s, "could not connect to proxyAddr") || strings.HasPrefix(s, "socks connect") {
|
||||
if strings.HasPrefix(s, "could not connect to proxyAddr") || strings.HasPrefix(s, "socks connect") || strings.Contains(s, parsedproxy) {
|
||||
return ProxyErr
|
||||
}
|
||||
}
|
||||
@@ -49,18 +59,44 @@ const H = len("https://" + api)
|
||||
|
||||
var newline = []byte("\n")
|
||||
|
||||
var sc_version = []byte(`<script>window.__sc_version="`)
|
||||
var script0 = []byte(`<script crossorigin src="https://a-v2.sndcdn.com/assets/0-`)
|
||||
var script = []byte(`<script crossorigin src="https://a-v2.sndcdn.com/assets/`)
|
||||
const sc_version = `<script>window.__sc_version="`
|
||||
const sc_hydration = `<script>window.__sc_hydration = `
|
||||
const script0 = `<script crossorigin src="https://a-v2.sndcdn.com/assets/0-`
|
||||
const script = `<script crossorigin src="https://a-v2.sndcdn.com/assets/`
|
||||
|
||||
var tlsConfig = &tls.Config{
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_AES_128_GCM_SHA256,
|
||||
tls.TLS_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
},
|
||||
}
|
||||
|
||||
var httpc = &fasthttp.HostClient{
|
||||
Addr: api + ":443",
|
||||
IsTLS: true,
|
||||
MaxIdleConnDuration: cfg.MaxIdleConnDuration,
|
||||
DialDualStack: cfg.DialDualStack,
|
||||
TLSConfig: tlsConfig,
|
||||
}
|
||||
|
||||
var genericClient = &fasthttp.Client{
|
||||
DialDualStack: cfg.DialDualStack,
|
||||
TLSConfig: tlsConfig,
|
||||
}
|
||||
|
||||
// var verRegex = regexp2.MustCompile(`^<script>window\.__sc_version="([0-9]{10})"</script>$`, 2)
|
||||
@@ -69,6 +105,7 @@ var genericClient = &fasthttp.Client{
|
||||
|
||||
//go:generate go tool regexp2cg -package sc -o regexp2_codegen.go
|
||||
var clientIdRegex = regexp2.MustCompile(`client_id:"([A-Za-z0-9]{32})"`, 0) //regexp2.MustCompile(`\("client_id=([A-Za-z0-9]{32})"\)`, 0)
|
||||
var hydrationClientIdRegex = regexp2.MustCompile(`{"hydratable":"apiClient","data":{"id":"([A-Za-z0-9]{32})"`, 0)
|
||||
var ErrVersionNotFound = errors.New("version not found")
|
||||
var ErrScriptNotFound = errors.New("script not found")
|
||||
var ErrIDNotFound = errors.New("clientid not found")
|
||||
@@ -120,7 +157,7 @@ func processFile(wg *sync.WaitGroup, ch chan string, uri []byte, isDone *bool) {
|
||||
return
|
||||
}
|
||||
|
||||
m2, _ := clientIdRegex.FindStringMatch(string(data))
|
||||
m2, _ := clientIdRegex.FindStringMatch(cfg.B2s(data))
|
||||
if m2 != nil {
|
||||
g := m2.GroupByNumber(1)
|
||||
if g != nil {
|
||||
@@ -138,10 +175,7 @@ func processFile(wg *sync.WaitGroup, ch chan string, uri []byte, isDone *bool) {
|
||||
misc.Log("not found in", string(uri))
|
||||
}
|
||||
|
||||
// Experimental method, which asserts that the clientId is inside the file that starts with "0-"
|
||||
const experimental_GetClientID = false
|
||||
|
||||
// inspired by github.com/imputnet/cobalt
|
||||
// now faster and more error-prone (?)
|
||||
func GetClientID() (string, error) {
|
||||
if cfg.ClientID != "" {
|
||||
return cfg.ClientID, nil
|
||||
@@ -155,7 +189,8 @@ func GetClientID() (string, error) {
|
||||
req := fasthttp.AcquireRequest()
|
||||
defer fasthttp.ReleaseRequest(req)
|
||||
|
||||
req.SetRequestURI("https://soundcloud.com")
|
||||
req.URI().SetScheme("https")
|
||||
req.URI().SetHost("soundcloud.com")
|
||||
req.Header.SetUserAgent(cfg.UserAgent)
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
|
||||
@@ -172,19 +207,20 @@ func GetClientID() (string, error) {
|
||||
data = resp.Body()
|
||||
}
|
||||
|
||||
if experimental_GetClientID {
|
||||
var ver string
|
||||
var scriptUrl []byte
|
||||
var hydration []byte
|
||||
for l := range bytes.SplitSeq(data, newline) { // version usually comes earlier, but retest this sometimes !!!
|
||||
if ver == "" && bytes.HasPrefix(l, sc_version) {
|
||||
if ver == "" && len(l) > len(sc_version)+len(`"</script>`) && string(l[:len(sc_version)]) == sc_version {
|
||||
ver = cfg.B2s(l[len(sc_version) : len(l)-len(`"</script>`)])
|
||||
misc.Log("found ver:", ver)
|
||||
if ClientIDCache.Version != "" && ver == ClientIDCache.Version {
|
||||
goto verCacheHit
|
||||
misc.Log("clientidcache hit @ ver")
|
||||
ClientIDCache.NextCheck = time.Now().Add(cfg.ClientIDTTL)
|
||||
return ClientIDCache.ClientID, nil
|
||||
}
|
||||
} else if bytes.HasPrefix(l, script0) {
|
||||
scriptUrl = l[len(`<script crossorigin src="`) : len(l)-len(`"></script>`)]
|
||||
misc.Log("found scriptUrl:", string(scriptUrl))
|
||||
} else if len(l) > len(sc_hydration)+len(`;</script>`) && string(l[:len(sc_hydration)]) == sc_hydration {
|
||||
hydration = l[len(sc_hydration) : len(l)-len(`;</script>`)]
|
||||
misc.Log("found hydration:", cfg.B2s(hydration))
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -193,25 +229,13 @@ func GetClientID() (string, error) {
|
||||
return "", ErrVersionNotFound
|
||||
}
|
||||
|
||||
if scriptUrl == nil {
|
||||
return "", ErrScriptNotFound
|
||||
}
|
||||
|
||||
req.SetRequestURIBytes(scriptUrl)
|
||||
err = DoWithRetryAll(genericClient, req, resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data, err = resp.BodyUncompressed()
|
||||
if err != nil {
|
||||
data = resp.Body()
|
||||
}
|
||||
|
||||
m, _ := clientIdRegex.FindStringMatch(cfg.B2s(data))
|
||||
// inspired a bit by 4get
|
||||
if hydration != nil {
|
||||
m, _ := hydrationClientIdRegex.FindStringMatch(cfg.B2s(hydration))
|
||||
if m != nil {
|
||||
g := m.GroupByNumber(1)
|
||||
if g != nil {
|
||||
misc.Log("found using sc_hydration")
|
||||
ClientIDCache.ClientID = g.String()
|
||||
ClientIDCache.Version = ver
|
||||
ClientIDCache.NextCheck = time.Now().Add(cfg.ClientIDTTL)
|
||||
@@ -219,20 +243,16 @@ func GetClientID() (string, error) {
|
||||
return ClientIDCache.ClientID, nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
// fallback to searching inside JS scripts, inspired by cobalt
|
||||
ch := make(chan string, 1)
|
||||
wg := &sync.WaitGroup{}
|
||||
done := false
|
||||
|
||||
var ver string
|
||||
var scriptUrls = make([][]byte, 0, 10) // Usually only 8 chunks, so I went with jsut a bit more to be safe
|
||||
var scriptUrls = make([][]byte, 0, 9)
|
||||
for l := range bytes.SplitSeq(data, newline) {
|
||||
if ver == "" && bytes.HasPrefix(l, sc_version) {
|
||||
ver = cfg.B2s(l[len(sc_version) : len(l)-len(`"</script>`)])
|
||||
if ver == ClientIDCache.Version {
|
||||
goto verCacheHit
|
||||
}
|
||||
} else if bytes.HasPrefix(l, script) {
|
||||
if len(l) > len(script)+len(`"></script>`) && string(l[:len(script)]) == script {
|
||||
scriptUrls = append(scriptUrls, l[len(`<script crossorigin src="`):len(l)-len(`"></script>`)])
|
||||
}
|
||||
}
|
||||
@@ -277,14 +297,6 @@ func GetClientID() (string, error) {
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
return "", ErrIDNotFound
|
||||
|
||||
verCacheHit:
|
||||
misc.Log("clientidcache hit @ ver")
|
||||
ClientIDCache.NextCheck = time.Now().Add(cfg.ClientIDTTL)
|
||||
return ClientIDCache.ClientID, nil
|
||||
}
|
||||
|
||||
// Just retry any kind of errors, why not
|
||||
@@ -340,7 +352,11 @@ func Resolve(cid string, path string, out any) error {
|
||||
req := fasthttp.AcquireRequest()
|
||||
defer fasthttp.ReleaseRequest(req)
|
||||
|
||||
req.SetRequestURI("https://" + api + "/resolve?url=https%3A%2F%2Fsoundcloud.com%2F" + url.QueryEscape(path) + "&client_id=" + cid)
|
||||
req.URI().SetScheme("https")
|
||||
req.URI().SetHost(api)
|
||||
req.URI().SetPath("/resolve")
|
||||
req.URI().QueryArgs().Set("url", "https://soundcloud.com/"+path)
|
||||
req.URI().QueryArgs().Set("client_id", cid)
|
||||
req.Header.SetUserAgent(cfg.UserAgent)
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
|
||||
@@ -365,7 +381,8 @@ func Resolve(cid string, path string, out any) error {
|
||||
}
|
||||
|
||||
type Paginated[T any] struct {
|
||||
Next string `json:"next_href"`
|
||||
NextHref string `json:"next_href"`
|
||||
Next *fasthttp.URI `json:"-"`
|
||||
Collection []T `json:"collection"`
|
||||
Total int64 `json:"total_results"`
|
||||
}
|
||||
@@ -382,12 +399,17 @@ func (p *Paginated[T]) Proceed(cid string, shouldUnfold bool) error {
|
||||
req := fasthttp.AcquireRequest()
|
||||
defer fasthttp.ReleaseRequest(req)
|
||||
|
||||
oldNext := p.Next
|
||||
req.SetRequestURI(p.Next + "&client_id=" + cid)
|
||||
oldNext := p.NextHref
|
||||
if p.NextHref == "" {
|
||||
oldNext = p.Next.String()
|
||||
req.SetURI(p.Next)
|
||||
} else {
|
||||
req.SetRequestURI(p.NextHref)
|
||||
}
|
||||
req.URI().QueryArgs().Set("client_id", cid)
|
||||
req.Header.SetUserAgent(cfg.UserAgent)
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
req.Header.Set("Accept-Language", "en-US,en;q=0.5") // you get captcha without it :)
|
||||
|
||||
resp := fasthttp.AcquireResponse()
|
||||
defer fasthttp.ReleaseResponse(resp)
|
||||
|
||||
@@ -410,8 +432,8 @@ func (p *Paginated[T]) Proceed(cid string, shouldUnfold bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.Next == oldNext { // prevent loops of nothingness
|
||||
p.Next = ""
|
||||
if p.NextHref == oldNext { // prevent loops of nothingness
|
||||
p.NextHref = ""
|
||||
}
|
||||
|
||||
// in soundcloud api, pagination may not immediately return you something!
|
||||
@@ -419,7 +441,7 @@ func (p *Paginated[T]) Proceed(cid string, shouldUnfold bool) error {
|
||||
// maybe there could be a way to cache the last useless layer of pagination so soundcloak can start loading from there? might be a bit complicated, but would be great
|
||||
|
||||
// another note: in featured tracks it seems to just be forever stuck after 2-3~ pages so i added a way to disable this behaviour
|
||||
if shouldUnfold && len(p.Collection) == 0 && p.Next != "" {
|
||||
if shouldUnfold && len(p.Collection) == 0 && p.NextHref != "" {
|
||||
// this will make sure that we actually proceed to something useful and not emptiness
|
||||
return p.Proceed(cid, true)
|
||||
}
|
||||
@@ -429,8 +451,8 @@ func (p *Paginated[T]) Proceed(cid string, shouldUnfold bool) error {
|
||||
|
||||
func TagListParser(taglist string) (res []string) {
|
||||
inString := false
|
||||
cur := []rune{}
|
||||
for i, c := range taglist {
|
||||
cur := []byte{}
|
||||
for i, c := range cfg.S2b(taglist) {
|
||||
if c == '"' {
|
||||
if i == len(taglist)-1 {
|
||||
res = append(res, string(cur))
|
||||
@@ -443,7 +465,7 @@ func TagListParser(taglist string) (res []string) {
|
||||
|
||||
if !inString && c == ' ' {
|
||||
res = append(res, string(cur))
|
||||
cur = []rune{}
|
||||
cur = cur[:0]
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -462,7 +484,12 @@ type SearchSuggestion struct {
|
||||
}
|
||||
|
||||
func GetSearchSuggestions(cid string, query string) ([]string, error) {
|
||||
p := Paginated[SearchSuggestion]{Next: "https://" + api + "/search/queries?limit=10&q=" + url.QueryEscape(query)}
|
||||
uri := baseUri()
|
||||
uri.SetPath("/search/queries")
|
||||
uri.QueryArgs().Set("limit", "10")
|
||||
uri.QueryArgs().Set("q", query)
|
||||
|
||||
p := Paginated[SearchSuggestion]{Next: uri}
|
||||
err := p.Proceed(cid, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -476,6 +503,89 @@ func GetSearchSuggestions(cid string, query string) ([]string, error) {
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// polyglot type struct lol
|
||||
type UserPlaylistTrack struct {
|
||||
Kind string `json:"kind"` // "playlist" or "system-playlist" or "user" or "track"
|
||||
Permalink string `json:"permalink"`
|
||||
|
||||
// User
|
||||
Avatar string `json:"avatar_url"`
|
||||
Username string `json:"username"`
|
||||
FullName string `json:"full_name"`
|
||||
|
||||
// Playlist/track
|
||||
Title string `json:"title"`
|
||||
Author struct {
|
||||
Permalink string `string:"permalink"`
|
||||
Username string `json:"username"`
|
||||
} `json:"user"`
|
||||
Artwork string `json:"artwork_url"`
|
||||
|
||||
// Playlist
|
||||
Tracks []struct{} `json:"tracks"` // stub
|
||||
TrackCount int64 `json:"track_count"`
|
||||
}
|
||||
|
||||
func (p UserPlaylistTrack) Href() string {
|
||||
switch p.Kind {
|
||||
case "system-playlist":
|
||||
return "/discover/sets/" + p.Permalink
|
||||
case "playlist":
|
||||
return "/" + p.Author.Permalink + "/sets/" + p.Permalink
|
||||
case "track":
|
||||
return "/" + p.Author.Permalink + "/" + p.Permalink
|
||||
default:
|
||||
return "/" + p.Permalink
|
||||
}
|
||||
}
|
||||
|
||||
func (p *UserPlaylistTrack) Fix(prefs cfg.Preferences) {
|
||||
switch p.Kind {
|
||||
case "user":
|
||||
if p.Avatar == "https://a1.sndcdn.com/images/default_avatar_large.png" {
|
||||
p.Avatar = ""
|
||||
} else {
|
||||
p.Avatar = strings.Replace(p.Avatar, "-large.", "-t200x200.", 1)
|
||||
}
|
||||
|
||||
if p.Avatar != "" && cfg.ProxyImages && *prefs.ProxyImages {
|
||||
p.Avatar = "/_/proxy/images?url=" + url.QueryEscape(p.Avatar)
|
||||
}
|
||||
default:
|
||||
if p.Artwork != "" {
|
||||
p.Artwork = strings.Replace(p.Artwork, "-large.", "-t200x200.", 1)
|
||||
if cfg.ProxyImages && *prefs.ProxyImages {
|
||||
p.Artwork = "/_/proxy/images?url=" + url.QueryEscape(p.Artwork)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p UserPlaylistTrack) TracksCount() int64 {
|
||||
if p.TrackCount != 0 {
|
||||
return p.TrackCount
|
||||
}
|
||||
|
||||
return int64(len(p.Tracks))
|
||||
}
|
||||
|
||||
func Search(cid string, prefs cfg.Preferences, args []byte) (*Paginated[*UserPlaylistTrack], error) {
|
||||
uri := baseUri()
|
||||
uri.SetPath("/search")
|
||||
uri.SetQueryStringBytes(args)
|
||||
p := Paginated[*UserPlaylistTrack]{Next: uri}
|
||||
err := p.Proceed(cid, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, t := range p.Collection {
|
||||
t.Fix(prefs)
|
||||
}
|
||||
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// could probably make a generic function, whatever
|
||||
func init() {
|
||||
if cfg.SoundcloudApiProxy != "" {
|
||||
@@ -536,3 +646,11 @@ func init() {
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func baseUri() *fasthttp.URI {
|
||||
uri := fasthttp.AcquireURI()
|
||||
uri.SetScheme("https")
|
||||
uri.SetHost(api)
|
||||
|
||||
return uri
|
||||
}
|
||||
|
||||
@@ -71,8 +71,11 @@ func GetPlaylist(cid string, permalink string) (Playlist, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func SearchPlaylists(cid string, prefs cfg.Preferences, args string) (*Paginated[*Playlist], error) {
|
||||
p := Paginated[*Playlist]{Next: "https://" + api + "/search/playlists" + args}
|
||||
func SearchPlaylists(cid string, prefs cfg.Preferences, args []byte) (*Paginated[*Playlist], error) {
|
||||
uri := baseUri()
|
||||
uri.SetPath("/search/playlists")
|
||||
uri.SetQueryStringBytes(args)
|
||||
p := Paginated[*Playlist]{Next: uri}
|
||||
err := p.Proceed(cid, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -93,7 +93,7 @@ type Comment struct {
|
||||
Timestamp int `json:"timestamp"`
|
||||
}
|
||||
|
||||
func (m Media) SelectCompatible(mode string, opus bool) (*Transcoding, string) {
|
||||
func (m Media) SelectCompatible(mode string, opus bool, restream bool) (*Transcoding, string) {
|
||||
switch mode {
|
||||
case cfg.AudioBest:
|
||||
for _, t := range m.Transcodings {
|
||||
@@ -123,6 +123,13 @@ func (m Media) SelectCompatible(mode string, opus bool) (*Transcoding, string) {
|
||||
}
|
||||
}
|
||||
|
||||
if restream {
|
||||
for _, t := range m.Transcodings {
|
||||
if t.Format.Protocol == ProtocolProgressive && t.Format.MimeType == "audio/mpeg" {
|
||||
return &t, cfg.AudioMP3
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, t := range m.Transcodings {
|
||||
if t.Format.Protocol == ProtocolHLS && t.Format.MimeType == "audio/mpeg" {
|
||||
return &t, cfg.AudioMP3
|
||||
@@ -131,6 +138,15 @@ func (m Media) SelectCompatible(mode string, opus bool) (*Transcoding, string) {
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
func (m Media) SelectCompatibleProgressive() *Transcoding {
|
||||
for _, t := range m.Transcodings {
|
||||
if t.Format.Protocol == ProtocolProgressive && t.Format.MimeType == "audio/mpeg" {
|
||||
return &t
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetTrack(cid string, permalink string) (Track, error) {
|
||||
tracksCacheLock.RLock()
|
||||
if cell, ok := TracksCache[permalink]; ok {
|
||||
@@ -245,8 +261,11 @@ func GetArbitraryTrack(cid string, data string) (Track, error) {
|
||||
return Track{}, ErrKindNotCorrect
|
||||
}
|
||||
|
||||
func SearchTracks(cid string, prefs cfg.Preferences, args string) (*Paginated[*Track], error) {
|
||||
p := Paginated[*Track]{Next: "https://" + api + "/search/tracks" + args}
|
||||
func SearchTracks(cid string, prefs cfg.Preferences, args []byte) (*Paginated[*Track], error) {
|
||||
uri := baseUri()
|
||||
uri.SetPath("/search/tracks")
|
||||
uri.SetQueryStringBytes(args)
|
||||
p := Paginated[*Track]{Next: uri}
|
||||
err := p.Proceed(cid, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -343,12 +362,17 @@ func (tr Transcoding) GetStream(cid string, prefs cfg.Preferences, authorization
|
||||
return "", ErrNoURL
|
||||
}
|
||||
|
||||
if cfg.ProxyStreams && *prefs.ProxyStreams && *prefs.Player == cfg.HLSPlayer {
|
||||
if cfg.ProxyStreams && *prefs.ProxyStreams {
|
||||
switch *prefs.Player {
|
||||
case cfg.HLSPlayer:
|
||||
if tr.Preset == "aac_160k" {
|
||||
return "/_/proxy/streams/playlist/aac?url=" + url.QueryEscape(s.URL), nil
|
||||
}
|
||||
|
||||
return "/_/proxy/streams/playlist?url=" + url.QueryEscape(s.URL), nil
|
||||
case cfg.ProgressivePlayer:
|
||||
return "/_/proxy/streams?url=" + url.QueryEscape(s.URL), nil
|
||||
}
|
||||
}
|
||||
|
||||
return s.URL, nil
|
||||
@@ -456,8 +480,11 @@ func (t Track) Href() string {
|
||||
return "/" + t.Author.Permalink + "/" + t.Permalink
|
||||
}
|
||||
|
||||
func RecentTracks(cid string, prefs cfg.Preferences, args string) (*Paginated[*Track], error) {
|
||||
p := Paginated[*Track]{Next: "https://" + api + "/recent-tracks/" + args}
|
||||
func RecentTracks(cid string, prefs cfg.Preferences, tag, args string) (*Paginated[*Track], error) {
|
||||
uri := baseUri()
|
||||
uri.SetPath("/recent-tracks/" + tag)
|
||||
uri.SetQueryString(args)
|
||||
p := Paginated[*Track]{Next: uri}
|
||||
err := p.Proceed(cid, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -471,10 +498,15 @@ func RecentTracks(cid string, prefs cfg.Preferences, args string) (*Paginated[*T
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func (t Track) baseUri(subpath, args string) *fasthttp.URI {
|
||||
uri := baseUri()
|
||||
uri.SetPath("/tracks/" + string(t.ID) + "/" + subpath)
|
||||
uri.SetQueryString(args)
|
||||
return uri
|
||||
}
|
||||
|
||||
func (t Track) GetRelated(cid string, prefs cfg.Preferences, args string) (*Paginated[*Track], error) {
|
||||
p := Paginated[*Track]{
|
||||
Next: "https://" + api + "/tracks/" + string(t.ID) + "/related" + args,
|
||||
}
|
||||
p := Paginated[*Track]{Next: t.baseUri("related", args)}
|
||||
|
||||
err := p.Proceed(cid, true)
|
||||
if err != nil {
|
||||
@@ -490,9 +522,7 @@ func (t Track) GetRelated(cid string, prefs cfg.Preferences, args string) (*Pagi
|
||||
}
|
||||
|
||||
func (t Track) GetPlaylists(cid string, prefs cfg.Preferences, args string) (*Paginated[*Playlist], error) {
|
||||
p := Paginated[*Playlist]{
|
||||
Next: "https://" + api + "/tracks/" + string(t.ID) + "/playlists_without_albums" + args,
|
||||
}
|
||||
p := Paginated[*Playlist]{Next: t.baseUri("playlists_without_albums", args)}
|
||||
|
||||
err := p.Proceed(cid, true)
|
||||
if err != nil {
|
||||
@@ -508,9 +538,7 @@ func (t Track) GetPlaylists(cid string, prefs cfg.Preferences, args string) (*Pa
|
||||
}
|
||||
|
||||
func (t Track) GetAlbums(cid string, prefs cfg.Preferences, args string) (*Paginated[*Playlist], error) {
|
||||
p := Paginated[*Playlist]{
|
||||
Next: "https://" + api + "/tracks/" + string(t.ID) + "/albums" + args,
|
||||
}
|
||||
p := Paginated[*Playlist]{Next: t.baseUri("albums", args)}
|
||||
|
||||
err := p.Proceed(cid, true)
|
||||
if err != nil {
|
||||
@@ -526,9 +554,7 @@ func (t Track) GetAlbums(cid string, prefs cfg.Preferences, args string) (*Pagin
|
||||
}
|
||||
|
||||
func (t Track) GetComments(cid string, prefs cfg.Preferences, args string) (*Paginated[*Comment], error) {
|
||||
p := Paginated[*Comment]{
|
||||
Next: "https://" + api + "/tracks/" + string(t.ID) + "/comments" + args,
|
||||
}
|
||||
p := Paginated[*Comment]{Next: t.baseUri("comments", args)}
|
||||
|
||||
err := p.Proceed(cid, true)
|
||||
if err != nil {
|
||||
@@ -542,3 +568,16 @@ func (t Track) GetComments(cid string, prefs cfg.Preferences, args string) (*Pag
|
||||
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func ToExt(audio string) string {
|
||||
switch audio {
|
||||
case cfg.AudioAAC:
|
||||
return "m4a"
|
||||
case cfg.AudioOpus:
|
||||
return "ogg"
|
||||
case cfg.AudioMP3:
|
||||
return "mp3"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"git.maid.zone/stuff/soundcloak/lib/cfg"
|
||||
"git.maid.zone/stuff/soundcloak/lib/textparsing"
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/gorilla/feeds"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
@@ -137,8 +136,11 @@ func GetUser(cid string, permalink string) (User, error) {
|
||||
return u, err
|
||||
}
|
||||
|
||||
func SearchUsers(cid string, prefs cfg.Preferences, args string) (*Paginated[*User], error) {
|
||||
p := Paginated[*User]{Next: "https://" + api + "/search/users" + args}
|
||||
func SearchUsers(cid string, prefs cfg.Preferences, args []byte) (*Paginated[*User], error) {
|
||||
uri := baseUri()
|
||||
uri.SetPath("/search/users")
|
||||
uri.SetQueryStringBytes(args)
|
||||
p := Paginated[*User]{Next: uri}
|
||||
err := p.Proceed(cid, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -152,10 +154,15 @@ func SearchUsers(cid string, prefs cfg.Preferences, args string) (*Paginated[*Us
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func (u User) baseUri(subpath, args string) *fasthttp.URI {
|
||||
uri := baseUri()
|
||||
uri.SetPath("/users/" + string(u.ID) + "/" + subpath)
|
||||
uri.SetQueryString(args)
|
||||
return uri
|
||||
}
|
||||
|
||||
func (u User) GetTracks(cid string, prefs cfg.Preferences, args string) (*Paginated[*Track], error) {
|
||||
p := Paginated[*Track]{
|
||||
Next: "https://" + api + "/users/" + string(u.ID) + "/tracks" + args,
|
||||
}
|
||||
p := Paginated[*Track]{Next: u.baseUri("tracks", args)}
|
||||
|
||||
err := p.Proceed(cid, true)
|
||||
if err != nil {
|
||||
@@ -232,9 +239,7 @@ func (u *User) Postfix(prefs cfg.Preferences) {
|
||||
}
|
||||
|
||||
func (u User) GetPlaylists(cid string, prefs cfg.Preferences, args string) (*Paginated[*Playlist], error) {
|
||||
p := Paginated[*Playlist]{
|
||||
Next: "https://" + api + "/users/" + string(u.ID) + "/playlists_without_albums" + args,
|
||||
}
|
||||
p := Paginated[*Playlist]{Next: u.baseUri("playlists_without_albums", args)}
|
||||
|
||||
err := p.Proceed(cid, true)
|
||||
if err != nil {
|
||||
@@ -250,9 +255,7 @@ func (u User) GetPlaylists(cid string, prefs cfg.Preferences, args string) (*Pag
|
||||
}
|
||||
|
||||
func (u User) GetAlbums(cid string, prefs cfg.Preferences, args string) (*Paginated[*Playlist], error) {
|
||||
p := Paginated[*Playlist]{
|
||||
Next: "https://" + api + "/users/" + string(u.ID) + "/albums" + args,
|
||||
}
|
||||
p := Paginated[*Playlist]{Next: u.baseUri("albums", args)}
|
||||
|
||||
err := p.Proceed(cid, true)
|
||||
if err != nil {
|
||||
@@ -268,9 +271,10 @@ func (u User) GetAlbums(cid string, prefs cfg.Preferences, args string) (*Pagina
|
||||
}
|
||||
|
||||
func (u User) GetReposts(cid string, prefs cfg.Preferences, args string) (*Paginated[*Repost], error) {
|
||||
p := Paginated[*Repost]{
|
||||
Next: "https://" + api + "/stream/users/" + string(u.ID) + "/reposts" + args,
|
||||
}
|
||||
uri := baseUri()
|
||||
uri.SetPath("/stream/users/" + string(u.ID) + "/reposts")
|
||||
uri.SetQueryString(args)
|
||||
p := Paginated[*Repost]{Next: uri}
|
||||
|
||||
err := p.Proceed(cid, true)
|
||||
if err != nil {
|
||||
@@ -285,9 +289,7 @@ func (u User) GetReposts(cid string, prefs cfg.Preferences, args string) (*Pagin
|
||||
}
|
||||
|
||||
func (u User) GetLikes(cid string, prefs cfg.Preferences, args string) (*Paginated[*Like], error) {
|
||||
p := Paginated[*Like]{
|
||||
Next: "https://" + api + "/users/" + string(u.ID) + "/likes" + args,
|
||||
}
|
||||
p := Paginated[*Like]{Next: u.baseUri("likes", args)}
|
||||
|
||||
err := p.Proceed(cid, true)
|
||||
if err != nil {
|
||||
@@ -338,8 +340,11 @@ func (u *User) GetWebProfiles(cid string) error {
|
||||
}
|
||||
|
||||
func (u User) GetRelated(cid string, prefs cfg.Preferences) ([]*User, error) {
|
||||
uri := baseUri()
|
||||
uri.SetPath("/users/" + string(u.ID) + "/relatedartists")
|
||||
uri.QueryArgs().Set("page_size", "20")
|
||||
p := Paginated[*User]{
|
||||
Next: "https://" + api + "/users/" + string(u.ID) + "/relatedartists?page_size=20",
|
||||
Next: uri,
|
||||
}
|
||||
|
||||
err := p.Proceed(cid, true)
|
||||
@@ -356,9 +361,10 @@ func (u User) GetRelated(cid string, prefs cfg.Preferences) ([]*User, error) {
|
||||
}
|
||||
|
||||
func (u User) GetTopTracks(cid string, prefs cfg.Preferences) ([]*Track, error) {
|
||||
p := Paginated[*Track]{
|
||||
Next: "https://" + api + "/users/" + string(u.ID) + "/toptracks?limit=10",
|
||||
}
|
||||
uri := baseUri()
|
||||
uri.SetPath("/users/" + string(u.ID) + "/toptracks")
|
||||
uri.QueryArgs().Set("limit", "10")
|
||||
p := Paginated[*Track]{Next: uri}
|
||||
|
||||
err := p.Proceed(cid, true)
|
||||
if err != nil {
|
||||
@@ -374,9 +380,7 @@ func (u User) GetTopTracks(cid string, prefs cfg.Preferences) ([]*Track, error)
|
||||
}
|
||||
|
||||
func (u User) GetFollowers(cid string, prefs cfg.Preferences, args string) (*Paginated[*User], error) {
|
||||
p := Paginated[*User]{
|
||||
Next: "https://" + api + "/users/" + string(u.ID) + "/followers" + args,
|
||||
}
|
||||
p := Paginated[*User]{Next: u.baseUri("followers", args)}
|
||||
|
||||
err := p.Proceed(cid, true)
|
||||
if err != nil {
|
||||
@@ -392,9 +396,7 @@ func (u User) GetFollowers(cid string, prefs cfg.Preferences, args string) (*Pag
|
||||
}
|
||||
|
||||
func (u User) GetFollowing(cid string, prefs cfg.Preferences, args string) (*Paginated[*User], error) {
|
||||
p := Paginated[*User]{
|
||||
Next: "https://" + api + "/users/" + string(u.ID) + "/followings" + args,
|
||||
}
|
||||
p := Paginated[*User]{Next: u.baseUri("followings", args)}
|
||||
|
||||
err := p.Proceed(cid, true)
|
||||
if err != nil {
|
||||
@@ -420,12 +422,12 @@ func t(s string) string {
|
||||
|
||||
// TODO: maybe add option for caching generated feeds? could benefit when many people follow same artists
|
||||
func (u *User) GenerateFeed(ctx context.Context, cid string, prefs cfg.Preferences, base string) ([]byte, error) {
|
||||
tracks, err := u.GetTracks(cid, prefs, "?limit=20")
|
||||
tracks, err := u.GetTracks(cid, prefs, "limit=20")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f := feeds.RssFeed{
|
||||
f := RssFeed{
|
||||
Title: "Tracks from " + u.Username,
|
||||
Link: base + "/" + u.Permalink,
|
||||
ManagingEditor: u.Username + " (@" + u.Permalink + ")",
|
||||
@@ -439,12 +441,12 @@ func (u *User) GenerateFeed(ctx context.Context, cid string, prefs cfg.Preferenc
|
||||
if len(tracks.Collection) != 0 {
|
||||
f.LastBuildDate = t(tracks.Collection[0].LastModified)
|
||||
for _, track := range tracks.Collection {
|
||||
item := feeds.RssItem{
|
||||
item := RssItem{
|
||||
Title: track.Title,
|
||||
Link: base + "/" + u.Permalink + "/" + track.Permalink,
|
||||
|
||||
Category: track.Genre,
|
||||
Guid: &feeds.RssGuid{Id: string(track.ID), IsPermaLink: "false"},
|
||||
Guid: &RssGuid{Id: string(track.ID), IsPermaLink: "false"},
|
||||
PubDate: t(track.LastModified),
|
||||
}
|
||||
|
||||
@@ -469,7 +471,7 @@ func (u *User) GenerateFeed(ctx context.Context, cid string, prefs cfg.Preferenc
|
||||
}
|
||||
f.PubDate = f.LastBuildDate
|
||||
|
||||
return xml.Marshal(feeds.RssFeedXml{
|
||||
return xml.Marshal(RssFeedXml{
|
||||
Version: "2.0",
|
||||
Channel: &f,
|
||||
ContentNamespace: "http://purl.org/rss/1.0/modules/content/",
|
||||
|
||||
259
main.go
259
main.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
@@ -319,7 +320,7 @@ func main() {
|
||||
return err
|
||||
}
|
||||
|
||||
return r(c, "", templates.MainPage(prefs), templates.MainPageHead())
|
||||
return r(c, "", templates.MainPage(prefs), templates.MainPageHead(prefs))
|
||||
}
|
||||
|
||||
app.Get("/", mainPageHandler)
|
||||
@@ -345,6 +346,10 @@ func main() {
|
||||
return c.Redirect().Status(fiber.StatusPermanentRedirect).To("/_/static/favicon.ico")
|
||||
})
|
||||
|
||||
app.Get("apple-touch-icon.png", func(c fiber.Ctx) error {
|
||||
return c.Redirect().Status(fiber.StatusPermanentRedirect).To("/_/static/favicon.ico")
|
||||
})
|
||||
|
||||
app.Get("robots.txt", func(c fiber.Ctx) error {
|
||||
return c.SendString(`User-agent: *
|
||||
Disallow: /`)
|
||||
@@ -356,40 +361,84 @@ Disallow: /`)
|
||||
return err
|
||||
}
|
||||
|
||||
q := cfg.B2s(c.RequestCtx().QueryArgs().Peek("q"))
|
||||
q := 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)
|
||||
args := c.RequestCtx().QueryArgs().Peek("pagination")
|
||||
if len(args) == 0 {
|
||||
args = fasthttp.AppendQuotedArg([]byte("q="), q)
|
||||
}
|
||||
|
||||
switch t {
|
||||
case "any":
|
||||
q := cfg.B2s(q)
|
||||
p, err := sc.Search("", prefs, args)
|
||||
if err != nil {
|
||||
log.Printf("error getting any for %s: %s\n", q, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if q == "" {
|
||||
q = p.NextHref[sc.H+len("/search?"):]
|
||||
i := strings.LastIndexByte(q, '&')
|
||||
if i != -1 {
|
||||
q, _ = url.QueryUnescape(q[i+len("&q="):])
|
||||
}
|
||||
}
|
||||
|
||||
return r(c, q, templates.Search(p, prefs, q), templates.MainPageHead(prefs))
|
||||
case "tracks":
|
||||
q := cfg.B2s(q)
|
||||
p, err := sc.SearchTracks("", prefs, args)
|
||||
if err != nil {
|
||||
log.Printf("error getting tracks for %s: %s\n", q, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return r(c, "tracks: "+q, templates.SearchTracks(p), nil)
|
||||
if q == "" {
|
||||
q = p.NextHref[sc.H+len("/search/tracks?"):]
|
||||
i := strings.LastIndexByte(q, '&')
|
||||
if i != -1 {
|
||||
q, _ = url.QueryUnescape(q[i+len("&q="):])
|
||||
}
|
||||
}
|
||||
|
||||
return r(c, "tracks: "+q, templates.SearchTracks(p, prefs, q), templates.MainPageHead(prefs))
|
||||
|
||||
case "users":
|
||||
q := cfg.B2s(q)
|
||||
p, err := sc.SearchUsers("", prefs, args)
|
||||
if err != nil {
|
||||
log.Printf("error getting users for %s: %s\n", q, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return r(c, "users: "+q, templates.SearchUsers(p), nil)
|
||||
if q == "" {
|
||||
q = p.NextHref[sc.H+len("/search/users?"):]
|
||||
i := strings.LastIndexByte(q, '&')
|
||||
if i != -1 {
|
||||
q, _ = url.QueryUnescape(q[i+len("&q="):])
|
||||
}
|
||||
}
|
||||
|
||||
return r(c, "users: "+q, templates.SearchUsers(p, prefs, q), templates.MainPageHead(prefs))
|
||||
|
||||
case "playlists":
|
||||
q := cfg.B2s(q)
|
||||
p, err := sc.SearchPlaylists("", prefs, args)
|
||||
if err != nil {
|
||||
log.Printf("error getting playlists for %s: %s\n", q, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return r(c, "playlists: "+q, templates.SearchPlaylists(p), nil)
|
||||
if q == "" {
|
||||
q = p.NextHref[sc.H+len("/search/playlists?"):]
|
||||
i := strings.LastIndexByte(q, '&')
|
||||
if i != -1 {
|
||||
q, _ = url.QueryUnescape(q[i+len("&q="):])
|
||||
}
|
||||
}
|
||||
|
||||
return r(c, "playlists: "+q, templates.SearchPlaylists(p, prefs, q), templates.MainPageHead(prefs))
|
||||
}
|
||||
|
||||
return c.SendStatus(404)
|
||||
@@ -465,13 +514,28 @@ Disallow: /`)
|
||||
displayErr := ""
|
||||
stream := ""
|
||||
|
||||
if *prefs.Player != cfg.NonePlayer {
|
||||
tr, _ := track.Media.SelectCompatible(*prefs.HLSAudio, false)
|
||||
if *prefs.Player == cfg.HLSPlayer {
|
||||
var tr *sc.Transcoding
|
||||
tr, _ = track.Media.SelectCompatible(*prefs.HLSAudio, false, false)
|
||||
if tr == nil {
|
||||
err = sc.ErrIncompatibleStream
|
||||
} else if *prefs.Player == cfg.HLSPlayer {
|
||||
} else {
|
||||
stream, err = tr.GetStream(cid, prefs, track.Authorization)
|
||||
}
|
||||
} else if *prefs.Player == cfg.RestreamPlayer {
|
||||
_, audio := track.Media.SelectCompatible(*prefs.RestreamAudio, true, true)
|
||||
if audio == "" {
|
||||
err = sc.ErrIncompatibleStream
|
||||
}
|
||||
} else {
|
||||
var tr *sc.Transcoding
|
||||
tr = track.Media.SelectCompatibleProgressive()
|
||||
if tr == nil {
|
||||
err = sc.ErrIncompatibleStream
|
||||
} else {
|
||||
stream, err = tr.GetStream(cid, prefs, track.Authorization)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
displayErr = "Failed to get track stream: " + err.Error()
|
||||
@@ -479,7 +543,6 @@ Disallow: /`)
|
||||
displayErr += "\nThis track may be blocked in the country where this instance is hosted."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return render(c, templates.TrackEmbed(prefs, track, stream, displayErr))
|
||||
})
|
||||
@@ -491,7 +554,7 @@ Disallow: /`)
|
||||
}
|
||||
|
||||
tag := c.Params("tag")
|
||||
p, err := sc.RecentTracks("", prefs, c.Query("pagination", tag+"?limit=20"))
|
||||
p, err := sc.RecentTracks("", prefs, tag, c.Query("pagination", "limit=20"))
|
||||
if err != nil {
|
||||
log.Printf("error getting %s tagged recent-tracks: %s\n", tag, err)
|
||||
return err
|
||||
@@ -507,7 +570,12 @@ Disallow: /`)
|
||||
}
|
||||
|
||||
tag := c.Params("tag")
|
||||
p, err := sc.SearchTracks("", prefs, c.Query("pagination", "?q=*&filter.genre_or_tag="+tag+"&sort=popular"))
|
||||
args := c.RequestCtx().QueryArgs().Peek("pagination")
|
||||
if len(args) == 0 {
|
||||
args = []byte("q=*&filter.genre_or_tag=" + tag + "&sort=popular")
|
||||
}
|
||||
|
||||
p, err := sc.SearchTracks("", prefs, args)
|
||||
if err != nil {
|
||||
log.Printf("error getting %s tagged popular-tracks: %s\n", tag, err)
|
||||
return err
|
||||
@@ -524,7 +592,12 @@ Disallow: /`)
|
||||
|
||||
tag := c.Params("tag")
|
||||
// Using a different method, since /playlists/discovery endpoint seems to be broken :P
|
||||
p, err := sc.SearchPlaylists("", prefs, c.Query("pagination", "?q=*&filter.genre_or_tag="+tag))
|
||||
args := c.RequestCtx().QueryArgs().Peek("pagination")
|
||||
if len(args) == 0 {
|
||||
args = []byte("q=*&filter.genre_or_tag=" + tag)
|
||||
}
|
||||
|
||||
p, err := sc.SearchPlaylists("", prefs, args)
|
||||
if err != nil {
|
||||
log.Printf("error getting %s tagged playlists: %s\n", tag, err)
|
||||
return err
|
||||
@@ -594,6 +667,64 @@ Disallow: /`)
|
||||
|
||||
if cfg.Restream {
|
||||
restream.Load(app)
|
||||
|
||||
app.Get("/_/download/:author/:track", func(c fiber.Ctx) error {
|
||||
p, err := preferences.Get(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.ProxyImages = &cfg.False
|
||||
p.ProxyStreams = &cfg.False
|
||||
|
||||
cid, err := sc.GetClientID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t, err := sc.GetTrack(cid, c.Params("author")+"/"+c.Params("track"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
disabled_formats := map[string]bool{
|
||||
cfg.AudioBest: false,
|
||||
cfg.AudioAAC: true,
|
||||
cfg.AudioOpus: true,
|
||||
cfg.AudioMP3: true,
|
||||
}
|
||||
fmt.Println(t.Media.Transcodings)
|
||||
for _, tr := range t.Media.Transcodings {
|
||||
switch tr.Format.Protocol {
|
||||
case sc.ProtocolHLS:
|
||||
if tr.Preset == "aac_160k" {
|
||||
disabled_formats[cfg.AudioAAC] = false
|
||||
} else if tr.Format.MimeType == "audio/mpeg" {
|
||||
disabled_formats[cfg.AudioMP3] = false
|
||||
} else if strings.HasPrefix(tr.Preset, "opus_") {
|
||||
disabled_formats[cfg.AudioOpus] = false
|
||||
}
|
||||
case sc.ProtocolProgressive:
|
||||
if tr.Format.MimeType == "audio/mpeg" {
|
||||
disabled_formats[cfg.AudioMP3] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if disabled_formats[cfg.AudioAAC] && disabled_formats[cfg.AudioOpus] && disabled_formats[cfg.AudioMP3] {
|
||||
disabled_formats[cfg.AudioBest] = true
|
||||
}
|
||||
|
||||
if disabled_formats[*p.DownloadAudio] {
|
||||
pr := cfg.AudioMP3
|
||||
p.DownloadAudio = &pr
|
||||
}
|
||||
|
||||
return r(c, "Download "+t.Title+" by "+t.Author.Username, templates.DownloadTrack(p, t, disabled_formats), nil)
|
||||
})
|
||||
|
||||
app.Post("/_/download/:author/:track", func(c fiber.Ctx) error {
|
||||
return c.Redirect().To("/_/restream/" + c.Params("author") + "/" + c.Params("track") + "?metadata=true&" + strings.ReplaceAll(cfg.B2s(c.Body()), "+", "%20"))
|
||||
})
|
||||
}
|
||||
|
||||
preferences.Load(app)
|
||||
@@ -609,9 +740,64 @@ Disallow: /`)
|
||||
return err
|
||||
}
|
||||
|
||||
if string(c.RequestCtx().QueryArgs().Peek("format")) == "opensearch" {
|
||||
return c.JSON([]any{q, s})
|
||||
}
|
||||
|
||||
return c.JSON(s)
|
||||
})
|
||||
|
||||
{
|
||||
type URL struct {
|
||||
XMLName xml.Name `xml:"Url"`
|
||||
Method string `xml:"method,attr"`
|
||||
Template string `xml:"template,attr"`
|
||||
Type string `xml:"type,attr"`
|
||||
Rel string `xml:"rel,attr,omitempty"`
|
||||
}
|
||||
|
||||
type Description struct {
|
||||
XMLName xml.Name `xml:"http://a9.com/-/spec/opensearch/1.1/ OpenSearchDescription"`
|
||||
Description string
|
||||
LongName string
|
||||
ShortName string
|
||||
Image string
|
||||
URLs []URL
|
||||
}
|
||||
|
||||
app.Get("/_/opensearch.xml", func(c fiber.Ctx) error {
|
||||
base := c.BaseURL()
|
||||
|
||||
d := Description{
|
||||
ShortName: "soundcloak",
|
||||
LongName: "soundcloak",
|
||||
Description: "Frontend for SoundCloud",
|
||||
Image: base + "/_/static/favicon.ico",
|
||||
URLs: []URL{
|
||||
{
|
||||
Method: "get",
|
||||
Template: base + "/search?q={searchTerms}&type=any",
|
||||
Type: "text/html",
|
||||
Rel: "results",
|
||||
},
|
||||
{
|
||||
Method: "get",
|
||||
Template: base + "/_/searchSuggestions?q={searchTerms}&format=opensearch",
|
||||
Type: "application/x-suggestions+json",
|
||||
},
|
||||
{
|
||||
Method: "get",
|
||||
Template: base + "/_/opensearch.xml",
|
||||
Type: "application/opensearchdescription+xml",
|
||||
Rel: "self",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return c.XML(d)
|
||||
})
|
||||
}
|
||||
|
||||
// Currently, /:user is the tracks page
|
||||
app.Get("/:user/tracks", func(c fiber.Ctx) error {
|
||||
return c.Redirect().To("/" + c.Params("user"))
|
||||
@@ -635,7 +821,7 @@ Disallow: /`)
|
||||
}
|
||||
user.Postfix(prefs)
|
||||
|
||||
pl, err := user.GetPlaylists(cid, prefs, c.Query("pagination", "?limit=20"))
|
||||
pl, err := user.GetPlaylists(cid, prefs, c.Query("pagination", "limit=20"))
|
||||
if err != nil {
|
||||
log.Printf("error getting %s playlists: %s\n", c.Params("user"), err)
|
||||
return err
|
||||
@@ -662,7 +848,7 @@ Disallow: /`)
|
||||
}
|
||||
user.Postfix(prefs)
|
||||
|
||||
pl, err := user.GetAlbums(cid, prefs, c.Query("pagination", "?limit=20"))
|
||||
pl, err := user.GetAlbums(cid, prefs, c.Query("pagination", "limit=20"))
|
||||
if err != nil {
|
||||
log.Printf("error getting %s albums: %s\n", c.Params("user"), err)
|
||||
return err
|
||||
@@ -689,7 +875,7 @@ Disallow: /`)
|
||||
}
|
||||
user.Postfix(prefs)
|
||||
|
||||
p, err := user.GetReposts(cid, prefs, c.Query("pagination", "?limit=20"))
|
||||
p, err := user.GetReposts(cid, prefs, c.Query("pagination", "limit=20"))
|
||||
if err != nil {
|
||||
log.Printf("error getting %s reposts: %s\n", c.Params("user"), err)
|
||||
return err
|
||||
@@ -716,7 +902,7 @@ Disallow: /`)
|
||||
}
|
||||
user.Postfix(prefs)
|
||||
|
||||
p, err := user.GetLikes(cid, prefs, c.Query("pagination", "?limit=20"))
|
||||
p, err := user.GetLikes(cid, prefs, c.Query("pagination", "limit=20"))
|
||||
if err != nil {
|
||||
log.Printf("error getting %s likes: %s\n", c.Params("user"), err)
|
||||
return err
|
||||
@@ -770,7 +956,7 @@ Disallow: /`)
|
||||
}
|
||||
user.Postfix(prefs)
|
||||
|
||||
p, err := user.GetFollowers(cid, prefs, c.Query("pagination", "?limit=20"))
|
||||
p, err := user.GetFollowers(cid, prefs, c.Query("pagination", "limit=20"))
|
||||
if err != nil {
|
||||
log.Printf("error getting %s followers: %s\n", c.Params("user"), err)
|
||||
return err
|
||||
@@ -797,7 +983,7 @@ Disallow: /`)
|
||||
}
|
||||
user.Postfix(prefs)
|
||||
|
||||
p, err := user.GetFollowing(cid, prefs, c.Query("pagination", "?limit=20"))
|
||||
p, err := user.GetFollowing(cid, prefs, c.Query("pagination", "limit=20"))
|
||||
if err != nil {
|
||||
log.Printf("error getting %s following: %s\n", c.Params("user"), err)
|
||||
return err
|
||||
@@ -831,17 +1017,26 @@ Disallow: /`)
|
||||
if *prefs.Player != cfg.NonePlayer {
|
||||
if *prefs.Player == cfg.HLSPlayer {
|
||||
var tr *sc.Transcoding
|
||||
tr, audio = track.Media.SelectCompatible(*prefs.HLSAudio, false)
|
||||
tr, audio = track.Media.SelectCompatible(*prefs.HLSAudio, false, false)
|
||||
if tr == nil {
|
||||
err = sc.ErrIncompatibleStream
|
||||
} else {
|
||||
stream, err = tr.GetStream(cid, prefs, track.Authorization)
|
||||
}
|
||||
} else {
|
||||
_, audio = track.Media.SelectCompatible(*prefs.RestreamAudio, true)
|
||||
} else if *prefs.Player == cfg.RestreamPlayer {
|
||||
_, audio = track.Media.SelectCompatible(*prefs.RestreamAudio, true, true)
|
||||
if audio == "" {
|
||||
err = sc.ErrIncompatibleStream
|
||||
}
|
||||
} else {
|
||||
audio = cfg.AudioMP3
|
||||
var tr *sc.Transcoding
|
||||
tr = track.Media.SelectCompatibleProgressive()
|
||||
if tr == nil {
|
||||
err = sc.ErrIncompatibleStream
|
||||
} else {
|
||||
stream, err = tr.GetStream(cid, prefs, track.Authorization)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -918,7 +1113,7 @@ Disallow: /`)
|
||||
|
||||
var downloadAudio *string
|
||||
if cfg.Restream {
|
||||
_, audio := track.Media.SelectCompatible(*prefs.DownloadAudio, true)
|
||||
_, audio := track.Media.SelectCompatible(*prefs.DownloadAudio, true, true)
|
||||
downloadAudio = &audio
|
||||
}
|
||||
|
||||
@@ -947,8 +1142,9 @@ Disallow: /`)
|
||||
return err
|
||||
}
|
||||
|
||||
if comm.Next != "" {
|
||||
c.Set("next", "?pagination="+url.QueryEscape(strings.Split(comm.Next, "/comments")[1]))
|
||||
if comm.NextHref != "" {
|
||||
misc.Log(comm.NextHref)
|
||||
c.Set("next", "?pagination="+url.QueryEscape(strings.Split(comm.NextHref, "/comments?")[1]))
|
||||
} else {
|
||||
c.Set("next", "done")
|
||||
}
|
||||
@@ -1008,7 +1204,7 @@ Disallow: /`)
|
||||
}
|
||||
usr.Postfix(prefs)
|
||||
|
||||
p, err := usr.GetTracks(cid, prefs, c.Query("pagination", "?limit=20"))
|
||||
p, err := usr.GetTracks(cid, prefs, c.Query("pagination", "limit=20"))
|
||||
if err != nil {
|
||||
log.Printf("error getting %s tracks: %s\n", c.Params("user"), err)
|
||||
return err
|
||||
@@ -1102,7 +1298,7 @@ Disallow: /`)
|
||||
}
|
||||
track.Postfix(prefs, true)
|
||||
|
||||
rel, err := track.GetRelated(cid, prefs, c.Query("pagination", "?limit=20"))
|
||||
rel, err := track.GetRelated(cid, prefs, c.Query("pagination", "limit=20"))
|
||||
if err != nil {
|
||||
log.Printf("error getting %s from %s related tracks: %s\n", c.Params("track"), c.Params("user"), err)
|
||||
return err
|
||||
@@ -1129,7 +1325,7 @@ Disallow: /`)
|
||||
}
|
||||
track.Postfix(prefs, true)
|
||||
|
||||
p, err := track.GetPlaylists(cid, prefs, c.Query("pagination", "?limit=20"))
|
||||
p, err := track.GetPlaylists(cid, prefs, c.Query("pagination", "limit=20"))
|
||||
if err != nil {
|
||||
log.Printf("error getting %s from %s sets: %s\n", c.Params("track"), c.Params("user"), err)
|
||||
return err
|
||||
@@ -1156,7 +1352,7 @@ Disallow: /`)
|
||||
}
|
||||
track.Postfix(prefs, true)
|
||||
|
||||
p, err := track.GetAlbums(cid, prefs, c.Query("pagination", "?limit=20"))
|
||||
p, err := track.GetAlbums(cid, prefs, c.Query("pagination", "limit=20"))
|
||||
if err != nil {
|
||||
log.Printf("error getting %s from %s albums: %s\n", c.Params("track"), c.Params("user"), err)
|
||||
return err
|
||||
@@ -1190,6 +1386,7 @@ Disallow: /`)
|
||||
if cfg.Addr[0] == ':' {
|
||||
table["Listening on"] = "127.0.0.1" + cfg.Addr
|
||||
}
|
||||
table["Listening on"] = "http://" + table["Listening on"]
|
||||
longest := ""
|
||||
for key := range table {
|
||||
if len(key) > len(longest) {
|
||||
|
||||
@@ -17,11 +17,3 @@
|
||||
#search-suggestions > li:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
footer > div {
|
||||
margin-top: 5rem;
|
||||
gap: 1rem;
|
||||
display: grid;
|
||||
grid-template: auto / auto auto auto;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ templ Base(title string, content templ.Component, head templ.Component) {
|
||||
<link rel="stylesheet" href="/_/static/global.css"/>
|
||||
<link rel="stylesheet" href="/_/static/instance.css"/>
|
||||
<link rel="icon" href="/_/static/favicon.ico"/>
|
||||
<link rel="search" type="application/opensearchdescription+xml" title="soundcloak" href="/_/opensearch.xml"/>
|
||||
<title>
|
||||
if title != "" {
|
||||
{ title } ~
|
||||
@@ -31,36 +32,22 @@ templ Base(title string, content templ.Component, head templ.Component) {
|
||||
</html>
|
||||
}
|
||||
|
||||
templ MainPageHead() {
|
||||
templ MainPageHead(p cfg.Preferences) {
|
||||
if *p.SearchSuggestions {
|
||||
<link rel="stylesheet" href="/_/static/index.css"/>
|
||||
}
|
||||
}
|
||||
|
||||
templ MainPage(p cfg.Preferences) {
|
||||
<form action="/search">
|
||||
<div style="position: relative">
|
||||
<div style="display: flex; gap: .5rem;">
|
||||
<input id="q" name="q" type="text" autocomplete="off" autofill="off" style="padding: .5rem .6rem; flex-grow: 1;"/>
|
||||
<select name="type">
|
||||
<option value="tracks">Tracks</option>
|
||||
<option value="users">Users</option>
|
||||
<option value="playlists">Playlists</option>
|
||||
</select>
|
||||
</div>
|
||||
if *p.SearchSuggestions {
|
||||
<ul id="search-suggestions" style="display: none;"></ul>
|
||||
<script async src="/_/static/index.js"></script>
|
||||
}
|
||||
</div>
|
||||
<input type="submit" value="Search" class="btn" style="width: 100%; margin-top: .5rem;"/>
|
||||
</form>
|
||||
@searchbar(p, "", "")
|
||||
<footer>
|
||||
<div>
|
||||
<div style="margin-top:5rem;gap:1rem;display:grid;grid-template:auto/auto auto auto;justify-content:center">
|
||||
<a class="btn" href="/discover">Discover Playlists</a>
|
||||
<a class="btn" href="/_/preferences">Preferences</a>
|
||||
<a class="btn" href="https://git.maid.zone/stuff/soundcloak">Source code</a>
|
||||
<a class="btn" href="/_/static/notice.txt">Legal notice</a>
|
||||
</div>
|
||||
|
||||
<p style="text-align: center;">Build <a class="link" href={cfg.CommitURL}>{cfg.Commit}</a></p>
|
||||
<p style="text-align:center">Build <a class="link" href={templ.SafeURL(cfg.CommitURL)}>{cfg.Commit}</a></p>
|
||||
</footer>
|
||||
}
|
||||
|
||||
51
templates/download.templ
Normal file
51
templates/download.templ
Normal file
@@ -0,0 +1,51 @@
|
||||
package templates
|
||||
|
||||
import "git.maid.zone/stuff/soundcloak/lib/sc"
|
||||
import "git.maid.zone/stuff/soundcloak/lib/cfg"
|
||||
|
||||
templ sel_audio2(name string, selected string, f map[string]bool) {
|
||||
@sel(name, []option{
|
||||
{cfg.AudioBest, "Best", f[cfg.AudioBest]},
|
||||
{cfg.AudioAAC, "M4A AAC 160kb/s", f[cfg.AudioAAC]},
|
||||
{cfg.AudioOpus, "OGG Opus 72kb/s", f[cfg.AudioOpus]},
|
||||
{cfg.AudioMP3, "MP3 128kb/s", f[cfg.AudioMP3]},
|
||||
}, selected)
|
||||
}
|
||||
|
||||
templ text(name string, value string) {
|
||||
<input name={ name } type="text" autocomplete="off" value={value}/>
|
||||
}
|
||||
|
||||
templ DownloadTrack(prefs cfg.Preferences, t sc.Track, disabled_formats map[string]bool) {
|
||||
if t.Artwork != "" {
|
||||
<img src={ t.Artwork } width="300px"/>
|
||||
}
|
||||
<h1><a href={templ.SafeURL(t.Href())} id="title">{ t.Title }</a></h1>
|
||||
if t.Policy == sc.PolicySnip {
|
||||
<h2>Full track not available, only a 30-second snippet.</h2>
|
||||
}
|
||||
// just nice script to redirect you back to track page :)
|
||||
<form method="post" autocomplete="off" onsubmit="setTimeout(function(){location='/'+location.pathname.split('/').slice(3).join('/')},7500)">
|
||||
@sel_audio2("audio", *prefs.RestreamAudio, disabled_formats)
|
||||
<details style="margin-top: 1rem; margin-bottom: 1rem">
|
||||
<summary>File metadata</summary>
|
||||
|
||||
<label>
|
||||
Title:
|
||||
@text("title", t.Title)
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Author:
|
||||
@text("author", t.Author.Username)
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Genre:
|
||||
@text("genre", t.Genre)
|
||||
</label>
|
||||
</details>
|
||||
<input type="submit" value="Download" class="btn" style="margin-top: 1rem;"/>
|
||||
</form>
|
||||
<style>label{display:flex;gap:.5rem;align-items:center;margin-bottom:.35rem}</style>
|
||||
}
|
||||
@@ -5,32 +5,6 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
templ PlaylistOrUserItem(pl *sc.PlaylistOrUser) {
|
||||
<a class="listing" href={ templ.SafeURL(pl.Href()) }>
|
||||
{{
|
||||
img := pl.Artwork
|
||||
if pl.Kind == "user" {
|
||||
img = pl.Avatar
|
||||
}
|
||||
if img == "" {
|
||||
img = "/_/static/placeholder.jpg"
|
||||
}
|
||||
}}
|
||||
<img loading="lazy" fetchpriority="low" src={ img }/>
|
||||
<div class="meta">
|
||||
if pl.Kind == "user" {
|
||||
<h3>{ pl.Username }</h3>
|
||||
if pl.FullName != "" {
|
||||
<span>{ pl.FullName }</span>
|
||||
}
|
||||
} else {
|
||||
<h3>{ pl.Title }</h3>
|
||||
<p>{ strconv.FormatInt(pl.TrackCount, 10) } tracks</p>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
|
||||
templ Discover(p *sc.Paginated[*sc.Selection]) {
|
||||
<h1>Discover Playlists</h1> // also tracks apparently? haven't seen any
|
||||
<span>Got { strconv.FormatInt(int64(len(p.Collection)), 10) } selections</span>
|
||||
@@ -43,7 +17,7 @@ templ Discover(p *sc.Paginated[*sc.Selection]) {
|
||||
|
||||
<h2>{selection.Title}</h2>
|
||||
for _, pl := range selection.Items.Collection {
|
||||
@PlaylistOrUserItem(pl)
|
||||
@UserPlaylistTrackItem(pl)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,13 @@ package templates
|
||||
|
||||
import (
|
||||
"git.maid.zone/stuff/soundcloak/lib/cfg"
|
||||
"git.maid.zone/stuff/soundcloak/lib/sc"
|
||||
"git.maid.zone/stuff/soundcloak/lib/textparsing"
|
||||
"github.com/valyala/fasthttp"
|
||||
"context"
|
||||
"io"
|
||||
"strconv"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
templ Description(prefs cfg.Preferences, text string, injected templ.Component) {
|
||||
@@ -24,3 +30,80 @@ templ Description(prefs cfg.Preferences, text string, injected templ.Component)
|
||||
</details>
|
||||
}
|
||||
}
|
||||
|
||||
func Bytes(b []byte) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
|
||||
_, err := w.Write(fasthttp.AppendHTMLEscapeBytes(nil, b))
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
templ UserPlaylistTrackItem(pl *sc.UserPlaylistTrack) {
|
||||
<a class="listing" href={ templ.SafeURL(pl.Href()) }>
|
||||
{{
|
||||
img := pl.Artwork
|
||||
if pl.Kind == "user" {
|
||||
img = pl.Avatar
|
||||
}
|
||||
if img == "" {
|
||||
img = "/_/static/placeholder.jpg"
|
||||
}
|
||||
}}
|
||||
<img loading="lazy" fetchpriority="low" src={ img }/>
|
||||
<div class="meta">
|
||||
if pl.Kind == "user" {
|
||||
<h3>{ pl.Username }</h3>
|
||||
if pl.FullName != "" {
|
||||
<p>{ pl.FullName }</p>
|
||||
}
|
||||
} else {
|
||||
<h3>{ pl.Title }</h3>
|
||||
if pl.Kind == "track" {
|
||||
<span>{ pl.Author.Username }</span>
|
||||
} else {
|
||||
<p>{ strconv.FormatInt(pl.TracksCount(), 10) } tracks</p>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
|
||||
templ Search(p *sc.Paginated[*sc.UserPlaylistTrack], prefs cfg.Preferences, content string) {
|
||||
@searchbar(prefs, content, "any")
|
||||
<br>
|
||||
<span>Found { strconv.FormatInt(p.Total, 10) } results</span>
|
||||
<br/>
|
||||
<br/>
|
||||
if len(p.Collection) == 0 && p.Total != 0 {
|
||||
<p>no more results</p>
|
||||
} else {
|
||||
for _, u := range p.Collection {
|
||||
@UserPlaylistTrackItem(u)
|
||||
}
|
||||
if p.NextHref != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?type=any&pagination=" + url.QueryEscape(p.NextHref[sc.H+len("/search?"):])) } rel="noreferrer">more results</a>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
templ searchbar(p cfg.Preferences, content string, typ string) {
|
||||
<form action="/search">
|
||||
<div style="position:relative">
|
||||
<div style="display:flex;gap:.5rem">
|
||||
<input id="q" name="q" type="text" autocomplete="off" autofill="off" value={content} style="padding:.5rem.6rem;flex-grow:1"/>
|
||||
@sel("type", []option{
|
||||
{"any", "Anything", false},
|
||||
{"tracks", "Tracks", false},
|
||||
{"users", "Users", false},
|
||||
{"playlists", "Playlists", false},
|
||||
}, typ)
|
||||
</div>
|
||||
if *p.SearchSuggestions {
|
||||
<ul id="search-suggestions" style="display:none"></ul>
|
||||
<script async src="/_/static/index.js"></script>
|
||||
}
|
||||
</div>
|
||||
<input type="submit" value="Search" class="btn" style="width:100%;margin-top:.5rem"/>
|
||||
</form>
|
||||
}
|
||||
@@ -78,7 +78,9 @@ templ Playlist(prefs cfg.Preferences, p sc.Playlist) {
|
||||
</div>
|
||||
}
|
||||
|
||||
templ SearchPlaylists(p *sc.Paginated[*sc.Playlist]) {
|
||||
templ SearchPlaylists(p *sc.Paginated[*sc.Playlist], prefs cfg.Preferences, content string) {
|
||||
@searchbar(prefs, content, "playlist")
|
||||
<br>
|
||||
<span>Found { strconv.FormatInt(p.Total, 10) } playlists</span>
|
||||
<br/>
|
||||
<br/>
|
||||
@@ -90,8 +92,8 @@ templ SearchPlaylists(p *sc.Paginated[*sc.Playlist]) {
|
||||
for _, playlist := range p.Collection {
|
||||
@PlaylistItem(playlist, true)
|
||||
}
|
||||
if p.Next != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?type=playlists&pagination=" + url.QueryEscape(p.Next[sc.H+len("/search/playlists"):])) } rel="noreferrer">more playlists</a>
|
||||
if p.NextHref != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?type=playlists&pagination=" + url.QueryEscape(p.NextHref[sc.H+len("/search/playlists?"):])) } rel="noreferrer">more playlists</a>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,10 @@ templ sel_audio(name string, selected string, noOpus bool) {
|
||||
}, selected)
|
||||
}
|
||||
|
||||
templ js() {
|
||||
<span class="js">(requires JS)</span>
|
||||
}
|
||||
|
||||
templ Preferences(prefs cfg.Preferences) {
|
||||
<h1>Preferences</h1>
|
||||
<form method="post" autocomplete="off">
|
||||
@@ -49,11 +53,54 @@ templ Preferences(prefs cfg.Preferences) {
|
||||
Player:
|
||||
@sel("Player", []option{
|
||||
{cfg.RestreamPlayer, "Restream Player", !cfg.Restream},
|
||||
{cfg.HLSPlayer, "HLS Player (requires JS)", false},
|
||||
{cfg.HLSPlayer, "HLS Player", false},
|
||||
{cfg.ProgressivePlayer, "Progressive Player", cfg.Restream},
|
||||
{cfg.NonePlayer, "None", false},
|
||||
}, *prefs.Player)
|
||||
</label>
|
||||
<details style="margin-bottom:1rem">
|
||||
<summary>Explanation of each player</summary>
|
||||
Each player option uses a different way to deliver/playback the music for you.
|
||||
<ul>
|
||||
<li>
|
||||
Restream Player
|
||||
<p>
|
||||
It's most suitable for listening to music, because audio must be loaded from start to finish.
|
||||
Streaming protocols are handled on server-side, so works without JavaScript enabled.
|
||||
You can stream all the available audio presets with it.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
HLS Player
|
||||
<p>
|
||||
It's recommended if you listen to long audios. Since streaming protocol (HLS) is handled on your browser,
|
||||
you can skip parts of an audio without needing to load them.
|
||||
<span class="js">It also requires enabled JavaScript to function.</span>
|
||||
ogg/opus audio preset cannot be streamed with it.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
Progressive Player
|
||||
<p>
|
||||
Similar to Restream Player, but available in case restream is disabled. You can only stream MP3 at 128kb/s using it.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
None
|
||||
<p>
|
||||
Don't stream audio at all :)
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
switch *prefs.Player {
|
||||
case cfg.ProgressivePlayer:
|
||||
if cfg.ProxyStreams {
|
||||
<label>
|
||||
Proxy song streams:
|
||||
@checkbox("ProxyStreams", *prefs.ProxyStreams)
|
||||
</label>
|
||||
}
|
||||
case cfg.HLSPlayer:
|
||||
if cfg.ProxyStreams {
|
||||
<label>
|
||||
@@ -93,20 +140,20 @@ templ Preferences(prefs cfg.Preferences) {
|
||||
<label>
|
||||
Fetch search suggestions:
|
||||
@checkbox("SearchSuggestions", *prefs.SearchSuggestions)
|
||||
(requires JS)
|
||||
@js()
|
||||
</label>
|
||||
<label>
|
||||
Dynamically load comments:
|
||||
@checkbox("DynamicLoadComments", *prefs.DynamicLoadComments)
|
||||
(requires JS)
|
||||
@js()
|
||||
</label>
|
||||
<label>
|
||||
Keep player focus:
|
||||
@checkbox("KeepPlayerFocus", *prefs.KeepPlayerFocus)
|
||||
(requires JS)
|
||||
@js()
|
||||
</label>
|
||||
<h2 style="margin-bottom: .35rem">Autoplay</h2>
|
||||
<i>Requires JS. You also need to allow autoplay from this domain</i>
|
||||
<i><span class="js">Requires JS. </span>You also need to allow autoplay from this domain</i>
|
||||
<label style="margin-top: 1rem">
|
||||
Autoplay next track in playlists:
|
||||
@checkbox("AutoplayNextTrack", *prefs.AutoplayNextTrack)
|
||||
@@ -138,5 +185,7 @@ templ Preferences(prefs cfg.Preferences) {
|
||||
<input class="btn" type="file" autocomplete="off" name="prefs"/>
|
||||
<input type="submit" value="Import" class="btn"/>
|
||||
</form>
|
||||
<style>label{display:flex;gap:.5rem;align-items:center;margin-bottom:.35rem}</style>
|
||||
// oh mah gah
|
||||
<style>label{display:flex;gap:.5rem;align-items:center;margin-bottom:.35rem}li>p{margin-top:0}</style>
|
||||
<script>var y=document.getElementsByClassName('js');for(x=0;x<y.length;x++)y[x].style='display:none'</script>
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ templ RecentTracks(tag string, p *sc.Paginated[*sc.Track]) {
|
||||
for _, track := range p.Collection {
|
||||
@TrackItem(track, true, "")
|
||||
}
|
||||
if p.Next != "" {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.Next[sc.H+len("/recent-tracks/"):])) } rel="noreferrer">more tracks</a>
|
||||
if p.NextHref != "" {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.NextHref[sc.H+len("/recent-tracks/")+len(tag)+1:])) } rel="noreferrer">more tracks</a>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,8 +49,8 @@ templ PopularTracks(tag string, p *sc.Paginated[*sc.Track]) {
|
||||
for _, track := range p.Collection {
|
||||
@TrackItem(track, true, "")
|
||||
}
|
||||
if p.Next != "" {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.Next[sc.H+len("/search/tracks"):])) } rel="noreferrer">more tracks</a>
|
||||
if p.NextHref != "" {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.NextHref[sc.H+len("/search/tracks?"):])) } rel="noreferrer">more tracks</a>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,8 +69,8 @@ templ TaggedPlaylists(tag string, p *sc.Paginated[*sc.Playlist]) {
|
||||
for _, playlist := range p.Collection {
|
||||
@PlaylistItem(playlist, true)
|
||||
}
|
||||
if p.Next != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.Next[sc.H+len("/search/playlists"):])) } rel="noreferrer">more playlists</a>
|
||||
if p.NextHref != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.NextHref[sc.H+len("/search/playlists?"):])) } rel="noreferrer">more playlists</a>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,19 +8,6 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func toExt(audio string) string {
|
||||
switch audio {
|
||||
case cfg.AudioAAC:
|
||||
return "m4a"
|
||||
case cfg.AudioOpus:
|
||||
return "ogg"
|
||||
case cfg.AudioMP3:
|
||||
return "mp3"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
templ TrackButtons(current string, track sc.Track) {
|
||||
<div class="btns">
|
||||
for _, b := range [...]btn{{"related tracks", "/recommended", false, false},{"in albums", "/albums", false, false},{"in playlists", "/sets", false, false},{"track station", "/discover/sets/"+track.Station, true, false},{"view on soundcloud", "https://soundcloud.com"+track.Href(), true, true}} {
|
||||
@@ -50,7 +37,7 @@ templ TrackHeader(prefs cfg.Preferences, t sc.Track, needPlayer bool) {
|
||||
<meta name="og:title" content={ t.Title }/>
|
||||
<meta name="og:description" content={ t.FormatDescription() }/>
|
||||
<meta name="og:image" content={ t.Artwork }/>
|
||||
<link rel="icon" type="image/x-icon" href={ t.Artwork }/>
|
||||
<link rel="icon" href={ t.Artwork }/>
|
||||
if needPlayer && *prefs.Player == cfg.HLSPlayer {
|
||||
<script src="/_/static/external/hls.light.min.js"></script>
|
||||
}
|
||||
@@ -106,6 +93,24 @@ templ TrackPlayer(prefs cfg.Preferences, track sc.Track, stream string, displayE
|
||||
if *prefs.KeepPlayerFocus {
|
||||
<script async src="/_/static/keepfocus.js"></script>
|
||||
}
|
||||
} else if *prefs.Player == cfg.RestreamPlayer || *prefs.Player == cfg.ProgressivePlayer {
|
||||
{{ audioPref = &cfg.MP3 }}
|
||||
<audio
|
||||
id="track"
|
||||
src={ stream }
|
||||
controls
|
||||
autoplay?={ autoplay }
|
||||
if nextTrack != nil {
|
||||
data-next={ next(&track, nextTrack, playlist, mode, "") }
|
||||
volume={ volume }
|
||||
}
|
||||
></audio>
|
||||
if nextTrack != nil {
|
||||
<script async src="/_/static/restream.js"></script>
|
||||
}
|
||||
if *prefs.KeepPlayerFocus {
|
||||
<script async src="/_/static/keepfocus.js"></script>
|
||||
}
|
||||
} else if stream != "" {
|
||||
{{ audioPref = prefs.HLSAudio }}
|
||||
<audio
|
||||
@@ -132,10 +137,14 @@ templ TrackPlayer(prefs cfg.Preferences, track sc.Track, stream string, displayE
|
||||
<noscript>
|
||||
<br/>
|
||||
JavaScript is disabled! Audio playback may not work without it enabled.
|
||||
if cfg.Restream {
|
||||
<br/>
|
||||
<a class="link" href="/_/preferences">You can enable Restream player in the preferences. It works without JavaScript.</a>
|
||||
<a class="link" href="/_/preferences">You can enable
|
||||
if cfg.Restream {
|
||||
Restream
|
||||
} else {
|
||||
Progressive
|
||||
}
|
||||
player in the preferences. It works without JavaScript.</a>
|
||||
</noscript>
|
||||
}
|
||||
if track.Policy == sc.PolicySnip {
|
||||
@@ -194,7 +203,7 @@ templ Track(prefs cfg.Preferences, t sc.Track, stream string, displayErr string,
|
||||
@TrackPlayer(prefs, t, stream, displayErr, autoplay, nextTrack, playlist, volume, mode, audio)
|
||||
if displayErr == "" && cfg.Restream {
|
||||
<div style="display: flex; margin-bottom: 1rem;">
|
||||
<a class="btn" href={ templ.SafeURL("/_/restream" + t.Href() + "?metadata=true") } download={ t.Permalink + "." + toExt(*downloadAudio) }>download</a>
|
||||
<a class="btn" href={ templ.SafeURL("/_/download" + t.Href()) }>download</a>
|
||||
</div>
|
||||
}
|
||||
if t.Genre != "" {
|
||||
@@ -252,8 +261,8 @@ templ Track(prefs cfg.Preferences, t sc.Track, stream string, displayErr string,
|
||||
@Comments(comments)
|
||||
</div>
|
||||
<script async src="/_/static/comments.js"></script>
|
||||
if comments.Next != "" {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(comments.Next[sc.H+len("/tracks/")+len(string(t.ID))+len("/comments"):])) } rel="noreferrer" onclick="event.preventDefault(); comments(this)" data-id={ string(t.ID) }>more comments</a>
|
||||
if comments.NextHref != "" {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(comments.NextHref[sc.H+len("/tracks/")+len(string(t.ID))+len("/comments?"):])) } rel="noreferrer" onclick="event.preventDefault(); comments(this)" data-id={ string(t.ID) }>more comments</a>
|
||||
}
|
||||
} else {
|
||||
<div id="comments"></div>
|
||||
@@ -265,8 +274,8 @@ templ Track(prefs cfg.Preferences, t sc.Track, stream string, displayErr string,
|
||||
<div>
|
||||
@Comments(comments)
|
||||
</div>
|
||||
if comments.Next != "" {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(comments.Next[sc.H+len("/tracks/")+len(string(t.ID))+len("/comments"):])) } rel="noreferrer">more comments</a>
|
||||
if comments.NextHref != "" {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(comments.NextHref[sc.H+len("/tracks/")+len(string(t.ID))+len("/comments?"):])) } rel="noreferrer">more comments</a>
|
||||
}
|
||||
} else {
|
||||
<a class="btn" href="?pagination=%3Flimit%3D20%26threaded%3D1">load comments</a>
|
||||
@@ -315,7 +324,9 @@ templ TrackEmbed(prefs cfg.Preferences, t sc.Track, stream string, displayErr st
|
||||
</html>
|
||||
}
|
||||
|
||||
templ SearchTracks(p *sc.Paginated[*sc.Track]) {
|
||||
templ SearchTracks(p *sc.Paginated[*sc.Track], prefs cfg.Preferences, content string) {
|
||||
@searchbar(prefs, content, "tracks")
|
||||
<br>
|
||||
<span>Found { strconv.FormatInt(p.Total, 10) } tracks</span>
|
||||
<br/>
|
||||
<br/>
|
||||
@@ -325,8 +336,8 @@ templ SearchTracks(p *sc.Paginated[*sc.Track]) {
|
||||
for _, track := range p.Collection {
|
||||
@TrackItem(track, true, "")
|
||||
}
|
||||
if p.Next != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?type=tracks&pagination=" + url.QueryEscape(p.Next[sc.H+len("/search/tracks"):])) } rel="noreferrer">more tracks</a>
|
||||
if p.NextHref != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?type=tracks&pagination=" + url.QueryEscape(p.NextHref[sc.H+len("/search/tracks?"):])) } rel="noreferrer">more tracks</a>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,8 +355,8 @@ templ RelatedTracks(t sc.Track, p *sc.Paginated[*sc.Track]) {
|
||||
for _, track := range p.Collection {
|
||||
@TrackItem(track, true, "")
|
||||
}
|
||||
if p.Next != "" {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.Next[sc.H+len("/tracks/")+len(string(t.ID))+len("/related"):])) } rel="noreferrer">more tracks</a>
|
||||
if p.NextHref != "" {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.NextHref[sc.H+len("/tracks/")+len(string(t.ID))+len("/related?"):])) } rel="noreferrer">more tracks</a>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -363,8 +374,8 @@ templ TrackInAlbums(t sc.Track, p *sc.Paginated[*sc.Playlist]) {
|
||||
for _, playlist := range p.Collection {
|
||||
@PlaylistItem(playlist, true)
|
||||
}
|
||||
if p.Next != "" {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.Next[sc.H+len("/tracks/")+len(string(t.ID))+len("/albums"):])) } rel="noreferrer">more albums</a>
|
||||
if p.NextHref != "" {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.NextHref[sc.H+len("/tracks/")+len(string(t.ID))+len("/albums?"):])) } rel="noreferrer">more albums</a>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,8 +393,8 @@ templ TrackInPlaylists(t sc.Track, p *sc.Paginated[*sc.Playlist]) {
|
||||
for _, playlist := range p.Collection {
|
||||
@PlaylistItem(playlist, true)
|
||||
}
|
||||
if p.Next != "" {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.Next[sc.H+len("/tracks/")+len(string(t.ID))+len("/playlists_without_albums"):])) } rel="noreferrer">more playlists</a>
|
||||
if p.NextHref != "" {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.NextHref[sc.H+len("/tracks/")+len(string(t.ID))+len("/playlists_without_albums?"):])) } rel="noreferrer">more playlists</a>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ templ UserHeader(u sc.User) {
|
||||
<meta name="og:title" content={ u.FormatUsername() }/>
|
||||
<meta name="og:description" content={ u.FormatDescription() }/>
|
||||
<meta name="og:image" content={ u.Avatar }/>
|
||||
<link rel="icon" type="image/x-icon" href={ u.Avatar }/>
|
||||
<link rel="icon" href={ u.Avatar }/>
|
||||
}
|
||||
|
||||
templ UserItem(user *sc.User) {
|
||||
@@ -127,8 +127,8 @@ templ User(prefs cfg.Preferences, u sc.User, p *sc.Paginated[*sc.Track]) {
|
||||
@TrackItem(track, false, "")
|
||||
}
|
||||
</div>
|
||||
if p.Next != "" && len(p.Collection) != int(u.Tracks) {
|
||||
<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>
|
||||
if p.NextHref != "" && len(p.Collection) != int(u.Tracks) {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.NextHref[sc.H+len("/users/")+len(u.ID)+len("/tracks?"):])) } rel="noreferrer">more tracks</a>
|
||||
}
|
||||
} else {
|
||||
<span>no more tracks</span>
|
||||
@@ -145,8 +145,8 @@ templ UserPlaylists(prefs cfg.Preferences, u sc.User, p *sc.Paginated[*sc.Playli
|
||||
@PlaylistItem(playlist, false)
|
||||
}
|
||||
</div>
|
||||
if p.Next != "" && len(p.Collection) != int(p.Total) {
|
||||
<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>
|
||||
if p.NextHref != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.NextHref[sc.H+len("/users/")+len(u.ID)+len("/playlists_without_albums?"):])) } rel="noreferrer">more playlists</a>
|
||||
}
|
||||
} else {
|
||||
<span>no more playlists</span>
|
||||
@@ -163,8 +163,8 @@ templ UserAlbums(prefs cfg.Preferences, u sc.User, p *sc.Paginated[*sc.Playlist]
|
||||
@PlaylistItem(playlist, false)
|
||||
}
|
||||
</div>
|
||||
if p.Next != "" && len(p.Collection) != int(p.Total) {
|
||||
<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>
|
||||
if p.NextHref != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.NextHref[sc.H+len("/users/")+len(u.ID)+len("/albums?"):])) } rel="noreferrer">more albums</a>
|
||||
}
|
||||
} else {
|
||||
<span>no more albums</span>
|
||||
@@ -185,8 +185,8 @@ 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(p.Next[sc.H+len("/stream/users/")+len(u.ID)+len("/reposts"):])) } rel="noreferrer">more reposts</a>
|
||||
if p.NextHref != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.NextHref[sc.H+len("/stream/users/")+len(u.ID)+len("/reposts?"):])) } rel="noreferrer">more reposts</a>
|
||||
}
|
||||
} else {
|
||||
<span>no more reposts</span>
|
||||
@@ -207,8 +207,8 @@ 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(p.Next[sc.H+len("/users/")+len(u.ID)+len("/likes"):])) } rel="noreferrer">more likes</a>
|
||||
if p.NextHref != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.NextHref[sc.H+len("/users/")+len(u.ID)+len("/likes?"):])) } rel="noreferrer">more likes</a>
|
||||
}
|
||||
} else {
|
||||
<span>no more likes</span>
|
||||
@@ -255,8 +255,8 @@ templ UserFollowers(prefs cfg.Preferences, u sc.User, p *sc.Paginated[*sc.User])
|
||||
@UserItem(user)
|
||||
}
|
||||
</div>
|
||||
if p.Next != "" && len(p.Collection) != int(p.Total) {
|
||||
<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>
|
||||
if p.NextHref != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.NextHref[sc.H+len("/users/")+len(u.ID)+len("/followers?"):])) } rel="noreferrer">more users</a>
|
||||
}
|
||||
} else {
|
||||
<span>no more users</span>
|
||||
@@ -273,15 +273,17 @@ templ UserFollowing(prefs cfg.Preferences, u sc.User, p *sc.Paginated[*sc.User])
|
||||
@UserItem(user)
|
||||
}
|
||||
</div>
|
||||
if p.Next != "" && len(p.Collection) != int(p.Total) {
|
||||
<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>
|
||||
if p.NextHref != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?pagination=" + url.QueryEscape(p.NextHref[sc.H+len("/users/")+len(u.ID)+len("/followings?"):])) } rel="noreferrer">more users</a>
|
||||
}
|
||||
} else {
|
||||
<span>no more users</span>
|
||||
}
|
||||
}
|
||||
|
||||
templ SearchUsers(p *sc.Paginated[*sc.User]) {
|
||||
templ SearchUsers(p *sc.Paginated[*sc.User], prefs cfg.Preferences, content string) {
|
||||
@searchbar(prefs,content, "users")
|
||||
<br>
|
||||
<span>Found { strconv.FormatInt(p.Total, 10) } users</span>
|
||||
<br/>
|
||||
<br/>
|
||||
@@ -293,8 +295,8 @@ templ SearchUsers(p *sc.Paginated[*sc.User]) {
|
||||
for _, user := range p.Collection {
|
||||
@UserItem(user)
|
||||
}
|
||||
if p.Next != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?type=users&pagination=" + url.QueryEscape(p.Next[sc.H+len("/search/users"):])) } rel="noreferrer">more users</a>
|
||||
if p.NextHref != "" && len(p.Collection) != int(p.Total) {
|
||||
<a class="btn" href={ templ.SafeURL("?type=users&pagination=" + url.QueryEscape(p.NextHref[sc.H+len("/search/users?"):])) } rel="noreferrer">more users</a>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user