Merge branch 'immich-main' into feat/shared-album-owner-labels

This commit is contained in:
CJPeckover
2025-11-25 22:59:30 -05:00
131 changed files with 5921 additions and 3168 deletions
+1 -1
View File
@@ -1 +1 @@
24.11.0 24.11.1
+1 -1
View File
@@ -74,7 +74,7 @@ jobs:
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Download APK - name: Download APK
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with: with:
name: release-apk-signed name: release-apk-signed
github-token: ${{ steps.generate-token.outputs.token }} github-token: ${{ steps.generate-token.outputs.token }}
+1 -1
View File
@@ -1 +1 @@
24.11.0 24.11.1
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:22.16.0-alpine3.20@sha256:2289fb1fba0f4633b08ec47b94a89c7e20b829fc5679f9b7b298eaa2f1ed8b7e AS core FROM node:24.1.0-alpine3.20@sha256:8fe019e0d57dbdce5f5c27c0b63d2775cf34b00e3755a7dea969802d7e0c2b25 AS core
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY package* pnpm* .pnpmfile.cjs ./ COPY package* pnpm* .pnpmfile.cjs ./
+2 -2
View File
@@ -20,7 +20,7 @@
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/micromatch": "^4.0.9", "@types/micromatch": "^4.0.9",
"@types/mock-fs": "^4.13.1", "@types/mock-fs": "^4.13.1",
"@types/node": "^22.19.1", "@types/node": "^24.10.1",
"@vitest/coverage-v8": "^3.0.0", "@vitest/coverage-v8": "^3.0.0",
"byte-size": "^9.0.0", "byte-size": "^9.0.0",
"cli-progress": "^3.12.0", "cli-progress": "^3.12.0",
@@ -69,6 +69,6 @@
"micromatch": "^4.0.8" "micromatch": "^4.0.8"
}, },
"volta": { "volta": {
"node": "24.11.0" "node": "24.11.1"
} }
} }
+1 -1
View File
@@ -95,7 +95,7 @@ services:
command: ['./run.sh', '-disable-reporting'] command: ['./run.sh', '-disable-reporting']
ports: ports:
- 3000:3000 - 3000:3000
image: grafana/grafana:12.2.1-ubuntu@sha256:797530c642f7b41ba7848c44cfda5e361ef1f3391a98bed1e5d448c472b6826a image: grafana/grafana:12.3.0-ubuntu@sha256:cee936306135e1925ab21dffa16f8a411535d16ab086bef2309339a8e74d62df
volumes: volumes:
- grafana-data:/var/lib/grafana - grafana-data:/var/lib/grafana
+1 -1
View File
@@ -1 +1 @@
24.11.0 24.11.1
+4 -4
View File
@@ -14,15 +14,15 @@ When contributing code through a pull request, please check the following:
- [ ] `pnpm run check:typescript` (check typescript) - [ ] `pnpm run check:typescript` (check typescript)
- [ ] `pnpm test` (unit tests) - [ ] `pnpm test` (unit tests)
:::tip AIO
Run all web checks with `pnpm run check:all`
:::
## Documentation ## Documentation
- [ ] `pnpm run format` (formatting via Prettier) - [ ] `pnpm run format` (formatting via Prettier)
- [ ] Update the `_redirects` file if you have renamed a page or removed it from the documentation. - [ ] Update the `_redirects` file if you have renamed a page or removed it from the documentation.
:::tip AIO
Run all web checks with `pnpm run check:all`
:::
## Server Checks ## Server Checks
- [ ] `pnpm run lint` (linting via ESLint) - [ ] `pnpm run lint` (linting via ESLint)
+1 -1
View File
@@ -93,7 +93,7 @@ Information on the current workers can be found [here](/administration/jobs-work
All `DB_` variables must be provided to all Immich workers, including `api` and `microservices`. All `DB_` variables must be provided to all Immich workers, including `api` and `microservices`.
`DB_URL` must be in the format `postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport/immichdatabasename`. `DB_URL` must be in the format `postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport/immichdatabasename`.
You can require SSL by adding `?sslmode=require` to the end of the `DB_URL` string, or require SSL and skip certificate verification by adding `?sslmode=require&sslmode=no-verify`. You can require SSL by adding `?sslmode=require` to the end of the `DB_URL` string, or require SSL and skip certificate verification by adding `?sslmode=require&uselibpqcompat=true`. This allows both immich and `pg_dumpall` (the utility used for database backups) to [properly connect](https://github.com/brianc/node-postgres/tree/master/packages/pg-connection-string#tcp-connections) to your database.
When `DB_URL` is defined, the `DB_HOSTNAME`, `DB_PORT`, `DB_USERNAME`, `DB_PASSWORD` and `DB_DATABASE_NAME` database variables are ignored. When `DB_URL` is defined, the `DB_HOSTNAME`, `DB_PORT`, `DB_USERNAME`, `DB_PASSWORD` and `DB_DATABASE_NAME` database variables are ignored.
+1 -1
View File
@@ -57,6 +57,6 @@
"node": ">=20" "node": ">=20"
}, },
"volta": { "volta": {
"node": "24.11.0" "node": "24.11.1"
} }
} }
+1 -1
View File
@@ -1 +1 @@
24.11.0 24.11.1
+2 -2
View File
@@ -26,7 +26,7 @@
"@playwright/test": "^1.44.1", "@playwright/test": "^1.44.1",
"@socket.io/component-emitter": "^3.1.2", "@socket.io/component-emitter": "^3.1.2",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/node": "^22.19.1", "@types/node": "^24.10.1",
"@types/oidc-provider": "^9.0.0", "@types/oidc-provider": "^9.0.0",
"@types/pg": "^8.15.1", "@types/pg": "^8.15.1",
"@types/pngjs": "^6.0.4", "@types/pngjs": "^6.0.4",
@@ -54,6 +54,6 @@
"vitest": "^3.0.0" "vitest": "^3.0.0"
}, },
"volta": { "volta": {
"node": "24.11.0" "node": "24.11.1"
} }
} }
+27 -17
View File
@@ -62,50 +62,60 @@ export const setupTimelineMockApiRoutes = async (
return route.continue(); return route.continue();
}); });
await context.route('**/api/assets/**', async (route, request) => { await context.route('**/api/assets/*', async (route, request) => {
const url = new URL(request.url());
const pathname = url.pathname;
const assetId = basename(pathname);
const asset = getAsset(timelineRestData, assetId);
return route.fulfill({
status: 200,
contentType: 'application/json',
json: asset,
});
});
await context.route('**/api/assets/*/ocr', async (route) => {
return route.fulfill({ status: 200, contentType: 'application/json', json: [] });
});
await context.route('**/api/assets/*/thumbnail?size=*', async (route, request) => {
const pattern = /\/api\/assets\/(?<assetId>[^/]+)\/thumbnail\?size=(?<size>preview|thumbnail)/; const pattern = /\/api\/assets\/(?<assetId>[^/]+)\/thumbnail\?size=(?<size>preview|thumbnail)/;
const match = request.url().match(pattern); const match = request.url().match(pattern);
if (!match) { if (!match?.groups) {
const url = new URL(request.url()); throw new Error(`Invalid URL for thumbnail endpoint: ${request.url()}`);
const pathname = url.pathname;
const assetId = basename(pathname);
const asset = getAsset(timelineRestData, assetId);
return route.fulfill({
status: 200,
contentType: 'application/json',
json: asset,
});
} }
if (match.groups?.size === 'preview') {
if (match.groups.size === 'preview') {
if (!route.request().serviceWorker()) { if (!route.request().serviceWorker()) {
return route.continue(); return route.continue();
} }
const asset = getAsset(timelineRestData, match.groups?.assetId); const asset = getAsset(timelineRestData, match.groups.assetId);
return route.fulfill({ return route.fulfill({
status: 200, status: 200,
headers: { 'content-type': 'image/jpeg', ETag: 'abc123', 'Cache-Control': 'public, max-age=3600' }, headers: { 'content-type': 'image/jpeg', ETag: 'abc123', 'Cache-Control': 'public, max-age=3600' },
body: await randomPreview( body: await randomPreview(
match.groups?.assetId, match.groups.assetId,
(asset?.exifInfo?.exifImageWidth ?? 0) / (asset?.exifInfo?.exifImageHeight ?? 1), (asset?.exifInfo?.exifImageWidth ?? 0) / (asset?.exifInfo?.exifImageHeight ?? 1),
), ),
}); });
} }
if (match.groups?.size === 'thumbnail') { if (match.groups.size === 'thumbnail') {
if (!route.request().serviceWorker()) { if (!route.request().serviceWorker()) {
return route.continue(); return route.continue();
} }
const asset = getAsset(timelineRestData, match.groups?.assetId); const asset = getAsset(timelineRestData, match.groups.assetId);
return route.fulfill({ return route.fulfill({
status: 200, status: 200,
headers: { 'content-type': 'image/jpeg' }, headers: { 'content-type': 'image/jpeg' },
body: await randomThumbnail( body: await randomThumbnail(
match.groups?.assetId, match.groups.assetId,
(asset?.exifInfo?.exifImageWidth ?? 0) / (asset?.exifInfo?.exifImageHeight ?? 1), (asset?.exifInfo?.exifImageWidth ?? 0) / (asset?.exifInfo?.exifImageHeight ?? 1),
), ),
}); });
} }
return route.continue(); return route.continue();
}); });
await context.route('**/api/albums/**', async (route, request) => { await context.route('**/api/albums/**', async (route, request) => {
const pattern = /\/api\/albums\/(?<albumId>[^/?]+)/; const pattern = /\/api\/albums\/(?<albumId>[^/?]+)/;
const match = request.url().match(pattern); const match = request.url().match(pattern);
+3 -3
View File
@@ -12,7 +12,7 @@ import {
PersonCreateDto, PersonCreateDto,
QueueCommandDto, QueueCommandDto,
QueueName, QueueName,
QueuesResponseDto, QueuesResponseLegacyDto,
SharedLinkCreateDto, SharedLinkCreateDto,
UpdateLibraryDto, UpdateLibraryDto,
UserAdminCreateDto, UserAdminCreateDto,
@@ -564,13 +564,13 @@ export const utils = {
await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) }); await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) });
}, },
isQueueEmpty: async (accessToken: string, queue: keyof QueuesResponseDto) => { isQueueEmpty: async (accessToken: string, queue: keyof QueuesResponseLegacyDto) => {
const queues = await getQueuesLegacy({ headers: asBearerAuth(accessToken) }); const queues = await getQueuesLegacy({ headers: asBearerAuth(accessToken) });
const jobCounts = queues[queue].jobCounts; const jobCounts = queues[queue].jobCounts;
return !jobCounts.active && !jobCounts.waiting; return !jobCounts.active && !jobCounts.waiting;
}, },
waitForQueueFinish: (accessToken: string, queue: keyof QueuesResponseDto, ms?: number) => { waitForQueueFinish: (accessToken: string, queue: keyof QueuesResponseLegacyDto, ms?: number) => {
// eslint-disable-next-line no-async-promise-executor // eslint-disable-next-line no-async-promise-executor
return new Promise<void>(async (resolve, reject) => { return new Promise<void>(async (resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Timed out waiting for queue to empty')), ms || 10_000); const timeout = setTimeout(() => reject(new Error('Timed out waiting for queue to empty')), ms || 10_000);
@@ -611,6 +611,53 @@ test.describe('Timeline', () => {
await page.getByText('Photos', { exact: true }).click(); await page.getByText('Photos', { exact: true }).click();
await thumbnailUtils.expectInViewport(page, assetToArchive.id); await thumbnailUtils.expectInViewport(page, assetToArchive.id);
}); });
test('open /archive, favorite photo, unfavorite', async ({ page }) => {
const assetToFavorite = assets[0];
changes.assetArchivals.push(assetToFavorite.id);
await pageUtils.openArchivePage(page);
const favorite = pageRoutePromise(page, '**/api/assets', async (route, request) => {
const requestJson = request.postDataJSON();
if (requestJson.isFavorite === undefined) {
return await route.continue();
}
const isFavorite = requestJson.isFavorite;
if (isFavorite) {
changes.assetFavorites.push(...requestJson.ids);
}
await route.fulfill({
status: 204,
});
});
await thumbnailUtils.withAssetId(page, assetToFavorite.id).hover();
await thumbnailUtils.selectButton(page, assetToFavorite.id).click();
await page.getByLabel('Favorite').click();
await expect(favorite).resolves.toEqual({
isFavorite: true,
ids: [assetToFavorite.id],
});
await expect(thumbnailUtils.withAssetId(page, assetToFavorite.id)).toHaveCount(1);
await thumbnailUtils.expectInViewport(page, assetToFavorite.id);
await thumbnailUtils.expectThumbnailIsFavorite(page, assetToFavorite.id);
await thumbnailUtils.withAssetId(page, assetToFavorite.id).hover();
await thumbnailUtils.selectButton(page, assetToFavorite.id).click();
const unFavoriteRequest = pageRoutePromise(page, '**/api/assets', async (route, request) => {
const requestJson = request.postDataJSON();
if (requestJson.isFavorite === undefined) {
return await route.continue();
}
changes.assetFavorites = changes.assetFavorites.filter((id) => !requestJson.ids.includes(id));
await route.fulfill({
status: 204,
});
});
await page.getByLabel('Remove from favorites').click();
await expect(unFavoriteRequest).resolves.toEqual({
isFavorite: false,
ids: [assetToFavorite.id],
});
await expect(thumbnailUtils.withAssetId(page, assetToFavorite.id)).toHaveCount(1);
await thumbnailUtils.expectThumbnailIsNotFavorite(page, assetToFavorite.id);
});
test('open album, archive photo, open album, unarchive', async ({ page }) => { test('open album, archive photo, open album, unarchive', async ({ page }) => {
const album = timelineRestData.album; const album = timelineRestData.album;
await pageUtils.openAlbumPage(page, album.id); await pageUtils.openAlbumPage(page, album.id);
@@ -633,8 +680,7 @@ test.describe('Timeline', () => {
visibility: 'archive', visibility: 'archive',
ids: [assetToArchive.id], ids: [assetToArchive.id],
}); });
console.log('Skipping assertion - TODO - fix that archiving in album doesnt add icon'); await thumbnailUtils.expectThumbnailIsArchive(page, assetToArchive.id);
// await thumbnail.expectThumbnailIsArchive(page, assetToArchive.id);
await page.locator('#asset-selection-app-bar').getByLabel('Close').click(); await page.locator('#asset-selection-app-bar').getByLabel('Close').click();
await page.getByRole('link').getByText('Archive').click(); await page.getByRole('link').getByText('Archive').click();
await timelineUtils.waitForTimelineLoad(page); await timelineUtils.waitForTimelineLoad(page);
@@ -656,8 +702,7 @@ test.describe('Timeline', () => {
visibility: 'timeline', visibility: 'timeline',
ids: [assetToArchive.id], ids: [assetToArchive.id],
}); });
console.log('Skipping assertion - TODO - fix bug with not removing asset from timeline-manager after unarchive'); await expect(thumbnailUtils.withAssetId(page, assetToArchive.id)).toHaveCount(0);
// await expect(thumbnail.withAssetId(page, assetToArchive.id)).toHaveCount(0);
await pageUtils.openAlbumPage(page, album.id); await pageUtils.openAlbumPage(page, album.id);
await thumbnailUtils.expectInViewport(page, assetToArchive.id); await thumbnailUtils.expectInViewport(page, assetToArchive.id);
}); });
@@ -712,6 +757,50 @@ test.describe('Timeline', () => {
await page.getByText('Photos', { exact: true }).click(); await page.getByText('Photos', { exact: true }).click();
await thumbnailUtils.expectInViewport(page, assetToFavorite.id); await thumbnailUtils.expectInViewport(page, assetToFavorite.id);
}); });
test('open /favorites, archive photo, unarchive photo', async ({ page }) => {
await pageUtils.openFavorites(page);
const assetToArchive = getAsset(timelineRestData, 'ad31e29f-2069-4574-b9a9-ad86523c92cb')!;
await thumbnailUtils.withAssetId(page, assetToArchive.id).hover();
await thumbnailUtils.selectButton(page, assetToArchive.id).click();
await page.getByLabel('Menu').click();
const archive = pageRoutePromise(page, '**/api/assets', async (route, request) => {
const requestJson = request.postDataJSON();
if (requestJson.visibility !== 'archive') {
return await route.continue();
}
await route.fulfill({
status: 204,
});
changes.assetArchivals.push(...requestJson.ids);
});
await page.getByRole('menuitem').getByText('Archive').click();
await expect(archive).resolves.toEqual({
visibility: 'archive',
ids: [assetToArchive.id],
});
await page.getByRole('link').getByText('Archive').click();
await thumbnailUtils.expectInViewport(page, assetToArchive.id);
await thumbnailUtils.expectThumbnailIsNotArchive(page, assetToArchive.id);
await thumbnailUtils.withAssetId(page, assetToArchive.id).hover();
await thumbnailUtils.selectButton(page, assetToArchive.id).click();
const unarchiveRequest = pageRoutePromise(page, '**/api/assets', async (route, request) => {
const requestJson = request.postDataJSON();
if (requestJson.visibility !== 'timeline') {
return await route.continue();
}
changes.assetArchivals = changes.assetArchivals.filter((id) => !requestJson.ids.includes(id));
await route.fulfill({
status: 204,
});
});
await page.getByLabel('Unarchive').click();
await expect(unarchiveRequest).resolves.toEqual({
visibility: 'timeline',
ids: [assetToArchive.id],
});
await expect(thumbnailUtils.withAssetId(page, assetToArchive.id)).toHaveCount(0);
await thumbnailUtils.expectThumbnailIsNotArchive(page, assetToArchive.id);
});
test('Open album, favorite photo, open /favorites, remove favorite, Open album', async ({ page }) => { test('Open album, favorite photo, open /favorites, remove favorite, Open album', async ({ page }) => {
const album = timelineRestData.album; const album = timelineRestData.album;
await pageUtils.openAlbumPage(page, album.id); await pageUtils.openAlbumPage(page, album.id);
+16 -12
View File
@@ -105,20 +105,16 @@ export const thumbnailUtils = {
return await poll(page, () => thumbnailUtils.queryThumbnailInViewport(page, collector)); return await poll(page, () => thumbnailUtils.queryThumbnailInViewport(page, collector));
}, },
async expectThumbnailIsFavorite(page: Page, assetId: string) { async expectThumbnailIsFavorite(page: Page, assetId: string) {
await expect( await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-favorite]')).toHaveCount(1);
thumbnailUtils },
.withAssetId(page, assetId) async expectThumbnailIsNotFavorite(page: Page, assetId: string) {
.locator( await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-favorite]')).toHaveCount(0);
'path[d="M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z"]',
),
).toHaveCount(1);
}, },
async expectThumbnailIsArchive(page: Page, assetId: string) { async expectThumbnailIsArchive(page: Page, assetId: string) {
await expect( await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-archive]')).toHaveCount(1);
thumbnailUtils },
.withAssetId(page, assetId) async expectThumbnailIsNotArchive(page: Page, assetId: string) {
.locator('path[d="M20 21H4V10H6V19H18V10H20V21M3 3H21V9H3V3M5 5V7H19V5M10.5 11V14H8L12 18L16 14H13.5V11"]'), await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-archive]')).toHaveCount(0);
).toHaveCount(1);
}, },
async expectSelectedReadonly(page: Page, assetId: string) { async expectSelectedReadonly(page: Page, assetId: string) {
// todo - need a data attribute for selected // todo - need a data attribute for selected
@@ -208,10 +204,18 @@ export const pageUtils = {
await page.goto(`/photos`); await page.goto(`/photos`);
await timelineUtils.waitForTimelineLoad(page); await timelineUtils.waitForTimelineLoad(page);
}, },
async openFavorites(page: Page) {
await page.goto(`/favorites`);
await timelineUtils.waitForTimelineLoad(page);
},
async openAlbumPage(page: Page, albumId: string) { async openAlbumPage(page: Page, albumId: string) {
await page.goto(`/albums/${albumId}`); await page.goto(`/albums/${albumId}`);
await timelineUtils.waitForTimelineLoad(page); await timelineUtils.waitForTimelineLoad(page);
}, },
async openArchivePage(page: Page) {
await page.goto(`/archive`);
await timelineUtils.waitForTimelineLoad(page);
},
async deepLinkAlbumPage(page: Page, albumId: string, assetId: string) { async deepLinkAlbumPage(page: Page, albumId: string, assetId: string) {
await page.goto(`/albums/${albumId}?at=${assetId}`); await page.goto(`/albums/${albumId}?at=${assetId}`);
await timelineUtils.waitForTimelineLoad(page); await timelineUtils.waitForTimelineLoad(page);
+2 -2
View File
@@ -54,7 +54,7 @@ test.describe('User Administration', () => {
await page.getByRole('button', { name: 'Edit' }).click(); await page.getByRole('button', { name: 'Edit' }).click();
await expect(page.getByLabel('Admin User')).not.toBeChecked(); await expect(page.getByLabel('Admin User')).not.toBeChecked();
await page.getByText('Admin User').click(); await page.getByLabel('Admin User').click();
await expect(page.getByLabel('Admin User')).toBeChecked(); await expect(page.getByLabel('Admin User')).toBeChecked();
await page.getByRole('button', { name: 'Confirm' }).click(); await page.getByRole('button', { name: 'Confirm' }).click();
@@ -83,7 +83,7 @@ test.describe('User Administration', () => {
await page.getByRole('button', { name: 'Edit' }).click(); await page.getByRole('button', { name: 'Edit' }).click();
await expect(page.getByLabel('Admin User')).toBeChecked(); await expect(page.getByLabel('Admin User')).toBeChecked();
await page.getByText('Admin User').click(); await page.getByLabel('Admin User').click();
await expect(page.getByLabel('Admin User')).not.toBeChecked(); await expect(page.getByLabel('Admin User')).not.toBeChecked();
await page.getByRole('button', { name: 'Confirm' }).click(); await page.getByRole('button', { name: 'Confirm' }).click();
+1 -1
View File
@@ -25,7 +25,7 @@ FROM builder-cpu AS builder-rknn
FROM rocm/dev-ubuntu-22.04:6.4.3-complete@sha256:1f7e92ca7e3a3785680473329ed1091fc99db3e90fcb3a1688f2933e870ed76b AS builder-rocm FROM rocm/dev-ubuntu-22.04:6.4.3-complete@sha256:1f7e92ca7e3a3785680473329ed1091fc99db3e90fcb3a1688f2933e870ed76b AS builder-rocm
# renovate: datasource=github-releases depName=Microsoft/onnxruntime # renovate: datasource=github-releases depName=Microsoft/onnxruntime
ARG ONNXRUNTIME_VERSION="v1.20.1" ARG ONNXRUNTIME_VERSION="v1.22.1"
WORKDIR /code WORKDIR /code
RUN apt-get update && apt-get install -y --no-install-recommends wget git python3.10-venv RUN apt-get update && apt-get install -y --no-install-recommends wget git python3.10-venv
@@ -1,13 +1,13 @@
diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt
index d90a2a355..bb1a7de12 100644 index 2714e6f59..a69da76b4 100644
--- a/cmake/CMakeLists.txt --- a/cmake/CMakeLists.txt
+++ b/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt
@@ -295,7 +295,7 @@ if (onnxruntime_USE_ROCM) @@ -338,7 +338,7 @@ if (onnxruntime_USE_ROCM)
if (ROCM_VERSION_DEV VERSION_LESS "6.2")
message(FATAL_ERROR "CMAKE_HIP_ARCHITECTURES is not set when ROCm version < 6.2")
else()
- set(CMAKE_HIP_ARCHITECTURES "gfx908;gfx90a;gfx1030;gfx1100;gfx1101;gfx940;gfx941;gfx942;gfx1200;gfx1201")
+ set(CMAKE_HIP_ARCHITECTURES "gfx900;gfx908;gfx90a;gfx1030;gfx1100;gfx1101;gfx1102;gfx940;gfx941;gfx942;gfx1200;gfx1201")
endif()
endif() endif()
if (NOT CMAKE_HIP_ARCHITECTURES)
- set(CMAKE_HIP_ARCHITECTURES "gfx908;gfx90a;gfx1030;gfx1100;gfx1101;gfx940;gfx941;gfx942;gfx1200;gfx1201")
+ set(CMAKE_HIP_ARCHITECTURES "gfx900;gfx908;gfx90a;gfx1030;gfx1100;gfx1101;gfx1102;gfx940;gfx941;gfx942;gfx1200;gfx1201")
endif()
file(GLOB rocm_cmake_components ${onnxruntime_ROCM_HOME}/lib/cmake/*)
+3 -2
View File
@@ -1,11 +1,12 @@
experimental_monorepo_root = true experimental_monorepo_root = true
[tools] [tools]
node = "24.11.0" node = "24.11.1"
flutter = "3.35.7" flutter = "3.35.7"
pnpm = "10.20.0" pnpm = "10.22.0"
terragrunt = "0.91.2" terragrunt = "0.91.2"
opentofu = "1.10.6" opentofu = "1.10.6"
java = "25.0.1"
[tools."github:CQLabs/homebrew-dcm"] [tools."github:CQLabs/homebrew-dcm"]
version = "1.30.0" version = "1.30.0"
@@ -75,6 +75,20 @@ class AssetService {
isFlipped = false; isFlipped = false;
} }
if (width == null || height == null) {
if (asset.hasRemote) {
final id = asset is LocalAsset ? asset.remoteId! : (asset as RemoteAsset).id;
final remoteAsset = await _remoteAssetRepository.get(id);
width = remoteAsset?.width?.toDouble();
height = remoteAsset?.height?.toDouble();
} else {
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
final localAsset = await _localAssetRepository.get(id);
width = localAsset?.width?.toDouble();
height = localAsset?.height?.toDouble();
}
}
final orientedWidth = isFlipped ? height : width; final orientedWidth = isFlipped ? height : width;
final orientedHeight = isFlipped ? width : height; final orientedHeight = isFlipped ? width : height;
if (orientedWidth != null && orientedHeight != null && orientedHeight > 0) { if (orientedWidth != null && orientedHeight != null && orientedHeight > 0) {
@@ -363,14 +363,14 @@ extension on Iterable<PlatformAsset> {
} }
} }
extension on PlatformAsset { extension PlatformToLocalAsset on PlatformAsset {
LocalAsset toLocalAsset() => LocalAsset( LocalAsset toLocalAsset() => LocalAsset(
id: id, id: id,
name: name, name: name,
checksum: null, checksum: null,
type: AssetType.values.elementAtOrNull(type) ?? AssetType.other, type: AssetType.values.elementAtOrNull(type) ?? AssetType.other,
createdAt: tryFromSecondsSinceEpoch(createdAt, isUtc: true) ?? DateTime.timestamp(), createdAt: tryFromSecondsSinceEpoch(createdAt, isUtc: true) ?? DateTime.timestamp(),
updatedAt: tryFromSecondsSinceEpoch(createdAt, isUtc: true) ?? DateTime.timestamp(), updatedAt: tryFromSecondsSinceEpoch(updatedAt, isUtc: true) ?? DateTime.timestamp(),
width: width, width: width,
height: height, height: height,
durationInSeconds: durationInSeconds, durationInSeconds: durationInSeconds,
+35 -1
View File
@@ -22,14 +22,16 @@ import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/datetime_helpers.dart';
import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/diff.dart'; import 'package:immich_mobile/utils/diff.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
// ignore: import_rule_photo_manager // ignore: import_rule_photo_manager
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
const int targetVersion = 18; const int targetVersion = 19;
Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async { Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
final hasVersion = Store.tryGet(StoreKey.version) != null; final hasVersion = Store.tryGet(StoreKey.version) != null;
@@ -78,6 +80,12 @@ Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
await Store.put(StoreKey.shouldResetSync, true); await Store.put(StoreKey.shouldResetSync, true);
} }
if (version < 19 && Store.isBetaTimelineEnabled) {
if (!await _populateUpdatedAtTime(drift)) {
return;
}
}
if (targetVersion >= 12) { if (targetVersion >= 12) {
await Store.put(StoreKey.version, targetVersion); await Store.put(StoreKey.version, targetVersion);
return; return;
@@ -221,6 +229,32 @@ Future<void> _migrateDeviceAsset(Isar db) async {
}); });
} }
Future<bool> _populateUpdatedAtTime(Drift db) async {
try {
final nativeApi = NativeSyncApi();
final albums = await nativeApi.getAlbums();
for (final album in albums) {
final assets = await nativeApi.getAssetsForAlbum(album.id);
await db.batch((batch) async {
for (final asset in assets) {
batch.update(
db.localAssetEntity,
LocalAssetEntityCompanion(
updatedAt: Value(tryFromSecondsSinceEpoch(asset.updatedAt, isUtc: true) ?? DateTime.timestamp()),
),
where: (t) => t.id.equals(asset.id),
);
}
});
}
return true;
} catch (error) {
dPrint(() => "[MIGRATION] Error while populating updatedAt time: $error");
return false;
}
}
Future<void> migrateDeviceAssetToSqlite(Isar db, Drift drift) async { Future<void> migrateDeviceAssetToSqlite(Isar db, Drift drift) async {
try { try {
final isarDeviceAssets = await db.deviceAssetEntitys.where().findAll(); final isarDeviceAssets = await db.deviceAssetEntitys.where().findAll();
+15 -2
View File
@@ -137,8 +137,10 @@ Class | Method | HTTP request | Description
*DeprecatedApi* | [**getAllUserAssetsByDeviceId**](doc//DeprecatedApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Retrieve assets by device ID *DeprecatedApi* | [**getAllUserAssetsByDeviceId**](doc//DeprecatedApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Retrieve assets by device ID
*DeprecatedApi* | [**getDeltaSync**](doc//DeprecatedApi.md#getdeltasync) | **POST** /sync/delta-sync | Get delta sync for user *DeprecatedApi* | [**getDeltaSync**](doc//DeprecatedApi.md#getdeltasync) | **POST** /sync/delta-sync | Get delta sync for user
*DeprecatedApi* | [**getFullSyncForUser**](doc//DeprecatedApi.md#getfullsyncforuser) | **POST** /sync/full-sync | Get full sync for user *DeprecatedApi* | [**getFullSyncForUser**](doc//DeprecatedApi.md#getfullsyncforuser) | **POST** /sync/full-sync | Get full sync for user
*DeprecatedApi* | [**getQueuesLegacy**](doc//DeprecatedApi.md#getqueueslegacy) | **GET** /jobs | Retrieve queue counts and status
*DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random | Get random assets *DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random | Get random assets
*DeprecatedApi* | [**replaceAsset**](doc//DeprecatedApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace asset *DeprecatedApi* | [**replaceAsset**](doc//DeprecatedApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace asset
*DeprecatedApi* | [**runQueueCommandLegacy**](doc//DeprecatedApi.md#runqueuecommandlegacy) | **PUT** /jobs/{name} | Run jobs
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | Download asset archive *DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | Download asset archive
*DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | Retrieve download information *DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | Retrieve download information
*DuplicatesApi* | [**deleteDuplicate**](doc//DuplicatesApi.md#deleteduplicate) | **DELETE** /duplicates/{id} | Delete a duplicate *DuplicatesApi* | [**deleteDuplicate**](doc//DuplicatesApi.md#deleteduplicate) | **DELETE** /duplicates/{id} | Delete a duplicate
@@ -198,6 +200,11 @@ Class | Method | HTTP request | Description
*PeopleApi* | [**updatePerson**](doc//PeopleApi.md#updateperson) | **PUT** /people/{id} | Update person *PeopleApi* | [**updatePerson**](doc//PeopleApi.md#updateperson) | **PUT** /people/{id} | Update person
*PluginsApi* | [**getPlugin**](doc//PluginsApi.md#getplugin) | **GET** /plugins/{id} | Retrieve a plugin *PluginsApi* | [**getPlugin**](doc//PluginsApi.md#getplugin) | **GET** /plugins/{id} | Retrieve a plugin
*PluginsApi* | [**getPlugins**](doc//PluginsApi.md#getplugins) | **GET** /plugins | List all plugins *PluginsApi* | [**getPlugins**](doc//PluginsApi.md#getplugins) | **GET** /plugins | List all plugins
*QueuesApi* | [**emptyQueue**](doc//QueuesApi.md#emptyqueue) | **DELETE** /queues/{name}/jobs | Empty a queue
*QueuesApi* | [**getQueue**](doc//QueuesApi.md#getqueue) | **GET** /queues/{name} | Retrieve a queue
*QueuesApi* | [**getQueueJobs**](doc//QueuesApi.md#getqueuejobs) | **GET** /queues/{name}/jobs | Retrieve queue jobs
*QueuesApi* | [**getQueues**](doc//QueuesApi.md#getqueues) | **GET** /queues | List all queues
*QueuesApi* | [**updateQueue**](doc//QueuesApi.md#updatequeue) | **PUT** /queues/{name} | Update a queue
*SearchApi* | [**getAssetsByCity**](doc//SearchApi.md#getassetsbycity) | **GET** /search/cities | Retrieve assets by city *SearchApi* | [**getAssetsByCity**](doc//SearchApi.md#getassetsbycity) | **GET** /search/cities | Retrieve assets by city
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | Retrieve explore data *SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | Retrieve explore data
*SearchApi* | [**getSearchSuggestions**](doc//SearchApi.md#getsearchsuggestions) | **GET** /search/suggestions | Retrieve search suggestions *SearchApi* | [**getSearchSuggestions**](doc//SearchApi.md#getsearchsuggestions) | **GET** /search/suggestions | Retrieve search suggestions
@@ -396,6 +403,7 @@ Class | Method | HTTP request | Description
- [FoldersUpdate](doc//FoldersUpdate.md) - [FoldersUpdate](doc//FoldersUpdate.md)
- [ImageFormat](doc//ImageFormat.md) - [ImageFormat](doc//ImageFormat.md)
- [JobCreateDto](doc//JobCreateDto.md) - [JobCreateDto](doc//JobCreateDto.md)
- [JobName](doc//JobName.md)
- [JobSettingsDto](doc//JobSettingsDto.md) - [JobSettingsDto](doc//JobSettingsDto.md)
- [LibraryResponseDto](doc//LibraryResponseDto.md) - [LibraryResponseDto](doc//LibraryResponseDto.md)
- [LibraryStatsResponseDto](doc//LibraryStatsResponseDto.md) - [LibraryStatsResponseDto](doc//LibraryStatsResponseDto.md)
@@ -465,11 +473,16 @@ Class | Method | HTTP request | Description
- [PurchaseUpdate](doc//PurchaseUpdate.md) - [PurchaseUpdate](doc//PurchaseUpdate.md)
- [QueueCommand](doc//QueueCommand.md) - [QueueCommand](doc//QueueCommand.md)
- [QueueCommandDto](doc//QueueCommandDto.md) - [QueueCommandDto](doc//QueueCommandDto.md)
- [QueueDeleteDto](doc//QueueDeleteDto.md)
- [QueueJobResponseDto](doc//QueueJobResponseDto.md)
- [QueueJobStatus](doc//QueueJobStatus.md)
- [QueueName](doc//QueueName.md) - [QueueName](doc//QueueName.md)
- [QueueResponseDto](doc//QueueResponseDto.md) - [QueueResponseDto](doc//QueueResponseDto.md)
- [QueueResponseLegacyDto](doc//QueueResponseLegacyDto.md)
- [QueueStatisticsDto](doc//QueueStatisticsDto.md) - [QueueStatisticsDto](doc//QueueStatisticsDto.md)
- [QueueStatusDto](doc//QueueStatusDto.md) - [QueueStatusLegacyDto](doc//QueueStatusLegacyDto.md)
- [QueuesResponseDto](doc//QueuesResponseDto.md) - [QueueUpdateDto](doc//QueueUpdateDto.md)
- [QueuesResponseLegacyDto](doc//QueuesResponseLegacyDto.md)
- [RandomSearchDto](doc//RandomSearchDto.md) - [RandomSearchDto](doc//RandomSearchDto.md)
- [RatingsResponse](doc//RatingsResponse.md) - [RatingsResponse](doc//RatingsResponse.md)
- [RatingsUpdate](doc//RatingsUpdate.md) - [RatingsUpdate](doc//RatingsUpdate.md)
+9 -2
View File
@@ -50,6 +50,7 @@ part 'api/notifications_admin_api.dart';
part 'api/partners_api.dart'; part 'api/partners_api.dart';
part 'api/people_api.dart'; part 'api/people_api.dart';
part 'api/plugins_api.dart'; part 'api/plugins_api.dart';
part 'api/queues_api.dart';
part 'api/search_api.dart'; part 'api/search_api.dart';
part 'api/server_api.dart'; part 'api/server_api.dart';
part 'api/sessions_api.dart'; part 'api/sessions_api.dart';
@@ -154,6 +155,7 @@ part 'model/folders_response.dart';
part 'model/folders_update.dart'; part 'model/folders_update.dart';
part 'model/image_format.dart'; part 'model/image_format.dart';
part 'model/job_create_dto.dart'; part 'model/job_create_dto.dart';
part 'model/job_name.dart';
part 'model/job_settings_dto.dart'; part 'model/job_settings_dto.dart';
part 'model/library_response_dto.dart'; part 'model/library_response_dto.dart';
part 'model/library_stats_response_dto.dart'; part 'model/library_stats_response_dto.dart';
@@ -223,11 +225,16 @@ part 'model/purchase_response.dart';
part 'model/purchase_update.dart'; part 'model/purchase_update.dart';
part 'model/queue_command.dart'; part 'model/queue_command.dart';
part 'model/queue_command_dto.dart'; part 'model/queue_command_dto.dart';
part 'model/queue_delete_dto.dart';
part 'model/queue_job_response_dto.dart';
part 'model/queue_job_status.dart';
part 'model/queue_name.dart'; part 'model/queue_name.dart';
part 'model/queue_response_dto.dart'; part 'model/queue_response_dto.dart';
part 'model/queue_response_legacy_dto.dart';
part 'model/queue_statistics_dto.dart'; part 'model/queue_statistics_dto.dart';
part 'model/queue_status_dto.dart'; part 'model/queue_status_legacy_dto.dart';
part 'model/queues_response_dto.dart'; part 'model/queue_update_dto.dart';
part 'model/queues_response_legacy_dto.dart';
part 'model/random_search_dto.dart'; part 'model/random_search_dto.dart';
part 'model/ratings_response.dart'; part 'model/ratings_response.dart';
part 'model/ratings_update.dart'; part 'model/ratings_update.dart';
+109
View File
@@ -248,6 +248,54 @@ class DeprecatedApi {
return null; return null;
} }
/// Retrieve queue counts and status
///
/// Retrieve the counts of the current queue, as well as the current status.
///
/// Note: This method returns the HTTP [Response].
Future<Response> getQueuesLegacyWithHttpInfo() async {
// ignore: prefer_const_declarations
final apiPath = r'/jobs';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
apiPath,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Retrieve queue counts and status
///
/// Retrieve the counts of the current queue, as well as the current status.
Future<QueuesResponseLegacyDto?> getQueuesLegacy() async {
final response = await getQueuesLegacyWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'QueuesResponseLegacyDto',) as QueuesResponseLegacyDto;
}
return null;
}
/// Get random assets /// Get random assets
/// ///
/// Retrieve a specified number of random assets for the authenticated user. /// Retrieve a specified number of random assets for the authenticated user.
@@ -444,4 +492,65 @@ class DeprecatedApi {
} }
return null; return null;
} }
/// Run jobs
///
/// Queue all assets for a specific job type. Defaults to only queueing assets that have not yet been processed, but the force command can be used to re-process all assets.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [QueueName] name (required):
///
/// * [QueueCommandDto] queueCommandDto (required):
Future<Response> runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto,) async {
// ignore: prefer_const_declarations
final apiPath = r'/jobs/{name}'
.replaceAll('{name}', name.toString());
// ignore: prefer_final_locals
Object? postBody = queueCommandDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Run jobs
///
/// Queue all assets for a specific job type. Defaults to only queueing assets that have not yet been processed, but the force command can be used to re-process all assets.
///
/// Parameters:
///
/// * [QueueName] name (required):
///
/// * [QueueCommandDto] queueCommandDto (required):
Future<QueueResponseLegacyDto?> runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async {
final response = await runQueueCommandLegacyWithHttpInfo(name, queueCommandDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'QueueResponseLegacyDto',) as QueueResponseLegacyDto;
}
return null;
}
} }
+4 -4
View File
@@ -97,7 +97,7 @@ class JobsApi {
/// Retrieve queue counts and status /// Retrieve queue counts and status
/// ///
/// Retrieve the counts of the current queue, as well as the current status. /// Retrieve the counts of the current queue, as well as the current status.
Future<QueuesResponseDto?> getQueuesLegacy() async { Future<QueuesResponseLegacyDto?> getQueuesLegacy() async {
final response = await getQueuesLegacyWithHttpInfo(); final response = await getQueuesLegacyWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
@@ -106,7 +106,7 @@ class JobsApi {
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string. // FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'QueuesResponseDto',) as QueuesResponseDto; return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'QueuesResponseLegacyDto',) as QueuesResponseLegacyDto;
} }
return null; return null;
@@ -158,7 +158,7 @@ class JobsApi {
/// * [QueueName] name (required): /// * [QueueName] name (required):
/// ///
/// * [QueueCommandDto] queueCommandDto (required): /// * [QueueCommandDto] queueCommandDto (required):
Future<QueueResponseDto?> runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async { Future<QueueResponseLegacyDto?> runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async {
final response = await runQueueCommandLegacyWithHttpInfo(name, queueCommandDto,); final response = await runQueueCommandLegacyWithHttpInfo(name, queueCommandDto,);
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
@@ -167,7 +167,7 @@ class JobsApi {
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string. // FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'QueueResponseDto',) as QueueResponseDto; return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'QueueResponseLegacyDto',) as QueueResponseLegacyDto;
} }
return null; return null;
+308
View File
@@ -0,0 +1,308 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class QueuesApi {
QueuesApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
final ApiClient apiClient;
/// Empty a queue
///
/// Removes all jobs from the specified queue.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [QueueName] name (required):
///
/// * [QueueDeleteDto] queueDeleteDto (required):
Future<Response> emptyQueueWithHttpInfo(QueueName name, QueueDeleteDto queueDeleteDto,) async {
// ignore: prefer_const_declarations
final apiPath = r'/queues/{name}/jobs'
.replaceAll('{name}', name.toString());
// ignore: prefer_final_locals
Object? postBody = queueDeleteDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'DELETE',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Empty a queue
///
/// Removes all jobs from the specified queue.
///
/// Parameters:
///
/// * [QueueName] name (required):
///
/// * [QueueDeleteDto] queueDeleteDto (required):
Future<void> emptyQueue(QueueName name, QueueDeleteDto queueDeleteDto,) async {
final response = await emptyQueueWithHttpInfo(name, queueDeleteDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Retrieve a queue
///
/// Retrieves a specific queue by its name.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [QueueName] name (required):
Future<Response> getQueueWithHttpInfo(QueueName name,) async {
// ignore: prefer_const_declarations
final apiPath = r'/queues/{name}'
.replaceAll('{name}', name.toString());
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
apiPath,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Retrieve a queue
///
/// Retrieves a specific queue by its name.
///
/// Parameters:
///
/// * [QueueName] name (required):
Future<QueueResponseDto?> getQueue(QueueName name,) async {
final response = await getQueueWithHttpInfo(name,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'QueueResponseDto',) as QueueResponseDto;
}
return null;
}
/// Retrieve queue jobs
///
/// Retrieves a list of queue jobs from the specified queue.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [QueueName] name (required):
///
/// * [List<QueueJobStatus>] status:
Future<Response> getQueueJobsWithHttpInfo(QueueName name, { List<QueueJobStatus>? status, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/queues/{name}/jobs'
.replaceAll('{name}', name.toString());
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (status != null) {
queryParams.addAll(_queryParams('multi', 'status', status));
}
const contentTypes = <String>[];
return apiClient.invokeAPI(
apiPath,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Retrieve queue jobs
///
/// Retrieves a list of queue jobs from the specified queue.
///
/// Parameters:
///
/// * [QueueName] name (required):
///
/// * [List<QueueJobStatus>] status:
Future<List<QueueJobResponseDto>?> getQueueJobs(QueueName name, { List<QueueJobStatus>? status, }) async {
final response = await getQueueJobsWithHttpInfo(name, status: status, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<QueueJobResponseDto>') as List)
.cast<QueueJobResponseDto>()
.toList(growable: false);
}
return null;
}
/// List all queues
///
/// Retrieves a list of queues.
///
/// Note: This method returns the HTTP [Response].
Future<Response> getQueuesWithHttpInfo() async {
// ignore: prefer_const_declarations
final apiPath = r'/queues';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
apiPath,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// List all queues
///
/// Retrieves a list of queues.
Future<List<QueueResponseDto>?> getQueues() async {
final response = await getQueuesWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<QueueResponseDto>') as List)
.cast<QueueResponseDto>()
.toList(growable: false);
}
return null;
}
/// Update a queue
///
/// Change the paused status of a specific queue.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [QueueName] name (required):
///
/// * [QueueUpdateDto] queueUpdateDto (required):
Future<Response> updateQueueWithHttpInfo(QueueName name, QueueUpdateDto queueUpdateDto,) async {
// ignore: prefer_const_declarations
final apiPath = r'/queues/{name}'
.replaceAll('{name}', name.toString());
// ignore: prefer_final_locals
Object? postBody = queueUpdateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Update a queue
///
/// Change the paused status of a specific queue.
///
/// Parameters:
///
/// * [QueueName] name (required):
///
/// * [QueueUpdateDto] queueUpdateDto (required):
Future<QueueResponseDto?> updateQueue(QueueName name, QueueUpdateDto queueUpdateDto,) async {
final response = await updateQueueWithHttpInfo(name, queueUpdateDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'QueueResponseDto',) as QueueResponseDto;
}
return null;
}
}
+16 -4
View File
@@ -358,6 +358,8 @@ class ApiClient {
return ImageFormatTypeTransformer().decode(value); return ImageFormatTypeTransformer().decode(value);
case 'JobCreateDto': case 'JobCreateDto':
return JobCreateDto.fromJson(value); return JobCreateDto.fromJson(value);
case 'JobName':
return JobNameTypeTransformer().decode(value);
case 'JobSettingsDto': case 'JobSettingsDto':
return JobSettingsDto.fromJson(value); return JobSettingsDto.fromJson(value);
case 'LibraryResponseDto': case 'LibraryResponseDto':
@@ -496,16 +498,26 @@ class ApiClient {
return QueueCommandTypeTransformer().decode(value); return QueueCommandTypeTransformer().decode(value);
case 'QueueCommandDto': case 'QueueCommandDto':
return QueueCommandDto.fromJson(value); return QueueCommandDto.fromJson(value);
case 'QueueDeleteDto':
return QueueDeleteDto.fromJson(value);
case 'QueueJobResponseDto':
return QueueJobResponseDto.fromJson(value);
case 'QueueJobStatus':
return QueueJobStatusTypeTransformer().decode(value);
case 'QueueName': case 'QueueName':
return QueueNameTypeTransformer().decode(value); return QueueNameTypeTransformer().decode(value);
case 'QueueResponseDto': case 'QueueResponseDto':
return QueueResponseDto.fromJson(value); return QueueResponseDto.fromJson(value);
case 'QueueResponseLegacyDto':
return QueueResponseLegacyDto.fromJson(value);
case 'QueueStatisticsDto': case 'QueueStatisticsDto':
return QueueStatisticsDto.fromJson(value); return QueueStatisticsDto.fromJson(value);
case 'QueueStatusDto': case 'QueueStatusLegacyDto':
return QueueStatusDto.fromJson(value); return QueueStatusLegacyDto.fromJson(value);
case 'QueuesResponseDto': case 'QueueUpdateDto':
return QueuesResponseDto.fromJson(value); return QueueUpdateDto.fromJson(value);
case 'QueuesResponseLegacyDto':
return QueuesResponseLegacyDto.fromJson(value);
case 'RandomSearchDto': case 'RandomSearchDto':
return RandomSearchDto.fromJson(value); return RandomSearchDto.fromJson(value);
case 'RatingsResponse': case 'RatingsResponse':
+6
View File
@@ -94,6 +94,9 @@ String parameterToString(dynamic value) {
if (value is ImageFormat) { if (value is ImageFormat) {
return ImageFormatTypeTransformer().encode(value).toString(); return ImageFormatTypeTransformer().encode(value).toString();
} }
if (value is JobName) {
return JobNameTypeTransformer().encode(value).toString();
}
if (value is LogLevel) { if (value is LogLevel) {
return LogLevelTypeTransformer().encode(value).toString(); return LogLevelTypeTransformer().encode(value).toString();
} }
@@ -133,6 +136,9 @@ String parameterToString(dynamic value) {
if (value is QueueCommand) { if (value is QueueCommand) {
return QueueCommandTypeTransformer().encode(value).toString(); return QueueCommandTypeTransformer().encode(value).toString();
} }
if (value is QueueJobStatus) {
return QueueJobStatusTypeTransformer().encode(value).toString();
}
if (value is QueueName) { if (value is QueueName) {
return QueueNameTypeTransformer().encode(value).toString(); return QueueNameTypeTransformer().encode(value).toString();
} }
+244
View File
@@ -0,0 +1,244 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class JobName {
/// Instantiate a new enum with the provided [value].
const JobName._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const assetDelete = JobName._(r'AssetDelete');
static const assetDeleteCheck = JobName._(r'AssetDeleteCheck');
static const assetDetectFacesQueueAll = JobName._(r'AssetDetectFacesQueueAll');
static const assetDetectFaces = JobName._(r'AssetDetectFaces');
static const assetDetectDuplicatesQueueAll = JobName._(r'AssetDetectDuplicatesQueueAll');
static const assetDetectDuplicates = JobName._(r'AssetDetectDuplicates');
static const assetEncodeVideoQueueAll = JobName._(r'AssetEncodeVideoQueueAll');
static const assetEncodeVideo = JobName._(r'AssetEncodeVideo');
static const assetEmptyTrash = JobName._(r'AssetEmptyTrash');
static const assetExtractMetadataQueueAll = JobName._(r'AssetExtractMetadataQueueAll');
static const assetExtractMetadata = JobName._(r'AssetExtractMetadata');
static const assetFileMigration = JobName._(r'AssetFileMigration');
static const assetGenerateThumbnailsQueueAll = JobName._(r'AssetGenerateThumbnailsQueueAll');
static const assetGenerateThumbnails = JobName._(r'AssetGenerateThumbnails');
static const auditLogCleanup = JobName._(r'AuditLogCleanup');
static const auditTableCleanup = JobName._(r'AuditTableCleanup');
static const databaseBackup = JobName._(r'DatabaseBackup');
static const facialRecognitionQueueAll = JobName._(r'FacialRecognitionQueueAll');
static const facialRecognition = JobName._(r'FacialRecognition');
static const fileDelete = JobName._(r'FileDelete');
static const fileMigrationQueueAll = JobName._(r'FileMigrationQueueAll');
static const libraryDeleteCheck = JobName._(r'LibraryDeleteCheck');
static const libraryDelete = JobName._(r'LibraryDelete');
static const libraryRemoveAsset = JobName._(r'LibraryRemoveAsset');
static const libraryScanAssetsQueueAll = JobName._(r'LibraryScanAssetsQueueAll');
static const librarySyncAssets = JobName._(r'LibrarySyncAssets');
static const librarySyncFilesQueueAll = JobName._(r'LibrarySyncFilesQueueAll');
static const librarySyncFiles = JobName._(r'LibrarySyncFiles');
static const libraryScanQueueAll = JobName._(r'LibraryScanQueueAll');
static const memoryCleanup = JobName._(r'MemoryCleanup');
static const memoryGenerate = JobName._(r'MemoryGenerate');
static const notificationsCleanup = JobName._(r'NotificationsCleanup');
static const notifyUserSignup = JobName._(r'NotifyUserSignup');
static const notifyAlbumInvite = JobName._(r'NotifyAlbumInvite');
static const notifyAlbumUpdate = JobName._(r'NotifyAlbumUpdate');
static const userDelete = JobName._(r'UserDelete');
static const userDeleteCheck = JobName._(r'UserDeleteCheck');
static const userSyncUsage = JobName._(r'UserSyncUsage');
static const personCleanup = JobName._(r'PersonCleanup');
static const personFileMigration = JobName._(r'PersonFileMigration');
static const personGenerateThumbnail = JobName._(r'PersonGenerateThumbnail');
static const sessionCleanup = JobName._(r'SessionCleanup');
static const sendMail = JobName._(r'SendMail');
static const sidecarQueueAll = JobName._(r'SidecarQueueAll');
static const sidecarCheck = JobName._(r'SidecarCheck');
static const sidecarWrite = JobName._(r'SidecarWrite');
static const smartSearchQueueAll = JobName._(r'SmartSearchQueueAll');
static const smartSearch = JobName._(r'SmartSearch');
static const storageTemplateMigration = JobName._(r'StorageTemplateMigration');
static const storageTemplateMigrationSingle = JobName._(r'StorageTemplateMigrationSingle');
static const tagCleanup = JobName._(r'TagCleanup');
static const versionCheck = JobName._(r'VersionCheck');
static const ocrQueueAll = JobName._(r'OcrQueueAll');
static const ocr = JobName._(r'Ocr');
static const workflowRun = JobName._(r'WorkflowRun');
/// List of all possible values in this [enum][JobName].
static const values = <JobName>[
assetDelete,
assetDeleteCheck,
assetDetectFacesQueueAll,
assetDetectFaces,
assetDetectDuplicatesQueueAll,
assetDetectDuplicates,
assetEncodeVideoQueueAll,
assetEncodeVideo,
assetEmptyTrash,
assetExtractMetadataQueueAll,
assetExtractMetadata,
assetFileMigration,
assetGenerateThumbnailsQueueAll,
assetGenerateThumbnails,
auditLogCleanup,
auditTableCleanup,
databaseBackup,
facialRecognitionQueueAll,
facialRecognition,
fileDelete,
fileMigrationQueueAll,
libraryDeleteCheck,
libraryDelete,
libraryRemoveAsset,
libraryScanAssetsQueueAll,
librarySyncAssets,
librarySyncFilesQueueAll,
librarySyncFiles,
libraryScanQueueAll,
memoryCleanup,
memoryGenerate,
notificationsCleanup,
notifyUserSignup,
notifyAlbumInvite,
notifyAlbumUpdate,
userDelete,
userDeleteCheck,
userSyncUsage,
personCleanup,
personFileMigration,
personGenerateThumbnail,
sessionCleanup,
sendMail,
sidecarQueueAll,
sidecarCheck,
sidecarWrite,
smartSearchQueueAll,
smartSearch,
storageTemplateMigration,
storageTemplateMigrationSingle,
tagCleanup,
versionCheck,
ocrQueueAll,
ocr,
workflowRun,
];
static JobName? fromJson(dynamic value) => JobNameTypeTransformer().decode(value);
static List<JobName> listFromJson(dynamic json, {bool growable = false,}) {
final result = <JobName>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = JobName.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [JobName] to String,
/// and [decode] dynamic data back to [JobName].
class JobNameTypeTransformer {
factory JobNameTypeTransformer() => _instance ??= const JobNameTypeTransformer._();
const JobNameTypeTransformer._();
String encode(JobName data) => data.value;
/// Decodes a [dynamic value][data] to a JobName.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
JobName? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'AssetDelete': return JobName.assetDelete;
case r'AssetDeleteCheck': return JobName.assetDeleteCheck;
case r'AssetDetectFacesQueueAll': return JobName.assetDetectFacesQueueAll;
case r'AssetDetectFaces': return JobName.assetDetectFaces;
case r'AssetDetectDuplicatesQueueAll': return JobName.assetDetectDuplicatesQueueAll;
case r'AssetDetectDuplicates': return JobName.assetDetectDuplicates;
case r'AssetEncodeVideoQueueAll': return JobName.assetEncodeVideoQueueAll;
case r'AssetEncodeVideo': return JobName.assetEncodeVideo;
case r'AssetEmptyTrash': return JobName.assetEmptyTrash;
case r'AssetExtractMetadataQueueAll': return JobName.assetExtractMetadataQueueAll;
case r'AssetExtractMetadata': return JobName.assetExtractMetadata;
case r'AssetFileMigration': return JobName.assetFileMigration;
case r'AssetGenerateThumbnailsQueueAll': return JobName.assetGenerateThumbnailsQueueAll;
case r'AssetGenerateThumbnails': return JobName.assetGenerateThumbnails;
case r'AuditLogCleanup': return JobName.auditLogCleanup;
case r'AuditTableCleanup': return JobName.auditTableCleanup;
case r'DatabaseBackup': return JobName.databaseBackup;
case r'FacialRecognitionQueueAll': return JobName.facialRecognitionQueueAll;
case r'FacialRecognition': return JobName.facialRecognition;
case r'FileDelete': return JobName.fileDelete;
case r'FileMigrationQueueAll': return JobName.fileMigrationQueueAll;
case r'LibraryDeleteCheck': return JobName.libraryDeleteCheck;
case r'LibraryDelete': return JobName.libraryDelete;
case r'LibraryRemoveAsset': return JobName.libraryRemoveAsset;
case r'LibraryScanAssetsQueueAll': return JobName.libraryScanAssetsQueueAll;
case r'LibrarySyncAssets': return JobName.librarySyncAssets;
case r'LibrarySyncFilesQueueAll': return JobName.librarySyncFilesQueueAll;
case r'LibrarySyncFiles': return JobName.librarySyncFiles;
case r'LibraryScanQueueAll': return JobName.libraryScanQueueAll;
case r'MemoryCleanup': return JobName.memoryCleanup;
case r'MemoryGenerate': return JobName.memoryGenerate;
case r'NotificationsCleanup': return JobName.notificationsCleanup;
case r'NotifyUserSignup': return JobName.notifyUserSignup;
case r'NotifyAlbumInvite': return JobName.notifyAlbumInvite;
case r'NotifyAlbumUpdate': return JobName.notifyAlbumUpdate;
case r'UserDelete': return JobName.userDelete;
case r'UserDeleteCheck': return JobName.userDeleteCheck;
case r'UserSyncUsage': return JobName.userSyncUsage;
case r'PersonCleanup': return JobName.personCleanup;
case r'PersonFileMigration': return JobName.personFileMigration;
case r'PersonGenerateThumbnail': return JobName.personGenerateThumbnail;
case r'SessionCleanup': return JobName.sessionCleanup;
case r'SendMail': return JobName.sendMail;
case r'SidecarQueueAll': return JobName.sidecarQueueAll;
case r'SidecarCheck': return JobName.sidecarCheck;
case r'SidecarWrite': return JobName.sidecarWrite;
case r'SmartSearchQueueAll': return JobName.smartSearchQueueAll;
case r'SmartSearch': return JobName.smartSearch;
case r'StorageTemplateMigration': return JobName.storageTemplateMigration;
case r'StorageTemplateMigrationSingle': return JobName.storageTemplateMigrationSingle;
case r'TagCleanup': return JobName.tagCleanup;
case r'VersionCheck': return JobName.versionCheck;
case r'OcrQueueAll': return JobName.ocrQueueAll;
case r'Ocr': return JobName.ocr;
case r'WorkflowRun': return JobName.workflowRun;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [JobNameTypeTransformer] instance.
static JobNameTypeTransformer? _instance;
}
+18
View File
@@ -152,6 +152,12 @@ class Permission {
static const userProfileImagePeriodRead = Permission._(r'userProfileImage.read'); static const userProfileImagePeriodRead = Permission._(r'userProfileImage.read');
static const userProfileImagePeriodUpdate = Permission._(r'userProfileImage.update'); static const userProfileImagePeriodUpdate = Permission._(r'userProfileImage.update');
static const userProfileImagePeriodDelete = Permission._(r'userProfileImage.delete'); static const userProfileImagePeriodDelete = Permission._(r'userProfileImage.delete');
static const queuePeriodRead = Permission._(r'queue.read');
static const queuePeriodUpdate = Permission._(r'queue.update');
static const queueJobPeriodCreate = Permission._(r'queueJob.create');
static const queueJobPeriodRead = Permission._(r'queueJob.read');
static const queueJobPeriodUpdate = Permission._(r'queueJob.update');
static const queueJobPeriodDelete = Permission._(r'queueJob.delete');
static const workflowPeriodCreate = Permission._(r'workflow.create'); static const workflowPeriodCreate = Permission._(r'workflow.create');
static const workflowPeriodRead = Permission._(r'workflow.read'); static const workflowPeriodRead = Permission._(r'workflow.read');
static const workflowPeriodUpdate = Permission._(r'workflow.update'); static const workflowPeriodUpdate = Permission._(r'workflow.update');
@@ -294,6 +300,12 @@ class Permission {
userProfileImagePeriodRead, userProfileImagePeriodRead,
userProfileImagePeriodUpdate, userProfileImagePeriodUpdate,
userProfileImagePeriodDelete, userProfileImagePeriodDelete,
queuePeriodRead,
queuePeriodUpdate,
queueJobPeriodCreate,
queueJobPeriodRead,
queueJobPeriodUpdate,
queueJobPeriodDelete,
workflowPeriodCreate, workflowPeriodCreate,
workflowPeriodRead, workflowPeriodRead,
workflowPeriodUpdate, workflowPeriodUpdate,
@@ -471,6 +483,12 @@ class PermissionTypeTransformer {
case r'userProfileImage.read': return Permission.userProfileImagePeriodRead; case r'userProfileImage.read': return Permission.userProfileImagePeriodRead;
case r'userProfileImage.update': return Permission.userProfileImagePeriodUpdate; case r'userProfileImage.update': return Permission.userProfileImagePeriodUpdate;
case r'userProfileImage.delete': return Permission.userProfileImagePeriodDelete; case r'userProfileImage.delete': return Permission.userProfileImagePeriodDelete;
case r'queue.read': return Permission.queuePeriodRead;
case r'queue.update': return Permission.queuePeriodUpdate;
case r'queueJob.create': return Permission.queueJobPeriodCreate;
case r'queueJob.read': return Permission.queueJobPeriodRead;
case r'queueJob.update': return Permission.queueJobPeriodUpdate;
case r'queueJob.delete': return Permission.queueJobPeriodDelete;
case r'workflow.create': return Permission.workflowPeriodCreate; case r'workflow.create': return Permission.workflowPeriodCreate;
case r'workflow.read': return Permission.workflowPeriodRead; case r'workflow.read': return Permission.workflowPeriodRead;
case r'workflow.update': return Permission.workflowPeriodUpdate; case r'workflow.update': return Permission.workflowPeriodUpdate;
+109
View File
@@ -0,0 +1,109 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class QueueDeleteDto {
/// Returns a new [QueueDeleteDto] instance.
QueueDeleteDto({
this.failed,
});
/// If true, will also remove failed jobs from the queue.
///
/// 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
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
bool? failed;
@override
bool operator ==(Object other) => identical(this, other) || other is QueueDeleteDto &&
other.failed == failed;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(failed == null ? 0 : failed!.hashCode);
@override
String toString() => 'QueueDeleteDto[failed=$failed]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.failed != null) {
json[r'failed'] = this.failed;
} else {
// json[r'failed'] = null;
}
return json;
}
/// Returns a new [QueueDeleteDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static QueueDeleteDto? fromJson(dynamic value) {
upgradeDto(value, "QueueDeleteDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return QueueDeleteDto(
failed: mapValueOfType<bool>(json, r'failed'),
);
}
return null;
}
static List<QueueDeleteDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <QueueDeleteDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = QueueDeleteDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, QueueDeleteDto> mapFromJson(dynamic json) {
final map = <String, QueueDeleteDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = QueueDeleteDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of QueueDeleteDto-objects as value to a dart map
static Map<String, List<QueueDeleteDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<QueueDeleteDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = QueueDeleteDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}
+132
View File
@@ -0,0 +1,132 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class QueueJobResponseDto {
/// Returns a new [QueueJobResponseDto] instance.
QueueJobResponseDto({
required this.data,
this.id,
required this.name,
required this.timestamp,
});
Object data;
///
/// 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
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? id;
JobName name;
int timestamp;
@override
bool operator ==(Object other) => identical(this, other) || other is QueueJobResponseDto &&
other.data == data &&
other.id == id &&
other.name == name &&
other.timestamp == timestamp;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(data.hashCode) +
(id == null ? 0 : id!.hashCode) +
(name.hashCode) +
(timestamp.hashCode);
@override
String toString() => 'QueueJobResponseDto[data=$data, id=$id, name=$name, timestamp=$timestamp]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'data'] = this.data;
if (this.id != null) {
json[r'id'] = this.id;
} else {
// json[r'id'] = null;
}
json[r'name'] = this.name;
json[r'timestamp'] = this.timestamp;
return json;
}
/// Returns a new [QueueJobResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static QueueJobResponseDto? fromJson(dynamic value) {
upgradeDto(value, "QueueJobResponseDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return QueueJobResponseDto(
data: mapValueOfType<Object>(json, r'data')!,
id: mapValueOfType<String>(json, r'id'),
name: JobName.fromJson(json[r'name'])!,
timestamp: mapValueOfType<int>(json, r'timestamp')!,
);
}
return null;
}
static List<QueueJobResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <QueueJobResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = QueueJobResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, QueueJobResponseDto> mapFromJson(dynamic json) {
final map = <String, QueueJobResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = QueueJobResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of QueueJobResponseDto-objects as value to a dart map
static Map<String, List<QueueJobResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<QueueJobResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = QueueJobResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'data',
'name',
'timestamp',
};
}
+97
View File
@@ -0,0 +1,97 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class QueueJobStatus {
/// Instantiate a new enum with the provided [value].
const QueueJobStatus._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const active = QueueJobStatus._(r'active');
static const failed = QueueJobStatus._(r'failed');
static const completed = QueueJobStatus._(r'completed');
static const delayed = QueueJobStatus._(r'delayed');
static const waiting = QueueJobStatus._(r'waiting');
static const paused = QueueJobStatus._(r'paused');
/// List of all possible values in this [enum][QueueJobStatus].
static const values = <QueueJobStatus>[
active,
failed,
completed,
delayed,
waiting,
paused,
];
static QueueJobStatus? fromJson(dynamic value) => QueueJobStatusTypeTransformer().decode(value);
static List<QueueJobStatus> listFromJson(dynamic json, {bool growable = false,}) {
final result = <QueueJobStatus>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = QueueJobStatus.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [QueueJobStatus] to String,
/// and [decode] dynamic data back to [QueueJobStatus].
class QueueJobStatusTypeTransformer {
factory QueueJobStatusTypeTransformer() => _instance ??= const QueueJobStatusTypeTransformer._();
const QueueJobStatusTypeTransformer._();
String encode(QueueJobStatus data) => data.value;
/// Decodes a [dynamic value][data] to a QueueJobStatus.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
QueueJobStatus? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'active': return QueueJobStatus.active;
case r'failed': return QueueJobStatus.failed;
case r'completed': return QueueJobStatus.completed;
case r'delayed': return QueueJobStatus.delayed;
case r'waiting': return QueueJobStatus.waiting;
case r'paused': return QueueJobStatus.paused;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [QueueJobStatusTypeTransformer] instance.
static QueueJobStatusTypeTransformer? _instance;
}
+23 -15
View File
@@ -13,32 +13,38 @@ part of openapi.api;
class QueueResponseDto { class QueueResponseDto {
/// Returns a new [QueueResponseDto] instance. /// Returns a new [QueueResponseDto] instance.
QueueResponseDto({ QueueResponseDto({
required this.jobCounts, required this.isPaused,
required this.queueStatus, required this.name,
required this.statistics,
}); });
QueueStatisticsDto jobCounts; bool isPaused;
QueueStatusDto queueStatus; QueueName name;
QueueStatisticsDto statistics;
@override @override
bool operator ==(Object other) => identical(this, other) || other is QueueResponseDto && bool operator ==(Object other) => identical(this, other) || other is QueueResponseDto &&
other.jobCounts == jobCounts && other.isPaused == isPaused &&
other.queueStatus == queueStatus; other.name == name &&
other.statistics == statistics;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(jobCounts.hashCode) + (isPaused.hashCode) +
(queueStatus.hashCode); (name.hashCode) +
(statistics.hashCode);
@override @override
String toString() => 'QueueResponseDto[jobCounts=$jobCounts, queueStatus=$queueStatus]'; String toString() => 'QueueResponseDto[isPaused=$isPaused, name=$name, statistics=$statistics]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'jobCounts'] = this.jobCounts; json[r'isPaused'] = this.isPaused;
json[r'queueStatus'] = this.queueStatus; json[r'name'] = this.name;
json[r'statistics'] = this.statistics;
return json; return json;
} }
@@ -51,8 +57,9 @@ class QueueResponseDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return QueueResponseDto( return QueueResponseDto(
jobCounts: QueueStatisticsDto.fromJson(json[r'jobCounts'])!, isPaused: mapValueOfType<bool>(json, r'isPaused')!,
queueStatus: QueueStatusDto.fromJson(json[r'queueStatus'])!, name: QueueName.fromJson(json[r'name'])!,
statistics: QueueStatisticsDto.fromJson(json[r'statistics'])!,
); );
} }
return null; return null;
@@ -100,8 +107,9 @@ class QueueResponseDto {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'jobCounts', 'isPaused',
'queueStatus', 'name',
'statistics',
}; };
} }
+107
View File
@@ -0,0 +1,107 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class QueueResponseLegacyDto {
/// Returns a new [QueueResponseLegacyDto] instance.
QueueResponseLegacyDto({
required this.jobCounts,
required this.queueStatus,
});
QueueStatisticsDto jobCounts;
QueueStatusLegacyDto queueStatus;
@override
bool operator ==(Object other) => identical(this, other) || other is QueueResponseLegacyDto &&
other.jobCounts == jobCounts &&
other.queueStatus == queueStatus;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(jobCounts.hashCode) +
(queueStatus.hashCode);
@override
String toString() => 'QueueResponseLegacyDto[jobCounts=$jobCounts, queueStatus=$queueStatus]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'jobCounts'] = this.jobCounts;
json[r'queueStatus'] = this.queueStatus;
return json;
}
/// Returns a new [QueueResponseLegacyDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static QueueResponseLegacyDto? fromJson(dynamic value) {
upgradeDto(value, "QueueResponseLegacyDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return QueueResponseLegacyDto(
jobCounts: QueueStatisticsDto.fromJson(json[r'jobCounts'])!,
queueStatus: QueueStatusLegacyDto.fromJson(json[r'queueStatus'])!,
);
}
return null;
}
static List<QueueResponseLegacyDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <QueueResponseLegacyDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = QueueResponseLegacyDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, QueueResponseLegacyDto> mapFromJson(dynamic json) {
final map = <String, QueueResponseLegacyDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = QueueResponseLegacyDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of QueueResponseLegacyDto-objects as value to a dart map
static Map<String, List<QueueResponseLegacyDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<QueueResponseLegacyDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = QueueResponseLegacyDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'jobCounts',
'queueStatus',
};
}
@@ -10,9 +10,9 @@
part of openapi.api; part of openapi.api;
class QueueStatusDto { class QueueStatusLegacyDto {
/// Returns a new [QueueStatusDto] instance. /// Returns a new [QueueStatusLegacyDto] instance.
QueueStatusDto({ QueueStatusLegacyDto({
required this.isActive, required this.isActive,
required this.isPaused, required this.isPaused,
}); });
@@ -22,7 +22,7 @@ class QueueStatusDto {
bool isPaused; bool isPaused;
@override @override
bool operator ==(Object other) => identical(this, other) || other is QueueStatusDto && bool operator ==(Object other) => identical(this, other) || other is QueueStatusLegacyDto &&
other.isActive == isActive && other.isActive == isActive &&
other.isPaused == isPaused; other.isPaused == isPaused;
@@ -33,7 +33,7 @@ class QueueStatusDto {
(isPaused.hashCode); (isPaused.hashCode);
@override @override
String toString() => 'QueueStatusDto[isActive=$isActive, isPaused=$isPaused]'; String toString() => 'QueueStatusLegacyDto[isActive=$isActive, isPaused=$isPaused]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@@ -42,15 +42,15 @@ class QueueStatusDto {
return json; return json;
} }
/// Returns a new [QueueStatusDto] instance and imports its values from /// Returns a new [QueueStatusLegacyDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise. /// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods // ignore: prefer_constructors_over_static_methods
static QueueStatusDto? fromJson(dynamic value) { static QueueStatusLegacyDto? fromJson(dynamic value) {
upgradeDto(value, "QueueStatusDto"); upgradeDto(value, "QueueStatusLegacyDto");
if (value is Map) { if (value is Map) {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return QueueStatusDto( return QueueStatusLegacyDto(
isActive: mapValueOfType<bool>(json, r'isActive')!, isActive: mapValueOfType<bool>(json, r'isActive')!,
isPaused: mapValueOfType<bool>(json, r'isPaused')!, isPaused: mapValueOfType<bool>(json, r'isPaused')!,
); );
@@ -58,11 +58,11 @@ class QueueStatusDto {
return null; return null;
} }
static List<QueueStatusDto> listFromJson(dynamic json, {bool growable = false,}) { static List<QueueStatusLegacyDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <QueueStatusDto>[]; final result = <QueueStatusLegacyDto>[];
if (json is List && json.isNotEmpty) { if (json is List && json.isNotEmpty) {
for (final row in json) { for (final row in json) {
final value = QueueStatusDto.fromJson(row); final value = QueueStatusLegacyDto.fromJson(row);
if (value != null) { if (value != null) {
result.add(value); result.add(value);
} }
@@ -71,12 +71,12 @@ class QueueStatusDto {
return result.toList(growable: growable); return result.toList(growable: growable);
} }
static Map<String, QueueStatusDto> mapFromJson(dynamic json) { static Map<String, QueueStatusLegacyDto> mapFromJson(dynamic json) {
final map = <String, QueueStatusDto>{}; final map = <String, QueueStatusLegacyDto>{};
if (json is Map && json.isNotEmpty) { if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) { for (final entry in json.entries) {
final value = QueueStatusDto.fromJson(entry.value); final value = QueueStatusLegacyDto.fromJson(entry.value);
if (value != null) { if (value != null) {
map[entry.key] = value; map[entry.key] = value;
} }
@@ -85,14 +85,14 @@ class QueueStatusDto {
return map; return map;
} }
// maps a json object with a list of QueueStatusDto-objects as value to a dart map // maps a json object with a list of QueueStatusLegacyDto-objects as value to a dart map
static Map<String, List<QueueStatusDto>> mapListFromJson(dynamic json, {bool growable = false,}) { static Map<String, List<QueueStatusLegacyDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<QueueStatusDto>>{}; final map = <String, List<QueueStatusLegacyDto>>{};
if (json is Map && json.isNotEmpty) { if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments // ignore: parameter_assignments
json = json.cast<String, dynamic>(); json = json.cast<String, dynamic>();
for (final entry in json.entries) { for (final entry in json.entries) {
map[entry.key] = QueueStatusDto.listFromJson(entry.value, growable: growable,); map[entry.key] = QueueStatusLegacyDto.listFromJson(entry.value, growable: growable,);
} }
} }
return map; return map;
+108
View File
@@ -0,0 +1,108 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class QueueUpdateDto {
/// Returns a new [QueueUpdateDto] instance.
QueueUpdateDto({
this.isPaused,
});
///
/// 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
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
bool? isPaused;
@override
bool operator ==(Object other) => identical(this, other) || other is QueueUpdateDto &&
other.isPaused == isPaused;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(isPaused == null ? 0 : isPaused!.hashCode);
@override
String toString() => 'QueueUpdateDto[isPaused=$isPaused]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.isPaused != null) {
json[r'isPaused'] = this.isPaused;
} else {
// json[r'isPaused'] = null;
}
return json;
}
/// Returns a new [QueueUpdateDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static QueueUpdateDto? fromJson(dynamic value) {
upgradeDto(value, "QueueUpdateDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return QueueUpdateDto(
isPaused: mapValueOfType<bool>(json, r'isPaused'),
);
}
return null;
}
static List<QueueUpdateDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <QueueUpdateDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = QueueUpdateDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, QueueUpdateDto> mapFromJson(dynamic json) {
final map = <String, QueueUpdateDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = QueueUpdateDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of QueueUpdateDto-objects as value to a dart map
static Map<String, List<QueueUpdateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<QueueUpdateDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = QueueUpdateDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}
@@ -10,9 +10,9 @@
part of openapi.api; part of openapi.api;
class QueuesResponseDto { class QueuesResponseLegacyDto {
/// Returns a new [QueuesResponseDto] instance. /// Returns a new [QueuesResponseLegacyDto] instance.
QueuesResponseDto({ QueuesResponseLegacyDto({
required this.backgroundTask, required this.backgroundTask,
required this.backupDatabase, required this.backupDatabase,
required this.duplicateDetection, required this.duplicateDetection,
@@ -32,42 +32,42 @@ class QueuesResponseDto {
required this.workflow, required this.workflow,
}); });
QueueResponseDto backgroundTask; QueueResponseLegacyDto backgroundTask;
QueueResponseDto backupDatabase; QueueResponseLegacyDto backupDatabase;
QueueResponseDto duplicateDetection; QueueResponseLegacyDto duplicateDetection;
QueueResponseDto faceDetection; QueueResponseLegacyDto faceDetection;
QueueResponseDto facialRecognition; QueueResponseLegacyDto facialRecognition;
QueueResponseDto library_; QueueResponseLegacyDto library_;
QueueResponseDto metadataExtraction; QueueResponseLegacyDto metadataExtraction;
QueueResponseDto migration; QueueResponseLegacyDto migration;
QueueResponseDto notifications; QueueResponseLegacyDto notifications;
QueueResponseDto ocr; QueueResponseLegacyDto ocr;
QueueResponseDto search; QueueResponseLegacyDto search;
QueueResponseDto sidecar; QueueResponseLegacyDto sidecar;
QueueResponseDto smartSearch; QueueResponseLegacyDto smartSearch;
QueueResponseDto storageTemplateMigration; QueueResponseLegacyDto storageTemplateMigration;
QueueResponseDto thumbnailGeneration; QueueResponseLegacyDto thumbnailGeneration;
QueueResponseDto videoConversion; QueueResponseLegacyDto videoConversion;
QueueResponseDto workflow; QueueResponseLegacyDto workflow;
@override @override
bool operator ==(Object other) => identical(this, other) || other is QueuesResponseDto && bool operator ==(Object other) => identical(this, other) || other is QueuesResponseLegacyDto &&
other.backgroundTask == backgroundTask && other.backgroundTask == backgroundTask &&
other.backupDatabase == backupDatabase && other.backupDatabase == backupDatabase &&
other.duplicateDetection == duplicateDetection && other.duplicateDetection == duplicateDetection &&
@@ -108,7 +108,7 @@ class QueuesResponseDto {
(workflow.hashCode); (workflow.hashCode);
@override @override
String toString() => 'QueuesResponseDto[backgroundTask=$backgroundTask, backupDatabase=$backupDatabase, duplicateDetection=$duplicateDetection, faceDetection=$faceDetection, facialRecognition=$facialRecognition, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, ocr=$ocr, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, storageTemplateMigration=$storageTemplateMigration, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion, workflow=$workflow]'; String toString() => 'QueuesResponseLegacyDto[backgroundTask=$backgroundTask, backupDatabase=$backupDatabase, duplicateDetection=$duplicateDetection, faceDetection=$faceDetection, facialRecognition=$facialRecognition, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, ocr=$ocr, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, storageTemplateMigration=$storageTemplateMigration, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion, workflow=$workflow]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@@ -132,42 +132,42 @@ class QueuesResponseDto {
return json; return json;
} }
/// Returns a new [QueuesResponseDto] instance and imports its values from /// Returns a new [QueuesResponseLegacyDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise. /// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods // ignore: prefer_constructors_over_static_methods
static QueuesResponseDto? fromJson(dynamic value) { static QueuesResponseLegacyDto? fromJson(dynamic value) {
upgradeDto(value, "QueuesResponseDto"); upgradeDto(value, "QueuesResponseLegacyDto");
if (value is Map) { if (value is Map) {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return QueuesResponseDto( return QueuesResponseLegacyDto(
backgroundTask: QueueResponseDto.fromJson(json[r'backgroundTask'])!, backgroundTask: QueueResponseLegacyDto.fromJson(json[r'backgroundTask'])!,
backupDatabase: QueueResponseDto.fromJson(json[r'backupDatabase'])!, backupDatabase: QueueResponseLegacyDto.fromJson(json[r'backupDatabase'])!,
duplicateDetection: QueueResponseDto.fromJson(json[r'duplicateDetection'])!, duplicateDetection: QueueResponseLegacyDto.fromJson(json[r'duplicateDetection'])!,
faceDetection: QueueResponseDto.fromJson(json[r'faceDetection'])!, faceDetection: QueueResponseLegacyDto.fromJson(json[r'faceDetection'])!,
facialRecognition: QueueResponseDto.fromJson(json[r'facialRecognition'])!, facialRecognition: QueueResponseLegacyDto.fromJson(json[r'facialRecognition'])!,
library_: QueueResponseDto.fromJson(json[r'library'])!, library_: QueueResponseLegacyDto.fromJson(json[r'library'])!,
metadataExtraction: QueueResponseDto.fromJson(json[r'metadataExtraction'])!, metadataExtraction: QueueResponseLegacyDto.fromJson(json[r'metadataExtraction'])!,
migration: QueueResponseDto.fromJson(json[r'migration'])!, migration: QueueResponseLegacyDto.fromJson(json[r'migration'])!,
notifications: QueueResponseDto.fromJson(json[r'notifications'])!, notifications: QueueResponseLegacyDto.fromJson(json[r'notifications'])!,
ocr: QueueResponseDto.fromJson(json[r'ocr'])!, ocr: QueueResponseLegacyDto.fromJson(json[r'ocr'])!,
search: QueueResponseDto.fromJson(json[r'search'])!, search: QueueResponseLegacyDto.fromJson(json[r'search'])!,
sidecar: QueueResponseDto.fromJson(json[r'sidecar'])!, sidecar: QueueResponseLegacyDto.fromJson(json[r'sidecar'])!,
smartSearch: QueueResponseDto.fromJson(json[r'smartSearch'])!, smartSearch: QueueResponseLegacyDto.fromJson(json[r'smartSearch'])!,
storageTemplateMigration: QueueResponseDto.fromJson(json[r'storageTemplateMigration'])!, storageTemplateMigration: QueueResponseLegacyDto.fromJson(json[r'storageTemplateMigration'])!,
thumbnailGeneration: QueueResponseDto.fromJson(json[r'thumbnailGeneration'])!, thumbnailGeneration: QueueResponseLegacyDto.fromJson(json[r'thumbnailGeneration'])!,
videoConversion: QueueResponseDto.fromJson(json[r'videoConversion'])!, videoConversion: QueueResponseLegacyDto.fromJson(json[r'videoConversion'])!,
workflow: QueueResponseDto.fromJson(json[r'workflow'])!, workflow: QueueResponseLegacyDto.fromJson(json[r'workflow'])!,
); );
} }
return null; return null;
} }
static List<QueuesResponseDto> listFromJson(dynamic json, {bool growable = false,}) { static List<QueuesResponseLegacyDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <QueuesResponseDto>[]; final result = <QueuesResponseLegacyDto>[];
if (json is List && json.isNotEmpty) { if (json is List && json.isNotEmpty) {
for (final row in json) { for (final row in json) {
final value = QueuesResponseDto.fromJson(row); final value = QueuesResponseLegacyDto.fromJson(row);
if (value != null) { if (value != null) {
result.add(value); result.add(value);
} }
@@ -176,12 +176,12 @@ class QueuesResponseDto {
return result.toList(growable: growable); return result.toList(growable: growable);
} }
static Map<String, QueuesResponseDto> mapFromJson(dynamic json) { static Map<String, QueuesResponseLegacyDto> mapFromJson(dynamic json) {
final map = <String, QueuesResponseDto>{}; final map = <String, QueuesResponseLegacyDto>{};
if (json is Map && json.isNotEmpty) { if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) { for (final entry in json.entries) {
final value = QueuesResponseDto.fromJson(entry.value); final value = QueuesResponseLegacyDto.fromJson(entry.value);
if (value != null) { if (value != null) {
map[entry.key] = value; map[entry.key] = value;
} }
@@ -190,14 +190,14 @@ class QueuesResponseDto {
return map; return map;
} }
// maps a json object with a list of QueuesResponseDto-objects as value to a dart map // maps a json object with a list of QueuesResponseLegacyDto-objects as value to a dart map
static Map<String, List<QueuesResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) { static Map<String, List<QueuesResponseLegacyDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<QueuesResponseDto>>{}; final map = <String, List<QueuesResponseLegacyDto>>{};
if (json is Map && json.isNotEmpty) { if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments // ignore: parameter_assignments
json = json.cast<String, dynamic>(); json = json.cast<String, dynamic>();
for (final entry in json.entries) { for (final entry in json.entries) {
map[entry.key] = QueuesResponseDto.listFromJson(entry.value, growable: growable,); map[entry.key] = QueuesResponseLegacyDto.listFromJson(entry.value, growable: growable,);
} }
} }
return map; return map;
@@ -0,0 +1,165 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/domain/services/asset.service.dart';
import 'package:mocktail/mocktail.dart';
import '../../infrastructure/repository.mock.dart';
import '../../test_utils.dart';
void main() {
late AssetService sut;
late MockRemoteAssetRepository mockRemoteAssetRepository;
late MockDriftLocalAssetRepository mockLocalAssetRepository;
setUp(() {
mockRemoteAssetRepository = MockRemoteAssetRepository();
mockLocalAssetRepository = MockDriftLocalAssetRepository();
sut = AssetService(
remoteAssetRepository: mockRemoteAssetRepository,
localAssetRepository: mockLocalAssetRepository,
);
});
group('getAspectRatio', () {
test('flips dimensions on Android for 90° and 270° orientations', () async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
for (final orientation in [90, 270]) {
final localAsset = TestUtils.createLocalAsset(
id: 'local-$orientation',
width: 1920,
height: 1080,
orientation: orientation,
);
final result = await sut.getAspectRatio(localAsset);
expect(result, 1080 / 1920, reason: 'Orientation $orientation should flip on Android');
}
});
test('does not flip dimensions on iOS regardless of orientation', () async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
for (final orientation in [0, 90, 270]) {
final localAsset = TestUtils.createLocalAsset(
id: 'local-$orientation',
width: 1920,
height: 1080,
orientation: orientation,
);
final result = await sut.getAspectRatio(localAsset);
expect(result, 1920 / 1080, reason: 'iOS should never flip dimensions');
}
});
test('fetches dimensions from remote repository when missing from asset', () async {
final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null);
final exif = const ExifInfo(orientation: '1');
final fetchedAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: 1920, height: 1080);
when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif);
when(() => mockRemoteAssetRepository.get('remote-1')).thenAnswer((_) async => fetchedAsset);
final result = await sut.getAspectRatio(remoteAsset);
expect(result, 1920 / 1080);
verify(() => mockRemoteAssetRepository.get('remote-1')).called(1);
});
test('fetches dimensions from local repository when missing from local asset', () async {
final localAsset = TestUtils.createLocalAsset(id: 'local-1', width: null, height: null, orientation: 0);
final fetchedAsset = TestUtils.createLocalAsset(id: 'local-1', width: 1920, height: 1080, orientation: 0);
when(() => mockLocalAssetRepository.get('local-1')).thenAnswer((_) async => fetchedAsset);
final result = await sut.getAspectRatio(localAsset);
expect(result, 1920 / 1080);
verify(() => mockLocalAssetRepository.get('local-1')).called(1);
});
test('returns 1.0 when dimensions are still unavailable after fetching', () async {
final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null);
final exif = const ExifInfo(orientation: '1');
when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif);
when(() => mockRemoteAssetRepository.get('remote-1')).thenAnswer((_) async => null);
final result = await sut.getAspectRatio(remoteAsset);
expect(result, 1.0);
});
test('returns 1.0 when height is zero', () async {
final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: 1920, height: 0);
final exif = const ExifInfo(orientation: '1');
when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif);
final result = await sut.getAspectRatio(remoteAsset);
expect(result, 1.0);
});
test('handles local asset with remoteId and uses exif from remote', () async {
final localAsset = TestUtils.createLocalAsset(
id: 'local-1',
remoteId: 'remote-1',
width: 1920,
height: 1080,
orientation: 0,
);
final exif = const ExifInfo(orientation: '6');
when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif);
final result = await sut.getAspectRatio(localAsset);
expect(result, 1080 / 1920);
});
test('handles various flipped EXIF orientations correctly', () async {
final flippedOrientations = ['5', '6', '7', '8', '90', '-90'];
for (final orientation in flippedOrientations) {
final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-$orientation', width: 1920, height: 1080);
final exif = ExifInfo(orientation: orientation);
when(() => mockRemoteAssetRepository.getExif('remote-$orientation')).thenAnswer((_) async => exif);
final result = await sut.getAspectRatio(remoteAsset);
expect(result, 1080 / 1920, reason: 'Orientation $orientation should flip dimensions');
}
});
test('handles various non-flipped EXIF orientations correctly', () async {
final nonFlippedOrientations = ['1', '2', '3', '4'];
for (final orientation in nonFlippedOrientations) {
final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-$orientation', width: 1920, height: 1080);
final exif = ExifInfo(orientation: orientation);
when(() => mockRemoteAssetRepository.getExif('remote-$orientation')).thenAnswer((_) async => exif);
final result = await sut.getAspectRatio(remoteAsset);
expect(result, 1920 / 1080, reason: 'Orientation $orientation should NOT flip dimensions');
}
});
});
}
@@ -54,12 +54,7 @@ void main() {
when(() => mockNativeSyncApi.shouldFullSync()).thenAnswer((_) async => false); when(() => mockNativeSyncApi.shouldFullSync()).thenAnswer((_) async => false);
when(() => mockNativeSyncApi.getMediaChanges()).thenAnswer( when(() => mockNativeSyncApi.getMediaChanges()).thenAnswer(
(_) async => SyncDelta( (_) async => SyncDelta(hasChanges: false, updates: const [], deletes: const [], assetAlbums: const {}),
hasChanges: false,
updates: const [],
deletes: const [],
assetAlbums: const {},
),
); );
when(() => mockNativeSyncApi.getTrashedAssets()).thenAnswer((_) async => {}); when(() => mockNativeSyncApi.getTrashedAssets()).thenAnswer((_) async => {});
when(() => mockTrashedLocalAssetRepository.processTrashSnapshot(any())).thenAnswer((_) async {}); when(() => mockTrashedLocalAssetRepository.processTrashSnapshot(any())).thenAnswer((_) async {});
@@ -144,13 +139,19 @@ void main() {
}); });
final localAssetToTrash = LocalAssetStub.image2.copyWith(id: 'local-trash', checksum: 'checksum-trash'); final localAssetToTrash = LocalAssetStub.image2.copyWith(id: 'local-trash', checksum: 'checksum-trash');
when(() => mockTrashedLocalAssetRepository.getToTrash()).thenAnswer((_) async => {'album-a': [localAssetToTrash]}); when(() => mockTrashedLocalAssetRepository.getToTrash()).thenAnswer(
(_) async => {
'album-a': [localAssetToTrash],
},
);
final assetEntity = MockAssetEntity(); final assetEntity = MockAssetEntity();
when(() => assetEntity.getMediaUrl()).thenAnswer((_) async => 'content://local-trash'); when(() => assetEntity.getMediaUrl()).thenAnswer((_) async => 'content://local-trash');
when(() => mockStorageRepository.getAssetEntityForAsset(localAssetToTrash)).thenAnswer((_) async => assetEntity); when(() => mockStorageRepository.getAssetEntityForAsset(localAssetToTrash)).thenAnswer((_) async => assetEntity);
await sut.processTrashedAssets({'album-a': [platformAsset]}); await sut.processTrashedAssets({
'album-a': [platformAsset],
});
verify(() => mockTrashedLocalAssetRepository.processTrashSnapshot(any())).called(1); verify(() => mockTrashedLocalAssetRepository.processTrashSnapshot(any())).called(1);
verify(() => mockTrashedLocalAssetRepository.getToTrash()).called(1); verify(() => mockTrashedLocalAssetRepository.getToTrash()).called(1);
@@ -159,8 +160,7 @@ void main() {
verify(() => mockTrashedLocalAssetRepository.applyRestoredAssets(restoredIds)).called(1); verify(() => mockTrashedLocalAssetRepository.applyRestoredAssets(restoredIds)).called(1);
verify(() => mockStorageRepository.getAssetEntityForAsset(localAssetToTrash)).called(1); verify(() => mockStorageRepository.getAssetEntityForAsset(localAssetToTrash)).called(1);
final moveArgs = final moveArgs = verify(() => mockLocalFilesManager.moveToTrash(captureAny())).captured.single as List<String>;
verify(() => mockLocalFilesManager.moveToTrash(captureAny())).captured.single as List<String>;
expect(moveArgs, ['content://local-trash']); expect(moveArgs, ['content://local-trash']);
final trashArgs = final trashArgs =
verify(() => mockTrashedLocalAssetRepository.trashLocalAsset(captureAny())).captured.single verify(() => mockTrashedLocalAssetRepository.trashLocalAsset(captureAny())).captured.single
@@ -187,4 +187,25 @@ void main() {
verifyNever(() => mockTrashedLocalAssetRepository.trashLocalAsset(any())); verifyNever(() => mockTrashedLocalAssetRepository.trashLocalAsset(any()));
}); });
}); });
group('LocalSyncService - PlatformAsset conversion', () {
test('toLocalAsset uses correct updatedAt timestamp', () {
final platformAsset = PlatformAsset(
id: 'test-id',
name: 'test.jpg',
type: AssetType.image.index,
durationInSeconds: 0,
orientation: 0,
isFavorite: false,
createdAt: 1700000000,
updatedAt: 1732000000,
);
final localAsset = platformAsset.toLocalAsset();
expect(localAsset.createdAt.millisecondsSinceEpoch ~/ 1000, 1700000000);
expect(localAsset.updatedAt.millisecondsSinceEpoch ~/ 1000, 1732000000);
expect(localAsset.updatedAt, isNot(localAsset.createdAt));
});
});
} }
@@ -4,6 +4,7 @@ import 'package:immich_mobile/infrastructure/repositories/local_album.repository
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
@@ -35,6 +36,8 @@ class MockLocalAssetRepository extends Mock implements DriftLocalAssetRepository
class MockDriftLocalAssetRepository extends Mock implements DriftLocalAssetRepository {} class MockDriftLocalAssetRepository extends Mock implements DriftLocalAssetRepository {}
class MockRemoteAssetRepository extends Mock implements RemoteAssetRepository {}
class MockTrashedLocalAssetRepository extends Mock implements DriftTrashedLocalAssetRepository {} class MockTrashedLocalAssetRepository extends Mock implements DriftTrashedLocalAssetRepository {}
class MockStorageRepository extends Mock implements StorageRepository {} class MockStorageRepository extends Mock implements StorageRepository {}
+40
View File
@@ -5,6 +5,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:fake_async/fake_async.dart'; import 'package:fake_async/fake_async.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as domain;
import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/android_device_asset.entity.dart'; import 'package:immich_mobile/entities/android_device_asset.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
@@ -116,4 +117,43 @@ abstract final class TestUtils {
} }
return result; return result;
} }
static domain.RemoteAsset createRemoteAsset({required String id, int? width, int? height, String? ownerId}) {
return domain.RemoteAsset(
id: id,
checksum: 'checksum1',
ownerId: ownerId ?? 'owner1',
name: 'test.jpg',
type: domain.AssetType.image,
createdAt: DateTime(2024, 1, 1),
updatedAt: DateTime(2024, 1, 1),
durationInSeconds: 0,
isFavorite: false,
width: width,
height: height,
);
}
static domain.LocalAsset createLocalAsset({
required String id,
String? remoteId,
int? width,
int? height,
int orientation = 0,
}) {
return domain.LocalAsset(
id: id,
remoteId: remoteId,
checksum: 'checksum1',
name: 'test.jpg',
type: domain.AssetType.image,
createdAt: DateTime(2024, 1, 1),
updatedAt: DateTime(2024, 1, 1),
durationInSeconds: 0,
isFavorite: false,
width: width,
height: height,
orientation: orientation,
);
}
} }
+493 -26
View File
@@ -4929,6 +4929,7 @@
}, },
"/jobs": { "/jobs": {
"get": { "get": {
"deprecated": true,
"description": "Retrieve the counts of the current queue, as well as the current status.", "description": "Retrieve the counts of the current queue, as well as the current status.",
"operationId": "getQueuesLegacy", "operationId": "getQueuesLegacy",
"parameters": [], "parameters": [],
@@ -4937,7 +4938,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/QueuesResponseDto" "$ref": "#/components/schemas/QueuesResponseLegacyDto"
} }
} }
}, },
@@ -4957,7 +4958,8 @@
], ],
"summary": "Retrieve queue counts and status", "summary": "Retrieve queue counts and status",
"tags": [ "tags": [
"Jobs" "Jobs",
"Deprecated"
], ],
"x-immich-admin-only": true, "x-immich-admin-only": true,
"x-immich-history": [ "x-immich-history": [
@@ -4972,10 +4974,14 @@
{ {
"version": "v2", "version": "v2",
"state": "Stable" "state": "Stable"
},
{
"version": "v2.4.0",
"state": "Deprecated"
} }
], ],
"x-immich-permission": "job.read", "x-immich-permission": "job.read",
"x-immich-state": "Stable" "x-immich-state": "Deprecated"
}, },
"post": { "post": {
"description": "Run a specific job. Most jobs are queued automatically, but this endpoint allows for manual creation of a handful of jobs, including various cleanup tasks, as well as creating a new database backup.", "description": "Run a specific job. Most jobs are queued automatically, but this endpoint allows for manual creation of a handful of jobs, including various cleanup tasks, as well as creating a new database backup.",
@@ -5032,6 +5038,7 @@
}, },
"/jobs/{name}": { "/jobs/{name}": {
"put": { "put": {
"deprecated": true,
"description": "Queue all assets for a specific job type. Defaults to only queueing assets that have not yet been processed, but the force command can be used to re-process all assets.", "description": "Queue all assets for a specific job type. Defaults to only queueing assets that have not yet been processed, but the force command can be used to re-process all assets.",
"operationId": "runQueueCommandLegacy", "operationId": "runQueueCommandLegacy",
"parameters": [ "parameters": [
@@ -5059,7 +5066,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/QueueResponseDto" "$ref": "#/components/schemas/QueueResponseLegacyDto"
} }
} }
}, },
@@ -5079,7 +5086,8 @@
], ],
"summary": "Run jobs", "summary": "Run jobs",
"tags": [ "tags": [
"Jobs" "Jobs",
"Deprecated"
], ],
"x-immich-admin-only": true, "x-immich-admin-only": true,
"x-immich-history": [ "x-immich-history": [
@@ -5094,10 +5102,14 @@
{ {
"version": "v2", "version": "v2",
"state": "Stable" "state": "Stable"
},
{
"version": "v2.4.0",
"state": "Deprecated"
} }
], ],
"x-immich-permission": "job.create", "x-immich-permission": "job.create",
"x-immich-state": "Stable" "x-immich-state": "Deprecated"
} }
}, },
"/libraries": { "/libraries": {
@@ -8064,6 +8076,303 @@
"x-immich-state": "Alpha" "x-immich-state": "Alpha"
} }
}, },
"/queues": {
"get": {
"description": "Retrieves a list of queues.",
"operationId": "getQueues",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/QueueResponseDto"
},
"type": "array"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "List all queues",
"tags": [
"Queues"
],
"x-immich-admin-only": true,
"x-immich-history": [
{
"version": "v2.4.0",
"state": "Added"
},
{
"version": "v2.4.0",
"state": "Alpha"
}
],
"x-immich-permission": "queue.read",
"x-immich-state": "Alpha"
}
},
"/queues/{name}": {
"get": {
"description": "Retrieves a specific queue by its name.",
"operationId": "getQueue",
"parameters": [
{
"name": "name",
"required": true,
"in": "path",
"schema": {
"$ref": "#/components/schemas/QueueName"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/QueueResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "Retrieve a queue",
"tags": [
"Queues"
],
"x-immich-admin-only": true,
"x-immich-history": [
{
"version": "v2.4.0",
"state": "Added"
},
{
"version": "v2.4.0",
"state": "Alpha"
}
],
"x-immich-permission": "queue.read",
"x-immich-state": "Alpha"
},
"put": {
"description": "Change the paused status of a specific queue.",
"operationId": "updateQueue",
"parameters": [
{
"name": "name",
"required": true,
"in": "path",
"schema": {
"$ref": "#/components/schemas/QueueName"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/QueueUpdateDto"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/QueueResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "Update a queue",
"tags": [
"Queues"
],
"x-immich-admin-only": true,
"x-immich-history": [
{
"version": "v2.4.0",
"state": "Added"
},
{
"version": "v2.4.0",
"state": "Alpha"
}
],
"x-immich-permission": "queue.update",
"x-immich-state": "Alpha"
}
},
"/queues/{name}/jobs": {
"delete": {
"description": "Removes all jobs from the specified queue.",
"operationId": "emptyQueue",
"parameters": [
{
"name": "name",
"required": true,
"in": "path",
"schema": {
"$ref": "#/components/schemas/QueueName"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/QueueDeleteDto"
}
}
},
"required": true
},
"responses": {
"204": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "Empty a queue",
"tags": [
"Queues"
],
"x-immich-admin-only": true,
"x-immich-history": [
{
"version": "v2.4.0",
"state": "Added"
},
{
"version": "v2.4.0",
"state": "Alpha"
}
],
"x-immich-permission": "queueJob.delete",
"x-immich-state": "Alpha"
},
"get": {
"description": "Retrieves a list of queue jobs from the specified queue.",
"operationId": "getQueueJobs",
"parameters": [
{
"name": "name",
"required": true,
"in": "path",
"schema": {
"$ref": "#/components/schemas/QueueName"
}
},
{
"name": "status",
"required": false,
"in": "query",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/QueueJobStatus"
}
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/QueueJobResponseDto"
},
"type": "array"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "Retrieve queue jobs",
"tags": [
"Queues"
],
"x-immich-admin-only": true,
"x-immich-history": [
{
"version": "v2.4.0",
"state": "Added"
},
{
"version": "v2.4.0",
"state": "Alpha"
}
],
"x-immich-permission": "queueJob.read",
"x-immich-state": "Alpha"
}
},
"/search/cities": { "/search/cities": {
"get": { "get": {
"description": "Retrieve a list of assets with each asset belonging to a different city. This endpoint is used on the places pages to show a single thumbnail for each city the user has assets in.", "description": "Retrieve a list of assets with each asset belonging to a different city. This endpoint is used on the places pages to show a single thumbnail for each city the user has assets in.",
@@ -14043,6 +14352,10 @@
"name": "Plugins", "name": "Plugins",
"description": "A plugin is an installed module that makes filters and actions available for the workflow feature." "description": "A plugin is an installed module that makes filters and actions available for the workflow feature."
}, },
{
"name": "Queues",
"description": "Queues and background jobs are used for processing tasks asynchronously. Queues can be paused and resumed as needed."
},
{ {
"name": "Search", "name": "Search",
"description": "Endpoints related to searching assets via text, smart search, optical character recognition (OCR), and other filters like person, album, and other metadata. Search endpoints usually support pagination and sorting." "description": "Endpoints related to searching assets via text, smart search, optical character recognition (OCR), and other filters like person, album, and other metadata. Search endpoints usually support pagination and sorting."
@@ -16291,6 +16604,66 @@
], ],
"type": "object" "type": "object"
}, },
"JobName": {
"enum": [
"AssetDelete",
"AssetDeleteCheck",
"AssetDetectFacesQueueAll",
"AssetDetectFaces",
"AssetDetectDuplicatesQueueAll",
"AssetDetectDuplicates",
"AssetEncodeVideoQueueAll",
"AssetEncodeVideo",
"AssetEmptyTrash",
"AssetExtractMetadataQueueAll",
"AssetExtractMetadata",
"AssetFileMigration",
"AssetGenerateThumbnailsQueueAll",
"AssetGenerateThumbnails",
"AuditLogCleanup",
"AuditTableCleanup",
"DatabaseBackup",
"FacialRecognitionQueueAll",
"FacialRecognition",
"FileDelete",
"FileMigrationQueueAll",
"LibraryDeleteCheck",
"LibraryDelete",
"LibraryRemoveAsset",
"LibraryScanAssetsQueueAll",
"LibrarySyncAssets",
"LibrarySyncFilesQueueAll",
"LibrarySyncFiles",
"LibraryScanQueueAll",
"MemoryCleanup",
"MemoryGenerate",
"NotificationsCleanup",
"NotifyUserSignup",
"NotifyAlbumInvite",
"NotifyAlbumUpdate",
"UserDelete",
"UserDeleteCheck",
"UserSyncUsage",
"PersonCleanup",
"PersonFileMigration",
"PersonGenerateThumbnail",
"SessionCleanup",
"SendMail",
"SidecarQueueAll",
"SidecarCheck",
"SidecarWrite",
"SmartSearchQueueAll",
"SmartSearch",
"StorageTemplateMigration",
"StorageTemplateMigrationSingle",
"TagCleanup",
"VersionCheck",
"OcrQueueAll",
"Ocr",
"WorkflowRun"
],
"type": "string"
},
"JobSettingsDto": { "JobSettingsDto": {
"properties": { "properties": {
"concurrency": { "concurrency": {
@@ -17583,6 +17956,12 @@
"userProfileImage.read", "userProfileImage.read",
"userProfileImage.update", "userProfileImage.update",
"userProfileImage.delete", "userProfileImage.delete",
"queue.read",
"queue.update",
"queueJob.create",
"queueJob.read",
"queueJob.update",
"queueJob.delete",
"workflow.create", "workflow.create",
"workflow.read", "workflow.read",
"workflow.update", "workflow.update",
@@ -18083,6 +18462,63 @@
], ],
"type": "object" "type": "object"
}, },
"QueueDeleteDto": {
"properties": {
"failed": {
"description": "If true, will also remove failed jobs from the queue.",
"type": "boolean",
"x-immich-history": [
{
"version": "v2.4.0",
"state": "Added"
},
{
"version": "v2.4.0",
"state": "Alpha"
}
],
"x-immich-state": "Alpha"
}
},
"type": "object"
},
"QueueJobResponseDto": {
"properties": {
"data": {
"type": "object"
},
"id": {
"type": "string"
},
"name": {
"allOf": [
{
"$ref": "#/components/schemas/JobName"
}
]
},
"timestamp": {
"type": "integer"
}
},
"required": [
"data",
"name",
"timestamp"
],
"type": "object"
},
"QueueJobStatus": {
"enum": [
"active",
"failed",
"completed",
"delayed",
"waiting",
"paused"
],
"type": "string"
},
"QueueName": { "QueueName": {
"enum": [ "enum": [
"thumbnailGeneration", "thumbnailGeneration",
@@ -18106,12 +18542,35 @@
"type": "string" "type": "string"
}, },
"QueueResponseDto": { "QueueResponseDto": {
"properties": {
"isPaused": {
"type": "boolean"
},
"name": {
"allOf": [
{
"$ref": "#/components/schemas/QueueName"
}
]
},
"statistics": {
"$ref": "#/components/schemas/QueueStatisticsDto"
}
},
"required": [
"isPaused",
"name",
"statistics"
],
"type": "object"
},
"QueueResponseLegacyDto": {
"properties": { "properties": {
"jobCounts": { "jobCounts": {
"$ref": "#/components/schemas/QueueStatisticsDto" "$ref": "#/components/schemas/QueueStatisticsDto"
}, },
"queueStatus": { "queueStatus": {
"$ref": "#/components/schemas/QueueStatusDto" "$ref": "#/components/schemas/QueueStatusLegacyDto"
} }
}, },
"required": [ "required": [
@@ -18151,7 +18610,7 @@
], ],
"type": "object" "type": "object"
}, },
"QueueStatusDto": { "QueueStatusLegacyDto": {
"properties": { "properties": {
"isActive": { "isActive": {
"type": "boolean" "type": "boolean"
@@ -18166,58 +18625,66 @@
], ],
"type": "object" "type": "object"
}, },
"QueuesResponseDto": { "QueueUpdateDto": {
"properties": {
"isPaused": {
"type": "boolean"
}
},
"type": "object"
},
"QueuesResponseLegacyDto": {
"properties": { "properties": {
"backgroundTask": { "backgroundTask": {
"$ref": "#/components/schemas/QueueResponseDto" "$ref": "#/components/schemas/QueueResponseLegacyDto"
}, },
"backupDatabase": { "backupDatabase": {
"$ref": "#/components/schemas/QueueResponseDto" "$ref": "#/components/schemas/QueueResponseLegacyDto"
}, },
"duplicateDetection": { "duplicateDetection": {
"$ref": "#/components/schemas/QueueResponseDto" "$ref": "#/components/schemas/QueueResponseLegacyDto"
}, },
"faceDetection": { "faceDetection": {
"$ref": "#/components/schemas/QueueResponseDto" "$ref": "#/components/schemas/QueueResponseLegacyDto"
}, },
"facialRecognition": { "facialRecognition": {
"$ref": "#/components/schemas/QueueResponseDto" "$ref": "#/components/schemas/QueueResponseLegacyDto"
}, },
"library": { "library": {
"$ref": "#/components/schemas/QueueResponseDto" "$ref": "#/components/schemas/QueueResponseLegacyDto"
}, },
"metadataExtraction": { "metadataExtraction": {
"$ref": "#/components/schemas/QueueResponseDto" "$ref": "#/components/schemas/QueueResponseLegacyDto"
}, },
"migration": { "migration": {
"$ref": "#/components/schemas/QueueResponseDto" "$ref": "#/components/schemas/QueueResponseLegacyDto"
}, },
"notifications": { "notifications": {
"$ref": "#/components/schemas/QueueResponseDto" "$ref": "#/components/schemas/QueueResponseLegacyDto"
}, },
"ocr": { "ocr": {
"$ref": "#/components/schemas/QueueResponseDto" "$ref": "#/components/schemas/QueueResponseLegacyDto"
}, },
"search": { "search": {
"$ref": "#/components/schemas/QueueResponseDto" "$ref": "#/components/schemas/QueueResponseLegacyDto"
}, },
"sidecar": { "sidecar": {
"$ref": "#/components/schemas/QueueResponseDto" "$ref": "#/components/schemas/QueueResponseLegacyDto"
}, },
"smartSearch": { "smartSearch": {
"$ref": "#/components/schemas/QueueResponseDto" "$ref": "#/components/schemas/QueueResponseLegacyDto"
}, },
"storageTemplateMigration": { "storageTemplateMigration": {
"$ref": "#/components/schemas/QueueResponseDto" "$ref": "#/components/schemas/QueueResponseLegacyDto"
}, },
"thumbnailGeneration": { "thumbnailGeneration": {
"$ref": "#/components/schemas/QueueResponseDto" "$ref": "#/components/schemas/QueueResponseLegacyDto"
}, },
"videoConversion": { "videoConversion": {
"$ref": "#/components/schemas/QueueResponseDto" "$ref": "#/components/schemas/QueueResponseLegacyDto"
}, },
"workflow": { "workflow": {
"$ref": "#/components/schemas/QueueResponseDto" "$ref": "#/components/schemas/QueueResponseLegacyDto"
} }
}, },
"required": [ "required": [
+1 -1
View File
@@ -1 +1 @@
24.11.0 24.11.1
+2 -2
View File
@@ -19,7 +19,7 @@
"@oazapfts/runtime": "^1.0.2" "@oazapfts/runtime": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.19.1", "@types/node": "^24.10.1",
"typescript": "^5.3.3" "typescript": "^5.3.3"
}, },
"repository": { "repository": {
@@ -28,6 +28,6 @@
"directory": "open-api/typescript-sdk" "directory": "open-api/typescript-sdk"
}, },
"volta": { "volta": {
"node": "24.11.0" "node": "24.11.1"
} }
} }
+181 -23
View File
@@ -716,32 +716,32 @@ export type QueueStatisticsDto = {
paused: number; paused: number;
waiting: number; waiting: number;
}; };
export type QueueStatusDto = { export type QueueStatusLegacyDto = {
isActive: boolean; isActive: boolean;
isPaused: boolean; isPaused: boolean;
}; };
export type QueueResponseDto = { export type QueueResponseLegacyDto = {
jobCounts: QueueStatisticsDto; jobCounts: QueueStatisticsDto;
queueStatus: QueueStatusDto; queueStatus: QueueStatusLegacyDto;
}; };
export type QueuesResponseDto = { export type QueuesResponseLegacyDto = {
backgroundTask: QueueResponseDto; backgroundTask: QueueResponseLegacyDto;
backupDatabase: QueueResponseDto; backupDatabase: QueueResponseLegacyDto;
duplicateDetection: QueueResponseDto; duplicateDetection: QueueResponseLegacyDto;
faceDetection: QueueResponseDto; faceDetection: QueueResponseLegacyDto;
facialRecognition: QueueResponseDto; facialRecognition: QueueResponseLegacyDto;
library: QueueResponseDto; library: QueueResponseLegacyDto;
metadataExtraction: QueueResponseDto; metadataExtraction: QueueResponseLegacyDto;
migration: QueueResponseDto; migration: QueueResponseLegacyDto;
notifications: QueueResponseDto; notifications: QueueResponseLegacyDto;
ocr: QueueResponseDto; ocr: QueueResponseLegacyDto;
search: QueueResponseDto; search: QueueResponseLegacyDto;
sidecar: QueueResponseDto; sidecar: QueueResponseLegacyDto;
smartSearch: QueueResponseDto; smartSearch: QueueResponseLegacyDto;
storageTemplateMigration: QueueResponseDto; storageTemplateMigration: QueueResponseLegacyDto;
thumbnailGeneration: QueueResponseDto; thumbnailGeneration: QueueResponseLegacyDto;
videoConversion: QueueResponseDto; videoConversion: QueueResponseLegacyDto;
workflow: QueueResponseDto; workflow: QueueResponseLegacyDto;
}; };
export type JobCreateDto = { export type JobCreateDto = {
name: ManualJobName; name: ManualJobName;
@@ -966,6 +966,24 @@ export type PluginResponseDto = {
updatedAt: string; updatedAt: string;
version: string; version: string;
}; };
export type QueueResponseDto = {
isPaused: boolean;
name: QueueName;
statistics: QueueStatisticsDto;
};
export type QueueUpdateDto = {
isPaused?: boolean;
};
export type QueueDeleteDto = {
/** If true, will also remove failed jobs from the queue. */
failed?: boolean;
};
export type QueueJobResponseDto = {
data: object;
id?: string;
name: JobName;
timestamp: number;
};
export type SearchExploreItem = { export type SearchExploreItem = {
data: AssetResponseDto; data: AssetResponseDto;
value: string; value: string;
@@ -2925,7 +2943,7 @@ export function reassignFacesById({ id, faceDto }: {
export function getQueuesLegacy(opts?: Oazapfts.RequestOpts) { export function getQueuesLegacy(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
status: 200; status: 200;
data: QueuesResponseDto; data: QueuesResponseLegacyDto;
}>("/jobs", { }>("/jobs", {
...opts ...opts
})); }));
@@ -2951,7 +2969,7 @@ export function runQueueCommandLegacy({ name, queueCommandDto }: {
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
status: 200; status: 200;
data: QueueResponseDto; data: QueueResponseLegacyDto;
}>(`/jobs/${encodeURIComponent(name)}`, oazapfts.json({ }>(`/jobs/${encodeURIComponent(name)}`, oazapfts.json({
...opts, ...opts,
method: "PUT", method: "PUT",
@@ -3651,6 +3669,75 @@ export function getPlugin({ id }: {
...opts ...opts
})); }));
} }
/**
* List all queues
*/
export function getQueues(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: QueueResponseDto[];
}>("/queues", {
...opts
}));
}
/**
* Retrieve a queue
*/
export function getQueue({ name }: {
name: QueueName;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: QueueResponseDto;
}>(`/queues/${encodeURIComponent(name)}`, {
...opts
}));
}
/**
* Update a queue
*/
export function updateQueue({ name, queueUpdateDto }: {
name: QueueName;
queueUpdateDto: QueueUpdateDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: QueueResponseDto;
}>(`/queues/${encodeURIComponent(name)}`, oazapfts.json({
...opts,
method: "PUT",
body: queueUpdateDto
})));
}
/**
* Empty a queue
*/
export function emptyQueue({ name, queueDeleteDto }: {
name: QueueName;
queueDeleteDto: QueueDeleteDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText(`/queues/${encodeURIComponent(name)}/jobs`, oazapfts.json({
...opts,
method: "DELETE",
body: queueDeleteDto
})));
}
/**
* Retrieve queue jobs
*/
export function getQueueJobs({ name, status }: {
name: QueueName;
status?: QueueJobStatus[];
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: QueueJobResponseDto[];
}>(`/queues/${encodeURIComponent(name)}/jobs${QS.query(QS.explode({
status
}))}`, {
...opts
}));
}
/** /**
* Retrieve assets by city * Retrieve assets by city
*/ */
@@ -5241,6 +5328,12 @@ export enum Permission {
UserProfileImageRead = "userProfileImage.read", UserProfileImageRead = "userProfileImage.read",
UserProfileImageUpdate = "userProfileImage.update", UserProfileImageUpdate = "userProfileImage.update",
UserProfileImageDelete = "userProfileImage.delete", UserProfileImageDelete = "userProfileImage.delete",
QueueRead = "queue.read",
QueueUpdate = "queue.update",
QueueJobCreate = "queueJob.create",
QueueJobRead = "queueJob.read",
QueueJobUpdate = "queueJob.update",
QueueJobDelete = "queueJob.delete",
WorkflowCreate = "workflow.create", WorkflowCreate = "workflow.create",
WorkflowRead = "workflow.read", WorkflowRead = "workflow.read",
WorkflowUpdate = "workflow.update", WorkflowUpdate = "workflow.update",
@@ -5330,6 +5423,71 @@ export enum PluginContext {
Album = "album", Album = "album",
Person = "person" Person = "person"
} }
export enum QueueJobStatus {
Active = "active",
Failed = "failed",
Completed = "completed",
Delayed = "delayed",
Waiting = "waiting",
Paused = "paused"
}
export enum JobName {
AssetDelete = "AssetDelete",
AssetDeleteCheck = "AssetDeleteCheck",
AssetDetectFacesQueueAll = "AssetDetectFacesQueueAll",
AssetDetectFaces = "AssetDetectFaces",
AssetDetectDuplicatesQueueAll = "AssetDetectDuplicatesQueueAll",
AssetDetectDuplicates = "AssetDetectDuplicates",
AssetEncodeVideoQueueAll = "AssetEncodeVideoQueueAll",
AssetEncodeVideo = "AssetEncodeVideo",
AssetEmptyTrash = "AssetEmptyTrash",
AssetExtractMetadataQueueAll = "AssetExtractMetadataQueueAll",
AssetExtractMetadata = "AssetExtractMetadata",
AssetFileMigration = "AssetFileMigration",
AssetGenerateThumbnailsQueueAll = "AssetGenerateThumbnailsQueueAll",
AssetGenerateThumbnails = "AssetGenerateThumbnails",
AuditLogCleanup = "AuditLogCleanup",
AuditTableCleanup = "AuditTableCleanup",
DatabaseBackup = "DatabaseBackup",
FacialRecognitionQueueAll = "FacialRecognitionQueueAll",
FacialRecognition = "FacialRecognition",
FileDelete = "FileDelete",
FileMigrationQueueAll = "FileMigrationQueueAll",
LibraryDeleteCheck = "LibraryDeleteCheck",
LibraryDelete = "LibraryDelete",
LibraryRemoveAsset = "LibraryRemoveAsset",
LibraryScanAssetsQueueAll = "LibraryScanAssetsQueueAll",
LibrarySyncAssets = "LibrarySyncAssets",
LibrarySyncFilesQueueAll = "LibrarySyncFilesQueueAll",
LibrarySyncFiles = "LibrarySyncFiles",
LibraryScanQueueAll = "LibraryScanQueueAll",
MemoryCleanup = "MemoryCleanup",
MemoryGenerate = "MemoryGenerate",
NotificationsCleanup = "NotificationsCleanup",
NotifyUserSignup = "NotifyUserSignup",
NotifyAlbumInvite = "NotifyAlbumInvite",
NotifyAlbumUpdate = "NotifyAlbumUpdate",
UserDelete = "UserDelete",
UserDeleteCheck = "UserDeleteCheck",
UserSyncUsage = "UserSyncUsage",
PersonCleanup = "PersonCleanup",
PersonFileMigration = "PersonFileMigration",
PersonGenerateThumbnail = "PersonGenerateThumbnail",
SessionCleanup = "SessionCleanup",
SendMail = "SendMail",
SidecarQueueAll = "SidecarQueueAll",
SidecarCheck = "SidecarCheck",
SidecarWrite = "SidecarWrite",
SmartSearchQueueAll = "SmartSearchQueueAll",
SmartSearch = "SmartSearch",
StorageTemplateMigration = "StorageTemplateMigration",
StorageTemplateMigrationSingle = "StorageTemplateMigrationSingle",
TagCleanup = "TagCleanup",
VersionCheck = "VersionCheck",
OcrQueueAll = "OcrQueueAll",
Ocr = "Ocr",
WorkflowRun = "WorkflowRun"
}
export enum SearchSuggestionType { export enum SearchSuggestionType {
Country = "country", Country = "country",
State = "state", State = "state",
+1 -1
View File
@@ -3,7 +3,7 @@
"version": "0.0.1", "version": "0.0.1",
"description": "Monorepo for Immich", "description": "Monorepo for Immich",
"private": true, "private": true,
"packageManager": "pnpm@10.20.0+sha512.cf9998222162dd85864d0a8102e7892e7ba4ceadebbf5a31f9c2fce48dfce317a9c53b9f6464d1ef9042cba2e02ae02a9f7c143a2b438cd93c91840f0192b9dd", "packageManager": "pnpm@10.22.0+sha512.bf049efe995b28f527fd2b41ae0474ce29186f7edcb3bf545087bd61fbbebb2bf75362d1307fda09c2d288e1e499787ac12d4fcb617a974718a6051f2eee741c",
"engines": { "engines": {
"pnpm": ">=10.0.0" "pnpm": ">=10.0.0"
} }
+224 -134
View File
@@ -1,436 +1,519 @@
{ {
"name": "js-pdk-template", "name": "plugins",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "js-pdk-template", "name": "plugins",
"version": "1.0.0", "version": "1.0.0",
"license": "BSD-3-Clause", "license": "AGPL-3.0",
"devDependencies": { "devDependencies": {
"@extism/js-pdk": "^1.0.1", "@extism/js-pdk": "^1.0.1",
"esbuild": "^0.19.6", "esbuild": "^0.27.0",
"typescript": "^5.3.2" "typescript": "^5.3.2"
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.19.12", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz",
"integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"aix" "aix"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.19.12", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz",
"integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"android" "android"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.19.12", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz",
"integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"android" "android"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.19.12", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz",
"integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"android" "android"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.19.12", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz",
"integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.19.12", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz",
"integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.19.12", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz",
"integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"freebsd" "freebsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.19.12", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz",
"integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"freebsd" "freebsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.19.12", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz",
"integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.19.12", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz",
"integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.19.12", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz",
"integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.19.12", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz",
"integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.19.12", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz",
"integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.19.12", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz",
"integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.19.12", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz",
"integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.19.12", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz",
"integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.19.12", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz",
"integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-arm64": {
"version": "0.19.12", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz",
"integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==",
"cpu": [ "cpu": [
"x64" "arm64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"netbsd" "netbsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.19.12", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz",
"integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz",
"integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"openbsd" "openbsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.19.12", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz",
"integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz",
"integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz",
"integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"sunos" "sunos"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.19.12", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz",
"integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.19.12", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz",
"integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.19.12", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz",
"integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@extism/js-pdk": { "node_modules/@extism/js-pdk": {
"version": "1.0.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@extism/js-pdk/-/js-pdk-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@extism/js-pdk/-/js-pdk-1.1.1.tgz",
"integrity": "sha512-YJWfHGeOuJnQw4V8NPNHvbSr6S8iDd2Ga6VEukwlRP7tu62ozTxIgokYw8i+rajD/16zz/gK0KYARBpm2qPAmQ==", "integrity": "sha512-VZLn/dX0ttA1uKk2PZeR/FL3N+nA1S5Vc7E5gdjkR60LuUIwCZT9cYON245V4HowHlBA7YOegh0TLjkx+wNbrA==",
"dev": true "dev": true,
"license": "BSD-Clause-3",
"dependencies": {
"urlpattern-polyfill": "^8.0.2"
}
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.19.12", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz",
"integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT",
"bin": { "bin": {
"esbuild": "bin/esbuild" "esbuild": "bin/esbuild"
}, },
"engines": { "engines": {
"node": ">=12" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.19.12", "@esbuild/aix-ppc64": "0.27.0",
"@esbuild/android-arm": "0.19.12", "@esbuild/android-arm": "0.27.0",
"@esbuild/android-arm64": "0.19.12", "@esbuild/android-arm64": "0.27.0",
"@esbuild/android-x64": "0.19.12", "@esbuild/android-x64": "0.27.0",
"@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-arm64": "0.27.0",
"@esbuild/darwin-x64": "0.19.12", "@esbuild/darwin-x64": "0.27.0",
"@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-arm64": "0.27.0",
"@esbuild/freebsd-x64": "0.19.12", "@esbuild/freebsd-x64": "0.27.0",
"@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm": "0.27.0",
"@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-arm64": "0.27.0",
"@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-ia32": "0.27.0",
"@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-loong64": "0.27.0",
"@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-mips64el": "0.27.0",
"@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-ppc64": "0.27.0",
"@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-riscv64": "0.27.0",
"@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-s390x": "0.27.0",
"@esbuild/linux-x64": "0.19.12", "@esbuild/linux-x64": "0.27.0",
"@esbuild/netbsd-x64": "0.19.12", "@esbuild/netbsd-arm64": "0.27.0",
"@esbuild/openbsd-x64": "0.19.12", "@esbuild/netbsd-x64": "0.27.0",
"@esbuild/sunos-x64": "0.19.12", "@esbuild/openbsd-arm64": "0.27.0",
"@esbuild/win32-arm64": "0.19.12", "@esbuild/openbsd-x64": "0.27.0",
"@esbuild/win32-ia32": "0.19.12", "@esbuild/openharmony-arm64": "0.27.0",
"@esbuild/win32-x64": "0.19.12" "@esbuild/sunos-x64": "0.27.0",
"@esbuild/win32-arm64": "0.27.0",
"@esbuild/win32-ia32": "0.27.0",
"@esbuild/win32-x64": "0.27.0"
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.4.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -438,6 +521,13 @@
"engines": { "engines": {
"node": ">=14.17" "node": ">=14.17"
} }
},
"node_modules/urlpattern-polyfill": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-8.0.2.tgz",
"integrity": "sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==",
"dev": true,
"license": "MIT"
} }
} }
} }
+1 -1
View File
@@ -13,7 +13,7 @@
"license": "AGPL-3.0", "license": "AGPL-3.0",
"devDependencies": { "devDependencies": {
"@extism/js-pdk": "^1.0.1", "@extism/js-pdk": "^1.0.1",
"esbuild": "^0.19.6", "esbuild": "^0.27.0",
"typescript": "^5.3.2" "typescript": "^5.3.2"
} }
} }
+1960 -1784
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
24.11.0 24.11.1
+3 -3
View File
@@ -120,7 +120,7 @@
"@nestjs/schematics": "^11.0.0", "@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.4", "@nestjs/testing": "^11.0.4",
"@swc/core": "^1.4.14", "@swc/core": "^1.4.14",
"@types/archiver": "^6.0.0", "@types/archiver": "^7.0.0",
"@types/async-lock": "^1.4.2", "@types/async-lock": "^1.4.2",
"@types/bcrypt": "^6.0.0", "@types/bcrypt": "^6.0.0",
"@types/body-parser": "^1.19.6", "@types/body-parser": "^1.19.6",
@@ -134,7 +134,7 @@
"@types/luxon": "^3.6.2", "@types/luxon": "^3.6.2",
"@types/mock-fs": "^4.13.1", "@types/mock-fs": "^4.13.1",
"@types/multer": "^2.0.0", "@types/multer": "^2.0.0",
"@types/node": "^22.19.1", "@types/node": "^24.10.1",
"@types/nodemailer": "^7.0.0", "@types/nodemailer": "^7.0.0",
"@types/picomatch": "^4.0.0", "@types/picomatch": "^4.0.0",
"@types/pngjs": "^6.0.5", "@types/pngjs": "^6.0.5",
@@ -166,7 +166,7 @@
"vitest": "^3.0.0" "vitest": "^3.0.0"
}, },
"volta": { "volta": {
"node": "24.11.0" "node": "24.11.1"
}, },
"overrides": { "overrides": {
"sharp": "^0.34.4" "sharp": "^0.34.4"
+2
View File
@@ -163,6 +163,8 @@ export const endpointTags: Record<ApiTag, string> = {
'A person is a collection of faces, which can be favorited and named. A person can also be merged into another person. People are automatically created via the face recognition job.', 'A person is a collection of faces, which can be favorited and named. A person can also be merged into another person. People are automatically created via the face recognition job.',
[ApiTag.Plugins]: [ApiTag.Plugins]:
'A plugin is an installed module that makes filters and actions available for the workflow feature.', 'A plugin is an installed module that makes filters and actions available for the workflow feature.',
[ApiTag.Queues]:
'Queues and background jobs are used for processing tasks asynchronously. Queues can be paused and resumed as needed.',
[ApiTag.Search]: [ApiTag.Search]:
'Endpoints related to searching assets via text, smart search, optical character recognition (OCR), and other filters like person, album, and other metadata. Search endpoints usually support pagination and sorting.', 'Endpoints related to searching assets via text, smart search, optical character recognition (OCR), and other filters like person, album, and other metadata. Search endpoints usually support pagination and sorting.',
[ApiTag.Server]: [ApiTag.Server]:
+2
View File
@@ -20,6 +20,7 @@ import { OAuthController } from 'src/controllers/oauth.controller';
import { PartnerController } from 'src/controllers/partner.controller'; import { PartnerController } from 'src/controllers/partner.controller';
import { PersonController } from 'src/controllers/person.controller'; import { PersonController } from 'src/controllers/person.controller';
import { PluginController } from 'src/controllers/plugin.controller'; import { PluginController } from 'src/controllers/plugin.controller';
import { QueueController } from 'src/controllers/queue.controller';
import { SearchController } from 'src/controllers/search.controller'; import { SearchController } from 'src/controllers/search.controller';
import { ServerController } from 'src/controllers/server.controller'; import { ServerController } from 'src/controllers/server.controller';
import { SessionController } from 'src/controllers/session.controller'; import { SessionController } from 'src/controllers/session.controller';
@@ -59,6 +60,7 @@ export const controllers = [
PartnerController, PartnerController,
PersonController, PersonController,
PluginController, PluginController,
QueueController,
SearchController, SearchController,
ServerController, ServerController,
SessionController, SessionController,
+13 -8
View File
@@ -1,10 +1,12 @@
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common'; import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { Endpoint, HistoryBuilder } from 'src/decorators'; import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto';
import { JobCreateDto } from 'src/dtos/job.dto'; import { JobCreateDto } from 'src/dtos/job.dto';
import { QueueCommandDto, QueueNameParamDto, QueueResponseDto, QueuesResponseDto } from 'src/dtos/queue.dto'; import { QueueResponseLegacyDto, QueuesResponseLegacyDto } from 'src/dtos/queue-legacy.dto';
import { QueueCommandDto, QueueNameParamDto } from 'src/dtos/queue.dto';
import { ApiTag, Permission } from 'src/enum'; import { ApiTag, Permission } from 'src/enum';
import { Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { JobService } from 'src/services/job.service'; import { JobService } from 'src/services/job.service';
import { QueueService } from 'src/services/queue.service'; import { QueueService } from 'src/services/queue.service';
@@ -21,10 +23,10 @@ export class JobController {
@Endpoint({ @Endpoint({
summary: 'Retrieve queue counts and status', summary: 'Retrieve queue counts and status',
description: 'Retrieve the counts of the current queue, as well as the current status.', description: 'Retrieve the counts of the current queue, as well as the current status.',
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), history: new HistoryBuilder().added('v1').beta('v1').stable('v2').deprecated('v2.4.0'),
}) })
getQueuesLegacy(): Promise<QueuesResponseDto> { getQueuesLegacy(@Auth() auth: AuthDto): Promise<QueuesResponseLegacyDto> {
return this.queueService.getAll(); return this.queueService.getAllLegacy(auth);
} }
@Post() @Post()
@@ -46,9 +48,12 @@ export class JobController {
summary: 'Run jobs', summary: 'Run jobs',
description: description:
'Queue all assets for a specific job type. Defaults to only queueing assets that have not yet been processed, but the force command can be used to re-process all assets.', 'Queue all assets for a specific job type. Defaults to only queueing assets that have not yet been processed, but the force command can be used to re-process all assets.',
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), history: new HistoryBuilder().added('v1').beta('v1').stable('v2').deprecated('v2.4.0'),
}) })
runQueueCommandLegacy(@Param() { name }: QueueNameParamDto, @Body() dto: QueueCommandDto): Promise<QueueResponseDto> { runQueueCommandLegacy(
return this.queueService.runCommand(name, dto); @Param() { name }: QueueNameParamDto,
@Body() dto: QueueCommandDto,
): Promise<QueueResponseLegacyDto> {
return this.queueService.runCommandLegacy(name, dto);
} }
} }
@@ -0,0 +1,85 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto';
import {
QueueDeleteDto,
QueueJobResponseDto,
QueueJobSearchDto,
QueueNameParamDto,
QueueResponseDto,
QueueUpdateDto,
} from 'src/dtos/queue.dto';
import { ApiTag, Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { QueueService } from 'src/services/queue.service';
@ApiTags(ApiTag.Queues)
@Controller('queues')
export class QueueController {
constructor(private service: QueueService) {}
@Get()
@Authenticated({ permission: Permission.QueueRead, admin: true })
@Endpoint({
summary: 'List all queues',
description: 'Retrieves a list of queues.',
history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'),
})
getQueues(@Auth() auth: AuthDto): Promise<QueueResponseDto[]> {
return this.service.getAll(auth);
}
@Get(':name')
@Authenticated({ permission: Permission.QueueRead, admin: true })
@Endpoint({
summary: 'Retrieve a queue',
description: 'Retrieves a specific queue by its name.',
history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'),
})
getQueue(@Auth() auth: AuthDto, @Param() { name }: QueueNameParamDto): Promise<QueueResponseDto> {
return this.service.get(auth, name);
}
@Put(':name')
@Authenticated({ permission: Permission.QueueUpdate, admin: true })
@Endpoint({
summary: 'Update a queue',
description: 'Change the paused status of a specific queue.',
history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'),
})
updateQueue(
@Auth() auth: AuthDto,
@Param() { name }: QueueNameParamDto,
@Body() dto: QueueUpdateDto,
): Promise<QueueResponseDto> {
return this.service.update(auth, name, dto);
}
@Get(':name/jobs')
@Authenticated({ permission: Permission.QueueJobRead, admin: true })
@Endpoint({
summary: 'Retrieve queue jobs',
description: 'Retrieves a list of queue jobs from the specified queue.',
history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'),
})
getQueueJobs(
@Auth() auth: AuthDto,
@Param() { name }: QueueNameParamDto,
@Query() dto: QueueJobSearchDto,
): Promise<QueueJobResponseDto[]> {
return this.service.searchJobs(auth, name, dto);
}
@Delete(':name/jobs')
@Authenticated({ permission: Permission.QueueJobDelete, admin: true })
@HttpCode(HttpStatus.NO_CONTENT)
@Endpoint({
summary: 'Empty a queue',
description: 'Removes all jobs from the specified queue.',
history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'),
})
emptyQueue(@Auth() auth: AuthDto, @Param() { name }: QueueNameParamDto, @Body() dto: QueueDeleteDto): Promise<void> {
return this.service.emptyQueue(auth, name, dto);
}
}
+89
View File
@@ -0,0 +1,89 @@
import { ApiProperty } from '@nestjs/swagger';
import { QueueResponseDto, QueueStatisticsDto } from 'src/dtos/queue.dto';
import { QueueName } from 'src/enum';
export class QueueStatusLegacyDto {
isActive!: boolean;
isPaused!: boolean;
}
export class QueueResponseLegacyDto {
@ApiProperty({ type: QueueStatusLegacyDto })
queueStatus!: QueueStatusLegacyDto;
@ApiProperty({ type: QueueStatisticsDto })
jobCounts!: QueueStatisticsDto;
}
export class QueuesResponseLegacyDto implements Record<QueueName, QueueResponseLegacyDto> {
@ApiProperty({ type: QueueResponseLegacyDto })
[QueueName.ThumbnailGeneration]!: QueueResponseLegacyDto;
@ApiProperty({ type: QueueResponseLegacyDto })
[QueueName.MetadataExtraction]!: QueueResponseLegacyDto;
@ApiProperty({ type: QueueResponseLegacyDto })
[QueueName.VideoConversion]!: QueueResponseLegacyDto;
@ApiProperty({ type: QueueResponseLegacyDto })
[QueueName.SmartSearch]!: QueueResponseLegacyDto;
@ApiProperty({ type: QueueResponseLegacyDto })
[QueueName.StorageTemplateMigration]!: QueueResponseLegacyDto;
@ApiProperty({ type: QueueResponseLegacyDto })
[QueueName.Migration]!: QueueResponseLegacyDto;
@ApiProperty({ type: QueueResponseLegacyDto })
[QueueName.BackgroundTask]!: QueueResponseLegacyDto;
@ApiProperty({ type: QueueResponseLegacyDto })
[QueueName.Search]!: QueueResponseLegacyDto;
@ApiProperty({ type: QueueResponseLegacyDto })
[QueueName.DuplicateDetection]!: QueueResponseLegacyDto;
@ApiProperty({ type: QueueResponseLegacyDto })
[QueueName.FaceDetection]!: QueueResponseLegacyDto;
@ApiProperty({ type: QueueResponseLegacyDto })
[QueueName.FacialRecognition]!: QueueResponseLegacyDto;
@ApiProperty({ type: QueueResponseLegacyDto })
[QueueName.Sidecar]!: QueueResponseLegacyDto;
@ApiProperty({ type: QueueResponseLegacyDto })
[QueueName.Library]!: QueueResponseLegacyDto;
@ApiProperty({ type: QueueResponseLegacyDto })
[QueueName.Notification]!: QueueResponseLegacyDto;
@ApiProperty({ type: QueueResponseLegacyDto })
[QueueName.BackupDatabase]!: QueueResponseLegacyDto;
@ApiProperty({ type: QueueResponseLegacyDto })
[QueueName.Ocr]!: QueueResponseLegacyDto;
@ApiProperty({ type: QueueResponseLegacyDto })
[QueueName.Workflow]!: QueueResponseLegacyDto;
}
export const mapQueueLegacy = (response: QueueResponseDto): QueueResponseLegacyDto => {
return {
queueStatus: {
isPaused: response.isPaused,
isActive: response.statistics.active > 0,
},
jobCounts: response.statistics,
};
};
export const mapQueuesLegacy = (responses: QueueResponseDto[]): QueuesResponseLegacyDto => {
const legacy = new QueuesResponseLegacyDto();
for (const response of responses) {
legacy[response.name] = mapQueueLegacy(response);
}
return legacy;
};
+42 -67
View File
@@ -1,5 +1,6 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { QueueCommand, QueueName } from 'src/enum'; import { HistoryBuilder, Property } from 'src/decorators';
import { JobName, QueueCommand, QueueJobStatus, QueueName } from 'src/enum';
import { ValidateBoolean, ValidateEnum } from 'src/validation'; import { ValidateBoolean, ValidateEnum } from 'src/validation';
export class QueueNameParamDto { export class QueueNameParamDto {
@@ -15,6 +16,46 @@ export class QueueCommandDto {
force?: boolean; // TODO: this uses undefined as a third state, which should be refactored to be more explicit force?: boolean; // TODO: this uses undefined as a third state, which should be refactored to be more explicit
} }
export class QueueUpdateDto {
@ValidateBoolean({ optional: true })
isPaused?: boolean;
}
export class QueueDeleteDto {
@ValidateBoolean({ optional: true })
@Property({
description: 'If true, will also remove failed jobs from the queue.',
history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'),
})
failed?: boolean;
}
export class QueueJobSearchDto {
@ValidateEnum({ enum: QueueJobStatus, name: 'QueueJobStatus', optional: true, each: true })
status?: QueueJobStatus[];
}
export class QueueJobResponseDto {
id?: string;
@ValidateEnum({ enum: JobName, name: 'JobName' })
name!: JobName;
data!: object;
@ApiProperty({ type: 'integer' })
timestamp!: number;
}
export class QueueResponseDto {
@ValidateEnum({ enum: QueueName, name: 'QueueName' })
name!: QueueName;
@ValidateBoolean()
isPaused!: boolean;
statistics!: QueueStatisticsDto;
}
export class QueueStatisticsDto { export class QueueStatisticsDto {
@ApiProperty({ type: 'integer' }) @ApiProperty({ type: 'integer' })
active!: number; active!: number;
@@ -29,69 +70,3 @@ export class QueueStatisticsDto {
@ApiProperty({ type: 'integer' }) @ApiProperty({ type: 'integer' })
paused!: number; paused!: number;
} }
export class QueueStatusDto {
isActive!: boolean;
isPaused!: boolean;
}
export class QueueResponseDto {
@ApiProperty({ type: QueueStatisticsDto })
jobCounts!: QueueStatisticsDto;
@ApiProperty({ type: QueueStatusDto })
queueStatus!: QueueStatusDto;
}
export class QueuesResponseDto implements Record<QueueName, QueueResponseDto> {
@ApiProperty({ type: QueueResponseDto })
[QueueName.ThumbnailGeneration]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.MetadataExtraction]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.VideoConversion]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.SmartSearch]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.StorageTemplateMigration]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.Migration]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.BackgroundTask]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.Search]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.DuplicateDetection]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.FaceDetection]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.FacialRecognition]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.Sidecar]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.Library]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.Notification]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.BackupDatabase]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.Ocr]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.Workflow]!: QueueResponseDto;
}
+22
View File
@@ -248,6 +248,14 @@ export enum Permission {
UserProfileImageUpdate = 'userProfileImage.update', UserProfileImageUpdate = 'userProfileImage.update',
UserProfileImageDelete = 'userProfileImage.delete', UserProfileImageDelete = 'userProfileImage.delete',
QueueRead = 'queue.read',
QueueUpdate = 'queue.update',
QueueJobCreate = 'queueJob.create',
QueueJobRead = 'queueJob.read',
QueueJobUpdate = 'queueJob.update',
QueueJobDelete = 'queueJob.delete',
WorkflowCreate = 'workflow.create', WorkflowCreate = 'workflow.create',
WorkflowRead = 'workflow.read', WorkflowRead = 'workflow.read',
WorkflowUpdate = 'workflow.update', WorkflowUpdate = 'workflow.update',
@@ -543,6 +551,15 @@ export enum QueueName {
Workflow = 'workflow', Workflow = 'workflow',
} }
export enum QueueJobStatus {
Active = 'active',
Failed = 'failed',
Complete = 'completed',
Delayed = 'delayed',
Waiting = 'waiting',
Paused = 'paused',
}
export enum JobName { export enum JobName {
AssetDelete = 'AssetDelete', AssetDelete = 'AssetDelete',
AssetDeleteCheck = 'AssetDeleteCheck', AssetDeleteCheck = 'AssetDeleteCheck',
@@ -624,9 +641,13 @@ export enum JobName {
export enum QueueCommand { export enum QueueCommand {
Start = 'start', Start = 'start',
/** @deprecated Use `updateQueue` instead */
Pause = 'pause', Pause = 'pause',
/** @deprecated Use `updateQueue` instead */
Resume = 'resume', Resume = 'resume',
/** @deprecated Use `emptyQueue` instead */
Empty = 'empty', Empty = 'empty',
/** @deprecated Use `emptyQueue` instead */
ClearFailed = 'clear-failed', ClearFailed = 'clear-failed',
} }
@@ -823,6 +844,7 @@ export enum ApiTag {
Partners = 'Partners', Partners = 'Partners',
People = 'People', People = 'People',
Plugins = 'Plugins', Plugins = 'Plugins',
Queues = 'Queues',
Search = 'Search', Search = 'Search',
Server = 'Server', Server = 'Server',
Sessions = 'Sessions', Sessions = 'Sessions',
+1 -1
View File
@@ -249,7 +249,7 @@ const getEnv = (): EnvData => {
prefix: 'immich_bull', prefix: 'immich_bull',
connection: { ...redisConfig }, connection: { ...redisConfig },
defaultJobOptions: { defaultJobOptions: {
attempts: 3, attempts: 1,
removeOnComplete: true, removeOnComplete: true,
removeOnFail: false, removeOnFail: false,
}, },
+27 -14
View File
@@ -5,11 +5,12 @@ import { JobsOptions, Queue, Worker } from 'bullmq';
import { ClassConstructor } from 'class-transformer'; import { ClassConstructor } from 'class-transformer';
import { setTimeout } from 'node:timers/promises'; import { setTimeout } from 'node:timers/promises';
import { JobConfig } from 'src/decorators'; import { JobConfig } from 'src/decorators';
import { JobName, JobStatus, MetadataKey, QueueCleanType, QueueName } from 'src/enum'; import { QueueJobResponseDto, QueueJobSearchDto } from 'src/dtos/queue.dto';
import { JobName, JobStatus, MetadataKey, QueueCleanType, QueueJobStatus, QueueName } from 'src/enum';
import { ConfigRepository } from 'src/repositories/config.repository'; import { ConfigRepository } from 'src/repositories/config.repository';
import { EventRepository } from 'src/repositories/event.repository'; import { EventRepository } from 'src/repositories/event.repository';
import { LoggingRepository } from 'src/repositories/logging.repository'; import { LoggingRepository } from 'src/repositories/logging.repository';
import { JobCounts, JobItem, JobOf, QueueStatus } from 'src/types'; import { JobCounts, JobItem, JobOf } from 'src/types';
import { getKeyByValue, getMethodNames, ImmichStartupError } from 'src/utils/misc'; import { getKeyByValue, getMethodNames, ImmichStartupError } from 'src/utils/misc';
type JobMapItem = { type JobMapItem = {
@@ -115,13 +116,14 @@ export class JobRepository {
worker.concurrency = concurrency; worker.concurrency = concurrency;
} }
async getQueueStatus(name: QueueName): Promise<QueueStatus> { async isActive(name: QueueName): Promise<boolean> {
const queue = this.getQueue(name); const queue = this.getQueue(name);
const count = await queue.getActiveCount();
return count > 0;
}
return { async isPaused(name: QueueName): Promise<boolean> {
isActive: !!(await queue.getActiveCount()), return this.getQueue(name).isPaused();
isPaused: await queue.isPaused(),
};
} }
pause(name: QueueName) { pause(name: QueueName) {
@@ -192,17 +194,28 @@ export class JobRepository {
} }
async waitForQueueCompletion(...queues: QueueName[]): Promise<void> { async waitForQueueCompletion(...queues: QueueName[]): Promise<void> {
let activeQueue: QueueStatus | undefined; const getPending = async () => {
do { const results = await Promise.all(queues.map(async (name) => ({ pending: await this.isActive(name), name })));
const statuses = await Promise.all(queues.map((name) => this.getQueueStatus(name))); return results.filter(({ pending }) => pending).map(({ name }) => name);
activeQueue = statuses.find((status) => status.isActive); };
} while (activeQueue);
{ let pending = await getPending();
this.logger.verbose(`Waiting for ${activeQueue} queue to stop...`);
while (pending.length > 0) {
this.logger.verbose(`Waiting for ${pending[0]} queue to stop...`);
await setTimeout(1000); await setTimeout(1000);
pending = await getPending();
} }
} }
async searchJobs(name: QueueName, dto: QueueJobSearchDto): Promise<QueueJobResponseDto[]> {
const jobs = await this.getQueue(name).getJobs(dto.status ?? Object.values(QueueJobStatus), 0, 1000);
return jobs.map((job) => {
const { id, name, timestamp, data } = job;
return { id, name: name as JobName, timestamp, data };
});
}
private getJobOptions(item: JobItem): JobsOptions | null { private getJobOptions(item: JobItem): JobsOptions | null {
switch (item.name) { switch (item.name) {
case JobName.NotifyAlbumUpdate: { case JobName.NotifyAlbumUpdate: {
@@ -153,6 +153,37 @@ describe(BackupService.name, () => {
mocks.storage.createWriteStream.mockReturnValue(new PassThrough()); mocks.storage.createWriteStream.mockReturnValue(new PassThrough());
}); });
it('should sanitize DB_URL (remove uselibpqcompat) before calling pg_dumpall', async () => {
// create a service instance with a URL connection that includes libpqcompat
const dbUrl = 'postgresql://postgres:pwd@host:5432/immich?sslmode=require&uselibpqcompat=true';
const configMock = {
getEnv: () => ({ database: { config: { connectionType: 'url', url: dbUrl }, skipMigrations: false } }),
getWorker: () => ImmichWorker.Api,
isDev: () => false,
} as unknown as any;
({ sut, mocks } = newTestService(BackupService, { config: configMock }));
mocks.storage.readdir.mockResolvedValue([]);
mocks.process.spawn.mockReturnValue(mockSpawn(0, 'data', ''));
mocks.storage.rename.mockResolvedValue();
mocks.storage.unlink.mockResolvedValue();
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled);
mocks.storage.createWriteStream.mockReturnValue(new PassThrough());
mocks.database.getPostgresVersion.mockResolvedValue('14.10');
await sut.handleBackupDatabase();
expect(mocks.process.spawn).toHaveBeenCalled();
const call = mocks.process.spawn.mock.calls[0];
const args = call[1] as string[];
// ['--dbname', '<url>', '--clean', '--if-exists']
expect(args[0]).toBe('--dbname');
const passedUrl = args[1];
expect(passedUrl).not.toContain('uselibpqcompat');
expect(passedUrl).toContain('sslmode=require');
});
it('should run a database backup successfully', async () => { it('should run a database backup successfully', async () => {
const result = await sut.handleBackupDatabase(); const result = await sut.handleBackupDatabase();
expect(result).toBe(JobStatus.Success); expect(result).toBe(JobStatus.Success);
+10 -2
View File
@@ -81,8 +81,16 @@ export class BackupService extends BaseService {
const isUrlConnection = config.connectionType === 'url'; const isUrlConnection = config.connectionType === 'url';
let connectionUrl: string = isUrlConnection ? config.url : '';
if (URL.canParse(connectionUrl)) {
// remove known bad url parameters for pg_dumpall
const url = new URL(connectionUrl);
url.searchParams.delete('uselibpqcompat');
connectionUrl = url.toString();
}
const databaseParams = isUrlConnection const databaseParams = isUrlConnection
? ['--dbname', config.url] ? ['--dbname', connectionUrl]
: [ : [
'--username', '--username',
config.username, config.username,
@@ -118,7 +126,7 @@ export class BackupService extends BaseService {
{ {
env: { env: {
PATH: process.env.PATH, PATH: process.env.PATH,
PGPASSWORD: isUrlConnection ? new URL(config.url).password : config.password, PGPASSWORD: isUrlConnection ? new URL(connectionUrl).password : config.password,
}, },
}, },
); );
+35 -3
View File
@@ -1017,12 +1017,44 @@ describe(MetadataService.name, () => {
); );
}); });
it('should ignore duration from exif data', async () => { it('should use Duration from exif', async () => {
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
mockReadTags({}, { Duration: { Value: 123 } }); mockReadTags({ Duration: 123 }, {});
await sut.handleMetadataExtraction({ id: assetStub.image.id }); await sut.handleMetadataExtraction({ id: assetStub.image.id });
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: null }));
expect(mocks.metadata.readTags).toHaveBeenCalledTimes(1);
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:02:03.000' }));
});
it('should prefer Duration from exif over sidecar', async () => {
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
...assetStub.image,
sidecarPath: '/path/to/something',
});
mockReadTags({ Duration: 123 }, { Duration: 456 });
await sut.handleMetadataExtraction({ id: assetStub.image.id });
expect(mocks.metadata.readTags).toHaveBeenCalledTimes(2);
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:02:03.000' }));
});
it('should ignore Duration from exif for videos', async () => {
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video);
mockReadTags({ Duration: 123 }, {});
mocks.media.probe.mockResolvedValue({
...probeStub.videoStreamH264,
format: {
...probeStub.videoStreamH264.format,
duration: 456,
},
});
await sut.handleMetadataExtraction({ id: assetStub.video.id });
expect(mocks.metadata.readTags).toHaveBeenCalledTimes(1);
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:07:36.000' }));
}); });
it('should trim whitespace from description', async () => { it('should trim whitespace from description', async () => {
+22 -16
View File
@@ -291,7 +291,7 @@ export class MetadataService extends BaseService {
this.assetRepository.upsertExif(exifData), this.assetRepository.upsertExif(exifData),
this.assetRepository.update({ this.assetRepository.update({
id: asset.id, id: asset.id,
duration: exifTags.Duration?.toString() ?? null, duration: this.getDuration(exifTags),
localDateTime: dates.localDateTime, localDateTime: dates.localDateTime,
fileCreatedAt: dates.dateTimeOriginal ?? undefined, fileCreatedAt: dates.dateTimeOriginal ?? undefined,
fileModifiedAt: stats.mtime, fileModifiedAt: stats.mtime,
@@ -457,19 +457,7 @@ export class MetadataService extends BaseService {
return { width, height }; return { width, height };
} }
private getExifTags(asset: { private async getExifTags(asset: {
originalPath: string;
sidecarPath: string | null;
type: AssetType;
}): Promise<ImmichTags> {
if (!asset.sidecarPath && asset.type === AssetType.Image) {
return this.metadataRepository.readTags(asset.originalPath);
}
return this.mergeExifTags(asset);
}
private async mergeExifTags(asset: {
originalPath: string; originalPath: string;
sidecarPath: string | null; sidecarPath: string | null;
type: AssetType; type: AssetType;
@@ -492,7 +480,11 @@ export class MetadataService extends BaseService {
} }
// prefer duration from video tags // prefer duration from video tags
delete mediaTags.Duration; if (videoTags) {
delete mediaTags.Duration;
}
// never use duration from sidecar
delete sidecarTags?.Duration; delete sidecarTags?.Duration;
return { ...mediaTags, ...videoTags, ...sidecarTags }; return { ...mediaTags, ...videoTags, ...sidecarTags };
@@ -934,6 +926,20 @@ export class MetadataService extends BaseService {
return bitsPerSample; return bitsPerSample;
} }
private getDuration(tags: ImmichTags): string | null {
const duration = tags.Duration;
if (typeof duration === 'string') {
return duration;
}
if (typeof duration === 'number') {
return Duration.fromObject({ seconds: duration }).toFormat('hh:mm:ss.SSS');
}
return null;
}
private async getVideoTags(originalPath: string) { private async getVideoTags(originalPath: string) {
const { videoStreams, format } = await this.mediaRepository.probe(originalPath); const { videoStreams, format } = await this.mediaRepository.probe(originalPath);
@@ -961,7 +967,7 @@ export class MetadataService extends BaseService {
} }
if (format.duration) { if (format.duration) {
tags.Duration = Duration.fromObject({ seconds: format.duration }).toFormat('hh:mm:ss.SSS'); tags.Duration = format.duration;
} }
return tags; return tags;
+63 -69
View File
@@ -2,6 +2,7 @@ import { BadRequestException } from '@nestjs/common';
import { defaults, SystemConfig } from 'src/config'; import { defaults, SystemConfig } from 'src/config';
import { ImmichWorker, JobName, QueueCommand, QueueName } from 'src/enum'; import { ImmichWorker, JobName, QueueCommand, QueueName } from 'src/enum';
import { QueueService } from 'src/services/queue.service'; import { QueueService } from 'src/services/queue.service';
import { factory } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils'; import { newTestService, ServiceMocks } from 'test/utils';
describe(QueueService.name, () => { describe(QueueService.name, () => {
@@ -52,80 +53,64 @@ describe(QueueService.name, () => {
describe('getAllJobStatus', () => { describe('getAllJobStatus', () => {
it('should get all job statuses', async () => { it('should get all job statuses', async () => {
mocks.job.getJobCounts.mockResolvedValue({ const stats = factory.queueStatistics({ active: 1 });
active: 1, const expected = { jobCounts: stats, queueStatus: { isActive: true, isPaused: true } };
completed: 1,
failed: 1,
delayed: 1,
waiting: 1,
paused: 1,
});
mocks.job.getQueueStatus.mockResolvedValue({
isActive: true,
isPaused: true,
});
const expectedJobStatus = { mocks.job.getJobCounts.mockResolvedValue(stats);
jobCounts: { mocks.job.isPaused.mockResolvedValue(true);
active: 1,
completed: 1,
delayed: 1,
failed: 1,
waiting: 1,
paused: 1,
},
queueStatus: {
isActive: true,
isPaused: true,
},
};
await expect(sut.getAll()).resolves.toEqual({ await expect(sut.getAllLegacy(factory.auth())).resolves.toEqual({
[QueueName.BackgroundTask]: expectedJobStatus, [QueueName.BackgroundTask]: expected,
[QueueName.DuplicateDetection]: expectedJobStatus, [QueueName.DuplicateDetection]: expected,
[QueueName.SmartSearch]: expectedJobStatus, [QueueName.SmartSearch]: expected,
[QueueName.MetadataExtraction]: expectedJobStatus, [QueueName.MetadataExtraction]: expected,
[QueueName.Search]: expectedJobStatus, [QueueName.Search]: expected,
[QueueName.StorageTemplateMigration]: expectedJobStatus, [QueueName.StorageTemplateMigration]: expected,
[QueueName.Migration]: expectedJobStatus, [QueueName.Migration]: expected,
[QueueName.ThumbnailGeneration]: expectedJobStatus, [QueueName.ThumbnailGeneration]: expected,
[QueueName.VideoConversion]: expectedJobStatus, [QueueName.VideoConversion]: expected,
[QueueName.FaceDetection]: expectedJobStatus, [QueueName.FaceDetection]: expected,
[QueueName.FacialRecognition]: expectedJobStatus, [QueueName.FacialRecognition]: expected,
[QueueName.Sidecar]: expectedJobStatus, [QueueName.Sidecar]: expected,
[QueueName.Library]: expectedJobStatus, [QueueName.Library]: expected,
[QueueName.Notification]: expectedJobStatus, [QueueName.Notification]: expected,
[QueueName.BackupDatabase]: expectedJobStatus, [QueueName.BackupDatabase]: expected,
[QueueName.Ocr]: expectedJobStatus, [QueueName.Ocr]: expected,
[QueueName.Workflow]: expectedJobStatus, [QueueName.Workflow]: expected,
}); });
}); });
}); });
describe('handleCommand', () => { describe('handleCommand', () => {
it('should handle a pause command', async () => { it('should handle a pause command', async () => {
await sut.runCommand(QueueName.MetadataExtraction, { command: QueueCommand.Pause, force: false }); mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics());
await sut.runCommandLegacy(QueueName.MetadataExtraction, { command: QueueCommand.Pause, force: false });
expect(mocks.job.pause).toHaveBeenCalledWith(QueueName.MetadataExtraction); expect(mocks.job.pause).toHaveBeenCalledWith(QueueName.MetadataExtraction);
}); });
it('should handle a resume command', async () => { it('should handle a resume command', async () => {
await sut.runCommand(QueueName.MetadataExtraction, { command: QueueCommand.Resume, force: false }); mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics());
await sut.runCommandLegacy(QueueName.MetadataExtraction, { command: QueueCommand.Resume, force: false });
expect(mocks.job.resume).toHaveBeenCalledWith(QueueName.MetadataExtraction); expect(mocks.job.resume).toHaveBeenCalledWith(QueueName.MetadataExtraction);
}); });
it('should handle an empty command', async () => { it('should handle an empty command', async () => {
await sut.runCommand(QueueName.MetadataExtraction, { command: QueueCommand.Empty, force: false }); mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics());
await sut.runCommandLegacy(QueueName.MetadataExtraction, { command: QueueCommand.Empty, force: false });
expect(mocks.job.empty).toHaveBeenCalledWith(QueueName.MetadataExtraction); expect(mocks.job.empty).toHaveBeenCalledWith(QueueName.MetadataExtraction);
}); });
it('should not start a job that is already running', async () => { it('should not start a job that is already running', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: true, isPaused: false }); mocks.job.isActive.mockResolvedValue(true);
await expect( await expect(
sut.runCommand(QueueName.VideoConversion, { command: QueueCommand.Start, force: false }), sut.runCommandLegacy(QueueName.VideoConversion, { command: QueueCommand.Start, force: false }),
).rejects.toBeInstanceOf(BadRequestException); ).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.job.queue).not.toHaveBeenCalled(); expect(mocks.job.queue).not.toHaveBeenCalled();
@@ -133,33 +118,37 @@ describe(QueueService.name, () => {
}); });
it('should handle a start video conversion command', async () => { it('should handle a start video conversion command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); mocks.job.isActive.mockResolvedValue(false);
mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics());
await sut.runCommand(QueueName.VideoConversion, { command: QueueCommand.Start, force: false }); await sut.runCommandLegacy(QueueName.VideoConversion, { command: QueueCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetEncodeVideoQueueAll, data: { force: false } }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetEncodeVideoQueueAll, data: { force: false } });
}); });
it('should handle a start storage template migration command', async () => { it('should handle a start storage template migration command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); mocks.job.isActive.mockResolvedValue(false);
mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics());
await sut.runCommand(QueueName.StorageTemplateMigration, { command: QueueCommand.Start, force: false }); await sut.runCommandLegacy(QueueName.StorageTemplateMigration, { command: QueueCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.StorageTemplateMigration }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.StorageTemplateMigration });
}); });
it('should handle a start smart search command', async () => { it('should handle a start smart search command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); mocks.job.isActive.mockResolvedValue(false);
mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics());
await sut.runCommand(QueueName.SmartSearch, { command: QueueCommand.Start, force: false }); await sut.runCommandLegacy(QueueName.SmartSearch, { command: QueueCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SmartSearchQueueAll, data: { force: false } }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SmartSearchQueueAll, data: { force: false } });
}); });
it('should handle a start metadata extraction command', async () => { it('should handle a start metadata extraction command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); mocks.job.isActive.mockResolvedValue(false);
mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics());
await sut.runCommand(QueueName.MetadataExtraction, { command: QueueCommand.Start, force: false }); await sut.runCommandLegacy(QueueName.MetadataExtraction, { command: QueueCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ expect(mocks.job.queue).toHaveBeenCalledWith({
name: JobName.AssetExtractMetadataQueueAll, name: JobName.AssetExtractMetadataQueueAll,
@@ -168,17 +157,19 @@ describe(QueueService.name, () => {
}); });
it('should handle a start sidecar command', async () => { it('should handle a start sidecar command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); mocks.job.isActive.mockResolvedValue(false);
mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics());
await sut.runCommand(QueueName.Sidecar, { command: QueueCommand.Start, force: false }); await sut.runCommandLegacy(QueueName.Sidecar, { command: QueueCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SidecarQueueAll, data: { force: false } }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SidecarQueueAll, data: { force: false } });
}); });
it('should handle a start thumbnail generation command', async () => { it('should handle a start thumbnail generation command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); mocks.job.isActive.mockResolvedValue(false);
mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics());
await sut.runCommand(QueueName.ThumbnailGeneration, { command: QueueCommand.Start, force: false }); await sut.runCommandLegacy(QueueName.ThumbnailGeneration, { command: QueueCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ expect(mocks.job.queue).toHaveBeenCalledWith({
name: JobName.AssetGenerateThumbnailsQueueAll, name: JobName.AssetGenerateThumbnailsQueueAll,
@@ -187,34 +178,37 @@ describe(QueueService.name, () => {
}); });
it('should handle a start face detection command', async () => { it('should handle a start face detection command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); mocks.job.isActive.mockResolvedValue(false);
mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics());
await sut.runCommand(QueueName.FaceDetection, { command: QueueCommand.Start, force: false }); await sut.runCommandLegacy(QueueName.FaceDetection, { command: QueueCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetDetectFacesQueueAll, data: { force: false } }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetDetectFacesQueueAll, data: { force: false } });
}); });
it('should handle a start facial recognition command', async () => { it('should handle a start facial recognition command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); mocks.job.isActive.mockResolvedValue(false);
mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics());
await sut.runCommand(QueueName.FacialRecognition, { command: QueueCommand.Start, force: false }); await sut.runCommandLegacy(QueueName.FacialRecognition, { command: QueueCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.FacialRecognitionQueueAll, data: { force: false } }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.FacialRecognitionQueueAll, data: { force: false } });
}); });
it('should handle a start backup database command', async () => { it('should handle a start backup database command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); mocks.job.isActive.mockResolvedValue(false);
mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics());
await sut.runCommand(QueueName.BackupDatabase, { command: QueueCommand.Start, force: false }); await sut.runCommandLegacy(QueueName.BackupDatabase, { command: QueueCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.DatabaseBackup, data: { force: false } }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.DatabaseBackup, data: { force: false } });
}); });
it('should throw a bad request when an invalid queue is used', async () => { it('should throw a bad request when an invalid queue is used', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); mocks.job.isActive.mockResolvedValue(false);
await expect( await expect(
sut.runCommand(QueueName.BackgroundTask, { command: QueueCommand.Start, force: false }), sut.runCommandLegacy(QueueName.BackgroundTask, { command: QueueCommand.Start, force: false }),
).rejects.toBeInstanceOf(BadRequestException); ).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.job.queue).not.toHaveBeenCalled(); expect(mocks.job.queue).not.toHaveBeenCalled();
+60 -14
View File
@@ -2,7 +2,21 @@ import { BadRequestException, Injectable } from '@nestjs/common';
import { ClassConstructor } from 'class-transformer'; import { ClassConstructor } from 'class-transformer';
import { SystemConfig } from 'src/config'; import { SystemConfig } from 'src/config';
import { OnEvent } from 'src/decorators'; import { OnEvent } from 'src/decorators';
import { QueueCommandDto, QueueResponseDto, QueuesResponseDto } from 'src/dtos/queue.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import {
mapQueueLegacy,
mapQueuesLegacy,
QueueResponseLegacyDto,
QueuesResponseLegacyDto,
} from 'src/dtos/queue-legacy.dto';
import {
QueueCommandDto,
QueueDeleteDto,
QueueJobResponseDto,
QueueJobSearchDto,
QueueResponseDto,
QueueUpdateDto,
} from 'src/dtos/queue.dto';
import { import {
BootstrapEventPriority, BootstrapEventPriority,
CronJob, CronJob,
@@ -86,7 +100,7 @@ export class QueueService extends BaseService {
this.services = services; this.services = services;
} }
async runCommand(name: QueueName, dto: QueueCommandDto): Promise<QueueResponseDto> { async runCommandLegacy(name: QueueName, dto: QueueCommandDto): Promise<QueueResponseLegacyDto> {
this.logger.debug(`Handling command: queue=${name},command=${dto.command},force=${dto.force}`); this.logger.debug(`Handling command: queue=${name},command=${dto.command},force=${dto.force}`);
switch (dto.command) { switch (dto.command) {
@@ -117,28 +131,60 @@ export class QueueService extends BaseService {
} }
} }
const response = await this.getByName(name);
return mapQueueLegacy(response);
}
async getAll(_auth: AuthDto): Promise<QueueResponseDto[]> {
return Promise.all(Object.values(QueueName).map((name) => this.getByName(name)));
}
async getAllLegacy(auth: AuthDto): Promise<QueuesResponseLegacyDto> {
const responses = await this.getAll(auth);
return mapQueuesLegacy(responses);
}
get(auth: AuthDto, name: QueueName): Promise<QueueResponseDto> {
return this.getByName(name); return this.getByName(name);
} }
async getAll(): Promise<QueuesResponseDto> { async update(auth: AuthDto, name: QueueName, dto: QueueUpdateDto): Promise<QueueResponseDto> {
const response = new QueuesResponseDto(); if (dto.isPaused === true) {
for (const name of Object.values(QueueName)) { if (name === QueueName.BackgroundTask) {
response[name] = await this.getByName(name); throw new BadRequestException(`The BackgroundTask queue cannot be paused`);
}
await this.jobRepository.pause(name);
} }
return response;
if (dto.isPaused === false) {
await this.jobRepository.resume(name);
}
return this.getByName(name);
} }
async getByName(name: QueueName): Promise<QueueResponseDto> { searchJobs(auth: AuthDto, name: QueueName, dto: QueueJobSearchDto): Promise<QueueJobResponseDto[]> {
const [jobCounts, queueStatus] = await Promise.all([ return this.jobRepository.searchJobs(name, dto);
this.jobRepository.getJobCounts(name), }
this.jobRepository.getQueueStatus(name),
]);
return { jobCounts, queueStatus }; async emptyQueue(auth: AuthDto, name: QueueName, dto: QueueDeleteDto) {
await this.jobRepository.empty(name);
if (dto.failed) {
await this.jobRepository.clear(name, QueueCleanType.Failed);
}
}
private async getByName(name: QueueName): Promise<QueueResponseDto> {
const [statistics, isPaused] = await Promise.all([
this.jobRepository.getJobCounts(name),
this.jobRepository.isPaused(name),
]);
return { name, isPaused, statistics };
} }
private async start(name: QueueName, { force }: QueueCommandDto): Promise<void> { private async start(name: QueueName, { force }: QueueCommandDto): Promise<void> {
const { isActive } = await this.jobRepository.getQueueStatus(name); const isActive = await this.jobRepository.isActive(name);
if (isActive) { if (isActive) {
throw new BadRequestException(`Job is already running`); throw new BadRequestException(`Job is already running`);
} }
-5
View File
@@ -291,11 +291,6 @@ export interface JobCounts {
paused: number; paused: number;
} }
export interface QueueStatus {
isActive: boolean;
isPaused: boolean;
}
export type JobItem = export type JobItem =
// Audit // Audit
| { name: JobName.AuditTableCleanup; data?: IBaseJob } | { name: JobName.AuditTableCleanup; data?: IBaseJob }
@@ -11,9 +11,11 @@ export const newJobRepositoryMock = (): Mocked<RepositoryInterface<JobRepository
empty: vitest.fn(), empty: vitest.fn(),
pause: vitest.fn(), pause: vitest.fn(),
resume: vitest.fn(), resume: vitest.fn(),
searchJobs: vitest.fn(),
queue: vitest.fn().mockImplementation(() => Promise.resolve()), queue: vitest.fn().mockImplementation(() => Promise.resolve()),
queueAll: vitest.fn().mockImplementation(() => Promise.resolve()), queueAll: vitest.fn().mockImplementation(() => Promise.resolve()),
getQueueStatus: vitest.fn(), isActive: vitest.fn(),
isPaused: vitest.fn(),
getJobCounts: vitest.fn(), getJobCounts: vitest.fn(),
clear: vitest.fn(), clear: vitest.fn(),
waitForQueueCompletion: vitest.fn(), waitForQueueCompletion: vitest.fn(),
+12
View File
@@ -14,6 +14,7 @@ import {
} from 'src/database'; } from 'src/database';
import { MapAsset } from 'src/dtos/asset-response.dto'; import { MapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { QueueStatisticsDto } from 'src/dtos/queue.dto';
import { AssetStatus, AssetType, AssetVisibility, MemoryType, Permission, UserMetadataKey, UserStatus } from 'src/enum'; import { AssetStatus, AssetType, AssetVisibility, MemoryType, Permission, UserMetadataKey, UserStatus } from 'src/enum';
import { OnThisDayData, UserMetadataItem } from 'src/types'; import { OnThisDayData, UserMetadataItem } from 'src/types';
import { v4, v7 } from 'uuid'; import { v4, v7 } from 'uuid';
@@ -139,6 +140,16 @@ const sessionFactory = (session: Partial<Session> = {}) => ({
...session, ...session,
}); });
const queueStatisticsFactory = (dto?: Partial<QueueStatisticsDto>) => ({
active: 0,
completed: 0,
failed: 0,
delayed: 0,
waiting: 0,
paused: 0,
...dto,
});
const stackFactory = () => ({ const stackFactory = () => ({
id: newUuid(), id: newUuid(),
ownerId: newUuid(), ownerId: newUuid(),
@@ -353,6 +364,7 @@ export const factory = {
library: libraryFactory, library: libraryFactory,
memory: memoryFactory, memory: memoryFactory,
partner: partnerFactory, partner: partnerFactory,
queueStatistics: queueStatisticsFactory,
session: sessionFactory, session: sessionFactory,
stack: stackFactory, stack: stackFactory,
user: userFactory, user: userFactory,
+1 -1
View File
@@ -1 +1 @@
24.11.0 24.11.1
+8 -8
View File
@@ -11,13 +11,13 @@
"preview": "vite preview", "preview": "vite preview",
"check:svelte": "svelte-check --no-tsconfig --fail-on-warnings", "check:svelte": "svelte-check --no-tsconfig --fail-on-warnings",
"check:typescript": "tsc --noEmit", "check:typescript": "tsc --noEmit",
"check:watch": "npm run check:svelte -- --watch", "check:watch": "pnpm run check:svelte --watch",
"check:code": "npm run format && npm run lint:p && npm run check:svelte && npm run check:typescript", "check:code": "pnpm run format && pnpm run lint && pnpm run check:svelte && pnpm run check:typescript",
"check:all": "npm run check:code && npm run test:cov", "check:all": "pnpm run check:code && pnpm run test:cov",
"lint": "eslint . --max-warnings 0 --concurrency 4", "lint": "eslint . --max-warnings 0 --concurrency 4",
"lint:fix": "npm run lint -- --fix", "lint:fix": "pnpm run lint --fix",
"format": "prettier --check .", "format": "prettier --check .",
"format:fix": "prettier --write . && npm run format:i18n", "format:fix": "prettier --write . && pnpm run format:i18n",
"format:i18n": "pnpm dlx sort-json ../i18n/*.json", "format:i18n": "pnpm dlx sort-json ../i18n/*.json",
"test": "vitest --run", "test": "vitest --run",
"test:cov": "vitest --coverage", "test:cov": "vitest --coverage",
@@ -28,7 +28,7 @@
"@formatjs/icu-messageformat-parser": "^2.9.8", "@formatjs/icu-messageformat-parser": "^2.9.8",
"@immich/justified-layout-wasm": "^0.4.3", "@immich/justified-layout-wasm": "^0.4.3",
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
"@immich/ui": "^0.43.0", "@immich/ui": "^0.49.1",
"@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mapbox/mapbox-gl-rtl-text": "0.2.3",
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.14.0", "@photo-sphere-viewer/core": "^5.14.0",
@@ -97,7 +97,7 @@
"prettier-plugin-sort-json": "^4.1.1", "prettier-plugin-sort-json": "^4.1.1",
"prettier-plugin-svelte": "^3.3.3", "prettier-plugin-svelte": "^3.3.3",
"rollup-plugin-visualizer": "^6.0.0", "rollup-plugin-visualizer": "^6.0.0",
"svelte": "5.43.0", "svelte": "5.43.12",
"svelte-check": "^4.1.5", "svelte-check": "^4.1.5",
"svelte-eslint-parser": "^1.3.3", "svelte-eslint-parser": "^1.3.3",
"tailwindcss": "^4.1.7", "tailwindcss": "^4.1.7",
@@ -107,6 +107,6 @@
"vitest": "^3.0.0" "vitest": "^3.0.0"
}, },
"volta": { "volta": {
"node": "24.11.0" "node": "24.11.1"
} }
} }
-8
View File
@@ -76,14 +76,6 @@
--immich-dark-gray: 33 33 33; --immich-dark-gray: 33 33 33;
} }
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: rgb(var(--immich-ui-default-border));
}
button:not(:disabled), button:not(:disabled),
[role='button']:not(:disabled) { [role='button']:not(:disabled) {
cursor: pointer; cursor: pointer;
+3 -5
View File
@@ -1,16 +1,14 @@
<script lang="ts"> <script lang="ts">
import type { ActionItem } from '$lib/types'; import { IconButton, type ActionItem } from '@immich/ui';
import { IconButton, type IconButtonProps } from '@immich/ui';
type Props = { type Props = {
action: ActionItem; action: ActionItem;
}; };
const { action }: Props = $props(); const { action }: Props = $props();
const { title, icon, color = 'secondary', props: other = {}, onSelect } = $derived(action); const { title, icon, color = 'secondary', onAction } = $derived(action);
const onclick = (event: Event) => onSelect?.({ event, item: action });
</script> </script>
{#if action.$if?.() ?? true} {#if action.$if?.() ?? true}
<IconButton variant="ghost" {color} shape="round" {...other as IconButtonProps} {icon} aria-label={title} {onclick} /> <IconButton variant="ghost" shape="round" {color} {icon} aria-label={title} onclick={() => onAction(action)} />
{/if} {/if}
+5 -6
View File
@@ -1,18 +1,17 @@
<script lang="ts"> <script lang="ts">
import type { ActionItem } from '$lib/types'; import { type ActionItem, Button, Text } from '@immich/ui';
import { Button, type ButtonProps, Text } from '@immich/ui';
type Props = { type Props = {
action: ActionItem; action: ActionItem;
title?: string;
}; };
const { action }: Props = $props(); const { action, title: titleAttr }: Props = $props();
const { title, icon, color = 'secondary', props: other = {}, onSelect } = $derived(action); const { title, icon, color = 'secondary', onAction } = $derived(action);
const onclick = (event: Event) => onSelect?.({ event, item: action });
</script> </script>
{#if action.$if?.() ?? true} {#if action.$if?.() ?? true}
<Button variant="ghost" size="small" {color} {...other as ButtonProps} leadingIcon={icon} {onclick}> <Button variant="ghost" size="small" {color} leadingIcon={icon} onclick={() => onAction(action)} title={titleAttr}>
<Text class="hidden md:block">{title}</Text> <Text class="hidden md:block">{title}</Text>
</Button> </Button>
{/if} {/if}
+3 -5
View File
@@ -1,16 +1,14 @@
<script lang="ts"> <script lang="ts">
import type { ActionItem } from '$lib/types'; import { IconButton, type ActionItem } from '@immich/ui';
import { IconButton, type IconButtonProps } from '@immich/ui';
type Props = { type Props = {
action: ActionItem; action: ActionItem;
}; };
const { action }: Props = $props(); const { action }: Props = $props();
const { title, icon, props: other = {}, onSelect } = $derived(action); const { title, icon, onAction } = $derived(action);
const onclick = (event: Event) => onSelect?.({ event, item: action });
</script> </script>
{#if action.$if?.() ?? true} {#if action.$if?.() ?? true}
<IconButton shape="round" color="primary" {...other as IconButtonProps} {icon} aria-label={title} {onclick} /> <IconButton shape="round" color="primary" {icon} aria-label={title} onclick={() => onAction(action)} />
{/if} {/if}
@@ -32,7 +32,7 @@
.filter(Boolean) .filter(Boolean)
.join(' • '); .join(' • ');
const SharedLinkActions = $derived(getSharedLinkActions($t, sharedLink)); const { ViewQrCode, Copy } = $derived(getSharedLinkActions($t, sharedLink));
</script> </script>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
@@ -41,7 +41,7 @@
<Text size="tiny" color="muted">{getShareProperties()}</Text> <Text size="tiny" color="muted">{getShareProperties()}</Text>
</div> </div>
<div class="flex"> <div class="flex">
<ActionButton action={SharedLinkActions.ViewQrCode} /> <ActionButton action={ViewQrCode} />
<ActionButton action={SharedLinkActions.Copy} /> <ActionButton action={Copy} />
</div> </div>
</div> </div>
@@ -33,7 +33,6 @@
import { groupBy } from 'lodash-es'; import { groupBy } from 'lodash-es';
import { onMount, type Snippet } from 'svelte'; import { onMount, type Snippet } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { run } from 'svelte/legacy';
interface Props { interface Props {
ownedAlbums?: AlbumResponseDto[]; ownedAlbums?: AlbumResponseDto[];
@@ -128,65 +127,45 @@
}, },
}; };
let albums: AlbumResponseDto[] = $state([]); let albums = $derived.by(() => {
let filteredAlbums: AlbumResponseDto[] = $state([]); switch (userSettings.filter) {
let groupedAlbums: AlbumGroup[] = $state([]); case AlbumFilter.Owned: {
return ownedAlbums;
}
case AlbumFilter.Shared: {
return sharedAlbums;
}
default: {
const nonOwnedAlbums = sharedAlbums.filter((album) => album.ownerId !== $user.id);
return nonOwnedAlbums.length > 0 ? ownedAlbums.concat(nonOwnedAlbums) : ownedAlbums;
}
}
});
const normalizedSearchQuery = $derived(normalizeSearchString(searchQuery));
let filteredAlbums = $derived(
normalizedSearchQuery
? albums.filter(({ albumName }) => normalizeSearchString(albumName).includes(normalizedSearchQuery))
: albums,
);
let albumGroupOption: string = $state(AlbumGroupBy.None); let albumGroupOption = $derived(getSelectedAlbumGroupOption(userSettings));
let groupedAlbums = $derived.by(() => {
const groupFunc = groupOptions[albumGroupOption] ?? groupOptions[AlbumGroupBy.None];
const groupedAlbums = groupFunc(stringToSortOrder(userSettings.groupOrder), filteredAlbums);
let albumToShare: AlbumResponseDto | null = $state(null); return groupedAlbums.map((group) => ({
id: group.id,
name: group.name,
albums: sortAlbums(group.albums, { sortBy: userSettings.sortBy, orderBy: userSettings.sortOrder }),
}));
});
let contextMenuPosition: ContextMenuPosition = $state({ x: 0, y: 0 }); let contextMenuPosition: ContextMenuPosition = $state({ x: 0, y: 0 });
let selectedAlbum: AlbumResponseDto | undefined = $state(); let selectedAlbum: AlbumResponseDto | undefined = $state();
let isOpen = $state(false); let isOpen = $state(false);
// Step 1: Filter between Owned and Shared albums, or both. // TODO get rid of this
run(() => { $effect(() => {
switch (userSettings.filter) {
case AlbumFilter.Owned: {
albums = ownedAlbums;
break;
}
case AlbumFilter.Shared: {
albums = sharedAlbums;
break;
}
default: {
const userId = $user.id;
const nonOwnedAlbums = sharedAlbums.filter((album) => album.ownerId !== userId);
albums = nonOwnedAlbums.length > 0 ? ownedAlbums.concat(nonOwnedAlbums) : ownedAlbums;
}
}
});
// Step 2: Filter using the given search query.
run(() => {
if (searchQuery) {
const searchAlbumNormalized = normalizeSearchString(searchQuery);
filteredAlbums = albums.filter((album) => {
return normalizeSearchString(album.albumName).includes(searchAlbumNormalized);
});
} else {
filteredAlbums = albums;
}
});
// Step 3: Group albums.
run(() => {
albumGroupOption = getSelectedAlbumGroupOption(userSettings);
const groupFunc = groupOptions[albumGroupOption] ?? groupOptions[AlbumGroupBy.None];
groupedAlbums = groupFunc(stringToSortOrder(userSettings.groupOrder), filteredAlbums);
});
// Step 4: Sort albums amongst each group.
run(() => {
groupedAlbums = groupedAlbums.map((group) => ({
id: group.id,
name: group.name,
albums: sortAlbums(group.albums, { sortBy: userSettings.sortBy, orderBy: userSettings.sortOrder }),
}));
albumGroupIds = groupedAlbums.map(({ id }) => id); albumGroupIds = groupedAlbums.map(({ id }) => id);
}); });
@@ -231,7 +210,7 @@
const result = await modalManager.show(AlbumShareModal, { album: selectedAlbum }); const result = await modalManager.show(AlbumShareModal, { album: selectedAlbum });
switch (result?.action) { switch (result?.action) {
case 'sharedUsers': { case 'sharedUsers': {
await handleAddUsers(result.data); await handleAddUsers(selectedAlbum, result.data);
break; break;
} }
@@ -300,22 +279,17 @@
updateRecentAlbumInfo(album); updateRecentAlbumInfo(album);
}; };
const handleAddUsers = async (albumUsers: AlbumUserAddDto[]) => { const handleAddUsers = async (album: AlbumResponseDto, albumUsers: AlbumUserAddDto[]) => {
if (!albumToShare) {
return;
}
try { try {
const album = await addUsersToAlbum({ const updatedAlbum = await addUsersToAlbum({
id: albumToShare.id, id: album.id,
addUsersDto: { addUsersDto: {
albumUsers, albumUsers,
}, },
}); });
updateAlbumInfo(album); updateAlbumInfo(updatedAlbum);
} catch (error) { } catch (error) {
handleError(error, $t('errors.unable_to_add_album_users')); handleError(error, $t('errors.unable_to_add_album_users'));
} finally {
albumToShare = null;
} }
}; };
@@ -52,7 +52,7 @@
let innerHeight: number = $state(0); let innerHeight: number = $state(0);
let activityHeight: number = $state(0); let activityHeight: number = $state(0);
let chatHeight: number = $state(0); let chatHeight: number = $state(0);
let divHeight: number = $state(0); let divHeight = $derived(innerHeight - activityHeight);
let previousAssetId: string | undefined = $state(assetId); let previousAssetId: string | undefined = $state(assetId);
let message = $state(''); let message = $state('');
let isSendingMessage = $state(false); let isSendingMessage = $state(false);
@@ -96,11 +96,7 @@
} }
isSendingMessage = false; isSendingMessage = false;
}; };
$effect(() => {
if (innerHeight && activityHeight) {
divHeight = innerHeight - activityHeight;
}
});
$effect(() => { $effect(() => {
if (assetId && previousAssetId != assetId) { if (assetId && previousAssetId != assetId) {
previousAssetId = assetId; previousAssetId = assetId;
@@ -35,15 +35,13 @@
}); });
}; };
let albumNameArray: string[] = $state(['', '', '']);
// This part of the code is responsible for splitting album name into 3 parts where part 2 is the search query // This part of the code is responsible for splitting album name into 3 parts where part 2 is the search query
// It is used to highlight the search query in the album name // It is used to highlight the search query in the album name
$effect(() => { const albumNameArray: string[] = $derived.by(() => {
let { albumName } = album; let { albumName } = album;
let findIndex = normalizeSearchString(albumName).indexOf(normalizeSearchString(searchQuery)); let findIndex = normalizeSearchString(albumName).indexOf(normalizeSearchString(searchQuery));
let findLength = searchQuery.length; let findLength = searchQuery.length;
albumNameArray = [ return [
albumName.slice(0, findIndex), albumName.slice(0, findIndex),
albumName.slice(findIndex, findIndex + findLength), albumName.slice(findIndex, findIndex + findLength),
albumName.slice(findIndex + findLength), albumName.slice(findIndex + findLength),
@@ -395,12 +395,12 @@
} }
}); });
let currentAssetId = $derived(asset.id); // primarily, this is reactive on `asset`
$effect(() => { $effect(() => {
if (currentAssetId) { handlePromiseError(handleGetAllAlbums());
untrack(() => handlePromiseError(handleGetAllAlbums())); ocrManager.clear();
ocrManager.clear(); if (!sharedLink) {
handlePromiseError(ocrManager.getAssetOcr(currentAssetId)); handlePromiseError(ocrManager.getAssetOcr(asset.id));
} }
}); });
</script> </script>
@@ -23,6 +23,7 @@
import { Icon, IconButton, LoadingSpinner, modalManager } from '@immich/ui'; import { Icon, IconButton, LoadingSpinner, modalManager } from '@immich/ui';
import { import {
mdiCalendar, mdiCalendar,
mdiCamera,
mdiCameraIris, mdiCameraIris,
mdiClose, mdiClose,
mdiEye, mdiEye,
@@ -372,9 +373,9 @@
</div> </div>
</div> </div>
{#if asset.exifInfo?.make || asset.exifInfo?.model || asset.exifInfo?.fNumber} {#if asset.exifInfo?.make || asset.exifInfo?.model || asset.exifInfo?.exposureTime || asset.exifInfo?.iso}
<div class="flex gap-4 py-4"> <div class="flex gap-4 py-4">
<div><Icon icon={mdiCameraIris} size="24" /></div> <div><Icon icon={mdiCamera} size="24" /></div>
<div> <div>
{#if asset.exifInfo?.make || asset.exifInfo?.model} {#if asset.exifInfo?.make || asset.exifInfo?.model}
@@ -395,20 +396,34 @@
</p> </p>
{/if} {/if}
<div class="flex gap-2 text-sm">
{#if asset.exifInfo.exposureTime}
<p>{`${asset.exifInfo.exposureTime} s`}</p>
{/if}
{#if asset.exifInfo.iso}
<p>{`ISO ${asset.exifInfo.iso}`}</p>
{/if}
</div>
</div>
</div>
{/if}
{#if asset.exifInfo?.lensModel || asset.exifInfo?.fNumber || asset.exifInfo?.focalLength}
<div class="flex gap-4 py-4">
<div><Icon icon={mdiCameraIris} size="24" /></div>
<div>
{#if asset.exifInfo?.lensModel} {#if asset.exifInfo?.lensModel}
<div class="flex gap-2 text-sm"> <p>
<p> <a
<a href={resolve(`${AppRoute.SEARCH}?${getMetadataSearchQuery({ lensModel: asset.exifInfo.lensModel })}`)}
href={resolve( title="{$t('search_for')} {asset.exifInfo.lensModel}"
`${AppRoute.SEARCH}?${getMetadataSearchQuery({ lensModel: asset.exifInfo.lensModel })}`, class="hover:text-primary line-clamp-1"
)} >
title="{$t('search_for')} {asset.exifInfo.lensModel}" {asset.exifInfo.lensModel}
class="hover:text-primary line-clamp-1" </a>
> </p>
{asset.exifInfo.lensModel}
</a>
</p>
</div>
{/if} {/if}
<div class="flex gap-2 text-sm"> <div class="flex gap-2 text-sm">
@@ -416,19 +431,9 @@
<p>ƒ/{asset.exifInfo.fNumber.toLocaleString($locale)}</p> <p>ƒ/{asset.exifInfo.fNumber.toLocaleString($locale)}</p>
{/if} {/if}
{#if asset.exifInfo.exposureTime}
<p>{`${asset.exifInfo.exposureTime} s`}</p>
{/if}
{#if asset.exifInfo.focalLength} {#if asset.exifInfo.focalLength}
<p>{`${asset.exifInfo.focalLength.toLocaleString($locale)} mm`}</p> <p>{`${asset.exifInfo.focalLength.toLocaleString($locale)} mm`}</p>
{/if} {/if}
{#if asset.exifInfo.iso}
<p>
{`ISO ${asset.exifInfo.iso}`}
</p>
{/if}
</div> </div>
</div> </div>
</div> </div>
@@ -171,7 +171,6 @@
$effect(() => { $effect(() => {
if (assetFileUrl) { if (assetFileUrl) {
// this can't be in an async context with $effect
void cast(assetFileUrl); void cast(assetFileUrl);
} }
}); });
@@ -43,7 +43,9 @@
let videoPlayer: HTMLVideoElement | undefined = $state(); let videoPlayer: HTMLVideoElement | undefined = $state();
let isLoading = $state(true); let isLoading = $state(true);
let assetFileUrl = $state(''); let assetFileUrl = $derived(
playOriginalVideo ? getAssetOriginalUrl({ id: assetId, cacheKey }) : getAssetPlaybackUrl({ id: assetId, cacheKey }),
);
let isScrubbing = $state(false); let isScrubbing = $state(false);
let showVideo = $state(false); let showVideo = $state(false);
@@ -53,11 +55,9 @@
}); });
$effect(() => { $effect(() => {
assetFileUrl = playOriginalVideo // reactive on `assetFileUrl` changes
? getAssetOriginalUrl({ id: assetId, cacheKey }) if (assetFileUrl) {
: getAssetPlaybackUrl({ id: assetId, cacheKey }); videoPlayer?.load();
if (videoPlayer) {
videoPlayer.load();
} }
}); });
@@ -35,7 +35,6 @@
$effect(() => { $effect(() => {
if (assetFileUrl) { if (assetFileUrl) {
// this can't be in an async context with $effect
void cast(assetFileUrl); void cast(assetFileUrl);
} }
}); });
@@ -268,7 +268,7 @@
<!-- Favorite asset star --> <!-- Favorite asset star -->
{#if !authManager.isSharedLink && asset.isFavorite} {#if !authManager.isSharedLink && asset.isFavorite}
<div class="absolute bottom-2 start-2"> <div class="absolute bottom-2 start-2">
<Icon icon={mdiHeart} size="24" class="text-white" /> <Icon data-icon-favorite icon={mdiHeart} size="24" class="text-white" />
</div> </div>
{/if} {/if}
@@ -282,14 +282,14 @@
{#if !authManager.isSharedLink && showArchiveIcon && asset.visibility === AssetVisibility.Archive} {#if !authManager.isSharedLink && showArchiveIcon && asset.visibility === AssetVisibility.Archive}
<div class={['absolute start-2', asset.isFavorite ? 'bottom-10' : 'bottom-2']}> <div class={['absolute start-2', asset.isFavorite ? 'bottom-10' : 'bottom-2']}>
<Icon icon={mdiArchiveArrowDownOutline} size="24" class="text-white" /> <Icon data-icon-archive icon={mdiArchiveArrowDownOutline} size="24" class="text-white" />
</div> </div>
{/if} {/if}
{#if asset.isImage && asset.projectionType === ProjectionType.EQUIRECTANGULAR} {#if asset.isImage && asset.projectionType === ProjectionType.EQUIRECTANGULAR}
<div class="absolute end-0 top-0 flex place-items-center gap-1 text-xs font-medium text-white"> <div class="absolute end-0 top-0 flex place-items-center gap-1 text-xs font-medium text-white">
<span class="pe-2 pt-2"> <span class="pe-2 pt-2">
<Icon icon={mdiRotate360} size="24" /> <Icon data-icon-equirectangular icon={mdiRotate360} size="24" />
</span> </span>
</div> </div>
{/if} {/if}
@@ -297,7 +297,7 @@
{#if asset.isImage && asset.duration && !asset.duration.includes('0:00:00.000')} {#if asset.isImage && asset.duration && !asset.duration.includes('0:00:00.000')}
<div class="absolute end-0 top-0 flex place-items-center gap-1 text-xs font-medium text-white"> <div class="absolute end-0 top-0 flex place-items-center gap-1 text-xs font-medium text-white">
<span class="pe-2 pt-2"> <span class="pe-2 pt-2">
<Icon icon={mdiFileGifBox} size="24" /> <Icon data-icon-playable icon={mdiFileGifBox} size="24" />
</span> </span>
</div> </div>
{/if} {/if}
@@ -312,7 +312,7 @@
> >
<span class="pe-2 pt-2 flex place-items-center gap-1"> <span class="pe-2 pt-2 flex place-items-center gap-1">
<p>{asset.stack.assetCount.toLocaleString($locale)}</p> <p>{asset.stack.assetCount.toLocaleString($locale)}</p>
<Icon icon={mdiCameraBurst} size="24" /> <Icon data-icon-stack icon={mdiCameraBurst} size="24" />
</span> </span>
</div> </div>
{/if} {/if}
@@ -378,7 +378,7 @@
/> />
<div class="absolute end-0 top-0 flex place-items-center gap-1 text-xs font-medium text-white"> <div class="absolute end-0 top-0 flex place-items-center gap-1 text-xs font-medium text-white">
<span class="pe-2 pt-2"> <span class="pe-2 pt-2">
<Icon icon={mdiMotionPauseOutline} size="24" /> <Icon data-icon-playable-pause icon={mdiMotionPauseOutline} size="24" />
</span> </span>
</div> </div>
</div> </div>
@@ -418,13 +418,13 @@
{disabled} {disabled}
> >
{#if disabled} {#if disabled}
<Icon icon={mdiCheckCircle} size="24" class="text-zinc-800" /> <Icon data-icon-select icon={mdiCheckCircle} size="24" class="text-zinc-800" />
{:else if selected} {:else if selected}
<div class="rounded-full bg-[#D9DCEF] dark:bg-[#232932]"> <div class="rounded-full bg-[#D9DCEF] dark:bg-[#232932]">
<Icon icon={mdiCheckCircle} size="24" class="text-primary" /> <Icon data-icon-select icon={mdiCheckCircle} size="24" class="text-primary" />
</div> </div>
{:else} {:else}
<Icon icon={mdiCheckCircle} size="24" class="text-white/80 hover:text-white" /> <Icon data-icon-select icon={mdiCheckCircle} size="24" class="text-white/80 hover:text-white" />
{/if} {/if}
</button> </button>
{/if} {/if}
+2 -2
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import Badge from '$lib/elements/Badge.svelte'; import Badge from '$lib/elements/Badge.svelte';
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import { QueueCommand, type QueueCommandDto, type QueueStatisticsDto, type QueueStatusDto } from '@immich/sdk'; import { QueueCommand, type QueueCommandDto, type QueueStatisticsDto, type QueueStatusLegacyDto } from '@immich/sdk';
import { Icon, IconButton } from '@immich/ui'; import { Icon, IconButton } from '@immich/ui';
import { import {
mdiAlertCircle, mdiAlertCircle,
@@ -23,7 +23,7 @@
subtitle: string | undefined; subtitle: string | undefined;
description: Component | undefined; description: Component | undefined;
statistics: QueueStatisticsDto; statistics: QueueStatisticsDto;
queueStatus: QueueStatusDto; queueStatus: QueueStatusLegacyDto;
icon: string; icon: string;
disabled?: boolean; disabled?: boolean;
allText: string | undefined; allText: string | undefined;
+2 -2
View File
@@ -6,7 +6,7 @@
QueueCommand, QueueCommand,
type QueueCommandDto, type QueueCommandDto,
QueueName, QueueName,
type QueuesResponseDto, type QueuesResponseLegacyDto,
runQueueCommandLegacy, runQueueCommandLegacy,
} from '@immich/sdk'; } from '@immich/sdk';
import { modalManager, toastManager } from '@immich/ui'; import { modalManager, toastManager } from '@immich/ui';
@@ -29,7 +29,7 @@
import StorageMigrationDescription from './StorageMigrationDescription.svelte'; import StorageMigrationDescription from './StorageMigrationDescription.svelte';
interface Props { interface Props {
jobs: QueuesResponseDto; jobs: QueuesResponseLegacyDto;
} }
let { jobs = $bindable() }: Props = $props(); let { jobs = $bindable() }: Props = $props();
@@ -4,16 +4,16 @@
import NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte'; import NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
import AdminSidebar from '$lib/sidebars/AdminSidebar.svelte'; import AdminSidebar from '$lib/sidebars/AdminSidebar.svelte';
import { sidebarStore } from '$lib/stores/sidebar.svelte'; import { sidebarStore } from '$lib/stores/sidebar.svelte';
import { AppShell, AppShellHeader, AppShellSidebar, Scrollable } from '@immich/ui'; import { AppShell, AppShellHeader, AppShellSidebar, Scrollable, type BreadcrumbItem } from '@immich/ui';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
type Props = { type Props = {
title: string; breadcrumbs: BreadcrumbItem[];
buttons?: Snippet; buttons?: Snippet;
children?: Snippet; children?: Snippet;
}; };
let { title, buttons, children }: Props = $props(); let { breadcrumbs, buttons, children }: Props = $props();
</script> </script>
<AppShell> <AppShell>
@@ -24,7 +24,7 @@
<AdminSidebar /> <AdminSidebar />
</AppShellSidebar> </AppShellSidebar>
<TitleLayout {title} {buttons}> <TitleLayout {breadcrumbs} {buttons}>
<Scrollable class="grow"> <Scrollable class="grow">
<PageContent> <PageContent>
{@render children?.()} {@render children?.()}
@@ -1,26 +1,20 @@
<script lang="ts"> <script lang="ts">
import { Text } from '@immich/ui'; import { Breadcrumbs, type BreadcrumbItem } from '@immich/ui';
import { mdiSlashForward } from '@mdi/js';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
interface Props { type Props = {
id?: string; breadcrumbs: BreadcrumbItem[];
title?: string;
description?: string;
buttons?: Snippet; buttons?: Snippet;
children?: Snippet; children?: Snippet;
} };
let { id, title, description, buttons, children }: Props = $props(); let { breadcrumbs, buttons, children }: Props = $props();
</script> </script>
<div class="h-full flex flex-col"> <div class="h-full flex flex-col">
<div class="flex h-16 w-full place-items-center justify-between border-b p-2"> <div class="flex h-16 w-full place-items-center justify-between border-b p-2">
<div class="flex gap-1"> <Breadcrumbs items={breadcrumbs} separator={mdiSlashForward} />
<div class="font-medium outline-none" tabindex="-1" {id}>{title}</div>
{#if description}
<Text color="muted">{description}</Text>
{/if}
</div>
{@render buttons?.()} {@render buttons?.()}
</div> </div>
{@render children?.()} {@render children?.()}
@@ -9,7 +9,6 @@
import { type PlacesGroup, getSelectedPlacesGroupOption } from '$lib/utils/places-utils'; import { type PlacesGroup, getSelectedPlacesGroupOption } from '$lib/utils/places-utils';
import { Icon } from '@immich/ui'; import { Icon } from '@immich/ui';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { run } from 'svelte/legacy';
interface Props { interface Props {
places?: AssetResponseDto[]; places?: AssetResponseDto[];
@@ -70,39 +69,27 @@
}, },
}; };
let filteredPlaces: AssetResponseDto[] = $state([]); const filteredPlaces = $derived.by(() => {
let groupedPlaces: PlacesGroup[] = $state([]); const searchQueryNormalized = normalizeSearchString(searchQuery);
return searchQueryNormalized
? places.filter((place) => normalizeSearchString(place.exifInfo?.city ?? '').includes(searchQueryNormalized))
: places;
});
let placesGroupOption: string = $state(PlacesGroupBy.None); const placesGroupOption: string = $derived(getSelectedPlacesGroupOption(userSettings));
const groupingFunction = $derived(groupOptions[placesGroupOption] ?? groupOptions[PlacesGroupBy.None]);
let hasPlaces = $derived(places.length > 0); const groupedPlaces: PlacesGroup[] = $derived(groupingFunction(filteredPlaces));
// Step 1: Filter using the given search query.
run(() => {
if (searchQuery) {
const searchQueryNormalized = normalizeSearchString(searchQuery);
filteredPlaces = places.filter((place) => {
return normalizeSearchString(place.exifInfo?.city ?? '').includes(searchQueryNormalized);
});
} else {
filteredPlaces = places;
}
$effect(() => {
searchResultCount = filteredPlaces.length; searchResultCount = filteredPlaces.length;
}); });
// Step 2: Group places. $effect(() => {
run(() => {
placesGroupOption = getSelectedPlacesGroupOption(userSettings);
const groupFunc = groupOptions[placesGroupOption] ?? groupOptions[PlacesGroupBy.None];
groupedPlaces = groupFunc(filteredPlaces);
placesGroupIds = groupedPlaces.map(({ id }) => id); placesGroupIds = groupedPlaces.map(({ id }) => id);
}); });
</script> </script>
{#if hasPlaces} {#if places.length > 0}
<!-- Album Cards --> <!-- Album Cards -->
{#if placesGroupOption === PlacesGroupBy.None} {#if placesGroupOption === PlacesGroupBy.None}
<PlacesCardGroup places={groupedPlaces[0].places} /> <PlacesCardGroup places={groupedPlaces[0].places} />
@@ -7,20 +7,11 @@
import { mdiCameraIris, mdiChartPie, mdiPlayCircle } from '@mdi/js'; import { mdiCameraIris, mdiChartPie, mdiPlayCircle } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
interface Props { type Props = {
stats?: ServerStatsResponseDto; stats: ServerStatsResponseDto;
} };
let { const { stats }: Props = $props();
stats = {
photos: 0,
videos: 0,
usage: 0,
usagePhotos: 0,
usageVideos: 0,
usageByUser: [],
},
}: Props = $props();
const zeros = (value: number) => { const zeros = (value: number) => {
const maxLength = 13; const maxLength = 13;
@@ -27,7 +27,7 @@
let { asset = undefined, point: initialPoint, onClose }: Props = $props(); let { asset = undefined, point: initialPoint, onClose }: Props = $props();
let places: PlacesResponseDto[] = $state([]); let places: PlacesResponseDto[] = $state([]);
let suggestedPlaces: PlacesResponseDto[] = $state([]); let suggestedPlaces: PlacesResponseDto[] = $derived(places.slice(0, 5));
let searchWord: string = $state(''); let searchWord: string = $state('');
let latestSearchTimeout: number; let latestSearchTimeout: number;
let showLoadingSpinner = $state(false); let showLoadingSpinner = $state(false);
@@ -52,9 +52,6 @@
}); });
$effect(() => { $effect(() => {
if (places) {
suggestedPlaces = places.slice(0, 5);
}
if (searchWord === '') { if (searchWord === '') {
suggestedPlaces = []; suggestedPlaces = [];
} }
@@ -33,37 +33,36 @@
children, children,
}: Props = $props(); }: Props = $props();
let left: number = $state(0); const swap = (direction: string) => (direction === 'left' ? 'right' : 'left');
let top: number = $state(0);
const layoutDirection = $derived(languageManager.rtl ? swap(direction) : direction);
const position = $derived.by(() => {
if (!menuElement) {
return { left: 0, top: 0 };
}
const rect = menuElement.getBoundingClientRect();
const directionWidth = layoutDirection === 'left' ? rect.width : 0;
const menuHeight = Math.min(menuElement.clientHeight, height) || 0;
const left = Math.max(8, Math.min(window.innerWidth - rect.width, x - directionWidth));
const top = Math.max(8, Math.min(window.innerHeight - menuHeight, y));
return { left, top };
});
// We need to bind clientHeight since the bounding box may return a height // We need to bind clientHeight since the bounding box may return a height
// of zero when starting the 'slide' animation. // of zero when starting the 'slide' animation.
let height: number = $state(0); let height: number = $state(0);
let isTransitioned = $state(false); let isTransitioned = $state(false);
$effect(() => {
if (menuElement) {
let layoutDirection = direction;
if (languageManager.rtl) {
layoutDirection = direction === 'left' ? 'right' : 'left';
}
const rect = menuElement.getBoundingClientRect();
const directionWidth = layoutDirection === 'left' ? rect.width : 0;
const menuHeight = Math.min(menuElement.clientHeight, height) || 0;
left = Math.max(8, Math.min(window.innerWidth - rect.width, x - directionWidth));
top = Math.max(8, Math.min(window.innerHeight - menuHeight, y));
}
});
</script> </script>
<div <div
bind:clientHeight={height} bind:clientHeight={height}
class="fixed min-w-50 w-max max-w-75 overflow-hidden rounded-lg shadow-lg z-1" class="fixed min-w-50 w-max max-w-75 overflow-hidden rounded-lg shadow-lg z-1"
style:left="{left}px" style:left="{position.left}px"
style:top="{top}px" style:top="{position.top}px"
transition:slide={{ duration: 250, easing: quintOut }} transition:slide={{ duration: 250, easing: quintOut }}
use:clickOutside={{ onOutclick: onClose }} use:clickOutside={{ onOutclick: onClose }}
onintroend={() => { onintroend={() => {
@@ -64,10 +64,11 @@
} }
} }
let makeFilter = $derived(filters.make); const makeFilter = $derived(filters.make);
let modelFilter = $derived(filters.model); const modelFilter = $derived(filters.model);
let lensModelFilter = $derived(filters.lensModel); const lensModelFilter = $derived(filters.lensModel);
// TODO replace by async $derived, at the latest when it's in stable https://svelte.dev/docs/svelte/await-expressions
$effect(() => { $effect(() => {
handlePromiseError(updateMakes()); handlePromiseError(updateMakes());
}); });
@@ -7,11 +7,10 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { run } from 'svelte/legacy';
import Combobox, { asComboboxOptions, asSelectedOption } from '$lib/components/shared-components/combobox.svelte'; import Combobox, { asComboboxOptions, asSelectedOption } from '$lib/components/shared-components/combobox.svelte';
import { handlePromiseError } from '$lib/utils'; import { handlePromiseError } from '$lib/utils';
import { getSearchSuggestions, SearchSuggestionType } from '@immich/sdk'; import { getSearchSuggestions, SearchSuggestionType } from '@immich/sdk';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
interface Props { interface Props {
@@ -66,15 +65,12 @@
} }
let countryFilter = $derived(filters.country); let countryFilter = $derived(filters.country);
let stateFilter = $derived(filters.state); let stateFilter = $derived(filters.state);
run(() => {
handlePromiseError(updateCountries()); // TODO replace by async $derived, at the latest when it's in stable https://svelte.dev/docs/svelte/await-expressions
}); $effect(() => handlePromiseError(updateStates(countryFilter)));
run(() => { $effect(() => handlePromiseError(updateCities(countryFilter, stateFilter)));
handlePromiseError(updateStates(countryFilter));
}); onMount(() => updateCountries());
run(() => {
handlePromiseError(updateCities(countryFilter, stateFilter));
});
</script> </script>
<div id="location-selection"> <div id="location-selection">
@@ -15,7 +15,7 @@
let { sharedLink, preload = false, class: className = '' }: Props = $props(); let { sharedLink, preload = false, class: className = '' }: Props = $props();
</script> </script>
<div class="relative shrink-0 size-24"> <div class="relative shrink-0 size-22">
{#if sharedLink?.album} {#if sharedLink?.album}
<AlbumCover album={sharedLink.album} class={className} {preload} /> <AlbumCover album={sharedLink.album} class={className} {preload} />
{:else if sharedLink.assets[0]} {:else if sharedLink.assets[0]}
@@ -2,10 +2,10 @@
import ActionButton from '$lib/components/ActionButton.svelte'; import ActionButton from '$lib/components/ActionButton.svelte';
import ShareCover from '$lib/components/sharedlinks-page/covers/share-cover.svelte'; import ShareCover from '$lib/components/sharedlinks-page/covers/share-cover.svelte';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import Badge from '$lib/elements/Badge.svelte';
import { getSharedLinkActions } from '$lib/services/shared-link.service'; import { getSharedLinkActions } from '$lib/services/shared-link.service';
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import { SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk'; import { SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk';
import { Badge, ContextMenuButton, MenuItemType, Text } from '@immich/ui';
import { DateTime, type ToRelativeUnit } from 'luxon'; import { DateTime, type ToRelativeUnit } from 'luxon';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@@ -31,7 +31,7 @@
} }
}; };
const SharedLinkActions = $derived(getSharedLinkActions($t, sharedLink)); const { Edit, Copy, Delete } = $derived(getSharedLinkActions($t, sharedLink));
</script> </script>
<div <div
@@ -44,64 +44,65 @@
> >
<ShareCover class="transition-all duration-300 hover:shadow-lg" {sharedLink} /> <ShareCover class="transition-all duration-300 hover:shadow-lg" {sharedLink} />
<div class="flex flex-col justify-between"> <div class="flex flex-col gap-2">
<div class="info-top"> <Text size="large" color="primary" class="flex place-items-center gap-2 break-all">
<div class="font-mono text-xs font-semibold text-gray-500 dark:text-gray-400"> {#if sharedLink.type === SharedLinkType.Album}
{#if isExpired} {sharedLink.album?.albumName}
<p class="font-bold text-red-600 dark:text-red-400">{$t('expired')}</p> {:else if sharedLink.type === SharedLinkType.Individual}
{:else if expiresAt} {$t('individual_share')}
<p> {/if}
{$t('expires_date', { values: { date: getCountDownExpirationDate(expiresAt, now) } })} </Text>
</p>
{:else}
<p>{$t('expires_date', { values: { date: '∞' } })}</p>
{/if}
</div>
<div class="text-sm pb-2"> <div class="flex flex-wrap gap-1">
<p class="flex place-items-center gap-2 text-primary break-all uppercase"> {#if isExpired}
{#if sharedLink.type === SharedLinkType.Album} <Badge size="small" color="danger">{$t('expired')}</Badge>
{sharedLink.album?.albumName} {:else if expiresAt}
{:else if sharedLink.type === SharedLinkType.Individual} <Badge size="small" color="secondary">
{$t('individual_share')} {$t('expires_date', { values: { date: getCountDownExpirationDate(expiresAt, now) } })}
{/if} </Badge>
</p> {:else}
<Badge size="small" color="secondary">{$t('expires_date', { values: { date: '∞' } })}</Badge>
<p class="text-sm">{sharedLink.description ?? ''}</p>
</div>
</div>
<div class="flex flex-wrap gap-2 text-xl">
{#if sharedLink.allowUpload}
<Badge rounded="full"><span class="text-xs px-1">{$t('upload')}</span></Badge>
{/if} {/if}
{#if sharedLink.allowDownload} {#if sharedLink.slug}
<Badge rounded="full"><span class="text-xs px-1">{$t('download')}</span></Badge> <Badge size="small" color="secondary">{$t('custom_url')}</Badge>
{/if}
{#if sharedLink.allowUpload}
<Badge size="small" color="secondary">{$t('upload')}</Badge>
{/if}
{#if sharedLink.showMetadata && sharedLink.allowDownload}
<Badge size="small" color="secondary">{$t('download')}</Badge>
{/if} {/if}
{#if sharedLink.showMetadata} {#if sharedLink.showMetadata}
<Badge rounded="full"><span class="uppercase text-xs px-1">{$t('exif')}</span></Badge> <Badge size="small" color="secondary">{$t('exif')}</Badge>
{/if} {/if}
{#if sharedLink.password} {#if sharedLink.password}
<Badge rounded="full"><span class="text-xs px-1">{$t('password')}</span></Badge> <Badge size="small" color="secondary">{$t('password')}</Badge>
{/if}
{#if sharedLink.slug}
<Badge rounded="full"><span class="text-xs px-1">{$t('custom_url')}</span></Badge>
{/if} {/if}
</div> </div>
{#if sharedLink.description}
<Text size="small" class="line-clamp-1">{sharedLink.description}</Text>
{/if}
</div> </div>
</svelte:element> </svelte:element>
<div class="flex flex-auto flex-col place-content-center place-items-end text-end ms-4"> <div class="flex flex-auto flex-col place-content-center place-items-end text-end ms-4">
<div class="sm:flex hidden"> <div class="sm:flex hidden">
<ActionButton action={SharedLinkActions.Edit} /> <ActionButton action={Edit} />
<ActionButton action={SharedLinkActions.Copy} /> <ActionButton action={Copy} />
<ActionButton action={SharedLinkActions.Delete} /> <ActionButton action={Delete} />
</div> </div>
<div class="sm:hidden"> <div class="sm:hidden">
<ActionButton action={SharedLinkActions.ContextMenu} /> <ContextMenuButton
aria-label={$t('shared_link_options')}
position="top-right"
items={[Edit, Copy, MenuItemType.Divider, Delete]}
/>
</div> </div>
</div> </div>
</div> </div>

Some files were not shown because too many files have changed in this diff Show More