Media-only view
This commit is contained in:
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user