eg
This commit is contained in:
43
server/handler/handler.go
Normal file
43
server/handler/handler.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/wader/gormstore"
|
||||
|
||||
"github.com/sentriz/gonic/model"
|
||||
)
|
||||
|
||||
type contextKey int
|
||||
|
||||
const (
|
||||
contextUserKey contextKey = iota
|
||||
contextSessionKey
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
DB *gorm.DB
|
||||
SessDB *gormstore.Store
|
||||
Templates map[string]*template.Template
|
||||
MusicPath string
|
||||
}
|
||||
|
||||
func (c *Controller) GetSetting(key string) string {
|
||||
var setting model.Setting
|
||||
c.DB.Where("key = ?", key).First(&setting)
|
||||
return setting.Value
|
||||
}
|
||||
|
||||
func (c *Controller) SetSetting(key, value string) {
|
||||
c.DB.
|
||||
Where(model.Setting{Key: key}).
|
||||
Assign(model.Setting{Value: value}).
|
||||
FirstOrCreate(&model.Setting{})
|
||||
}
|
||||
|
||||
func (c *Controller) GetUserFromName(name string) *model.User {
|
||||
var user model.User
|
||||
c.DB.Where("name = ?", name).First(&user)
|
||||
return &user
|
||||
}
|
||||
245
server/handler/handler_admin.go
Normal file
245
server/handler/handler_admin.go
Normal file
@@ -0,0 +1,245 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/sentriz/gonic/model"
|
||||
"github.com/sentriz/gonic/lastfm"
|
||||
)
|
||||
|
||||
func (c *Controller) ServeLogin(w http.ResponseWriter, r *http.Request) {
|
||||
renderTemplate(w, r, c.Templates["login"], nil)
|
||||
}
|
||||
|
||||
func (c *Controller) ServeLoginDo(w http.ResponseWriter, r *http.Request) {
|
||||
session := r.Context().Value(contextSessionKey).(*sessions.Session)
|
||||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
if username == "" || password == "" {
|
||||
session.AddFlash("please provide both a username and password")
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
user := c.GetUserFromName(username)
|
||||
if !(username == user.Name && password == user.Password) {
|
||||
session.AddFlash("invalid username / password")
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
// put the user name into the session. future endpoints after this one
|
||||
// are wrapped with WithUserSession() which will get the name from the
|
||||
// session and put the row into the request context.
|
||||
session.Values["user"] = user.Name
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (c *Controller) ServeLogout(w http.ResponseWriter, r *http.Request) {
|
||||
session := r.Context().Value(contextSessionKey).(*sessions.Session)
|
||||
session.Options.MaxAge = -1
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (c *Controller) ServeHome(w http.ResponseWriter, r *http.Request) {
|
||||
var data templateData
|
||||
c.DB.Table("album_artists").Count(&data.ArtistCount)
|
||||
c.DB.Table("albums").Count(&data.AlbumCount)
|
||||
c.DB.Table("tracks").Count(&data.TrackCount)
|
||||
c.DB.Find(&data.AllUsers)
|
||||
data.CurrentLastFMAPIKey = c.GetSetting("lastfm_api_key")
|
||||
scheme := firstExisting(
|
||||
"http", // fallback
|
||||
r.Header.Get("X-Forwarded-Proto"),
|
||||
r.Header.Get("X-Forwarded-Scheme"),
|
||||
r.URL.Scheme,
|
||||
)
|
||||
host := firstExisting(
|
||||
"localhost:7373", // fallback
|
||||
r.Header.Get("X-Forwarded-Host"),
|
||||
r.Host,
|
||||
)
|
||||
data.RequestRoot = fmt.Sprintf("%s://%s", scheme, host)
|
||||
renderTemplate(w, r, c.Templates["home"], &data)
|
||||
}
|
||||
|
||||
func (c *Controller) ServeChangeOwnPassword(w http.ResponseWriter, r *http.Request) {
|
||||
renderTemplate(w, r, c.Templates["change_own_password"], nil)
|
||||
}
|
||||
|
||||
func (c *Controller) ServeChangeOwnPasswordDo(w http.ResponseWriter, r *http.Request) {
|
||||
session := r.Context().Value(contextSessionKey).(*sessions.Session)
|
||||
passwordOne := r.FormValue("password_one")
|
||||
passwordTwo := r.FormValue("password_two")
|
||||
err := validatePasswords(passwordOne, passwordTwo)
|
||||
if err != nil {
|
||||
session.AddFlash(err.Error())
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
user := r.Context().Value(contextUserKey).(*model.User)
|
||||
user.Password = passwordOne
|
||||
c.DB.Save(user)
|
||||
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (c *Controller) ServeLinkLastFMDo(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.URL.Query().Get("token")
|
||||
if token == "" {
|
||||
http.Error(w, "please provide a token", 400)
|
||||
return
|
||||
}
|
||||
sessionKey, err := lastfm.GetSession(
|
||||
c.GetSetting("lastfm_api_key"),
|
||||
c.GetSetting("lastfm_secret"),
|
||||
token,
|
||||
)
|
||||
session := r.Context().Value(contextSessionKey).(*sessions.Session)
|
||||
if err != nil {
|
||||
session.AddFlash(err.Error())
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
user := r.Context().Value(contextUserKey).(*model.User)
|
||||
user.LastFMSession = sessionKey
|
||||
c.DB.Save(&user)
|
||||
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (c *Controller) ServeUnlinkLastFMDo(w http.ResponseWriter, r *http.Request) {
|
||||
user := r.Context().Value(contextUserKey).(*model.User)
|
||||
user.LastFMSession = ""
|
||||
c.DB.Save(&user)
|
||||
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (c *Controller) ServeChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||
username := r.URL.Query().Get("user")
|
||||
if username == "" {
|
||||
http.Error(w, "please provide a username", 400)
|
||||
return
|
||||
}
|
||||
var user model.User
|
||||
err := c.DB.Where("name = ?", username).First(&user).Error
|
||||
if gorm.IsRecordNotFoundError(err) {
|
||||
http.Error(w, "couldn't find a user with that name", 400)
|
||||
return
|
||||
}
|
||||
var data templateData
|
||||
data.SelectedUser = &user
|
||||
renderTemplate(w, r, c.Templates["change_password"], &data)
|
||||
}
|
||||
|
||||
func (c *Controller) ServeChangePasswordDo(w http.ResponseWriter, r *http.Request) {
|
||||
session := r.Context().Value(contextSessionKey).(*sessions.Session)
|
||||
username := r.URL.Query().Get("user")
|
||||
var user model.User
|
||||
c.DB.Where("name = ?", username).First(&user)
|
||||
passwordOne := r.FormValue("password_one")
|
||||
passwordTwo := r.FormValue("password_two")
|
||||
err := validatePasswords(passwordOne, passwordTwo)
|
||||
if err != nil {
|
||||
session.AddFlash(err.Error())
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
user.Password = passwordOne
|
||||
c.DB.Save(&user)
|
||||
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (c *Controller) ServeDeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
username := r.URL.Query().Get("user")
|
||||
if username == "" {
|
||||
http.Error(w, "please provide a username", 400)
|
||||
return
|
||||
}
|
||||
var user model.User
|
||||
err := c.DB.Where("name = ?", username).First(&user).Error
|
||||
if gorm.IsRecordNotFoundError(err) {
|
||||
http.Error(w, "couldn't find a user with that name", 400)
|
||||
return
|
||||
}
|
||||
var data templateData
|
||||
data.SelectedUser = &user
|
||||
renderTemplate(w, r, c.Templates["delete_user"], &data)
|
||||
}
|
||||
|
||||
func (c *Controller) ServeDeleteUserDo(w http.ResponseWriter, r *http.Request) {
|
||||
username := r.URL.Query().Get("user")
|
||||
var user model.User
|
||||
c.DB.Where("name = ?", username).First(&user)
|
||||
c.DB.Delete(&user)
|
||||
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (c *Controller) ServeCreateUser(w http.ResponseWriter, r *http.Request) {
|
||||
renderTemplate(w, r, c.Templates["create_user"], nil)
|
||||
}
|
||||
|
||||
func (c *Controller) ServeCreateUserDo(w http.ResponseWriter, r *http.Request) {
|
||||
session := r.Context().Value(contextSessionKey).(*sessions.Session)
|
||||
username := r.FormValue("username")
|
||||
err := validateUsername(username)
|
||||
if err != nil {
|
||||
session.AddFlash(err.Error())
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
passwordOne := r.FormValue("password_one")
|
||||
passwordTwo := r.FormValue("password_two")
|
||||
err = validatePasswords(passwordOne, passwordTwo)
|
||||
if err != nil {
|
||||
session.AddFlash(err.Error())
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
user := model.User{
|
||||
Name: username,
|
||||
Password: passwordOne,
|
||||
}
|
||||
err = c.DB.Create(&user).Error
|
||||
if err != nil {
|
||||
session.AddFlash(fmt.Sprintf(
|
||||
"could not create user `%s`: %v", username, err,
|
||||
))
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (c *Controller) ServeUpdateLastFMAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||
var data templateData
|
||||
data.CurrentLastFMAPIKey = c.GetSetting("lastfm_api_key")
|
||||
data.CurrentLastFMAPISecret = c.GetSetting("lastfm_secret")
|
||||
renderTemplate(w, r, c.Templates["update_lastfm_api_key"], &data)
|
||||
}
|
||||
|
||||
func (c *Controller) ServeUpdateLastFMAPIKeyDo(w http.ResponseWriter, r *http.Request) {
|
||||
session := r.Context().Value(contextSessionKey).(*sessions.Session)
|
||||
apiKey := r.FormValue("api_key")
|
||||
secret := r.FormValue("secret")
|
||||
err := validateAPIKey(apiKey, secret)
|
||||
if err != nil {
|
||||
session.AddFlash(err.Error())
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
c.SetSetting("lastfm_api_key", apiKey)
|
||||
c.SetSetting("lastfm_secret", secret)
|
||||
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
||||
}
|
||||
42
server/handler/handler_admin_utils.go
Normal file
42
server/handler/handler_admin_utils.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package handler
|
||||
|
||||
import "fmt"
|
||||
|
||||
func validateUsername(username string) error {
|
||||
if username == "" {
|
||||
return fmt.Errorf("please enter the username")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePasswords(pOne, pTwo string) error {
|
||||
if pOne == "" || pTwo == "" {
|
||||
return fmt.Errorf("please enter the password twice")
|
||||
}
|
||||
if !(pOne == pTwo) {
|
||||
return fmt.Errorf("the two passwords entered were not the same")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateAPIKey(apiKey, secret string) error {
|
||||
if apiKey == "" || secret == "" {
|
||||
return fmt.Errorf("please enter both the api key and secret")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func firstExisting(or string, strings ...string) string {
|
||||
current := ""
|
||||
for _, s := range strings {
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
current = s
|
||||
break
|
||||
}
|
||||
if current == "" {
|
||||
return or
|
||||
}
|
||||
return current
|
||||
}
|
||||
179
server/handler/handler_sub_by_folder.go
Normal file
179
server/handler/handler_sub_by_folder.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/sentriz/gonic/model"
|
||||
"github.com/sentriz/gonic/server/subsonic"
|
||||
)
|
||||
|
||||
func (c *Controller) GetIndexes(w http.ResponseWriter, r *http.Request) {
|
||||
// we are browsing by folder, but the subsonic docs show sub <artist> elements
|
||||
// for this, so we're going to return root directories as "artists"
|
||||
var folders []*model.Folder
|
||||
c.DB.Where("parent_id = ?", 1).Find(&folders)
|
||||
var indexMap = make(map[rune]*subsonic.Index)
|
||||
var indexes []*subsonic.Index
|
||||
for _, folder := range folders {
|
||||
i := indexOf(folder.Name)
|
||||
index, ok := indexMap[i]
|
||||
if !ok {
|
||||
index = &subsonic.Index{
|
||||
Name: string(i),
|
||||
Artists: []*subsonic.Artist{},
|
||||
}
|
||||
indexMap[i] = index
|
||||
indexes = append(indexes, index)
|
||||
}
|
||||
index.Artists = append(index.Artists, &subsonic.Artist{
|
||||
ID: folder.ID,
|
||||
Name: folder.Name,
|
||||
})
|
||||
}
|
||||
sub := subsonic.NewResponse()
|
||||
sub.Indexes = &subsonic.Indexes{
|
||||
LastModified: 0,
|
||||
Index: indexes,
|
||||
}
|
||||
respond(w, r, sub)
|
||||
}
|
||||
|
||||
func (c *Controller) GetMusicDirectory(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := getIntParam(r, "id")
|
||||
if err != nil {
|
||||
respondError(w, r, 10, "please provide an `id` parameter")
|
||||
return
|
||||
}
|
||||
childrenObj := []*subsonic.Child{}
|
||||
var cFolder model.Folder
|
||||
c.DB.First(&cFolder, id)
|
||||
//
|
||||
// start looking for child folders in the current dir
|
||||
var folders []*model.Folder
|
||||
c.DB.
|
||||
Where("parent_id = ?", id).
|
||||
Find(&folders)
|
||||
for _, folder := range folders {
|
||||
childrenObj = append(childrenObj, &subsonic.Child{
|
||||
Parent: cFolder.ID,
|
||||
ID: folder.ID,
|
||||
Title: folder.Name,
|
||||
IsDir: true,
|
||||
CoverID: folder.CoverID,
|
||||
})
|
||||
}
|
||||
//
|
||||
// start looking for child tracks in the current dir
|
||||
var tracks []*model.Track
|
||||
c.DB.
|
||||
Where("folder_id = ?", id).
|
||||
Preload("Album").
|
||||
Order("track_number").
|
||||
Find(&tracks)
|
||||
for _, track := range tracks {
|
||||
if getStrParam(r, "c") == "Jamstash" {
|
||||
// jamstash thinks it can't play flacs
|
||||
track.ContentType = "audio/mpeg"
|
||||
track.Suffix = "mp3"
|
||||
}
|
||||
childrenObj = append(childrenObj, &subsonic.Child{
|
||||
ID: track.ID,
|
||||
Album: track.Album.Title,
|
||||
Artist: track.Artist,
|
||||
ContentType: track.ContentType,
|
||||
CoverID: cFolder.CoverID,
|
||||
Duration: 0,
|
||||
IsDir: false,
|
||||
Parent: cFolder.ID,
|
||||
Path: track.Path,
|
||||
Size: track.Size,
|
||||
Suffix: track.Suffix,
|
||||
Title: track.Title,
|
||||
Track: track.TrackNumber,
|
||||
Type: "music",
|
||||
})
|
||||
}
|
||||
//
|
||||
// respond section
|
||||
sub := subsonic.NewResponse()
|
||||
sub.Directory = &subsonic.Directory{
|
||||
ID: cFolder.ID,
|
||||
Parent: cFolder.ParentID,
|
||||
Name: cFolder.Name,
|
||||
Children: childrenObj,
|
||||
}
|
||||
respond(w, r, sub)
|
||||
}
|
||||
|
||||
// changes to this function should be reflected in in _by_tags.go's
|
||||
// getAlbumListTwo() function
|
||||
func (c *Controller) GetAlbumList(w http.ResponseWriter, r *http.Request) {
|
||||
listType := getStrParam(r, "type")
|
||||
if listType == "" {
|
||||
respondError(w, r, 10, "please provide a `type` parameter")
|
||||
return
|
||||
}
|
||||
q := c.DB
|
||||
switch listType {
|
||||
case "alphabeticalByArtist":
|
||||
// not sure what it meant by "artist" since we're browsing by folder
|
||||
// - so we'll consider the parent folder's name to be the "artist"
|
||||
q = q.Joins(`
|
||||
JOIN folders AS parent_folders
|
||||
ON folders.parent_id = parent_folders.id`)
|
||||
q = q.Order("parent_folders.name")
|
||||
case "alphabeticalByName":
|
||||
// not sure about "name" either, so lets use the folder's name
|
||||
q = q.Order("name")
|
||||
case "frequent":
|
||||
user := r.Context().Value(contextUserKey).(*model.User)
|
||||
q = q.Joins(`
|
||||
JOIN plays
|
||||
ON folders.id = plays.folder_id AND plays.user_id = ?`,
|
||||
user.ID)
|
||||
q = q.Order("plays.count DESC")
|
||||
case "newest":
|
||||
q = q.Order("updated_at DESC")
|
||||
case "random":
|
||||
q = q.Order(gorm.Expr("random()"))
|
||||
case "recent":
|
||||
user := r.Context().Value(contextUserKey).(*model.User)
|
||||
q = q.Joins(`
|
||||
JOIN plays
|
||||
ON folders.id = plays.folder_id AND plays.user_id = ?`,
|
||||
user.ID)
|
||||
q = q.Order("plays.time DESC")
|
||||
default:
|
||||
respondError(w, r, 10, fmt.Sprintf(
|
||||
"unknown value `%s` for parameter 'type'", listType,
|
||||
))
|
||||
return
|
||||
}
|
||||
var folders []*model.Folder
|
||||
q.
|
||||
Where("folders.has_tracks = 1").
|
||||
Offset(getIntParamOr(r, "offset", 0)).
|
||||
Limit(getIntParamOr(r, "size", 10)).
|
||||
Preload("Parent").
|
||||
Find(&folders)
|
||||
listObj := []*subsonic.Album{}
|
||||
for _, folder := range folders {
|
||||
listObj = append(listObj, &subsonic.Album{
|
||||
ID: folder.ID,
|
||||
Title: folder.Name,
|
||||
Album: folder.Name,
|
||||
CoverID: folder.CoverID,
|
||||
ParentID: folder.ParentID,
|
||||
IsDir: true,
|
||||
Artist: folder.Parent.Name,
|
||||
})
|
||||
}
|
||||
sub := subsonic.NewResponse()
|
||||
sub.Albums = &subsonic.Albums{
|
||||
List: listObj,
|
||||
}
|
||||
respond(w, r, sub)
|
||||
}
|
||||
180
server/handler/handler_sub_by_tags.go
Normal file
180
server/handler/handler_sub_by_tags.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/sentriz/gonic/model"
|
||||
"github.com/sentriz/gonic/server/subsonic"
|
||||
)
|
||||
|
||||
func (c *Controller) GetArtists(w http.ResponseWriter, r *http.Request) {
|
||||
var artists []*model.AlbumArtist
|
||||
c.DB.Find(&artists)
|
||||
var indexMap = make(map[rune]*subsonic.Index)
|
||||
var indexes subsonic.Artists
|
||||
for _, artist := range artists {
|
||||
i := indexOf(artist.Name)
|
||||
index, ok := indexMap[i]
|
||||
if !ok {
|
||||
index = &subsonic.Index{
|
||||
Name: string(i),
|
||||
Artists: []*subsonic.Artist{},
|
||||
}
|
||||
indexMap[i] = index
|
||||
indexes.List = append(indexes.List, index)
|
||||
}
|
||||
index.Artists = append(index.Artists, &subsonic.Artist{
|
||||
ID: artist.ID,
|
||||
Name: artist.Name,
|
||||
})
|
||||
}
|
||||
sub := subsonic.NewResponse()
|
||||
sub.Artists = &indexes
|
||||
respond(w, r, sub)
|
||||
}
|
||||
|
||||
func (c *Controller) GetArtist(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := getIntParam(r, "id")
|
||||
if err != nil {
|
||||
respondError(w, r, 10, "please provide an `id` parameter")
|
||||
return
|
||||
}
|
||||
var artist model.AlbumArtist
|
||||
c.DB.
|
||||
Preload("Albums").
|
||||
First(&artist, id)
|
||||
albumsObj := []*subsonic.Album{}
|
||||
for _, album := range artist.Albums {
|
||||
albumsObj = append(albumsObj, &subsonic.Album{
|
||||
ID: album.ID,
|
||||
Name: album.Title,
|
||||
Created: album.CreatedAt,
|
||||
Artist: artist.Name,
|
||||
ArtistID: artist.ID,
|
||||
CoverID: album.CoverID,
|
||||
})
|
||||
}
|
||||
sub := subsonic.NewResponse()
|
||||
sub.Artist = &subsonic.Artist{
|
||||
ID: artist.ID,
|
||||
Name: artist.Name,
|
||||
Albums: albumsObj,
|
||||
}
|
||||
respond(w, r, sub)
|
||||
}
|
||||
|
||||
func (c *Controller) GetAlbum(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := getIntParam(r, "id")
|
||||
if err != nil {
|
||||
respondError(w, r, 10, "please provide an `id` parameter")
|
||||
return
|
||||
}
|
||||
var album model.Album
|
||||
c.DB.
|
||||
Preload("AlbumArtist").
|
||||
Preload("Tracks").
|
||||
First(&album, id)
|
||||
tracksObj := []*subsonic.Track{}
|
||||
for _, track := range album.Tracks {
|
||||
tracksObj = append(tracksObj, &subsonic.Track{
|
||||
ID: track.ID,
|
||||
Title: track.Title,
|
||||
Artist: track.Artist, // track artist
|
||||
TrackNo: track.TrackNumber,
|
||||
ContentType: track.ContentType,
|
||||
Path: track.Path,
|
||||
Suffix: track.Suffix,
|
||||
Created: track.CreatedAt,
|
||||
Size: track.Size,
|
||||
Album: album.Title,
|
||||
AlbumID: album.ID,
|
||||
ArtistID: album.AlbumArtist.ID, // album artist
|
||||
CoverID: album.CoverID,
|
||||
Type: "music",
|
||||
})
|
||||
}
|
||||
sub := subsonic.NewResponse()
|
||||
sub.Album = &subsonic.Album{
|
||||
ID: album.ID,
|
||||
Name: album.Title,
|
||||
CoverID: album.CoverID,
|
||||
Created: album.CreatedAt,
|
||||
Artist: album.AlbumArtist.Name,
|
||||
Tracks: tracksObj,
|
||||
}
|
||||
respond(w, r, sub)
|
||||
}
|
||||
|
||||
// changes to this function should be reflected in in _by_folder.go's
|
||||
// getAlbumList() function
|
||||
func (c *Controller) GetAlbumListTwo(w http.ResponseWriter, r *http.Request) {
|
||||
listType := getStrParam(r, "type")
|
||||
if listType == "" {
|
||||
respondError(w, r, 10, "please provide a `type` parameter")
|
||||
return
|
||||
}
|
||||
q := c.DB
|
||||
switch listType {
|
||||
case "alphabeticalByArtist":
|
||||
q = q.Joins(`
|
||||
JOIN album_artists
|
||||
ON albums.album_artist_id = album_artists.id`)
|
||||
q = q.Order("album_artists.name")
|
||||
case "alphabeticalByName":
|
||||
q = q.Order("title")
|
||||
case "byYear":
|
||||
q = q.Where(
|
||||
"year BETWEEN ? AND ?",
|
||||
getIntParamOr(r, "fromYear", 1800),
|
||||
getIntParamOr(r, "toYear", 2200))
|
||||
q = q.Order("year")
|
||||
case "frequent":
|
||||
user := r.Context().Value(contextUserKey).(*model.User)
|
||||
q = q.Joins(`
|
||||
JOIN plays
|
||||
ON albums.id = plays.album_id AND plays.user_id = ?`,
|
||||
user.ID)
|
||||
q = q.Order("plays.count DESC")
|
||||
case "newest":
|
||||
q = q.Order("updated_at DESC")
|
||||
case "random":
|
||||
q = q.Order(gorm.Expr("random()"))
|
||||
case "recent":
|
||||
user := r.Context().Value(contextUserKey).(*model.User)
|
||||
q = q.Joins(`
|
||||
JOIN plays
|
||||
ON albums.id = plays.album_id AND plays.user_id = ?`,
|
||||
user.ID)
|
||||
q = q.Order("plays.time DESC")
|
||||
default:
|
||||
respondError(w, r, 10, fmt.Sprintf(
|
||||
"unknown value `%s` for parameter 'type'", listType,
|
||||
))
|
||||
return
|
||||
}
|
||||
var albums []*model.Album
|
||||
q.
|
||||
Offset(getIntParamOr(r, "offset", 0)).
|
||||
Limit(getIntParamOr(r, "size", 10)).
|
||||
Preload("AlbumArtist").
|
||||
Find(&albums)
|
||||
listObj := []*subsonic.Album{}
|
||||
for _, album := range albums {
|
||||
listObj = append(listObj, &subsonic.Album{
|
||||
ID: album.ID,
|
||||
Name: album.Title,
|
||||
Created: album.CreatedAt,
|
||||
CoverID: album.CoverID,
|
||||
Artist: album.AlbumArtist.Name,
|
||||
ArtistID: album.AlbumArtist.ID,
|
||||
})
|
||||
}
|
||||
sub := subsonic.NewResponse()
|
||||
sub.AlbumsTwo = &subsonic.Albums{
|
||||
List: listObj,
|
||||
}
|
||||
respond(w, r, sub)
|
||||
}
|
||||
157
server/handler/handler_sub_common.go
Normal file
157
server/handler/handler_sub_common.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/rainycape/unidecode"
|
||||
|
||||
"github.com/sentriz/gonic/lastfm"
|
||||
"github.com/sentriz/gonic/model"
|
||||
"github.com/sentriz/gonic/scanner"
|
||||
"github.com/sentriz/gonic/server/subsonic"
|
||||
)
|
||||
|
||||
func indexOf(s string) rune {
|
||||
first := string(s[0])
|
||||
c := rune(unidecode.Unidecode(first)[0])
|
||||
if !unicode.IsLetter(c) {
|
||||
return '#'
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Controller) Stream(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := getIntParam(r, "id")
|
||||
if err != nil {
|
||||
respondError(w, r, 10, "please provide an `id` parameter")
|
||||
return
|
||||
}
|
||||
var track model.Track
|
||||
c.DB.
|
||||
Preload("Album").
|
||||
Preload("Folder").
|
||||
First(&track, id)
|
||||
if track.Path == "" {
|
||||
respondError(w, r, 70, fmt.Sprintf("media with id `%d` was not found", id))
|
||||
return
|
||||
}
|
||||
file, err := os.Open(track.Path)
|
||||
if err != nil {
|
||||
respondError(w, r, 0, fmt.Sprintf("error while streaming media: %v", err))
|
||||
return
|
||||
}
|
||||
stat, _ := file.Stat()
|
||||
http.ServeContent(w, r, track.Path, stat.ModTime(), file)
|
||||
//
|
||||
// after we've served the file, mark the album as played
|
||||
user := r.Context().Value(contextUserKey).(*model.User)
|
||||
play := model.Play{
|
||||
AlbumID: track.Album.ID,
|
||||
FolderID: track.Folder.ID,
|
||||
UserID: user.ID,
|
||||
}
|
||||
c.DB.Where(play).First(&play)
|
||||
play.Time = time.Now() // for getAlbumList?type=recent
|
||||
play.Count++ // for getAlbumList?type=frequent
|
||||
c.DB.Save(&play)
|
||||
}
|
||||
|
||||
func (c *Controller) GetCoverArt(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := getIntParam(r, "id")
|
||||
if err != nil {
|
||||
respondError(w, r, 10, "please provide an `id` parameter")
|
||||
return
|
||||
}
|
||||
var cover model.Cover
|
||||
c.DB.First(&cover, id)
|
||||
w.Write(cover.Image)
|
||||
}
|
||||
|
||||
func (c *Controller) GetLicence(w http.ResponseWriter, r *http.Request) {
|
||||
sub := subsonic.NewResponse()
|
||||
sub.Licence = &subsonic.Licence{
|
||||
Valid: true,
|
||||
}
|
||||
respond(w, r, sub)
|
||||
}
|
||||
|
||||
func (c *Controller) Ping(w http.ResponseWriter, r *http.Request) {
|
||||
sub := subsonic.NewResponse()
|
||||
respond(w, r, sub)
|
||||
}
|
||||
|
||||
func (c *Controller) Scrobble(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := getIntParam(r, "id")
|
||||
if err != nil {
|
||||
respondError(w, r, 10, "please provide an `id` parameter")
|
||||
return
|
||||
}
|
||||
// fetch user to get lastfm session
|
||||
user := r.Context().Value(contextUserKey).(*model.User)
|
||||
if user.LastFMSession == "" {
|
||||
respondError(w, r, 0, fmt.Sprintf("no last.fm session for this user: %v", err))
|
||||
return
|
||||
}
|
||||
// fetch track for getting info to send to last.fm function
|
||||
var track model.Track
|
||||
c.DB.
|
||||
Preload("Album").
|
||||
Preload("AlbumArtist").
|
||||
First(&track, id)
|
||||
// get time from args or use now
|
||||
time := getIntParamOr(r, "time", int(time.Now().Unix()))
|
||||
// get submission, where the default is true. we will
|
||||
// check if it's false later
|
||||
submission := getStrParamOr(r, "submission", "true")
|
||||
// scrobble with above info
|
||||
err = lastfm.Scrobble(
|
||||
c.GetSetting("lastfm_api_key"),
|
||||
c.GetSetting("lastfm_secret"),
|
||||
user.LastFMSession,
|
||||
&track,
|
||||
time,
|
||||
submission != "false",
|
||||
)
|
||||
if err != nil {
|
||||
respondError(w, r, 0, fmt.Sprintf("error when submitting: %v", err))
|
||||
return
|
||||
}
|
||||
sub := subsonic.NewResponse()
|
||||
respond(w, r, sub)
|
||||
}
|
||||
|
||||
func (c *Controller) GetMusicFolders(w http.ResponseWriter, r *http.Request) {
|
||||
folders := &subsonic.MusicFolders{}
|
||||
folders.List = []*subsonic.MusicFolder{
|
||||
{ID: 1, Name: "music"},
|
||||
}
|
||||
sub := subsonic.NewResponse()
|
||||
sub.MusicFolders = folders
|
||||
respond(w, r, sub)
|
||||
}
|
||||
|
||||
func (c *Controller) StartScan(w http.ResponseWriter, r *http.Request) {
|
||||
scanC := scanner.New(c.DB, c.MusicPath)
|
||||
go scanC.Start()
|
||||
c.GetScanStatus(w, r)
|
||||
}
|
||||
|
||||
func (c *Controller) GetScanStatus(w http.ResponseWriter, r *http.Request) {
|
||||
var trackCount int
|
||||
c.DB.Model(&model.Track{}).Count(&trackCount)
|
||||
sub := subsonic.NewResponse()
|
||||
sub.ScanStatus = &subsonic.ScanStatus{
|
||||
Scanning: atomic.LoadInt32(&scanner.IsScanning) == 1,
|
||||
Count: trackCount,
|
||||
}
|
||||
respond(w, r, sub)
|
||||
}
|
||||
|
||||
func (c *Controller) NotFound(w http.ResponseWriter, r *http.Request) {
|
||||
respondError(w, r, 0, "unknown route")
|
||||
}
|
||||
59
server/handler/middleware_admin.go
Normal file
59
server/handler/middleware_admin.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
|
||||
"github.com/sentriz/gonic/model"
|
||||
)
|
||||
|
||||
func (c *Controller) WithSession(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
session, _ := c.SessDB.Get(r, "gonic")
|
||||
withSession := context.WithValue(r.Context(), contextSessionKey, session)
|
||||
next.ServeHTTP(w, r.WithContext(withSession))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) WithUserSession(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// session exists at this point
|
||||
session := r.Context().Value(contextSessionKey).(*sessions.Session)
|
||||
username, ok := session.Values["user"].(string)
|
||||
if !ok {
|
||||
session.AddFlash("you are not authenticated")
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
// take username from sesion and add the user row to the context
|
||||
user := c.GetUserFromName(username)
|
||||
if user.ID == 0 {
|
||||
// the username in the client's session no longer relates to a
|
||||
// user in the database (maybe the user was deleted)
|
||||
session.Options.MaxAge = -1
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
withUser := context.WithValue(r.Context(), contextUserKey, user)
|
||||
next.ServeHTTP(w, r.WithContext(withUser))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) WithAdminSession(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// session and user exist at this point
|
||||
session := r.Context().Value(contextSessionKey).(*sessions.Session)
|
||||
user := r.Context().Value(contextUserKey).(*model.User)
|
||||
if !user.IsAdmin {
|
||||
session.AddFlash("you are not an admin")
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
13
server/handler/middleware_common.go
Normal file
13
server/handler/middleware_common.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (c *Controller) WithLogging(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("connection from `%s` for `%s`", r.RemoteAddr, r.URL)
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
98
server/handler/middleware_sub.go
Normal file
98
server/handler/middleware_sub.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
var (
|
||||
requiredParameters = []string{
|
||||
"u", "v", "c",
|
||||
}
|
||||
)
|
||||
|
||||
func checkHasAllParams(params url.Values) error {
|
||||
for _, req := range requiredParameters {
|
||||
param := params.Get(req)
|
||||
if param != "" {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("please provide a `%s` parameter", req)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkCredentialsToken(password, token, salt string) bool {
|
||||
toHash := fmt.Sprintf("%s%s", password, salt)
|
||||
hash := md5.Sum([]byte(toHash))
|
||||
expToken := hex.EncodeToString(hash[:])
|
||||
return token == expToken
|
||||
}
|
||||
|
||||
func checkCredentialsBasic(password, givenPassword string) bool {
|
||||
if givenPassword[:4] == "enc:" {
|
||||
bytes, _ := hex.DecodeString(givenPassword[4:])
|
||||
givenPassword = string(bytes)
|
||||
}
|
||||
return password == givenPassword
|
||||
}
|
||||
|
||||
func (c *Controller) WithValidSubsonicArgs(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
err := checkHasAllParams(r.URL.Query())
|
||||
if err != nil {
|
||||
respondError(w, r, 10, err.Error())
|
||||
return
|
||||
}
|
||||
username, password := r.URL.Query().Get("u"),
|
||||
r.URL.Query().Get("p")
|
||||
token, salt := r.URL.Query().Get("t"),
|
||||
r.URL.Query().Get("s")
|
||||
passwordAuth, tokenAuth := token == "" && salt == "",
|
||||
password == ""
|
||||
if tokenAuth == passwordAuth {
|
||||
respondError(w, r,
|
||||
10, "please provide parameters `t` and `s`, or just `p`",
|
||||
)
|
||||
return
|
||||
}
|
||||
user := c.GetUserFromName(username)
|
||||
if user.ID == 0 {
|
||||
// the user does not exist
|
||||
respondError(w, r, 40, "invalid username")
|
||||
return
|
||||
}
|
||||
var credsOk bool
|
||||
if tokenAuth {
|
||||
credsOk = checkCredentialsToken(user.Password, token, salt)
|
||||
} else {
|
||||
credsOk = checkCredentialsBasic(user.Password, password)
|
||||
}
|
||||
if !credsOk {
|
||||
respondError(w, r, 40, "invalid password")
|
||||
return
|
||||
}
|
||||
withUser := context.WithValue(r.Context(), contextUserKey, user)
|
||||
next.ServeHTTP(w, r.WithContext(withUser))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) WithCORS(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods",
|
||||
"POST, GET, OPTIONS, PUT, DELETE",
|
||||
)
|
||||
w.Header().Set("Access-Control-Allow-Headers",
|
||||
"Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization",
|
||||
)
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
39
server/handler/parse.go
Normal file
39
server/handler/parse.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func getStrParam(r *http.Request, key string) string {
|
||||
return r.URL.Query().Get(key)
|
||||
}
|
||||
|
||||
func getStrParamOr(r *http.Request, key, or string) string {
|
||||
val := getStrParam(r, key)
|
||||
if val == "" {
|
||||
return or
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func getIntParam(r *http.Request, key string) (int, error) {
|
||||
strVal := r.URL.Query().Get(key)
|
||||
if strVal == "" {
|
||||
return 0, fmt.Errorf("no param with key `%s`", key)
|
||||
}
|
||||
val, err := strconv.Atoi(strVal)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("not an int `%s`", strVal)
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func getIntParamOr(r *http.Request, key string, or int) int {
|
||||
val, err := getIntParam(r, key)
|
||||
if err != nil {
|
||||
return or
|
||||
}
|
||||
return val
|
||||
}
|
||||
43
server/handler/respond_admin.go
Normal file
43
server/handler/respond_admin.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
|
||||
"github.com/sentriz/gonic/model"
|
||||
)
|
||||
|
||||
type templateData struct {
|
||||
Flashes []interface{}
|
||||
User *model.User
|
||||
SelectedUser *model.User
|
||||
AllUsers []*model.User
|
||||
ArtistCount int
|
||||
AlbumCount int
|
||||
TrackCount int
|
||||
CurrentLastFMAPIKey string
|
||||
CurrentLastFMAPISecret string
|
||||
RequestRoot string
|
||||
}
|
||||
|
||||
func renderTemplate(w http.ResponseWriter, r *http.Request,
|
||||
tmpl *template.Template, data *templateData) {
|
||||
session := r.Context().Value(contextSessionKey).(*sessions.Session)
|
||||
if data == nil {
|
||||
data = &templateData{}
|
||||
}
|
||||
data.Flashes = session.Flashes()
|
||||
session.Save(r, w)
|
||||
user, ok := r.Context().Value(contextUserKey).(*model.User)
|
||||
if ok {
|
||||
data.User = user
|
||||
}
|
||||
err := tmpl.Execute(w, data)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("500 when executing: %v", err), 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
56
server/handler/respond_sub.go
Normal file
56
server/handler/respond_sub.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/sentriz/gonic/server/subsonic"
|
||||
)
|
||||
|
||||
func respondRaw(w http.ResponseWriter, r *http.Request,
|
||||
code int, sub *subsonic.Response) {
|
||||
res := subsonic.MetaResponse{
|
||||
Response: sub,
|
||||
}
|
||||
switch r.URL.Query().Get("f") {
|
||||
case "json":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
data, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
log.Printf("could not marshall to json: %v\n", err)
|
||||
}
|
||||
w.Write(data)
|
||||
case "jsonp":
|
||||
w.Header().Set("Content-Type", "application/javascript")
|
||||
data, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
log.Printf("could not marshall to json: %v\n", err)
|
||||
}
|
||||
callback := r.URL.Query().Get("callback")
|
||||
w.Write([]byte(callback))
|
||||
w.Write([]byte("("))
|
||||
w.Write(data)
|
||||
w.Write([]byte(");"))
|
||||
default:
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
data, err := xml.MarshalIndent(res, "", " ")
|
||||
if err != nil {
|
||||
log.Printf("could not marshall to xml: %v\n", err)
|
||||
}
|
||||
w.Write(data)
|
||||
}
|
||||
}
|
||||
|
||||
func respond(w http.ResponseWriter, r *http.Request,
|
||||
sub *subsonic.Response) {
|
||||
respondRaw(w, r, http.StatusOK, sub)
|
||||
}
|
||||
|
||||
func respondError(w http.ResponseWriter, r *http.Request,
|
||||
code int, message string) {
|
||||
respondRaw(w, r, http.StatusBadRequest, subsonic.NewError(
|
||||
code, message,
|
||||
))
|
||||
}
|
||||
54
server/server.go
Normal file
54
server/server.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
|
||||
"github.com/sentriz/gonic/server/handler"
|
||||
)
|
||||
|
||||
type middleware func(next http.HandlerFunc) http.HandlerFunc
|
||||
|
||||
func newChain(wares ...middleware) middleware {
|
||||
return func(final http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
last := final
|
||||
for i := len(wares) - 1; i >= 0; i-- {
|
||||
last = wares[i](last)
|
||||
}
|
||||
last(w, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
mux *http.ServeMux
|
||||
*handler.Controller
|
||||
*http.Server
|
||||
}
|
||||
|
||||
func New(db *gorm.DB, musicPath string, listenAddr string) *Server {
|
||||
mux := http.NewServeMux()
|
||||
server := &http.Server{
|
||||
Addr: listenAddr,
|
||||
Handler: mux,
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
IdleTimeout: 15 * time.Second,
|
||||
}
|
||||
controller := &handler.Controller{
|
||||
DB: db,
|
||||
MusicPath: musicPath,
|
||||
}
|
||||
ret := &Server{
|
||||
mux: mux,
|
||||
Server: server,
|
||||
Controller: controller,
|
||||
}
|
||||
ret.setupAdmin()
|
||||
ret.setupSubsonic()
|
||||
return ret
|
||||
}
|
||||
83
server/setup_admin.go
Normal file
83
server/setup_admin.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gobuffalo/packr/v2"
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/wader/gormstore"
|
||||
)
|
||||
|
||||
func extendFromBox(tmpl *template.Template, box *packr.Box, key string) *template.Template {
|
||||
strT, err := box.FindString(key)
|
||||
if err != nil {
|
||||
log.Fatalf("error when reading template from box: %v", err)
|
||||
}
|
||||
if tmpl == nil {
|
||||
tmpl = template.New("layout")
|
||||
} else {
|
||||
tmpl = template.Must(tmpl.Clone())
|
||||
}
|
||||
newT, err := tmpl.Parse(strT)
|
||||
if err != nil {
|
||||
log.Fatalf("error when parsing template template: %v", err)
|
||||
}
|
||||
return newT
|
||||
}
|
||||
|
||||
func (s *Server) setupAdmin() {
|
||||
sessionKey := []byte(s.GetSetting("session_key"))
|
||||
if len(sessionKey) == 0 {
|
||||
sessionKey = securecookie.GenerateRandomKey(32)
|
||||
s.SetSetting("session_key", string(sessionKey))
|
||||
}
|
||||
// create gormstore (and cleanup) for backend sessions
|
||||
s.SessDB = gormstore.New(s.DB, []byte(sessionKey))
|
||||
go s.SessDB.PeriodicCleanup(1*time.Hour, nil)
|
||||
// using packr to bundle templates and static files
|
||||
box := packr.New("templates", "templates")
|
||||
layoutT := extendFromBox(nil, box, "layout.tmpl")
|
||||
userT := extendFromBox(layoutT, box, "user.tmpl")
|
||||
s.Templates = map[string]*template.Template{
|
||||
"login": extendFromBox(layoutT, box, "pages/login.tmpl"),
|
||||
"home": extendFromBox(userT, box, "pages/home.tmpl"),
|
||||
"change_own_password": extendFromBox(userT, box, "pages/change_own_password.tmpl"),
|
||||
"change_password": extendFromBox(userT, box, "pages/change_password.tmpl"),
|
||||
"delete_user": extendFromBox(userT, box, "pages/delete_user.tmpl"),
|
||||
"create_user": extendFromBox(userT, box, "pages/create_user.tmpl"),
|
||||
"update_lastfm_api_key": extendFromBox(userT, box, "pages/update_lastfm_api_key.tmpl"),
|
||||
}
|
||||
withPublicWare := newChain(
|
||||
s.WithLogging,
|
||||
s.WithSession,
|
||||
)
|
||||
withUserWare := newChain(
|
||||
withPublicWare,
|
||||
s.WithUserSession,
|
||||
)
|
||||
withAdminWare := newChain(
|
||||
withUserWare,
|
||||
s.WithAdminSession,
|
||||
)
|
||||
server := http.FileServer(packr.New("static", "static"))
|
||||
s.mux.Handle("/admin/static/", http.StripPrefix("/admin/static/", server))
|
||||
s.mux.HandleFunc("/admin/login", withPublicWare(s.ServeLogin))
|
||||
s.mux.HandleFunc("/admin/login_do", withPublicWare(s.ServeLoginDo))
|
||||
s.mux.HandleFunc("/admin/logout", withUserWare(s.ServeLogout))
|
||||
s.mux.HandleFunc("/admin/home", withUserWare(s.ServeHome))
|
||||
s.mux.HandleFunc("/admin/change_own_password", withUserWare(s.ServeChangeOwnPassword))
|
||||
s.mux.HandleFunc("/admin/change_own_password_do", withUserWare(s.ServeChangeOwnPasswordDo))
|
||||
s.mux.HandleFunc("/admin/link_lastfm_do", withUserWare(s.ServeLinkLastFMDo))
|
||||
s.mux.HandleFunc("/admin/unlink_lastfm_do", withUserWare(s.ServeUnlinkLastFMDo))
|
||||
s.mux.HandleFunc("/admin/change_password", withAdminWare(s.ServeChangePassword))
|
||||
s.mux.HandleFunc("/admin/change_password_do", withAdminWare(s.ServeChangePasswordDo))
|
||||
s.mux.HandleFunc("/admin/delete_user", withAdminWare(s.ServeDeleteUser))
|
||||
s.mux.HandleFunc("/admin/delete_user_do", withAdminWare(s.ServeDeleteUserDo))
|
||||
s.mux.HandleFunc("/admin/create_user", withAdminWare(s.ServeCreateUser))
|
||||
s.mux.HandleFunc("/admin/create_user_do", withAdminWare(s.ServeCreateUserDo))
|
||||
s.mux.HandleFunc("/admin/update_lastfm_api_key", withAdminWare(s.ServeUpdateLastFMAPIKey))
|
||||
s.mux.HandleFunc("/admin/update_lastfm_api_key_do", withAdminWare(s.ServeUpdateLastFMAPIKeyDo))
|
||||
}
|
||||
44
server/setup_subsonic.go
Normal file
44
server/setup_subsonic.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package server
|
||||
|
||||
func (s *Server) setupSubsonic() {
|
||||
withWare := newChain(
|
||||
s.WithLogging,
|
||||
s.WithCORS,
|
||||
s.WithValidSubsonicArgs,
|
||||
)
|
||||
// common
|
||||
s.mux.HandleFunc("/rest/download", withWare(s.Stream))
|
||||
s.mux.HandleFunc("/rest/download.view", withWare(s.Stream))
|
||||
s.mux.HandleFunc("/rest/stream", withWare(s.Stream))
|
||||
s.mux.HandleFunc("/rest/stream.view", withWare(s.Stream))
|
||||
s.mux.HandleFunc("/rest/getCoverArt", withWare(s.GetCoverArt))
|
||||
s.mux.HandleFunc("/rest/getCoverArt.view", withWare(s.GetCoverArt))
|
||||
s.mux.HandleFunc("/rest/getLicense", withWare(s.GetLicence))
|
||||
s.mux.HandleFunc("/rest/getLicense.view", withWare(s.GetLicence))
|
||||
s.mux.HandleFunc("/rest/ping", withWare(s.Ping))
|
||||
s.mux.HandleFunc("/rest/ping.view", withWare(s.Ping))
|
||||
s.mux.HandleFunc("/rest/scrobble", withWare(s.Scrobble))
|
||||
s.mux.HandleFunc("/rest/scrobble.view", withWare(s.Scrobble))
|
||||
s.mux.HandleFunc("/rest/getMusicFolders", withWare(s.GetMusicFolders))
|
||||
s.mux.HandleFunc("/rest/getMusicFolders.view", withWare(s.GetMusicFolders))
|
||||
s.mux.HandleFunc("/rest/startScan", withWare(s.StartScan))
|
||||
s.mux.HandleFunc("/rest/startScan.view", withWare(s.StartScan))
|
||||
s.mux.HandleFunc("/rest/getScanStatus", withWare(s.GetScanStatus))
|
||||
s.mux.HandleFunc("/rest/getScanStatus.view", withWare(s.GetScanStatus))
|
||||
// browse by tag
|
||||
s.mux.HandleFunc("/rest/getAlbum", withWare(s.GetAlbum))
|
||||
s.mux.HandleFunc("/rest/getAlbum.view", withWare(s.GetAlbum))
|
||||
s.mux.HandleFunc("/rest/getAlbumList2", withWare(s.GetAlbumListTwo))
|
||||
s.mux.HandleFunc("/rest/getAlbumList2.view", withWare(s.GetAlbumListTwo))
|
||||
s.mux.HandleFunc("/rest/getArtist", withWare(s.GetArtist))
|
||||
s.mux.HandleFunc("/rest/getArtist.view", withWare(s.GetArtist))
|
||||
s.mux.HandleFunc("/rest/getArtists", withWare(s.GetArtists))
|
||||
s.mux.HandleFunc("/rest/getArtists.view", withWare(s.GetArtists))
|
||||
// browse by folder
|
||||
s.mux.HandleFunc("/rest/getIndexes", withWare(s.GetIndexes))
|
||||
s.mux.HandleFunc("/rest/getIndexes.view", withWare(s.GetIndexes))
|
||||
s.mux.HandleFunc("/rest/getMusicDirectory", withWare(s.GetMusicDirectory))
|
||||
s.mux.HandleFunc("/rest/getMusicDirectory.view", withWare(s.GetMusicDirectory))
|
||||
s.mux.HandleFunc("/rest/getAlbumList", withWare(s.GetAlbumList))
|
||||
s.mux.HandleFunc("/rest/getAlbumList.view", withWare(s.GetAlbumList))
|
||||
}
|
||||
BIN
server/static/images/favicon.ico
Normal file
BIN
server/static/images/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
server/static/images/gonic.png
Normal file
BIN
server/static/images/gonic.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 236 KiB |
7
server/static/stylesheets/awsm.css
Normal file
7
server/static/stylesheets/awsm.css
Normal file
File diff suppressed because one or more lines are too long
114
server/static/stylesheets/main.css
Normal file
114
server/static/stylesheets/main.css
Normal file
@@ -0,0 +1,114 @@
|
||||
form input[type],
|
||||
form select,
|
||||
form textarea {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
form {
|
||||
max-width: 400px;
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
form input[type=password],
|
||||
form input[type=text] {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
form input[type=submit] {
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
div {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
a,
|
||||
a:visited {
|
||||
color: #0064c1;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
#content>* {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
#header {
|
||||
border-bottom: 2px solid #0000001a;
|
||||
padding-top: 3rem;
|
||||
}
|
||||
|
||||
#header img {
|
||||
max-width: 580px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#flashes {
|
||||
background-color: #fd1b1b1c;
|
||||
border-right: 2px solid #fd1b1b1c;
|
||||
border-bottom: 2px solid #fd1b1b1c;
|
||||
}
|
||||
|
||||
.right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.light {
|
||||
color: #00000082;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.pre {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.box {
|
||||
background-color: #00000005;
|
||||
border-right: 2px solid #0000000c;
|
||||
border-bottom: 2px solid #0000000c;
|
||||
}
|
||||
|
||||
.box-title {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.padded {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.side-padded {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.angry {
|
||||
background-color: #f4433669;
|
||||
}
|
||||
|
||||
i.mdi {
|
||||
font-size: 14px;
|
||||
}
|
||||
3
server/static/stylesheets/tacit.css
Normal file
3
server/static/stylesheets/tacit.css
Normal file
File diff suppressed because one or more lines are too long
123
server/subsonic/media.go
Normal file
123
server/subsonic/media.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package subsonic
|
||||
|
||||
import "time"
|
||||
|
||||
type Albums struct {
|
||||
List []*Album `xml:"album" json:"album,omitempty"`
|
||||
}
|
||||
|
||||
type Album struct {
|
||||
// common
|
||||
ID int `xml:"id,attr,omitempty" json:"id"`
|
||||
CoverID int `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"`
|
||||
ArtistID int `xml:"artistId,attr,omitempty" json:"artistId,omitempty"`
|
||||
Artist string `xml:"artist,attr,omitempty" json:"artist,omitempty"`
|
||||
// browsing by folder (getAlbumList)
|
||||
Title string `xml:"title,attr,omitempty" json:"title,omitempty"`
|
||||
Album string `xml:"album,attr,omitempty" json:"album,omitempty"`
|
||||
ParentID int `xml:"parent,attr,omitempty" json:"parent,omitempty"`
|
||||
IsDir bool `xml:"isDir,attr,omitempty" json:"isDir,omitempty"`
|
||||
// browsing by tags (getAlbumList2)
|
||||
Name string `xml:"name,attr,omitempty" json:"name,omitempty"`
|
||||
TrackCount int `xml:"songCount,attr,omitempty" json:"songCount,omitempty"`
|
||||
Duration int `xml:"duration,attr,omitempty" json:"duration,omitempty"`
|
||||
Created time.Time `xml:"created,attr,omitempty" json:"created,omitempty"`
|
||||
Tracks []*Track `xml:"song,omitempty" json:"song,omitempty"`
|
||||
}
|
||||
|
||||
type RandomTracks struct {
|
||||
Tracks []*Track `xml:"song" json:"song"`
|
||||
}
|
||||
|
||||
type Track struct {
|
||||
ID int `xml:"id,attr,omitempty" json:"id"`
|
||||
Parent int `xml:"parent,attr,omitempty" json:"parent"`
|
||||
Title string `xml:"title,attr,omitempty" json:"title"`
|
||||
Album string `xml:"album,attr,omitempty" json:"album"`
|
||||
Artist string `xml:"artist,attr,omitempty" json:"artist"`
|
||||
IsDir bool `xml:"isDir,attr,omitempty" json:"isDir"`
|
||||
CoverID int `xml:"coverArt,attr,omitempty" json:"coverArt"`
|
||||
Created time.Time `xml:"created,attr,omitempty" json:"created"`
|
||||
Duration int `xml:"duration,attr,omitempty" json:"duration"`
|
||||
Genre string `xml:"genre,attr,omitempty" json:"genre"`
|
||||
Bitrate int `xml:"bitRate,attr,omitempty" json:"bitRate"`
|
||||
Size int `xml:"size,attr,omitempty" json:"size"`
|
||||
Suffix string `xml:"suffix,attr,omitempty" json:"suffix"`
|
||||
ContentType string `xml:"contentType,attr,omitempty" json:"contentType"`
|
||||
IsVideo bool `xml:"isVideo,attr,omitempty" json:"isVideo"`
|
||||
Path string `xml:"path,attr,omitempty" json:"path"`
|
||||
AlbumID int `xml:"albumId,attr,omitempty" json:"albumId"`
|
||||
ArtistID int `xml:"artistId,attr,omitempty" json:"artistId"`
|
||||
TrackNo int `xml:"track,attr,omitempty" json:"track"`
|
||||
Type string `xml:"type,attr,omitempty" json:"type"`
|
||||
}
|
||||
|
||||
type Artists struct {
|
||||
List []*Index `xml:"index,omitempty" json:"index,omitempty"`
|
||||
}
|
||||
|
||||
type Artist struct {
|
||||
ID int `xml:"id,attr,omitempty" json:"id"`
|
||||
Name string `xml:"name,attr,omitempty" json:"name"`
|
||||
CoverID int `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"`
|
||||
AlbumCount int `xml:"albumCount,attr,omitempty" json:"albumCount,omitempty"`
|
||||
Albums []*Album `xml:"album,omitempty" json:"album,omitempty"`
|
||||
}
|
||||
|
||||
type Indexes struct {
|
||||
LastModified int `xml:"lastModified,attr,omitempty" json:"lastModified"`
|
||||
Index []*Index `xml:"index,omitempty" json:"index"`
|
||||
}
|
||||
|
||||
type Index struct {
|
||||
Name string `xml:"name,attr,omitempty" json:"name"`
|
||||
Artists []*Artist `xml:"artist,omitempty" json:"artist"`
|
||||
}
|
||||
|
||||
type Directory struct {
|
||||
ID int `xml:"id,attr,omitempty" json:"id"`
|
||||
Parent int `xml:"parent,attr,omitempty" json:"parent"`
|
||||
Name string `xml:"name,attr,omitempty" json:"name"`
|
||||
Starred string `xml:"starred,attr,omitempty" json:"starred,omitempty"`
|
||||
Children []*Child `xml:"child,omitempty" json:"child"`
|
||||
}
|
||||
|
||||
type Child struct {
|
||||
ID int `xml:"id,attr,omitempty" json:"id,omitempty"`
|
||||
Parent int `xml:"parent,attr,omitempty" json:"parent,omitempty"`
|
||||
Title string `xml:"title,attr,omitempty" json:"title,omitempty"`
|
||||
IsDir bool `xml:"isDir,attr,omitempty" json:"isDir,omitempty"`
|
||||
Album string `xml:"album,attr,omitempty" json:"album,omitempty"`
|
||||
AlbumID int `xml:"albumId,attr,omitempty" json:"albumId,omitempty"`
|
||||
Artist string `xml:"artist,attr,omitempty" json:"artist,omitempty"`
|
||||
ArtistID int `xml:"artistId,attr,omitempty" json:"artistId,omitempty"`
|
||||
Track int `xml:"track,attr,omitempty" json:"track,omitempty"`
|
||||
Year int `xml:"year,attr,omitempty" json:"year,omitempty"`
|
||||
Genre string `xml:"genre,attr,omitempty" json:"genre,omitempty"`
|
||||
CoverID int `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"`
|
||||
Size int `xml:"size,attr,omitempty" json:"size,omitempty"`
|
||||
ContentType string `xml:"contentType,attr,omitempty" json:"contentType,omitempty"`
|
||||
Suffix string `xml:"suffix,attr,omitempty" json:"suffix,omitempty"`
|
||||
Duration int `xml:"duration,attr,omitempty" json:"duration,omitempty"`
|
||||
Bitrate int `xml:"bitRate,attr,omitempty" json:"bitrate,omitempty"`
|
||||
Path string `xml:"path,attr,omitempty" json:"path,omitempty"`
|
||||
Type string `xml:"type,attr,omitempty" json:"type,omitempty"`
|
||||
}
|
||||
|
||||
type MusicFolders struct {
|
||||
List []*MusicFolder `xml:"musicFolder,omitempty" json:"musicFolder,omitempty"`
|
||||
}
|
||||
|
||||
type MusicFolder struct {
|
||||
ID int `xml:"id,attr,omitempty" json:"id,omitempty"`
|
||||
Name string `xml:"name,attr,omitempty" json:"name,omitempty"`
|
||||
}
|
||||
|
||||
type Licence struct {
|
||||
Valid bool `xml:"valid,attr,omitempty" json:"valid,omitempty"`
|
||||
}
|
||||
|
||||
type ScanStatus struct {
|
||||
Scanning bool `xml:"scanning,attr" json:"scanning"`
|
||||
Count int `xml:"count,attr,omitempty" json:"count,omitempty"`
|
||||
}
|
||||
59
server/subsonic/response.go
Normal file
59
server/subsonic/response.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// from "sonicmonkey" by https://github.com/jeena/sonicmonkey/
|
||||
|
||||
package subsonic
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
var (
|
||||
apiVersion = "1.9.0"
|
||||
xmlns = "http://subsonic.org/restapi"
|
||||
)
|
||||
|
||||
type MetaResponse struct {
|
||||
XMLName xml.Name `xml:"subsonic-response" json:"-"`
|
||||
*Response `json:"subsonic-response"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Status string `xml:"status,attr" json:"status"`
|
||||
Version string `xml:"version,attr" json:"version"`
|
||||
XMLNS string `xml:"xmlns,attr" json:"-"`
|
||||
Error *Error `xml:"error" json:"error,omitempty"`
|
||||
AlbumsTwo *Albums `xml:"albumList2" json:"albumList2,omitempty"`
|
||||
Albums *Albums `xml:"albumList" json:"albumList,omitempty"`
|
||||
Album *Album `xml:"album" json:"album,omitempty"`
|
||||
Track *Track `xml:"song" json:"song,omitempty"`
|
||||
Indexes *Indexes `xml:"indexes" json:"indexes,omitempty"`
|
||||
Artists *Artists `xml:"artists" json:"artists,omitempty"`
|
||||
Artist *Artist `xml:"artist" json:"artist,omitempty"`
|
||||
Directory *Directory `xml:"directory" json:"directory,omitempty"`
|
||||
RandomTracks *RandomTracks `xml:"randomSongs" json:"randomSongs,omitempty"`
|
||||
MusicFolders *MusicFolders `xml:"musicFolders" json:"musicFolders,omitempty"`
|
||||
ScanStatus *ScanStatus `xml:"scanStatus" json:"scanStatus,omitempty"`
|
||||
Licence *Licence `xml:"license" json:"license,omitempty"`
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
Code int `xml:"code,attr" json:"code"`
|
||||
Message string `xml:"message,attr" json:"message"`
|
||||
}
|
||||
|
||||
func NewResponse() *Response {
|
||||
return &Response{
|
||||
Status: "ok",
|
||||
XMLNS: xmlns,
|
||||
Version: apiVersion,
|
||||
}
|
||||
}
|
||||
|
||||
func NewError(code int, message string) *Response {
|
||||
return &Response{
|
||||
Status: "failed",
|
||||
XMLNS: xmlns,
|
||||
Version: apiVersion,
|
||||
Error: &Error{
|
||||
Code: code,
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
}
|
||||
35
server/templates/layout.tmpl
Normal file
35
server/templates/layout.tmpl
Normal file
@@ -0,0 +1,35 @@
|
||||
{{ define "layout" }}
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{ template "title" }}</title>
|
||||
<link rel="stylesheet" href="https://cdn.materialdesignicons.com/3.6.95/css/materialdesignicons.min.css">
|
||||
<link rel="stylesheet" href="/admin/static/stylesheets/awsm.css">
|
||||
<link rel="stylesheet" href="/admin/static/stylesheets/main.css">
|
||||
<link rel="shortcut icon" href="/admin/static/images/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" href="/admin/static/images/favicon.ico" type="image/x-icon">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<div id="header">
|
||||
<img src="/admin/static/images/gonic.png">
|
||||
</div>
|
||||
<div id="content">
|
||||
{{ if .Flashes }}
|
||||
<div id="flashes" class="padded mono">
|
||||
<i class="mdi mdi-alert-circle"></i> {{ index .Flashes 0 }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ template "content" . }}
|
||||
</div>
|
||||
</div>
|
||||
<div id="footer" class="padded mono">
|
||||
senan kelly, 2019
|
||||
<span class="light">|</span>
|
||||
<a href="https://github.com/sentriz/gonic">github</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
||||
14
server/templates/pages/change_own_password.tmpl
Normal file
14
server/templates/pages/change_own_password.tmpl
Normal file
@@ -0,0 +1,14 @@
|
||||
{{ define "title" }}home{{ end }}
|
||||
|
||||
{{ define "user" }}
|
||||
<div class="padded box mono">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-account-key"></i> changing account password
|
||||
</div>
|
||||
<form action="/admin/change_own_password_do" method="post">
|
||||
<input type="password" id="password_one" name="password_one" placeholder="new password">
|
||||
<input type="password" id="password_two" name="password_two" placeholder="verify new password">
|
||||
<input type="submit" value="change">
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
14
server/templates/pages/change_password.tmpl
Normal file
14
server/templates/pages/change_password.tmpl
Normal file
@@ -0,0 +1,14 @@
|
||||
{{ define "title" }}home{{ end }}
|
||||
|
||||
{{ define "user" }}
|
||||
<div class="padded box mono">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-account-key"></i> changing {{ .SelectedUser.Name }}'s password
|
||||
</div>
|
||||
<form action="/admin/change_password_do?user={{ .SelectedUser.Name }}" method="post">
|
||||
<input type="password" id="password_one" name="password_one" placeholder="new password">
|
||||
<input type="password" id="password_two" name="password_two" placeholder="verify new password">
|
||||
<input type="submit" value="change">
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
15
server/templates/pages/create_user.tmpl
Normal file
15
server/templates/pages/create_user.tmpl
Normal file
@@ -0,0 +1,15 @@
|
||||
{{ define "title" }}home{{ end }}
|
||||
|
||||
{{ define "user" }}
|
||||
<div class="padded box mono">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-account-plus"></i> creating new user
|
||||
</div>
|
||||
<form action="/admin/create_user_do" method="post">
|
||||
<input type="text" id="username" name="username" placeholder="username">
|
||||
<input type="password" id="password_one" name="password_one" placeholder="password">
|
||||
<input type="password" id="password_two" name="password_two" placeholder="verify password">
|
||||
<input type="submit" value="create">
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
15
server/templates/pages/delete_user.tmpl
Normal file
15
server/templates/pages/delete_user.tmpl
Normal file
@@ -0,0 +1,15 @@
|
||||
{{ define "title" }}home{{ end }}
|
||||
|
||||
{{ define "user" }}
|
||||
<div class="padded box mono">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-account-remove"></i> deleting user {{ .SelectedUser.Name }}
|
||||
</div>
|
||||
<div class="right">
|
||||
are you sure?
|
||||
</div>
|
||||
<form action="/admin/delete_user_do?user={{ .SelectedUser.Name }}" method="post">
|
||||
<input type="submit" value="yes">
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
64
server/templates/pages/home.tmpl
Normal file
64
server/templates/pages/home.tmpl
Normal file
@@ -0,0 +1,64 @@
|
||||
{{ define "title" }}home{{ end }}
|
||||
|
||||
{{ define "user" }}
|
||||
<div class="padded box mono">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-chart-arc"></i> stats
|
||||
</div>
|
||||
<div class="right">
|
||||
<span class="pre">artists: {{ printf "%7v" .ArtistCount }}</span><br/>
|
||||
<span class="pre">albums: {{ printf "%7v" .AlbumCount }}</span><br/>
|
||||
<span class="pre">tracks: {{ printf "%7v" .TrackCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="padded box mono">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-lastfm"></i> last.fm
|
||||
</div>
|
||||
<div class="right">
|
||||
{{ if .User.IsAdmin }}
|
||||
<a href="/admin/update_lastfm_api_key">update api key</a><br/>
|
||||
{{ end }}
|
||||
{{ if .CurrentLastFMAPIKey }}
|
||||
<span class="light">current status</span>
|
||||
{{ if .User.LastFMSession }}
|
||||
linked
|
||||
<span class="light">|</span>
|
||||
<a href="/admin/unlink_lastfm_do">unlink</a><br/>
|
||||
{{ else }}
|
||||
<span class="angry">unlinked</span>
|
||||
<a href="https://www.last.fm/api/auth/?api_key={{ .CurrentLastFMAPIKey }}&cb={{ .RequestRoot }}/admin/link_lastfm_do">link</a><br/>
|
||||
{{ end }}
|
||||
{{ else if not .User.IsAdmin }}
|
||||
<span class="light">api key not set. please ask your admin to set it</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="padded box mono">
|
||||
{{ if .User.IsAdmin }}
|
||||
{{/* admin panel to manage all users */}}
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-account-multiple"></i> users
|
||||
</div>
|
||||
<div class="right">
|
||||
{{ range $user := .AllUsers }}
|
||||
<i>{{ $user.Name }}</i>
|
||||
<span class="light">{{ $user.CreatedAt.Format "jan 02, 2006" }}</span>
|
||||
<span class="light">|</span>
|
||||
<a href="/admin/change_password?user={{ $user.Name }}">change password</a>
|
||||
<span class="light">|</span>
|
||||
<a href="/admin/delete_user?user={{ $user.Name }}">delete</a><br/>
|
||||
{{ end }}
|
||||
<a href="/admin/create_user" class="button">create new</a>
|
||||
</div>
|
||||
{{ else }}
|
||||
{{/* user panel to manage themselves */}}
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-account"></i> your account
|
||||
</div>
|
||||
<div class="right">
|
||||
<a href="/admin/change_own_password" class="button">change password</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
14
server/templates/pages/login.tmpl
Normal file
14
server/templates/pages/login.tmpl
Normal file
@@ -0,0 +1,14 @@
|
||||
{{ define "title" }}gonic{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
<div class="padded box mono">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-login-variant"></i> login
|
||||
</div>
|
||||
<form action="/admin/login_do" method="post">
|
||||
<input type="text" id="username" name="username" placeholder="username">
|
||||
<input type="password" id="password" name="password" placeholder="password">
|
||||
<input type="submit" value="login">
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
18
server/templates/pages/update_lastfm_api_key.tmpl
Normal file
18
server/templates/pages/update_lastfm_api_key.tmpl
Normal file
@@ -0,0 +1,18 @@
|
||||
{{ define "title" }}home{{ end }}
|
||||
|
||||
{{ define "user" }}
|
||||
<div class="padded box mono">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-key-change"></i> updating last.fm keys
|
||||
</div>
|
||||
<div class="right">
|
||||
<span class="light">current key</span> <i>{{ if .CurrentLastFMAPIKey }}{{ .CurrentLastFMAPIKey }}{{ else }}not set{{ end }}</i><br/>
|
||||
<span class="light">current secret</span> <i>{{ if .CurrentLastFMAPISecret }}{{ .CurrentLastFMAPISecret }}{{ else }}not set{{ end }}</i>
|
||||
</div>
|
||||
<form action="/admin/update_lastfm_api_key_do" method="post">
|
||||
<input type="text" id="api_key" name="api_key" placeholder="new key">
|
||||
<input type="text" id="secret" name="secret" placeholder="new secret">
|
||||
<input type="submit" value="update">
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
12
server/templates/user.tmpl
Normal file
12
server/templates/user.tmpl
Normal file
@@ -0,0 +1,12 @@
|
||||
{{ define "title" }}home{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
<div class="side-padded light right mono">
|
||||
welcome {{ .User.Name }}
|
||||
|
|
||||
<a href="/admin/home">home</a>
|
||||
|
|
||||
<a href="/admin/logout">logout <i class="mdi mdi-logout-variant"></i></a>
|
||||
</div>
|
||||
{{ template "user" . }}
|
||||
{{ end }}
|
||||
Reference in New Issue
Block a user