Compare commits
7 Commits
Author | SHA1 | Date |
---|---|---|
danoloan10 | db76fd1d3a | |
danoloan10 | 78f3e96aa6 | |
danoloan10 | b1e21c6d8d | |
danoloan10 | 45913bdcdc | |
danoloan10 | 336140c1fa | |
danoloan10 | b9c8278246 | |
danoloan10 | f5b190e762 |
|
@ -12,6 +12,8 @@
|
|||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
__debug_bin
|
||||
muse
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="initial-scale=1">
|
||||
<link href="/static/style.css" rel="stylesheet" type="text/css" />
|
||||
<link href="../static/admin.css" rel="stylesheet" type="text/css" />
|
||||
<title>splog: zona del admin 😎</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1> Zona del admin 😎 </h1>
|
||||
<div class="action-wrap">
|
||||
<button class="cajetin">Añadir canción</button>
|
||||
<form id="add" class="action">
|
||||
<div class="form-elem"> <input name="artist" type="text" placeholder="Artista"/> </div>
|
||||
<div class="form-elem"> <input name="track" type="text" placeholder="Canción"/> </div>
|
||||
<div class="form-elem"> <input name="album" type="text" placeholder="Álbum"/> </div>
|
||||
<div class="form-elem"> <input name="linkto" type="text" placeholder="Enlace"/> </div>
|
||||
<div class="form-elem"> <input name="date" type="date" placeholder="Fecha"/> </div>
|
||||
<hr/>
|
||||
<input type="submit" value="Añadir">
|
||||
</form>
|
||||
</div>
|
||||
<div class="action-wrap">
|
||||
<button class="cajetin">Eliminar canción</button>
|
||||
<div class="action">
|
||||
<div class="scroll" id="dellist"></div>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<a href="..">Volver al inicio</a>
|
||||
</footer>
|
||||
<script src="static/main.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,114 +0,0 @@
|
|||
function refreshHeight(elem) {
|
||||
elem.style.maxHeight = `${elem.scrollHeight}px`;
|
||||
}
|
||||
function validate(form) {
|
||||
var valid = true;
|
||||
if (form['artist'].value == '') {
|
||||
form['artist'].parentElement.setAttribute('error',
|
||||
'Se necesita el nombre del artista');
|
||||
valid = false;
|
||||
}
|
||||
if (form['album'].value == '' && form['track'].value == '') {
|
||||
form['album'].parentElement.setAttribute('error',
|
||||
'Se necesita al menos uno de entre «canción» o «áblum»');
|
||||
form['track'].parentElement.setAttribute('error',
|
||||
'Se necesita al menos uno de entre «canción» o «áblum»');
|
||||
valid = false;
|
||||
}
|
||||
refreshHeight(form.closest('.action-wrap'));
|
||||
return valid;
|
||||
}
|
||||
|
||||
function deleteSong() {
|
||||
var button = this;
|
||||
var data = button.closest('.data-elem');
|
||||
var index = data.getAttribute('data-info');
|
||||
if (confirm('¿Seguro?')) {
|
||||
fetch(`del?p=${index}`).then(response => {
|
||||
var wrap = button.closest('.action-wrap');
|
||||
if (response.ok) {
|
||||
data.parentElement.removeChild(data);
|
||||
} else {
|
||||
button.closest('.action')
|
||||
.setAttribute('result', 'Error en la solicitud');
|
||||
}
|
||||
refreshHeight(wrap);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fetch('../list')
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then(list => {
|
||||
list.forEach((elem, index) => {
|
||||
var newButton = document.createElement('button');
|
||||
var title = elem['track'] == undefined ? elem['album'] : elem['track'];
|
||||
newButton.setAttribute('class', 'data-elem');
|
||||
newButton.onclick = deleteSong;
|
||||
newButton.setAttribute('data-info', index);
|
||||
newButton.innerText = `${elem['artist']} - ${title}`;
|
||||
document.querySelector('#dellist').appendChild(newButton);
|
||||
});
|
||||
})
|
||||
.catch(reason => console.log(reason));
|
||||
|
||||
document.querySelectorAll('.action-wrap').forEach(elem => {
|
||||
var cajetin = elem.querySelector('.cajetin');
|
||||
elem.setAttribute('collapsed', 'true');
|
||||
elem.style.maxHeight = `${cajetin.scrollHeight}px`;
|
||||
cajetin.onclick = function() {
|
||||
var form = elem.querySelector('form');
|
||||
if (elem.getAttribute('collapsed') == 'true') {
|
||||
refreshHeight(elem);
|
||||
elem.setAttribute('collapsed', 'false');
|
||||
} else {
|
||||
elem.style.maxHeight = `${cajetin.scrollHeight}px`;
|
||||
elem.setAttribute('collapsed', 'true');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.form-elem > input').forEach(elem => {
|
||||
elem.addEventListener('input', () => {
|
||||
elem.parentElement.removeAttribute('error');
|
||||
elem.closest('form').removeAttribute('result');
|
||||
refreshHeight(elem.closest('.action-wrap'));
|
||||
});
|
||||
});
|
||||
|
||||
const add = document.querySelector('#add');
|
||||
add.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
if (validate(add)) {
|
||||
var xmlHttp = new XMLHttpRequest();
|
||||
var json = {}
|
||||
var data = new FormData(add);
|
||||
data.forEach((v, k) => json[k] = v);
|
||||
if (!json['date']) {
|
||||
const date = new Date(Date.now());
|
||||
const year = `${date.getFullYear()}`;
|
||||
const month = `${date.getMonth() < 10 ? '0' : '' }${date.getMonth()+1}`;
|
||||
const day = `${date.getDate() < 10 ? '0' : '' }${date.getDate()+1}`;
|
||||
json['date'] = `${year}-${month}-${day}`;
|
||||
}
|
||||
fetch('add', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(json),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
add.setAttribute('result', 'Canción añadida');
|
||||
} else {
|
||||
add.setAttribute('result', 'Error en la solicitud');
|
||||
}
|
||||
refreshHeight(add.closest('.action-wrap'));
|
||||
});
|
||||
}
|
||||
return false;
|
||||
});
|
308
main.go
308
main.go
|
@ -1,285 +1,121 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"encoding/json"
|
||||
"crypto/sha512"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"io/ioutil"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"muse/storage"
|
||||
|
||||
"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
|
||||
TemplateRenderer struct {
|
||||
template *template.Template
|
||||
}
|
||||
Template string
|
||||
)
|
||||
|
||||
const (
|
||||
jsTimeLayout = "2006-01-02"
|
||||
adminUsername = "danolo"
|
||||
adminPassword = "bd4cad796950f50352225de3c773d8f3c39622bc17f34ad661eabe615cdf6d32751c5751e0648dc17d890f40330018334a2ae899878f200f6dc80121ddb70cc9"
|
||||
)
|
||||
|
||||
var (
|
||||
list *entryList = &entryList{
|
||||
filename: "/var/lib/muse.json",
|
||||
modtime: time.Unix(0, 0),
|
||||
list *storage.EntryList = &storage.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
|
||||
// TemplateRenderer
|
||||
func (renderer *TemplateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
|
||||
return renderer.template.ExecuteTemplate(w, name, data)
|
||||
}
|
||||
|
||||
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)
|
||||
func (template Template) TemplateController(c echo.Context) (err error) {
|
||||
entries, err := list.GetEntries()
|
||||
if err == nil {
|
||||
mod = list.modtime.Before(info.ModTime())
|
||||
list.modtime = info.ModTime()
|
||||
} else {
|
||||
mod = true
|
||||
err = c.Render(http.StatusOK, string(template), struct {
|
||||
Entries []*storage.Entry
|
||||
Today storage.JSTime
|
||||
}{
|
||||
Entries: entries,
|
||||
Today: storage.JSTime(time.Now()),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (list *entryList) write() (err error) {
|
||||
data, err := json.MarshalIndent(list.v, "", " ")
|
||||
func AddController(c echo.Context) (err error) {
|
||||
entry := new(storage.Entry)
|
||||
err = c.Bind(entry)
|
||||
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)
|
||||
if c.FormValue("date") == "" {
|
||||
entry.Date = storage.JSTime(time.Now())
|
||||
}
|
||||
err = list.AddEntry(entry)
|
||||
}
|
||||
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]
|
||||
}
|
||||
err = c.Redirect(http.StatusFound, "..")
|
||||
}
|
||||
|
||||
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
|
||||
func DelController(c echo.Context) error {
|
||||
var url string
|
||||
var status int
|
||||
var pos int
|
||||
|
||||
pos, err := strconv.Atoi(c.QueryParam("p"))
|
||||
pos, err := strconv.Atoi(c.Param("index"))
|
||||
if err == nil {
|
||||
err = list.deleteElement(pos)
|
||||
err = list.DeleteElement(pos)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
res = Response{ true, "OK" }
|
||||
status = http.StatusOK
|
||||
url = "../.."
|
||||
status = http.StatusFound
|
||||
} else {
|
||||
res = Response{ false, "Failed to delete from list" }
|
||||
status = http.StatusInternalServerError
|
||||
}
|
||||
|
||||
return c.JSON(status, res)
|
||||
return c.Redirect(status, url)
|
||||
}
|
||||
|
||||
func DelConfirmController(c echo.Context) error {
|
||||
var entries []*storage.Entry
|
||||
failed := c.QueryParam("failed")
|
||||
indexStr := c.Param("index")
|
||||
index, err := strconv.Atoi(indexStr)
|
||||
if err == nil {
|
||||
entries, err = list.GetEntries()
|
||||
}
|
||||
if index >= len(entries) {
|
||||
err = errors.New("Index out of range")
|
||||
}
|
||||
if err == nil {
|
||||
err = c.Render(http.StatusOK, "del.html", struct {
|
||||
Index int
|
||||
Elem *storage.Entry
|
||||
Failed string
|
||||
}{
|
||||
Index: index,
|
||||
Elem: entries[index],
|
||||
Failed: failed,
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func auth(username, password string, c echo.Context) (bool, error) {
|
||||
|
@ -292,7 +128,7 @@ func auth(username, password string, c echo.Context) (bool, error) {
|
|||
}
|
||||
|
||||
func RSSController(c echo.Context) (err error) {
|
||||
if blob, err := list.getFeed().ToRss(); err == nil {
|
||||
if blob, err := list.GetFeed().ToRss(); err == nil {
|
||||
c.Blob(http.StatusOK, "application/xml", []byte(blob))
|
||||
}
|
||||
return
|
||||
|
@ -301,16 +137,20 @@ func RSSController(c echo.Context) (err error) {
|
|||
func main() {
|
||||
e := echo.New()
|
||||
|
||||
e.GET("/list", ListController)
|
||||
e.Renderer = &TemplateRenderer{
|
||||
template: template.Must(template.ParseGlob("templates/*.html")),
|
||||
}
|
||||
|
||||
e.GET("/", Template("index.html").TemplateController)
|
||||
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.POST("/del/:index", DelController)
|
||||
admin.GET("/del/:index", DelConfirmController)
|
||||
admin.GET("/", Template("admin.html").TemplateController)
|
||||
admin.Static("/static", "admin/static")
|
||||
|
||||
e.Logger.Fatal(e.Start(":30303"))
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
* {
|
||||
max-width: inherit;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
@ -17,17 +21,6 @@ footer > a {
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
.action {
|
||||
padding: 20px;
|
||||
}
|
||||
.action[result]::after {
|
||||
content: attr(result);
|
||||
display: block;
|
||||
padding-top: 1em;
|
||||
text-align: center;
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.action-wrap {
|
||||
margin: 0.5em auto;
|
||||
border-radius: 6px;
|
||||
|
@ -61,11 +54,41 @@ footer > a {
|
|||
overflow: auto;
|
||||
}
|
||||
|
||||
.cajetin {
|
||||
/* Dropdown */
|
||||
button.dropdown {
|
||||
width: 100%;
|
||||
display: block;
|
||||
padding: 0.5em;
|
||||
}
|
||||
button.dropdown {
|
||||
padding: 0;
|
||||
}
|
||||
button.dropdown > input[type=checkbox] {
|
||||
display: none;
|
||||
}
|
||||
button.dropdown > label {
|
||||
padding: 0.5em;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
button.dropdown:has(input[type=checkbox]:checked) ~ .action {
|
||||
max-height: 50vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.action {
|
||||
max-height: 0;
|
||||
padding: 0;
|
||||
transition: max-height 0.2s linear;
|
||||
}
|
||||
|
||||
.action[result]::after {
|
||||
content: attr(result);
|
||||
display: block;
|
||||
padding-top: 1em;
|
||||
text-align: center;
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.data-elem {
|
||||
display: block;
|
||||
|
|
|
@ -58,12 +58,14 @@ button,
|
|||
.button {
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type=checkbox].button:hover ~ button,
|
||||
input[type=button]:hover,
|
||||
input[type=submit]:hover,
|
||||
button:hover,
|
||||
.button:hover {
|
||||
background-color: #444;
|
||||
}
|
||||
input[type=checkbox].button:active ~ button,
|
||||
input[type=button]:active,
|
||||
input[type=submit]:active,
|
||||
button:active,
|
||||
|
@ -100,6 +102,7 @@ hr {
|
|||
background-color: #eee;
|
||||
}
|
||||
|
||||
input[type=checkbox].button:hover ~ button,
|
||||
input[type=button]:hover,
|
||||
input[type=submit]:hover,
|
||||
button:hover,
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package storage
|
||||
|
||||
import "fmt"
|
||||
|
||||
type (
|
||||
Entry struct {
|
||||
Artist string `json:"artist" form:"artist"`
|
||||
Linkto string `json:"linkto" form:"linkto"`
|
||||
Track string `json:"track" form:"track"`
|
||||
Album string `json:"album" form:"album"`
|
||||
Cover string `json:"cover" form:"cover"`
|
||||
Date JSTime `json:"date" form:"date"`
|
||||
}
|
||||
)
|
||||
|
||||
func (entry *Entry) Showname() string {
|
||||
var name string
|
||||
if len(entry.Track) > 0 {
|
||||
name = entry.Track
|
||||
} else {
|
||||
name = entry.Album
|
||||
}
|
||||
return fmt.Sprintf("%s - %s", entry.Artist, name)
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/feeds"
|
||||
)
|
||||
|
||||
type (
|
||||
EntryList struct {
|
||||
Filename string
|
||||
ModTime time.Time
|
||||
v []*Entry
|
||||
mu sync.RWMutex
|
||||
}
|
||||
)
|
||||
|
||||
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) GetEntries() (entry []*Entry, err error) {
|
||||
list.mu.RLock()
|
||||
defer list.mu.RUnlock()
|
||||
|
||||
err = list.read()
|
||||
entry = 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
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
JSTime time.Time
|
||||
)
|
||||
|
||||
const (
|
||||
jsTimeLite = "2006-01-02"
|
||||
jsTimeLayout = "2006-01-02 15:04:05"
|
||||
)
|
||||
|
||||
func (ct *JSTime) UnmarshalJSON(blob []byte) (err error) {
|
||||
var jsonString string
|
||||
if err = json.Unmarshal(blob, &jsonString); err == nil {
|
||||
err = ct.UnmarshalParam(jsonString)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ct *JSTime) UnmarshalParam(param string) (err error) {
|
||||
loc, _ := time.LoadLocation("Europe/Madrid")
|
||||
nt, err := time.ParseInLocation(jsTimeLite, param, loc)
|
||||
*ct = JSTime(nt)
|
||||
return
|
||||
}
|
||||
|
||||
func (ct JSTime) MarshalJSON() ([]byte, error) {
|
||||
return []byte(strconv.Quote(ct.String())), nil
|
||||
}
|
||||
|
||||
func (ct JSTime) String() string {
|
||||
t := time.Time(ct)
|
||||
return fmt.Sprintf("%s", t.Format(jsTimeLite))
|
||||
}
|
||||
|
||||
func (ct JSTime) Unix() int64 {
|
||||
return time.Time(ct).Unix()
|
||||
}
|
||||
|
||||
func (ct JSTime) Local() JSTime {
|
||||
return JSTime(time.Time(ct).Local())
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="initial-scale=1">
|
||||
<link href="/static/style.css" rel="stylesheet" type="text/css" />
|
||||
<link href="../static/admin.css" rel="stylesheet" type="text/css" />
|
||||
<title>muse: zona del admin 😎</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1> Zona del admin 😎 </h1>
|
||||
<div class="action-wrap">
|
||||
<button class="dropdown">
|
||||
<input id="check-add" class="dropdown" type="checkbox"/>
|
||||
<label for="check-add">
|
||||
Añadir canción
|
||||
</label>
|
||||
</button>
|
||||
<form id="add" class="action" action="add" method="post" type="application/json">
|
||||
<div class="form-elem"> <input name="artist" type="text" placeholder="Artista"/> </div>
|
||||
<div class="form-elem"> <input name="track" type="text" placeholder="Canción"/> </div>
|
||||
<div class="form-elem"> <input name="album" type="text" placeholder="Álbum"/> </div>
|
||||
<div class="form-elem"> <input name="linkto" type="text" placeholder="Enlace"/> </div>
|
||||
<div class="form-elem"> <input name="date" type="date" placeholder="Fecha" value="{{ .Today.String }}"/> </div>
|
||||
<hr/>
|
||||
<input type="submit" value="Añadir">
|
||||
</form>
|
||||
</div>
|
||||
<div class="action-wrap">
|
||||
<button class="dropdown">
|
||||
<input id="check-del" class="dropdown" type="checkbox"/>
|
||||
<label for="check-del">
|
||||
Eliminar canción
|
||||
</label>
|
||||
</button>
|
||||
<div class="action">
|
||||
<div class="scroll">
|
||||
{{ range $index, $elem := .Entries }}
|
||||
<a href="del/{{ $index }}" class="data-elem">
|
||||
{{ $elem.Showname }}
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<a href="..">Volver al inicio</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,25 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="initial-scale=1">
|
||||
<link href="/static/style.css" rel="stylesheet" type="text/css" />
|
||||
<link href="../static/admin.css" rel="stylesheet" type="text/css" />
|
||||
<title>muse: zona del admin 😎</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1> Zona del admin 😎 </h1>
|
||||
<p>
|
||||
<form action="{{ .Index }}" method="post">
|
||||
¿Quieres borrar <b>{{ .Elem.Showname }}</b>?
|
||||
<input type="submit" value="Confirmar">
|
||||
</form>
|
||||
</p>
|
||||
{{ if .Failed }}
|
||||
<p style="color: red;">El borrado falló ☹️</p>
|
||||
{{ end }}
|
||||
<footer>
|
||||
<a href="..">Volver al inicio</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
|
@ -20,10 +20,10 @@ body {
|
|||
display: inline-table;
|
||||
background-color: #333;
|
||||
color: #ddd;
|
||||
transition: transform .1s;
|
||||
transition: transform .1s;
|
||||
box-sizing: border-box;
|
||||
word-break: break-word;
|
||||
font-size: calc(0.5em + 1vw);
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
.elem:hover {
|
||||
|
@ -31,6 +31,10 @@ body {
|
|||
background-color: #373737;
|
||||
}
|
||||
|
||||
main > a {
|
||||
font-size: 0px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 10px 20px;
|
||||
font-weight: bold;
|
||||
|
@ -85,14 +89,14 @@ nav > a:hover {
|
|||
}
|
||||
@media screen and (max-width: 700px) {
|
||||
.elem {
|
||||
width: calc((100% - (5px * 6)) / 2);
|
||||
width: calc((100% - (5px * 4)) / 2);
|
||||
font-size: medium;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 320px) {
|
||||
.elem {
|
||||
width: 100%;
|
||||
display: block;
|
||||
font-size: calc(0.65em + 1vw);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -122,8 +126,6 @@ nav > a:hover {
|
|||
}
|
||||
|
||||
</style>
|
||||
<script src="static/prog.js"></script>
|
||||
<script src="static/main.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
|
@ -131,16 +133,13 @@ nav > a:hover {
|
|||
<a href="/">danoloan</a>
|
||||
</h1>
|
||||
<p>
|
||||
bitácora musical
|
||||
bitácora musical
|
||||
</p>
|
||||
<p style="font-size: 75%">
|
||||
la música que me va
|
||||
la música que me va
|
||||
</p>
|
||||
<p style="font-size: 75%">
|
||||
pincha en una canción para abrirla en <em>spotify</em>
|
||||
</p>
|
||||
<p style="font-size: 75%">
|
||||
en <strong><a href="https://open.spotify.com/playlist/2J5wJd8yzwswKgC52f4iRV?si=lLqi6f0_T4Ki509CEceglQ">este enlace</a></strong> está la lista de reproducción de Spotify con todas las canciones <span style="font-size:50%">bueno no todas que soy un vago y no la actualizo</span>
|
||||
pincha en una canción para abrirla (en <em>spotify</em>, <em>youtube</em>, etc.)
|
||||
</p>
|
||||
</header>
|
||||
<nav>
|
||||
|
@ -148,22 +147,16 @@ nav > a:hover {
|
|||
<a href="admin/">Zona del admin <span class="emoji">😎</span></a>
|
||||
</nav>
|
||||
<main>
|
||||
<noscript>
|
||||
<div style="font-size: 15px; text-align: left; max-width: 600px; margin:auto">
|
||||
<p>
|
||||
Esta página utiliza código JavaScript en el navegador
|
||||
para generar los contenidos en pantalla.
|
||||
Por favor, active JavaScript para poder usarla.
|
||||
</p>
|
||||
<p>
|
||||
El código utilizado por la página es bastante sencillo,
|
||||
si lo quiere revisar pinche
|
||||
<a href="static/main.js">aquí</a>.
|
||||
{{ range .Entries }}
|
||||
<a href="{{ .Linkto }}">
|
||||
<div class="elem">
|
||||
<img src="{{ .Cover }}" class="cover" loading="lazy">
|
||||
<div class="title">{{ .Showname }}</div>
|
||||
<div class="date">{{ .Date }}</div>
|
||||
</p>
|
||||
</div>
|
||||
</noscript>
|
||||
</a>
|
||||
{{ end }}
|
||||
</main>
|
||||
<div id="load">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue