Compare commits

...

12 Commits

Author SHA1 Message Date
github-actions
a2ff075e9a chore: version v2.6.3 2026-03-26 16:23:35 +00:00
Brandon Wees
d8b39906f9 fix: incorrect asset face sync (#27243)
* fix: incorrect asset face sync

* chore: sync sql
2026-03-26 09:39:02 -05:00
Michel Heusschen
b36911a16b fix(server): filter out empty search suggestions (#27292)
* fix(server): filter out empty search suggestions

* make sql
2026-03-26 09:36:04 -05:00
Alex
b074ee202e chore: move slideshow control button group to the left (#27287) 2026-03-26 14:31:11 +00:00
bo0tzz
78bb6cf926 chore: log id of existing asset on duplicate upload (#27266) 2026-03-26 11:50:53 +01:00
Yaros
c980f5fc19 chore(docs): withPeople parameter description (#27262)
* fix(server): withPeople inconsistent

* fix: query failing in some occasions

* test: add medium tests for withPeople option

* Revert "test: add medium tests for withPeople option"

This reverts commit 6c1505ba6b.

* Revert "fix: query failing in some occasions"

This reverts commit 221feeca45.

* Revert "fix(server): withPeople inconsistent"

This reverts commit 4289a9f23d.

* chore: change endpoint description

* chore: generate open-api
2026-03-26 11:50:29 +01:00
Yaros
a26d9e05ba fix(web): shifting motion image button (#27275) 2026-03-26 11:49:21 +01:00
bo0tzz
c862163204 fix: explicitly specify repo in auto-close job (#27291) 2026-03-26 10:43:51 +00:00
Michel Heusschen
5fb8f9bf1a fix(web): prevent horizontal scroll bar in asset viewer side panel (#27270)
* fix(web): prevent horizontal scroll bar in asset viewer side panel

* simplify
2026-03-25 21:02:31 -05:00
Mees Frensel
b9b5dba037 fix(web): crop square ratio i18n (#27257) 2026-03-25 14:05:43 -05:00
renovate[bot]
8bfa75087c chore(deps): update base-image to v202603251709 (major) (#27273)
chore(deps): update base-image to v202603251709

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 14:04:26 -05:00
bo0tzz
95280edd6c fix: let renovate update base images (#27272) 2026-03-25 18:00:40 +00:00
35 changed files with 89 additions and 49 deletions

View File

@@ -66,7 +66,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: gh pr edit "$PR_NUMBER" --add-label "auto-closed:template"
run: gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-label "auto-closed:template"
close_llm:
runs-on: ubuntu-latest
@@ -113,7 +113,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: gh pr edit "$PR_NUMBER" --remove-label "auto-closed:template" || true
run: gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --remove-label "auto-closed:template" || true
- name: Check for remaining auto-closed labels
id: check_labels
@@ -121,7 +121,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
REMAINING=$(gh pr view "$PR_NUMBER" --json labels \
REMAINING=$(gh pr view "$PR_NUMBER" --repo "${{ github.repository }}" --json labels \
--jq '[.labels[].name | select(startswith("auto-closed:"))] | length')
echo "remaining=$REMAINING" >> "$GITHUB_OUTPUT"

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.6.2",
"version": "2.6.3",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",

View File

@@ -1,7 +1,7 @@
[
{
"label": "v2.6.2",
"url": "https://docs.v2.6.2.archive.immich.app"
"label": "v2.6.3",
"url": "https://docs.v2.6.3.archive.immich.app"
},
{
"label": "v2.5.6",

View File

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "2.6.2",
"version": "2.6.3",
"description": "",
"main": "index.js",
"type": "module",

View File

@@ -866,6 +866,7 @@
"crop_aspect_ratio_fixed": "Fixed",
"crop_aspect_ratio_free": "Free",
"crop_aspect_ratio_original": "Original",
"crop_aspect_ratio_square": "Square",
"curated_object_page_title": "Things",
"current_device": "Current device",
"current_pin_code": "Current PIN code",

View File

@@ -1,6 +1,6 @@
{
"name": "immich-i18n",
"version": "2.6.2",
"version": "2.6.3",
"private": true,
"scripts": {
"format": "prettier --cache --check .",

View File

@@ -1,6 +1,6 @@
[project]
name = "immich-ml"
version = "2.6.2"
version = "2.6.3"
description = ""
authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }]
requires-python = ">=3.11,<4.0"

View File

@@ -898,7 +898,7 @@ wheels = [
[[package]]
name = "immich-ml"
version = "2.6.2"
version = "2.6.3"
source = { editable = "." }
dependencies = [
{ name = "aiocache" },

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 3040,
"android.injected.version.name" => "2.6.2",
"android.injected.version.code" => 3041,
"android.injected.version.name" => "2.6.3",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@@ -80,7 +80,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.6.2</string>
<string>2.6.3</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 2.6.2
- API version: 2.6.3
- Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen

View File

@@ -379,7 +379,7 @@ class MetadataSearchDto {
///
bool? withExif;
/// Include assets with people
/// Include people data in response
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated

View File

@@ -273,7 +273,7 @@ class RandomSearchDto {
///
bool? withExif;
/// Include assets with people
/// Include people data in response
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none'
version: 2.6.2+3040
version: 2.6.3+3041
environment:
sdk: '>=3.8.0 <4.0.0'

View File

@@ -15166,7 +15166,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "2.6.2",
"version": "2.6.3",
"contact": {}
},
"tags": [
@@ -19129,7 +19129,7 @@
"type": "boolean"
},
"withPeople": {
"description": "Include assets with people",
"description": "Include people data in response",
"type": "boolean"
},
"withStacked": {
@@ -20868,7 +20868,7 @@
"type": "boolean"
},
"withPeople": {
"description": "Include assets with people",
"description": "Include people data in response",
"type": "boolean"
},
"withStacked": {

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/sdk",
"version": "2.6.2",
"version": "2.6.3",
"description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module",
"main": "./build/index.js",

View File

@@ -1,6 +1,6 @@
/**
* Immich
* 2.6.2
* 2.6.3
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/
@@ -1741,7 +1741,7 @@ export type MetadataSearchDto = {
withDeleted?: boolean;
/** Include EXIF data in response */
withExif?: boolean;
/** Include assets with people */
/** Include people data in response */
withPeople?: boolean;
/** Include stacked assets */
withStacked?: boolean;
@@ -1855,7 +1855,7 @@ export type RandomSearchDto = {
withDeleted?: boolean;
/** Include EXIF data in response */
withExif?: boolean;
/** Include assets with people */
/** Include people data in response */
withPeople?: boolean;
/** Include stacked assets */
withStacked?: boolean;

View File

@@ -1,6 +1,6 @@
{
"name": "immich-monorepo",
"version": "2.6.2",
"version": "2.6.3",
"description": "Monorepo for Immich",
"private": true,
"packageManager": "pnpm@10.30.3+sha512.c961d1e0a2d8e354ecaa5166b822516668b7f44cb5bd95122d590dd81922f606f5473b6d23ec4a5be05e7fcd18e8488d47d978bbe981872f1145d06e9a740017",

View File

@@ -27,6 +27,10 @@
"matchUpdateTypes": ["major"],
"enabled": false
},
{
"matchPackageNames": ["ghcr.io/immich-app/base-server-*"],
"maxMajorIncrement": 0
},
{
"matchPackageNames": ["ruby"],
"groupName": "ruby",

View File

@@ -1,4 +1,4 @@
FROM ghcr.io/immich-app/base-server-dev:202603031112@sha256:837536db5fd9e432f0f474ef9b61712fe3b3815821c3e4edf5e5b0b1f1ed30ad AS builder
FROM ghcr.io/immich-app/base-server-dev:202603251709@sha256:2bf3053c732fcb87ec90c3c614632ac44847423468ccc57fd935bff771828d9d AS builder
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
CI=1 \
COREPACK_HOME=/tmp \
@@ -71,7 +71,7 @@ RUN --mount=type=cache,id=pnpm-plugins,target=/buildcache/pnpm-store \
--mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
cd plugins && mise run build
FROM ghcr.io/immich-app/base-server-prod:202603031112@sha256:bb8c8645ee61977140121e56ba09db7ae656a7506f9a6af1be8461b4d81fdf03
FROM ghcr.io/immich-app/base-server-prod:202603251709@sha256:17de30977ff87aa06758a56ad7f10d6b5c97bf9dab76e4ec4177a2a8d1b2b5f3
WORKDIR /usr/src/app
ENV NODE_ENV=production \

View File

@@ -1,5 +1,5 @@
# dev build
FROM ghcr.io/immich-app/base-server-dev:202603031112@sha256:837536db5fd9e432f0f474ef9b61712fe3b3815821c3e4edf5e5b0b1f1ed30ad AS dev
FROM ghcr.io/immich-app/base-server-dev:202603251709@sha256:2bf3053c732fcb87ec90c3c614632ac44847423468ccc57fd935bff771828d9d AS dev
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
CI=1 \

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "2.6.2",
"version": "2.6.3",
"description": "",
"author": "",
"private": true,

View File

@@ -146,7 +146,7 @@ export class RandomSearchDto extends BaseSearchWithResultsDto {
@ValidateBoolean({ optional: true, description: 'Include stacked assets' })
withStacked?: boolean;
@ValidateBoolean({ optional: true, description: 'Include assets with people' })
@ValidateBoolean({ optional: true, description: 'Include people data in response' })
withPeople?: boolean;
}

View File

@@ -254,6 +254,7 @@ where
and "visibility" = $2
and "deletedAt" is null
and "state" is not null
and "state" != $3
-- SearchRepository.getCities
select distinct
@@ -266,6 +267,7 @@ where
and "visibility" = $2
and "deletedAt" is null
and "city" is not null
and "city" != $3
-- SearchRepository.getCameraMakes
select distinct
@@ -278,6 +280,7 @@ where
and "visibility" = $2
and "deletedAt" is null
and "make" is not null
and "make" != $3
-- SearchRepository.getCameraModels
select distinct
@@ -290,6 +293,7 @@ where
and "visibility" = $2
and "deletedAt" is null
and "model" is not null
and "model" != $3
-- SearchRepository.getCameraLensModels
select distinct
@@ -302,3 +306,4 @@ where
and "visibility" = $2
and "deletedAt" is null
and "lensModel" is not null
and "lensModel" != $3

View File

@@ -582,7 +582,6 @@ where
"asset_face"."updateId" < $1
and "asset_face"."updateId" > $2
and "asset"."ownerId" = $3
and "asset_face"."isVisible" = $4
order by
"asset_face"."updateId" asc

View File

@@ -502,10 +502,7 @@ export class SearchRepository {
return res.map((row) => row.lensModel!);
}
private getExifField<K extends 'city' | 'state' | 'country' | 'make' | 'model' | 'lensModel'>(
field: K,
userIds: string[],
) {
private getExifField(field: 'city' | 'state' | 'country' | 'make' | 'model' | 'lensModel', userIds: string[]) {
return this.db
.selectFrom('asset_exif')
.select(field)
@@ -514,6 +511,7 @@ export class SearchRepository {
.where('ownerId', '=', anyUuid(userIds))
.where('visibility', '=', AssetVisibility.Timeline)
.where('deletedAt', 'is', null)
.where(field, 'is not', null);
.where(field, 'is not', null)
.where(field, '!=', '');
}
}

View File

@@ -487,7 +487,6 @@ class AssetFaceSync extends BaseSync {
])
.leftJoin('asset', 'asset.id', 'asset_face.assetId')
.where('asset.ownerId', '=', options.userId)
.where('asset_face.isVisible', '=', true)
.stream();
}
}

View File

@@ -0,0 +1,10 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
// Sync query for faces was incorrect on server <=2.6.2
await sql`DELETE FROM session_sync_checkpoint WHERE type in ('AssetFaceV1', 'AssetFaceV2')`.execute(db);
}
export async function down(): Promise<void> {
// Not implemented
}

View File

@@ -356,6 +356,7 @@ export class AssetMediaService extends BaseService {
await this.addToSharedLink(auth.sharedLink, duplicateId);
}
this.logger.debug(`Duplicate asset upload rejected: existing asset ${duplicateId}`);
return { status: AssetMediaStatus.DUPLICATE, id: duplicateId };
}

View File

@@ -1,4 +1,5 @@
import { Kysely } from 'kysely';
import { SearchSuggestionType } from 'src/dtos/search.dto';
import { AccessRepository } from 'src/repositories/access.repository';
import { AssetRepository } from 'src/repositories/asset.repository';
import { DatabaseRepository } from 'src/repositories/database.repository';
@@ -108,4 +109,25 @@ describe(SearchService.name, () => {
expect(response.assets.items[0].id).toBe(unstackedAsset.id);
});
});
describe('getSearchSuggestions', () => {
it('should filter out empty search suggestions', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const { asset } = await ctx.newAsset({ ownerId: user.id });
await ctx.newExif({ assetId: asset.id, make: 'Canon' });
const { asset: assetWithEmptyMake } = await ctx.newAsset({ ownerId: user.id });
await ctx.newExif({ assetId: assetWithEmptyMake.id, make: '' });
const auth = factory.auth({ user: { id: user.id } });
const suggestions = await sut.getSearchSuggestions(auth, {
type: SearchSuggestionType.CAMERA_MAKE,
includeNull: true,
});
expect(suggestions).toEqual(['Canon', null]);
});
});
});

View File

@@ -1,6 +1,6 @@
{
"name": "immich-web",
"version": "2.6.2",
"version": "2.6.3",
"license": "GNU Affero General Public License version 3",
"type": "module",
"scripts": {

View File

@@ -120,10 +120,10 @@
<ActionButton action={Cast} />
<ActionButton action={Actions.Share} />
<ActionButton action={Actions.Offline} />
<ActionButton action={Actions.PlayMotionPhoto} />
<ActionButton action={Actions.StopMotionPhoto} />
<ActionButton action={Actions.ZoomIn} />
<ActionButton action={Actions.ZoomOut} />
<ActionButton action={Actions.PlayMotionPhoto} />
<ActionButton action={Actions.StopMotionPhoto} />
<ActionButton action={Actions.Copy} />
<ActionButton action={Actions.SharedLinkDownload} />
<ActionButton action={Actions.Info} />

View File

@@ -1,5 +1,6 @@
import { getAnimateMock } from '$lib/__mocks__/animate.mock';
import { getResizeObserverMock } from '$lib/__mocks__/resize-observer.mock';
import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
import { preferences as preferencesStore, resetSavedUser, user as userStore } from '$lib/stores/user.store';
import { renderWithTooltips } from '$tests/helpers';
import { updateAsset } from '@immich/sdk';
@@ -41,6 +42,7 @@ describe('AssetViewer', () => {
});
afterEach(() => {
slideshowStore.slideshowState.set(SlideshowState.None);
resetSavedUser();
vi.clearAllMocks();
});

View File

@@ -485,7 +485,7 @@
{/if}
{#if $slideshowState != SlideshowState.None}
<div class="absolute w-full flex justify-center">
<div class="absolute inset-s-0 top-0 flex w-full justify-start">
<SlideshowBar
{isFullScreen}
assetType={previewStackedAsset?.type ?? asset.type}
@@ -580,17 +580,16 @@
<div
transition:fly={{ duration: 150 }}
id="detail-panel"
class="row-start-1 row-span-4 overflow-y-auto transition-all dark:border-l dark:border-s-immich-dark-gray bg-light"
class={[
'row-start-1 row-span-4 overflow-y-auto transition-all dark:border-l dark:border-s-immich-dark-gray bg-light',
showDetailPanel ? 'w-90' : 'w-100',
]}
translate="yes"
>
{#if showDetailPanel}
<div class="w-90 h-full">
<DetailPanel {asset} currentAlbum={album} />
</div>
<DetailPanel {asset} currentAlbum={album} />
{:else if assetViewerManager.isShowEditor}
<div class="w-100 h-full">
<EditorPanel {asset} onClose={closeEditor} />
</div>
<EditorPanel {asset} onClose={closeEditor} />
{/if}
</div>
{/if}

View File

@@ -23,7 +23,7 @@
{ label: '2:3', value: '2:3', width: 16, height: 24 },
{ label: '16:9', value: '16:9', width: 24, height: 14 },
{ label: '9:16', value: '9:16', width: 14, height: 24 },
{ label: 'Square', value: '1:1', width: 20, height: 20 },
{ label: $t('crop_aspect_ratio_square'), value: '1:1', width: 20, height: 20 },
];
let isRotated = $derived(transformManager.normalizedRotation % 180 !== 0);