diff --git a/api/graphql/generated.go b/api/graphql/generated.go index 9b3469f2..bc8d82a9 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -82,6 +82,7 @@ type ComplexityRoot struct { Thumbnail func(childComplexity int) int Title func(childComplexity int) int Type func(childComplexity int) int + VideoWeb func(childComplexity int) int } MediaDownload struct { @@ -204,6 +205,7 @@ type AlbumResolver interface { type MediaResolver interface { Thumbnail(ctx context.Context, obj *models.Media) (*models.MediaURL, error) HighRes(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) Exif(ctx context.Context, obj *models.Media) (*models.MediaEXIF, error) @@ -441,6 +443,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Media.Type(childComplexity), true + case "Media.videoWeb": + if e.complexity.Media.VideoWeb == nil { + break + } + + return e.complexity.Media.VideoWeb(childComplexity), true + case "MediaDownload.height": if e.complexity.MediaDownload.Height == nil { break @@ -1326,6 +1335,8 @@ type Media { thumbnail: MediaURL! "URL to display the photo in full resolution, will be null for videos" highRes: MediaURL + "URL to get the video in a web format that can be played in the browser, will be null for photos" + videoWeb: MediaURL "The album that holds the media" album: Album! exif: MediaEXIF @@ -2535,6 +2546,37 @@ func (ec *executionContext) _Media_highRes(ctx context.Context, field graphql.Co return ec.marshalOMediaURL2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaURL(ctx, field.Selections, res) } +func (ec *executionContext) _Media_videoWeb(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().VideoWeb(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*models.MediaURL) + fc.Result = res + return ec.marshalOMediaURL2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaURL(ctx, field.Selections, res) +} + func (ec *executionContext) _Media_album(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -6687,6 +6729,17 @@ func (ec *executionContext) _Media(ctx context.Context, sel ast.SelectionSet, ob res = ec._Media_highRes(ctx, field, obj) return res }) + case "videoWeb": + 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_videoWeb(ctx, field, obj) + return res + }) case "album": field := field out.Concurrently(i, func() (res graphql.Marshaler) { diff --git a/api/graphql/models/media.go b/api/graphql/models/media.go index 0566076f..bf38d12f 100644 --- a/api/graphql/models/media.go +++ b/api/graphql/models/media.go @@ -71,7 +71,11 @@ func NewMediaFromRows(rows *sql.Rows) ([]*Media, error) { func (p *MediaURL) URL() string { imageUrl := utils.ApiEndpointUrl() - imageUrl.Path = path.Join(imageUrl.Path, "photo", p.MediaName) + if p.Purpose != VideoWeb { + imageUrl.Path = path.Join(imageUrl.Path, "photo", p.MediaName) + } else { + imageUrl.Path = path.Join(imageUrl.Path, "video", p.MediaName) + } return imageUrl.String() } diff --git a/api/graphql/resolvers/media.go b/api/graphql/resolvers/media.go index cf487ff2..9fa0f8d8 100644 --- a/api/graphql/resolvers/media.go +++ b/api/graphql/resolvers/media.go @@ -153,6 +153,17 @@ func (r *mediaResolver) Thumbnail(ctx context.Context, obj *models.Media) (*mode return url, nil } +func (r *mediaResolver) VideoWeb(ctx context.Context, obj *models.Media) (*models.MediaURL, error) { + row := r.Database.QueryRow("SELECT * FROM media_url WHERE media_id = ? AND (purpose = ?)", obj.MediaID, models.VideoWeb) + + url, err := models.NewMediaURLFromRow(row) + if err != nil { + return nil, errors.Wrapf(err, "could not query video web-format url (%s)", obj.Path) + } + + return url, nil +} + func (r *mediaResolver) Album(ctx context.Context, obj *models.Media) (*models.Album, error) { row := r.Database.QueryRow("SELECT album.* from media JOIN album ON media.album_id = album.album_id WHERE media_id = ?", obj.MediaID) return models.NewAlbumFromRow(row) diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index b56b7e5c..0053c932 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -212,6 +212,8 @@ type Media { thumbnail: MediaURL! "URL to display the photo in full resolution, will be null for videos" highRes: MediaURL + "URL to get the video in a web format that can be played in the browser, will be null for photos" + videoWeb: MediaURL "The album that holds the media" album: Album! exif: MediaEXIF diff --git a/api/routes/photos.go b/api/routes/photos.go index ef255623..d418fb81 100644 --- a/api/routes/photos.go +++ b/api/routes/photos.go @@ -134,13 +134,17 @@ func RegisterPhotoRoutes(db *sql.DB, router *mux.Router) { if purpose == models.PhotoThumbnail || purpose == models.PhotoHighRes || purpose == models.VideoThumbnail { cachedPath = path.Join(scanner.PhotoCache(), strconv.Itoa(media.AlbumId), strconv.Itoa(media_id), media_name) - } - - if purpose == models.MediaOriginal { + } else if purpose == models.MediaOriginal { cachedPath = media.Path + } else { + log.Printf("ERROR: Can not handle media_purpose for photo: %s\n", purpose) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("internal server error")) + return } file, err = os.Open(cachedPath) + defer file.Close() if err != nil { if os.IsNotExist(err) { tx, err := db.Begin() diff --git a/api/routes/videos.go b/api/routes/videos.go new file mode 100644 index 00000000..f4f05fc1 --- /dev/null +++ b/api/routes/videos.go @@ -0,0 +1,88 @@ +package routes + +import ( + "database/sql" + "log" + "net/http" + "os" + "path" + "strconv" + + "github.com/gorilla/mux" + "github.com/viktorstrate/photoview/api/graphql/models" + "github.com/viktorstrate/photoview/api/scanner" +) + +func RegisterVideoRoutes(db *sql.DB, router *mux.Router) { + + router.HandleFunc("/{name}", func(w http.ResponseWriter, r *http.Request) { + media_name := mux.Vars(r)["name"] + + row := db.QueryRow("SELECT media_url.purpose, media_url.media_id FROM media_url, media WHERE media_url.media_name = ? AND media_url.media_id = media.media_id", media_name) + + var purpose models.MediaPurpose + var media_id int + + if err := row.Scan(&purpose, &media_id); err != nil { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("404")) + return + } + + row = db.QueryRow("SELECT * FROM media WHERE media_id = ?", media_id) + media, err := models.NewMediaFromRow(row) + if err != nil { + log.Printf("WARN: %s", err) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("internal server error")) + } + + // TODO: Make sure user is authorized to access video + + var cachedPath string + + if purpose == models.VideoWeb { + cachedPath = path.Join(scanner.PhotoCache(), strconv.Itoa(media.AlbumId), strconv.Itoa(media_id), media_name) + } else { + log.Printf("ERROR: Can not handle media_purpose for video: %s\n", purpose) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("internal server error")) + return + } + + _, err = os.Stat(cachedPath) + if err != nil { + if os.IsNotExist(err) { + tx, err := db.Begin() + if err != nil { + log.Printf("ERROR: %s\n", err) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("internal server error")) + return + } + + _, err = scanner.ProcessMedia(tx, media) + if err != nil { + log.Printf("ERROR: processing video not found in cache: %s\n", err) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("internal server error")) + tx.Rollback() + return + } + + _, err = os.Stat(cachedPath) + if err != nil { + log.Printf("ERROR: after reprocessing video not found in cache: %s\n", err) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("internal server error")) + tx.Rollback() + return + } + + tx.Commit() + } + } + + http.ServeFile(w, r, cachedPath) + }) +} diff --git a/api/server.go b/api/server.go index 7d4dc83c..a764b7a2 100644 --- a/api/server.go +++ b/api/server.go @@ -81,6 +81,9 @@ func main() { photoRouter := endpointRouter.PathPrefix("/photo").Subrouter() routes.RegisterPhotoRoutes(db, photoRouter) + videoRouter := endpointRouter.PathPrefix("/video").Subrouter() + routes.RegisterVideoRoutes(db, videoRouter) + shouldServeUI := os.Getenv("SERVE_UI") == "1" if shouldServeUI {