eg
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,8 +1,6 @@
|
||||
*.db
|
||||
*-packr.go
|
||||
packrd/
|
||||
scanner
|
||||
server
|
||||
cmd/scanner/scanner
|
||||
cmd/server/server
|
||||
_test*
|
||||
|
||||
@@ -1,245 +1,46 @@
|
||||
// this scanner tries to scan with a single unsorted walk of the music
|
||||
// directory - which means you can come across the cover of an album/folder
|
||||
// before the tracks (and therefore the album) which is an issue because
|
||||
// when inserting into the album table, we need a reference to the cover.
|
||||
// to solve this we're using godirwalk's PostChildrenCallback and some globals
|
||||
//
|
||||
// Album -> needs a CoverID
|
||||
// -> needs a FolderID (American Football)
|
||||
// Folder -> needs a CoverID
|
||||
// -> needs a ParentID
|
||||
// Track -> needs an AlbumID
|
||||
// -> needs a FolderID
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/dhowden/tag"
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
"github.com/karrick/godirwalk"
|
||||
"github.com/peterbourgon/ff"
|
||||
|
||||
"github.com/sentriz/gonic/db"
|
||||
"github.com/sentriz/gonic/scanner"
|
||||
)
|
||||
|
||||
var (
|
||||
orm *gorm.DB
|
||||
tx *gorm.DB
|
||||
// seenPaths is used to keep every path we've seen so that
|
||||
// we can remove old tracks, folders, and covers by path when we
|
||||
// are in the cleanDatabase stage
|
||||
seenPaths = make(map[string]bool)
|
||||
// currentDirStack is used for inserting to the folders (subsonic browse
|
||||
// by folder) which helps us work out a folder's parent
|
||||
currentDirStack = make(dirStack, 0)
|
||||
// currentCover because we find a cover anywhere among the tracks during the
|
||||
// walk and need a reference to it when we update folder and album records
|
||||
// when we exit a folder
|
||||
currentCover = &db.Cover{}
|
||||
// currentAlbum because we update this record when we exit a folder with
|
||||
// our new reference to it's cover
|
||||
currentAlbum = &db.Album{}
|
||||
const (
|
||||
programName = "gonic"
|
||||
programVar = "GONIC"
|
||||
)
|
||||
|
||||
func readTags(fullPath string) (tag.Metadata, error) {
|
||||
trackData, err := os.Open(fullPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("when tags from disk: %v", err)
|
||||
}
|
||||
defer trackData.Close()
|
||||
tags, err := tag.ReadFrom(trackData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func updateAlbum(fullPath string, album *db.Album) {
|
||||
if currentAlbum.ID != 0 {
|
||||
return
|
||||
}
|
||||
directory, _ := path.Split(fullPath)
|
||||
// update album table (the currentAlbum record will be updated when
|
||||
// we exit this folder)
|
||||
err := tx.Where("path = ?", directory).First(currentAlbum).Error
|
||||
if !gorm.IsRecordNotFoundError(err) {
|
||||
// we found the record
|
||||
// TODO: think about mod time here
|
||||
return
|
||||
}
|
||||
currentAlbum = &db.Album{
|
||||
Path: directory,
|
||||
Title: album.Title,
|
||||
AlbumArtistID: album.AlbumArtistID,
|
||||
Year: album.Year,
|
||||
}
|
||||
tx.Save(currentAlbum)
|
||||
}
|
||||
|
||||
func handleCover(fullPath string, stat os.FileInfo) error {
|
||||
modTime := stat.ModTime()
|
||||
err := tx.Where("path = ?", fullPath).First(currentCover).Error
|
||||
if !gorm.IsRecordNotFoundError(err) &&
|
||||
modTime.Before(currentCover.UpdatedAt) {
|
||||
// we found the record but it hasn't changed
|
||||
return nil
|
||||
}
|
||||
image, err := ioutil.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("when reading cover: %v", err)
|
||||
}
|
||||
currentCover = &db.Cover{
|
||||
Path: fullPath,
|
||||
Image: image,
|
||||
NewlyInserted: true,
|
||||
}
|
||||
tx.Save(currentCover)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleFolder(fullPath string, stat os.FileInfo) error {
|
||||
// update folder table for browsing by folder
|
||||
folder := &db.Folder{}
|
||||
defer currentDirStack.Push(folder)
|
||||
modTime := stat.ModTime()
|
||||
err := tx.Where("path = ?", fullPath).First(folder).Error
|
||||
if !gorm.IsRecordNotFoundError(err) &&
|
||||
modTime.Before(folder.UpdatedAt) {
|
||||
// we found the record but it hasn't changed
|
||||
return nil
|
||||
}
|
||||
_, folderName := path.Split(fullPath)
|
||||
folder.Path = fullPath
|
||||
folder.ParentID = currentDirStack.PeekID()
|
||||
folder.Name = folderName
|
||||
tx.Save(folder)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleFolderCompletion(fullPath string, info *godirwalk.Dirent) error {
|
||||
currentDir := currentDirStack.Peek()
|
||||
defer currentDirStack.Pop()
|
||||
var dirShouldSave bool
|
||||
if currentAlbum.ID != 0 {
|
||||
currentAlbum.CoverID = currentCover.ID
|
||||
tx.Save(currentAlbum)
|
||||
currentDir.HasTracks = true
|
||||
dirShouldSave = true
|
||||
}
|
||||
if currentCover.NewlyInserted {
|
||||
currentDir.CoverID = currentCover.ID
|
||||
dirShouldSave = true
|
||||
}
|
||||
if dirShouldSave {
|
||||
tx.Save(currentDir)
|
||||
}
|
||||
currentCover = &db.Cover{}
|
||||
currentAlbum = &db.Album{}
|
||||
log.Printf("processed folder `%s`\n", fullPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleTrack(fullPath string, stat os.FileInfo, mime, exten string) error {
|
||||
//
|
||||
// set track basics
|
||||
track := &db.Track{}
|
||||
modTime := stat.ModTime()
|
||||
err := tx.Where("path = ?", fullPath).First(track).Error
|
||||
if !gorm.IsRecordNotFoundError(err) &&
|
||||
modTime.Before(track.UpdatedAt) {
|
||||
// we found the record but it hasn't changed
|
||||
return nil
|
||||
}
|
||||
tags, err := readTags(fullPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("when reading tags: %v", err)
|
||||
}
|
||||
trackNumber, totalTracks := tags.Track()
|
||||
discNumber, totalDiscs := tags.Disc()
|
||||
track.Path = fullPath
|
||||
track.Title = tags.Title()
|
||||
track.Artist = tags.Artist()
|
||||
track.DiscNumber = discNumber
|
||||
track.TotalDiscs = totalDiscs
|
||||
track.TotalTracks = totalTracks
|
||||
track.TrackNumber = trackNumber
|
||||
track.Year = tags.Year()
|
||||
track.Suffix = exten
|
||||
track.ContentType = mime
|
||||
track.Size = int(stat.Size())
|
||||
track.FolderID = currentDirStack.PeekID()
|
||||
//
|
||||
// set album artist basics
|
||||
albumArtist := &db.AlbumArtist{}
|
||||
err = tx.Where("name = ?", tags.AlbumArtist()).
|
||||
First(albumArtist).
|
||||
Error
|
||||
if gorm.IsRecordNotFoundError(err) {
|
||||
albumArtist.Name = tags.AlbumArtist()
|
||||
tx.Save(albumArtist)
|
||||
}
|
||||
track.AlbumArtistID = albumArtist.ID
|
||||
//
|
||||
// set temporary album's basics - will be updated with
|
||||
// cover after the tracks inserted when we exit the folder
|
||||
updateAlbum(fullPath, &db.Album{
|
||||
AlbumArtistID: albumArtist.ID,
|
||||
Title: tags.Album(),
|
||||
Year: tags.Year(),
|
||||
})
|
||||
//
|
||||
// update the track with our new album and finally save
|
||||
track.AlbumID = currentAlbum.ID
|
||||
tx.Save(track)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleItem(fullPath string, info *godirwalk.Dirent) error {
|
||||
// seenPaths = append(seenPaths, fullPath)
|
||||
seenPaths[fullPath] = true
|
||||
stat, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error stating: %v", err)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return handleFolder(fullPath, stat)
|
||||
}
|
||||
if isCover(fullPath) {
|
||||
return handleCover(fullPath, stat)
|
||||
}
|
||||
if mime, exten, ok := isAudio(fullPath); ok {
|
||||
return handleTrack(fullPath, stat, mime, exten)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
log.Fatalf("usage: %s <path to music>", os.Args[0])
|
||||
}
|
||||
orm = db.New()
|
||||
orm.SetLogger(log.New(os.Stdout, "gorm ", 0))
|
||||
tx = orm.Begin()
|
||||
createDatabase()
|
||||
startTime := time.Now()
|
||||
err := godirwalk.Walk(os.Args[1], &godirwalk.Options{
|
||||
Callback: handleItem,
|
||||
PostChildrenCallback: handleFolderCompletion,
|
||||
Unsorted: true,
|
||||
})
|
||||
set := flag.NewFlagSet(programName, flag.ExitOnError)
|
||||
musicPath := set.String(
|
||||
"music-path", "",
|
||||
"path to music")
|
||||
dbPath := set.String(
|
||||
"db-path", "gonic.db",
|
||||
"path to database (optional)")
|
||||
err := ff.Parse(set, os.Args[1:])
|
||||
if err != nil {
|
||||
log.Fatalf("error when walking: %v\n", err)
|
||||
log.Fatalf("error parsing args: %v\n", err)
|
||||
}
|
||||
log.Printf("scanned in %s\n", time.Since(startTime))
|
||||
startTime = time.Now()
|
||||
cleanDatabase()
|
||||
log.Printf("cleaned in %s\n", time.Since(startTime))
|
||||
tx.Commit()
|
||||
if _, err := os.Stat(*musicPath); os.IsNotExist(err) {
|
||||
log.Fatal("please provide a valid music directory")
|
||||
}
|
||||
db, err := gorm.Open("sqlite3", *dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("error opening database: %v\n", err)
|
||||
}
|
||||
db.SetLogger(log.New(os.Stdout, "gorm ", 0))
|
||||
s := scanner.New(
|
||||
db,
|
||||
*musicPath,
|
||||
)
|
||||
s.MigrateDB()
|
||||
s.Start()
|
||||
}
|
||||
|
||||
@@ -1,162 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
"os"
|
||||
|
||||
"github.com/gobuffalo/packr/v2"
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
"github.com/wader/gormstore"
|
||||
"github.com/peterbourgon/ff"
|
||||
|
||||
"github.com/sentriz/gonic/db"
|
||||
"github.com/sentriz/gonic/handler"
|
||||
"github.com/sentriz/gonic/server"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 setSubsonicRoutes(cont handler.Controller, mux *http.ServeMux) {
|
||||
withWare := newChain(
|
||||
cont.WithLogging,
|
||||
cont.WithCORS,
|
||||
cont.WithValidSubsonicArgs,
|
||||
const (
|
||||
programName = "gonic"
|
||||
programVar = "GONIC"
|
||||
)
|
||||
// common
|
||||
mux.HandleFunc("/rest/download", withWare(cont.Stream))
|
||||
mux.HandleFunc("/rest/download.view", withWare(cont.Stream))
|
||||
mux.HandleFunc("/rest/stream", withWare(cont.Stream))
|
||||
mux.HandleFunc("/rest/stream.view", withWare(cont.Stream))
|
||||
mux.HandleFunc("/rest/getCoverArt", withWare(cont.GetCoverArt))
|
||||
mux.HandleFunc("/rest/getCoverArt.view", withWare(cont.GetCoverArt))
|
||||
mux.HandleFunc("/rest/getLicense", withWare(cont.GetLicence))
|
||||
mux.HandleFunc("/rest/getLicense.view", withWare(cont.GetLicence))
|
||||
mux.HandleFunc("/rest/ping", withWare(cont.Ping))
|
||||
mux.HandleFunc("/rest/ping.view", withWare(cont.Ping))
|
||||
mux.HandleFunc("/rest/scrobble", withWare(cont.Scrobble))
|
||||
mux.HandleFunc("/rest/scrobble.view", withWare(cont.Scrobble))
|
||||
mux.HandleFunc("/rest/getMusicFolders", withWare(cont.GetMusicFolders))
|
||||
mux.HandleFunc("/rest/getMusicFolders.view", withWare(cont.GetMusicFolders))
|
||||
// browse by tag
|
||||
mux.HandleFunc("/rest/getAlbum", withWare(cont.GetAlbum))
|
||||
mux.HandleFunc("/rest/getAlbum.view", withWare(cont.GetAlbum))
|
||||
mux.HandleFunc("/rest/getAlbumList2", withWare(cont.GetAlbumListTwo))
|
||||
mux.HandleFunc("/rest/getAlbumList2.view", withWare(cont.GetAlbumListTwo))
|
||||
mux.HandleFunc("/rest/getArtist", withWare(cont.GetArtist))
|
||||
mux.HandleFunc("/rest/getArtist.view", withWare(cont.GetArtist))
|
||||
mux.HandleFunc("/rest/getArtists", withWare(cont.GetArtists))
|
||||
mux.HandleFunc("/rest/getArtists.view", withWare(cont.GetArtists))
|
||||
// browse by folder
|
||||
mux.HandleFunc("/rest/getIndexes", withWare(cont.GetIndexes))
|
||||
mux.HandleFunc("/rest/getIndexes.view", withWare(cont.GetIndexes))
|
||||
mux.HandleFunc("/rest/getMusicDirectory", withWare(cont.GetMusicDirectory))
|
||||
mux.HandleFunc("/rest/getMusicDirectory.view", withWare(cont.GetMusicDirectory))
|
||||
mux.HandleFunc("/rest/getAlbumList", withWare(cont.GetAlbumList))
|
||||
mux.HandleFunc("/rest/getAlbumList.view", withWare(cont.GetAlbumList))
|
||||
}
|
||||
|
||||
func setAdminRoutes(cont handler.Controller, mux *http.ServeMux) {
|
||||
sessionKey := []byte(cont.GetSetting("session_key"))
|
||||
if len(sessionKey) == 0 {
|
||||
sessionKey = securecookie.GenerateRandomKey(32)
|
||||
cont.SetSetting("session_key", string(sessionKey))
|
||||
}
|
||||
// create gormstore (and cleanup) for backend sessions
|
||||
cont.SStore = gormstore.New(cont.DB, []byte(sessionKey))
|
||||
go cont.SStore.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")
|
||||
cont.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(
|
||||
cont.WithLogging,
|
||||
cont.WithSession,
|
||||
)
|
||||
withUserWare := newChain(
|
||||
withPublicWare,
|
||||
cont.WithUserSession,
|
||||
)
|
||||
withAdminWare := newChain(
|
||||
withUserWare,
|
||||
cont.WithAdminSession,
|
||||
)
|
||||
server := http.FileServer(packr.New("static", "../../static"))
|
||||
mux.Handle("/admin/static/", http.StripPrefix("/admin/static/", server))
|
||||
mux.HandleFunc("/admin/login", withPublicWare(cont.ServeLogin))
|
||||
mux.HandleFunc("/admin/login_do", withPublicWare(cont.ServeLoginDo))
|
||||
mux.HandleFunc("/admin/logout", withUserWare(cont.ServeLogout))
|
||||
mux.HandleFunc("/admin/home", withUserWare(cont.ServeHome))
|
||||
mux.HandleFunc("/admin/change_own_password", withUserWare(cont.ServeChangeOwnPassword))
|
||||
mux.HandleFunc("/admin/change_own_password_do", withUserWare(cont.ServeChangeOwnPasswordDo))
|
||||
mux.HandleFunc("/admin/link_lastfm_do", withUserWare(cont.ServeLinkLastFMDo))
|
||||
mux.HandleFunc("/admin/unlink_lastfm_do", withUserWare(cont.ServeUnlinkLastFMDo))
|
||||
mux.HandleFunc("/admin/change_password", withAdminWare(cont.ServeChangePassword))
|
||||
mux.HandleFunc("/admin/change_password_do", withAdminWare(cont.ServeChangePasswordDo))
|
||||
mux.HandleFunc("/admin/delete_user", withAdminWare(cont.ServeDeleteUser))
|
||||
mux.HandleFunc("/admin/delete_user_do", withAdminWare(cont.ServeDeleteUserDo))
|
||||
mux.HandleFunc("/admin/create_user", withAdminWare(cont.ServeCreateUser))
|
||||
mux.HandleFunc("/admin/create_user_do", withAdminWare(cont.ServeCreateUserDo))
|
||||
mux.HandleFunc("/admin/update_lastfm_api_key", withAdminWare(cont.ServeUpdateLastFMAPIKey))
|
||||
mux.HandleFunc("/admin/update_lastfm_api_key_do", withAdminWare(cont.ServeUpdateLastFMAPIKeyDo))
|
||||
}
|
||||
|
||||
func main() {
|
||||
address := "0.0.0.0:6969"
|
||||
mux := http.NewServeMux()
|
||||
// create a new controller and pass a copy to both routes.
|
||||
// they will add more fields to their copy if they need them
|
||||
baseController := handler.Controller{DB: db.New()}
|
||||
setSubsonicRoutes(baseController, mux)
|
||||
setAdminRoutes(baseController, mux)
|
||||
server := &http.Server{
|
||||
Addr: address,
|
||||
Handler: mux,
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
IdleTimeout: 15 * time.Second,
|
||||
}
|
||||
log.Printf("starting server at `%s`\n", address)
|
||||
err := server.ListenAndServe()
|
||||
set := flag.NewFlagSet(programName, flag.ExitOnError)
|
||||
listenAddr := set.String(
|
||||
"listen-addr", "0.0.0.0:6969",
|
||||
"listen address (optional)")
|
||||
musicPath := set.String(
|
||||
"music-path", "",
|
||||
"path to music")
|
||||
dbPath := set.String(
|
||||
"db-path", "gonic.db",
|
||||
"path to database (optional)")
|
||||
_ = set.String(
|
||||
"config-path", "",
|
||||
"path to config (optional)")
|
||||
err := ff.Parse(set, os.Args[1:],
|
||||
ff.WithConfigFileFlag("config-path"),
|
||||
ff.WithConfigFileParser(ff.PlainParser),
|
||||
ff.WithEnvVarPrefix(programVar),
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("when starting server: %v\n", err)
|
||||
log.Fatalf("error parsing args: %v\n", err)
|
||||
}
|
||||
if _, err := os.Stat(*musicPath); os.IsNotExist(err) {
|
||||
log.Fatal("please provide a valid music directory")
|
||||
}
|
||||
db, err := gorm.Open("sqlite3", *dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("error opening database: %v\n", err)
|
||||
}
|
||||
s := server.New(
|
||||
db,
|
||||
*musicPath,
|
||||
*listenAddr,
|
||||
)
|
||||
log.Printf("starting server at %s", *listenAddr)
|
||||
err = s.ListenAndServe()
|
||||
if err != nil {
|
||||
log.Fatalf("error starting server: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
22
db/db.go
22
db/db.go
@@ -1,22 +0,0 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"log"
|
||||
"runtime"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
// cFile is the path to this go file
|
||||
_, cFile, _, _ = runtime.Caller(0)
|
||||
)
|
||||
|
||||
// New creates a new GORM connection to the database
|
||||
func New() *gorm.DB {
|
||||
db, err := gorm.Open("sqlite3", "gonic.db")
|
||||
if err != nil {
|
||||
log.Printf("when opening database: %v\n", err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
2
go.mod
2
go.mod
@@ -18,6 +18,8 @@ require (
|
||||
github.com/karrick/godirwalk v1.8.0
|
||||
github.com/lib/pq v1.0.0 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.10.0 // indirect
|
||||
github.com/peterbourgon/ff v1.2.0
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be
|
||||
github.com/wader/gormstore v0.0.0-20190302154359-acb787ba3755
|
||||
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@@ -133,6 +133,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/openzipkin/zipkin-go v0.1.3/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/peterbourgon/ff v1.2.0 h1:wGn2NwdHk8MTlRQpnXnO91UKegxt5DvlwR/bTK/L2hc=
|
||||
github.com/peterbourgon/ff v1.2.0/go.mod h1:ljiF7yxtUvZaxUDyUqQa0+uiEOgwVboj+Q2S2+0nq40=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/wader/gormstore"
|
||||
|
||||
"github.com/sentriz/gonic/db"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
DB *gorm.DB // common
|
||||
SStore *gormstore.Store // admin
|
||||
Templates map[string]*template.Template // admin
|
||||
}
|
||||
|
||||
func (c *Controller) GetSetting(key string) string {
|
||||
var setting db.Setting
|
||||
c.DB.Where("key = ?", key).First(&setting)
|
||||
return setting.Value
|
||||
}
|
||||
|
||||
func (c *Controller) SetSetting(key, value string) {
|
||||
c.DB.
|
||||
Where(db.Setting{Key: key}).
|
||||
Assign(db.Setting{Value: value}).
|
||||
FirstOrCreate(&db.Setting{})
|
||||
}
|
||||
|
||||
func (c *Controller) GetUserFromName(name string) *db.User {
|
||||
var user db.User
|
||||
c.DB.Where("name = ?", name).First(&user)
|
||||
return &user
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package handler
|
||||
|
||||
type contextKey int
|
||||
|
||||
const (
|
||||
contextUserKey contextKey = iota
|
||||
contextSessionKey
|
||||
)
|
||||
@@ -11,7 +11,9 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/sentriz/gonic/db"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/sentriz/gonic/model"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -43,14 +45,14 @@ func makeRequest(method string, params url.Values) (*LastFM, error) {
|
||||
req.URL.RawQuery = params.Encode()
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get: %v", err)
|
||||
return nil, errors.Wrap(err, "get")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
decoder := xml.NewDecoder(resp.Body)
|
||||
var lastfm LastFM
|
||||
err = decoder.Decode(&lastfm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding: %v", err)
|
||||
return nil, errors.Wrap(err, "decoding")
|
||||
}
|
||||
if lastfm.Error != nil {
|
||||
return nil, fmt.Errorf("parsing: %v", lastfm.Error.Value)
|
||||
@@ -72,7 +74,7 @@ func GetSession(apiKey, secret, token string) (string, error) {
|
||||
}
|
||||
|
||||
func Scrobble(apiKey, secret, session string,
|
||||
track *db.Track, stamp int, submission bool) error {
|
||||
track *model.Track, stamp int, submission bool) error {
|
||||
params := url.Values{}
|
||||
if submission {
|
||||
params.Add("method", "track.Scrobble")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package db
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
@@ -1,4 +1,4 @@
|
||||
package db
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
37
scanner/dir_stack.go
Normal file
37
scanner/dir_stack.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"github.com/sentriz/gonic/model"
|
||||
)
|
||||
|
||||
type dirStack []*model.Folder
|
||||
|
||||
func (s *dirStack) Push(v *model.Folder) {
|
||||
*s = append(*s, v)
|
||||
}
|
||||
|
||||
func (s *dirStack) Pop() *model.Folder {
|
||||
l := len(*s)
|
||||
if l == 0 {
|
||||
return nil
|
||||
}
|
||||
r := (*s)[l-1]
|
||||
*s = (*s)[:l-1]
|
||||
return r
|
||||
}
|
||||
|
||||
func (s *dirStack) Peek() *model.Folder {
|
||||
l := len(*s)
|
||||
if l == 0 {
|
||||
return nil
|
||||
}
|
||||
return (*s)[l-1]
|
||||
}
|
||||
|
||||
func (s *dirStack) PeekID() int {
|
||||
l := len(*s)
|
||||
if l == 0 {
|
||||
return 0
|
||||
}
|
||||
return (*s)[l-1].ID
|
||||
}
|
||||
303
scanner/scanner.go
Normal file
303
scanner/scanner.go
Normal file
@@ -0,0 +1,303 @@
|
||||
package scanner
|
||||
|
||||
// this scanner tries to scan with a single unsorted walk of the music
|
||||
// directory - which means you can come across the cover of an album/folder
|
||||
// before the tracks (and therefore the album) which is an issue because
|
||||
// when inserting into the album table, we need a reference to the cover.
|
||||
// to solve this we're using godirwalk's PostChildrenCallback and some globals
|
||||
//
|
||||
// Album -> needs a CoverID
|
||||
// -> needs a FolderID (American Football)
|
||||
// Folder -> needs a CoverID
|
||||
// -> needs a ParentID
|
||||
// Track -> needs an AlbumID
|
||||
// -> needs a FolderID
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/karrick/godirwalk"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/sentriz/gonic/model"
|
||||
)
|
||||
|
||||
var (
|
||||
IsScanning int32
|
||||
)
|
||||
|
||||
type Scanner struct {
|
||||
db *gorm.DB
|
||||
tx *gorm.DB
|
||||
musicPath string
|
||||
// seenPaths is used to keep every path we've seen so that
|
||||
// we can remove old tracks, folders, and covers by path when we
|
||||
// are in the cleanDatabase stage
|
||||
seenPaths map[string]bool
|
||||
// currentDirStack is used for inserting to the folders (subsonic browse
|
||||
// by folder) which helps us work out a folder's parent
|
||||
currentDirStack dirStack
|
||||
// currentCover because we find a cover anywhere among the tracks during the
|
||||
// walk and need a reference to it when we update folder and album records
|
||||
// when we exit a folder
|
||||
currentCover *model.Cover
|
||||
// currentAlbum because we update this record when we exit a folder with
|
||||
// our new reference to it's cover
|
||||
currentAlbum *model.Album
|
||||
}
|
||||
|
||||
func New(db *gorm.DB, musicPath string) *Scanner {
|
||||
return &Scanner{
|
||||
db: db,
|
||||
musicPath: musicPath,
|
||||
seenPaths: make(map[string]bool),
|
||||
currentDirStack: make(dirStack, 0),
|
||||
currentCover: &model.Cover{},
|
||||
currentAlbum: &model.Album{},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scanner) updateAlbum(fullPath string, album *model.Album) {
|
||||
if s.currentAlbum.ID != 0 {
|
||||
return
|
||||
}
|
||||
directory, _ := path.Split(fullPath)
|
||||
// update album table (the currentAlbum record will be updated when
|
||||
// we exit this folder)
|
||||
err := s.tx.Where("path = ?", directory).First(s.currentAlbum).Error
|
||||
if !gorm.IsRecordNotFoundError(err) {
|
||||
// we found the record
|
||||
// TODO: think about mod time here
|
||||
return
|
||||
}
|
||||
s.currentAlbum = &model.Album{
|
||||
Path: directory,
|
||||
Title: album.Title,
|
||||
AlbumArtistID: album.AlbumArtistID,
|
||||
Year: album.Year,
|
||||
}
|
||||
s.tx.Save(s.currentAlbum)
|
||||
}
|
||||
|
||||
func (s *Scanner) handleCover(fullPath string, stat os.FileInfo) error {
|
||||
modTime := stat.ModTime()
|
||||
err := s.tx.Where("path = ?", fullPath).First(s.currentCover).Error
|
||||
if !gorm.IsRecordNotFoundError(err) &&
|
||||
modTime.Before(s.currentCover.UpdatedAt) {
|
||||
// we found the record but it hasn't changed
|
||||
return nil
|
||||
}
|
||||
image, err := ioutil.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("when reading cover: %v", err)
|
||||
}
|
||||
s.currentCover = &model.Cover{
|
||||
Path: fullPath,
|
||||
Image: image,
|
||||
NewlyInserted: true,
|
||||
}
|
||||
s.tx.Save(s.currentCover)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Scanner) handleFolder(fullPath string, stat os.FileInfo) error {
|
||||
// update folder table for browsing by folder
|
||||
folder := &model.Folder{}
|
||||
defer s.currentDirStack.Push(folder)
|
||||
modTime := stat.ModTime()
|
||||
err := s.tx.Where("path = ?", fullPath).First(folder).Error
|
||||
if !gorm.IsRecordNotFoundError(err) &&
|
||||
modTime.Before(folder.UpdatedAt) {
|
||||
// we found the record but it hasn't changed
|
||||
return nil
|
||||
}
|
||||
_, folderName := path.Split(fullPath)
|
||||
folder.Path = fullPath
|
||||
folder.ParentID = s.currentDirStack.PeekID()
|
||||
folder.Name = folderName
|
||||
s.tx.Save(folder)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Scanner) handleFolderCompletion(fullPath string, info *godirwalk.Dirent) error {
|
||||
currentDir := s.currentDirStack.Peek()
|
||||
defer s.currentDirStack.Pop()
|
||||
var dirShouldSave bool
|
||||
if s.currentAlbum.ID != 0 {
|
||||
s.currentAlbum.CoverID = s.currentCover.ID
|
||||
s.tx.Save(s.currentAlbum)
|
||||
currentDir.HasTracks = true
|
||||
dirShouldSave = true
|
||||
}
|
||||
if s.currentCover.NewlyInserted {
|
||||
currentDir.CoverID = s.currentCover.ID
|
||||
dirShouldSave = true
|
||||
}
|
||||
if dirShouldSave {
|
||||
s.tx.Save(currentDir)
|
||||
}
|
||||
s.currentCover = &model.Cover{}
|
||||
s.currentAlbum = &model.Album{}
|
||||
log.Printf("processed folder `%s`\n", fullPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Scanner) handleTrack(fullPath string, stat os.FileInfo, mime, exten string) error {
|
||||
//
|
||||
// set track basics
|
||||
track := &model.Track{}
|
||||
modTime := stat.ModTime()
|
||||
err := s.tx.Where("path = ?", fullPath).First(track).Error
|
||||
if !gorm.IsRecordNotFoundError(err) &&
|
||||
modTime.Before(track.UpdatedAt) {
|
||||
// we found the record but it hasn't changed
|
||||
return nil
|
||||
}
|
||||
tags, err := readTags(fullPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("when reading tags: %v", err)
|
||||
}
|
||||
trackNumber, totalTracks := tags.Track()
|
||||
discNumber, totalDiscs := tags.Disc()
|
||||
track.Path = fullPath
|
||||
track.Title = tags.Title()
|
||||
track.Artist = tags.Artist()
|
||||
track.DiscNumber = discNumber
|
||||
track.TotalDiscs = totalDiscs
|
||||
track.TotalTracks = totalTracks
|
||||
track.TrackNumber = trackNumber
|
||||
track.Year = tags.Year()
|
||||
track.Suffix = exten
|
||||
track.ContentType = mime
|
||||
track.Size = int(stat.Size())
|
||||
track.FolderID = s.currentDirStack.PeekID()
|
||||
//
|
||||
// set album artist basics
|
||||
albumArtist := &model.AlbumArtist{}
|
||||
err = s.tx.Where("name = ?", tags.AlbumArtist()).
|
||||
First(albumArtist).
|
||||
Error
|
||||
if gorm.IsRecordNotFoundError(err) {
|
||||
albumArtist.Name = tags.AlbumArtist()
|
||||
s.tx.Save(albumArtist)
|
||||
}
|
||||
track.AlbumArtistID = albumArtist.ID
|
||||
//
|
||||
// set temporary album's basics - will be updated with
|
||||
// cover after the tracks inserted when we exit the folder
|
||||
s.updateAlbum(fullPath, &model.Album{
|
||||
AlbumArtistID: albumArtist.ID,
|
||||
Title: tags.Album(),
|
||||
Year: tags.Year(),
|
||||
})
|
||||
//
|
||||
// update the track with our new album and finally save
|
||||
track.AlbumID = s.currentAlbum.ID
|
||||
s.tx.Save(track)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Scanner) handleItem(fullPath string, info *godirwalk.Dirent) error {
|
||||
s.seenPaths[fullPath] = true
|
||||
stat, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error stating: %v", err)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return s.handleFolder(fullPath, stat)
|
||||
}
|
||||
if isCover(fullPath) {
|
||||
return s.handleCover(fullPath, stat)
|
||||
}
|
||||
if mime, exten, ok := isAudio(fullPath); ok {
|
||||
return s.handleTrack(fullPath, stat, mime, exten)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Scanner) MigrateDB() error {
|
||||
defer logElapsed(time.Now(), "migrating database")
|
||||
s.tx = s.db.Begin()
|
||||
defer s.tx.Commit()
|
||||
s.tx.AutoMigrate(
|
||||
&model.Album{},
|
||||
&model.AlbumArtist{},
|
||||
&model.Track{},
|
||||
&model.Cover{},
|
||||
&model.User{},
|
||||
&model.Setting{},
|
||||
&model.Play{},
|
||||
&model.Folder{},
|
||||
)
|
||||
// set starting value for `albums` table's
|
||||
// auto increment
|
||||
s.tx.Exec(`
|
||||
INSERT INTO sqlite_sequence(name, seq)
|
||||
SELECT 'albums', 500000
|
||||
WHERE NOT EXISTS (SELECT *
|
||||
FROM sqlite_sequence);
|
||||
`)
|
||||
// create the first user if there is none
|
||||
s.tx.FirstOrCreate(&model.User{}, model.User{
|
||||
Name: "admin",
|
||||
Password: "admin",
|
||||
IsAdmin: true,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Scanner) Start() error {
|
||||
if atomic.LoadInt32(&IsScanning) == 1 {
|
||||
return errors.New("already scanning")
|
||||
}
|
||||
atomic.StoreInt32(&IsScanning, 1)
|
||||
defer atomic.StoreInt32(&IsScanning, 0)
|
||||
defer logElapsed(time.Now(), "scanning")
|
||||
s.tx = s.db.Begin()
|
||||
defer s.tx.Commit()
|
||||
//
|
||||
// start scan logic
|
||||
err := godirwalk.Walk(s.musicPath, &godirwalk.Options{
|
||||
Callback: s.handleItem,
|
||||
PostChildrenCallback: s.handleFolderCompletion,
|
||||
Unsorted: true,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "walking filesystem")
|
||||
}
|
||||
//
|
||||
// start cleaning logic
|
||||
log.Println("cleaning database")
|
||||
var tracks []*model.Track
|
||||
s.tx.Select("id, path").Find(&tracks)
|
||||
for _, track := range tracks {
|
||||
_, ok := s.seenPaths[track.Path]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
s.tx.Delete(&track)
|
||||
log.Println("removed", track.Path)
|
||||
}
|
||||
// delete albums without tracks
|
||||
s.tx.Exec(`
|
||||
DELETE FROM albums
|
||||
WHERE (SELECT count(id)
|
||||
FROM tracks
|
||||
WHERE album_id = albums.id) = 0;
|
||||
`)
|
||||
// delete artists without tracks
|
||||
s.tx.Exec(`
|
||||
DELETE FROM album_artists
|
||||
WHERE (SELECT count(id)
|
||||
FROM albums
|
||||
WHERE album_artist_id = album_artists.id) = 0;
|
||||
`)
|
||||
return nil
|
||||
}
|
||||
69
scanner/utilities.go
Normal file
69
scanner/utilities.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dhowden/tag"
|
||||
)
|
||||
|
||||
var audioExtensions = map[string]string{
|
||||
"mp3": "audio/mpeg",
|
||||
"flac": "audio/x-flac",
|
||||
"aac": "audio/x-aac",
|
||||
"m4a": "audio/m4a",
|
||||
"ogg": "audio/ogg",
|
||||
}
|
||||
|
||||
func isAudio(fullPath string) (string, string, bool) {
|
||||
exten := filepath.Ext(fullPath)[1:]
|
||||
mine, ok := audioExtensions[exten]
|
||||
if !ok {
|
||||
return "", "", false
|
||||
}
|
||||
return mine, exten, true
|
||||
}
|
||||
|
||||
var coverFilenames = map[string]bool{
|
||||
"cover.png": true,
|
||||
"cover.jpg": true,
|
||||
"cover.jpeg": true,
|
||||
"folder.png": true,
|
||||
"folder.jpg": true,
|
||||
"folder.jpeg": true,
|
||||
"album.png": true,
|
||||
"album.jpg": true,
|
||||
"album.jpeg": true,
|
||||
"front.png": true,
|
||||
"front.jpg": true,
|
||||
"front.jpeg": true,
|
||||
}
|
||||
|
||||
func isCover(fullPath string) bool {
|
||||
_, filename := path.Split(fullPath)
|
||||
_, ok := coverFilenames[strings.ToLower(filename)]
|
||||
return ok
|
||||
}
|
||||
|
||||
func readTags(fullPath string) (tag.Metadata, error) {
|
||||
trackData, err := os.Open(fullPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("when tags from disk: %v", err)
|
||||
}
|
||||
defer trackData.Close()
|
||||
tags, err := tag.ReadFrom(trackData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func logElapsed(start time.Time, name string) {
|
||||
elapsed := time.Since(start)
|
||||
log.Printf("finished %s in %s\n", name, elapsed)
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/sentriz/gonic/db"
|
||||
"github.com/sentriz/gonic/model"
|
||||
"github.com/sentriz/gonic/lastfm"
|
||||
)
|
||||
|
||||
@@ -84,7 +84,7 @@ func (c *Controller) ServeChangeOwnPasswordDo(w http.ResponseWriter, r *http.Req
|
||||
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
user := r.Context().Value(contextUserKey).(*db.User)
|
||||
user := r.Context().Value(contextUserKey).(*model.User)
|
||||
user.Password = passwordOne
|
||||
c.DB.Save(user)
|
||||
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
||||
@@ -108,14 +108,14 @@ func (c *Controller) ServeLinkLastFMDo(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
user := r.Context().Value(contextUserKey).(*db.User)
|
||||
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).(*db.User)
|
||||
user := r.Context().Value(contextUserKey).(*model.User)
|
||||
user.LastFMSession = ""
|
||||
c.DB.Save(&user)
|
||||
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
||||
@@ -127,7 +127,7 @@ func (c *Controller) ServeChangePassword(w http.ResponseWriter, r *http.Request)
|
||||
http.Error(w, "please provide a username", 400)
|
||||
return
|
||||
}
|
||||
var user db.User
|
||||
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)
|
||||
@@ -141,7 +141,7 @@ func (c *Controller) ServeChangePassword(w http.ResponseWriter, r *http.Request)
|
||||
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 db.User
|
||||
var user model.User
|
||||
c.DB.Where("name = ?", username).First(&user)
|
||||
passwordOne := r.FormValue("password_one")
|
||||
passwordTwo := r.FormValue("password_two")
|
||||
@@ -163,7 +163,7 @@ func (c *Controller) ServeDeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "please provide a username", 400)
|
||||
return
|
||||
}
|
||||
var user db.User
|
||||
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)
|
||||
@@ -176,7 +176,7 @@ func (c *Controller) ServeDeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (c *Controller) ServeDeleteUserDo(w http.ResponseWriter, r *http.Request) {
|
||||
username := r.URL.Query().Get("user")
|
||||
var user db.User
|
||||
var user model.User
|
||||
c.DB.Where("name = ?", username).First(&user)
|
||||
c.DB.Delete(&user)
|
||||
http.Redirect(w, r, "/admin/home", http.StatusSeeOther)
|
||||
@@ -205,7 +205,7 @@ func (c *Controller) ServeCreateUserDo(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
user := db.User{
|
||||
user := model.User{
|
||||
Name: username,
|
||||
Password: passwordOne,
|
||||
}
|
||||
@@ -6,14 +6,14 @@ import (
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/sentriz/gonic/db"
|
||||
"github.com/sentriz/gonic/subsonic"
|
||||
"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 []*db.Folder
|
||||
var folders []*model.Folder
|
||||
c.DB.Where("parent_id = ?", 1).Find(&folders)
|
||||
var indexMap = make(map[rune]*subsonic.Index)
|
||||
var indexes []*subsonic.Index
|
||||
@@ -48,11 +48,11 @@ func (c *Controller) GetMusicDirectory(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
childrenObj := []*subsonic.Child{}
|
||||
var cFolder db.Folder
|
||||
var cFolder model.Folder
|
||||
c.DB.First(&cFolder, id)
|
||||
//
|
||||
// start looking for child folders in the current dir
|
||||
var folders []*db.Folder
|
||||
var folders []*model.Folder
|
||||
c.DB.
|
||||
Where("parent_id = ?", id).
|
||||
Find(&folders)
|
||||
@@ -67,7 +67,7 @@ func (c *Controller) GetMusicDirectory(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
//
|
||||
// start looking for child tracks in the current dir
|
||||
var tracks []*db.Track
|
||||
var tracks []*model.Track
|
||||
c.DB.
|
||||
Where("folder_id = ?", id).
|
||||
Preload("Album").
|
||||
@@ -129,7 +129,7 @@ func (c *Controller) GetAlbumList(w http.ResponseWriter, r *http.Request) {
|
||||
// not sure about "name" either, so lets use the folder's name
|
||||
q = q.Order("name")
|
||||
case "frequent":
|
||||
user := r.Context().Value(contextUserKey).(*db.User)
|
||||
user := r.Context().Value(contextUserKey).(*model.User)
|
||||
q = q.Joins(`
|
||||
JOIN plays
|
||||
ON folders.id = plays.folder_id AND plays.user_id = ?`,
|
||||
@@ -140,7 +140,7 @@ func (c *Controller) GetAlbumList(w http.ResponseWriter, r *http.Request) {
|
||||
case "random":
|
||||
q = q.Order(gorm.Expr("random()"))
|
||||
case "recent":
|
||||
user := r.Context().Value(contextUserKey).(*db.User)
|
||||
user := r.Context().Value(contextUserKey).(*model.User)
|
||||
q = q.Joins(`
|
||||
JOIN plays
|
||||
ON folders.id = plays.folder_id AND plays.user_id = ?`,
|
||||
@@ -152,7 +152,7 @@ func (c *Controller) GetAlbumList(w http.ResponseWriter, r *http.Request) {
|
||||
))
|
||||
return
|
||||
}
|
||||
var folders []*db.Folder
|
||||
var folders []*model.Folder
|
||||
q.
|
||||
Where("folders.has_tracks = 1").
|
||||
Offset(getIntParamOr(r, "offset", 0)).
|
||||
@@ -6,12 +6,12 @@ import (
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/sentriz/gonic/db"
|
||||
"github.com/sentriz/gonic/subsonic"
|
||||
"github.com/sentriz/gonic/model"
|
||||
"github.com/sentriz/gonic/server/subsonic"
|
||||
)
|
||||
|
||||
func (c *Controller) GetArtists(w http.ResponseWriter, r *http.Request) {
|
||||
var artists []*db.AlbumArtist
|
||||
var artists []*model.AlbumArtist
|
||||
c.DB.Find(&artists)
|
||||
var indexMap = make(map[rune]*subsonic.Index)
|
||||
var indexes subsonic.Artists
|
||||
@@ -42,7 +42,7 @@ func (c *Controller) GetArtist(w http.ResponseWriter, r *http.Request) {
|
||||
respondError(w, r, 10, "please provide an `id` parameter")
|
||||
return
|
||||
}
|
||||
var artist db.AlbumArtist
|
||||
var artist model.AlbumArtist
|
||||
c.DB.
|
||||
Preload("Albums").
|
||||
First(&artist, id)
|
||||
@@ -72,7 +72,7 @@ func (c *Controller) GetAlbum(w http.ResponseWriter, r *http.Request) {
|
||||
respondError(w, r, 10, "please provide an `id` parameter")
|
||||
return
|
||||
}
|
||||
var album db.Album
|
||||
var album model.Album
|
||||
c.DB.
|
||||
Preload("AlbumArtist").
|
||||
Preload("Tracks").
|
||||
@@ -132,7 +132,7 @@ func (c *Controller) GetAlbumListTwo(w http.ResponseWriter, r *http.Request) {
|
||||
getIntParamOr(r, "toYear", 2200))
|
||||
q = q.Order("year")
|
||||
case "frequent":
|
||||
user := r.Context().Value(contextUserKey).(*db.User)
|
||||
user := r.Context().Value(contextUserKey).(*model.User)
|
||||
q = q.Joins(`
|
||||
JOIN plays
|
||||
ON albums.id = plays.album_id AND plays.user_id = ?`,
|
||||
@@ -143,7 +143,7 @@ func (c *Controller) GetAlbumListTwo(w http.ResponseWriter, r *http.Request) {
|
||||
case "random":
|
||||
q = q.Order(gorm.Expr("random()"))
|
||||
case "recent":
|
||||
user := r.Context().Value(contextUserKey).(*db.User)
|
||||
user := r.Context().Value(contextUserKey).(*model.User)
|
||||
q = q.Joins(`
|
||||
JOIN plays
|
||||
ON albums.id = plays.album_id AND plays.user_id = ?`,
|
||||
@@ -155,7 +155,7 @@ func (c *Controller) GetAlbumListTwo(w http.ResponseWriter, r *http.Request) {
|
||||
))
|
||||
return
|
||||
}
|
||||
var albums []*db.Album
|
||||
var albums []*model.Album
|
||||
q.
|
||||
Offset(getIntParamOr(r, "offset", 0)).
|
||||
Limit(getIntParamOr(r, "size", 10)).
|
||||
@@ -4,14 +4,16 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/rainycape/unidecode"
|
||||
|
||||
"github.com/sentriz/gonic/db"
|
||||
"github.com/sentriz/gonic/lastfm"
|
||||
"github.com/sentriz/gonic/subsonic"
|
||||
"github.com/sentriz/gonic/model"
|
||||
"github.com/sentriz/gonic/scanner"
|
||||
"github.com/sentriz/gonic/server/subsonic"
|
||||
)
|
||||
|
||||
func indexOf(s string) rune {
|
||||
@@ -29,7 +31,7 @@ func (c *Controller) Stream(w http.ResponseWriter, r *http.Request) {
|
||||
respondError(w, r, 10, "please provide an `id` parameter")
|
||||
return
|
||||
}
|
||||
var track db.Track
|
||||
var track model.Track
|
||||
c.DB.
|
||||
Preload("Album").
|
||||
Preload("Folder").
|
||||
@@ -47,8 +49,8 @@ func (c *Controller) Stream(w http.ResponseWriter, r *http.Request) {
|
||||
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).(*db.User)
|
||||
play := db.Play{
|
||||
user := r.Context().Value(contextUserKey).(*model.User)
|
||||
play := model.Play{
|
||||
AlbumID: track.Album.ID,
|
||||
FolderID: track.Folder.ID,
|
||||
UserID: user.ID,
|
||||
@@ -65,7 +67,7 @@ func (c *Controller) GetCoverArt(w http.ResponseWriter, r *http.Request) {
|
||||
respondError(w, r, 10, "please provide an `id` parameter")
|
||||
return
|
||||
}
|
||||
var cover db.Cover
|
||||
var cover model.Cover
|
||||
c.DB.First(&cover, id)
|
||||
w.Write(cover.Image)
|
||||
}
|
||||
@@ -90,13 +92,13 @@ func (c *Controller) Scrobble(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
// fetch user to get lastfm session
|
||||
user := r.Context().Value(contextUserKey).(*db.User)
|
||||
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 db.Track
|
||||
var track model.Track
|
||||
c.DB.
|
||||
Preload("Album").
|
||||
Preload("AlbumArtist").
|
||||
@@ -133,6 +135,23 @@ func (c *Controller) GetMusicFolders(w http.ResponseWriter, r *http.Request) {
|
||||
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")
|
||||
}
|
||||
@@ -6,12 +6,12 @@ import (
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
|
||||
"github.com/sentriz/gonic/db"
|
||||
"github.com/sentriz/gonic/model"
|
||||
)
|
||||
|
||||
func (c *Controller) WithSession(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
session, _ := c.SStore.Get(r, "gonic")
|
||||
session, _ := c.SessDB.Get(r, "gonic")
|
||||
withSession := context.WithValue(r.Context(), contextSessionKey, session)
|
||||
next.ServeHTTP(w, r.WithContext(withSession))
|
||||
}
|
||||
@@ -47,7 +47,7 @@ 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).(*db.User)
|
||||
user := r.Context().Value(contextUserKey).(*model.User)
|
||||
if !user.IsAdmin {
|
||||
session.AddFlash("you are not an admin")
|
||||
session.Save(r, w)
|
||||
@@ -7,14 +7,14 @@ import (
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
|
||||
"github.com/sentriz/gonic/db"
|
||||
"github.com/sentriz/gonic/model"
|
||||
)
|
||||
|
||||
type templateData struct {
|
||||
Flashes []interface{}
|
||||
User *db.User
|
||||
SelectedUser *db.User
|
||||
AllUsers []*db.User
|
||||
User *model.User
|
||||
SelectedUser *model.User
|
||||
AllUsers []*model.User
|
||||
ArtistCount int
|
||||
AlbumCount int
|
||||
TrackCount int
|
||||
@@ -31,7 +31,7 @@ func renderTemplate(w http.ResponseWriter, r *http.Request,
|
||||
}
|
||||
data.Flashes = session.Flashes()
|
||||
session.Save(r, w)
|
||||
user, ok := r.Context().Value(contextUserKey).(*db.User)
|
||||
user, ok := r.Context().Value(contextUserKey).(*model.User)
|
||||
if ok {
|
||||
data.User = user
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/sentriz/gonic/subsonic"
|
||||
"github.com/sentriz/gonic/server/subsonic"
|
||||
)
|
||||
|
||||
func respondRaw(w http.ResponseWriter, r *http.Request,
|
||||
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))
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 236 KiB After Width: | Height: | Size: 236 KiB |
@@ -116,3 +116,8 @@ type MusicFolder struct {
|
||||
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"`
|
||||
}
|
||||
@@ -29,6 +29,7 @@ type Response struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user