mirror of
https://git.maid.zone/stuff/soundcloak.git
synced 2025-12-10 05:39:38 +05:00
init
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
main
|
||||
package-lock.json
|
||||
*_templ.go
|
||||
fly.toml
|
||||
5
README.md
Normal file
5
README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# soundcloak
|
||||
wip opensource ui for soundcloud
|
||||
|
||||
# [official public instance](https://sc.maid.zone)
|
||||
there is no image and audio proxy for now so beware
|
||||
0
assets/favicon.ico
Normal file
0
assets/favicon.ico
Normal file
11
assets/index.html
Normal file
11
assets/index.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>soundcloak</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
25
go.mod
Normal file
25
go.mod
Normal file
@@ -0,0 +1,25 @@
|
||||
module github.com/maid-zone/soundcloak
|
||||
|
||||
go 1.21.3
|
||||
|
||||
require (
|
||||
github.com/a-h/templ v0.2.747
|
||||
github.com/gofiber/fiber/v2 v2.52.5
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.55.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
)
|
||||
43
go.sum
Normal file
43
go.sum
Normal file
@@ -0,0 +1,43 @@
|
||||
github.com/a-h/templ v0.2.747 h1:D0dQ2lxC3W7Dxl6fxQ/1zZHBQslSkTSvl5FxP/CfdKg=
|
||||
github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2yop4=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
|
||||
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
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.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
|
||||
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
22
lib/cfg/init.go
Normal file
22
lib/cfg/init.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package cfg
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// time-to-live for clientid cache
|
||||
// larger number will improve performance (no need to recheck everytime) but might make soundcloak unusable after soundcloud updates the website
|
||||
const ClientIDTTL = 5 * time.Minute
|
||||
|
||||
// time-to-live for user profile cache
|
||||
const UserTTL = 5 * time.Minute
|
||||
|
||||
// time-to-live for track cache
|
||||
const TrackTTL = 5 * time.Minute
|
||||
|
||||
// default fasthttp one was causing connections to be stuck? todo make it cycle browser useragents or just choose random at startup
|
||||
const UserAgent = "insomnia/2023.2.0"
|
||||
|
||||
var JSON = jsoniter.ConfigFastest
|
||||
286
lib/sc/init.go
Normal file
286
lib/sc/init.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package sc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/maid-zone/soundcloak/lib/cfg"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
var clientIdCache struct {
|
||||
ClientID []byte
|
||||
ClientIDString string
|
||||
Version []byte
|
||||
NextCheck time.Time
|
||||
}
|
||||
|
||||
type cached[T any] struct {
|
||||
Value T
|
||||
Expires time.Time
|
||||
}
|
||||
|
||||
var usersCache = map[string]cached[User]{}
|
||||
var tracksCache = map[string]cached[Track]{}
|
||||
|
||||
var verRegex = regexp.MustCompile(`(?m)^<script>window\.__sc_version="([0-9]{10})"</script>$`)
|
||||
var scriptsRegex = regexp.MustCompile(`(?m)^<script crossorigin src="(https://a-v2\.sndcdn\.com/assets/.+\.js)"></script>$`)
|
||||
var clientIdRegex = regexp.MustCompile(`\("client_id=([A-Za-z0-9]{32})"\)`)
|
||||
var ErrVersionNotFound = errors.New("version not found")
|
||||
var ErrScriptNotFound = errors.New("script not found")
|
||||
var ErrIDNotFound = errors.New("clientid not found")
|
||||
var ErrKindNotCorrect = errors.New("entity of incorrect kind")
|
||||
var ErrIncompatibleStream = errors.New("incompatible stream")
|
||||
var ErrNoURL = errors.New("no url")
|
||||
|
||||
// inspired by github.com/imputnet/cobalt (mostly stolen lol)
|
||||
func GetClientID() (string, error) {
|
||||
if clientIdCache.NextCheck.After(time.Now()) {
|
||||
return clientIdCache.ClientIDString, nil
|
||||
}
|
||||
|
||||
req := fasthttp.AcquireRequest()
|
||||
defer fasthttp.ReleaseRequest(req)
|
||||
|
||||
req.SetRequestURI("https://soundcloud.com/h") // 404 page
|
||||
req.Header.Set("User-Agent", cfg.UserAgent) // the connection is stuck with fasthttp useragent lol, maybe randomly select from a list of browser useragents in the future? low priority for now
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
|
||||
resp := fasthttp.AcquireResponse()
|
||||
defer fasthttp.ReleaseResponse(resp)
|
||||
|
||||
err := fasthttp.Do(req, resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data, err := resp.BodyUncompressed()
|
||||
if err != nil {
|
||||
data = resp.Body()
|
||||
}
|
||||
|
||||
//fmt.Println(string(data), err)
|
||||
|
||||
res := verRegex.FindSubmatch(data)
|
||||
if len(res) != 2 {
|
||||
return "", ErrVersionNotFound
|
||||
}
|
||||
|
||||
if bytes.Equal(res[1], clientIdCache.Version) {
|
||||
return clientIdCache.ClientIDString, nil
|
||||
}
|
||||
|
||||
ver := res[1]
|
||||
|
||||
scripts := scriptsRegex.FindAllSubmatch(data, -1)
|
||||
if len(scripts) == 0 {
|
||||
return "", ErrScriptNotFound
|
||||
}
|
||||
|
||||
for _, scr := range scripts {
|
||||
if len(scr) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
req.SetRequestURIBytes(scr[1])
|
||||
|
||||
err = fasthttp.Do(req, resp)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
data, err = resp.BodyUncompressed()
|
||||
if err != nil {
|
||||
data = resp.Body()
|
||||
}
|
||||
|
||||
res = clientIdRegex.FindSubmatch(data)
|
||||
if len(res) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
clientIdCache.ClientID = res[1]
|
||||
clientIdCache.ClientIDString = string(res[1])
|
||||
clientIdCache.Version = ver
|
||||
clientIdCache.NextCheck = time.Now().Add(cfg.ClientIDTTL)
|
||||
return clientIdCache.ClientIDString, nil
|
||||
}
|
||||
|
||||
return "", ErrIDNotFound
|
||||
}
|
||||
|
||||
func Resolve(path string, out any) error {
|
||||
cid, err := GetClientID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := fasthttp.AcquireRequest()
|
||||
defer fasthttp.ReleaseRequest(req)
|
||||
|
||||
req.SetRequestURI("https://api-v2.soundcloud.com/resolve?url=https%3A%2F%2Fsoundcloud.com%2F" + url.QueryEscape(path) + "&client_id=" + cid)
|
||||
req.Header.Set("User-Agent", cfg.UserAgent)
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
|
||||
resp := fasthttp.AcquireResponse()
|
||||
defer fasthttp.ReleaseResponse(resp)
|
||||
|
||||
err = fasthttp.Do(req, resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode() != 200 {
|
||||
return fmt.Errorf("resolve: got status code %d", resp.StatusCode())
|
||||
}
|
||||
|
||||
data, err := resp.BodyUncompressed()
|
||||
if err != nil {
|
||||
data = resp.Body()
|
||||
}
|
||||
|
||||
return cfg.JSON.Unmarshal(data, out)
|
||||
}
|
||||
|
||||
func GetUser(permalink string) (User, error) {
|
||||
if cell, ok := usersCache[permalink]; ok && cell.Expires.After(time.Now()) {
|
||||
return cell.Value, nil
|
||||
}
|
||||
|
||||
var u User
|
||||
err := Resolve(permalink, &u)
|
||||
if err != nil {
|
||||
return u, err
|
||||
}
|
||||
|
||||
if u.Kind != "user" {
|
||||
err = ErrKindNotCorrect
|
||||
return u, err
|
||||
}
|
||||
|
||||
u.Avatar = strings.Replace(u.Avatar, "-large.", "-t200x200.", 1)
|
||||
ls := strings.Split(u.URN, ":")
|
||||
u.ID = ls[len(ls)-1]
|
||||
usersCache[permalink] = cached[User]{Value: u, Expires: time.Now().Add(cfg.UserTTL)}
|
||||
|
||||
return u, err
|
||||
}
|
||||
|
||||
func GetTrack(permalink string) (Track, error) {
|
||||
if cell, ok := tracksCache[permalink]; ok && cell.Expires.After(time.Now()) {
|
||||
return cell.Value, nil
|
||||
}
|
||||
|
||||
var u Track
|
||||
err := Resolve(permalink, &u)
|
||||
if err != nil {
|
||||
return u, err
|
||||
}
|
||||
|
||||
if u.Kind != "track" {
|
||||
return u, ErrKindNotCorrect
|
||||
}
|
||||
|
||||
tracksCache[permalink] = cached[Track]{Value: u, Expires: time.Now().Add(cfg.TrackTTL)}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (p *Paginated[T]) Proceed() error {
|
||||
cid, err := GetClientID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := fasthttp.AcquireRequest()
|
||||
defer fasthttp.ReleaseRequest(req)
|
||||
|
||||
req.SetRequestURI(p.Next + "&client_id=" + cid)
|
||||
req.Header.Set("User-Agent", cfg.UserAgent)
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
|
||||
resp := fasthttp.AcquireResponse()
|
||||
defer fasthttp.ReleaseResponse(resp)
|
||||
|
||||
err = fasthttp.Do(req, resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode() != 200 {
|
||||
return fmt.Errorf("paginated.proceed: got status code %d", resp.StatusCode())
|
||||
}
|
||||
|
||||
data, err := resp.BodyUncompressed()
|
||||
if err != nil {
|
||||
data = resp.Body()
|
||||
}
|
||||
|
||||
return cfg.JSON.Unmarshal(data, p)
|
||||
}
|
||||
|
||||
func (u User) GetTracks(args string) (*Paginated[Track], error) {
|
||||
p := Paginated[Track]{
|
||||
Next: "https://api-v2.soundcloud.com/users/" + u.ID + "/tracks" + args,
|
||||
}
|
||||
|
||||
err := p.Proceed()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func (t Track) GetStream() (string, error) {
|
||||
cid, err := GetClientID()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tr := t.Media.SelectCompatible()
|
||||
if tr == nil {
|
||||
return "", ErrIncompatibleStream
|
||||
}
|
||||
|
||||
req := fasthttp.AcquireRequest()
|
||||
defer fasthttp.ReleaseRequest(req)
|
||||
|
||||
req.SetRequestURI(tr.URL + "?client_id=" + cid + "&track_authorization=" + t.Authorization)
|
||||
req.Header.Set("User-Agent", cfg.UserAgent)
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
|
||||
resp := fasthttp.AcquireResponse()
|
||||
defer fasthttp.ReleaseResponse(resp)
|
||||
|
||||
err = fasthttp.Do(req, resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.StatusCode() != 200 {
|
||||
return "", fmt.Errorf("resolve: got status code %d", resp.StatusCode())
|
||||
}
|
||||
|
||||
data, err := resp.BodyUncompressed()
|
||||
if err != nil {
|
||||
data = resp.Body()
|
||||
}
|
||||
|
||||
var s Stream
|
||||
err = cfg.JSON.Unmarshal(data, &s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if s.URL == "" {
|
||||
return "", ErrNoURL
|
||||
}
|
||||
|
||||
return s.URL, nil
|
||||
}
|
||||
81
lib/sc/structs.go
Normal file
81
lib/sc/structs.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package sc
|
||||
|
||||
type User struct {
|
||||
Avatar string `json:"avatar_url"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Description string `json:"description"`
|
||||
Followers int64 `json:"followers_count"`
|
||||
Following int64 `json:"followings_count"`
|
||||
FullName string `json:"full_name"`
|
||||
Kind string `json:"kind"` // should always be "user"!
|
||||
LastModified string `json:"last_modified"`
|
||||
Liked int `json:"likes_count"`
|
||||
Permalink string `json:"permalink"`
|
||||
Playlists int `json:"playlist_count"`
|
||||
Tracks int64 `json:"track_count"`
|
||||
URN string `json:"urn"`
|
||||
Username string `json:"username"`
|
||||
Verified bool `json:"verified"`
|
||||
|
||||
ID string `json:"-"`
|
||||
}
|
||||
|
||||
type Protocol string
|
||||
|
||||
const (
|
||||
ProtocolHLS Protocol = "hls"
|
||||
ProtocolProgressive Protocol = "progressive"
|
||||
)
|
||||
|
||||
type Format struct {
|
||||
Protocol Protocol `json:"protocol"`
|
||||
MimeType string `json:"mime_type"`
|
||||
}
|
||||
|
||||
type Transcoding struct {
|
||||
URL string `json:"url"`
|
||||
Preset string `json:"preset"`
|
||||
Format Format `json:"format"`
|
||||
Quality string `json:"quality"`
|
||||
}
|
||||
|
||||
type Media struct {
|
||||
Transcodings []Transcoding `json:"transcodings"`
|
||||
}
|
||||
|
||||
func (m Media) SelectCompatible() *Transcoding {
|
||||
for _, t := range m.Transcodings {
|
||||
if t.Format.Protocol == "hls" && t.Format.MimeType == "audio/mpeg" {
|
||||
return &t
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Track struct {
|
||||
Artwork string `json:"artwork_url"`
|
||||
Comments int `json:"comment_count"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Duration int `json:"duration"` // there are duration and full_duration fields wtf does that mean
|
||||
Genre string `json:"genre"`
|
||||
ID int `json:"id"`
|
||||
Kind string `json:"kind"` // should always be "track"!
|
||||
LastModified string `json:"last_modified"`
|
||||
Likes int `json:"likes_count"`
|
||||
Permalink string `json:"permalink"`
|
||||
Played int `json:"playback_count"`
|
||||
Title string `json:"title"`
|
||||
Media Media `json:"media"`
|
||||
Authorization string `json:"track_authorization"`
|
||||
Author User `json:"user"`
|
||||
}
|
||||
|
||||
type Paginated[T any] struct {
|
||||
Collection []T `json:"collection"`
|
||||
Next string `json:"next_href"`
|
||||
}
|
||||
|
||||
type Stream struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
61
main.go
Normal file
61
main.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/compress"
|
||||
"github.com/gofiber/fiber/v2/middleware/earlydata"
|
||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||
|
||||
"github.com/maid-zone/soundcloak/lib/sc"
|
||||
"github.com/maid-zone/soundcloak/templates"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := fiber.New()
|
||||
app.Use(compress.New())
|
||||
app.Use(recover.New())
|
||||
app.Use(earlydata.New())
|
||||
|
||||
app.Static("/", "assets", fiber.Static{Compress: true, MaxAge: 3600})
|
||||
app.Static("/js/hls.js/", "node_modules/hls.js/dist", fiber.Static{Compress: true, MaxAge: 3600})
|
||||
|
||||
app.Get("/:user/:track", func(c *fiber.Ctx) error {
|
||||
track, err := sc.GetTrack(c.Params("user") + "/" + c.Params("track"))
|
||||
if err != nil {
|
||||
fmt.Printf("error getting %s from %s: %s\n", c.Params("track"), c.Params("user"), err)
|
||||
return c.SendStatus(404)
|
||||
}
|
||||
|
||||
stream, err := track.GetStream()
|
||||
if err != nil {
|
||||
fmt.Printf("error getting %s stream from %s: %s\n", c.Params("track"), c.Params("user"), err)
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base(track.Title+" by "+track.Author.Username, templates.Track(track, stream)).Render(context.Background(), c)
|
||||
})
|
||||
|
||||
app.Get("/:user", func(c *fiber.Ctx) error {
|
||||
usr, err := sc.GetUser(c.Params("user"))
|
||||
if err != nil {
|
||||
fmt.Printf("error getting %s: %s\n", c.Params("user"), err)
|
||||
return c.SendStatus(404)
|
||||
}
|
||||
|
||||
p, err := usr.GetTracks(c.Query("pagination", "?limit=20"))
|
||||
if err != nil {
|
||||
fmt.Printf("error getting %s tracks: %s\n", c.Params("user"), err)
|
||||
return c.SendStatus(404)
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return templates.Base(usr.Username, templates.User(usr, p)).Render(context.Background(), c)
|
||||
})
|
||||
|
||||
log.Fatal(app.Listen(":4664"))
|
||||
}
|
||||
5
package.json
Normal file
5
package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"hls.js": "^1.5.15"
|
||||
}
|
||||
}
|
||||
19
templates/base.templ
Normal file
19
templates/base.templ
Normal file
@@ -0,0 +1,19 @@
|
||||
package templates
|
||||
|
||||
templ Base(title string, content templ.Component) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
if title != "" {
|
||||
<title>{ title } • soundcloak</title>
|
||||
} else {
|
||||
<title>soundcloak</title>
|
||||
}
|
||||
</head>
|
||||
<body>
|
||||
@content
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
25
templates/track.templ
Normal file
25
templates/track.templ
Normal file
@@ -0,0 +1,25 @@
|
||||
package templates
|
||||
|
||||
import "github.com/maid-zone/soundcloak/lib/sc"
|
||||
|
||||
templ Track(t sc.Track, stream string) {
|
||||
<script src="/js/hls.js/hls.light.js"></script>
|
||||
|
||||
<h1>{t.Title}</h1>
|
||||
|
||||
<video id="track" data-stream={stream} controls style="width: 50rem; height: 2.5rem;"></video>
|
||||
|
||||
<script>
|
||||
var video = document.getElementById('track');
|
||||
var videoSrc = video.getAttribute('data-stream');
|
||||
if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
video.src = videoSrc;
|
||||
} else if (Hls.isSupported()) {
|
||||
var hls = new Hls();
|
||||
hls.loadSource(videoSrc);
|
||||
hls.attachMedia(video);
|
||||
} else {
|
||||
alert('hls not supported');
|
||||
}
|
||||
</script>
|
||||
}
|
||||
41
templates/user.templ
Normal file
41
templates/user.templ
Normal file
@@ -0,0 +1,41 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"github.com/maid-zone/soundcloak/lib/sc"
|
||||
"strconv"
|
||||
"strings"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
templ User(u sc.User, p *sc.Paginated[sc.Track]) {
|
||||
<div>
|
||||
if u.Avatar != "" {
|
||||
<img src={u.Avatar}>
|
||||
}
|
||||
|
||||
<h1>{u.Username}</h1>
|
||||
if u.FullName != "" {
|
||||
<h2>{u.FullName}</h2>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>{strconv.FormatInt(u.Followers, 10)} followers</h2>
|
||||
<h2>{strconv.FormatInt(u.Following, 10)} following</h2>
|
||||
<h2>{strconv.FormatInt(u.Tracks, 10)} tracks</h2>
|
||||
</div>
|
||||
|
||||
if len(p.Collection) != 0 {
|
||||
<div>
|
||||
for _, track := range p.Collection {
|
||||
<div>
|
||||
<h1><a href={templ.URL("/" + track.Author.Permalink + "/" + track.Permalink)}>{track.Title}</a></h1>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<a href={templ.URL("?pagination=" + url.QueryEscape(strings.Split(p.Next, "/tracks")[1]))}>more tracks</a>
|
||||
} else {
|
||||
<h1>no mroe tracks</h1>
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user