精简后的项目:仅保留 api/ui 源码、文档、构建依赖
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
package processing_tasks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding"
|
||||
"github.com/photoview/photoview/api/scanner/media_type"
|
||||
"github.com/photoview/photoview/api/scanner/scanner_task"
|
||||
"github.com/photoview/photoview/api/utils"
|
||||
)
|
||||
|
||||
type CounterpartFilesTask struct {
|
||||
scanner_task.ScannerTaskBase
|
||||
}
|
||||
|
||||
func (t CounterpartFilesTask) MediaFound(ctx scanner_task.TaskContext, fileInfo fs.FileInfo, mediaPath string) (skip bool, err error) {
|
||||
fileType := media_type.GetMediaType(mediaPath)
|
||||
|
||||
if !fileType.IsSupported() {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if utils.EnvDisableRawProcessing.GetBool() {
|
||||
if !fileType.IsWebCompatible() {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Don't skip the JPEGs if raw processing is disabled. Treat them as standalone files.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if fileType.IsWebCompatible() {
|
||||
_, existed := media_type.FindRawCounterpart(mediaPath)
|
||||
if existed {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (t CounterpartFilesTask) BeforeProcessMedia(ctx scanner_task.TaskContext, mediaData *media_encoding.EncodeMediaData) (scanner_task.TaskContext, error) {
|
||||
|
||||
mediaType := ctx.GetCache().GetMediaType(mediaData.Media.Path)
|
||||
if mediaType == media_type.TypeUnknown {
|
||||
return ctx, fmt.Errorf("scan for counterpart file %s failed: media type is %s", mediaData.Media.Path, mediaType)
|
||||
}
|
||||
|
||||
if mediaType.IsWebCompatible() {
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
counterpartFile, ok := media_type.FindWebCounterpart(mediaData.Media.Path)
|
||||
if !ok {
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
mediaData.CounterpartPath = &counterpartFile
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package processing_tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/photoview/photoview/api/scanner/scanner_task"
|
||||
"github.com/photoview/photoview/api/test_utils"
|
||||
"github.com/photoview/photoview/api/utils"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
test_utils.IntegrationTestRun(m)
|
||||
}
|
||||
|
||||
func TestCounterpartFilesTaskMediaFound(t *testing.T) {
|
||||
mediaPath := test_utils.PathFromAPIRoot("scanner", "test_media", "real_media")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
file string
|
||||
disableRawProcessing bool
|
||||
wantSkip bool
|
||||
}{
|
||||
{
|
||||
name: "StandaloneProcessRaw",
|
||||
file: "standalone_jpg.jpg",
|
||||
disableRawProcessing: false,
|
||||
wantSkip: false,
|
||||
},
|
||||
{
|
||||
name: "StandaloneNotProcessRaw",
|
||||
file: "standalone_jpg.jpg",
|
||||
disableRawProcessing: true,
|
||||
wantSkip: false,
|
||||
},
|
||||
{
|
||||
name: "RawJpegProcessRaw",
|
||||
file: "raw_with_jpg.jpg",
|
||||
disableRawProcessing: false,
|
||||
wantSkip: true,
|
||||
},
|
||||
{
|
||||
name: "RawJpegNotProcessRaw",
|
||||
file: "raw_with_jpg.jpg",
|
||||
disableRawProcessing: true,
|
||||
wantSkip: false,
|
||||
},
|
||||
{
|
||||
name: "RawProcessRaw",
|
||||
file: "raw_with_jpg.tiff",
|
||||
disableRawProcessing: false,
|
||||
wantSkip: false,
|
||||
},
|
||||
{
|
||||
name: "RawNotProcessRaw",
|
||||
file: "raw_with_jpg.tiff",
|
||||
disableRawProcessing: true,
|
||||
wantSkip: true,
|
||||
},
|
||||
{
|
||||
name: "UnknownProcessRaw",
|
||||
file: "file.pdf",
|
||||
disableRawProcessing: false,
|
||||
wantSkip: true,
|
||||
},
|
||||
{
|
||||
name: "UnknownNotProcessRaw",
|
||||
file: "file.pdf",
|
||||
disableRawProcessing: true,
|
||||
wantSkip: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Setenv(string(utils.EnvDisableRawProcessing), fmt.Sprintf("%v", tc.disableRawProcessing))
|
||||
|
||||
ctx := scanner_task.NewTaskContext(context.Background(), nil, nil, nil)
|
||||
|
||||
fname := filepath.Join(mediaPath, tc.file)
|
||||
fi, err := os.Stat(fname)
|
||||
if err != nil {
|
||||
t.Fatalf("Stat(%q) error: %v", fname, err)
|
||||
}
|
||||
|
||||
var task CounterpartFilesTask
|
||||
got, err := task.MediaFound(ctx, fi, fname)
|
||||
if err != nil {
|
||||
t.Fatalf("task.MediaFound(ctx, %q) error: %v", fname, err)
|
||||
}
|
||||
|
||||
if got, want := got, tc.wantSkip; got != want {
|
||||
t.Errorf("task.MediaFound(ctx, %q) = (skip)%v, want skip: %v", fname, got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package processing_tasks
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/photoview/photoview/api/log"
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding"
|
||||
"github.com/photoview/photoview/api/scanner/scanner_task"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type ProcessPhotoTask struct {
|
||||
scanner_task.ScannerTaskBase
|
||||
}
|
||||
|
||||
func (t ProcessPhotoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *media_encoding.EncodeMediaData, mediaCachePath string) ([]*models.MediaURL, error) {
|
||||
if mediaData.Media.Type != models.MediaTypePhoto {
|
||||
return []*models.MediaURL{}, nil
|
||||
}
|
||||
|
||||
updatedURLs := make([]*models.MediaURL, 0)
|
||||
photo := mediaData.Media
|
||||
|
||||
log.Info(ctx, "Processing photo", "photo", photo.Path)
|
||||
|
||||
photoURLFromDB := makePhotoURLChecker(ctx.GetDB(), photo.ID)
|
||||
|
||||
// original photo url
|
||||
origURL, err := photoURLFromDB(models.MediaOriginal)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, err
|
||||
}
|
||||
|
||||
// Thumbnail
|
||||
thumbURL, err := photoURLFromDB(models.PhotoThumbnail)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrap(err, "error processing photo thumbnail")
|
||||
}
|
||||
|
||||
// Highres
|
||||
highResURL, err := photoURLFromDB(models.PhotoHighRes)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrap(err, "error processing photo highres")
|
||||
}
|
||||
|
||||
var baseImagePath string = photo.Path
|
||||
|
||||
// Generate high res jpeg
|
||||
if highResURL == nil {
|
||||
|
||||
contentType, err := mediaData.ContentType()
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, err
|
||||
}
|
||||
|
||||
if !contentType.IsWebCompatible() {
|
||||
highresName := generateUniqueMediaNamePrefixed("highres", photo.Path, ".jpg")
|
||||
baseImagePath = path.Join(mediaCachePath, highresName)
|
||||
|
||||
highRes, err := generateSaveHighResJPEG(ctx.GetDB(), photo, mediaData, highresName, baseImagePath, nil)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, err
|
||||
}
|
||||
|
||||
updatedURLs = append(updatedURLs, highRes)
|
||||
}
|
||||
} else {
|
||||
// Verify that highres photo still exists in cache
|
||||
baseImagePath = path.Join(mediaCachePath, highResURL.MediaName)
|
||||
|
||||
if _, err := os.Stat(baseImagePath); os.IsNotExist(err) {
|
||||
log.Info(ctx, "High-res photo found in database but not in cache, re-encoding photo to cache", "media_name", highResURL.MediaName)
|
||||
updatedURLs = append(updatedURLs, highResURL)
|
||||
|
||||
err = mediaData.EncodeHighRes(baseImagePath)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrap(err, "creating high-res cached image")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save original photo to database
|
||||
if origURL == nil {
|
||||
|
||||
// Make sure photo dimensions is set
|
||||
photoDimensions, err := media_encoding.GetPhotoDimensions(photo.Path)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, err
|
||||
}
|
||||
|
||||
original, err := saveOriginalPhotoToDB(ctx.GetDB(), photo, mediaData, photoDimensions)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrap(err, "saving original photo to database")
|
||||
}
|
||||
|
||||
updatedURLs = append(updatedURLs, original)
|
||||
}
|
||||
|
||||
// Save thumbnail to cache
|
||||
if thumbURL == nil {
|
||||
thumbnailName := generateUniqueMediaNamePrefixed("thumbnail", photo.Path, ".jpg")
|
||||
thumbnail, err := generateSaveThumbnailJPEG(ctx.GetDB(), photo, thumbnailName, mediaCachePath, baseImagePath, nil)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, err
|
||||
}
|
||||
|
||||
updatedURLs = append(updatedURLs, thumbnail)
|
||||
} else {
|
||||
// Verify that thumbnail photo still exists in cache
|
||||
thumbPath := path.Join(mediaCachePath, thumbURL.MediaName)
|
||||
|
||||
if _, err := os.Stat(thumbPath); os.IsNotExist(err) {
|
||||
updatedURLs = append(updatedURLs, thumbURL)
|
||||
log.Info(ctx, "Thumbnail photo found in database but not in cache, re-encoding photo to cache", "media_name", thumbURL.MediaName)
|
||||
|
||||
_, err := media_encoding.EncodeThumbnail(ctx.GetDB(), baseImagePath, thumbPath)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrap(err, "could not create thumbnail cached image")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updatedURLs, nil
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package processing_tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/photoview/photoview/api/log"
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding"
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding/executable_worker"
|
||||
"github.com/photoview/photoview/api/scanner/scanner_task"
|
||||
"github.com/photoview/photoview/api/utils"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/vansante/go-ffprobe.v2"
|
||||
)
|
||||
|
||||
type ProcessVideoTask struct {
|
||||
scanner_task.ScannerTaskBase
|
||||
}
|
||||
|
||||
func (t ProcessVideoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *media_encoding.EncodeMediaData, mediaCachePath string) ([]*models.MediaURL, error) {
|
||||
if mediaData.Media.Type != models.MediaTypeVideo {
|
||||
return []*models.MediaURL{}, nil
|
||||
}
|
||||
|
||||
updatedURLs := make([]*models.MediaURL, 0)
|
||||
video := mediaData.Media
|
||||
|
||||
log.Info(ctx, "Processing video", "video", video.Path)
|
||||
|
||||
mediaURLFromDB := makePhotoURLChecker(ctx.GetDB(), video.ID)
|
||||
|
||||
videoOriginalURL, err := mediaURLFromDB(models.MediaOriginal)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrap(err, "error processing video original format")
|
||||
}
|
||||
|
||||
videoWebURL, err := mediaURLFromDB(models.VideoWeb)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrap(err, "error processing video web-format")
|
||||
}
|
||||
|
||||
videoThumbnailURL, err := mediaURLFromDB(models.VideoThumbnail)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrap(err, "error processing video thumbnail")
|
||||
}
|
||||
|
||||
videoType, err := mediaData.ContentType()
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, fmt.Errorf("getting video content type error: %w", err)
|
||||
}
|
||||
|
||||
if videoOriginalURL == nil && videoType.IsWebCompatible() {
|
||||
origVideoPath := video.Path
|
||||
videoMediaName := generateUniqueMediaName(video.Path)
|
||||
|
||||
webMetadata, err := ReadVideoStreamMetadata(origVideoPath)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrapf(err, "failed to read metadata for original video (%s)", video.Title)
|
||||
}
|
||||
|
||||
fileStats, err := os.Stat(origVideoPath)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrap(err, "reading file stats of original video")
|
||||
}
|
||||
|
||||
mediaURL := models.MediaURL{
|
||||
MediaID: video.ID,
|
||||
MediaName: videoMediaName,
|
||||
Width: webMetadata.Width,
|
||||
Height: webMetadata.Height,
|
||||
Purpose: models.MediaOriginal,
|
||||
ContentType: videoType.String(),
|
||||
FileSize: fileStats.Size(),
|
||||
}
|
||||
|
||||
if err := ctx.GetDB().Create(&mediaURL).Error; err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrapf(err, "insert original video into database (%s)", video.Title)
|
||||
}
|
||||
|
||||
updatedURLs = append(updatedURLs, &mediaURL)
|
||||
}
|
||||
|
||||
if videoWebURL == nil && !videoType.IsWebCompatible() {
|
||||
webVideoName := fmt.Sprintf("web_video_%s_%s", path.Base(video.Path), utils.GenerateToken())
|
||||
webVideoName = strings.ReplaceAll(webVideoName, ".", "_")
|
||||
webVideoName = strings.ReplaceAll(webVideoName, " ", "_")
|
||||
webVideoName = webVideoName + ".mp4"
|
||||
|
||||
webVideoPath := path.Join(mediaCachePath, webVideoName)
|
||||
|
||||
err = executable_worker.Ffmpeg.EncodeMp4(video.Path, webVideoPath)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrapf(err, "could not encode mp4 video (%s)", video.Path)
|
||||
}
|
||||
|
||||
webMetadata, err := ReadVideoStreamMetadata(webVideoPath)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrapf(err, "failed to read metadata for encoded web-video (%s)", video.Title)
|
||||
}
|
||||
|
||||
fileStats, err := os.Stat(webVideoPath)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrap(err, "reading file stats of web-optimized video")
|
||||
}
|
||||
|
||||
mediaURL := models.MediaURL{
|
||||
MediaID: video.ID,
|
||||
MediaName: webVideoName,
|
||||
Width: webMetadata.Width,
|
||||
Height: webMetadata.Height,
|
||||
Purpose: models.VideoWeb,
|
||||
ContentType: "video/mp4",
|
||||
FileSize: fileStats.Size(),
|
||||
}
|
||||
|
||||
if err := ctx.GetDB().Create(&mediaURL).Error; err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrapf(err, "failed to insert encoded web-video into database (%s)", video.Title)
|
||||
}
|
||||
|
||||
updatedURLs = append(updatedURLs, &mediaURL)
|
||||
}
|
||||
|
||||
probeData, err := mediaData.VideoMetadata()
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, err
|
||||
}
|
||||
|
||||
if videoThumbnailURL == nil {
|
||||
videoThumbName := fmt.Sprintf("video_thumb_%s_%s", path.Base(video.Path), utils.GenerateToken())
|
||||
videoThumbName = strings.ReplaceAll(videoThumbName, ".", "_")
|
||||
videoThumbName = strings.ReplaceAll(videoThumbName, " ", "_")
|
||||
videoThumbName = videoThumbName + ".jpg"
|
||||
|
||||
thumbImagePath := path.Join(mediaCachePath, videoThumbName)
|
||||
|
||||
err = executable_worker.Ffmpeg.EncodeVideoThumbnail(video.Path, thumbImagePath, probeData)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrapf(err, "failed to generate thumbnail for video (%s)", video.Title)
|
||||
}
|
||||
|
||||
thumbDimensions, err := media_encoding.GetPhotoDimensions(thumbImagePath)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrap(err, "get dimensions of video thumbnail image")
|
||||
}
|
||||
|
||||
fileStats, err := os.Stat(thumbImagePath)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrap(err, "reading file stats of video thumbnail")
|
||||
}
|
||||
|
||||
thumbMediaURL := models.MediaURL{
|
||||
MediaID: video.ID,
|
||||
MediaName: videoThumbName,
|
||||
Width: thumbDimensions.Width,
|
||||
Height: thumbDimensions.Height,
|
||||
Purpose: models.VideoThumbnail,
|
||||
ContentType: "image/jpeg",
|
||||
FileSize: fileStats.Size(),
|
||||
}
|
||||
|
||||
if err := ctx.GetDB().Create(&thumbMediaURL).Error; err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrapf(err, "failed to insert video thumbnail image into database (%s)", video.Title)
|
||||
}
|
||||
|
||||
updatedURLs = append(updatedURLs, &thumbMediaURL)
|
||||
} else {
|
||||
// Verify that video thumbnail still exists in cache
|
||||
thumbImagePath := path.Join(mediaCachePath, videoThumbnailURL.MediaName)
|
||||
|
||||
if _, err := os.Stat(thumbImagePath); os.IsNotExist(err) {
|
||||
log.Info(ctx, "Video thumbnail found in database but not in cache, re-encoding video thumbnail to cache", "video", videoThumbnailURL.MediaName)
|
||||
updatedURLs = append(updatedURLs, videoThumbnailURL)
|
||||
|
||||
err = executable_worker.Ffmpeg.EncodeVideoThumbnail(video.Path, thumbImagePath, probeData)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrapf(err, "failed to generate thumbnail for video (%s)", video.Title)
|
||||
}
|
||||
|
||||
thumbDimensions, err := media_encoding.GetPhotoDimensions(thumbImagePath)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrap(err, "get dimensions of video thumbnail image")
|
||||
}
|
||||
|
||||
fileStats, err := os.Stat(thumbImagePath)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrap(err, "reading file stats of video thumbnail")
|
||||
}
|
||||
|
||||
videoThumbnailURL.Width = thumbDimensions.Width
|
||||
videoThumbnailURL.Height = thumbDimensions.Height
|
||||
videoThumbnailURL.FileSize = fileStats.Size()
|
||||
|
||||
if err := ctx.GetDB().Save(videoThumbnailURL).Error; err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrap(err, "updating video thumbnail url in database after re-encoding")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updatedURLs, nil
|
||||
}
|
||||
|
||||
func ReadVideoMetadata(videoPath string) (*ffprobe.ProbeData, error) {
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), utils.MediaProbeTimeout())
|
||||
defer cancelFn()
|
||||
|
||||
data, err := ffprobe.ProbeURL(ctx, videoPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not read video metadata (%s)", path.Base(videoPath))
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func ReadVideoStreamMetadata(videoPath string) (*ffprobe.Stream, error) {
|
||||
data, err := ReadVideoMetadata(videoPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "read video stream metadata")
|
||||
}
|
||||
|
||||
stream := data.FirstVideoStream()
|
||||
if stream == nil {
|
||||
return nil, fmt.Errorf("could not get stream from file metadata (%s)", path.Base(videoPath))
|
||||
}
|
||||
|
||||
return stream, nil
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package processing_tasks
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func generateSaveHighResJPEG(tx *gorm.DB, media *models.Media, imageData *media_encoding.EncodeMediaData, highResName string, imagePath string, mediaURL *models.MediaURL) (*models.MediaURL, error) {
|
||||
|
||||
err := imageData.EncodeHighRes(imagePath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "creating high-res cached image")
|
||||
}
|
||||
|
||||
photoDimensions, err := media_encoding.GetPhotoDimensions(imagePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileStats, err := os.Stat(imagePath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "reading file stats of highres photo")
|
||||
}
|
||||
|
||||
if mediaURL == nil {
|
||||
|
||||
mediaURL = &models.MediaURL{
|
||||
MediaID: media.ID,
|
||||
MediaName: highResName,
|
||||
Width: photoDimensions.Width,
|
||||
Height: photoDimensions.Height,
|
||||
Purpose: models.PhotoHighRes,
|
||||
ContentType: "image/jpeg",
|
||||
FileSize: fileStats.Size(),
|
||||
}
|
||||
|
||||
if err := tx.Create(&mediaURL).Error; err != nil {
|
||||
return nil, errors.Wrapf(err, "could not insert highres media url (%d, %s)", media.ID, highResName)
|
||||
}
|
||||
} else {
|
||||
mediaURL.Width = photoDimensions.Width
|
||||
mediaURL.Height = photoDimensions.Height
|
||||
mediaURL.FileSize = fileStats.Size()
|
||||
|
||||
if err := tx.Save(&mediaURL).Error; err != nil {
|
||||
return nil, errors.Wrapf(err, "could not update media url after side car changes (%d, %s)", media.ID, highResName)
|
||||
}
|
||||
}
|
||||
|
||||
return mediaURL, nil
|
||||
}
|
||||
|
||||
func generateSaveThumbnailJPEG(tx *gorm.DB, media *models.Media, thumbnailName string, photoCachePath string, baseImagePath string, mediaURL *models.MediaURL) (*models.MediaURL, error) {
|
||||
thumbOutputPath := path.Join(photoCachePath, thumbnailName)
|
||||
|
||||
thumbSize, err := media_encoding.EncodeThumbnail(tx, baseImagePath, thumbOutputPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create thumbnail cached image")
|
||||
}
|
||||
|
||||
fileStats, err := os.Stat(thumbOutputPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "reading file stats of thumbnail photo")
|
||||
}
|
||||
|
||||
if mediaURL == nil {
|
||||
|
||||
mediaURL = &models.MediaURL{
|
||||
MediaID: media.ID,
|
||||
MediaName: thumbnailName,
|
||||
Width: thumbSize.Width,
|
||||
Height: thumbSize.Height,
|
||||
Purpose: models.PhotoThumbnail,
|
||||
ContentType: "image/jpeg",
|
||||
FileSize: fileStats.Size(),
|
||||
}
|
||||
|
||||
if err := tx.Create(&mediaURL).Error; err != nil {
|
||||
return nil, errors.Wrapf(err, "could not insert thumbnail media url (%d, %s)", media.ID, thumbnailName)
|
||||
}
|
||||
} else {
|
||||
mediaURL.Width = thumbSize.Width
|
||||
mediaURL.Height = thumbSize.Height
|
||||
mediaURL.FileSize = fileStats.Size()
|
||||
|
||||
if err := tx.Save(&mediaURL).Error; err != nil {
|
||||
return nil, errors.Wrapf(err, "could not update media url after side car changes (%d, %s)", media.ID, thumbnailName)
|
||||
}
|
||||
}
|
||||
|
||||
return mediaURL, nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package processing_tasks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding"
|
||||
"github.com/photoview/photoview/api/utils"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Higher order function used to check if MediaURL for a given MediaPurpose exists
|
||||
func makePhotoURLChecker(tx *gorm.DB, mediaID int) func(purpose models.MediaPurpose) (*models.MediaURL, error) {
|
||||
return func(purpose models.MediaPurpose) (*models.MediaURL, error) {
|
||||
var mediaURL []*models.MediaURL
|
||||
|
||||
result := tx.Where("purpose = ?", purpose).Where("media_id = ?", mediaID).Find(&mediaURL)
|
||||
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
if result.RowsAffected > 0 {
|
||||
return mediaURL[0], nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func generateUniqueMediaNamePrefixed(prefix string, mediaPath string, extension string) string {
|
||||
mediaName := fmt.Sprintf("%s_%s_%s", prefix, path.Base(mediaPath), utils.GenerateToken())
|
||||
mediaName = models.SanitizeMediaName(mediaName)
|
||||
mediaName = mediaName + extension
|
||||
return mediaName
|
||||
}
|
||||
|
||||
func generateUniqueMediaName(mediaPath string) string {
|
||||
|
||||
filename := path.Base(mediaPath)
|
||||
baseName := filename[0 : len(filename)-len(path.Ext(filename))]
|
||||
baseExt := path.Ext(filename)
|
||||
|
||||
mediaName := fmt.Sprintf("%s_%s", baseName, utils.GenerateToken())
|
||||
mediaName = models.SanitizeMediaName(mediaName) + baseExt
|
||||
|
||||
return mediaName
|
||||
}
|
||||
|
||||
func saveOriginalPhotoToDB(tx *gorm.DB, photo *models.Media, imageData *media_encoding.EncodeMediaData, photoDimensions media_encoding.Dimension) (*models.MediaURL, error) {
|
||||
originalImageName := generateUniqueMediaName(photo.Path)
|
||||
|
||||
contentType, err := imageData.ContentType()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileStats, err := os.Stat(photo.Path)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "reading file stats of original photo")
|
||||
}
|
||||
|
||||
mediaURL := models.MediaURL{
|
||||
Media: photo,
|
||||
MediaName: originalImageName,
|
||||
Width: photoDimensions.Width,
|
||||
Height: photoDimensions.Height,
|
||||
Purpose: models.MediaOriginal,
|
||||
ContentType: contentType.String(),
|
||||
FileSize: fileStats.Size(),
|
||||
}
|
||||
|
||||
if err := tx.Create(&mediaURL).Error; err != nil {
|
||||
return nil, errors.Wrapf(err, "inserting original photo url: %d, %s", photo.ID, photo.Title)
|
||||
}
|
||||
|
||||
return &mediaURL, nil
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package processing_tasks
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding"
|
||||
"github.com/photoview/photoview/api/scanner/media_type"
|
||||
"github.com/photoview/photoview/api/scanner/scanner_task"
|
||||
"github.com/photoview/photoview/api/scanner/scanner_utils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type SidecarTask struct {
|
||||
scanner_task.ScannerTaskBase
|
||||
}
|
||||
|
||||
func (t SidecarTask) AfterMediaFound(ctx scanner_task.TaskContext, media *models.Media, newMedia bool) error {
|
||||
if media.Type != models.MediaTypePhoto || !newMedia {
|
||||
return nil
|
||||
}
|
||||
|
||||
mediaType := ctx.GetCache().GetMediaType(media.Path)
|
||||
if mediaType == media_type.TypeUnknown {
|
||||
return fmt.Errorf("scan for sidecar file %s failed: media type is %s", media.Path, mediaType)
|
||||
}
|
||||
|
||||
if mediaType.IsWebCompatible() {
|
||||
return nil
|
||||
}
|
||||
|
||||
var sideCarPath *string = nil
|
||||
var sideCarHash *string = nil
|
||||
|
||||
sideCarPath = scanForSideCarFile(media.Path)
|
||||
if sideCarPath != nil {
|
||||
sideCarHash = hashSideCarFile(sideCarPath)
|
||||
}
|
||||
|
||||
// Add sidecar data to media
|
||||
media.SideCarPath = sideCarPath
|
||||
media.SideCarHash = sideCarHash
|
||||
if err := ctx.GetDB().Save(media).Error; err != nil {
|
||||
return errors.Wrapf(err, "update media sidecar info (%s)", *sideCarPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t SidecarTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *media_encoding.EncodeMediaData, mediaCachePath string) (updatedURLs []*models.MediaURL, err error) {
|
||||
mediaType, err := mediaData.ContentType()
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrap(err, "sidecar task, process media")
|
||||
}
|
||||
|
||||
if mediaType.IsWebCompatible() {
|
||||
return []*models.MediaURL{}, nil
|
||||
}
|
||||
|
||||
photo := mediaData.Media
|
||||
|
||||
sideCarFileHasChanged := false
|
||||
var currentFileHash *string
|
||||
currentSideCarPath := scanForSideCarFile(photo.Path)
|
||||
|
||||
if currentSideCarPath != nil {
|
||||
currentFileHash = hashSideCarFile(currentSideCarPath)
|
||||
if photo.SideCarHash == nil || *photo.SideCarHash != *currentFileHash {
|
||||
sideCarFileHasChanged = true
|
||||
}
|
||||
} else if photo.SideCarPath != nil { // sidecar has been deleted since last scan
|
||||
sideCarFileHasChanged = true
|
||||
}
|
||||
|
||||
if !sideCarFileHasChanged {
|
||||
return []*models.MediaURL{}, nil
|
||||
}
|
||||
|
||||
fmt.Printf("Detected changed sidecar file for %s recreating JPG's to reflect changes\n", photo.Path)
|
||||
|
||||
highResURL, err := photo.GetHighRes()
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrap(err, "sidecar task, get high-res media_url")
|
||||
}
|
||||
|
||||
thumbURL, err := photo.GetThumbnail()
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrap(err, "sidecar task, get high-res media_url")
|
||||
}
|
||||
|
||||
// update high res image may be cropped so dimentions and file size can change
|
||||
baseImagePath := path.Join(mediaCachePath, highResURL.MediaName) // update base image path for thumbnail
|
||||
tempHighResPath := baseImagePath + ".hold"
|
||||
os.Rename(baseImagePath, tempHighResPath)
|
||||
updatedHighRes, err := generateSaveHighResJPEG(ctx.GetDB(), photo, mediaData, highResURL.MediaName, baseImagePath, highResURL)
|
||||
if err != nil {
|
||||
os.Rename(tempHighResPath, baseImagePath)
|
||||
return []*models.MediaURL{}, errors.Wrap(err, "sidecar task, recreating high-res cached image")
|
||||
}
|
||||
os.Remove(tempHighResPath)
|
||||
|
||||
// update thumbnail image may be cropped so dimentions and file size can change
|
||||
thumbPath := path.Join(mediaCachePath, thumbURL.MediaName)
|
||||
tempThumbPath := thumbPath + ".hold" // hold onto the original image incase for some reason we fail to recreate one with the new settings
|
||||
os.Rename(thumbPath, tempThumbPath)
|
||||
updatedThumbnail, err := generateSaveThumbnailJPEG(ctx.GetDB(), photo, thumbURL.MediaName, mediaCachePath, baseImagePath, thumbURL)
|
||||
if err != nil {
|
||||
os.Rename(tempThumbPath, thumbPath)
|
||||
return []*models.MediaURL{}, errors.Wrap(err, "recreating thumbnail cached image")
|
||||
}
|
||||
os.Remove(tempThumbPath)
|
||||
|
||||
photo.SideCarHash = currentFileHash
|
||||
photo.SideCarPath = currentSideCarPath
|
||||
|
||||
// save new side car hash
|
||||
if err := ctx.GetDB().Save(&photo).Error; err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrapf(err, "could not update side car hash for media: %s", photo.Path)
|
||||
}
|
||||
|
||||
return []*models.MediaURL{
|
||||
updatedThumbnail,
|
||||
updatedHighRes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func scanForSideCarFile(path string) *string {
|
||||
testPath := path + ".xmp"
|
||||
|
||||
if scanner_utils.FileExists(testPath) {
|
||||
return &testPath
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func hashSideCarFile(path *string) *string {
|
||||
if path == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
f, err := os.Open(*path)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: %s", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := md5.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
log.Printf("ERROR: %s", err)
|
||||
}
|
||||
hash := hex.EncodeToString(h.Sum(nil))
|
||||
return &hash
|
||||
}
|
||||
Reference in New Issue
Block a user