mirror of
https://github.com/nxshock/gallery.git
synced 2024-11-28 00:21:01 +05:00
Upload code
This commit is contained in:
parent
237137c3f7
commit
328331768a
22
README.md
Normal file
22
README.md
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# Gallery
|
||||||
|
|
||||||
|
Simple gallery.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Create config file and set `WorkingDirectory`
|
||||||
|
2. Start program
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gallery <path to config>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Config structure
|
||||||
|
|
||||||
|
```toml
|
||||||
|
WorkingDirectory = "/home/user/pictures" # Working directory
|
||||||
|
Crf = 40 # Preview CRF
|
||||||
|
# from 1 - maximum quality
|
||||||
|
# to 63 - minimum preview cache size
|
||||||
|
ProcessCount = 4 # Preview generator threads
|
||||||
|
```
|
30
config.go
Normal file
30
config.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/ilyakaznacheev/cleanenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
WorkingDirectory string `env-default:"."`
|
||||||
|
Crf uint64 `env-default:"40"`
|
||||||
|
ProcessCount uint64 `env-default:"4"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfig(path string) (*Config, error) {
|
||||||
|
var config Config
|
||||||
|
|
||||||
|
err := cleanenv.ReadConfig("gallery.toml", &config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
workingDirectory, err := filepath.Abs(config.WorkingDirectory)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
config.WorkingDirectory = workingDirectory
|
||||||
|
|
||||||
|
return &config, nil
|
||||||
|
}
|
5
consts.go
Normal file
5
consts.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultConfigPath = "gallery.conf"
|
||||||
|
)
|
16
go.mod
Normal file
16
go.mod
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
module github.com/nxshock/gallery
|
||||||
|
|
||||||
|
go 1.19
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/ilyakaznacheev/cleanenv v1.4.1
|
||||||
|
github.com/nxshock/zkv v0.0.9
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/BurntSushi/toml v1.1.0 // indirect
|
||||||
|
github.com/joho/godotenv v1.4.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.15.12 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
|
||||||
|
)
|
19
go.sum
Normal file
19
go.sum
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
|
||||||
|
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/ilyakaznacheev/cleanenv v1.4.1 h1:zroQjmb8e3w6DBcgbgFXtlQTX8xP8XCOg1etuYv4hX0=
|
||||||
|
github.com/ilyakaznacheev/cleanenv v1.4.1/go.mod h1:i0owW+HDxeGKE0/JPREJOdSCPIyOnmh6C0xhWAkF/xA=
|
||||||
|
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||||
|
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM=
|
||||||
|
github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
|
||||||
|
github.com/nxshock/zkv v0.0.9 h1:WS4Rr79y9EFufwgOSI7gQsVyQJlbaYiB8633rt8Bijo=
|
||||||
|
github.com/nxshock/zkv v0.0.9/go.mod h1:5hMUAMHPeTWWW3HFD0CD+znB02BQLSEa+Z3+fZSOeh4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=
|
||||||
|
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=
|
72
handlers.go
Normal file
72
handlers.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getResponse(path string) ([]Item, error) {
|
||||||
|
fileNames, err := filepath.Glob(filepath.Join(path, "*"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range fileNames {
|
||||||
|
fileNames[i] = strings.TrimPrefix(filepath.ToSlash(fileNames[i]), filepath.ToSlash(config.WorkingDirectory))
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]Item, 0)
|
||||||
|
|
||||||
|
for _, fileName := range fileNames {
|
||||||
|
itemType, err := getType(filepath.Join(config.WorkingDirectory, fileName))
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if itemType == Unknown {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
items = append(items, Item{itemType, Path(fileName)})
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(items, func(i, j int) bool {
|
||||||
|
return items[i].ItemType < items[j].ItemType
|
||||||
|
})
|
||||||
|
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rootHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
items, err := getResponse(filepath.Join(config.WorkingDirectory, r.URL.Path[1:]))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = t.Execute(w, items)
|
||||||
|
}
|
||||||
|
|
||||||
|
func previewHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fileName := strings.TrimPrefix(r.URL.Path, "/preview/")
|
||||||
|
|
||||||
|
b, err := previewCache.Read(filepath.Join(config.WorkingDirectory, fileName))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "image/avif")
|
||||||
|
w.Header().Set("Cache-Control", "max-age=31536000")
|
||||||
|
_, _ = w.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func viewHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fileName := strings.TrimPrefix(r.URL.Path, "/view/")
|
||||||
|
|
||||||
|
http.ServeFile(w, r, filepath.Join(config.WorkingDirectory, fileName))
|
||||||
|
}
|
221
index.html
Normal file
221
index.html
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Gallery</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||||
|
<link rel="shortcut icon" href="#" type="image/x-icon">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--base00: #282c34;
|
||||||
|
--base01: #353b45;
|
||||||
|
--base02: #3e4451;
|
||||||
|
--base03: #545862;
|
||||||
|
--base04: #565c64;
|
||||||
|
--base05: #abb2bf;
|
||||||
|
--base06: #b6bdca;
|
||||||
|
--base07: #c8ccd4;
|
||||||
|
--base08: #e06c75;
|
||||||
|
--base09: #d19a66;
|
||||||
|
--base0A: #e5c07b;
|
||||||
|
--base0B: #98c379;
|
||||||
|
--base0C: #56b6c2;
|
||||||
|
--base0D: #61afef;
|
||||||
|
--base0E: #c678dd;
|
||||||
|
--base0F: #be5046;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: Verdana;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--base05);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--base00);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: none;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
body>ul>li {
|
||||||
|
width: 240px;
|
||||||
|
height: 240px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid var(--base01);
|
||||||
|
}
|
||||||
|
|
||||||
|
body>ul>li>a {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body>ul>li>a>span {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--base00);
|
||||||
|
}
|
||||||
|
|
||||||
|
body>ul>li>img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
/* The Modal (background) */
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 2;
|
||||||
|
/* padding-top: 100px; */
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: rgb(0, 0, 0);
|
||||||
|
background-color: rgba(0, 0, 0, 0.9);
|
||||||
|
}
|
||||||
|
/* Modal Content (image) */
|
||||||
|
|
||||||
|
#modal-content {
|
||||||
|
margin: auto;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
/* The Close Button */
|
||||||
|
|
||||||
|
.close {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
right: 35px;
|
||||||
|
color: var(--base05);
|
||||||
|
font-size: 40px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close:hover,
|
||||||
|
.close:focus {
|
||||||
|
color: #bbb;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<ul>
|
||||||
|
{{range .}} {{if eq .ItemType 1}}
|
||||||
|
<li>
|
||||||
|
<a href="/{{.Path}}"><img src="/preview/{{.Path}}"><span>{{.Path.Base}}</span></a>{{end}} {{if eq .ItemType 2}}
|
||||||
|
<li><img src="/preview/{{.Path}}"></li>{{end}} {{if eq .ItemType 3}}
|
||||||
|
<li><img src="/preview/{{.Path}}"></li>{{end}} {{end}}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- The Modal -->
|
||||||
|
<div id="myModal" class="modal">
|
||||||
|
<span class="close">×</span>
|
||||||
|
<!--<img class="modal-content" id="img01">-->
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var imageIndex = 0
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (event) => {
|
||||||
|
let childNumber = 0
|
||||||
|
if (event.key == "ArrowRight") {
|
||||||
|
imageIndex += 1
|
||||||
|
if (document.querySelector("body > ul").children.length == imageIndex) {
|
||||||
|
imageIndex -= 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
childNumber = imageIndex + 1
|
||||||
|
} else if (event.key == "ArrowLeft") {
|
||||||
|
imageIndex -= 1
|
||||||
|
if (imageIndex < 0) {
|
||||||
|
imageIndex = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
childNumber = imageIndex + 1
|
||||||
|
}
|
||||||
|
document.querySelector("body > ul > li:nth-child(" + childNumber + ") > img").click()
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
// document.title = decodeURI(window.location.pathname).substring(1, window.location.pathname.length - 1) + " :: Gallery"
|
||||||
|
|
||||||
|
// Get the modal
|
||||||
|
var modal = document.getElementById('myModal');
|
||||||
|
|
||||||
|
// Get the image and insert it inside the modal - use its "alt" text as a caption
|
||||||
|
// var img = document.getElementById('myImg');
|
||||||
|
var modalImg = document.getElementById("img01");
|
||||||
|
|
||||||
|
for (let img of document.querySelectorAll('body > ul > li > img')) {
|
||||||
|
img.onclick = function() {
|
||||||
|
try {
|
||||||
|
document.getElementById("modal-content").remove()
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
imageIndex = Array.from(img.parentNode.parentNode.children).indexOf(img.parentNode)
|
||||||
|
modal.style.display = "block"
|
||||||
|
|
||||||
|
let tag = ""
|
||||||
|
|
||||||
|
switch (this.src.split('.').pop().toLowerCase()) {
|
||||||
|
case "jpg":
|
||||||
|
case "jpeg":
|
||||||
|
case "png":
|
||||||
|
case "bmp":
|
||||||
|
case "gif":
|
||||||
|
case "avif":
|
||||||
|
tag = "img"
|
||||||
|
break;
|
||||||
|
case "mkv":
|
||||||
|
case "webm":
|
||||||
|
case "mp4":
|
||||||
|
case "mov":
|
||||||
|
case "avi":
|
||||||
|
case "3gp":
|
||||||
|
tag = "video"
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let imgModal = document.createElement(tag)
|
||||||
|
imgModal.id = "modal-content"
|
||||||
|
imgModal.src = this.src.replace("/preview/", "/view/")
|
||||||
|
imgModal.setAttribute("controls", "controls")
|
||||||
|
|
||||||
|
document.getElementById("myModal").append(imgModal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the <span> element that closes the modal
|
||||||
|
var span = document.getElementsByClassName("close")[0];
|
||||||
|
|
||||||
|
// When the user clicks on <span> (x), close the modal
|
||||||
|
span.onclick = function() {
|
||||||
|
modal.style.display = "none";
|
||||||
|
document.getElementById("modal-content").remove()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</html>
|
47
itemtype.go
Normal file
47
itemtype.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ItemType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Unknown ItemType = iota
|
||||||
|
Directory
|
||||||
|
Picture
|
||||||
|
Video
|
||||||
|
)
|
||||||
|
|
||||||
|
type Path string
|
||||||
|
|
||||||
|
type Item struct {
|
||||||
|
ItemType
|
||||||
|
Path Path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Path) Base() string {
|
||||||
|
return filepath.Base(string(*p))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getType(path string) (ItemType, error) {
|
||||||
|
stat, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return Unknown, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if stat.IsDir() {
|
||||||
|
return Directory, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(filepath.Ext(path)) {
|
||||||
|
case ".jpg", ".jpeg", ".png", ".avif", ".bmp", ".gif":
|
||||||
|
return Picture, nil
|
||||||
|
case ".mov", ".mp4", ".avi", ".mkv", ".3gp", ".webm":
|
||||||
|
return Video, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return Unknown, nil
|
||||||
|
}
|
58
main.go
Normal file
58
main.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
previewCache = NewPreviewCache("cache.zkv")
|
||||||
|
config *Config
|
||||||
|
semaphore chan struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log.SetFlags(0)
|
||||||
|
rand.Seed(time.Now().Unix())
|
||||||
|
|
||||||
|
configPath := defaultConfigPath
|
||||||
|
if len(os.Args) > 1 {
|
||||||
|
configPath = os.Args[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
config, err = loadConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Chdir(config.WorkingDirectory)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
semaphore = make(chan struct{}, config.ProcessCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
go func() {
|
||||||
|
http.HandleFunc("/preview/", previewHandler)
|
||||||
|
http.HandleFunc("/view/", viewHandler)
|
||||||
|
http.HandleFunc("/", rootHandler)
|
||||||
|
log.Fatalln(http.ListenAndServe(":8080", nil))
|
||||||
|
}()
|
||||||
|
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(c, os.Interrupt)
|
||||||
|
<-c
|
||||||
|
|
||||||
|
err := previewCache.Save()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
}
|
103
preview.go
Normal file
103
preview.go
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/nxshock/zkv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PreviewCache struct {
|
||||||
|
store *zkv.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPreviewCache(filePath string) *PreviewCache {
|
||||||
|
store, err := zkv.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reviewCache := &PreviewCache{store}
|
||||||
|
|
||||||
|
return reviewCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PreviewCache) Add(filePath string) ([]byte, error) {
|
||||||
|
defer func() {
|
||||||
|
<-semaphore
|
||||||
|
}()
|
||||||
|
semaphore <- struct{}{}
|
||||||
|
|
||||||
|
tempFileName, err := TempFileName("preview*.avif")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer os.Remove(tempFileName)
|
||||||
|
|
||||||
|
stat, err := os.Stat(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
if stat.IsDir() { // https://trac.ffmpeg.org/wiki/Create%20a%20mosaic%20out%20of%20several%20input%20videos
|
||||||
|
fileNames, err := getRandomFiles(filePath, 4)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd = exec.Command("ffmpeg",
|
||||||
|
"-i", fileNames[0],
|
||||||
|
"-i", fileNames[1],
|
||||||
|
"-i", fileNames[2],
|
||||||
|
"-i", fileNames[3],
|
||||||
|
"-filter_complex", "nullsrc=size=240x240 [base];[0:v] setpts=PTS-STARTPTS, scale=120x120:force_original_aspect_ratio=increase [upperleft];[1:v] setpts=PTS-STARTPTS, scale=120x120:force_original_aspect_ratio=increase [upperright];[2:v] setpts=PTS-STARTPTS, scale=120x120:force_original_aspect_ratio=increase [lowerleft];[3:v] setpts=PTS-STARTPTS, scale=120x120:force_original_aspect_ratio=increase [lowerright];[base][upperleft] overlay=shortest=1 [tmp1];[tmp1][upperright] overlay=shortest=1:x=120 [tmp2];[tmp2][lowerleft] overlay=shortest=1:y=120 [tmp3];[tmp3][lowerright] overlay=shortest=1:x=120:y=120",
|
||||||
|
"-frames:v", "1",
|
||||||
|
"-crf", strconv.FormatUint(config.Crf, 10),
|
||||||
|
"-f", "avif",
|
||||||
|
tempFileName)
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command("ffmpeg.exe",
|
||||||
|
"-i", filepath.FromSlash(filePath),
|
||||||
|
"-vf", "scale=240:240:force_original_aspect_ratio=increase,crop=240:240:exact=1",
|
||||||
|
"-frames:v", "1",
|
||||||
|
"-crf", "40",
|
||||||
|
"-f", "avif",
|
||||||
|
tempFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := os.ReadFile(tempFileName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = pc.store.Set(filePath, b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PreviewCache) Read(filePath string) ([]byte, error) {
|
||||||
|
var b []byte
|
||||||
|
|
||||||
|
err := pc.store.Get(filePath, &b)
|
||||||
|
if err != nil {
|
||||||
|
return pc.Add(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PreviewCache) Save() error {
|
||||||
|
return pc.store.Close()
|
||||||
|
}
|
11
template.go
Normal file
11
template.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"html/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed index.html
|
||||||
|
var mainTemplateStr string
|
||||||
|
|
||||||
|
var t = template.Must(template.New("main").Parse(mainTemplateStr))
|
99
utils.go
Normal file
99
utils.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errPatternHasSeparator = errors.New("pattern contains path separator")
|
||||||
|
|
||||||
|
func TempFileName(pattern string) (string, error) {
|
||||||
|
dir := os.TempDir()
|
||||||
|
|
||||||
|
prefix, suffix, err := prefixAndSuffix(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return "", &os.PathError{Op: "createtemp", Path: pattern, Err: err}
|
||||||
|
}
|
||||||
|
prefix = joinPath(dir, prefix)
|
||||||
|
|
||||||
|
try := 0
|
||||||
|
for {
|
||||||
|
name := prefix + nextRandom() + suffix
|
||||||
|
if exists, _ := IsFileExists(name); exists {
|
||||||
|
if try++; try < 10000 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return "", &os.PathError{Op: "createtemp", Path: prefix + "*" + suffix, Err: os.ErrExist}
|
||||||
|
}
|
||||||
|
return name, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsFileExists(filePath string) (bool, error) {
|
||||||
|
if _, err := os.Stat(filePath); err == nil {
|
||||||
|
return true, nil
|
||||||
|
} else if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return false, nil
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func prefixAndSuffix(pattern string) (prefix, suffix string, err error) {
|
||||||
|
for i := 0; i < len(pattern); i++ {
|
||||||
|
if os.IsPathSeparator(pattern[i]) {
|
||||||
|
return "", "", errPatternHasSeparator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pos := lastIndex(pattern, '*'); pos != -1 {
|
||||||
|
prefix, suffix = pattern[:pos], pattern[pos+1:]
|
||||||
|
} else {
|
||||||
|
prefix = pattern
|
||||||
|
}
|
||||||
|
return prefix, suffix, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinPath(dir, name string) string {
|
||||||
|
if len(dir) > 0 && os.IsPathSeparator(dir[len(dir)-1]) {
|
||||||
|
return dir + name
|
||||||
|
}
|
||||||
|
return dir + string(os.PathSeparator) + name
|
||||||
|
}
|
||||||
|
|
||||||
|
func nextRandom() string {
|
||||||
|
|
||||||
|
return strconv.Itoa(int(rand.Uint64()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func lastIndex(s string, sep byte) int {
|
||||||
|
for i := len(s) - 1; i >= 0; i-- {
|
||||||
|
if s[i] == sep {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRandomFiles(path string, numberOfFiles int) ([]string, error) { // TODO: оптимизировать алгоритм и предусмотреть папки с менее 4 шт. файлов
|
||||||
|
fileNames, err := filepath.Glob(path + "/*")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
for i := len(fileNames) - 1; i > 0; i-- {
|
||||||
|
j := rand.Intn(i + 1)
|
||||||
|
fileNames[i], fileNames[j] = fileNames[j], fileNames[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
for len(fileNames) < 4 {
|
||||||
|
fileNames = append(fileNames, "black.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileNames[:4], nil
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user