From 3564866f41c73437afde53044c3b134d1c02c4de Mon Sep 17 00:00:00 2001 From: viktorstrate 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 79a68fe3..7ec01cbd 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 f47a7ffe..6af4188f 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 55ce82f5..0526e18a 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 b061c065..d9d35863 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 c240ac29..5e928fe5 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 00000000..ed38af0d --- /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 be14b962..00161a8d 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)