Skip to content

Commit

Permalink
feat(subsonic): add internet radio support
Browse files Browse the repository at this point in the history
* Initial commit of internet radio support.

* Added first test for internet radio.

* Refactor to prepare for more test cases.

* Added a few more tests. Realized that I was not calling as admin so added ability to mock admin.

* Added more internet radio tests. Added proper JSON unmarshaling for ID.

* More test cases. Fixed some accidental tabs in files.

* Fixed some more tabs.

* lint fixes

* Changed placeholder for homepage URL to fit into box.

* Finished out internet radio test cases. Found a few bad error codes in internet radio AND podcasts (mea culpa).

* Realized that delete via website was not checking properly if id existed. Fixed.

gofmt
  • Loading branch information
brian-doherty authored and sentriz committed Jun 21, 2022
1 parent 2afc63f commit 7ab378a
Show file tree
Hide file tree
Showing 14 changed files with 743 additions and 12 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -16,3 +16,4 @@ gonicembed
.vscode
*.swp
.tags*
*.test
7 changes: 7 additions & 0 deletions db/migrations.go
Expand Up @@ -42,6 +42,7 @@ func (db *DB) Migrate(ctx MigrationContext) error {
construct(ctx, "202202121809", migrateAlbumRootDirAgain),
construct(ctx, "202202241218", migratePublicPlaylist),
construct(ctx, "202204270903", migratePodcastDropUserID),
construct(ctx, "202206011628", migrateInternetRadioStations),
}

return gormigrate.
Expand Down Expand Up @@ -356,3 +357,9 @@ func migratePodcastDropUserID(tx *gorm.DB, _ MigrationContext) error {
return nil
}

func migrateInternetRadioStations(tx *gorm.DB, _ MigrationContext) error {
return tx.AutoMigrate(
InternetRadioStation{},
).
Error
}
11 changes: 11 additions & 0 deletions db/model.go
Expand Up @@ -396,3 +396,14 @@ type Bookmark struct {
CreatedAt time.Time
UpdatedAt time.Time
}

type InternetRadioStation struct {
ID int `gorm:"primary_key"`
StreamURL string
Name string
HomepageURL string
}

func (ir *InternetRadioStation) SID() *specid.ID {
return &specid.ID{Type: specid.InternetRadioStation, Value: ir.ID}
}
32 changes: 32 additions & 0 deletions server/assets/pages/home.tmpl
Expand Up @@ -208,6 +208,38 @@
</div>
</div>
{{ end }}
{{ if .User.IsAdmin }}
<div class="padded box">
<div class="box-title">
<i class="mdi mdi-radio"></i> internet_radio_stations
</div>
<div class="box-description text-light">
<p>you can add and update internet radio stations here</p>
</div>
<div>
<table id="irs-preferences">
{{ range $pref := .InternetRadioStations }}
<tr>
<form id="irs-{{ $pref.ID }}-update" action="{{ printf "/admin/update_internet_radio_station_do?id=%d" $pref.ID | path }}" method="post"></form>
<form id="irs-{{ $pref.ID }}-delete" action="{{ printf "/admin/delete_internet_radio_station_do?id=%d" $pref.ID | path }}" method="post"></form>
<td><input form="irs-{{ $pref.ID }}-update" type="text" name="streamURL" value={{ $pref.StreamURL }}></td>
<td><input form="irs-{{ $pref.ID }}-update" type="text" name="name" value={{ $pref.Name }}></td>
<td><input form="irs-{{ $pref.ID }}-update" type="text" name="homepageURL" value={{ $pref.HomepageURL }}></td>
<td><input form="irs-{{ $pref.ID }}-update" type="submit" value="update"></td>
<td><input form="irs-{{ $pref.ID }}-delete" type="submit" value="delete"></td>
</tr>
{{ end }}
<tr>
<form id="irs-add" action="{{ path "/admin/add_internet_radio_station_do" }}" method="post"></form>
<td><input form="irs-add" type="text" name="streamURL" placeholder="stream URL"></td>
<td><input form="irs-add" type="text" name="name" placeholder="name"></td>
<td><input form="irs-add" type="text" name="homepageURL" placeholder="[homepage URL]"></td>
<td><input form="irs-add" type="submit" value="add"></td>
</tr>
</table>
</div>
</div>
{{ end }}
<div class="padded box">
<div class="box-title">
<i class="mdi mdi-playlist-music"></i> playlists
Expand Down
1 change: 1 addition & 0 deletions server/ctrladmin/ctrl.go
Expand Up @@ -123,6 +123,7 @@ type templateData struct {
SelectedUser *db.User

Podcasts []*db.Podcast
InternetRadioStations []*db.InternetRadioStation
}

type Response struct {
Expand Down
108 changes: 107 additions & 1 deletion server/ctrladmin/handlers.go
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"log"
"net/http"
"net/url"
"strconv"
"time"

Expand Down Expand Up @@ -73,6 +74,9 @@ func (c *Controller) ServeHome(r *http.Request) *Response {
// podcasts box
c.DB.Find(&data.Podcasts)

// internet radio box
c.DB.Find(&data.InternetRadioStations)

return &Response{
template: "home.tmpl",
data: data,
Expand Down Expand Up @@ -400,7 +404,7 @@ func (c *Controller) ServePodcastAddDo(r *http.Request) *Response {
flashW: []string{fmt.Sprintf("could not create feed: %v", err)},
}
}
if _, err = c.Podcasts.AddNewPodcast(rssURL, feed); err != nil {
if _, err := c.Podcasts.AddNewPodcast(rssURL, feed); err != nil {
return &Response{
redirect: "/admin/home",
flashW: []string{fmt.Sprintf("could not create feed: %v", err)},
Expand Down Expand Up @@ -464,3 +468,105 @@ func (c *Controller) ServePodcastDeleteDo(r *http.Request) *Response {
redirect: "/admin/home",
}
}

func (c *Controller) ServeInternetRadioStationAddDo(r *http.Request) *Response {
streamURL := r.FormValue("streamURL")
name := r.FormValue("name")
homepageURL := r.FormValue("homepageURL")

if name == "" {
return &Response{redirect: "/admin/home", flashW: []string{"no name provided"}}
}

if _, err := url.ParseRequestURI(streamURL); err != nil {
return &Response{redirect: "/admin/home", flashW: []string{fmt.Sprintf("bad stream URL provided: %v", err)}}
}

if homepageURL != "" {
if _, err := url.ParseRequestURI(homepageURL); err != nil {
return &Response{redirect: "/admin/home", flashW: []string{fmt.Sprintf("bad homepage URL provided: %v", err)}}
}
}

var station db.InternetRadioStation
station.StreamURL = streamURL
station.Name = name
station.HomepageURL = homepageURL
if err := c.DB.Save(&station).Error; err != nil {
return &Response{code: 500, err: fmt.Sprintf("error saving station: %v", err)}
}

return &Response{
redirect: "/admin/home",
}
}

func (c *Controller) ServeInternetRadioStationUpdateDo(r *http.Request) *Response {
stationID, err := strconv.Atoi(r.URL.Query().Get("id"))
if err != nil {
return &Response{code: 400, err: "please provide a valid internet radio station id"}
}

streamURL := r.FormValue("streamURL")
name := r.FormValue("name")
homepageURL := r.FormValue("homepageURL")

if name == "" {
return &Response{
redirect: "/admin/home",
flashW: []string{"no name provided"},
}
}

if _, err := url.ParseRequestURI(streamURL); err != nil {
return &Response{
redirect: "/admin/home",
flashW: []string{fmt.Sprintf("bad stream URL provided: %v", err)},
}
}

if homepageURL != "" {
if _, err := url.ParseRequestURI(homepageURL); err != nil {
return &Response{
redirect: "/admin/home",
flashW: []string{fmt.Sprintf("bad homepage URL provided: %v", err)},
}
}
}

var station db.InternetRadioStation
if err := c.DB.Where("id=?", stationID).First(&station).Error; err != nil {
return &Response{code: 404, err: fmt.Sprintf("find station by id: %v", err)}
}

station.StreamURL = streamURL
station.Name = name
station.HomepageURL = homepageURL
if err := c.DB.Save(&station).Error; err != nil {
return &Response{code: 500, err: "please provide a valid internet radio station id"}
}

return &Response{
redirect: "/admin/home",
}
}

func (c *Controller) ServeInternetRadioStationDeleteDo(r *http.Request) *Response {
stationID, err := strconv.Atoi(r.URL.Query().Get("id"))
if err != nil {
return &Response{code: 400, err: "please provide a valid internet radio station id"}
}

var station db.InternetRadioStation
if err := c.DB.Where("id=?", stationID).First(&station).Error; err != nil {
return &Response{code: 404, err: fmt.Sprintf("find station by id: %v", err)}
}

if err := c.DB.Where("id=?", stationID).Delete(&db.InternetRadioStation{}).Error; err != nil {
return &Response{code: 500, err: fmt.Sprintf("deleting radio station: %v", err)}
}

return &Response{
redirect: "/admin/home",
}
}
9 changes: 9 additions & 0 deletions server/ctrlsubsonic/ctrl_test.go
Expand Up @@ -69,6 +69,15 @@ func makeHTTPMock(query url.Values) (*httptest.ResponseRecorder, *http.Request)
return rr, req
}

func makeHTTPMockWithAdmin(query url.Values) (*httptest.ResponseRecorder, *http.Request) {
rr, req := makeHTTPMock(query)
ctx := req.Context()
ctx = context.WithValue(ctx, CtxUser, &db.User{IsAdmin: true})
req = req.WithContext(ctx)

return rr, req
}

func serveRaw(t *testing.T, contr *Controller, h handlerSubsonicRaw, rr *httptest.ResponseRecorder, req *http.Request) {
type middleware func(http.Handler) http.Handler
middlewares := []middleware{
Expand Down
131 changes: 131 additions & 0 deletions server/ctrlsubsonic/handlers_internet_radio.go
@@ -0,0 +1,131 @@
package ctrlsubsonic

import (
"net/http"
"net/url"

"go.senan.xyz/gonic/db"
"go.senan.xyz/gonic/server/ctrlsubsonic/params"
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
)

func (c *Controller) ServeGetInternetRadioStations(r *http.Request) *spec.Response {
var stations []*db.InternetRadioStation
if err := c.DB.Find(&stations).Error; err != nil {
return spec.NewError(0, "find stations: %v", err)
}
sub := spec.NewResponse()
sub.InternetRadioStations = &spec.InternetRadioStations{
List: make([]*spec.InternetRadioStation, len(stations)),
}
for i, station := range stations {
sub.InternetRadioStations.List[i] = spec.NewInternetRadioStation(station)
}
return sub
}

func (c *Controller) ServeCreateInternetRadioStation(r *http.Request) *spec.Response {
user := r.Context().Value(CtxUser).(*db.User)
if !user.IsAdmin {
return spec.NewError(50, "user not admin")
}

params := r.Context().Value(CtxParams).(params.Params)

streamURL, err := params.Get("streamUrl")
if err != nil {
return spec.NewError(10, "no stream URL provided: %v", err)
}
if _, err := url.ParseRequestURI(streamURL); err != nil {
return spec.NewError(70, "bad stream URL provided: %v", err)
}
name, err := params.Get("name")
if err != nil {
return spec.NewError(10, "no name provided: %v", err)
}
homepageURL, err := params.Get("homepageUrl")
if err == nil {
if _, err := url.ParseRequestURI(homepageURL); err != nil {
return spec.NewError(70, "bad homepage URL provided: %v", err)
}
}

var station db.InternetRadioStation
station.StreamURL = streamURL
station.Name = name
station.HomepageURL = homepageURL

if err := c.DB.Save(&station).Error; err != nil {
return spec.NewError(0, "save station: %v", err)
}

return spec.NewResponse()
}

func (c *Controller) ServeUpdateInternetRadioStation(r *http.Request) *spec.Response {
user := r.Context().Value(CtxUser).(*db.User)
if !user.IsAdmin {
return spec.NewError(50, "user not admin")
}
params := r.Context().Value(CtxParams).(params.Params)

stationID, err := params.GetID("id")
if err != nil {
return spec.NewError(10, "no id provided: %v", err)
}
streamURL, err := params.Get("streamUrl")
if err != nil {
return spec.NewError(10, "no stream URL provided: %v", err)
}
if _, err = url.ParseRequestURI(streamURL); err != nil {
return spec.NewError(70, "bad stream URL provided: %v", err)
}
name, err := params.Get("name")
if err != nil {
return spec.NewError(10, "no name provided: %v", err)
}
homepageURL, err := params.Get("homepageUrl")
if err == nil {
if _, err := url.ParseRequestURI(homepageURL); err != nil {
return spec.NewError(70, "bad homepage URL provided: %v", err)
}
}

var station db.InternetRadioStation
if err := c.DB.Where("id=?", stationID.Value).First(&station).Error; err != nil {
return spec.NewError(70, "id not found: %v", err)
}

station.StreamURL = streamURL
station.Name = name
station.HomepageURL = homepageURL

if err := c.DB.Save(&station).Error; err != nil {
return spec.NewError(0, "save station: %v", err)
}
return spec.NewResponse()
}

func (c *Controller) ServeDeleteInternetRadioStation(r *http.Request) *spec.Response {
user := r.Context().Value(CtxUser).(*db.User)
if !user.IsAdmin {
return spec.NewError(50, "user not admin")
}
params := r.Context().Value(CtxParams).(params.Params)

stationID, err := params.GetID("id")
if err != nil {
return spec.NewError(10, "no id provided: %v", err)
}

var station db.InternetRadioStation
if err := c.DB.Where("id=?", stationID.Value).First(&station).Error; err != nil {
return spec.NewError(70, "id not found: %v", err)
}

if err := c.DB.Delete(&station).Error; err != nil {
return spec.NewError(70, "id not found: %v", err)
}

return spec.NewResponse()
}

0 comments on commit 7ab378a

Please sign in to comment.