Add video support

This commit is contained in:
Dominic Ferrando
2025-04-22 00:06:11 -04:00
parent fac517b28d
commit 741ac24e3e
15 changed files with 236 additions and 102 deletions
+11 -11
View File
@@ -93,7 +93,7 @@ func GetThread(db *sql.DB, threadId int) (Thread, error) {
func GetPosts(db *sql.DB, threadId int) ([]Post, error) {
rows, err := db.Query(`
SELECT id, thread_id, author, body, created_at, media_path, ip_hash, number
SELECT id, thread_id, author, body, created_at, media_path, ip_hash, number, thumb_path
FROM posts
WHERE thread_id = ?`, threadId)
@@ -105,7 +105,7 @@ func GetPosts(db *sql.DB, threadId int) ([]Post, error) {
var result []Post
for rows.Next() {
var p Post
err := rows.Scan(&p.Id, &p.ThreadId, &p.Author, &p.Body, &p.CreatedAt, &p.MediaPath, &p.IpHash, &p.Number)
err := rows.Scan(&p.Id, &p.ThreadId, &p.Author, &p.Body, &p.CreatedAt, &p.MediaPath, &p.IpHash, &p.Number, &p.ThumbPath)
if err != nil {
return nil, err
}
@@ -117,20 +117,20 @@ func GetPosts(db *sql.DB, threadId int) ([]Post, error) {
func GetOriginalPost(db *sql.DB, threadId int) (Post, error) {
row := db.QueryRow(`
SELECT id, thread_id, author, body, created_at, media_path, ip_hash, number
SELECT id, thread_id, author, body, created_at, media_path, ip_hash, number, thumb_path
FROM posts
WHERE thread_id = ?
ORDER BY created_at ASC LIMIT 1`, threadId)
var r Post
err := row.Scan(&r.Id, &r.ThreadId, &r.Author, &r.Body, &r.CreatedAt, &r.MediaPath, &r.IpHash, &r.Number)
err := row.Scan(&r.Id, &r.ThreadId, &r.Author, &r.Body, &r.CreatedAt, &r.MediaPath, &r.IpHash, &r.Number, &r.ThumbPath)
if err != nil {
return Post{}, err
}
return r, row.Err()
}
func PutThread(db *sql.DB, boardSlug string, subject string, body string, mediaPath string, ip_hash string) error {
func PutThread(db *sql.DB, boardSlug string, subject string, body string, mediaPath string, thumbPath string, ip_hash string) error {
tx, err := db.Begin()
if err != nil {
return err
@@ -150,7 +150,7 @@ func PutThread(db *sql.DB, boardSlug string, subject string, body string, mediaP
return err
}
if err := PutPost(tx, boardSlug, int(threadId), body, mediaPath, ip_hash); err != nil {
if err := PutPost(tx, boardSlug, int(threadId), body, mediaPath, thumbPath, ip_hash); err != nil {
return err
}
@@ -188,7 +188,7 @@ func PutThread(db *sql.DB, boardSlug string, subject string, body string, mediaP
return nil
}
func PutPost(db Queryer, boardSlug string, threadId int, body string, mediaPath string, ip_hash string) error {
func PutPost(db Queryer, boardSlug string, threadId int, body string, mediaPath string, thumbPath string, ip_hash string) error {
row := db.QueryRow(`
SELECT MAX(p.number)
FROM posts p
@@ -206,8 +206,8 @@ func PutPost(db Queryer, boardSlug string, threadId int, body string, mediaPath
}
_, err := db.Exec(`
INSERT INTO posts (thread_id, body, media_path, ip_hash, number)
VALUES (?, ?, ?, ?, ?)`, threadId, body, mediaPath, ip_hash, newPostNumber)
INSERT INTO posts (thread_id, body, media_path, ip_hash, number, thumb_path)
VALUES (?, ?, ?, ?, ?, ?)`, threadId, body, mediaPath, ip_hash, newPostNumber, thumbPath)
if err != nil {
return err
}
@@ -241,10 +241,10 @@ func DeleteThread(db Queryer, threadId int) error {
}
for _, pruneMediaPath := range pruneMediaPaths {
if err := os.Remove(path.Join(util.POST_IMG_FULL_PATH, pruneMediaPath)); err != nil && !os.IsNotExist(err) {
if err := os.Remove(path.Join(util.POST_MEDIA_FULL_PATH, pruneMediaPath)); err != nil && !os.IsNotExist(err) {
return err
}
if err := os.Remove(path.Join(util.POST_IMG_THUMB_PATH, pruneMediaPath)); err != nil && !os.IsNotExist(err) {
if err := os.Remove(path.Join(util.POST_MEDIA_THUMB_PATH, pruneMediaPath)); err != nil && !os.IsNotExist(err) {
return err
}
}
+1
View File
@@ -24,6 +24,7 @@ type Post struct {
Body string
CreatedAt time.Time
MediaPath string
ThumbPath string
IpHash string
Number int
}
+2 -1
View File
@@ -23,11 +23,12 @@ CREATE TABLE IF NOT EXISTS threads (
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
thread_id INTEGER NOT NULL,
number INTEGER NOT NULL,
number INTEGER NOT NULL ,
author TEXT DEFAULT 'Anonymous',
body TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
media_path TEXT NOT NULL DEFAULT '',
thumb_path TEXT NOT NULL DEFAULT '',
ip_hash TEXT NOT NULL,
FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE
);
+113 -45
View File
@@ -7,7 +7,9 @@ import (
"io"
"log"
"mime/multipart"
"net/http"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
@@ -17,10 +19,12 @@ import (
// 10 MB memory limit
const FILE_MEM_LIMIT int64 = 10 << 20
const POST_IMG_FULL_PATH = "web/static/img/posts/full"
const POST_IMG_THUMB_PATH = "web/static/img/posts/thumb"
const POST_MEDIA_FULL_PATH = "web/static/media/posts/full"
const POST_MEDIA_THUMB_PATH = "web/static/media/posts/thumb"
const MAX_THREAD_COUNT = 50
var SUPPORTED_VID_FORMATS = []string{".mp4", ".webm", ".ogg"}
func EnrichPost(body string) string {
var b strings.Builder
for _, rawLine := range strings.Split(body, "\n") {
@@ -58,86 +62,142 @@ func EnrichPost(body string) string {
return b.String()
}
func SavePostFile(file *multipart.File, filename string) error {
dstPathFull := filepath.Join(POST_IMG_FULL_PATH, filename)
dstPathThumb := filepath.Join(POST_IMG_THUMB_PATH, filename)
func SavePostFile(file multipart.File, fileName string) (error, string, string) {
// read file type
buffer := make([]byte, 512)
file.Read(buffer)
file.Seek(0, 0)
fileType := http.DetectContentType(buffer)
isFileVideo := strings.HasPrefix(fileType, "video/")
fileExt := strings.ToLower(filepath.Ext(fileName))
// FULL
if err := os.MkdirAll(POST_IMG_FULL_PATH, 0755); err != nil {
if err := os.MkdirAll(POST_MEDIA_FULL_PATH, 0755); err != nil {
log.Printf("MkdirAll (full): %v", err)
return err
return err, "", ""
}
dstPathFull := filepath.Join(POST_MEDIA_FULL_PATH, fileName)
dstFull, err := os.Create(dstPathFull)
if err != nil {
log.Printf("os.Create (full): %v", err)
return err
return err, "", ""
}
defer dstFull.Close()
if _, err := io.Copy(dstFull, *file); err != nil {
if _, err := io.Copy(dstFull, file); err != nil {
log.Printf("io.Copy (full): %v", err)
return err
return err, "", ""
}
if _, err := (*file).Seek(0, 0); err != nil { // rewind file
if _, err := (file).Seek(0, 0); err != nil { // rewind file
log.Printf("seek file (thumb): %v", err)
return err
return err, "", ""
}
// THUMBNAIL
if err := os.MkdirAll(POST_IMG_THUMB_PATH, 0755); err != nil {
dstPathThumb := filepath.Join(POST_MEDIA_THUMB_PATH, fileName)
if err := os.MkdirAll(POST_MEDIA_THUMB_PATH, 0755); err != nil {
log.Printf("MkdirAll (thumb): %v", err)
return err
return err, "", ""
}
img, _, err := image.Decode(*file)
if err != nil {
log.Printf("image.Decode: %v", err)
return err
var thumbFileName string
if isFileVideo {
fileNameNoExt := strings.TrimSuffix(fileName, fileExt)
thumbFileName = fileNameNoExt + ".jpg"
inputPath := filepath.Join(POST_MEDIA_FULL_PATH, fileName)
outputPath := filepath.Join(POST_MEDIA_THUMB_PATH, thumbFileName)
cmd := exec.Command(
"ffmpeg",
"-i", inputPath,
"-ss", "00:00:01.000",
"-vframes", "1",
"-vf", "scale=300:-1",
outputPath,
)
if err := cmd.Run(); err != nil {
log.Printf("ffmpeg error: %v", err)
return err, "", ""
}
} else {
thumbFileName = fileName
img, _, err := image.Decode(file)
if err != nil {
log.Printf("image.Decode: %v", err)
return err, "", ""
}
var thumb image.Image
if img.Bounds().Dx() > 300 {
thumb = imaging.Resize(img, 300, 0, imaging.Lanczos)
} else {
thumb = img
}
if err = imaging.Save(thumb, dstPathThumb); err != nil {
log.Printf("imaging.Save: %v", err)
return err, "", ""
}
}
thumb := imaging.Resize(img, 300, 0, imaging.Lanczos)
if err = imaging.Save(thumb, dstPathThumb); err != nil {
log.Printf("imaging.Save: %v", err)
return err
}
return nil
return nil, fileName, thumbFileName
}
type PostImageInfo struct {
Size int64
Height int
Width int
type PostFileInfo struct {
Size int64
Height int
Width int
IsVideo bool
}
func GetPostImageInfo(mediaPath string) PostImageInfo {
var result PostImageInfo
func GetPostFileInfo(mediaPath string) PostFileInfo {
var result PostFileInfo
imagePath := path.Join(POST_IMG_FULL_PATH, mediaPath)
filePath := path.Join(POST_MEDIA_FULL_PATH, mediaPath)
fileInfo, err := os.Stat(imagePath)
fileInfo, err := os.Stat(filePath)
if err != nil {
log.Printf("Failed to get image file info at %s: %v", mediaPath, err)
return PostImageInfo{}
log.Printf("Failed to get file info at %s: %v", mediaPath, err)
return PostFileInfo{}
}
file, err := os.Open(imagePath)
file, err := os.Open(filePath)
if err != nil {
log.Printf("Failed to open image at %s: %v", mediaPath, err)
return PostImageInfo{}
log.Printf("Failed to open file at %s: %v", mediaPath, err)
return PostFileInfo{}
}
defer file.Close()
cfg, _, err := image.DecodeConfig(file)
if err != nil {
log.Printf("Failed to decode image at %s: %v", mediaPath, err)
return PostImageInfo{}
}
// read file type
buffer := make([]byte, 512)
file.Read(buffer)
file.Seek(0, 0)
fileType := http.DetectContentType(buffer)
isFileVideo := strings.HasPrefix(fileType, "video/")
result.Size = fileInfo.Size()
result.Width = cfg.Width
result.Height = cfg.Height
if !isFileVideo {
cfg, _, err := image.DecodeConfig(file)
if err != nil {
log.Printf("Failed to decode image at %s: %v", mediaPath, err)
return PostFileInfo{}
}
result.Width = cfg.Width
result.Height = cfg.Height
result.IsVideo = false
} else {
result.IsVideo = true
}
return result
}
@@ -163,3 +223,11 @@ func FormatBytes(bytes int64) string {
return fmt.Sprintf("%d B", bytes)
}
}
func FormatFileInfo(fileInfo PostFileInfo) string {
humanSize := FormatBytes(fileInfo.Size)
if fileInfo.IsVideo {
return fmt.Sprintf("(%s)", humanSize)
}
return fmt.Sprintf("(%s, %dx%d)", humanSize, fileInfo.Width, fileInfo.Height)
}