diff --git a/api/database/migrations/008_video_support.down.sql b/api/database/migrations/008_video_support.down.sql
index e2bd1328..ea6cedc3 100644
--- a/api/database/migrations/008_video_support.down.sql
+++ b/api/database/migrations/008_video_support.down.sql
@@ -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;
\ No newline at end of file
diff --git a/api/database/migrations/008_video_support.up.sql b/api/database/migrations/008_video_support.up.sql
index 1827d1b2..c5d87d45 100644
--- a/api/database/migrations/008_video_support.up.sql
+++ b/api/database/migrations/008_video_support.up.sql
@@ -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);
diff --git a/api/go.mod b/api/go.mod
index 436aaddf..7c2a6891 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -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
diff --git a/api/go.sum b/api/go.sum
index 59b9de40..85449931 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -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=
diff --git a/api/gqlgen.yml b/api/gqlgen.yml
index eb985768..e12dcc01 100644
--- a/api/gqlgen.yml
+++ b/api/gqlgen.yml
@@ -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:
diff --git a/api/graphql/generated.go b/api/graphql/generated.go
index bc8d82a9..73b98a48 100644
--- a/api/graphql/generated.go
+++ b/api/graphql/generated.go
@@ -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
diff --git a/api/graphql/models/media.go b/api/graphql/models/media.go
index bf38d12f..4f3df094 100644
--- a/api/graphql/models/media.go
+++ b/api/graphql/models/media.go
@@ -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)
diff --git a/api/graphql/models/video_metadata.go b/api/graphql/models/video_metadata.go
new file mode 100644
index 00000000..51c14323
--- /dev/null
+++ b/api/graphql/models/video_metadata.go
@@ -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
+}
diff --git a/api/graphql/resolvers/media.go b/api/graphql/resolvers/media.go
index f3d83085..de4dffcd 100644
--- a/api/graphql/resolvers/media.go
+++ b/api/graphql/resolvers/media.go
@@ -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)
diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql
index 0053c932..6c44e542 100644
--- a/api/graphql/schema.graphql
+++ b/api/graphql/schema.graphql
@@ -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!]!
diff --git a/api/scanner/process_video.go b/api/scanner/process_video.go
index fe2962ee..1758cadb 100644
--- a/api/scanner/process_video.go
+++ b/api/scanner/process_video.go
@@ -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))
diff --git a/api/scanner/scanner_media.go b/api/scanner/scanner_media.go
index b68d5184..c67a2689 100644
--- a/api/scanner/scanner_media.go
+++ b/api/scanner/scanner_media.go
@@ -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
}
diff --git a/api/scanner/video_metadata.go b/api/scanner/video_metadata.go
new file mode 100644
index 00000000..f05bdc1a
--- /dev/null
+++ b/api/scanner/video_metadata.go
@@ -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
+}
diff --git a/ui/src/components/sidebar/MediaSidebar.js b/ui/src/components/sidebar/MediaSidebar.js
index e7b06a95..de9e1193 100644
--- a/ui/src/components/sidebar/MediaSidebar.js
+++ b/ui/src/components/sidebar/MediaSidebar.js
@@ -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 => (
))
}
+ 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 => (
+
+ ))
+ }
+
let previewImage = null
if (media) {
if (media.highRes) previewImage = media.highRes
@@ -175,7 +208,8 @@ const SidebarContent = ({ media, hidePreview }) => {
)}
{media && media.title}
- {exifItems}
+ {videoMetadataItems}
+ {exifItems}