view what albums/playlists track is in, related tracks

This commit is contained in:
Laptop
2024-12-22 10:55:44 +02:00
parent d303afbe51
commit 25606fbcb9
4 changed files with 247 additions and 25 deletions

View File

@@ -481,3 +481,57 @@ func RecentTracks(cid string, prefs cfg.Preferences, args string) (*Paginated[*T
return &p, nil return &p, nil
} }
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,
}
err := p.Proceed(cid, true)
if err != nil {
return nil, err
}
for _, t := range p.Collection {
t.Fix(false, false)
t.Postfix(prefs, false)
}
return &p, nil
}
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,
}
err := p.Proceed(cid, true)
if err != nil {
return nil, err
}
for _, p := range p.Collection {
p.Fix("", false, false)
p.Postfix(prefs, false, false)
}
return &p, nil
}
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,
}
err := p.Proceed(cid, true)
if err != nil {
return nil, err
}
for _, p := range p.Collection {
p.Fix("", false, false)
p.Postfix(prefs, false, false)
}
return &p, nil
}

92
main.go
View File

@@ -325,6 +325,11 @@ func main() {
preferences.Load(app) preferences.Load(app)
// Currently, /:user is the tracks page
app.Get("/:user/tracks", func(c *fiber.Ctx) error {
return c.Redirect("/" + c.Params("user"))
})
app.Get("/:user/sets", func(c *fiber.Ctx) error { app.Get("/:user/sets", func(c *fiber.Ctx) error {
prefs, err := preferences.Get(c) prefs, err := preferences.Get(c)
if err != nil { if err != nil {
@@ -612,7 +617,7 @@ func main() {
} }
c.Set("Content-Type", "text/html") c.Set("Content-Type", "text/html")
return templates.Base(track.Title+" by "+track.Author.Username, templates.Track(prefs, track, stream, displayErr, c.Query("autoplay") == "true", playlist, nextTrack, c.Query("volume"), mode, audio), templates.TrackHeader(prefs, track)).Render(context.Background(), c) return templates.Base(track.Title+" by "+track.Author.Username, templates.Track(prefs, track, stream, displayErr, c.Query("autoplay") == "true", playlist, nextTrack, c.Query("volume"), mode, audio), templates.TrackHeader(prefs, track, true)).Render(context.Background(), c)
}) })
app.Get("/:user", func(c *fiber.Ctx) error { app.Get("/:user", func(c *fiber.Ctx) error {
@@ -711,5 +716,90 @@ func main() {
return templates.Base(user.Username, templates.UserRelated(prefs, user, r), templates.UserHeader(user)).Render(context.Background(), c) return templates.Base(user.Username, templates.UserRelated(prefs, user, r), templates.UserHeader(user)).Render(context.Background(), c)
}) })
// I'd like to make this "related" but keeping it "recommended" to have the same url as soundcloud
app.Get("/:user/:track/recommended", func(c *fiber.Ctx) error {
prefs, err := preferences.Get(c)
if err != nil {
return err
}
cid, err := sc.GetClientID()
if err != nil {
return err
}
track, err := sc.GetTrack(cid, c.Params("user")+"/"+c.Params("track"))
if err != nil {
log.Printf("error getting %s from %s (related): %s\n", c.Params("track"), c.Params("user"), err)
return err
}
track.Postfix(prefs, true)
r, 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
}
c.Set("Content-Type", "text/html")
return templates.Base(track.Title+" by "+track.Author.Username, templates.RelatedTracks(track, r), templates.TrackHeader(prefs, track, false)).Render(context.Background(), c)
})
app.Get("/:user/:track/sets", func(c *fiber.Ctx) error {
prefs, err := preferences.Get(c)
if err != nil {
return err
}
cid, err := sc.GetClientID()
if err != nil {
return err
}
track, err := sc.GetTrack(cid, c.Params("user")+"/"+c.Params("track"))
if err != nil {
log.Printf("error getting %s from %s (sets): %s\n", c.Params("track"), c.Params("user"), err)
return err
}
track.Postfix(prefs, true)
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
}
c.Set("Content-Type", "text/html")
return templates.Base(track.Title+" by "+track.Author.Username, templates.TrackInPlaylists(track, p), templates.TrackHeader(prefs, track, false)).Render(context.Background(), c)
})
app.Get("/:user/:track/albums", func(c *fiber.Ctx) error {
prefs, err := preferences.Get(c)
if err != nil {
return err
}
cid, err := sc.GetClientID()
if err != nil {
return err
}
track, err := sc.GetTrack(cid, c.Params("user")+"/"+c.Params("track"))
if err != nil {
log.Printf("error getting %s from %s (albums): %s\n", c.Params("track"), c.Params("user"), err)
return err
}
track.Postfix(prefs, true)
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
}
c.Set("Content-Type", "text/html")
return templates.Base(track.Title+" by "+track.Author.Username, templates.TrackInAlbums(track, p), templates.TrackHeader(prefs, track, false)).Render(context.Background(), c)
})
log.Fatal(app.Listen(cfg.Addr)) log.Fatal(app.Listen(cfg.Addr))
} }

View File

@@ -10,24 +10,40 @@ import (
func toExt(audio string) string { func toExt(audio string) string {
switch audio { switch audio {
case cfg.AudioAAC: case cfg.AudioAAC:
return "m4a" return "m4a"
case cfg.AudioOpus: case cfg.AudioOpus:
return "ogg" return "ogg"
case cfg.AudioMP3: case cfg.AudioMP3:
return "mp3" return "mp3"
} }
return "" return ""
} }
templ TrackHeader(prefs cfg.Preferences, t sc.Track) { templ TrackButtons(current string, track sc.Track) {
<div class="btns">
for _, b := range [...]btn{{"related tracks", "/recommended", false},{"in albums", "/albums", false},{"in playlists", "/sets", false},{"view on soundcloud", "https://soundcloud.com"+track.Href(), true}} {
if b.text == current {
<a class="btn active">{ b.text }</a>
} else {
if b.external {
<a class="btn" href={ templ.URL(b.href) } referrerpolicy="no-referrer" rel="external nofollow noopener noreferrer" target="_blank">{ b.text }</a>
} else {
<a class="btn" href={ templ.URL(track.Href() + b.href) }>{ b.text }</a>
}
}
}
</div>
}
templ TrackHeader(prefs cfg.Preferences, t sc.Track, needPlayer bool) {
<meta name="og:site_name" content={ t.Author.Username + " ~ soundcloak" }/> <meta name="og:site_name" content={ t.Author.Username + " ~ soundcloak" }/>
<meta name="og:title" content={ t.Title }/> <meta name="og:title" content={ t.Title }/>
<meta name="og:description" content={ t.FormatDescription() }/> <meta name="og:description" content={ t.FormatDescription() }/>
<meta name="og:image" content={ t.Artwork }/> <meta name="og:image" content={ t.Artwork }/>
<link rel="icon" type="image/x-icon" href={ t.Artwork }/> <link rel="icon" type="image/x-icon" href={ t.Artwork }/>
if *prefs.Player == cfg.HLSPlayer { if needPlayer && *prefs.Player == cfg.HLSPlayer {
<script src="/js/hls.js/hls.light.min.js"></script> <script src="/js/hls.js/hls.light.min.js"></script>
} }
} }
@@ -55,7 +71,7 @@ templ TrackPlayer(prefs cfg.Preferences, track sc.Track, stream string, displayE
if cfg.Restream && *prefs.Player == cfg.RestreamPlayer { if cfg.Restream && *prefs.Player == cfg.RestreamPlayer {
{{ audioPref = *prefs.RestreamAudio }} {{ audioPref = *prefs.RestreamAudio }}
if nextTrack != nil { if nextTrack != nil {
<audio id="track" src={ "/_/restream" + track.Href() } controls autoplay?={ autoplay } data-next={ next(nextTrack, playlist, true, mode, "") } volume={volume}></audio> <audio id="track" src={ "/_/restream" + track.Href() } controls autoplay?={ autoplay } data-next={ next(nextTrack, playlist, true, mode, "") } volume={ volume }></audio>
<script async src="/restream.js"></script> <script async src="/restream.js"></script>
} else { } else {
<audio src={ "/_/restream" + track.Href() } controls autoplay?={ autoplay }></audio> <audio src={ "/_/restream" + track.Href() } controls autoplay?={ autoplay }></audio>
@@ -86,7 +102,6 @@ templ TrackPlayer(prefs cfg.Preferences, track sc.Track, stream string, displayE
<p>Only a 30-second snippet is available.</p> <p>Only a 30-second snippet is available.</p>
</div> </div>
} }
if *prefs.ShowAudio { if *prefs.ShowAudio {
<div> <div>
if audioPref == cfg.AudioBest { if audioPref == cfg.AudioBest {
@@ -132,8 +147,13 @@ templ Track(prefs cfg.Preferences, t sc.Track, stream string, displayErr string,
} }
<h1>{ t.Title }</h1> <h1>{ t.Title }</h1>
@TrackPlayer(prefs, t, stream, displayErr, autoplay, nextTrack, playlist, volume, mode, audio) @TrackPlayer(prefs, t, stream, displayErr, autoplay, nextTrack, playlist, volume, mode, audio)
if cfg.Restream {
<div style="display: flex; margin-bottom: 1rem;">
<a class="btn" href={ templ.URL("/_/restream" + t.Href() + "?metadata=true") } download={ t.Permalink + "." + toExt(audio) }>download</a>
</div>
}
if t.Genre != "" { if t.Genre != "" {
<a href={templ.URL("/tags/"+t.Genre)}><p class="tag">{ t.Genre }</p></a> <a href={ templ.URL("/tags/" + t.Genre) }><p class="tag">{ t.Genre }</p></a>
} }
if playlist != nil { if playlist != nil {
<details open style="margin-bottom: 1rem;"> <details open style="margin-bottom: 1rem;">
@@ -143,22 +163,23 @@ templ Track(prefs cfg.Preferences, t sc.Track, stream string, displayErr string,
<h2>Next track:</h2> <h2>Next track:</h2>
@TrackItem(nextTrack, true, next(nextTrack, playlist, true, mode, volume)) @TrackItem(nextTrack, true, next(nextTrack, playlist, true, mode, volume))
<div style="display: flex; gap: 1rem"> <div style="display: flex; gap: 1rem">
<a href={ templ.URL(t.Href()) } class="btn">Stop playlist playback</a> <a href={ templ.URL(t.Href()) } class="btn">Stop playlist playback</a>
if mode != cfg.AutoplayRandom { if mode != cfg.AutoplayRandom {
<a href={ templ.URL(next(&t, playlist, false, cfg.AutoplayRandom, volume)) } class="btn">Switch to random mode</a> <a href={ templ.URL(next(&t, playlist, false, cfg.AutoplayRandom, volume)) } class="btn">Switch to random mode</a>
} else { } else {
<a href={ templ.URL(next(&t, playlist, false, cfg.AutoplayNormal, volume)) } class="btn">Switch to normal mode</a> <a href={ templ.URL(next(&t, playlist, false, cfg.AutoplayNormal, volume)) } class="btn">Switch to normal mode</a>
} }
</div> </div>
</details> </details>
} }
@UserItem(&t.Author) @UserItem(&t.Author)
<div style="display: flex; gap: 1rem"> @TrackButtons("", t)
<a class="btn" href={ templ.URL("https://soundcloud.com" + t.Href()) }>view on soundcloud</a> // <div style="display: flex; gap: 1rem">
if cfg.Restream { // <a class="btn" href={ templ.URL("https://soundcloud.com" + t.Href()) }>view on soundcloud</a>
<a class="btn" href={ templ.URL("/_/restream" + t.Href() + "?metadata=true") } download={t.Permalink + "." + toExt(audio)}>download</a> // if cfg.Restream {
} // <a class="btn" href={ templ.URL("/_/restream" + t.Href() + "?metadata=true") } download={t.Permalink + "." + toExt(audio)}>download</a>
</div> // }
// </div>
<br/> <br/>
@Description(prefs, t.Description, nil) @Description(prefs, t.Description, nil)
<p>{ strconv.FormatInt(t.Likes, 10) } likes</p> <p>{ strconv.FormatInt(t.Likes, 10) } likes</p>
@@ -212,3 +233,60 @@ templ SearchTracks(p *sc.Paginated[*sc.Track]) {
} }
} }
} }
templ RelatedTracks(t sc.Track, p *sc.Paginated[*sc.Track]) {
if t.Artwork != "" {
<img src={ t.Artwork } width="300px"/>
}
<h1><a href={templ.URL(t.Href())}>{ t.Title }</a></h1>
@TrackButtons("related tracks", t)
<br/>
if len(p.Collection) == 0 {
<p>no more results</p>
} else {
for _, track := range p.Collection {
@TrackItem(track, true, "")
}
if p.Next != "" {
<a class="btn" href={ templ.URL("?pagination=" + url.QueryEscape(strings.Split(p.Next, "/related")[1])) } rel="noreferrer">more tracks</a>
}
}
}
templ TrackInAlbums(t sc.Track, p *sc.Paginated[*sc.Playlist]) {
if t.Artwork != "" {
<img src={ t.Artwork } width="300px"/>
}
<h1><a href={templ.URL(t.Href())}>{ t.Title }</a></h1>
@TrackButtons("in albums", t)
<br/>
if len(p.Collection) == 0 {
<p>no more albums</p>
} else {
for _, playlist := range p.Collection {
@PlaylistItem(playlist, true)
}
if p.Next != "" {
<a class="btn" href={ templ.URL("?pagination=" + url.QueryEscape(strings.Split(p.Next, "/albums")[1])) } rel="noreferrer">more albums</a>
}
}
}
templ TrackInPlaylists(t sc.Track, p *sc.Paginated[*sc.Playlist]) {
if t.Artwork != "" {
<img src={ t.Artwork } width="300px"/>
}
<h1><a href={templ.URL(t.Href())}>{ t.Title }</a></h1>
@TrackButtons("in playlists", t)
<br/>
if len(p.Collection) == 0 {
<p>no more playlists</p>
} else {
for _, playlist := range p.Collection {
@PlaylistItem(playlist, true)
}
if p.Next != "" {
<a class="btn" href={ templ.URL("?pagination=" + url.QueryEscape(strings.Split(p.Next, "/playlists_without_albums")[1])) } rel="noreferrer">more playlists</a>
}
}
}

View File

@@ -83,7 +83,7 @@ type btn struct {
templ UserButtons(current string, user string) { templ UserButtons(current string, user string) {
<div class="btns"> <div class="btns">
// this part is the tedious one now, because formatting breaks if i space the list out with newlines // this part is the tedious one now, because formatting breaks if i space the list out with newlines
for _, b := range [7]btn{{"tracks", "", false},{"popular tracks", "/popular-tracks", false},{"playlists", "/sets",false},{"albums", "/albums", false},{"reposts","/reposts", false},{"related", "/_/related", false},{"view on soundcloud", "https://soundcloud.com/"+user, true}} { for _, b := range [...]btn{{"tracks", "", false},{"popular tracks", "/popular-tracks", false},{"playlists", "/sets",false},{"albums", "/albums", false},{"reposts","/reposts", false},{"related", "/_/related", false},{"view on soundcloud", "https://soundcloud.com/"+user, true}} {
if b.text == current { if b.text == current {
<a class="btn active">{ b.text }</a> <a class="btn active">{ b.text }</a>
} else { } else {