diff --git a/api/graphql/models/media.go b/api/graphql/models/media.go index 02aae2d8..a3fe7b8d 100644 --- a/api/graphql/models/media.go +++ b/api/graphql/models/media.go @@ -77,6 +77,10 @@ func (m *Media) GetHighRes() (*MediaURL, error) { return nil, nil } +func (m *Media) CachePath() (string, error) { + return utils.CachePathForMedia(m.AlbumID, m.ID) +} + type MediaType string const ( diff --git a/api/routes/photos.go b/api/routes/photos.go index 74bea71a..b14a9a77 100644 --- a/api/routes/photos.go +++ b/api/routes/photos.go @@ -46,7 +46,7 @@ func RegisterPhotoRoutes(db *gorm.DB, router *mux.Router) { if _, err := os.Stat(cachedPath); os.IsNotExist((err)) { err := db.Transaction(func(tx *gorm.DB) error { - if _, err = scanner.ProcessMedia(tx, media); err != nil { + if err = scanner.ProcessSingleMedia(tx, media); err != nil { log.Printf("ERROR: processing image not found in cache (%s): %s\n", cachedPath, err) return err } diff --git a/api/routes/videos.go b/api/routes/videos.go index 4f90bf69..dd1d5021 100644 --- a/api/routes/videos.go +++ b/api/routes/videos.go @@ -52,7 +52,7 @@ func RegisterVideoRoutes(db *gorm.DB, router *mux.Router) { if _, err := os.Stat(cachedPath); err != nil { if os.IsNotExist(err) { err := db.Transaction(func(tx *gorm.DB) error { - if _, err := scanner.ProcessMedia(tx, media); err != nil { + if err := scanner.ProcessSingleMedia(tx, media); err != nil { log.Printf("ERROR: processing video not found in cache: %s\n", err) return err } diff --git a/api/scanner/media_encoding/encode_photo.go b/api/scanner/media_encoding/encode_photo.go index 257b3e21..fedc47c8 100644 --- a/api/scanner/media_encoding/encode_photo.go +++ b/api/scanner/media_encoding/encode_photo.go @@ -61,6 +61,12 @@ type EncodeMediaData struct { _videoMetadata *ffprobe.ProbeData } +func NewEncodeMediaData(media *models.Media) EncodeMediaData { + return EncodeMediaData{ + Media: media, + } +} + // ContentType reads the image to determine its content type func (img *EncodeMediaData) ContentType() (*media_type.MediaType, error) { if img._contentType != nil { diff --git a/api/scanner/scanner_album.go b/api/scanner/scanner_album.go index 6ee87b0b..97e126a4 100644 --- a/api/scanner/scanner_album.go +++ b/api/scanner/scanner_album.go @@ -6,7 +6,6 @@ import ( "log" "os" "path" - "strconv" "github.com/photoview/photoview/api/graphql/models" "github.com/photoview/photoview/api/scanner/media_encoding" @@ -104,9 +103,7 @@ func ScanAlbum(ctx scanner_task.TaskContext) error { for i, media := range albumMedia { updatedURLs := []*models.MediaURL{} - mediaData := media_encoding.EncodeMediaData{ - Media: media, - } + mediaData := media_encoding.NewEncodeMediaData(media) // define new ctx for scope of for-loop ctx, err := scanner_tasks.Tasks.BeforeProcessMedia(ctx, &mediaData) @@ -200,44 +197,11 @@ func findMediaForAlbum(ctx scanner_task.TaskContext) ([]*models.Media, error) { func processMedia(ctx scanner_task.TaskContext, mediaData *media_encoding.EncodeMediaData) ([]*models.MediaURL, error) { - _, err := mediaData.ContentType() - if err != nil { - return []*models.MediaURL{}, errors.Wrapf(err, "get content-type of media (%s)", mediaData.Media.Path) - } - // Make sure media cache directory exists - mediaCachePath, err := makeMediaCacheDir(mediaData.Media) + mediaCachePath, err := mediaData.Media.CachePath() if err != nil { return []*models.MediaURL{}, errors.Wrap(err, "cache directory error") } - return scanner_tasks.Tasks.ProcessMedia(ctx, mediaData, *mediaCachePath) -} - -func makeMediaCacheDir(media *models.Media) (*string, error) { - - // Make root cache dir if not exists - if _, err := os.Stat(utils.MediaCachePath()); os.IsNotExist(err) { - if err := os.Mkdir(utils.MediaCachePath(), os.ModePerm); err != nil { - return nil, errors.Wrap(err, "could not make root image cache directory") - } - } - - // Make album cache dir if not exists - albumCachePath := path.Join(utils.MediaCachePath(), strconv.Itoa(int(media.AlbumID))) - if _, err := os.Stat(albumCachePath); os.IsNotExist(err) { - if err := os.Mkdir(albumCachePath, os.ModePerm); err != nil { - return nil, errors.Wrap(err, "could not make album image cache directory") - } - } - - // Make photo cache dir if not exists - photoCachePath := path.Join(albumCachePath, strconv.Itoa(int(media.ID))) - if _, err := os.Stat(photoCachePath); os.IsNotExist(err) { - if err := os.Mkdir(photoCachePath, os.ModePerm); err != nil { - return nil, errors.Wrap(err, "could not make photo image cache directory") - } - } - - return &photoCachePath, nil + return scanner_tasks.Tasks.ProcessMedia(ctx, mediaData, mediaCachePath) } diff --git a/api/scanner/scanner_media.go b/api/scanner/scanner_media.go index e513ecd4..eaefb024 100644 --- a/api/scanner/scanner_media.go +++ b/api/scanner/scanner_media.go @@ -1,12 +1,16 @@ package scanner import ( + "context" "log" "os" "path" "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/media_encoding" "github.com/photoview/photoview/api/scanner/scanner_cache" + "github.com/photoview/photoview/api/scanner/scanner_task" + "github.com/photoview/photoview/api/scanner/scanner_tasks" "github.com/pkg/errors" "gorm.io/gorm" ) @@ -62,11 +66,41 @@ func ScanMedia(tx *gorm.DB, mediaPath string, albumId int, cache *scanner_cache. return nil, false, errors.Wrap(err, "could not insert media into database") } - 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 } + +// ProcessSingleMedia processes a single media, might be used to reprocess media with corrupted cache +// Function waits for processing to finish before returning. +func ProcessSingleMedia(db *gorm.DB, media *models.Media) error { + album_cache := scanner_cache.MakeAlbumCache() + + var album models.Album + if err := db.Model(media).Association("Album").Find(&album); err != nil { + return err + } + + media_data := media_encoding.NewEncodeMediaData(media) + + task_context := scanner_task.NewTaskContext(context.Background(), db, &album, album_cache) + new_ctx, err := scanner_tasks.Tasks.BeforeProcessMedia(task_context, &media_data) + if err != nil { + return err + } + + mediaCachePath, err := media.CachePath() + if err != nil { + return err + } + + updated_urls, err := scanner_tasks.Tasks.ProcessMedia(new_ctx, &media_data, mediaCachePath) + if err != nil { + return err + } + + err = scanner_tasks.Tasks.AfterProcessMedia(new_ctx, &media_data, updated_urls, 0, 1) + if err != nil { + return err + } + + return nil +} diff --git a/api/scanner/scanner_queue/queue.go b/api/scanner/scanner_queue/queue.go index 0c025931..7dea9502 100644 --- a/api/scanner/scanner_queue/queue.go +++ b/api/scanner/scanner_queue/queue.go @@ -217,6 +217,8 @@ func AddAllToQueue() error { return nil } +// AddUserToQueue finds all root albums owned by the given user and adds them to the scanner queue. +// Function does not block. func AddUserToQueue(user *models.User) error { album_cache := scanner_cache.MakeAlbumCache() albums, album_errors := scanner.FindAlbumsForUser(global_scanner_queue.db, user, album_cache) diff --git a/api/scanner/scanner_tasks/exif_task.go b/api/scanner/scanner_tasks/exif_task.go index 0900824f..5fd5894b 100644 --- a/api/scanner/scanner_tasks/exif_task.go +++ b/api/scanner/scanner_tasks/exif_task.go @@ -5,7 +5,6 @@ import ( "github.com/photoview/photoview/api/graphql/models" "github.com/photoview/photoview/api/scanner/exif" - "github.com/photoview/photoview/api/scanner/media_encoding" "github.com/photoview/photoview/api/scanner/scanner_task" ) @@ -13,11 +12,15 @@ type ExifTask struct { scanner_task.ScannerTaskBase } -func (t ExifTask) AfterProcessMedia(ctx scanner_task.TaskContext, mediaData *media_encoding.EncodeMediaData, updatedURLs []*models.MediaURL, mediaIndex int, mediaTotal int) error { +func (t ExifTask) AfterMediaFound(ctx scanner_task.TaskContext, media *models.Media, newMedia bool) error { - _, err := exif.SaveEXIF(ctx.GetDB(), mediaData.Media) + if !newMedia { + return nil + } + + _, err := exif.SaveEXIF(ctx.GetDB(), media) if err != nil { - log.Printf("WARN: SaveEXIF for %s failed: %s\n", mediaData.Media.Title, err) + log.Printf("WARN: SaveEXIF for %s failed: %s\n", media.Title, err) } return nil diff --git a/api/scanner/scanner_tasks/processing_tasks/process_video_task.go b/api/scanner/scanner_tasks/processing_tasks/process_video_task.go index 8f00d07e..b29f4825 100644 --- a/api/scanner/scanner_tasks/processing_tasks/process_video_task.go +++ b/api/scanner/scanner_tasks/processing_tasks/process_video_task.go @@ -59,7 +59,7 @@ func (t ProcessVideoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData * origVideoPath := video.Path videoMediaName := generateUniqueMediaName(video.Path) - webMetadata, err := readVideoStreamMetadata(origVideoPath) + webMetadata, err := ReadVideoStreamMetadata(origVideoPath) if err != nil { return []*models.MediaURL{}, errors.Wrapf(err, "failed to read metadata for original video (%s)", video.Title) } @@ -99,7 +99,7 @@ func (t ProcessVideoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData * return []*models.MediaURL{}, errors.Wrapf(err, "could not encode mp4 video (%s)", video.Path) } - webMetadata, err := readVideoStreamMetadata(webVideoPath) + webMetadata, err := ReadVideoStreamMetadata(webVideoPath) if err != nil { return []*models.MediaURL{}, errors.Wrapf(err, "failed to read metadata for encoded web-video (%s)", video.Title) } @@ -205,7 +205,7 @@ func (t ProcessVideoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData * return updatedURLs, nil } -func readVideoMetadata(videoPath string) (*ffprobe.ProbeData, error) { +func ReadVideoMetadata(videoPath string) (*ffprobe.ProbeData, error) { ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() @@ -217,8 +217,8 @@ func readVideoMetadata(videoPath string) (*ffprobe.ProbeData, error) { return data, nil } -func readVideoStreamMetadata(videoPath string) (*ffprobe.Stream, error) { - data, err := readVideoMetadata(videoPath) +func ReadVideoStreamMetadata(videoPath string) (*ffprobe.Stream, error) { + data, err := ReadVideoMetadata(videoPath) if err != nil { return nil, errors.Wrap(err, "read video stream metadata") } diff --git a/api/scanner/scanner_tasks/scanner_tasks.go b/api/scanner/scanner_tasks/scanner_tasks.go index 353176c8..5290670d 100644 --- a/api/scanner/scanner_tasks/scanner_tasks.go +++ b/api/scanner/scanner_tasks/scanner_tasks.go @@ -19,6 +19,7 @@ var allTasks []scanner_task.ScannerTask = []scanner_task.ScannerTask{ processing_tasks.ProcessVideoTask{}, FaceDetectionTask{}, ExifTask{}, + VideoMetadataTask{}, cleanup_tasks.MediaCleanupTask{}, } diff --git a/api/scanner/video_metadata.go b/api/scanner/scanner_tasks/video_metadata_task.go similarity index 72% rename from api/scanner/video_metadata.go rename to api/scanner/scanner_tasks/video_metadata_task.go index f79e0c5f..e67d6eb9 100644 --- a/api/scanner/video_metadata.go +++ b/api/scanner/scanner_tasks/video_metadata_task.go @@ -1,18 +1,39 @@ -package scanner +package scanner_tasks import ( "fmt" + "log" "strconv" "strings" "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/scanner/scanner_task" + "github.com/photoview/photoview/api/scanner/scanner_tasks/processing_tasks" "github.com/pkg/errors" "gorm.io/gorm" ) +type VideoMetadataTask struct { + scanner_task.ScannerTaskBase +} + +func (t VideoMetadataTask) AfterMediaFound(ctx scanner_task.TaskContext, media *models.Media, newMedia bool) error { + + if !newMedia || media.Type != models.MediaTypeVideo { + return nil + } + + err := ScanVideoMetadata(ctx.GetDB(), media) + if err != nil { + log.Printf("WARN: ScanVideoMetadata for %s failed: %s\n", media.Title, err) + } + + return nil +} + func ScanVideoMetadata(tx *gorm.DB, video *models.Media) error { - data, err := readVideoMetadata(video.Path) + data, err := processing_tasks.ReadVideoMetadata(video.Path) if err != nil { return errors.Wrapf(err, "scan video metadata failed (%s)", video.Title) } diff --git a/api/utils/media_cache.go b/api/utils/media_cache.go new file mode 100644 index 00000000..42a38d4b --- /dev/null +++ b/api/utils/media_cache.go @@ -0,0 +1,58 @@ +package utils + +import ( + "os" + "path" + "strconv" + + "github.com/pkg/errors" +) + +// CachePathForMedia is a low-level implementation for Media.CachePath() +func CachePathForMedia(albumID int, mediaID int) (string, error) { + + // Make root cache dir if not exists + if _, err := os.Stat(MediaCachePath()); os.IsNotExist(err) { + if err := os.Mkdir(MediaCachePath(), os.ModePerm); err != nil { + return "", errors.Wrap(err, "could not make root image cache directory") + } + } + + // Make album cache dir if not exists + albumCachePath := path.Join(MediaCachePath(), strconv.Itoa(int(albumID))) + if _, err := os.Stat(albumCachePath); os.IsNotExist(err) { + if err := os.Mkdir(albumCachePath, os.ModePerm); err != nil { + return "", errors.Wrap(err, "could not make album image cache directory") + } + } + + // Make photo cache dir if not exists + photoCachePath := path.Join(albumCachePath, strconv.Itoa(int(mediaID))) + if _, err := os.Stat(photoCachePath); os.IsNotExist(err) { + if err := os.Mkdir(photoCachePath, os.ModePerm); err != nil { + return "", errors.Wrap(err, "could not make photo image cache directory") + } + } + + return photoCachePath, nil +} + +var test_cache_path string = "" + +func ConfigureTestCache(tmp_dir string) { + test_cache_path = tmp_dir +} + +// MediaCachePath returns the path for where the media cache is located on the file system +func MediaCachePath() string { + if test_cache_path != "" { + return test_cache_path + } + + photoCache := EnvMediaCachePath.GetValue() + if photoCache == "" { + photoCache = "./media_cache" + } + + return photoCache +} diff --git a/api/utils/utils.go b/api/utils/utils.go index a69fc0c0..c6620d72 100644 --- a/api/utils/utils.go +++ b/api/utils/utils.go @@ -47,26 +47,6 @@ func HandleError(message string, err error) PhotoviewError { } } -var test_cache_path string = "" - -func ConfigureTestCache(tmp_dir string) { - test_cache_path = tmp_dir -} - -// MediaCachePath returns the path for where the media cache is located on the file system -func MediaCachePath() string { - if test_cache_path != "" { - return test_cache_path - } - - photoCache := EnvMediaCachePath.GetValue() - if photoCache == "" { - photoCache = "./media_cache" - } - - return photoCache -} - var test_face_recognition_models_path string = "" func ConfigureTestFaceRecognitionModelsPath(path string) {