Media-only view

This commit is contained in:
lowcarbdev
2025-12-05 21:40:38 -07:00
parent c8b6b80966
commit a6ed709a99
9 changed files with 792 additions and 6 deletions
+55
View File
@@ -824,6 +824,61 @@ func GetActivityByAddress(userDB *sql.DB, address string, startDate, endDate *ti
return activities, nil
}
// GetMediaByAddress fetches only media items (images/videos) for a specific address
func GetMediaByAddress(userDB *sql.DB, address string, startDate, endDate *time.Time) ([]Message, error) {
query := `
SELECT id, address, COALESCE(body, '') as body, date,
COALESCE(contact_name, '') as contact_name, COALESCE(media_type, '') as media_type,
read, thread_id
FROM messages
WHERE record_type IN (1, 2)
AND media_type IS NOT NULL
AND media_type != ''
AND (media_type LIKE 'image/%' OR media_type LIKE 'video/%')
`
args := []interface{}{}
if address != "" {
query += " AND address = ?"
args = append(args, address)
}
if startDate != nil {
query += " AND date >= ?"
args = append(args, startDate.Unix())
}
if endDate != nil {
query += " AND date <= ?"
args = append(args, endDate.Unix())
}
query += " ORDER BY date DESC"
rows, err := userDB.Query(query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var mediaItems []Message
for rows.Next() {
var m Message
var dateUnix int64
var readInt int64
err := rows.Scan(&m.ID, &m.Address, &m.Body, &dateUnix, &m.ContactName, &m.MediaType, &readInt, &m.ThreadID)
if err != nil {
return nil, err
}
m.Date = time.Unix(dateUnix, 0)
m.Read = readInt == 1
mediaItems = append(mediaItems, m)
}
return mediaItems, nil
}
func GetMessageMedia(userDB *sql.DB, messageID string) ([]byte, string, error) {
query := `
SELECT COALESCE(media_data, ''), COALESCE(media_type, '')
+101 -2
View File
@@ -8,6 +8,7 @@ import (
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/labstack/echo/v4"
@@ -240,6 +241,44 @@ func HandleMessages(c echo.Context) error {
return c.JSON(http.StatusOK, messages)
}
// HandleMediaItems returns only media (images/videos) for a conversation
func HandleMediaItems(c echo.Context) error {
userDB, err := getUserDB(c)
if err != nil {
slog.Error("Error getting user database", "error", err)
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to get user database",
})
}
address := c.QueryParam("address")
var startDate, endDate *time.Time
if startStr := c.QueryParam("start"); startStr != "" {
t, err := time.Parse(time.RFC3339, startStr)
if err == nil {
startDate = &t
}
}
if endStr := c.QueryParam("end"); endStr != "" {
t, err := time.Parse(time.RFC3339, endStr)
if err == nil {
endDate = &t
}
}
mediaItems, err := GetMediaByAddress(userDB, address, startDate, endDate)
if err != nil {
slog.Error("Error getting media items", "error", err)
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to get media items",
})
}
return c.JSON(http.StatusOK, mediaItems)
}
func HandleActivity(c echo.Context) error {
userDB, err := getUserDB(c)
if err != nil {
@@ -395,6 +434,9 @@ func HandleMedia(c echo.Context) error {
})
}
// Check if transcode is requested (for videos that browser can't play)
forceTranscode := c.QueryParam("transcode") == "true"
// Fetch media from database
media, contentType, err := GetMessageMedia(userDB, messageID)
if err != nil {
@@ -410,11 +452,68 @@ func HandleMedia(c echo.Context) error {
})
}
// If transcode is requested and this is a video, try to convert it
if forceTranscode && strings.HasPrefix(contentType, "video/") {
slog.Info("Transcode requested for video", "messageID", messageID, "contentType", contentType)
convertedData, err := convertVideoToMP4(media)
if err != nil {
slog.Error("Failed to transcode video", "messageID", messageID, "error", err)
// Continue with original video if conversion fails
} else {
slog.Info("Successfully transcoded video", "messageID", messageID)
media = convertedData
contentType = "video/mp4"
}
}
slog.Debug("Serving media", "messageID", messageID, "contentType", contentType, "size", len(media))
// Set appropriate headers
c.Response().Header().Set("Cache-Control", "public, max-age=31536000") // Cache for 1 year
c.Response().Header().Set("Content-Length", fmt.Sprintf("%d", len(media)))
c.Response().Header().Set("Accept-Ranges", "bytes") // Enable range requests for video streaming
// Write binary data with proper content type
// Check for Range header (needed for video playback)
rangeHeader := c.Request().Header.Get("Range")
if rangeHeader != "" {
contentLength := int64(len(media))
var start, end int64 = 0, contentLength - 1
slog.Debug("Range request received", "messageID", messageID, "range", rangeHeader, "contentType", contentType, "contentLength", contentLength)
// Parse range header (e.g., "bytes=0-1023" or "bytes=0-")
n, _ := fmt.Sscanf(rangeHeader, "bytes=%d-%d", &start, &end)
if n == 1 {
// Only start was specified (e.g., "bytes=0-")
end = contentLength - 1
} else if n == 0 {
// Invalid range, return 416 Range Not Satisfiable
slog.Warn("Invalid range header", "range", rangeHeader)
c.Response().Header().Set("Content-Range", fmt.Sprintf("bytes */%d", contentLength))
return c.NoContent(http.StatusRequestedRangeNotSatisfiable)
}
// Ensure valid range
if start < 0 || start >= contentLength || end >= contentLength || start > end {
slog.Warn("Range out of bounds", "start", start, "end", end, "contentLength", contentLength)
c.Response().Header().Set("Content-Range", fmt.Sprintf("bytes */%d", contentLength))
return c.NoContent(http.StatusRequestedRangeNotSatisfiable)
}
slog.Debug("Serving range", "start", start, "end", end, "size", end-start+1)
// Set response headers for partial content
c.Response().Header().Set("Content-Type", contentType)
c.Response().Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, contentLength))
c.Response().Header().Set("Content-Length", fmt.Sprintf("%d", end-start+1))
c.Response().WriteHeader(http.StatusPartialContent)
// Write the requested range
_, writeErr := c.Response().Write(media[start : end+1])
return writeErr
}
// No range request - serve full content
c.Response().Header().Set("Content-Length", fmt.Sprintf("%d", len(media)))
return c.Blob(http.StatusOK, contentType, media)
}
+1 -2
View File
@@ -1,6 +1,5 @@
package internal
import (
"bytes"
"database/sql"
@@ -440,10 +439,10 @@ func isHEICContentType(contentType string) bool {
// needsVideoConversion checks if a video format needs conversion for browser compatibility
func needsVideoConversion(contentType string) bool {
ct := strings.ToLower(strings.TrimSpace(contentType))
// 3GP, 3G2, and other old mobile formats that browsers don't support
unsupportedFormats := []string{
"3gpp", "3gp", "3g2", "3gpp2",
"video/3gpp", "video/3gp", "video/3gpp2", "video/3g2",
"video/x-matroska", // MKV container (may have various codecs)
}
for _, format := range unsupportedFormats {