Folder search lowercases the query term, but album_path is VARBINARY and
compared byte-exact (case-sensitive) on MySQL/MariaDB, so a lowercased
term no longer matched uppercase folder paths. A parent folder still
appeared via its case-insensitive title, while child folders - matchable
only by path - dropped out of the results.
Add a dialect-aware PathLike helper: MySQL converts album_path to a
case-insensitive collation for the LIKE, SQLite and other dialects fall
back to a plain LIKE (SQLite is already ASCII case-insensitive). Only the
folder search filter is affected; byte-exact album_path identity lookups
are unchanged, so emoji and case-distinct folders stay distinct.
Pictures shared only through a folder, moment, calendar, or region link
have no photos_albums row, so the by-UID and by-hash visibility checks
reported them out of scope and returned "Entity not found" - for example
when downloading a single image from a shared folder. The photo and file
visibility helpers now fall back to the shared album's filter, matching
what browsing and full-album downloads already allow. The fallback runs
only after the cheaper personal-scope check misses and skips regular
albums, so full-access and regular-share sessions add no queries.
* Tests: fix random selection of deleted record causing test failure
* Tests: fix test folders being left after test
* Tests: ensure that required records are available
* Tests: make each package execute against it's own testdata folder to ensure no interpackage test failures
* Tests: add unit tests of CleanupTestFolder
* Tests: Allow defer in TestMain
* Tests: rename testMain to runTestMain
When an XMP sidecar provides GPS coordinates, call UpdateLocation()
immediately so the S2 cell is resolved and cached in the local DB.
Without this, subsequent re-indexes always triggered a live geocoding
request (even though the coordinates were already known), producing a
spurious error on every re-index pass.
Downgrade the geocoding failure log in photo_location.go and
places/cell.go from Errorf to Warnf: a transient network failure to
the places service is non-fatal (the photo is indexed successfully,
just without a resolved place name) and must not surface as an
indexing error in the test suite.
* Frontend: Add settings for albums, favorites, folders, and media features
* Frontend: Enhance download settings for files and albums #848
* Frontend: Enhance download settings for files and albums #848
* Frontend: Update album download settings to restrict access to super admins
* Frontend: Add album sorting options to settings for super admins
* Frontend: Update download settings visibility
* Frontend: Refine download settings access control for user roles
* Tests: Add album, folder, moment, state, and month order selection options to page-model
* Frontend: Update access control for super admins in settings to include scope check
* Tests: Add new tests for download name and album order settings persistence
* Tests: Implement new tests for default album sort order persistence in settings
* Frontend: Move default album order settings
* Frontend: Add zoom accessibility option in settings #799
* Frontend: Update download name from "Share Identifier" to "Share Friendly" and refine hints for download options in settings
* Tests: Add new acceptance test for disabling album downloads while allowing file downloads in settings
* Frontend: Improve download options layout and hints
* Frontend: Refactor download options condition in settings page
* Frontend: Implement native zoom control in lightbox component
* Settings: Add Collections tab and move Services to its own page #848#5429
- Add a super-admin Collections tab after Content holding the album
Download and Sort Order settings; remove those cards from Content.
- Move Services from a settings tab to a dedicated page
(page/services.vue) with a search toolbar and a Reload / Learn More
action menu; link it from the navigation settings menu.
- Implement server-side services search (acc_name filter) with tests.
- De-duplicate the picture sort-order options into a shared
SortOrderOptions builder in options.js.
- Update acceptance/unit tests and page models to match.
* Frontend: Regenerate translation catalogs #848#5429
Ran `make gettext-extract` to pick up the new settings strings
(Collections, Features, Disable Downloads, and related messages) so
Weblate can translate them. Existing translations are preserved.
* Frontend: Move lightbox viewport-zoom handling into the view helper
- Move the viewport pinch-zoom lock/restore (disableNativeZoom /
restoreNativeZoom + savedViewportContent) from lightbox.vue onto the
View singleton in common/view.js.
- Drive it passively from View.apply() via a disableViewportZoom flag in
the PLightbox case, alongside hideScrollbar / disableScrolling /
disableNavigationGestures; lightbox.vue no longer references zoom.
- Make save/restore idempotent and extract the viewport constants.
- Cover the helpers and the apply() wiring in view.test.js.
* Settings: Add Accessibility section to General with Reduce Motion #848#5429
- Move the page-zoom checkbox out of the feature-flags grid into a
dedicated Accessibility card (super-admin only) with four toggles:
Open on Hover, Reduce Motion, Hide Scrollbar, and Allow Page Zoom.
- Add a UI.ReduceMotion flag (defaults false) that toggles an
html.reduce-motion class via config.setReduceMotion and suppresses map
fly-to animations non-destructively (util.mapAnimateDuration).
- Make Open on Hover apply live by reading shouldOpenOnHover() through a
reactive computed in the action/auth/lightbox menus; Zoom and Hide
Scrollbar reload since they are baked into the server-rendered HTML.
- Reword two feature-grid hints to the "pictures" convention and cover
the new helpers with Go and Vitest tests.
* Frontend: Regenerate translation catalogs #848#5429
Ran `make gettext-extract` to pick up the new Accessibility settings
strings (Accessibility, Allow Page Zoom, Open on Hover, Hide Scrollbar,
Reduce Motion, and their hints) and the reworded feature-grid hints.
Existing translations are preserved.
---------
Co-authored-by: Michael Mayer <michael@photoprism.app>
* Lightbox: Add 360° photo and video sphere viewer support
Wires Photo Sphere Viewer v5 into the lightbox for equirectangular
media. Backend detects UsePanoramaViewer and IsPhotosphere markers
alongside ProjectionType, and exposes Panorama + Projection in the
viewer / search result DTOs so the frontend can dispatch on the
projection type. The renderer (ThreeJS + PSV core + video plugin +
equirectangular video adapter) is lazy-loaded into a dedicated
webpack chunk so the base bundle is unchanged for users with no
360° media. PhotoPrism's existing lightbox controls drive the
underlying HTMLVideoElement that the PSV adapter creates for 360°
videos. Includes Vitest coverage for the sphere helper and the
lightbox dispatch, plus a TestCafe smoke spec and three new
ExifTool metadata fixtures.
* Lightbox: Fix 360° controls leak, hide zoom button, add sidebar icon
* Lightbox: Fix 360° touch panning and navigation arrows
* Lightbox: Fix fast swipe switching photos on 360° slides
Suppress PhotoSwipe swipe/drag navigation at its source via the dispatched pointer hook while a sphere slide is active, since the per-element gesture trap is outrun by a fast swipe whose pointer leaves the sphere container (touch-capable Windows). UI controls stay excluded so buttons and arrows still navigate.
* Lightbox: Route only equirectangular media to the 360° viewer
Routing was based on the loose photo_panorama flag for videos, which is also
true for cubemap and merely-wide (aspect > 1.9) videos, so non-equirectangular
media wrongly opened, distorted, in the sphere viewer. Route strictly on the
equirectangular projection for both photos and videos. Surface the video file's
projection in the viewer DTO via Photo.MediaProjection() (the primary search row
is a poster JPEG with no projection). Add an MP4 metadata-detection test.
* Lightbox: Open tagless 2:1 360° videos in the sphere viewer
Many 360° videos carry no projection tag PhotoPrism can read, so strict
equirectangular-projection routing left them in the flat player. Fall back to
equirectangular's defining 2:1 frame for panorama-flagged videos without a
projection, via is360Equirectangular() in common/sphere.js, while cubemap and
ultrawide (~2.35:1) clips stay flat.
* Tests: Make the 360° sphere video test assert instead of skipping
Bump abema/go-mp4 to v1.7.1 (only outdated direct dependency) and tidy
go.sum, pruning stale checksums left by the XMP sidecar merge. Regenerate
the license NOTICE for the updated dependency.
Use US-English spelling in the new XMP sidecar comments and tests, and
drop a specs/ reference from internal/meta/README.md so the public package
doc has no link into the private subrepo. Regenerate dialect_mysql.go from
SQL.
A "near=<uid>" geo search set the S2 cell but left the form lat/lng at
zero, so results sorted by distance from the (0,0) origin instead of
the referenced picture. Use the picture's own position in the ORDER BY,
falling back to time order when it has no usable coordinates.
- Add scoped session revocation (RevokeSessions / RevokeDerivedSessions)
shared by the user-update API and the CLI: a privilege-level change now
revokes logins and app-password-derived sessions while keeping the app
passwords themselves, so configured devices keep working.
- Identify app-password-derived sessions by auth_method "session".
- Deny app passwords on the REST API when the account cannot log in
(DenyLogIn); WebDAV access stays governed by CanUseWebDAV.
- Reduce the WebDAV auth cache expiration to one minute.
- Add IsSystemOrInvalid; allow deleting the initial admin account and skip
re-initializing a deactivated or deleted admin in InitAccount.
- Move session-revocation scopes to pkg/authn and extend tests.
Make the cameras/lenses/countries content-channel events publish only a
stable identity ([]string of slug or ISO code) instead of full entity
models, matching the UID-only invariant of every other content channel.
The update path no longer republishes count.* (an edit does not change
the count).
Restore surgical people updates: onPeople refetches only the affected
people by UID via Subject.search (refetchPeople) and upserts them in
place instead of forcing a full client-config reload (a #1307 regression).
Document the dormant folders.*/faces.* subscribers (no publisher, not
forwarded) and fix the misleading payload example in the event README.
Mirror the lens make/model editing surface for cameras: entity UpdateMakeModel/SaveForm, form validation, query, search, the GET/PUT /api/v1/cameras endpoints, and the cameras CLI command, plus a cameras ACL resource and scope.
Also tidy the lens surface for parity: self-validating SaveForm, empty make/model guard, X-Count search header, service-role grant, the empty-id/slug docs, and order cameras before lenses everywhere.
Adds the ability to override a lens's Make/Model (e.g. fixing Pentax lenses that ExifTool decodes as `4 38`) via a new photoprism lenses update CLI command and a `PUT /api/v1/lenses/:id endpoint`, plus a `GET /api/v1/lenses` search endpoint, a new lenses ACL resource, and an lenses ls list command.
Single-photo update/approve/set-primary and batch archive/restore/approve/private/delete now apply the same shared-scope visibility predicate as photo search, so by-UID and bulk edits stay consistent with album updates and never act on content outside the session's shared scope. Visibility is evaluated against both the client and user role for client sessions. Full-access sessions skip the check with no database query via PhotoSessionSeesEverything / SelectedPhotoUIDsForSession.
When an OIDC login matches a pre-existing non-OIDC account, log a
specific operator remedy (the exact 'users mod --auth oidc --auth-id'
command) to the audit and login-error logs instead of a bare provider
mismatch; the user-facing page stays a generic credentials error so the
account is not disclosed. Add a 'users mod --auth-issuer' flag so the
documented manual reconcile can pin the OIDC issuer, scoping the link to
that provider instead of matching any issuer with the same subject.
Adds an AppPasswords feature flag (default on) so app passwords can be disabled, closing the bypass where an app password minted by an OIDC-provisioned user keeps working after the account expires. When off, minting (oauth/token and auth add) is rejected and existing app passwords are no longer accepted on the REST API (403) or WebDAV (401). App passwords are identified by their session auth provider (IsApplication), covering every grant type. The Apps and Devices affordance is hidden for sessions that may not change a password; operators can force it off via PHOTOPRISM_DISABLE_FEATURES.
Thread admin authorization into SaveForm via byAdmin so the user-less cluster service principal can persist login/role/WebDAV instead of silently dropping them.
Adds the Portal-only cluster_admin operator role (declared in CE, registered
only on Portal builds), the acl.AdminRoles/IsAdminRole admin-tier set, and the
acl.IsFederatedRole/FederatedRoleUpdate helpers that keep cluster_admin and
visitor out of externally mapped account roles. SuperAdmin resolves to
cluster_admin on registered builds, SaveForm keeps an admin-level role on a
self-save, and UpdateUser rejects self-role changes. RoleStrings.Strings hides
visitor and none from the CLI role help.
Record that album_path is now a byte-exact VARBINARY column (alongside
album_slug, album_filter, photo_path) and that folder albums dedupe by
album_filter, in internal/entity/README.md. Add an Index Prefix Limits
note to the migrate README and AGENTS: InnoDB caps key prefixes at 767
bytes on COMPACT/REDUNDANT row formats, so VARBINARY prefix indexes use
512 by convention.
album_path mirrors photos.photo_path (already VARBINARY(1024)) and is
compared against it directly, but was VARCHAR with the utf8mb4_unicode_ci
collation, which collapses case and most emoji. That made album_path = ?
lookups collation-fuzzy and forced byte-exact re-checks in Go.
Store it as VARBINARY(1024) so folder album path comparisons are
byte-exact at the database, consistent with album_slug, album_filter,
and photo_path. The MySQL migration preserves the existing 768-byte
prefix index. SQLite compares TEXT byte-exact by default, so no SQLite
migration is needed.
Folder album slugs are not unique identities: long nested paths are
truncated to ClipSlug runes and slug.Make drops emoji, so distinct
sibling folders can share one album_slug. Deduplicate folder albums by
album_filter (the serialized path) instead, and keep slug matching only
for non-folder album types.
This stops RemoveDuplicateMoments from deleting legacy folder rows with
an empty album_path that merely share a truncated slug, which the
indexer then recreated on every run. The empty/NULL album_path escape
hatch in the previous byte-exact path guard no longer applies.
Add internal/entity/README.md covering the second-precision timestamp behavior,
the time helpers, MariaDB strict-mode test gotchas, and the utf8mb4 emoji
collation pitfalls so they don't have to be rediscovered.
The schema stores created_at/updated_at as DATETIME without fractional seconds,
so set gorm.NowFunc to entity.Now() (UTC truncated to the second). This keeps
in-memory and persisted times in sync and makes timestamp comparisons behave the
same on SQLite and MariaDB. Adapt the affected tests to compare with Sub() in a
sane range or a clearly past base instead of strict sub-second ordering, and give
the duplicate-client registry test distinct update times.
MariaDB strict mode rejects empty primary keys and out-of-range or oversized
values that SQLite accepts. Give the affected entity tests valid IDs: existing
fixtures where the row is additive, and throwaway in-range IDs where a real
reference would overwrite seeded data.
CreateUserSettings inserted an empty UserSettings, leaving the user_uid
primary key blank. Build it with NewUserSettings so the row carries the UID;
MariaDB strict mode rejected the missing value.
Legacy folder slugs drop emoji (slug.Make), so two siblings collide on the
VARBINARY album_slug and RemoveDuplicateMoments deleted one each pass while
the indexer recreated it. Skip folder albums whose album_path differs
byte-exact via HEX(), which compares the same on MariaDB and SQLite.
MariaDB's utf8mb4_unicode_ci collation collapses most emoji, so album_path
lookups matched a sibling folder and merged the two albums. Re-check the
path byte-exact in Go in FindFolderAlbum, the UpdateFolder dedup, and the
slug-collision scope check.
FileVisibleToSession excludes soft-deleted file rows; PhotoVisibleToSession's UnscopedDb use is documented (archived handling deferred to ScopeVisiblePhotos, full-access short-circuits). Adds TestUserPhotos_ScopeAuthorization pinning that a restricted session may scope only to albums it owns or has shared.
RedactForSession also clears the camera serial, XMP DocumentID, and per-file InstanceID for shared-only/unregistered sessions, matching common EXIF/XMP privacy practice; FileName, PhotoUID, and FileUID are kept.
GetPhoto returns a reduced view for sessions limited to shared content: album membership is restricted to the albums shared with the session, and labels, people, and owner/storage metadata are omitted. Full-access sessions are unaffected. Adds Photo.RedactForSession, in preparation for upcoming sharing features.
GetFile and the zip selection now limit results to the session's shared scope via the shared search helpers, consistent with photo search and in preparation for upcoming sharing features. No change for full-access sessions.
Introduces ScopePhotosForSession, ScopeVisiblePhotos, PhotoVisibleToSession, and FileVisibleToSession, and refactors searchPhotos to reuse the shared predicate, in preparation for upcoming sharing features. Full-access sessions short-circuit without an extra query.
FixPrimaries counted soft-deleted files as primaries, so a photo whose
primary was deleted kept photo_quality = -1 and stayed in Hidden even
with a present, valid preview file. Also clear the primary flag from
soft-deleted files so a present file is promoted and quality recovers.