From f537b1d60848f73700d608455ba5dc79e7fee527 Mon Sep 17 00:00:00 2001
From: viktorstrate <viktorstrate@gmail.com>
Date: Sat, 11 Jul 2020 15:57:58 +0200
Subject: [PATCH] Add video route (still unprotected)

---
 api/graphql/generated.go       | 53 ++++++++++++++++++++
 api/graphql/models/media.go    |  6 ++-
 api/graphql/resolvers/media.go | 11 +++++
 api/graphql/schema.graphql     |  2 +
 api/routes/photos.go           | 10 ++--
 api/routes/videos.go           | 88 ++++++++++++++++++++++++++++++++++
 api/server.go                  |  3 ++
 7 files changed, 169 insertions(+), 4 deletions(-)
 create mode 100644 api/routes/videos.go

diff --git a/api/graphql/generated.go b/api/graphql/generated.go
index 9b3469f..bc8d82a 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 0566076..bf38d12 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 cf487ff..9fa0f8d 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 b56b7e5..0053c93 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 ef25562..d418fb8 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 0000000..f4f05fc
--- /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 7d4dc83..a764b7a 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 {
