精简后的项目:仅保留 api/ui 源码、文档、构建依赖

This commit is contained in:
gallery
2026-09-11 16:11:09 +08:00
commit 490e85634e
457 changed files with 86862 additions and 0 deletions
@@ -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
}