Skip to content
Snippets Groups Projects
Commit ea173167 authored by Travis Ralston's avatar Travis Ralston
Browse files

Update media info API

parent bc5e22b1
No related branches found
No related tags found
No related merge requests found
package unstable package unstable
import ( import (
"bytes"
"database/sql"
"io"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/disintegration/imaging"
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
"github.com/sirupsen/logrus"
"github.com/turt2live/matrix-media-repo/api/_apimeta" "github.com/turt2live/matrix-media-repo/api/_apimeta"
"github.com/turt2live/matrix-media-repo/api/_responses" "github.com/turt2live/matrix-media-repo/api/_responses"
"github.com/turt2live/matrix-media-repo/api/_routers" "github.com/turt2live/matrix-media-repo/api/_routers"
"github.com/turt2live/matrix-media-repo/util"
"github.com/turt2live/matrix-media-repo/util/stream_util"
"github.com/disintegration/imaging"
"github.com/sirupsen/logrus"
"github.com/turt2live/matrix-media-repo/common" "github.com/turt2live/matrix-media-repo/common"
"github.com/turt2live/matrix-media-repo/common/rcontext" "github.com/turt2live/matrix-media-repo/common/rcontext"
"github.com/turt2live/matrix-media-repo/controllers/download_controller" "github.com/turt2live/matrix-media-repo/database"
"github.com/turt2live/matrix-media-repo/storage" "github.com/turt2live/matrix-media-repo/pipelines/pipeline_download"
"github.com/turt2live/matrix-media-repo/thumbnailing" "github.com/turt2live/matrix-media-repo/thumbnailing"
"github.com/turt2live/matrix-media-repo/thumbnailing/i" "github.com/turt2live/matrix-media-repo/thumbnailing/i"
"github.com/turt2live/matrix-media-repo/util/util_byte_seeker" "github.com/turt2live/matrix-media-repo/util"
) )
type mediaInfoHashes struct { type mediaInfoHashes struct {
...@@ -31,9 +26,11 @@ type mediaInfoHashes struct { ...@@ -31,9 +26,11 @@ type mediaInfoHashes struct {
} }
type mediaInfoThumbnail struct { type mediaInfoThumbnail struct {
Width int `json:"width"` Width int `json:"width"`
Height int `json:"height"` Height int `json:"height"`
Ready bool `json:"ready"` Ready bool `json:"ready"`
ContentType string `json:"content_type,omitempty"`
SizeBytes int64 `json:"size,omitempty"`
} }
type MediaInfoResponse struct { type MediaInfoResponse struct {
...@@ -79,47 +76,67 @@ func MediaInfo(r *http.Request, rctx rcontext.RequestContext, user _apimeta.User ...@@ -79,47 +76,67 @@ func MediaInfo(r *http.Request, rctx rcontext.RequestContext, user _apimeta.User
return _responses.MediaBlocked() return _responses.MediaBlocked()
} }
streamedMedia, err := download_controller.GetMedia(server, mediaId, downloadRemote, true, rctx) record, stream, err := pipeline_download.Execute(rctx, server, mediaId, pipeline_download.DownloadOpts{
FetchRemoteIfNeeded: downloadRemote,
StartByte: -1,
EndByte: -1,
BlockForReadUntil: 30 * time.Second,
RecordOnly: false,
})
// Error handling copied from download endpoint
if err != nil { if err != nil {
if err == common.ErrMediaNotFound { if err == common.ErrMediaNotFound {
return _responses.NotFoundError() return _responses.NotFoundError()
} else if err == common.ErrMediaTooLarge { } else if err == common.ErrMediaTooLarge {
return _responses.RequestTooLarge() return _responses.RequestTooLarge()
} else if err == common.ErrMediaQuarantined { } else if err == common.ErrMediaQuarantined {
return _responses.NotFoundError() // We lie for security rctx.Log.Debug("Quarantined media accessed. Has stream? ", stream != nil)
if stream != nil {
return _responses.MakeQuarantinedImageResponse(stream)
} else {
return _responses.NotFoundError() // We lie for security
}
} else if err == common.ErrMediaNotYetUploaded {
return _responses.NotYetUploaded()
} }
rctx.Log.Error("Unexpected error locating media: ", err) rctx.Log.Error("Unexpected error locating media: ", err)
sentry.CaptureException(err) sentry.CaptureException(err)
return _responses.InternalServerError("Unexpected Error") return _responses.InternalServerError("Unexpected Error")
} }
defer stream_util.DumpAndCloseStream(streamedMedia.Stream)
b, err := io.ReadAll(streamedMedia.Stream)
if err != nil {
rctx.Log.Error("Unexpected error processing media: ", err)
sentry.CaptureException(err)
return _responses.InternalServerError("Unexpected Error")
}
response := &MediaInfoResponse{ response := &MediaInfoResponse{
ContentUri: streamedMedia.KnownMedia.MxcUri(), ContentUri: util.MxcUri(record.Origin, record.MediaId),
ContentType: streamedMedia.KnownMedia.ContentType, ContentType: record.ContentType,
Size: streamedMedia.KnownMedia.SizeBytes, Size: record.SizeBytes,
Hashes: mediaInfoHashes{ Hashes: mediaInfoHashes{
Sha256: streamedMedia.KnownMedia.Sha256Hash, Sha256: record.Sha256Hash,
}, },
} }
img, err := imaging.Decode(bytes.NewBuffer(b)) if strings.HasPrefix(response.ContentType, "image/") {
if err == nil { img, err := imaging.Decode(stream)
response.Width = img.Bounds().Max.X if err == nil {
response.Height = img.Bounds().Max.Y response.Width = img.Bounds().Max.X
response.Height = img.Bounds().Max.Y
}
} else if strings.HasPrefix(response.ContentType, "audio/") {
generator, reconstructed, err := thumbnailing.GetGenerator(stream, response.ContentType, false)
if err == nil {
if audiogenerator, ok := generator.(i.AudioGenerator); ok {
audioInfo, err := audiogenerator.GetAudioData(reconstructed, 768, rctx)
if err == nil {
response.KeySamples = audioInfo.KeySamples
response.NumChannels = audioInfo.Channels
response.DurationSeconds = audioInfo.Duration.Seconds()
response.NumTotalSamples = audioInfo.TotalSamples
}
}
}
} }
thumbsDb := storage.GetDatabase().GetThumbnailStore(rctx) thumbs, err := database.GetInstance().Thumbnails.Prepare(rctx).GetForMedia(record.Origin, record.MediaId)
thumbs, err := thumbsDb.GetAllForMedia(streamedMedia.KnownMedia.Origin, streamedMedia.KnownMedia.MediaId) if err != nil {
if err != nil && err != sql.ErrNoRows { rctx.Log.Error("Unexpected error locating media thumbnails: ", err)
rctx.Log.Error("Unexpected error locating media: ", err)
sentry.CaptureException(err) sentry.CaptureException(err)
return _responses.InternalServerError("Unexpected Error") return _responses.InternalServerError("Unexpected Error")
} }
...@@ -128,28 +145,15 @@ func MediaInfo(r *http.Request, rctx rcontext.RequestContext, user _apimeta.User ...@@ -128,28 +145,15 @@ func MediaInfo(r *http.Request, rctx rcontext.RequestContext, user _apimeta.User
infoThumbs := make([]*mediaInfoThumbnail, 0) infoThumbs := make([]*mediaInfoThumbnail, 0)
for _, thumb := range thumbs { for _, thumb := range thumbs {
infoThumbs = append(infoThumbs, &mediaInfoThumbnail{ infoThumbs = append(infoThumbs, &mediaInfoThumbnail{
Width: thumb.Width, Width: thumb.Width,
Height: thumb.Height, Height: thumb.Height,
Ready: true, Ready: true,
ContentType: thumb.ContentType,
SizeBytes: thumb.SizeBytes,
}) })
} }
response.Thumbnails = infoThumbs response.Thumbnails = infoThumbs
} }
if strings.HasPrefix(response.ContentType, "audio/") {
generator, reconstructed, err := thumbnailing.GetGenerator(util_byte_seeker.NewByteSeeker(b), response.ContentType, false)
if err == nil {
if audiogenerator, ok := generator.(i.AudioGenerator); ok {
audioInfo, err := audiogenerator.GetAudioData(reconstructed, 768, rctx)
if err == nil {
response.KeySamples = audioInfo.KeySamples
response.NumChannels = audioInfo.Channels
response.DurationSeconds = audioInfo.Duration.Seconds()
response.NumTotalSamples = audioInfo.TotalSamples
}
}
}
}
return response return response
} }
...@@ -28,11 +28,13 @@ type DbThumbnail struct { ...@@ -28,11 +28,13 @@ type DbThumbnail struct {
const selectThumbnailByParams = "SELECT origin, media_id, content_type, width, height, method, animated, sha256_hash, size_bytes, creation_ts, datastore_id, location FROM thumbnails WHERE origin = $1 AND media_id = $2 AND width = $3 AND height = $4 AND method = $5 AND animated = $6;" const selectThumbnailByParams = "SELECT origin, media_id, content_type, width, height, method, animated, sha256_hash, size_bytes, creation_ts, datastore_id, location FROM thumbnails WHERE origin = $1 AND media_id = $2 AND width = $3 AND height = $4 AND method = $5 AND animated = $6;"
const insertThumbnail = "INSERT INTO thumbnails (origin, media_id, content_type, width, height, method, animated, sha256_hash, size_bytes, creation_ts, datastore_id, location) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12);" const insertThumbnail = "INSERT INTO thumbnails (origin, media_id, content_type, width, height, method, animated, sha256_hash, size_bytes, creation_ts, datastore_id, location) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12);"
const selectThumbnailByLocationExists = "SELECT TRUE FROM thumbnails WHERE datastore_id = $1 AND location = $2 LIMIT 1;" const selectThumbnailByLocationExists = "SELECT TRUE FROM thumbnails WHERE datastore_id = $1 AND location = $2 LIMIT 1;"
const selectThumbnailsForMedia = "SELECT origin, media_id, content_type, width, height, method, animated, sha256_hash, size_bytes, creation_ts, datastore_id, location FROM thumbnails WHERE origin = $1 AND media_id = $2;"
type thumbnailsTableStatements struct { type thumbnailsTableStatements struct {
selectThumbnailByParams *sql.Stmt selectThumbnailByParams *sql.Stmt
insertThumbnail *sql.Stmt insertThumbnail *sql.Stmt
selectThumbnailByLocationExists *sql.Stmt selectThumbnailByLocationExists *sql.Stmt
selectThumbnailsForMedia *sql.Stmt
} }
type thumbnailsTableWithContext struct { type thumbnailsTableWithContext struct {
...@@ -53,6 +55,9 @@ func prepareThumbnailsTables(db *sql.DB) (*thumbnailsTableStatements, error) { ...@@ -53,6 +55,9 @@ func prepareThumbnailsTables(db *sql.DB) (*thumbnailsTableStatements, error) {
if stmts.selectThumbnailByLocationExists, err = db.Prepare(selectThumbnailByLocationExists); err != nil { if stmts.selectThumbnailByLocationExists, err = db.Prepare(selectThumbnailByLocationExists); err != nil {
return nil, errors.New("error preparing selectThumbnailByLocationExists: " + err.Error()) return nil, errors.New("error preparing selectThumbnailByLocationExists: " + err.Error())
} }
if stmts.selectThumbnailsForMedia, err = db.Prepare(selectThumbnailsForMedia); err != nil {
return nil, errors.New("error preparing selectThumbnailsForMedia: " + err.Error())
}
return stmts, nil return stmts, nil
} }
...@@ -75,6 +80,25 @@ func (s *thumbnailsTableWithContext) GetByParams(origin string, mediaId string, ...@@ -75,6 +80,25 @@ func (s *thumbnailsTableWithContext) GetByParams(origin string, mediaId string,
return val, err return val, err
} }
func (s *thumbnailsTableWithContext) GetForMedia(origin string, mediaId string) ([]*DbThumbnail, error) {
results := make([]*DbThumbnail, 0)
rows, err := s.statements.selectThumbnailsForMedia.QueryContext(s.ctx, origin, mediaId)
if err != nil {
if err == sql.ErrNoRows {
return results, nil
}
return nil, err
}
for rows.Next() {
val := &DbThumbnail{Locatable: &Locatable{}}
if err = rows.Scan(&val.Origin, &val.MediaId, &val.ContentType, &val.Width, &val.Height, &val.Method, &val.Animated, &val.Sha256Hash, &val.SizeBytes, &val.CreationTs, &val.DatastoreId, &val.Location); err != nil {
return nil, err
}
results = append(results, val)
}
return results, nil
}
func (s *thumbnailsTableWithContext) Insert(record *DbThumbnail) error { func (s *thumbnailsTableWithContext) Insert(record *DbThumbnail) error {
_, err := s.statements.insertThumbnail.ExecContext(s.ctx, record.Origin, record.MediaId, record.ContentType, record.Width, record.Height, record.Method, record.Animated, record.Sha256Hash, record.SizeBytes, record.CreationTs, record.DatastoreId, record.Location) _, err := s.statements.insertThumbnail.ExecContext(s.ctx, record.Origin, record.MediaId, record.ContentType, record.Width, record.Height, record.Method, record.Animated, record.Sha256Hash, record.SizeBytes, record.CreationTs, record.DatastoreId, record.Location)
return err return err
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment