fix(move): resolve full per-photo file list so videos actually move
The move resolved photos via the /photos search, whose merged Files array is trimmed (often omitting a photo's video file) and which applies PhotoPrism's quality/review/archive filters — so a video's .mov was never listed to move and nothing happened. Resolve each UID via GET /photos/:uid instead (full file list, no filters), shared by photos-move and heap-convert via resolvePhotosFull. Unresolved UIDs are reported as skipped rather than aborting the batch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -90,9 +90,10 @@ func handleHeapConvert(cfg *Config, pp *ppClient) gin.HandlerFunc {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid targetFolder"})
|
||||
return
|
||||
}
|
||||
// Pull the heap's photos via the q=album:UID query. count=1000 covers
|
||||
// every realistic heap; merged=true expands stacked variants so we
|
||||
// move the JPG/HEIC sibling alongside the primary.
|
||||
// Pull the heap's membership via the q=album:UID query (count=1000
|
||||
// covers every realistic heap). We only need the UID list here — the
|
||||
// search's Files array is trimmed and drops videos, so we re-resolve
|
||||
// each photo's full file set below via resolvePhotosFull.
|
||||
q := url.QueryEscape("album:" + albumUID)
|
||||
listURL := "/api/v1/photos?q=" + q + "&count=1000&merged=true"
|
||||
resp, err := pp.call(c.Request.Context(), http.MethodGet, listURL, token, nil)
|
||||
@@ -104,17 +105,31 @@ func handleHeapConvert(cfg *Config, pp *ppClient) gin.HandlerFunc {
|
||||
c.JSON(resp.Status, gin.H{"error": "list photos failed"})
|
||||
return
|
||||
}
|
||||
var photos []heapPhoto
|
||||
if err := json.Unmarshal(resp.Body, &photos); err != nil {
|
||||
var listed []heapPhoto
|
||||
if err := json.Unmarshal(resp.Body, &listed); err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "decode photo list"})
|
||||
return
|
||||
}
|
||||
uids := make([]string, 0, len(listed))
|
||||
for _, p := range listed {
|
||||
uids = append(uids, p.UID)
|
||||
}
|
||||
|
||||
// Re-fetch each photo's complete file list so videos (and other multi-
|
||||
// file photos) move whole — the album search alone would orphan the
|
||||
// .mov. See resolvePhotosFull.
|
||||
photos, resolveErrs, err := resolvePhotosFull(c.Request.Context(), pp, token, uids)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
moved, copied, errs, err := movePhotoFiles(cfg, pp, token, photos, targetAbs, subfolder, mode)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
errs = append(resolveErrs, errs...)
|
||||
|
||||
heapDeleted := false
|
||||
if deleteHeap {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -57,32 +57,24 @@ func handlePhotosMove(cfg *Config, pp *ppClient) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve the photos via a single q=uid:a|b|c query. PhotoPrism's
|
||||
// search treats `|` as OR within a filter value, so one round-trip
|
||||
// covers the whole selection; merged=true pulls stacked variants so
|
||||
// the JPG/HEIC sibling travels with its primary.
|
||||
q := url.QueryEscape("uid:" + strings.Join(body.UIDs, "|"))
|
||||
listURL := "/api/v1/photos?q=" + q + "&count=" + itoa(len(body.UIDs)) + "&merged=true"
|
||||
resp, err := pp.call(c.Request.Context(), http.MethodGet, listURL, token, nil)
|
||||
// Resolve each photo's FULL file list via the single-photo endpoint
|
||||
// rather than the /photos search (see resolvePhotosFull) — the search
|
||||
// drops a photo's video file from its trimmed Files array and filters
|
||||
// videos out by quality/review, so the .mov never gets listed to move.
|
||||
photos, resolveErrs, err := resolvePhotosFull(c.Request.Context(), pp, token, body.UIDs)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !resp.OK {
|
||||
c.JSON(resp.Status, gin.H{"error": "list photos failed"})
|
||||
return
|
||||
}
|
||||
var photos []heapPhoto
|
||||
if err := json.Unmarshal(resp.Body, &photos); err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "decode photo list"})
|
||||
return
|
||||
}
|
||||
|
||||
moved, copied, errs, err := movePhotoFiles(cfg, pp, token, photos, targetAbs, subfolder, mode)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
// Surface UIDs PhotoPrism couldn't resolve alongside any per-file
|
||||
// errors so the client's "N skipped" summary stays accurate.
|
||||
errs = append(resolveErrs, errs...)
|
||||
|
||||
slog.Info("photos.move",
|
||||
"requested", len(body.UIDs),
|
||||
@@ -99,6 +91,36 @@ func handlePhotosMove(cfg *Config, pp *ppClient) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// resolvePhotosFull fetches each photo's complete file list via the
|
||||
// single-photo endpoint (GET /photos/:uid). Use this instead of the /photos
|
||||
// search whenever you need every file of a photo: the search — even with
|
||||
// merged=true — can return a trimmed Files array that omits the photo's video
|
||||
// file, and it applies PhotoPrism's default quality/review/archive filters.
|
||||
// Both silently drop videos (which PhotoPrism routinely files under review)
|
||||
// from a move. The per-UID lookup returns every file and ignores those
|
||||
// filters. UIDs PhotoPrism can't resolve are returned in `errs` so the batch
|
||||
// continues; a transport-level failure aborts with a fatal error. Mirrors
|
||||
// handleRename's single-photo resolution.
|
||||
func resolvePhotosFull(ctx context.Context, pp *ppClient, token string, uids []string) (photos []heapPhoto, errs []heapErr, err error) {
|
||||
photos = make([]heapPhoto, 0, len(uids))
|
||||
for _, uid := range uids {
|
||||
resp, e := pp.call(ctx, http.MethodGet, "/api/v1/photos/"+url.PathEscape(uid), token, nil)
|
||||
if e != nil {
|
||||
return nil, nil, e
|
||||
}
|
||||
if !resp.OK {
|
||||
errs = append(errs, heapErr{UID: uid, Reason: "photo not found"})
|
||||
continue
|
||||
}
|
||||
var p heapPhoto
|
||||
if e := json.Unmarshal(resp.Body, &p); e != nil {
|
||||
return nil, nil, e
|
||||
}
|
||||
photos = append(photos, p)
|
||||
}
|
||||
return photos, errs, nil
|
||||
}
|
||||
|
||||
type folderMoveBody struct {
|
||||
// Originals-relative destination parent. ""/"/"/"." mean the root.
|
||||
TargetParent string `json:"targetParent"`
|
||||
|
||||
Reference in New Issue
Block a user