14 changed files with 605 additions and 34 deletions
@ -1,2 +1,3 @@ |
|||||
*.yaml |
*.yaml |
||||
photo-bot |
photo-bot |
||||
|
statik |
||||
|
|||||
@ -0,0 +1,212 @@ |
|||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"html/template" |
||||
|
"io" |
||||
|
"io/ioutil" |
||||
|
"log" |
||||
|
"net/http" |
||||
|
"path" |
||||
|
"strings" |
||||
|
|
||||
|
_ "github.com/nmasse-itix/Telegram-Photo-Album-Bot/statik" |
||||
|
"github.com/rakyll/statik/fs" |
||||
|
) |
||||
|
|
||||
|
func slurpFile(statikFS http.FileSystem, filename string) (string, error) { |
||||
|
fd, err := statikFS.Open(filename) |
||||
|
if err != nil { |
||||
|
return "", err |
||||
|
} |
||||
|
defer fd.Close() |
||||
|
|
||||
|
content, err := ioutil.ReadAll(fd) |
||||
|
if err != nil { |
||||
|
return "", err |
||||
|
} |
||||
|
|
||||
|
return string(content), nil |
||||
|
} |
||||
|
|
||||
|
func getTemplate(statikFS http.FileSystem, filename string, name string) (*template.Template, error) { |
||||
|
tmpl := template.New(name) |
||||
|
content, err := slurpFile(statikFS, filename) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
customFunctions := template.FuncMap{ |
||||
|
"video": func(files []string) string { |
||||
|
for _, file := range files { |
||||
|
if strings.HasSuffix(file, ".mp4") { |
||||
|
return file |
||||
|
} |
||||
|
} |
||||
|
return "" |
||||
|
}, |
||||
|
"photo": func(files []string) string { |
||||
|
for _, file := range files { |
||||
|
if strings.HasSuffix(file, ".jpeg") { |
||||
|
return file |
||||
|
} |
||||
|
} |
||||
|
return "" |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
return tmpl.Funcs(customFunctions).Parse(content) |
||||
|
} |
||||
|
|
||||
|
// ShiftPath splits off the first component of p, which will be cleaned of
|
||||
|
// relative components before processing. head will never contain a slash and
|
||||
|
// tail will always be a rooted path without trailing slash.
|
||||
|
//
|
||||
|
// From https://blog.merovius.de/2017/06/18/how-not-to-use-an-http-router.html
|
||||
|
func ShiftPath(p string) (head, tail string) { |
||||
|
p = path.Clean("/" + p) |
||||
|
i := strings.Index(p[1:], "/") + 1 |
||||
|
if i <= 0 { |
||||
|
//log.Printf("head: %s, tail: /", p[1:])
|
||||
|
return p[1:], "/" |
||||
|
} |
||||
|
//log.Printf("head: %s, tail: %s", p[1:i], p[i:])
|
||||
|
return p[1:i], p[i:] |
||||
|
} |
||||
|
|
||||
|
type WebInterface struct { |
||||
|
AlbumTemplate *template.Template |
||||
|
MediaTemplate *template.Template |
||||
|
} |
||||
|
|
||||
|
func (bot *PhotoBot) ServeWebInterface(listenAddr string) { |
||||
|
statikFS, err := fs.New() |
||||
|
if err != nil { |
||||
|
log.Fatal(err) |
||||
|
} |
||||
|
|
||||
|
bot.WebInterface.AlbumTemplate, err = getTemplate(statikFS, "/album.html.template", "album") |
||||
|
if err != nil { |
||||
|
log.Fatal(err) |
||||
|
} |
||||
|
|
||||
|
bot.WebInterface.MediaTemplate, err = getTemplate(statikFS, "/media.html.template", "media") |
||||
|
if err != nil { |
||||
|
log.Fatal(err) |
||||
|
} |
||||
|
|
||||
|
router := http.NewServeMux() |
||||
|
router.Handle("/js/", http.FileServer(statikFS)) |
||||
|
router.Handle("/css/", http.FileServer(statikFS)) |
||||
|
router.Handle("/", bot) |
||||
|
|
||||
|
server := &http.Server{ |
||||
|
Addr: listenAddr, |
||||
|
Handler: router, |
||||
|
} |
||||
|
log.Fatal(server.ListenAndServe()) |
||||
|
} |
||||
|
|
||||
|
func (bot *PhotoBot) HandleFileNotFound(w http.ResponseWriter, r *http.Request) { |
||||
|
http.Error(w, "File not found", http.StatusNotFound) |
||||
|
} |
||||
|
|
||||
|
func (bot *PhotoBot) HandleError(w http.ResponseWriter, r *http.Request) { |
||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError) |
||||
|
} |
||||
|
|
||||
|
func (bot *PhotoBot) HandleDisplayAlbum(w http.ResponseWriter, r *http.Request, albumName string) { |
||||
|
if albumName == "latest" { |
||||
|
albumName = "" |
||||
|
} |
||||
|
|
||||
|
album, err := bot.MediaStore.GetAlbum(albumName, false) |
||||
|
if err != nil { |
||||
|
log.Printf("GetAlbum: %s", err) |
||||
|
bot.HandleError(w, r) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
err = bot.WebInterface.AlbumTemplate.Execute(w, album) |
||||
|
if err != nil { |
||||
|
log.Printf("Template.Execute: %s", err) |
||||
|
bot.HandleError(w, r) |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (bot *PhotoBot) HandleDisplayMedia(w http.ResponseWriter, r *http.Request, albumName string, mediaId string) { |
||||
|
if albumName == "latest" { |
||||
|
albumName = "" |
||||
|
} |
||||
|
|
||||
|
media, err := bot.MediaStore.GetMedia(albumName, mediaId) |
||||
|
if err != nil { |
||||
|
log.Printf("MediaStore.GetMedia: %s", err) |
||||
|
bot.HandleError(w, r) |
||||
|
return |
||||
|
|
||||
|
} |
||||
|
|
||||
|
if media == nil { |
||||
|
bot.HandleFileNotFound(w, r) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
err = bot.WebInterface.MediaTemplate.Execute(w, media) |
||||
|
if err != nil { |
||||
|
log.Printf("Template.Execute: %s", err) |
||||
|
bot.HandleError(w, r) |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (bot *PhotoBot) HandleGetMedia(w http.ResponseWriter, r *http.Request, albumName string, mediaFilename string) { |
||||
|
if albumName == "latest" { |
||||
|
albumName = "" |
||||
|
} |
||||
|
|
||||
|
fd, err := bot.MediaStore.OpenFile(albumName, mediaFilename) |
||||
|
if err != nil { |
||||
|
log.Printf("MediaStore.OpenFile: %s", err) |
||||
|
bot.HandleError(w, r) |
||||
|
return |
||||
|
} |
||||
|
defer fd.Close() |
||||
|
io.Copy(w, fd) // Best effort
|
||||
|
} |
||||
|
|
||||
|
func (bot *PhotoBot) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
||||
|
originalPath := r.URL.Path |
||||
|
var resource string |
||||
|
resource, r.URL.Path = ShiftPath(r.URL.Path) |
||||
|
|
||||
|
switch r.Method { |
||||
|
case "GET": |
||||
|
if resource == "album" { |
||||
|
var albumName, kind, media string |
||||
|
albumName, r.URL.Path = ShiftPath(r.URL.Path) |
||||
|
kind, r.URL.Path = ShiftPath(r.URL.Path) |
||||
|
media, r.URL.Path = ShiftPath(r.URL.Path) |
||||
|
if albumName != "" { |
||||
|
if kind == "" && media == "" { |
||||
|
if !strings.HasSuffix(originalPath, "/") { |
||||
|
http.Redirect(w, r, originalPath+"/", http.StatusMovedPermanently) |
||||
|
return |
||||
|
} |
||||
|
bot.HandleDisplayAlbum(w, r, albumName) |
||||
|
return |
||||
|
} else if kind == "raw" && media != "" { |
||||
|
bot.HandleGetMedia(w, r, albumName, media) |
||||
|
return |
||||
|
} else if kind == "media" && media != "" { |
||||
|
bot.HandleDisplayMedia(w, r, albumName, media) |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
default: |
||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
||||
|
return |
||||
|
} |
||||
|
bot.HandleFileNotFound(w, r) |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html> |
||||
|
<head> |
||||
|
<title>{{ .Title }}</title> |
||||
|
<meta charset="utf-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> |
||||
|
<link rel="stylesheet" href="/css/main.css"> |
||||
|
<script type="text/javascript" src="/js/main.js"></script> |
||||
|
</head> |
||||
|
<body> |
||||
|
<h1>{{ .Title }}</h1> |
||||
|
<ul> |
||||
|
{{ range .Media }} |
||||
|
{{ if eq .Type "photo" }} |
||||
|
<li> |
||||
|
<a href="media/{{ .ID }}/"><img src="raw/{{ .Files|photo }}" loading="lazy" /></a> |
||||
|
</li> |
||||
|
{{ else if eq .Type "video" }} |
||||
|
<li> |
||||
|
<a href="media/{{ .ID }}/"> |
||||
|
<video loop muted poster="raw/{{ .Files|photo }}"> |
||||
|
<source src="raw/{{ .Files|video }}" type="video/mp4"> |
||||
|
</video> |
||||
|
</a> |
||||
|
</li> |
||||
|
{{ end }} |
||||
|
{{ end }} |
||||
|
<li><!-- Last item is here as a filler for the last row of the flex box --></li> |
||||
|
</ul> |
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1,67 @@ |
|||||
|
/* CSS inspired by https://css-tricks.com/adaptive-photo-layout-with-flexbox/ */ |
||||
|
|
||||
|
/* Common */ |
||||
|
|
||||
|
h1 { |
||||
|
font-family: sans-serif; |
||||
|
} |
||||
|
|
||||
|
/* Album */ |
||||
|
|
||||
|
body { |
||||
|
margin: 3vh; |
||||
|
} |
||||
|
|
||||
|
ul { |
||||
|
display: flex; |
||||
|
flex-wrap: wrap; |
||||
|
align-items: flex-start; |
||||
|
padding: 0; |
||||
|
margin: 0; |
||||
|
} |
||||
|
|
||||
|
li { |
||||
|
height: 20vh; |
||||
|
flex-grow: 0.5; |
||||
|
list-style-type: none; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
li img, li video { |
||||
|
max-height: 100%; |
||||
|
min-width: 100%; |
||||
|
object-fit: cover; |
||||
|
vertical-align: bottom; |
||||
|
} |
||||
|
|
||||
|
li:last-child { |
||||
|
flex-grow: 10; |
||||
|
} |
||||
|
|
||||
|
/* Album */ |
||||
|
|
||||
|
div { |
||||
|
height: 100%; |
||||
|
width: 100%; |
||||
|
display: flex; |
||||
|
justify-content: center |
||||
|
} |
||||
|
|
||||
|
html, body.media { |
||||
|
height: 100%; |
||||
|
margin: 0 |
||||
|
} |
||||
|
|
||||
|
body.media h1 { |
||||
|
position: fixed; |
||||
|
bottom: 0px; |
||||
|
left: 10%; |
||||
|
right: 10%; |
||||
|
background-color: #888888AA; |
||||
|
padding: 1vh; |
||||
|
|
||||
|
} |
||||
|
|
||||
|
div img, div video { |
||||
|
object-fit: contain; |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
|
||||
|
document.addEventListener('DOMContentLoaded', function(event) { |
||||
|
var videos = document.getElementsByTagName("video"); |
||||
|
|
||||
|
for (var i = 0; i < videos.length; i++) { |
||||
|
var video = videos[i]; |
||||
|
video.addEventListener("mouseenter", function(event) { |
||||
|
event.target.muted = false; |
||||
|
event.target.play(); |
||||
|
}); |
||||
|
|
||||
|
video.addEventListener("mouseleave", function(event) { |
||||
|
event.target.muted = true; |
||||
|
event.target.pause(); |
||||
|
}); |
||||
|
} |
||||
|
}, false); |
||||
@ -0,0 +1,23 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html> |
||||
|
<head> |
||||
|
<title>{{ .Caption }}</title> |
||||
|
<meta charset="utf-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> |
||||
|
<link rel="stylesheet" href="/css/main.css"> |
||||
|
</head> |
||||
|
<body class="media"> |
||||
|
{{ if ne .Caption "" }} |
||||
|
<h1>{{ .Caption }}</h1> |
||||
|
{{ end }} |
||||
|
<div> |
||||
|
{{ if eq .Type "photo" }} |
||||
|
<img src="../../raw/{{ .Files|photo }}" /> |
||||
|
{{ else if eq .Type "video" }} |
||||
|
<video controls autoplay poster="../../raw/{{ .Files|photo }}"> |
||||
|
<source src="../../raw/{{ .Files|video }}" type="video/mp4"> |
||||
|
</video> |
||||
|
{{ end }} |
||||
|
</div> |
||||
|
</body> |
||||
|
</html> |
||||
Loading…
Reference in new issue