1
Fork 0

Add video metadata

This commit is contained in:
viktorstrate 2020-07-12 14:17:49 +02:00
parent 035aabb852
commit 0e9d37ca77
14 changed files with 798 additions and 33 deletions

View File

@ -9,3 +9,9 @@ ALTER TABLE photo_url CHANGE COLUMN media_name photo_name varchar(512) NOT NULL;
ALTER TABLE share_token CHANGE COLUMN media_id photo_id int;
ALTER TABLE photo DROP COLUMN media_type;
ALTER TABLE photo
DROP FOREIGN KEY photo_ibfk_3,
DROP COLUMN video_metadata_id;
DROP TABLE video_metadata;

View File

@ -4,8 +4,27 @@ ALTER TABLE photo_url RENAME TO media_url;
ALTER TABLE photo_exif RENAME TO media_exif;
ALTER TABLE media CHANGE COLUMN photo_id media_id int NOT NULL AUTO_INCREMENT;
ALTER TABLE media_url CHANGE COLUMN photo_id media_id int NOT NULL;
ALTER TABLE media_url CHANGE COLUMN photo_name media_name varchar(512) NOT NULL;
ALTER TABLE media_url
CHANGE COLUMN photo_id media_id int NOT NULL,
CHANGE COLUMN photo_name media_name varchar(512) NOT NULL;
ALTER TABLE share_token CHANGE COLUMN photo_id media_id int;
ALTER TABLE media ADD COLUMN media_type varchar(64) NOT NULL DEFAULT "photo";
CREATE TABLE video_metadata (
metadata_id int NOT NULL AUTO_INCREMENT,
width int(6) NOT NULL,
height int(6) NOT NULL,
duration double NOT NULL,
codec varchar(128),
framerate double,
bitrate int(24),
color_profile varchar(128),
audio varchar(128),
PRIMARY KEY (metadata_id)
);
ALTER TABLE media
ADD COLUMN media_type varchar(64) NOT NULL DEFAULT "photo",
ADD COLUMN video_metadata_id int,
ADD FOREIGN KEY (video_metadata_id) REFERENCES video_metadata(metadata_id);

View File

@ -25,7 +25,7 @@ require (
github.com/urfave/cli v1.22.4 // indirect
github.com/urfave/cli/v2 v2.2.0 // indirect
github.com/vektah/dataloaden v0.3.0 // indirect
github.com/vektah/gqlparser v1.3.1
github.com/vektah/gqlparser v1.3.1 // indirect
github.com/vektah/gqlparser/v2 v2.0.1
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0
github.com/xor-gate/goexif2 v1.1.0

View File

@ -165,6 +165,7 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View File

@ -30,6 +30,8 @@ models:
model: github.com/viktorstrate/photoview/api/graphql/models.MediaURL
MediaEXIF:
model: github.com/viktorstrate/photoview/api/graphql/models.MediaEXIF
VideoMetadata:
model: github.com/viktorstrate/photoview/api/graphql/models.VideoMetadata
Album:
model: github.com/viktorstrate/photoview/api/graphql/models.Album
ShareToken:

View File

@ -71,18 +71,19 @@ type ComplexityRoot struct {
}
Media struct {
Album func(childComplexity int) int
Downloads func(childComplexity int) int
Exif func(childComplexity int) int
Favorite func(childComplexity int) int
HighRes func(childComplexity int) int
ID func(childComplexity int) int
Path func(childComplexity int) int
Shares func(childComplexity int) int
Thumbnail func(childComplexity int) int
Title func(childComplexity int) int
Type func(childComplexity int) int
VideoWeb func(childComplexity int) int
Album func(childComplexity int) int
Downloads func(childComplexity int) int
Exif func(childComplexity int) int
Favorite func(childComplexity int) int
HighRes func(childComplexity int) int
ID func(childComplexity int) int
Path func(childComplexity int) int
Shares func(childComplexity int) int
Thumbnail func(childComplexity int) int
Title func(childComplexity int) int
Type func(childComplexity int) int
VideoMetadata func(childComplexity int) int
VideoWeb func(childComplexity int) int
}
MediaDownload struct {
@ -190,6 +191,19 @@ type ComplexityRoot struct {
RootPath func(childComplexity int) int
Username func(childComplexity int) int
}
VideoMetadata struct {
Audio func(childComplexity int) int
Bitrate func(childComplexity int) int
Codec func(childComplexity int) int
ColorProfile func(childComplexity int) int
Duration func(childComplexity int) int
Framerate func(childComplexity int) int
Height func(childComplexity int) int
ID func(childComplexity int) int
Media func(childComplexity int) int
Width func(childComplexity int) int
}
}
type AlbumResolver interface {
@ -208,6 +222,7 @@ type MediaResolver interface {
VideoWeb(ctx context.Context, obj *models.Media) (*models.MediaURL, error)
Album(ctx context.Context, obj *models.Media) (*models.Album, error)
Exif(ctx context.Context, obj *models.Media) (*models.MediaEXIF, error)
VideoMetadata(ctx context.Context, obj *models.Media) (*models.VideoMetadata, error)
Shares(ctx context.Context, obj *models.Media) ([]*models.ShareToken, error)
Downloads(ctx context.Context, obj *models.Media) ([]*models.MediaDownload, error)
@ -443,6 +458,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Media.Type(childComplexity), true
case "Media.videoMetadata":
if e.complexity.Media.VideoMetadata == nil {
break
}
return e.complexity.Media.VideoMetadata(childComplexity), true
case "Media.videoWeb":
if e.complexity.Media.VideoWeb == nil {
break
@ -1040,6 +1062,76 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.User.Username(childComplexity), true
case "VideoMetadata.audio":
if e.complexity.VideoMetadata.Audio == nil {
break
}
return e.complexity.VideoMetadata.Audio(childComplexity), true
case "VideoMetadata.bitrate":
if e.complexity.VideoMetadata.Bitrate == nil {
break
}
return e.complexity.VideoMetadata.Bitrate(childComplexity), true
case "VideoMetadata.codec":
if e.complexity.VideoMetadata.Codec == nil {
break
}
return e.complexity.VideoMetadata.Codec(childComplexity), true
case "VideoMetadata.colorProfile":
if e.complexity.VideoMetadata.ColorProfile == nil {
break
}
return e.complexity.VideoMetadata.ColorProfile(childComplexity), true
case "VideoMetadata.duration":
if e.complexity.VideoMetadata.Duration == nil {
break
}
return e.complexity.VideoMetadata.Duration(childComplexity), true
case "VideoMetadata.framerate":
if e.complexity.VideoMetadata.Framerate == nil {
break
}
return e.complexity.VideoMetadata.Framerate(childComplexity), true
case "VideoMetadata.height":
if e.complexity.VideoMetadata.Height == nil {
break
}
return e.complexity.VideoMetadata.Height(childComplexity), true
case "VideoMetadata.id":
if e.complexity.VideoMetadata.ID == nil {
break
}
return e.complexity.VideoMetadata.ID(childComplexity), true
case "VideoMetadata.media":
if e.complexity.VideoMetadata.Media == nil {
break
}
return e.complexity.VideoMetadata.Media(childComplexity), true
case "VideoMetadata.width":
if e.complexity.VideoMetadata.Width == nil {
break
}
return e.complexity.VideoMetadata.Width(childComplexity), true
}
return 0, false
}
@ -1340,6 +1432,7 @@ type Media {
"The album that holds the media"
album: Album!
exif: MediaEXIF
videoMetadata: VideoMetadata
favorite: Boolean!
type: MediaType!
@ -1372,6 +1465,19 @@ type MediaEXIF {
exposureProgram: Int
}
type VideoMetadata {
id: Int!
media: Media!
width: Int!
height: Int!
duration: Float!
codec: String
framerate: Float
bitrate: Int
colorProfile: String
audio: String
}
type SearchResult {
query: String!
albums: [Album!]!
@ -2642,6 +2748,37 @@ func (ec *executionContext) _Media_exif(ctx context.Context, field graphql.Colle
return ec.marshalOMediaEXIF2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaEXIF(ctx, field.Selections, res)
}
func (ec *executionContext) _Media_videoMetadata(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Media",
Field: field,
Args: nil,
IsMethod: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Media().VideoMetadata(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*models.VideoMetadata)
fc.Result = res
return ec.marshalOVideoMetadata2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐVideoMetadata(ctx, field.Selections, res)
}
func (ec *executionContext) _Media_favorite(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -5419,6 +5556,331 @@ func (ec *executionContext) _User_admin(ctx context.Context, field graphql.Colle
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) _VideoMetadata_id(ctx context.Context, field graphql.CollectedField, obj *models.VideoMetadata) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "VideoMetadata",
Field: field,
Args: nil,
IsMethod: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.ID(), nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(int)
fc.Result = res
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) _VideoMetadata_media(ctx context.Context, field graphql.CollectedField, obj *models.VideoMetadata) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "VideoMetadata",
Field: field,
Args: nil,
IsMethod: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Media(), nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*models.Media)
fc.Result = res
return ec.marshalNMedia2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx, field.Selections, res)
}
func (ec *executionContext) _VideoMetadata_width(ctx context.Context, field graphql.CollectedField, obj *models.VideoMetadata) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "VideoMetadata",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Width, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(int)
fc.Result = res
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) _VideoMetadata_height(ctx context.Context, field graphql.CollectedField, obj *models.VideoMetadata) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "VideoMetadata",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Height, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(int)
fc.Result = res
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) _VideoMetadata_duration(ctx context.Context, field graphql.CollectedField, obj *models.VideoMetadata) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "VideoMetadata",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Duration, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(float64)
fc.Result = res
return ec.marshalNFloat2float64(ctx, field.Selections, res)
}
func (ec *executionContext) _VideoMetadata_codec(ctx context.Context, field graphql.CollectedField, obj *models.VideoMetadata) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "VideoMetadata",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Codec, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*string)
fc.Result = res
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) _VideoMetadata_framerate(ctx context.Context, field graphql.CollectedField, obj *models.VideoMetadata) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "VideoMetadata",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Framerate, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*float64)
fc.Result = res
return ec.marshalOFloat2ᚖfloat64(ctx, field.Selections, res)
}
func (ec *executionContext) _VideoMetadata_bitrate(ctx context.Context, field graphql.CollectedField, obj *models.VideoMetadata) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "VideoMetadata",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Bitrate, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*int)
fc.Result = res
return ec.marshalOInt2ᚖint(ctx, field.Selections, res)
}
func (ec *executionContext) _VideoMetadata_colorProfile(ctx context.Context, field graphql.CollectedField, obj *models.VideoMetadata) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "VideoMetadata",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.ColorProfile, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*string)
fc.Result = res
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) _VideoMetadata_audio(ctx context.Context, field graphql.CollectedField, obj *models.VideoMetadata) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "VideoMetadata",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Audio, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*string)
fc.Result = res
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -6765,6 +7227,17 @@ func (ec *executionContext) _Media(ctx context.Context, sel ast.SelectionSet, ob
res = ec._Media_exif(ctx, field, obj)
return res
})
case "videoMetadata":
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._Media_videoMetadata(ctx, field, obj)
return res
})
case "favorite":
out.Values[i] = ec._Media_favorite(ctx, field, obj)
if out.Values[i] == graphql.Null {
@ -7481,6 +7954,63 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj
return out
}
var videoMetadataImplementors = []string{"VideoMetadata"}
func (ec *executionContext) _VideoMetadata(ctx context.Context, sel ast.SelectionSet, obj *models.VideoMetadata) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, videoMetadataImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("VideoMetadata")
case "id":
out.Values[i] = ec._VideoMetadata_id(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "media":
out.Values[i] = ec._VideoMetadata_media(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "width":
out.Values[i] = ec._VideoMetadata_width(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "height":
out.Values[i] = ec._VideoMetadata_height(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "duration":
out.Values[i] = ec._VideoMetadata_duration(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "codec":
out.Values[i] = ec._VideoMetadata_codec(ctx, field, obj)
case "framerate":
out.Values[i] = ec._VideoMetadata_framerate(ctx, field, obj)
case "bitrate":
out.Values[i] = ec._VideoMetadata_bitrate(ctx, field, obj)
case "colorProfile":
out.Values[i] = ec._VideoMetadata_colorProfile(ctx, field, obj)
case "audio":
out.Values[i] = ec._VideoMetadata_audio(ctx, field, obj)
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch()
if invalids > 0 {
return graphql.Null
}
return out
}
var __DirectiveImplementors = []string{"__Directive"}
func (ec *executionContext) ___Directive(ctx context.Context, sel ast.SelectionSet, obj *introspection.Directive) graphql.Marshaler {
@ -7805,6 +8335,20 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se
return res
}
func (ec *executionContext) unmarshalNFloat2float64(ctx context.Context, v interface{}) (float64, error) {
return graphql.UnmarshalFloat(v)
}
func (ec *executionContext) marshalNFloat2float64(ctx context.Context, sel ast.SelectionSet, v float64) graphql.Marshaler {
res := graphql.MarshalFloat(v)
if res == graphql.Null {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
}
return res
}
func (ec *executionContext) unmarshalNInt2int(ctx context.Context, v interface{}) (int, error) {
return graphql.UnmarshalInt(v)
}
@ -8619,6 +9163,17 @@ func (ec *executionContext) marshalOUser2ᚖgithubᚗcomᚋviktorstrateᚋphotov
return ec._User(ctx, sel, v)
}
func (ec *executionContext) marshalOVideoMetadata2githubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐVideoMetadata(ctx context.Context, sel ast.SelectionSet, v models.VideoMetadata) graphql.Marshaler {
return ec._VideoMetadata(ctx, sel, &v)
}
func (ec *executionContext) marshalOVideoMetadata2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐVideoMetadata(ctx context.Context, sel ast.SelectionSet, v *models.VideoMetadata) graphql.Marshaler {
if v == nil {
return graphql.Null
}
return ec._VideoMetadata(ctx, sel, v)
}
func (ec *executionContext) marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.EnumValue) graphql.Marshaler {
if v == nil {
return graphql.Null

View File

@ -8,14 +8,15 @@ import (
)
type Media struct {
MediaID int
Title string
Path string
PathHash string
AlbumId int
ExifId *int
Favorite bool
Type MediaType
MediaID int
Title string
Path string
PathHash string
AlbumId int
ExifId *int
Favorite bool
Type MediaType
VideoMetadataId *int
}
func (p *Media) ID() int {
@ -45,7 +46,7 @@ type MediaURL struct {
func NewMediaFromRow(row *sql.Row) (*Media, error) {
media := Media{}
if err := row.Scan(&media.MediaID, &media.Title, &media.Path, &media.PathHash, &media.AlbumId, &media.ExifId, &media.Favorite, &media.Type); err != nil {
if err := row.Scan(&media.MediaID, &media.Title, &media.Path, &media.PathHash, &media.AlbumId, &media.ExifId, &media.Favorite, &media.Type, &media.VideoMetadataId); err != nil {
return nil, err
}
@ -57,7 +58,7 @@ func NewMediaFromRows(rows *sql.Rows) ([]*Media, error) {
for rows.Next() {
var media Media
if err := rows.Scan(&media.MediaID, &media.Title, &media.Path, &media.PathHash, &media.AlbumId, &media.ExifId, &media.Favorite, &media.Type); err != nil {
if err := rows.Scan(&media.MediaID, &media.Title, &media.Path, &media.PathHash, &media.AlbumId, &media.ExifId, &media.Favorite, &media.Type, &media.VideoMetadataId); err != nil {
return nil, err
}
medias = append(medias, &media)

View File

@ -0,0 +1,33 @@
package models
import "database/sql"
type VideoMetadata struct {
MetadataID int
Width int
Height int
Duration float64
Codec *string
Framerate *float64
Bitrate *int
ColorProfile *string
Audio *string
}
func (metadata *VideoMetadata) ID() int {
return metadata.MetadataID
}
func (metadata *VideoMetadata) Media() *Media {
panic("not implemented")
}
func NewVideoMetadataFromRow(row *sql.Row) (*VideoMetadata, error) {
meta := VideoMetadata{}
if err := row.Scan(&meta.MetadataID, &meta.Width, &meta.Height, &meta.Duration, &meta.Codec, &meta.Framerate, &meta.Bitrate, &meta.ColorProfile, &meta.Audio); err != nil {
return nil, err
}
return &meta, nil
}

View File

@ -192,6 +192,21 @@ func (r *mediaResolver) Exif(ctx context.Context, obj *models.Media) (*models.Me
return exif, nil
}
func (r *mediaResolver) VideoMetadata(ctx context.Context, obj *models.Media) (*models.VideoMetadata, error) {
row := r.Database.QueryRow("SELECT video_metadata.* FROM media JOIN video_metadata ON media.video_metadata_id = video_metadata.metadata_id WHERE media.media_id = ?", obj.MediaID)
metadata, err := models.NewVideoMetadataFromRow(row)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
} else {
return nil, errors.Wrapf(err, "could not get video metadata of media from database")
}
}
return metadata, nil
}
func (r *mutationResolver) FavoriteMedia(ctx context.Context, mediaID int, favorite bool) (*models.Media, error) {
user := auth.UserFromContext(ctx)

View File

@ -217,6 +217,7 @@ type Media {
"The album that holds the media"
album: Album!
exif: MediaEXIF
videoMetadata: VideoMetadata
favorite: Boolean!
type: MediaType!
@ -249,6 +250,19 @@ type MediaEXIF {
exposureProgram: Int
}
type VideoMetadata {
id: Int!
media: Media!
width: Int!
height: Int!
duration: Float!
codec: String
framerate: Float
bitrate: Int
colorProfile: String
audio: String
}
type SearchResult {
query: String!
albums: [Album!]!

View File

@ -106,7 +106,7 @@ func (enc *EncodeMediaData) VideoMetadata() (*ffprobe.ProbeData, error) {
return enc._videoMetadata, nil
}
func readVideoStreamMetadata(videoPath string) (*ffprobe.Stream, error) {
func readVideoMetadata(videoPath string) (*ffprobe.ProbeData, error) {
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn()
@ -115,6 +115,15 @@ func readVideoStreamMetadata(videoPath string) (*ffprobe.Stream, error) {
return nil, errors.Wrapf(err, "could not read video metadata (%s)", path.Base(videoPath))
}
return data, nil
}
func readVideoStreamMetadata(videoPath string) (*ffprobe.Stream, error) {
data, err := readVideoMetadata(videoPath)
if err != nil {
return nil, errors.Wrap(err, "read video stream metadata")
}
stream := data.FirstVideoStream()
if stream == nil {
return nil, errors.Wrapf(err, "could not get stream from file metadata (%s)", path.Base(videoPath))

View File

@ -50,15 +50,21 @@ func ScanMedia(tx *sql.Tx, mediaPath string, albumId int, cache *AlbumScannerCac
}
row := tx.QueryRow("SELECT * FROM media WHERE media_id = ?", media_id)
photo, err := models.NewMediaFromRow(row)
media, err := models.NewMediaFromRow(row)
if err != nil {
return nil, false, errors.Wrap(err, "failed to get media by id from database")
}
_, err = ScanEXIF(tx, photo)
_, err = ScanEXIF(tx, media)
if err != nil {
log.Printf("WARN: ScanEXIF for %s failed: %s\n", mediaName, err)
}
return photo, true, nil
if media.Type == models.MediaTypeVideo {
if err = ScanVideoMetadata(tx, media); err != nil {
log.Printf("WARN: ScanVideoMetadata for %s failed: %s\n", mediaName, err)
}
}
return media, true, nil
}

View File

@ -0,0 +1,70 @@
package scanner
import (
"database/sql"
"fmt"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/viktorstrate/photoview/api/graphql/models"
)
func ScanVideoMetadata(tx *sql.Tx, video *models.Media) error {
data, err := readVideoMetadata(video.Path)
if err != nil {
return errors.Wrapf(err, "scan video metadata failed (%s)", video.Title)
}
stream := data.FirstVideoStream()
if stream == nil {
return errors.New(fmt.Sprintf("could not get video stream from metadata (%s)", video.Path))
}
audio := data.FirstAudioStream()
var audioText string
if audio == nil {
audioText = "No audio"
} else {
switch audio.Channels {
case 0:
audioText = "No audio"
case 1:
audioText = "Mono audio"
case 2:
audioText = "Stereo audio"
default:
audioText = fmt.Sprintf("Audio (%d channels)", audio.Channels)
}
}
var framerate *float64 = nil
if stream.AvgFrameRate != "" {
parts := strings.Split(stream.AvgFrameRate, "/")
if len(parts) == 2 {
if numerator, err := strconv.ParseInt(parts[0], 10, 64); err == nil {
if denominator, err := strconv.ParseInt(parts[1], 10, 64); err == nil {
result := float64(numerator) / float64(denominator)
framerate = &result
}
}
}
}
result, err := tx.Exec("INSERT INTO video_metadata (width, height, duration, codec, framerate, bitrate, color_profile, audio) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", stream.Width, stream.Height, data.Format.DurationSeconds, stream.CodecLongName, framerate, stream.BitRate, stream.Profile, audioText)
if err != nil {
return errors.Wrapf(err, "failed to insert video metadata into database (%s)", video.Title)
}
metadata_id, err := result.LastInsertId()
if err != nil {
return err
}
if _, err = tx.Exec("UPDATE media SET video_metadata_id = ? WHERE media_id = ?", metadata_id, video.MediaID); err != nil {
return err
}
return nil
}

View File

@ -29,6 +29,17 @@ const mediaQuery = gql`
width
height
}
videoMetadata {
id
width
height
duration
codec
framerate
bitrate
colorProfile
audio
}
exif {
camera
maker
@ -96,7 +107,7 @@ const Name = styled.div`
margin: 0.75rem 0 1rem;
`
const ExifInfo = styled.div`
const MetadataInfo = styled.div`
margin-bottom: 1.5rem;
`
@ -154,11 +165,33 @@ const SidebarContent = ({ media, hidePreview }) => {
exif.focalLength = `${exif.focalLength}mm`
}
exif.exposureProgram = exifItems = exifKeys.map(key => (
exifItems = exifKeys.map(key => (
<SidebarItem key={key} name={exifNameLookup[key]} value={exif[key]} />
))
}
let videoMetadataItems = []
if (media && media.videoMetadata) {
let metadata = Object.keys(media.videoMetadata)
.filter(x => !['id', '__typename', 'width', 'height'].includes(x))
.reduce(
(prev, curr) => ({
...prev,
[curr]: media.videoMetadata[curr],
}),
{}
)
metadata = {
dimensions: `${media.videoMetadata.width}x${media.videoMetadata.height}`,
...metadata,
}
videoMetadataItems = Object.keys(metadata).map(key => (
<SidebarItem key={key} name={key} value={metadata[key]} />
))
}
let previewImage = null
if (media) {
if (media.highRes) previewImage = media.highRes
@ -175,7 +208,8 @@ const SidebarContent = ({ media, hidePreview }) => {
</PreviewImageWrapper>
)}
<Name>{media && media.title}</Name>
<ExifInfo>{exifItems}</ExifInfo>
<MetadataInfo>{videoMetadataItems}</MetadataInfo>
<MetadataInfo>{exifItems}</MetadataInfo>
<SidebarDownload photo={media} />
<SidebarShare photo={media} />
</div>