318 lines
6.6 KiB
Go
318 lines
6.6 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"encoding/json"
|
|
"crypto/sha512"
|
|
"fmt"
|
|
"net/http"
|
|
"io/ioutil"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
"encoding/hex"
|
|
|
|
"github.com/labstack/echo"
|
|
"github.com/labstack/echo/middleware"
|
|
"github.com/gorilla/feeds"
|
|
)
|
|
|
|
type (
|
|
JSTime time.Time
|
|
Entry struct {
|
|
Artist string `json:"artist" form:"artist"`
|
|
Linkto string `json:"linkto,omitempty" form:"linkto,omitempty"`
|
|
Track string `json:"track,omitempty" form:"track,omitempty"`
|
|
Album string `json:"album,omitempty" form:"album,omitempty"`
|
|
Cover string `json:"cover,omitempty" form:"cover,omitempty"`
|
|
Date JSTime `json:"date" form:"date"`
|
|
}
|
|
Response struct {
|
|
Ok bool `json:"ok"`
|
|
Message string `json:"message"`
|
|
}
|
|
)
|
|
|
|
type (
|
|
entryList struct {
|
|
filename string
|
|
modtime time.Time
|
|
v []Entry
|
|
mu sync.RWMutex
|
|
}
|
|
)
|
|
|
|
const (
|
|
jsTimeLayout = "2006-01-02"
|
|
adminUsername = "danolo"
|
|
adminPassword = "bd4cad796950f50352225de3c773d8f3c39622bc17f34ad661eabe615cdf6d32751c5751e0648dc17d890f40330018334a2ae899878f200f6dc80121ddb70cc9"
|
|
)
|
|
|
|
var (
|
|
list *entryList = &entryList{
|
|
filename: "/var/lib/muse.json",
|
|
modtime: time.Unix(0, 0),
|
|
}
|
|
)
|
|
|
|
func (ct *JSTime) UnmarshalJSON(b []byte) (err error) {
|
|
s := strings.Trim(string(b), `"`)
|
|
nt, err := time.Parse(jsTimeLayout, s)
|
|
*ct = JSTime(nt)
|
|
return
|
|
}
|
|
|
|
func (ct JSTime) MarshalJSON() ([]byte, error) {
|
|
return []byte(ct.String()), nil
|
|
}
|
|
|
|
func (ct *JSTime) String() string {
|
|
t := time.Time(*ct)
|
|
return fmt.Sprintf("%q", t.Format(jsTimeLayout))
|
|
}
|
|
|
|
func (list *entryList) Len() int { return len(list.v) }
|
|
func (list *entryList) Swap(i, j int) { list.v[i], list.v[j] = list.v[j], list.v[i] }
|
|
func (list *entryList) Less(i, j int) bool {
|
|
return time.Time(list.v[i].Date).After(time.Time(list.v[j].Date))
|
|
}
|
|
|
|
func (list *entryList) modified() (mod bool) {
|
|
info, err := os.Stat(list.filename)
|
|
if err == nil {
|
|
mod = list.modtime.Before(info.ModTime())
|
|
list.modtime = info.ModTime()
|
|
} else {
|
|
mod = true
|
|
}
|
|
return
|
|
}
|
|
|
|
func (list *entryList) write() (err error) {
|
|
data, err := json.MarshalIndent(list.v, "", " ")
|
|
if err == nil {
|
|
err = ioutil.WriteFile(list.filename, data, 0644)
|
|
}
|
|
if err == nil {
|
|
list.modtime = time.Now()
|
|
}
|
|
return
|
|
}
|
|
|
|
func (list *entryList) read() (err error) {
|
|
if list.modified() {
|
|
list.v = make([]Entry, 0, len(list.v))
|
|
if data, err := ioutil.ReadFile(list.filename); err == nil {
|
|
err = json.Unmarshal(data, &list.v)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (list *entryList) addEntry(entry *Entry) (err error) {
|
|
list.mu.Lock()
|
|
defer list.mu.Unlock()
|
|
|
|
if err = list.read(); err == nil {
|
|
list.v = append(list.v, *entry)
|
|
sort.Sort(list)
|
|
err = list.write()
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (list *entryList) readSliceList(oval int, nval int) (entries []Entry, err error) {
|
|
list.mu.RLock()
|
|
defer list.mu.RUnlock()
|
|
|
|
if nval <= 0 {
|
|
nval = list.Len()
|
|
}
|
|
|
|
if oval <= 0 {
|
|
oval = 0
|
|
}
|
|
|
|
err = list.read()
|
|
if err == nil {
|
|
if oval >= list.Len() {
|
|
entries = nil
|
|
} else if oval+nval >= list.Len() {
|
|
entries = list.v[oval:]
|
|
} else {
|
|
entries = list.v[oval : oval+nval]
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (list *entryList) deleteElement(pos int) (err error) {
|
|
list.mu.Lock()
|
|
defer list.mu.Unlock()
|
|
|
|
if err = list.read(); err == nil {
|
|
if pos >= 0 && pos < list.Len() {
|
|
if pos == 0 {
|
|
list.v = list.v[pos+1:]
|
|
} else if pos == list.Len() - 1 {
|
|
list.v = list.v[:pos]
|
|
} else {
|
|
list.v = append(list.v[:pos], list.v[pos+1:]...)
|
|
}
|
|
|
|
err = list.write()
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// TODO rutas relativas
|
|
func (list *entryList) getFeed() (feed *feeds.Feed) {
|
|
list.mu.RLock()
|
|
defer list.mu.RUnlock()
|
|
|
|
err := list.read()
|
|
|
|
if err == nil {
|
|
feed = &feeds.Feed {
|
|
Title: "danoloan.es muse",
|
|
Link: &feeds.Link { Href: "https://danoloan.es/muse" },
|
|
Description: "bitácora musical de danoloan",
|
|
Author: &feeds.Author { Name: "danoloan", Email: "danolo@danoloan.es" },
|
|
}
|
|
|
|
feed.Items = make([]*feeds.Item, 0, list.Len())
|
|
for _, elem := range list.v {
|
|
item := &feeds.Item {
|
|
Created: time.Time(elem.Date),
|
|
}
|
|
|
|
if elem.Track != "" {
|
|
item.Title = elem.Track
|
|
} else if elem.Album != "" {
|
|
item.Title = elem.Album
|
|
}
|
|
|
|
item.Description = fmt.Sprintf("<p>%s - %s</p>\n", item.Title, elem.Artist)
|
|
if elem.Cover != ""{
|
|
item.Description = fmt.Sprintf("%s<img src=\"%s\"/>\n", item.Description, elem.Cover)
|
|
}
|
|
|
|
if elem.Linkto != "" {
|
|
item.Link = &feeds.Link{ Href: elem.Linkto }
|
|
} else {
|
|
item.Link = &feeds.Link{ Href: "https://danoloan.es/muse" }
|
|
}
|
|
|
|
feed.Items = append(feed.Items, item)
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func ListController(c echo.Context) (res error) {
|
|
var err error
|
|
var oval, nval int
|
|
|
|
oval, err = strconv.Atoi(c.QueryParam("o"))
|
|
if err != nil {
|
|
oval = 0
|
|
}
|
|
|
|
nval, err = strconv.Atoi(c.QueryParam("n"))
|
|
if err != nil {
|
|
nval = 0
|
|
}
|
|
|
|
slice, err := list.readSliceList(oval, nval)
|
|
if err == nil {
|
|
res = c.JSON(http.StatusOK, slice)
|
|
} else {
|
|
response := Response{ false, "Error fetching song list" }
|
|
res = c.JSON(http.StatusInternalServerError, response)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func AddController(c echo.Context) (error) {
|
|
var res Response
|
|
var status int
|
|
var err error
|
|
|
|
entry := new(Entry)
|
|
if err = c.Bind(entry); err != nil {
|
|
res = Response{ false, err.Error() }
|
|
status = http.StatusBadRequest
|
|
} else if err = list.addEntry(entry); err != nil {
|
|
res = Response{ false, "Failed to write into list" }
|
|
status = http.StatusInternalServerError
|
|
} else {
|
|
res = Response{ true, "OK" }
|
|
status = http.StatusOK
|
|
}
|
|
|
|
return c.JSON(status, res)
|
|
}
|
|
|
|
func DelController(c echo.Context) (error) {
|
|
var res Response
|
|
var status int
|
|
var pos int
|
|
|
|
pos, err := strconv.Atoi(c.QueryParam("p"))
|
|
if err == nil {
|
|
err = list.deleteElement(pos)
|
|
}
|
|
|
|
if err == nil {
|
|
res = Response{ true, "OK" }
|
|
status = http.StatusOK
|
|
} else {
|
|
res = Response{ false, "Failed to delete from list" }
|
|
status = http.StatusInternalServerError
|
|
}
|
|
|
|
return c.JSON(status, res)
|
|
}
|
|
|
|
func auth(username, password string, c echo.Context) (bool, error) {
|
|
hash := sha512.Sum512([]byte(password))
|
|
hashString := hex.EncodeToString(hash[:])
|
|
if username == adminUsername && hashString == adminPassword {
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func RSSController(c echo.Context) (err error) {
|
|
if blob, err := list.getFeed().ToRss(); err == nil {
|
|
c.Blob(http.StatusOK, "application/xml", []byte(blob))
|
|
}
|
|
return
|
|
}
|
|
|
|
func main() {
|
|
e := echo.New()
|
|
|
|
e.GET("/list", ListController)
|
|
|
|
e.GET("/rss", RSSController)
|
|
e.Static("/", "index.html")
|
|
e.Static("/static", "static")
|
|
|
|
admin := e.Group("/admin", middleware.BasicAuth(auth))
|
|
admin.POST("/add", AddController)
|
|
admin.GET("/del", DelController)
|
|
admin.Static("/", "admin/index.html")
|
|
admin.Static("/static", "admin/static")
|
|
|
|
e.Logger.Fatal(e.Start(":30303"))
|
|
}
|