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,
|
||||
))
|
||||
}
|
||||
Reference in New Issue
Block a user