Skip to content

Commit

Permalink
website/admin: implement songs page
Browse files Browse the repository at this point in the history
  • Loading branch information
Wessie committed Mar 25, 2024
1 parent f901486 commit 75445a6
Show file tree
Hide file tree
Showing 6 changed files with 563 additions and 18 deletions.
44 changes: 44 additions & 0 deletions mocks/radio.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions radio.go
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,8 @@ type TrackStorage interface {
Get(TrackID) (*Song, error)
// All returns all tracks in storage
All() ([]Song, error)
// Delete removes a track from storage
Delete(TrackID) error
// Unusable returns all tracks that are deemed unusable by the streamer
Unusable() ([]Song, error)
// Insert inserts a new track, errors if ID or TrackID is set
Expand Down
3 changes: 1 addition & 2 deletions website/admin/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,6 @@ func Route(ctx context.Context, s State) func(chi.Router) {
vmiddleware.RequirePermission(radio.PermDatabaseView, s.GetSongs))
r.Post("/songs",
vmiddleware.RequirePermission(radio.PermDatabaseEdit, s.PostSongs))
r.Delete("/songs",
vmiddleware.RequirePermission(radio.PermDatabaseDelete, s.DeleteSongs))

// proxy to the grafana host
grafana, _ := url.Parse("http://localhost:3000")
Expand All @@ -100,5 +98,6 @@ func (s *State) PostStreamerStop(w http.ResponseWriter, r *http.Request) {
}

func (s *State) errorHandler(w http.ResponseWriter, r *http.Request, err error) {
// TODO: implement this better
http.Error(w, err.Error(), http.StatusInternalServerError)
}
107 changes: 104 additions & 3 deletions website/admin/songs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ package admin

import (
"net/http"
"net/url"
"path/filepath"
"strconv"

radio "github.com/R-a-dio/valkyrie"
"github.com/R-a-dio/valkyrie/errors"
"github.com/R-a-dio/valkyrie/util"
"github.com/R-a-dio/valkyrie/website/middleware"
"github.com/R-a-dio/valkyrie/website/shared"
"github.com/rs/zerolog/hlog"
Expand Down Expand Up @@ -63,11 +67,13 @@ func NewSongsInput(s radio.SearchService, r *http.Request) (*SongsInput, error)
),
}

hasDelete := input.User.UserPermissions.Has(radio.PermDatabaseDelete)
hasEdit := input.User.UserPermissions.Has(radio.PermDatabaseEdit)
forms := make([]SongsForm, len(searchResult.Songs))
for i := range searchResult.Songs {
forms[i].Song = searchResult.Songs[i]
forms[i].HasDelete = input.User.UserPermissions.Has(radio.PermDatabaseDelete)
forms[i].HasEdit = input.User.UserPermissions.Has(radio.PermDatabaseEdit)
forms[i].HasDelete = hasDelete
forms[i].HasEdit = hasEdit
}

input.Forms = forms
Expand All @@ -89,9 +95,104 @@ func (s *State) GetSongs(w http.ResponseWriter, r *http.Request) {
}

func (s *State) PostSongs(w http.ResponseWriter, r *http.Request) {
form, err := s.postSongs(w, r)
if err != nil {
s.errorHandler(w, r, err)
return
}

if form == nil && util.IsHTMX(r) {
// delete operation that succeeded and htmx, return nothing
return
}

// otherwise just return the new listing
s.GetSongs(w, r)
}

func (s *State) postSongs(w http.ResponseWriter, r *http.Request) (*SongsForm, error) {
const op errors.Op = "website/admin.postSongs"
ctx := r.Context()

// parse the form explicitly, net/http otherwise eats any errors
if err := r.ParseForm(); err != nil {
return nil, errors.E(op, err, errors.InvalidForm)
}

ts := s.Storage.Track(r.Context())
user := middleware.UserFromContext(ctx)
if user == nil {
return nil, errors.E(op, errors.AccessDenied)
}

// construct the new updated song form from the input
form, err := NewSongsForm(ts, *user, r.Form)
if err != nil {
return nil, errors.E(op, err)
}

// delete action is separate from all the others
if r.Form.Get("action") == "delete" {
// make sure the user has permission to do this, since the route
// only checks for PermDatabaseEdit
if user.UserPermissions.Has(radio.PermDatabaseDelete) {
err = ts.Delete(form.Song.TrackID)
if err != nil {
return nil, errors.E(op, err)
}

// successfully deleted the song from the database, now we just
// need to remove the file we have on-disk
toRemovePath := form.Song.FilePath
if !filepath.IsAbs(toRemovePath) {
toRemovePath = filepath.Join(s.Conf().MusicPath, toRemovePath)
}

err = s.FS.Remove(toRemovePath)
if err != nil {
return nil, errors.E(op, err, errors.InternalServer)
}
return nil, nil
}
return form, errors.E(op, errors.AccessDenied)
}

// anything but delete is effectively an update
err = ts.UpdateMetadata(form.Song)
if err != nil {
return form, errors.E(op, err, errors.InternalServer)
}

return form, nil
}

func (s *State) DeleteSongs(w http.ResponseWriter, r *http.Request) {
func NewSongsForm(ts radio.TrackStorage, user radio.User, values url.Values) (*SongsForm, error) {
const op errors.Op = "website/admin.NewSongsForm"

var form SongsForm

id, err := strconv.ParseUint(values.Get("id"), 10, 64)
if err != nil {
return nil, errors.E(op, err, errors.InvalidForm, errors.Info("missing id in songs form"))
}
tid := radio.TrackID(id)
song, err := ts.Get(tid)
if err != nil {
return nil, errors.E(op, err, errors.InvalidForm)
}

song.Artist = values.Get("artist")
song.Album = values.Get("album")
song.Title = values.Get("title")
song.Tags = values.Get("tags")

if values.Get("action") == "mark-replacement" {
song.NeedReplacement = true
} else if values.Get("action") == "unmark-replacement" {
song.NeedReplacement = false
}
song.LastEditor = user.Username

form.Song = *song
return &form, nil
}
Loading

0 comments on commit 75445a6

Please sign in to comment.