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 share_token CHANGE COLUMN media_id photo_id int;
ALTER TABLE photo DROP COLUMN media_type; 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 photo_exif RENAME TO media_exif;
ALTER TABLE media CHANGE COLUMN photo_id media_id int NOT NULL AUTO_INCREMENT; 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
ALTER TABLE media_url CHANGE COLUMN photo_name media_name varchar(512) NOT NULL; 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 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 v1.22.4 // indirect
github.com/urfave/cli/v2 v2.2.0 // indirect github.com/urfave/cli/v2 v2.2.0 // indirect
github.com/vektah/dataloaden v0.3.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/vektah/gqlparser/v2 v2.0.1
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0
github.com/xor-gate/goexif2 v1.1.0 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-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 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-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/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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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 model: github.com/viktorstrate/photoview/api/graphql/models.MediaURL
MediaEXIF: MediaEXIF:
model: github.com/viktorstrate/photoview/api/graphql/models.MediaEXIF model: github.com/viktorstrate/photoview/api/graphql/models.MediaEXIF
VideoMetadata:
model: github.com/viktorstrate/photoview/api/graphql/models.VideoMetadata
Album: Album:
model: github.com/viktorstrate/photoview/api/graphql/models.Album model: github.com/viktorstrate/photoview/api/graphql/models.Album
ShareToken: ShareToken:

View File

@ -82,6 +82,7 @@ type ComplexityRoot struct {
Thumbnail func(childComplexity int) int Thumbnail func(childComplexity int) int
Title func(childComplexity int) int Title func(childComplexity int) int
Type func(childComplexity int) int Type func(childComplexity int) int
VideoMetadata func(childComplexity int) int
VideoWeb func(childComplexity int) int VideoWeb func(childComplexity int) int
} }
@ -190,6 +191,19 @@ type ComplexityRoot struct {
RootPath func(childComplexity int) int RootPath func(childComplexity int) int
Username 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 { type AlbumResolver interface {
@ -208,6 +222,7 @@ type MediaResolver interface {
VideoWeb(ctx context.Context, obj *models.Media) (*models.MediaURL, error) VideoWeb(ctx context.Context, obj *models.Media) (*models.MediaURL, error)
Album(ctx context.Context, obj *models.Media) (*models.Album, error) Album(ctx context.Context, obj *models.Media) (*models.Album, error)
Exif(ctx context.Context, obj *models.Media) (*models.MediaEXIF, 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) Shares(ctx context.Context, obj *models.Media) ([]*models.ShareToken, error)
Downloads(ctx context.Context, obj *models.Media) ([]*models.MediaDownload, 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 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": case "Media.videoWeb":
if e.complexity.Media.VideoWeb == nil { if e.complexity.Media.VideoWeb == nil {
break break
@ -1040,6 +1062,76 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.User.Username(childComplexity), true 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 return 0, false
} }
@ -1340,6 +1432,7 @@ type Media {
"The album that holds the media" "The album that holds the media"
album: Album! album: Album!
exif: MediaEXIF exif: MediaEXIF
videoMetadata: VideoMetadata
favorite: Boolean! favorite: Boolean!
type: MediaType! type: MediaType!
@ -1372,6 +1465,19 @@ type MediaEXIF {
exposureProgram: Int 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 { type SearchResult {
query: String! query: String!
albums: [Album!]! 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) 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) { func (ec *executionContext) _Media_favorite(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { 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) 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) { func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { 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) res = ec._Media_exif(ctx, field, obj)
return res 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": case "favorite":
out.Values[i] = ec._Media_favorite(ctx, field, obj) out.Values[i] = ec._Media_favorite(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
@ -7481,6 +7954,63 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj
return out 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"} var __DirectiveImplementors = []string{"__Directive"}
func (ec *executionContext) ___Directive(ctx context.Context, sel ast.SelectionSet, obj *introspection.Directive) graphql.Marshaler { 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 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) { func (ec *executionContext) unmarshalNInt2int(ctx context.Context, v interface{}) (int, error) {
return graphql.UnmarshalInt(v) return graphql.UnmarshalInt(v)
} }
@ -8619,6 +9163,17 @@ func (ec *executionContext) marshalOUser2ᚖgithubᚗcomᚋviktorstrateᚋphotov
return ec._User(ctx, sel, v) 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 { 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 { if v == nil {
return graphql.Null return graphql.Null

View File

@ -16,6 +16,7 @@ type Media struct {
ExifId *int ExifId *int
Favorite bool Favorite bool
Type MediaType Type MediaType
VideoMetadataId *int
} }
func (p *Media) ID() int { func (p *Media) ID() int {
@ -45,7 +46,7 @@ type MediaURL struct {
func NewMediaFromRow(row *sql.Row) (*Media, error) { func NewMediaFromRow(row *sql.Row) (*Media, error) {
media := Media{} 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 return nil, err
} }
@ -57,7 +58,7 @@ func NewMediaFromRows(rows *sql.Rows) ([]*Media, error) {
for rows.Next() { for rows.Next() {
var media Media 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 return nil, err
} }
medias = append(medias, &media) 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 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) { func (r *mutationResolver) FavoriteMedia(ctx context.Context, mediaID int, favorite bool) (*models.Media, error) {
user := auth.UserFromContext(ctx) user := auth.UserFromContext(ctx)

View File

@ -217,6 +217,7 @@ type Media {
"The album that holds the media" "The album that holds the media"
album: Album! album: Album!
exif: MediaEXIF exif: MediaEXIF
videoMetadata: VideoMetadata
favorite: Boolean! favorite: Boolean!
type: MediaType! type: MediaType!
@ -249,6 +250,19 @@ type MediaEXIF {
exposureProgram: Int 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 { type SearchResult {
query: String! query: String!
albums: [Album!]! albums: [Album!]!

View File

@ -106,7 +106,7 @@ func (enc *EncodeMediaData) VideoMetadata() (*ffprobe.ProbeData, error) {
return enc._videoMetadata, nil 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) ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn() 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 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() stream := data.FirstVideoStream()
if stream == nil { if stream == nil {
return nil, errors.Wrapf(err, "could not get stream from file metadata (%s)", path.Base(videoPath)) 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) row := tx.QueryRow("SELECT * FROM media WHERE media_id = ?", media_id)
photo, err := models.NewMediaFromRow(row) media, err := models.NewMediaFromRow(row)
if err != nil { if err != nil {
return nil, false, errors.Wrap(err, "failed to get media by id from database") 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 { if err != nil {
log.Printf("WARN: ScanEXIF for %s failed: %s\n", mediaName, err) 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 width
height height
} }
videoMetadata {
id
width
height
duration
codec
framerate
bitrate
colorProfile
audio
}
exif { exif {
camera camera
maker maker
@ -96,7 +107,7 @@ const Name = styled.div`
margin: 0.75rem 0 1rem; margin: 0.75rem 0 1rem;
` `
const ExifInfo = styled.div` const MetadataInfo = styled.div`
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
` `
@ -154,11 +165,33 @@ const SidebarContent = ({ media, hidePreview }) => {
exif.focalLength = `${exif.focalLength}mm` exif.focalLength = `${exif.focalLength}mm`
} }
exif.exposureProgram = exifItems = exifKeys.map(key => ( exifItems = exifKeys.map(key => (
<SidebarItem key={key} name={exifNameLookup[key]} value={exif[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 let previewImage = null
if (media) { if (media) {
if (media.highRes) previewImage = media.highRes if (media.highRes) previewImage = media.highRes
@ -175,7 +208,8 @@ const SidebarContent = ({ media, hidePreview }) => {
</PreviewImageWrapper> </PreviewImageWrapper>
)} )}
<Name>{media && media.title}</Name> <Name>{media && media.title}</Name>
<ExifInfo>{exifItems}</ExifInfo> <MetadataInfo>{videoMetadataItems}</MetadataInfo>
<MetadataInfo>{exifItems}</MetadataInfo>
<SidebarDownload photo={media} /> <SidebarDownload photo={media} />
<SidebarShare photo={media} /> <SidebarShare photo={media} />
</div> </div>