From 3564866f41c73437afde53044c3b134d1c02c4de Mon Sep 17 00:00:00 2001
From: viktorstrate <viktorstrate@gmail.com>
Date: Fri, 10 Jul 2020 12:58:11 +0200
Subject: [PATCH] Start on video processing

---
 api/graphql/models/photo.go    | 11 +++---
 api/graphql/resolvers/photo.go |  4 +--
 api/routes/photos.go           |  6 ++--
 api/scanner/photo_type.go      | 50 +++++++++++----------------
 api/scanner/process_photo.go   | 62 ++++++++++++++++++++--------------
 api/scanner/process_video.go   | 34 +++++++++++++++++++
 api/scanner/scanner_album.go   |  2 +-
 7 files changed, 102 insertions(+), 67 deletions(-)
 create mode 100644 api/scanner/process_video.go

diff --git a/api/graphql/models/photo.go b/api/graphql/models/photo.go
index 79a68fe..7ec01cb 100644
--- a/api/graphql/models/photo.go
+++ b/api/graphql/models/photo.go
@@ -21,12 +21,13 @@ func (p *Photo) ID() int {
 	return p.PhotoID
 }
 
-type PhotoPurpose string
+type MediaPurpose string
 
 const (
-	PhotoThumbnail PhotoPurpose = "thumbnail"
-	PhotoHighRes   PhotoPurpose = "high-res"
-	PhotoOriginal  PhotoPurpose = "original"
+	PhotoThumbnail MediaPurpose = "thumbnail"
+	PhotoHighRes   MediaPurpose = "high-res"
+	MediaOriginal  MediaPurpose = "original"
+	VideoWeb       MediaPurpose = "video-web"
 )
 
 type PhotoURL struct {
@@ -35,7 +36,7 @@ type PhotoURL struct {
 	PhotoName   string
 	Width       int
 	Height      int
-	Purpose     PhotoPurpose
+	Purpose     MediaPurpose
 	ContentType string
 }
 
diff --git a/api/graphql/resolvers/photo.go b/api/graphql/resolvers/photo.go
index f47a7ff..6af4188 100644
--- a/api/graphql/resolvers/photo.go
+++ b/api/graphql/resolvers/photo.go
@@ -96,7 +96,7 @@ func (r *photoResolver) Downloads(ctx context.Context, obj *models.Photo) ([]*mo
 
 		var title string
 		switch {
-		case url.Purpose == models.PhotoOriginal:
+		case url.Purpose == models.MediaOriginal:
 			title = "Original"
 		case url.Purpose == models.PhotoThumbnail:
 			title = "Small"
@@ -119,7 +119,7 @@ func (r *photoResolver) HighRes(ctx context.Context, obj *models.Photo) (*models
 	// Try high res first, then
 	web_types_questions := strings.Repeat("?,", len(scanner.WebMimetypes))[:len(scanner.WebMimetypes)*2-1]
 	args := make([]interface{}, 0)
-	args = append(args, obj.PhotoID, models.PhotoHighRes, models.PhotoOriginal)
+	args = append(args, obj.PhotoID, models.PhotoHighRes, models.MediaOriginal)
 	for _, webtype := range scanner.WebMimetypes {
 		args = append(args, webtype)
 	}
diff --git a/api/routes/photos.go b/api/routes/photos.go
index 55ce82f..0526e18 100644
--- a/api/routes/photos.go
+++ b/api/routes/photos.go
@@ -25,7 +25,7 @@ func RegisterPhotoRoutes(db *sql.DB, router *mux.Router) {
 
 		row := db.QueryRow("SELECT photo_url.purpose, photo_url.content_type, photo_url.photo_id FROM photo_url, photo WHERE photo_url.photo_name = ? AND photo_url.photo_id = photo.photo_id", image_name)
 
-		var purpose models.PhotoPurpose
+		var purpose models.MediaPurpose
 		var content_type string
 		var photo_id int
 
@@ -136,7 +136,7 @@ func RegisterPhotoRoutes(db *sql.DB, router *mux.Router) {
 			cachedPath = path.Join(scanner.PhotoCache(), strconv.Itoa(photo.AlbumId), strconv.Itoa(photo_id), image_name)
 		}
 
-		if purpose == models.PhotoOriginal {
+		if purpose == models.MediaOriginal {
 			cachedPath = photo.Path
 		}
 
@@ -151,7 +151,7 @@ func RegisterPhotoRoutes(db *sql.DB, router *mux.Router) {
 					return
 				}
 
-				_, err = scanner.ProcessPhoto(tx, photo)
+				_, err = scanner.ProcessMedia(tx, photo)
 				if err != nil {
 					log.Printf("ERROR: processing image not found in cache: %s\n", err)
 					w.WriteHeader(http.StatusInternalServerError)
diff --git a/api/scanner/photo_type.go b/api/scanner/photo_type.go
index b061c06..d9d3586 100644
--- a/api/scanner/photo_type.go
+++ b/api/scanner/photo_type.go
@@ -71,37 +71,6 @@ var SupportedMimetypes = [...]MediaType{
 	TypeTiff,
 	TypeWebp,
 	TypeBmp,
-
-	TypeDNG,
-	TypeARW,
-	TypeSR2,
-	TypeSRF,
-	TypeCR2,
-	TypeCRW,
-	TypeERF,
-	TypeDCS,
-	TypeDRF,
-	TypeDCR,
-	TypeK25,
-	TypeKDC,
-	TypeMRW,
-	TypeMDC,
-	TypeNEF,
-	TypeNRW,
-	TypeORF,
-	TypePEF,
-	TypeRAF,
-	TypeRAW,
-	TypeRW2,
-	TypeGPR,
-	Type3FR,
-	TypeFFF,
-	TypeMEF,
-	TypeCap,
-	TypeIIQ,
-	TypeMOS,
-	TypeRWL,
-	TypeSRW,
 }
 
 var WebMimetypes = [...]MediaType{
@@ -224,6 +193,17 @@ func (imgType *MediaType) isWebCompatible() bool {
 	return false
 }
 
+func (imgType *MediaType) isVideo() bool {
+	for _, video_mime := range VideoMimetypes {
+		if video_mime == *imgType {
+			return true
+		}
+	}
+
+	return false
+}
+
+// isSupported determines if the given type can be processed
 func (imgType *MediaType) isSupported() bool {
 	for _, supported_mime := range SupportedMimetypes {
 		if supported_mime == *imgType {
@@ -231,6 +211,14 @@ func (imgType *MediaType) isSupported() bool {
 		}
 	}
 
+	if DarktableCli.IsInstalled() && imgType.isRaw() {
+		return true
+	}
+
+	if FfmpegCli.IsInstalled() && imgType.isVideo() {
+		return true
+	}
+
 	return false
 }
 
diff --git a/api/scanner/process_photo.go b/api/scanner/process_photo.go
index c240ac2..5e928fe 100644
--- a/api/scanner/process_photo.go
+++ b/api/scanner/process_photo.go
@@ -23,13 +23,13 @@ import (
 )
 
 // Higher order function used to check if PhotoURL for a given PhotoPurpose exists
-func makePhotoURLChecker(tx *sql.Tx, photoID int) (func(purpose models.PhotoPurpose) (*models.PhotoURL, error), error) {
+func makePhotoURLChecker(tx *sql.Tx, photoID int) (func(purpose models.MediaPurpose) (*models.PhotoURL, error), error) {
 	photoURLExistsStmt, err := tx.Prepare("SELECT * FROM photo_url WHERE photo_id = ? AND purpose = ?")
 	if err != nil {
 		return nil, err
 	}
 
-	return func(purpose models.PhotoPurpose) (*models.PhotoURL, error) {
+	return func(purpose models.MediaPurpose) (*models.PhotoURL, error) {
 		row := photoURLExistsStmt.QueryRow(photoID, purpose)
 		photoURL, err := models.NewPhotoURLFromRow(row)
 		if err != nil {
@@ -43,23 +43,44 @@ func makePhotoURLChecker(tx *sql.Tx, photoID int) (func(purpose models.PhotoPurp
 	}, nil
 }
 
-func ProcessPhoto(tx *sql.Tx, photo *models.Photo) (bool, error) {
+func ProcessMedia(tx *sql.Tx, photo *models.Photo) (bool, error) {
+	imageData := EncodeImageData{
+		photo: photo,
+	}
+
+	contentType, err := imageData.ContentType()
+	if err != nil {
+		return false, errors.Wrapf(err, "get content-type of media (%s)", photo.Path)
+	}
+
+	// Make sure photo cache directory exists
+	mediaCachePath, err := makeMediaCacheDir(photo)
+	if err != nil {
+		return false, errors.Wrap(err, "cache directory error")
+	}
+
+	if contentType.isVideo() {
+		return processVideo(tx, &imageData, mediaCachePath)
+	} else {
+		return processPhoto(tx, &imageData, mediaCachePath)
+	}
+}
+
+func processPhoto(tx *sql.Tx, imageData *EncodeImageData, photoCachePath *string) (bool, error) {
+
+	photo := imageData.photo
 
 	log.Printf("Processing photo: %s\n", photo.Path)
 
 	didProcess := false
 
-	imageData := EncodeImageData{
-		photo: photo,
-	}
-
 	photoUrlFromDB, err := makePhotoURLChecker(tx, photo.PhotoID)
 	if err != nil {
 		return false, err
 	}
 
 	// original photo url
-	origURL, err := photoUrlFromDB(models.PhotoOriginal)
+	origURL, err := photoUrlFromDB(models.MediaOriginal)
 	if err != nil {
 		return false, err
 	}
@@ -67,19 +88,13 @@ func ProcessPhoto(tx *sql.Tx, photo *models.Photo) (bool, error) {
 	// Thumbnail
 	thumbURL, err := photoUrlFromDB(models.PhotoThumbnail)
 	if err != nil {
-		return false, errors.Wrap(err, "error processing thumbnail")
+		return false, errors.Wrap(err, "error processing photo thumbnail")
 	}
 
 	// Highres
 	highResURL, err := photoUrlFromDB(models.PhotoHighRes)
 	if err != nil {
-		return false, errors.Wrap(err, "error processing highres")
-	}
-
-	// Make sure photo cache directory exists
-	photoCachePath, err := makePhotoCacheDir(photo)
-	if err != nil {
-		return false, errors.Wrap(err, "cache directory error")
+		return false, errors.Wrap(err, "error processing photo highres")
 	}
 
 	// Generate high res jpeg
@@ -195,13 +210,12 @@ func ProcessPhoto(tx *sql.Tx, photo *models.Photo) (bool, error) {
 	return didProcess, nil
 }
 
-func makePhotoCacheDir(photo *models.Photo) (*string, error) {
+func makeMediaCacheDir(photo *models.Photo) (*string, error) {
 
 	// Make root cache dir if not exists
 	if _, err := os.Stat(PhotoCache()); os.IsNotExist(err) {
 		if err := os.Mkdir(PhotoCache(), os.ModePerm); err != nil {
-			log.Println("ERROR: Could not make root image cache directory")
-			return nil, err
+			return nil, errors.Wrap(err, "could not make root image cache directory")
 		}
 	}
 
@@ -209,8 +223,7 @@ func makePhotoCacheDir(photo *models.Photo) (*string, error) {
 	albumCachePath := path.Join(PhotoCache(), strconv.Itoa(photo.AlbumId))
 	if _, err := os.Stat(albumCachePath); os.IsNotExist(err) {
 		if err := os.Mkdir(albumCachePath, os.ModePerm); err != nil {
-			log.Println("ERROR: Could not make album image cache directory")
-			return nil, err
+			return nil, errors.Wrap(err, "could not make album image cache directory")
 		}
 	}
 
@@ -218,15 +231,14 @@ func makePhotoCacheDir(photo *models.Photo) (*string, error) {
 	photoCachePath := path.Join(albumCachePath, strconv.Itoa(photo.PhotoID))
 	if _, err := os.Stat(photoCachePath); os.IsNotExist(err) {
 		if err := os.Mkdir(photoCachePath, os.ModePerm); err != nil {
-			log.Println("ERROR: Could not make photo image cache directory")
-			return nil, err
+			return nil, errors.Wrap(err, "could not make photo image cache directory")
 		}
 	}
 
 	return &photoCachePath, nil
 }
 
-func saveOriginalPhotoToDB(tx *sql.Tx, photo *models.Photo, imageData EncodeImageData, photoDimensions *PhotoDimensions) error {
+func saveOriginalPhotoToDB(tx *sql.Tx, photo *models.Photo, imageData *EncodeImageData, photoDimensions *PhotoDimensions) error {
 	photoName := path.Base(photo.Path)
 	photoBaseName := photoName[0 : len(photoName)-len(path.Ext(photoName))]
 	photoBaseExt := path.Ext(photoName)
@@ -239,7 +251,7 @@ func saveOriginalPhotoToDB(tx *sql.Tx, photo *models.Photo, imageData EncodeImag
 		return err
 	}
 
-	_, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)", photo.PhotoID, original_image_name, photoDimensions.Width, photoDimensions.Height, models.PhotoOriginal, contentType)
+	_, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)", photo.PhotoID, original_image_name, photoDimensions.Width, photoDimensions.Height, models.MediaOriginal, contentType)
 	if err != nil {
 		log.Printf("Could not insert original photo url: %d, %s\n", photo.PhotoID, photoName)
 		return err
diff --git a/api/scanner/process_video.go b/api/scanner/process_video.go
new file mode 100644
index 0000000..ed38af0
--- /dev/null
+++ b/api/scanner/process_video.go
@@ -0,0 +1,34 @@
+package scanner
+
+import (
+	"database/sql"
+	"log"
+
+	"github.com/pkg/errors"
+	"github.com/viktorstrate/photoview/api/graphql/models"
+)
+
+func processVideo(tx *sql.Tx, imageData *EncodeImageData, videoCachePath *string) (bool, error) {
+	video := imageData.photo
+	didProcess := false
+
+	log.Printf("Processing video: %s", video.Path)
+
+	mediaUrlFromDB, err := makePhotoURLChecker(tx, video.PhotoID)
+	if err != nil {
+		return false, err
+	}
+
+	videoWebURL, err := mediaUrlFromDB(models.VideoWeb)
+	if err != nil {
+		return false, errors.Wrap(err, "error processing video web-format")
+	}
+
+	if videoWebURL == nil {
+		// TODO: Process web video
+	}
+
+	// TODO: Process video thumbnail
+
+	return didProcess, nil
+}
diff --git a/api/scanner/scanner_album.go b/api/scanner/scanner_album.go
index be14b96..00161a8 100644
--- a/api/scanner/scanner_album.go
+++ b/api/scanner/scanner_album.go
@@ -42,7 +42,7 @@ func scanAlbum(album *models.Album, cache *AlbumScannerCache, db *sql.DB) {
 			ScannerError("Failed to begin database transaction: %s", err)
 		}
 
-		processing_was_needed, err := ProcessPhoto(tx, photo)
+		processing_was_needed, err := ProcessMedia(tx, photo)
 		if err != nil {
 			tx.Rollback()
 			ScannerError("Failed to process photo (%s): %s", photo.Path, err)
