mirror of
https://github.com/immich-app/immich.git
synced 2026-03-16 07:08:38 -07:00
Compare commits
49 Commits
refactor/m
...
refactor/e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9a36f0391 | ||
|
|
d10dfc05cc | ||
|
|
0d5685a3fa | ||
|
|
4c33fbb5e0 | ||
|
|
13104d49cd | ||
|
|
2d5ec528d5 | ||
|
|
5226898184 | ||
|
|
dd4169876c | ||
|
|
8321c275b8 | ||
|
|
3d6c26350a | ||
|
|
db15e5e423 | ||
|
|
35d18da14a | ||
|
|
cb56a11f0b | ||
|
|
104fa09f69 | ||
|
|
66ae07ee39 | ||
|
|
939d2c8b27 | ||
|
|
2801a6e672 | ||
|
|
4742360469 | ||
|
|
b56fa62b32 | ||
|
|
ddbe485074 | ||
|
|
01310c6d86 | ||
|
|
512327ef69 | ||
|
|
8755cd59fd | ||
|
|
7694b342ed | ||
|
|
78553a0258 | ||
|
|
c1198b99b7 | ||
|
|
8b7b9ee394 | ||
|
|
d6b39a464d | ||
|
|
75d23fe135 | ||
|
|
c860809aa1 | ||
|
|
0498f6cb9d | ||
|
|
24e5dabb51 | ||
|
|
aecf064ec9 | ||
|
|
57be3ff8c7 | ||
|
|
99505f987e | ||
|
|
b687237d8f | ||
|
|
e75d9f5613 | ||
|
|
f353c99223 | ||
|
|
c0afde91a7 | ||
|
|
ff19cd0107 | ||
|
|
d227077543 | ||
|
|
6e0005acfd | ||
|
|
55a196bfa0 | ||
|
|
d0b49846dc | ||
|
|
8896b2dbf5 | ||
|
|
251e644b2a | ||
|
|
a02635f9a5 | ||
|
|
104f3dfcc3 | ||
|
|
b7e3b48a44 |
2
.github/.nvmrc
vendored
2
.github/.nvmrc
vendored
@@ -1 +1 @@
|
||||
24.11.0
|
||||
24.11.1
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -74,7 +74,7 @@ jobs:
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download APK
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: release-apk-signed
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
@@ -1 +1 @@
|
||||
24.11.0
|
||||
24.11.1
|
||||
|
||||
@@ -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
|
||||
COPY package* pnpm* .pnpmfile.cjs ./
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.19.1",
|
||||
"@types/node": "^24.10.1",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
@@ -69,6 +69,6 @@
|
||||
"micromatch": "^4.0.8"
|
||||
},
|
||||
"volta": {
|
||||
"node": "24.11.0"
|
||||
"node": "24.11.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ services:
|
||||
command: ['./run.sh', '-disable-reporting']
|
||||
ports:
|
||||
- 3000:3000
|
||||
image: grafana/grafana:12.2.1-ubuntu@sha256:797530c642f7b41ba7848c44cfda5e361ef1f3391a98bed1e5d448c472b6826a
|
||||
image: grafana/grafana:12.3.0-ubuntu@sha256:cee936306135e1925ab21dffa16f8a411535d16ab086bef2309339a8e74d62df
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
24.11.0
|
||||
24.11.1
|
||||
|
||||
@@ -14,15 +14,15 @@ When contributing code through a pull request, please check the following:
|
||||
- [ ] `pnpm run check:typescript` (check typescript)
|
||||
- [ ] `pnpm test` (unit tests)
|
||||
|
||||
:::tip AIO
|
||||
Run all web checks with `pnpm run check:all`
|
||||
:::
|
||||
|
||||
## Documentation
|
||||
|
||||
- [ ] `pnpm run format` (formatting via Prettier)
|
||||
- [ ] 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
|
||||
|
||||
- [ ] `pnpm run lint` (linting via ESLint)
|
||||
|
||||
@@ -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`.
|
||||
|
||||
`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.
|
||||
|
||||
|
||||
@@ -57,6 +57,6 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"volta": {
|
||||
"node": "24.11.0"
|
||||
"node": "24.11.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
24.11.0
|
||||
24.11.1
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@socket.io/component-emitter": "^3.1.2",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^22.19.1",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/oidc-provider": "^9.0.0",
|
||||
"@types/pg": "^8.15.1",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
@@ -54,6 +54,6 @@
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"volta": {
|
||||
"node": "24.11.0"
|
||||
"node": "24.11.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,50 +62,60 @@ export const setupTimelineMockApiRoutes = async (
|
||||
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 match = request.url().match(pattern);
|
||||
if (!match) {
|
||||
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,
|
||||
});
|
||||
if (!match?.groups) {
|
||||
throw new Error(`Invalid URL for thumbnail endpoint: ${request.url()}`);
|
||||
}
|
||||
if (match.groups?.size === 'preview') {
|
||||
|
||||
if (match.groups.size === 'preview') {
|
||||
if (!route.request().serviceWorker()) {
|
||||
return route.continue();
|
||||
}
|
||||
const asset = getAsset(timelineRestData, match.groups?.assetId);
|
||||
const asset = getAsset(timelineRestData, match.groups.assetId);
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
headers: { 'content-type': 'image/jpeg', ETag: 'abc123', 'Cache-Control': 'public, max-age=3600' },
|
||||
body: await randomPreview(
|
||||
match.groups?.assetId,
|
||||
match.groups.assetId,
|
||||
(asset?.exifInfo?.exifImageWidth ?? 0) / (asset?.exifInfo?.exifImageHeight ?? 1),
|
||||
),
|
||||
});
|
||||
}
|
||||
if (match.groups?.size === 'thumbnail') {
|
||||
if (match.groups.size === 'thumbnail') {
|
||||
if (!route.request().serviceWorker()) {
|
||||
return route.continue();
|
||||
}
|
||||
const asset = getAsset(timelineRestData, match.groups?.assetId);
|
||||
const asset = getAsset(timelineRestData, match.groups.assetId);
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
headers: { 'content-type': 'image/jpeg' },
|
||||
body: await randomThumbnail(
|
||||
match.groups?.assetId,
|
||||
match.groups.assetId,
|
||||
(asset?.exifInfo?.exifImageWidth ?? 0) / (asset?.exifInfo?.exifImageHeight ?? 1),
|
||||
),
|
||||
});
|
||||
}
|
||||
return route.continue();
|
||||
});
|
||||
|
||||
await context.route('**/api/albums/**', async (route, request) => {
|
||||
const pattern = /\/api\/albums\/(?<albumId>[^/?]+)/;
|
||||
const match = request.url().match(pattern);
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
PersonCreateDto,
|
||||
QueueCommandDto,
|
||||
QueueName,
|
||||
QueuesResponseDto,
|
||||
QueuesResponseLegacyDto,
|
||||
SharedLinkCreateDto,
|
||||
UpdateLibraryDto,
|
||||
UserAdminCreateDto,
|
||||
@@ -564,13 +564,13 @@ export const utils = {
|
||||
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 jobCounts = queues[queue].jobCounts;
|
||||
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
|
||||
return new Promise<void>(async (resolve, reject) => {
|
||||
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 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 }) => {
|
||||
const album = timelineRestData.album;
|
||||
await pageUtils.openAlbumPage(page, album.id);
|
||||
@@ -633,8 +680,7 @@ test.describe('Timeline', () => {
|
||||
visibility: 'archive',
|
||||
ids: [assetToArchive.id],
|
||||
});
|
||||
console.log('Skipping assertion - TODO - fix that archiving in album doesnt add icon');
|
||||
// await thumbnail.expectThumbnailIsArchive(page, assetToArchive.id);
|
||||
await thumbnailUtils.expectThumbnailIsArchive(page, assetToArchive.id);
|
||||
await page.locator('#asset-selection-app-bar').getByLabel('Close').click();
|
||||
await page.getByRole('link').getByText('Archive').click();
|
||||
await timelineUtils.waitForTimelineLoad(page);
|
||||
@@ -656,8 +702,7 @@ test.describe('Timeline', () => {
|
||||
visibility: 'timeline',
|
||||
ids: [assetToArchive.id],
|
||||
});
|
||||
console.log('Skipping assertion - TODO - fix bug with not removing asset from timeline-manager after unarchive');
|
||||
// await expect(thumbnail.withAssetId(page, assetToArchive.id)).toHaveCount(0);
|
||||
await expect(thumbnailUtils.withAssetId(page, assetToArchive.id)).toHaveCount(0);
|
||||
await pageUtils.openAlbumPage(page, album.id);
|
||||
await thumbnailUtils.expectInViewport(page, assetToArchive.id);
|
||||
});
|
||||
@@ -712,6 +757,50 @@ test.describe('Timeline', () => {
|
||||
await page.getByText('Photos', { exact: true }).click();
|
||||
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 }) => {
|
||||
const album = timelineRestData.album;
|
||||
await pageUtils.openAlbumPage(page, album.id);
|
||||
|
||||
@@ -105,28 +105,21 @@ export const thumbnailUtils = {
|
||||
return await poll(page, () => thumbnailUtils.queryThumbnailInViewport(page, collector));
|
||||
},
|
||||
async expectThumbnailIsFavorite(page: Page, assetId: string) {
|
||||
await expect(
|
||||
thumbnailUtils
|
||||
.withAssetId(page, assetId)
|
||||
.locator(
|
||||
'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);
|
||||
await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-favorite]')).toHaveCount(1);
|
||||
},
|
||||
async expectThumbnailIsNotFavorite(page: Page, assetId: string) {
|
||||
await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-favorite]')).toHaveCount(0);
|
||||
},
|
||||
async expectThumbnailIsArchive(page: Page, assetId: string) {
|
||||
await expect(
|
||||
thumbnailUtils
|
||||
.withAssetId(page, assetId)
|
||||
.locator('path[d="M20 21H4V10H6V19H18V10H20V21M3 3H21V9H3V3M5 5V7H19V5M10.5 11V14H8L12 18L16 14H13.5V11"]'),
|
||||
).toHaveCount(1);
|
||||
await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-archive]')).toHaveCount(1);
|
||||
},
|
||||
async expectThumbnailIsNotArchive(page: Page, assetId: string) {
|
||||
await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-archive]')).toHaveCount(0);
|
||||
},
|
||||
async expectSelectedReadonly(page: Page, assetId: string) {
|
||||
// todo - need a data attribute for selected
|
||||
await expect(
|
||||
page.locator(
|
||||
`[data-thumbnail-focus-container][data-asset="${assetId}"] > .group.cursor-not-allowed > .rounded-xl`,
|
||||
),
|
||||
).toBeVisible();
|
||||
const container = thumbnailUtils.withAssetId(page, assetId);
|
||||
await expect(container.locator('[data-selected]')).toBeVisible();
|
||||
await expect(container.locator('[data-disabled]')).toBeVisible();
|
||||
},
|
||||
async expectTimelineHasOnScreenAssets(page: Page) {
|
||||
const first = await thumbnailUtils.getFirstInViewport(page);
|
||||
@@ -208,10 +201,18 @@ export const pageUtils = {
|
||||
await page.goto(`/photos`);
|
||||
await timelineUtils.waitForTimelineLoad(page);
|
||||
},
|
||||
async openFavorites(page: Page) {
|
||||
await page.goto(`/favorites`);
|
||||
await timelineUtils.waitForTimelineLoad(page);
|
||||
},
|
||||
async openAlbumPage(page: Page, albumId: string) {
|
||||
await page.goto(`/albums/${albumId}`);
|
||||
await timelineUtils.waitForTimelineLoad(page);
|
||||
},
|
||||
async openArchivePage(page: Page) {
|
||||
await page.goto(`/archive`);
|
||||
await timelineUtils.waitForTimelineLoad(page);
|
||||
},
|
||||
async deepLinkAlbumPage(page: Page, albumId: string, assetId: string) {
|
||||
await page.goto(`/albums/${albumId}?at=${assetId}`);
|
||||
await timelineUtils.waitForTimelineLoad(page);
|
||||
|
||||
@@ -54,7 +54,7 @@ test.describe('User Administration', () => {
|
||||
|
||||
await page.getByRole('button', { name: 'Edit' }).click();
|
||||
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 page.getByRole('button', { name: 'Confirm' }).click();
|
||||
|
||||
@@ -83,7 +83,7 @@ test.describe('User Administration', () => {
|
||||
|
||||
await page.getByRole('button', { name: 'Edit' }).click();
|
||||
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 page.getByRole('button', { name: 'Confirm' }).click();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
# renovate: datasource=github-releases depName=Microsoft/onnxruntime
|
||||
ARG ONNXRUNTIME_VERSION="v1.20.1"
|
||||
ARG ONNXRUNTIME_VERSION="v1.22.1"
|
||||
WORKDIR /code
|
||||
|
||||
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
|
||||
index d90a2a355..bb1a7de12 100644
|
||||
index 2714e6f59..a69da76b4 100644
|
||||
--- a/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()
|
||||
|
||||
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/*)
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
experimental_monorepo_root = true
|
||||
|
||||
[tools]
|
||||
node = "24.11.0"
|
||||
node = "24.11.1"
|
||||
flutter = "3.35.7"
|
||||
pnpm = "10.20.0"
|
||||
pnpm = "10.22.0"
|
||||
terragrunt = "0.91.2"
|
||||
opentofu = "1.10.6"
|
||||
java = "25.0.1"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"]
|
||||
version = "1.30.0"
|
||||
|
||||
@@ -75,6 +75,20 @@ class AssetService {
|
||||
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 orientedHeight = isFlipped ? width : height;
|
||||
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(
|
||||
id: id,
|
||||
name: name,
|
||||
checksum: null,
|
||||
type: AssetType.values.elementAtOrNull(type) ?? AssetType.other,
|
||||
createdAt: tryFromSecondsSinceEpoch(createdAt, isUtc: true) ?? DateTime.timestamp(),
|
||||
updatedAt: tryFromSecondsSinceEpoch(createdAt, isUtc: true) ?? DateTime.timestamp(),
|
||||
updatedAt: tryFromSecondsSinceEpoch(updatedAt, isUtc: true) ?? DateTime.timestamp(),
|
||||
width: width,
|
||||
height: height,
|
||||
durationInSeconds: durationInSeconds,
|
||||
|
||||
@@ -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/repositories/db.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/utils/datetime_helpers.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
// ignore: import_rule_photo_manager
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
const int targetVersion = 18;
|
||||
const int targetVersion = 19;
|
||||
|
||||
Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
|
||||
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);
|
||||
}
|
||||
|
||||
if (version < 19 && Store.isBetaTimelineEnabled) {
|
||||
if (!await _populateUpdatedAtTime(drift)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetVersion >= 12) {
|
||||
await Store.put(StoreKey.version, targetVersion);
|
||||
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 {
|
||||
try {
|
||||
final isarDeviceAssets = await db.deviceAssetEntitys.where().findAll();
|
||||
|
||||
17
mobile/openapi/README.md
generated
17
mobile/openapi/README.md
generated
@@ -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* | [**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* | [**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* | [**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* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | Retrieve download information
|
||||
*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
|
||||
*PluginsApi* | [**getPlugin**](doc//PluginsApi.md#getplugin) | **GET** /plugins/{id} | Retrieve a plugin
|
||||
*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* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | Retrieve explore data
|
||||
*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)
|
||||
- [ImageFormat](doc//ImageFormat.md)
|
||||
- [JobCreateDto](doc//JobCreateDto.md)
|
||||
- [JobName](doc//JobName.md)
|
||||
- [JobSettingsDto](doc//JobSettingsDto.md)
|
||||
- [LibraryResponseDto](doc//LibraryResponseDto.md)
|
||||
- [LibraryStatsResponseDto](doc//LibraryStatsResponseDto.md)
|
||||
@@ -465,11 +473,16 @@ Class | Method | HTTP request | Description
|
||||
- [PurchaseUpdate](doc//PurchaseUpdate.md)
|
||||
- [QueueCommand](doc//QueueCommand.md)
|
||||
- [QueueCommandDto](doc//QueueCommandDto.md)
|
||||
- [QueueDeleteDto](doc//QueueDeleteDto.md)
|
||||
- [QueueJobResponseDto](doc//QueueJobResponseDto.md)
|
||||
- [QueueJobStatus](doc//QueueJobStatus.md)
|
||||
- [QueueName](doc//QueueName.md)
|
||||
- [QueueResponseDto](doc//QueueResponseDto.md)
|
||||
- [QueueResponseLegacyDto](doc//QueueResponseLegacyDto.md)
|
||||
- [QueueStatisticsDto](doc//QueueStatisticsDto.md)
|
||||
- [QueueStatusDto](doc//QueueStatusDto.md)
|
||||
- [QueuesResponseDto](doc//QueuesResponseDto.md)
|
||||
- [QueueStatusLegacyDto](doc//QueueStatusLegacyDto.md)
|
||||
- [QueueUpdateDto](doc//QueueUpdateDto.md)
|
||||
- [QueuesResponseLegacyDto](doc//QueuesResponseLegacyDto.md)
|
||||
- [RandomSearchDto](doc//RandomSearchDto.md)
|
||||
- [RatingsResponse](doc//RatingsResponse.md)
|
||||
- [RatingsUpdate](doc//RatingsUpdate.md)
|
||||
|
||||
11
mobile/openapi/lib/api.dart
generated
11
mobile/openapi/lib/api.dart
generated
@@ -50,6 +50,7 @@ part 'api/notifications_admin_api.dart';
|
||||
part 'api/partners_api.dart';
|
||||
part 'api/people_api.dart';
|
||||
part 'api/plugins_api.dart';
|
||||
part 'api/queues_api.dart';
|
||||
part 'api/search_api.dart';
|
||||
part 'api/server_api.dart';
|
||||
part 'api/sessions_api.dart';
|
||||
@@ -154,6 +155,7 @@ part 'model/folders_response.dart';
|
||||
part 'model/folders_update.dart';
|
||||
part 'model/image_format.dart';
|
||||
part 'model/job_create_dto.dart';
|
||||
part 'model/job_name.dart';
|
||||
part 'model/job_settings_dto.dart';
|
||||
part 'model/library_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/queue_command.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_response_dto.dart';
|
||||
part 'model/queue_response_legacy_dto.dart';
|
||||
part 'model/queue_statistics_dto.dart';
|
||||
part 'model/queue_status_dto.dart';
|
||||
part 'model/queues_response_dto.dart';
|
||||
part 'model/queue_status_legacy_dto.dart';
|
||||
part 'model/queue_update_dto.dart';
|
||||
part 'model/queues_response_legacy_dto.dart';
|
||||
part 'model/random_search_dto.dart';
|
||||
part 'model/ratings_response.dart';
|
||||
part 'model/ratings_update.dart';
|
||||
|
||||
109
mobile/openapi/lib/api/deprecated_api.dart
generated
109
mobile/openapi/lib/api/deprecated_api.dart
generated
@@ -248,6 +248,54 @@ class DeprecatedApi {
|
||||
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
|
||||
///
|
||||
/// Retrieve a specified number of random assets for the authenticated user.
|
||||
@@ -444,4 +492,65 @@ class DeprecatedApi {
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
8
mobile/openapi/lib/api/jobs_api.dart
generated
8
mobile/openapi/lib/api/jobs_api.dart
generated
@@ -97,7 +97,7 @@ class JobsApi {
|
||||
/// Retrieve queue counts and 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();
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
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"
|
||||
// FormatException when trying to decode an empty string.
|
||||
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;
|
||||
@@ -158,7 +158,7 @@ class JobsApi {
|
||||
/// * [QueueName] name (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,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
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"
|
||||
// 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 await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'QueueResponseLegacyDto',) as QueueResponseLegacyDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
|
||||
308
mobile/openapi/lib/api/queues_api.dart
generated
Normal file
308
mobile/openapi/lib/api/queues_api.dart
generated
Normal 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;
|
||||
}
|
||||
}
|
||||
20
mobile/openapi/lib/api_client.dart
generated
20
mobile/openapi/lib/api_client.dart
generated
@@ -358,6 +358,8 @@ class ApiClient {
|
||||
return ImageFormatTypeTransformer().decode(value);
|
||||
case 'JobCreateDto':
|
||||
return JobCreateDto.fromJson(value);
|
||||
case 'JobName':
|
||||
return JobNameTypeTransformer().decode(value);
|
||||
case 'JobSettingsDto':
|
||||
return JobSettingsDto.fromJson(value);
|
||||
case 'LibraryResponseDto':
|
||||
@@ -496,16 +498,26 @@ class ApiClient {
|
||||
return QueueCommandTypeTransformer().decode(value);
|
||||
case 'QueueCommandDto':
|
||||
return QueueCommandDto.fromJson(value);
|
||||
case 'QueueDeleteDto':
|
||||
return QueueDeleteDto.fromJson(value);
|
||||
case 'QueueJobResponseDto':
|
||||
return QueueJobResponseDto.fromJson(value);
|
||||
case 'QueueJobStatus':
|
||||
return QueueJobStatusTypeTransformer().decode(value);
|
||||
case 'QueueName':
|
||||
return QueueNameTypeTransformer().decode(value);
|
||||
case 'QueueResponseDto':
|
||||
return QueueResponseDto.fromJson(value);
|
||||
case 'QueueResponseLegacyDto':
|
||||
return QueueResponseLegacyDto.fromJson(value);
|
||||
case 'QueueStatisticsDto':
|
||||
return QueueStatisticsDto.fromJson(value);
|
||||
case 'QueueStatusDto':
|
||||
return QueueStatusDto.fromJson(value);
|
||||
case 'QueuesResponseDto':
|
||||
return QueuesResponseDto.fromJson(value);
|
||||
case 'QueueStatusLegacyDto':
|
||||
return QueueStatusLegacyDto.fromJson(value);
|
||||
case 'QueueUpdateDto':
|
||||
return QueueUpdateDto.fromJson(value);
|
||||
case 'QueuesResponseLegacyDto':
|
||||
return QueuesResponseLegacyDto.fromJson(value);
|
||||
case 'RandomSearchDto':
|
||||
return RandomSearchDto.fromJson(value);
|
||||
case 'RatingsResponse':
|
||||
|
||||
6
mobile/openapi/lib/api_helper.dart
generated
6
mobile/openapi/lib/api_helper.dart
generated
@@ -94,6 +94,9 @@ String parameterToString(dynamic value) {
|
||||
if (value is ImageFormat) {
|
||||
return ImageFormatTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is JobName) {
|
||||
return JobNameTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is LogLevel) {
|
||||
return LogLevelTypeTransformer().encode(value).toString();
|
||||
}
|
||||
@@ -133,6 +136,9 @@ String parameterToString(dynamic value) {
|
||||
if (value is QueueCommand) {
|
||||
return QueueCommandTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is QueueJobStatus) {
|
||||
return QueueJobStatusTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is QueueName) {
|
||||
return QueueNameTypeTransformer().encode(value).toString();
|
||||
}
|
||||
|
||||
244
mobile/openapi/lib/model/job_name.dart
generated
Normal file
244
mobile/openapi/lib/model/job_name.dart
generated
Normal 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
mobile/openapi/lib/model/permission.dart
generated
18
mobile/openapi/lib/model/permission.dart
generated
@@ -152,6 +152,12 @@ class Permission {
|
||||
static const userProfileImagePeriodRead = Permission._(r'userProfileImage.read');
|
||||
static const userProfileImagePeriodUpdate = Permission._(r'userProfileImage.update');
|
||||
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 workflowPeriodRead = Permission._(r'workflow.read');
|
||||
static const workflowPeriodUpdate = Permission._(r'workflow.update');
|
||||
@@ -294,6 +300,12 @@ class Permission {
|
||||
userProfileImagePeriodRead,
|
||||
userProfileImagePeriodUpdate,
|
||||
userProfileImagePeriodDelete,
|
||||
queuePeriodRead,
|
||||
queuePeriodUpdate,
|
||||
queueJobPeriodCreate,
|
||||
queueJobPeriodRead,
|
||||
queueJobPeriodUpdate,
|
||||
queueJobPeriodDelete,
|
||||
workflowPeriodCreate,
|
||||
workflowPeriodRead,
|
||||
workflowPeriodUpdate,
|
||||
@@ -471,6 +483,12 @@ class PermissionTypeTransformer {
|
||||
case r'userProfileImage.read': return Permission.userProfileImagePeriodRead;
|
||||
case r'userProfileImage.update': return Permission.userProfileImagePeriodUpdate;
|
||||
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.read': return Permission.workflowPeriodRead;
|
||||
case r'workflow.update': return Permission.workflowPeriodUpdate;
|
||||
|
||||
109
mobile/openapi/lib/model/queue_delete_dto.dart
generated
Normal file
109
mobile/openapi/lib/model/queue_delete_dto.dart
generated
Normal 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
mobile/openapi/lib/model/queue_job_response_dto.dart
generated
Normal file
132
mobile/openapi/lib/model/queue_job_response_dto.dart
generated
Normal 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
mobile/openapi/lib/model/queue_job_status.dart
generated
Normal file
97
mobile/openapi/lib/model/queue_job_status.dart
generated
Normal 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;
|
||||
}
|
||||
|
||||
38
mobile/openapi/lib/model/queue_response_dto.dart
generated
38
mobile/openapi/lib/model/queue_response_dto.dart
generated
@@ -13,32 +13,38 @@ part of openapi.api;
|
||||
class QueueResponseDto {
|
||||
/// Returns a new [QueueResponseDto] instance.
|
||||
QueueResponseDto({
|
||||
required this.jobCounts,
|
||||
required this.queueStatus,
|
||||
required this.isPaused,
|
||||
required this.name,
|
||||
required this.statistics,
|
||||
});
|
||||
|
||||
QueueStatisticsDto jobCounts;
|
||||
bool isPaused;
|
||||
|
||||
QueueStatusDto queueStatus;
|
||||
QueueName name;
|
||||
|
||||
QueueStatisticsDto statistics;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is QueueResponseDto &&
|
||||
other.jobCounts == jobCounts &&
|
||||
other.queueStatus == queueStatus;
|
||||
other.isPaused == isPaused &&
|
||||
other.name == name &&
|
||||
other.statistics == statistics;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(jobCounts.hashCode) +
|
||||
(queueStatus.hashCode);
|
||||
(isPaused.hashCode) +
|
||||
(name.hashCode) +
|
||||
(statistics.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'QueueResponseDto[jobCounts=$jobCounts, queueStatus=$queueStatus]';
|
||||
String toString() => 'QueueResponseDto[isPaused=$isPaused, name=$name, statistics=$statistics]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'jobCounts'] = this.jobCounts;
|
||||
json[r'queueStatus'] = this.queueStatus;
|
||||
json[r'isPaused'] = this.isPaused;
|
||||
json[r'name'] = this.name;
|
||||
json[r'statistics'] = this.statistics;
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -51,8 +57,9 @@ class QueueResponseDto {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return QueueResponseDto(
|
||||
jobCounts: QueueStatisticsDto.fromJson(json[r'jobCounts'])!,
|
||||
queueStatus: QueueStatusDto.fromJson(json[r'queueStatus'])!,
|
||||
isPaused: mapValueOfType<bool>(json, r'isPaused')!,
|
||||
name: QueueName.fromJson(json[r'name'])!,
|
||||
statistics: QueueStatisticsDto.fromJson(json[r'statistics'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@@ -100,8 +107,9 @@ class QueueResponseDto {
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'jobCounts',
|
||||
'queueStatus',
|
||||
'isPaused',
|
||||
'name',
|
||||
'statistics',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
107
mobile/openapi/lib/model/queue_response_legacy_dto.dart
generated
Normal file
107
mobile/openapi/lib/model/queue_response_legacy_dto.dart
generated
Normal 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;
|
||||
|
||||
class QueueStatusDto {
|
||||
/// Returns a new [QueueStatusDto] instance.
|
||||
QueueStatusDto({
|
||||
class QueueStatusLegacyDto {
|
||||
/// Returns a new [QueueStatusLegacyDto] instance.
|
||||
QueueStatusLegacyDto({
|
||||
required this.isActive,
|
||||
required this.isPaused,
|
||||
});
|
||||
@@ -22,7 +22,7 @@ class QueueStatusDto {
|
||||
bool isPaused;
|
||||
|
||||
@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.isPaused == isPaused;
|
||||
|
||||
@@ -33,7 +33,7 @@ class QueueStatusDto {
|
||||
(isPaused.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'QueueStatusDto[isActive=$isActive, isPaused=$isPaused]';
|
||||
String toString() => 'QueueStatusLegacyDto[isActive=$isActive, isPaused=$isPaused]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -42,15 +42,15 @@ class QueueStatusDto {
|
||||
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.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static QueueStatusDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "QueueStatusDto");
|
||||
static QueueStatusLegacyDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "QueueStatusLegacyDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return QueueStatusDto(
|
||||
return QueueStatusLegacyDto(
|
||||
isActive: mapValueOfType<bool>(json, r'isActive')!,
|
||||
isPaused: mapValueOfType<bool>(json, r'isPaused')!,
|
||||
);
|
||||
@@ -58,11 +58,11 @@ class QueueStatusDto {
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<QueueStatusDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <QueueStatusDto>[];
|
||||
static List<QueueStatusLegacyDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <QueueStatusLegacyDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = QueueStatusDto.fromJson(row);
|
||||
final value = QueueStatusLegacyDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
@@ -71,12 +71,12 @@ class QueueStatusDto {
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, QueueStatusDto> mapFromJson(dynamic json) {
|
||||
final map = <String, QueueStatusDto>{};
|
||||
static Map<String, QueueStatusLegacyDto> mapFromJson(dynamic json) {
|
||||
final map = <String, QueueStatusLegacyDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = QueueStatusDto.fromJson(entry.value);
|
||||
final value = QueueStatusLegacyDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
@@ -85,14 +85,14 @@ class QueueStatusDto {
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of QueueStatusDto-objects as value to a dart map
|
||||
static Map<String, List<QueueStatusDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<QueueStatusDto>>{};
|
||||
// maps a json object with a list of QueueStatusLegacyDto-objects as value to a dart map
|
||||
static Map<String, List<QueueStatusLegacyDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<QueueStatusLegacyDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
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;
|
||||
108
mobile/openapi/lib/model/queue_update_dto.dart
generated
Normal file
108
mobile/openapi/lib/model/queue_update_dto.dart
generated
Normal 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;
|
||||
|
||||
class QueuesResponseDto {
|
||||
/// Returns a new [QueuesResponseDto] instance.
|
||||
QueuesResponseDto({
|
||||
class QueuesResponseLegacyDto {
|
||||
/// Returns a new [QueuesResponseLegacyDto] instance.
|
||||
QueuesResponseLegacyDto({
|
||||
required this.backgroundTask,
|
||||
required this.backupDatabase,
|
||||
required this.duplicateDetection,
|
||||
@@ -32,42 +32,42 @@ class QueuesResponseDto {
|
||||
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
|
||||
bool operator ==(Object other) => identical(this, other) || other is QueuesResponseDto &&
|
||||
bool operator ==(Object other) => identical(this, other) || other is QueuesResponseLegacyDto &&
|
||||
other.backgroundTask == backgroundTask &&
|
||||
other.backupDatabase == backupDatabase &&
|
||||
other.duplicateDetection == duplicateDetection &&
|
||||
@@ -108,7 +108,7 @@ class QueuesResponseDto {
|
||||
(workflow.hashCode);
|
||||
|
||||
@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() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -132,42 +132,42 @@ class QueuesResponseDto {
|
||||
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.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static QueuesResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "QueuesResponseDto");
|
||||
static QueuesResponseLegacyDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "QueuesResponseLegacyDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return QueuesResponseDto(
|
||||
backgroundTask: QueueResponseDto.fromJson(json[r'backgroundTask'])!,
|
||||
backupDatabase: QueueResponseDto.fromJson(json[r'backupDatabase'])!,
|
||||
duplicateDetection: QueueResponseDto.fromJson(json[r'duplicateDetection'])!,
|
||||
faceDetection: QueueResponseDto.fromJson(json[r'faceDetection'])!,
|
||||
facialRecognition: QueueResponseDto.fromJson(json[r'facialRecognition'])!,
|
||||
library_: QueueResponseDto.fromJson(json[r'library'])!,
|
||||
metadataExtraction: QueueResponseDto.fromJson(json[r'metadataExtraction'])!,
|
||||
migration: QueueResponseDto.fromJson(json[r'migration'])!,
|
||||
notifications: QueueResponseDto.fromJson(json[r'notifications'])!,
|
||||
ocr: QueueResponseDto.fromJson(json[r'ocr'])!,
|
||||
search: QueueResponseDto.fromJson(json[r'search'])!,
|
||||
sidecar: QueueResponseDto.fromJson(json[r'sidecar'])!,
|
||||
smartSearch: QueueResponseDto.fromJson(json[r'smartSearch'])!,
|
||||
storageTemplateMigration: QueueResponseDto.fromJson(json[r'storageTemplateMigration'])!,
|
||||
thumbnailGeneration: QueueResponseDto.fromJson(json[r'thumbnailGeneration'])!,
|
||||
videoConversion: QueueResponseDto.fromJson(json[r'videoConversion'])!,
|
||||
workflow: QueueResponseDto.fromJson(json[r'workflow'])!,
|
||||
return QueuesResponseLegacyDto(
|
||||
backgroundTask: QueueResponseLegacyDto.fromJson(json[r'backgroundTask'])!,
|
||||
backupDatabase: QueueResponseLegacyDto.fromJson(json[r'backupDatabase'])!,
|
||||
duplicateDetection: QueueResponseLegacyDto.fromJson(json[r'duplicateDetection'])!,
|
||||
faceDetection: QueueResponseLegacyDto.fromJson(json[r'faceDetection'])!,
|
||||
facialRecognition: QueueResponseLegacyDto.fromJson(json[r'facialRecognition'])!,
|
||||
library_: QueueResponseLegacyDto.fromJson(json[r'library'])!,
|
||||
metadataExtraction: QueueResponseLegacyDto.fromJson(json[r'metadataExtraction'])!,
|
||||
migration: QueueResponseLegacyDto.fromJson(json[r'migration'])!,
|
||||
notifications: QueueResponseLegacyDto.fromJson(json[r'notifications'])!,
|
||||
ocr: QueueResponseLegacyDto.fromJson(json[r'ocr'])!,
|
||||
search: QueueResponseLegacyDto.fromJson(json[r'search'])!,
|
||||
sidecar: QueueResponseLegacyDto.fromJson(json[r'sidecar'])!,
|
||||
smartSearch: QueueResponseLegacyDto.fromJson(json[r'smartSearch'])!,
|
||||
storageTemplateMigration: QueueResponseLegacyDto.fromJson(json[r'storageTemplateMigration'])!,
|
||||
thumbnailGeneration: QueueResponseLegacyDto.fromJson(json[r'thumbnailGeneration'])!,
|
||||
videoConversion: QueueResponseLegacyDto.fromJson(json[r'videoConversion'])!,
|
||||
workflow: QueueResponseLegacyDto.fromJson(json[r'workflow'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<QueuesResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <QueuesResponseDto>[];
|
||||
static List<QueuesResponseLegacyDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <QueuesResponseLegacyDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = QueuesResponseDto.fromJson(row);
|
||||
final value = QueuesResponseLegacyDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
@@ -176,12 +176,12 @@ class QueuesResponseDto {
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, QueuesResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, QueuesResponseDto>{};
|
||||
static Map<String, QueuesResponseLegacyDto> mapFromJson(dynamic json) {
|
||||
final map = <String, QueuesResponseLegacyDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = QueuesResponseDto.fromJson(entry.value);
|
||||
final value = QueuesResponseLegacyDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
@@ -190,14 +190,14 @@ class QueuesResponseDto {
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of QueuesResponseDto-objects as value to a dart map
|
||||
static Map<String, List<QueuesResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<QueuesResponseDto>>{};
|
||||
// maps a json object with a list of QueuesResponseLegacyDto-objects as value to a dart map
|
||||
static Map<String, List<QueuesResponseLegacyDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<QueuesResponseLegacyDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
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;
|
||||
165
mobile/test/domain/services/asset.service_test.dart
Normal file
165
mobile/test/domain/services/asset.service_test.dart
Normal file
@@ -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.getMediaChanges()).thenAnswer(
|
||||
(_) async => SyncDelta(
|
||||
hasChanges: false,
|
||||
updates: const [],
|
||||
deletes: const [],
|
||||
assetAlbums: const {},
|
||||
),
|
||||
(_) async => SyncDelta(hasChanges: false, updates: const [], deletes: const [], assetAlbums: const {}),
|
||||
);
|
||||
when(() => mockNativeSyncApi.getTrashedAssets()).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');
|
||||
when(() => mockTrashedLocalAssetRepository.getToTrash()).thenAnswer((_) async => {'album-a': [localAssetToTrash]});
|
||||
when(() => mockTrashedLocalAssetRepository.getToTrash()).thenAnswer(
|
||||
(_) async => {
|
||||
'album-a': [localAssetToTrash],
|
||||
},
|
||||
);
|
||||
|
||||
final assetEntity = MockAssetEntity();
|
||||
when(() => assetEntity.getMediaUrl()).thenAnswer((_) async => 'content://local-trash');
|
||||
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.getToTrash()).called(1);
|
||||
@@ -159,8 +160,7 @@ void main() {
|
||||
verify(() => mockTrashedLocalAssetRepository.applyRestoredAssets(restoredIds)).called(1);
|
||||
|
||||
verify(() => mockStorageRepository.getAssetEntityForAsset(localAssetToTrash)).called(1);
|
||||
final moveArgs =
|
||||
verify(() => mockLocalFilesManager.moveToTrash(captureAny())).captured.single as List<String>;
|
||||
final moveArgs = verify(() => mockLocalFilesManager.moveToTrash(captureAny())).captured.single as List<String>;
|
||||
expect(moveArgs, ['content://local-trash']);
|
||||
final trashArgs =
|
||||
verify(() => mockTrashedLocalAssetRepository.trashLocalAsset(captureAny())).captured.single
|
||||
@@ -187,4 +187,25 @@ void main() {
|
||||
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/log.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/store.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 MockRemoteAssetRepository extends Mock implements RemoteAssetRepository {}
|
||||
|
||||
class MockTrashedLocalAssetRepository extends Mock implements DriftTrashedLocalAssetRepository {}
|
||||
|
||||
class MockStorageRepository extends Mock implements StorageRepository {}
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:fake_async/fake_async.dart';
|
||||
import 'package:flutter_test/flutter_test.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/android_device_asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
@@ -116,4 +117,43 @@ abstract final class TestUtils {
|
||||
}
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4929,6 +4929,7 @@
|
||||
},
|
||||
"/jobs": {
|
||||
"get": {
|
||||
"deprecated": true,
|
||||
"description": "Retrieve the counts of the current queue, as well as the current status.",
|
||||
"operationId": "getQueuesLegacy",
|
||||
"parameters": [],
|
||||
@@ -4937,7 +4938,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/QueuesResponseDto"
|
||||
"$ref": "#/components/schemas/QueuesResponseLegacyDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4957,7 +4958,8 @@
|
||||
],
|
||||
"summary": "Retrieve queue counts and status",
|
||||
"tags": [
|
||||
"Jobs"
|
||||
"Jobs",
|
||||
"Deprecated"
|
||||
],
|
||||
"x-immich-admin-only": true,
|
||||
"x-immich-history": [
|
||||
@@ -4972,10 +4974,14 @@
|
||||
{
|
||||
"version": "v2",
|
||||
"state": "Stable"
|
||||
},
|
||||
{
|
||||
"version": "v2.4.0",
|
||||
"state": "Deprecated"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "job.read",
|
||||
"x-immich-state": "Stable"
|
||||
"x-immich-state": "Deprecated"
|
||||
},
|
||||
"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.",
|
||||
@@ -5032,6 +5038,7 @@
|
||||
},
|
||||
"/jobs/{name}": {
|
||||
"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.",
|
||||
"operationId": "runQueueCommandLegacy",
|
||||
"parameters": [
|
||||
@@ -5059,7 +5066,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -5079,7 +5086,8 @@
|
||||
],
|
||||
"summary": "Run jobs",
|
||||
"tags": [
|
||||
"Jobs"
|
||||
"Jobs",
|
||||
"Deprecated"
|
||||
],
|
||||
"x-immich-admin-only": true,
|
||||
"x-immich-history": [
|
||||
@@ -5094,10 +5102,14 @@
|
||||
{
|
||||
"version": "v2",
|
||||
"state": "Stable"
|
||||
},
|
||||
{
|
||||
"version": "v2.4.0",
|
||||
"state": "Deprecated"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "job.create",
|
||||
"x-immich-state": "Stable"
|
||||
"x-immich-state": "Deprecated"
|
||||
}
|
||||
},
|
||||
"/libraries": {
|
||||
@@ -8064,6 +8076,303 @@
|
||||
"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": {
|
||||
"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.",
|
||||
@@ -14043,6 +14352,10 @@
|
||||
"name": "Plugins",
|
||||
"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",
|
||||
"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"
|
||||
},
|
||||
"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": {
|
||||
"properties": {
|
||||
"concurrency": {
|
||||
@@ -17583,6 +17956,12 @@
|
||||
"userProfileImage.read",
|
||||
"userProfileImage.update",
|
||||
"userProfileImage.delete",
|
||||
"queue.read",
|
||||
"queue.update",
|
||||
"queueJob.create",
|
||||
"queueJob.read",
|
||||
"queueJob.update",
|
||||
"queueJob.delete",
|
||||
"workflow.create",
|
||||
"workflow.read",
|
||||
"workflow.update",
|
||||
@@ -18083,6 +18462,63 @@
|
||||
],
|
||||
"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": {
|
||||
"enum": [
|
||||
"thumbnailGeneration",
|
||||
@@ -18106,12 +18542,35 @@
|
||||
"type": "string"
|
||||
},
|
||||
"QueueResponseDto": {
|
||||
"properties": {
|
||||
"isPaused": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/QueueName"
|
||||
}
|
||||
]
|
||||
},
|
||||
"statistics": {
|
||||
"$ref": "#/components/schemas/QueueStatisticsDto"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"isPaused",
|
||||
"name",
|
||||
"statistics"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"QueueResponseLegacyDto": {
|
||||
"properties": {
|
||||
"jobCounts": {
|
||||
"$ref": "#/components/schemas/QueueStatisticsDto"
|
||||
},
|
||||
"queueStatus": {
|
||||
"$ref": "#/components/schemas/QueueStatusDto"
|
||||
"$ref": "#/components/schemas/QueueStatusLegacyDto"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -18151,7 +18610,7 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"QueueStatusDto": {
|
||||
"QueueStatusLegacyDto": {
|
||||
"properties": {
|
||||
"isActive": {
|
||||
"type": "boolean"
|
||||
@@ -18166,58 +18625,66 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"QueuesResponseDto": {
|
||||
"QueueUpdateDto": {
|
||||
"properties": {
|
||||
"isPaused": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"QueuesResponseLegacyDto": {
|
||||
"properties": {
|
||||
"backgroundTask": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||
},
|
||||
"backupDatabase": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||
},
|
||||
"duplicateDetection": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||
},
|
||||
"faceDetection": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||
},
|
||||
"facialRecognition": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||
},
|
||||
"library": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||
},
|
||||
"metadataExtraction": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||
},
|
||||
"migration": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||
},
|
||||
"notifications": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||
},
|
||||
"ocr": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||
},
|
||||
"search": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||
},
|
||||
"sidecar": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||
},
|
||||
"smartSearch": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||
},
|
||||
"storageTemplateMigration": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||
},
|
||||
"thumbnailGeneration": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||
},
|
||||
"videoConversion": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||
},
|
||||
"workflow": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -1 +1 @@
|
||||
24.11.0
|
||||
24.11.1
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.19.1",
|
||||
"@types/node": "^24.10.1",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"repository": {
|
||||
@@ -28,6 +28,6 @@
|
||||
"directory": "open-api/typescript-sdk"
|
||||
},
|
||||
"volta": {
|
||||
"node": "24.11.0"
|
||||
"node": "24.11.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -716,32 +716,32 @@ export type QueueStatisticsDto = {
|
||||
paused: number;
|
||||
waiting: number;
|
||||
};
|
||||
export type QueueStatusDto = {
|
||||
export type QueueStatusLegacyDto = {
|
||||
isActive: boolean;
|
||||
isPaused: boolean;
|
||||
};
|
||||
export type QueueResponseDto = {
|
||||
export type QueueResponseLegacyDto = {
|
||||
jobCounts: QueueStatisticsDto;
|
||||
queueStatus: QueueStatusDto;
|
||||
queueStatus: QueueStatusLegacyDto;
|
||||
};
|
||||
export type QueuesResponseDto = {
|
||||
backgroundTask: QueueResponseDto;
|
||||
backupDatabase: QueueResponseDto;
|
||||
duplicateDetection: QueueResponseDto;
|
||||
faceDetection: QueueResponseDto;
|
||||
facialRecognition: QueueResponseDto;
|
||||
library: QueueResponseDto;
|
||||
metadataExtraction: QueueResponseDto;
|
||||
migration: QueueResponseDto;
|
||||
notifications: QueueResponseDto;
|
||||
ocr: QueueResponseDto;
|
||||
search: QueueResponseDto;
|
||||
sidecar: QueueResponseDto;
|
||||
smartSearch: QueueResponseDto;
|
||||
storageTemplateMigration: QueueResponseDto;
|
||||
thumbnailGeneration: QueueResponseDto;
|
||||
videoConversion: QueueResponseDto;
|
||||
workflow: QueueResponseDto;
|
||||
export type QueuesResponseLegacyDto = {
|
||||
backgroundTask: QueueResponseLegacyDto;
|
||||
backupDatabase: QueueResponseLegacyDto;
|
||||
duplicateDetection: QueueResponseLegacyDto;
|
||||
faceDetection: QueueResponseLegacyDto;
|
||||
facialRecognition: QueueResponseLegacyDto;
|
||||
library: QueueResponseLegacyDto;
|
||||
metadataExtraction: QueueResponseLegacyDto;
|
||||
migration: QueueResponseLegacyDto;
|
||||
notifications: QueueResponseLegacyDto;
|
||||
ocr: QueueResponseLegacyDto;
|
||||
search: QueueResponseLegacyDto;
|
||||
sidecar: QueueResponseLegacyDto;
|
||||
smartSearch: QueueResponseLegacyDto;
|
||||
storageTemplateMigration: QueueResponseLegacyDto;
|
||||
thumbnailGeneration: QueueResponseLegacyDto;
|
||||
videoConversion: QueueResponseLegacyDto;
|
||||
workflow: QueueResponseLegacyDto;
|
||||
};
|
||||
export type JobCreateDto = {
|
||||
name: ManualJobName;
|
||||
@@ -966,6 +966,24 @@ export type PluginResponseDto = {
|
||||
updatedAt: 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 = {
|
||||
data: AssetResponseDto;
|
||||
value: string;
|
||||
@@ -2925,7 +2943,7 @@ export function reassignFacesById({ id, faceDto }: {
|
||||
export function getQueuesLegacy(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: QueuesResponseDto;
|
||||
data: QueuesResponseLegacyDto;
|
||||
}>("/jobs", {
|
||||
...opts
|
||||
}));
|
||||
@@ -2951,7 +2969,7 @@ export function runQueueCommandLegacy({ name, queueCommandDto }: {
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: QueueResponseDto;
|
||||
data: QueueResponseLegacyDto;
|
||||
}>(`/jobs/${encodeURIComponent(name)}`, oazapfts.json({
|
||||
...opts,
|
||||
method: "PUT",
|
||||
@@ -3651,6 +3669,75 @@ export function getPlugin({ id }: {
|
||||
...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
|
||||
*/
|
||||
@@ -5241,6 +5328,12 @@ export enum Permission {
|
||||
UserProfileImageRead = "userProfileImage.read",
|
||||
UserProfileImageUpdate = "userProfileImage.update",
|
||||
UserProfileImageDelete = "userProfileImage.delete",
|
||||
QueueRead = "queue.read",
|
||||
QueueUpdate = "queue.update",
|
||||
QueueJobCreate = "queueJob.create",
|
||||
QueueJobRead = "queueJob.read",
|
||||
QueueJobUpdate = "queueJob.update",
|
||||
QueueJobDelete = "queueJob.delete",
|
||||
WorkflowCreate = "workflow.create",
|
||||
WorkflowRead = "workflow.read",
|
||||
WorkflowUpdate = "workflow.update",
|
||||
@@ -5330,6 +5423,71 @@ export enum PluginContext {
|
||||
Album = "album",
|
||||
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 {
|
||||
Country = "country",
|
||||
State = "state",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.1",
|
||||
"description": "Monorepo for Immich",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.20.0+sha512.cf9998222162dd85864d0a8102e7892e7ba4ceadebbf5a31f9c2fce48dfce317a9c53b9f6464d1ef9042cba2e02ae02a9f7c143a2b438cd93c91840f0192b9dd",
|
||||
"packageManager": "pnpm@10.22.0+sha512.bf049efe995b28f527fd2b41ae0474ce29186f7edcb3bf545087bd61fbbebb2bf75362d1307fda09c2d288e1e499787ac12d4fcb617a974718a6051f2eee741c",
|
||||
"engines": {
|
||||
"pnpm": ">=10.0.0"
|
||||
}
|
||||
|
||||
358
plugins/package-lock.json
generated
358
plugins/package-lock.json
generated
@@ -1,436 +1,519 @@
|
||||
{
|
||||
"name": "js-pdk-template",
|
||||
"name": "plugins",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "js-pdk-template",
|
||||
"name": "plugins",
|
||||
"version": "1.0.0",
|
||||
"license": "BSD-3-Clause",
|
||||
"license": "AGPL-3.0",
|
||||
"devDependencies": {
|
||||
"@extism/js-pdk": "^1.0.1",
|
||||
"esbuild": "^0.19.6",
|
||||
"esbuild": "^0.27.0",
|
||||
"typescript": "^5.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
|
||||
"integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==",
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz",
|
||||
"integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
|
||||
"integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz",
|
||||
"integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
|
||||
"integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz",
|
||||
"integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
|
||||
"integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz",
|
||||
"integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
|
||||
"integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz",
|
||||
"integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
|
||||
"integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz",
|
||||
"integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
|
||||
"integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz",
|
||||
"integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
|
||||
"integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz",
|
||||
"integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
|
||||
"integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz",
|
||||
"integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
|
||||
"integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz",
|
||||
"integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
|
||||
"integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz",
|
||||
"integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
|
||||
"integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz",
|
||||
"integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
|
||||
"integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz",
|
||||
"integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
|
||||
"integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz",
|
||||
"integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
|
||||
"integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz",
|
||||
"integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
|
||||
"integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz",
|
||||
"integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
|
||||
"integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz",
|
||||
"integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
|
||||
"integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz",
|
||||
"integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
|
||||
"integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz",
|
||||
"integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"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,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
|
||||
"integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz",
|
||||
"integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"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,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
|
||||
"integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz",
|
||||
"integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
|
||||
"integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz",
|
||||
"integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
|
||||
"integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz",
|
||||
"integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@extism/js-pdk": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@extism/js-pdk/-/js-pdk-1.0.1.tgz",
|
||||
"integrity": "sha512-YJWfHGeOuJnQw4V8NPNHvbSr6S8iDd2Ga6VEukwlRP7tu62ozTxIgokYw8i+rajD/16zz/gK0KYARBpm2qPAmQ==",
|
||||
"dev": true
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@extism/js-pdk/-/js-pdk-1.1.1.tgz",
|
||||
"integrity": "sha512-VZLn/dX0ttA1uKk2PZeR/FL3N+nA1S5Vc7E5gdjkR60LuUIwCZT9cYON245V4HowHlBA7YOegh0TLjkx+wNbrA==",
|
||||
"dev": true,
|
||||
"license": "BSD-Clause-3",
|
||||
"dependencies": {
|
||||
"urlpattern-polyfill": "^8.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
|
||||
"integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz",
|
||||
"integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.19.12",
|
||||
"@esbuild/android-arm": "0.19.12",
|
||||
"@esbuild/android-arm64": "0.19.12",
|
||||
"@esbuild/android-x64": "0.19.12",
|
||||
"@esbuild/darwin-arm64": "0.19.12",
|
||||
"@esbuild/darwin-x64": "0.19.12",
|
||||
"@esbuild/freebsd-arm64": "0.19.12",
|
||||
"@esbuild/freebsd-x64": "0.19.12",
|
||||
"@esbuild/linux-arm": "0.19.12",
|
||||
"@esbuild/linux-arm64": "0.19.12",
|
||||
"@esbuild/linux-ia32": "0.19.12",
|
||||
"@esbuild/linux-loong64": "0.19.12",
|
||||
"@esbuild/linux-mips64el": "0.19.12",
|
||||
"@esbuild/linux-ppc64": "0.19.12",
|
||||
"@esbuild/linux-riscv64": "0.19.12",
|
||||
"@esbuild/linux-s390x": "0.19.12",
|
||||
"@esbuild/linux-x64": "0.19.12",
|
||||
"@esbuild/netbsd-x64": "0.19.12",
|
||||
"@esbuild/openbsd-x64": "0.19.12",
|
||||
"@esbuild/sunos-x64": "0.19.12",
|
||||
"@esbuild/win32-arm64": "0.19.12",
|
||||
"@esbuild/win32-ia32": "0.19.12",
|
||||
"@esbuild/win32-x64": "0.19.12"
|
||||
"@esbuild/aix-ppc64": "0.27.0",
|
||||
"@esbuild/android-arm": "0.27.0",
|
||||
"@esbuild/android-arm64": "0.27.0",
|
||||
"@esbuild/android-x64": "0.27.0",
|
||||
"@esbuild/darwin-arm64": "0.27.0",
|
||||
"@esbuild/darwin-x64": "0.27.0",
|
||||
"@esbuild/freebsd-arm64": "0.27.0",
|
||||
"@esbuild/freebsd-x64": "0.27.0",
|
||||
"@esbuild/linux-arm": "0.27.0",
|
||||
"@esbuild/linux-arm64": "0.27.0",
|
||||
"@esbuild/linux-ia32": "0.27.0",
|
||||
"@esbuild/linux-loong64": "0.27.0",
|
||||
"@esbuild/linux-mips64el": "0.27.0",
|
||||
"@esbuild/linux-ppc64": "0.27.0",
|
||||
"@esbuild/linux-riscv64": "0.27.0",
|
||||
"@esbuild/linux-s390x": "0.27.0",
|
||||
"@esbuild/linux-x64": "0.27.0",
|
||||
"@esbuild/netbsd-arm64": "0.27.0",
|
||||
"@esbuild/netbsd-x64": "0.27.0",
|
||||
"@esbuild/openbsd-arm64": "0.27.0",
|
||||
"@esbuild/openbsd-x64": "0.27.0",
|
||||
"@esbuild/openharmony-arm64": "0.27.0",
|
||||
"@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": {
|
||||
"version": "5.4.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz",
|
||||
"integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==",
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -438,6 +521,13 @@
|
||||
"engines": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"license": "AGPL-3.0",
|
||||
"devDependencies": {
|
||||
"@extism/js-pdk": "^1.0.1",
|
||||
"esbuild": "^0.19.6",
|
||||
"esbuild": "^0.27.0",
|
||||
"typescript": "^5.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
3744
pnpm-lock.yaml
generated
3744
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
||||
24.11.0
|
||||
24.11.1
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.4",
|
||||
"@swc/core": "^1.4.14",
|
||||
"@types/archiver": "^6.0.0",
|
||||
"@types/archiver": "^7.0.0",
|
||||
"@types/async-lock": "^1.4.2",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/body-parser": "^1.19.6",
|
||||
@@ -134,7 +134,7 @@
|
||||
"@types/luxon": "^3.6.2",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/node": "^22.19.1",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/nodemailer": "^7.0.0",
|
||||
"@types/picomatch": "^4.0.0",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
@@ -166,7 +166,7 @@
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"volta": {
|
||||
"node": "24.11.0"
|
||||
"node": "24.11.1"
|
||||
},
|
||||
"overrides": {
|
||||
"sharp": "^0.34.4"
|
||||
|
||||
@@ -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.',
|
||||
[ApiTag.Plugins]:
|
||||
'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]:
|
||||
'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]:
|
||||
|
||||
@@ -20,6 +20,7 @@ import { OAuthController } from 'src/controllers/oauth.controller';
|
||||
import { PartnerController } from 'src/controllers/partner.controller';
|
||||
import { PersonController } from 'src/controllers/person.controller';
|
||||
import { PluginController } from 'src/controllers/plugin.controller';
|
||||
import { QueueController } from 'src/controllers/queue.controller';
|
||||
import { SearchController } from 'src/controllers/search.controller';
|
||||
import { ServerController } from 'src/controllers/server.controller';
|
||||
import { SessionController } from 'src/controllers/session.controller';
|
||||
@@ -59,6 +60,7 @@ export const controllers = [
|
||||
PartnerController,
|
||||
PersonController,
|
||||
PluginController,
|
||||
QueueController,
|
||||
SearchController,
|
||||
ServerController,
|
||||
SessionController,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.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 { Authenticated } from 'src/middleware/auth.guard';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { JobService } from 'src/services/job.service';
|
||||
import { QueueService } from 'src/services/queue.service';
|
||||
|
||||
@@ -21,10 +23,10 @@ export class JobController {
|
||||
@Endpoint({
|
||||
summary: 'Retrieve queue counts and 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> {
|
||||
return this.queueService.getAll();
|
||||
getQueuesLegacy(@Auth() auth: AuthDto): Promise<QueuesResponseLegacyDto> {
|
||||
return this.queueService.getAllLegacy(auth);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@@ -46,9 +48,12 @@ export class JobController {
|
||||
summary: 'Run jobs',
|
||||
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.',
|
||||
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> {
|
||||
return this.queueService.runCommand(name, dto);
|
||||
runQueueCommandLegacy(
|
||||
@Param() { name }: QueueNameParamDto,
|
||||
@Body() dto: QueueCommandDto,
|
||||
): Promise<QueueResponseLegacyDto> {
|
||||
return this.queueService.runCommandLegacy(name, dto);
|
||||
}
|
||||
}
|
||||
|
||||
85
server/src/controllers/queue.controller.ts
Normal file
85
server/src/controllers/queue.controller.ts
Normal file
@@ -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
server/src/dtos/queue-legacy.dto.ts
Normal file
89
server/src/dtos/queue-legacy.dto.ts
Normal 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;
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
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';
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
active!: number;
|
||||
@@ -29,69 +70,3 @@ export class QueueStatisticsDto {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -248,6 +248,14 @@ export enum Permission {
|
||||
UserProfileImageUpdate = 'userProfileImage.update',
|
||||
UserProfileImageDelete = 'userProfileImage.delete',
|
||||
|
||||
QueueRead = 'queue.read',
|
||||
QueueUpdate = 'queue.update',
|
||||
|
||||
QueueJobCreate = 'queueJob.create',
|
||||
QueueJobRead = 'queueJob.read',
|
||||
QueueJobUpdate = 'queueJob.update',
|
||||
QueueJobDelete = 'queueJob.delete',
|
||||
|
||||
WorkflowCreate = 'workflow.create',
|
||||
WorkflowRead = 'workflow.read',
|
||||
WorkflowUpdate = 'workflow.update',
|
||||
@@ -543,6 +551,15 @@ export enum QueueName {
|
||||
Workflow = 'workflow',
|
||||
}
|
||||
|
||||
export enum QueueJobStatus {
|
||||
Active = 'active',
|
||||
Failed = 'failed',
|
||||
Complete = 'completed',
|
||||
Delayed = 'delayed',
|
||||
Waiting = 'waiting',
|
||||
Paused = 'paused',
|
||||
}
|
||||
|
||||
export enum JobName {
|
||||
AssetDelete = 'AssetDelete',
|
||||
AssetDeleteCheck = 'AssetDeleteCheck',
|
||||
@@ -624,9 +641,13 @@ export enum JobName {
|
||||
|
||||
export enum QueueCommand {
|
||||
Start = 'start',
|
||||
/** @deprecated Use `updateQueue` instead */
|
||||
Pause = 'pause',
|
||||
/** @deprecated Use `updateQueue` instead */
|
||||
Resume = 'resume',
|
||||
/** @deprecated Use `emptyQueue` instead */
|
||||
Empty = 'empty',
|
||||
/** @deprecated Use `emptyQueue` instead */
|
||||
ClearFailed = 'clear-failed',
|
||||
}
|
||||
|
||||
@@ -823,6 +844,7 @@ export enum ApiTag {
|
||||
Partners = 'Partners',
|
||||
People = 'People',
|
||||
Plugins = 'Plugins',
|
||||
Queues = 'Queues',
|
||||
Search = 'Search',
|
||||
Server = 'Server',
|
||||
Sessions = 'Sessions',
|
||||
|
||||
@@ -249,7 +249,7 @@ const getEnv = (): EnvData => {
|
||||
prefix: 'immich_bull',
|
||||
connection: { ...redisConfig },
|
||||
defaultJobOptions: {
|
||||
attempts: 3,
|
||||
attempts: 1,
|
||||
removeOnComplete: true,
|
||||
removeOnFail: false,
|
||||
},
|
||||
|
||||
@@ -5,11 +5,12 @@ import { JobsOptions, Queue, Worker } from 'bullmq';
|
||||
import { ClassConstructor } from 'class-transformer';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
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 { EventRepository } from 'src/repositories/event.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';
|
||||
|
||||
type JobMapItem = {
|
||||
@@ -115,13 +116,14 @@ export class JobRepository {
|
||||
worker.concurrency = concurrency;
|
||||
}
|
||||
|
||||
async getQueueStatus(name: QueueName): Promise<QueueStatus> {
|
||||
async isActive(name: QueueName): Promise<boolean> {
|
||||
const queue = this.getQueue(name);
|
||||
const count = await queue.getActiveCount();
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
return {
|
||||
isActive: !!(await queue.getActiveCount()),
|
||||
isPaused: await queue.isPaused(),
|
||||
};
|
||||
async isPaused(name: QueueName): Promise<boolean> {
|
||||
return this.getQueue(name).isPaused();
|
||||
}
|
||||
|
||||
pause(name: QueueName) {
|
||||
@@ -192,17 +194,28 @@ export class JobRepository {
|
||||
}
|
||||
|
||||
async waitForQueueCompletion(...queues: QueueName[]): Promise<void> {
|
||||
let activeQueue: QueueStatus | undefined;
|
||||
do {
|
||||
const statuses = await Promise.all(queues.map((name) => this.getQueueStatus(name)));
|
||||
activeQueue = statuses.find((status) => status.isActive);
|
||||
} while (activeQueue);
|
||||
{
|
||||
this.logger.verbose(`Waiting for ${activeQueue} queue to stop...`);
|
||||
const getPending = async () => {
|
||||
const results = await Promise.all(queues.map(async (name) => ({ pending: await this.isActive(name), name })));
|
||||
return results.filter(({ pending }) => pending).map(({ name }) => name);
|
||||
};
|
||||
|
||||
let pending = await getPending();
|
||||
|
||||
while (pending.length > 0) {
|
||||
this.logger.verbose(`Waiting for ${pending[0]} queue to stop...`);
|
||||
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 {
|
||||
switch (item.name) {
|
||||
case JobName.NotifyAlbumUpdate: {
|
||||
|
||||
@@ -153,6 +153,37 @@ describe(BackupService.name, () => {
|
||||
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 () => {
|
||||
const result = await sut.handleBackupDatabase();
|
||||
expect(result).toBe(JobStatus.Success);
|
||||
|
||||
@@ -81,8 +81,16 @@ export class BackupService extends BaseService {
|
||||
|
||||
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
|
||||
? ['--dbname', config.url]
|
||||
? ['--dbname', connectionUrl]
|
||||
: [
|
||||
'--username',
|
||||
config.username,
|
||||
@@ -118,7 +126,7 @@ export class BackupService extends BaseService {
|
||||
{
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
PGPASSWORD: isUrlConnection ? new URL(config.url).password : config.password,
|
||||
PGPASSWORD: isUrlConnection ? new URL(connectionUrl).password : config.password,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
mockReadTags({}, { Duration: { Value: 123 } });
|
||||
mockReadTags({ Duration: 123 }, {});
|
||||
|
||||
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 () => {
|
||||
|
||||
@@ -291,7 +291,7 @@ export class MetadataService extends BaseService {
|
||||
this.assetRepository.upsertExif(exifData),
|
||||
this.assetRepository.update({
|
||||
id: asset.id,
|
||||
duration: exifTags.Duration?.toString() ?? null,
|
||||
duration: this.getDuration(exifTags),
|
||||
localDateTime: dates.localDateTime,
|
||||
fileCreatedAt: dates.dateTimeOriginal ?? undefined,
|
||||
fileModifiedAt: stats.mtime,
|
||||
@@ -457,19 +457,7 @@ export class MetadataService extends BaseService {
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
private 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: {
|
||||
private async getExifTags(asset: {
|
||||
originalPath: string;
|
||||
sidecarPath: string | null;
|
||||
type: AssetType;
|
||||
@@ -492,7 +480,11 @@ export class MetadataService extends BaseService {
|
||||
}
|
||||
|
||||
// prefer duration from video tags
|
||||
delete mediaTags.Duration;
|
||||
if (videoTags) {
|
||||
delete mediaTags.Duration;
|
||||
}
|
||||
|
||||
// never use duration from sidecar
|
||||
delete sidecarTags?.Duration;
|
||||
|
||||
return { ...mediaTags, ...videoTags, ...sidecarTags };
|
||||
@@ -934,6 +926,20 @@ export class MetadataService extends BaseService {
|
||||
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) {
|
||||
const { videoStreams, format } = await this.mediaRepository.probe(originalPath);
|
||||
|
||||
@@ -961,7 +967,7 @@ export class MetadataService extends BaseService {
|
||||
}
|
||||
|
||||
if (format.duration) {
|
||||
tags.Duration = Duration.fromObject({ seconds: format.duration }).toFormat('hh:mm:ss.SSS');
|
||||
tags.Duration = format.duration;
|
||||
}
|
||||
|
||||
return tags;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BadRequestException } from '@nestjs/common';
|
||||
import { defaults, SystemConfig } from 'src/config';
|
||||
import { ImmichWorker, JobName, QueueCommand, QueueName } from 'src/enum';
|
||||
import { QueueService } from 'src/services/queue.service';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(QueueService.name, () => {
|
||||
@@ -52,80 +53,64 @@ describe(QueueService.name, () => {
|
||||
|
||||
describe('getAllJobStatus', () => {
|
||||
it('should get all job statuses', async () => {
|
||||
mocks.job.getJobCounts.mockResolvedValue({
|
||||
active: 1,
|
||||
completed: 1,
|
||||
failed: 1,
|
||||
delayed: 1,
|
||||
waiting: 1,
|
||||
paused: 1,
|
||||
});
|
||||
mocks.job.getQueueStatus.mockResolvedValue({
|
||||
isActive: true,
|
||||
isPaused: true,
|
||||
});
|
||||
const stats = factory.queueStatistics({ active: 1 });
|
||||
const expected = { jobCounts: stats, queueStatus: { isActive: true, isPaused: true } };
|
||||
|
||||
const expectedJobStatus = {
|
||||
jobCounts: {
|
||||
active: 1,
|
||||
completed: 1,
|
||||
delayed: 1,
|
||||
failed: 1,
|
||||
waiting: 1,
|
||||
paused: 1,
|
||||
},
|
||||
queueStatus: {
|
||||
isActive: true,
|
||||
isPaused: true,
|
||||
},
|
||||
};
|
||||
mocks.job.getJobCounts.mockResolvedValue(stats);
|
||||
mocks.job.isPaused.mockResolvedValue(true);
|
||||
|
||||
await expect(sut.getAll()).resolves.toEqual({
|
||||
[QueueName.BackgroundTask]: expectedJobStatus,
|
||||
[QueueName.DuplicateDetection]: expectedJobStatus,
|
||||
[QueueName.SmartSearch]: expectedJobStatus,
|
||||
[QueueName.MetadataExtraction]: expectedJobStatus,
|
||||
[QueueName.Search]: expectedJobStatus,
|
||||
[QueueName.StorageTemplateMigration]: expectedJobStatus,
|
||||
[QueueName.Migration]: expectedJobStatus,
|
||||
[QueueName.ThumbnailGeneration]: expectedJobStatus,
|
||||
[QueueName.VideoConversion]: expectedJobStatus,
|
||||
[QueueName.FaceDetection]: expectedJobStatus,
|
||||
[QueueName.FacialRecognition]: expectedJobStatus,
|
||||
[QueueName.Sidecar]: expectedJobStatus,
|
||||
[QueueName.Library]: expectedJobStatus,
|
||||
[QueueName.Notification]: expectedJobStatus,
|
||||
[QueueName.BackupDatabase]: expectedJobStatus,
|
||||
[QueueName.Ocr]: expectedJobStatus,
|
||||
[QueueName.Workflow]: expectedJobStatus,
|
||||
await expect(sut.getAllLegacy(factory.auth())).resolves.toEqual({
|
||||
[QueueName.BackgroundTask]: expected,
|
||||
[QueueName.DuplicateDetection]: expected,
|
||||
[QueueName.SmartSearch]: expected,
|
||||
[QueueName.MetadataExtraction]: expected,
|
||||
[QueueName.Search]: expected,
|
||||
[QueueName.StorageTemplateMigration]: expected,
|
||||
[QueueName.Migration]: expected,
|
||||
[QueueName.ThumbnailGeneration]: expected,
|
||||
[QueueName.VideoConversion]: expected,
|
||||
[QueueName.FaceDetection]: expected,
|
||||
[QueueName.FacialRecognition]: expected,
|
||||
[QueueName.Sidecar]: expected,
|
||||
[QueueName.Library]: expected,
|
||||
[QueueName.Notification]: expected,
|
||||
[QueueName.BackupDatabase]: expected,
|
||||
[QueueName.Ocr]: expected,
|
||||
[QueueName.Workflow]: expected,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCommand', () => {
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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(
|
||||
sut.runCommand(QueueName.VideoConversion, { command: QueueCommand.Start, force: false }),
|
||||
sut.runCommandLegacy(QueueName.VideoConversion, { command: QueueCommand.Start, force: false }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
@@ -133,33 +118,37 @@ describe(QueueService.name, () => {
|
||||
});
|
||||
|
||||
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 } });
|
||||
});
|
||||
|
||||
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 });
|
||||
});
|
||||
|
||||
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 } });
|
||||
});
|
||||
|
||||
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({
|
||||
name: JobName.AssetExtractMetadataQueueAll,
|
||||
@@ -168,17 +157,19 @@ describe(QueueService.name, () => {
|
||||
});
|
||||
|
||||
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 } });
|
||||
});
|
||||
|
||||
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({
|
||||
name: JobName.AssetGenerateThumbnailsQueueAll,
|
||||
@@ -187,34 +178,37 @@ describe(QueueService.name, () => {
|
||||
});
|
||||
|
||||
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 } });
|
||||
});
|
||||
|
||||
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 } });
|
||||
});
|
||||
|
||||
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 } });
|
||||
});
|
||||
|
||||
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(
|
||||
sut.runCommand(QueueName.BackgroundTask, { command: QueueCommand.Start, force: false }),
|
||||
sut.runCommandLegacy(QueueName.BackgroundTask, { command: QueueCommand.Start, force: false }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
|
||||
@@ -2,7 +2,21 @@ import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { ClassConstructor } from 'class-transformer';
|
||||
import { SystemConfig } from 'src/config';
|
||||
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 {
|
||||
BootstrapEventPriority,
|
||||
CronJob,
|
||||
@@ -86,7 +100,7 @@ export class QueueService extends BaseService {
|
||||
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}`);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
async getAll(): Promise<QueuesResponseDto> {
|
||||
const response = new QueuesResponseDto();
|
||||
for (const name of Object.values(QueueName)) {
|
||||
response[name] = await this.getByName(name);
|
||||
async update(auth: AuthDto, name: QueueName, dto: QueueUpdateDto): Promise<QueueResponseDto> {
|
||||
if (dto.isPaused === true) {
|
||||
if (name === QueueName.BackgroundTask) {
|
||||
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> {
|
||||
const [jobCounts, queueStatus] = await Promise.all([
|
||||
this.jobRepository.getJobCounts(name),
|
||||
this.jobRepository.getQueueStatus(name),
|
||||
]);
|
||||
searchJobs(auth: AuthDto, name: QueueName, dto: QueueJobSearchDto): Promise<QueueJobResponseDto[]> {
|
||||
return this.jobRepository.searchJobs(name, dto);
|
||||
}
|
||||
|
||||
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> {
|
||||
const { isActive } = await this.jobRepository.getQueueStatus(name);
|
||||
const isActive = await this.jobRepository.isActive(name);
|
||||
if (isActive) {
|
||||
throw new BadRequestException(`Job is already running`);
|
||||
}
|
||||
|
||||
@@ -291,11 +291,6 @@ export interface JobCounts {
|
||||
paused: number;
|
||||
}
|
||||
|
||||
export interface QueueStatus {
|
||||
isActive: boolean;
|
||||
isPaused: boolean;
|
||||
}
|
||||
|
||||
export type JobItem =
|
||||
// Audit
|
||||
| { name: JobName.AuditTableCleanup; data?: IBaseJob }
|
||||
|
||||
@@ -11,9 +11,11 @@ export const newJobRepositoryMock = (): Mocked<RepositoryInterface<JobRepository
|
||||
empty: vitest.fn(),
|
||||
pause: vitest.fn(),
|
||||
resume: vitest.fn(),
|
||||
searchJobs: vitest.fn(),
|
||||
queue: vitest.fn().mockImplementation(() => Promise.resolve()),
|
||||
queueAll: vitest.fn().mockImplementation(() => Promise.resolve()),
|
||||
getQueueStatus: vitest.fn(),
|
||||
isActive: vitest.fn(),
|
||||
isPaused: vitest.fn(),
|
||||
getJobCounts: vitest.fn(),
|
||||
clear: vitest.fn(),
|
||||
waitForQueueCompletion: vitest.fn(),
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from 'src/database';
|
||||
import { MapAsset } from 'src/dtos/asset-response.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 { OnThisDayData, UserMetadataItem } from 'src/types';
|
||||
import { v4, v7 } from 'uuid';
|
||||
@@ -139,6 +140,16 @@ const sessionFactory = (session: Partial<Session> = {}) => ({
|
||||
...session,
|
||||
});
|
||||
|
||||
const queueStatisticsFactory = (dto?: Partial<QueueStatisticsDto>) => ({
|
||||
active: 0,
|
||||
completed: 0,
|
||||
failed: 0,
|
||||
delayed: 0,
|
||||
waiting: 0,
|
||||
paused: 0,
|
||||
...dto,
|
||||
});
|
||||
|
||||
const stackFactory = () => ({
|
||||
id: newUuid(),
|
||||
ownerId: newUuid(),
|
||||
@@ -353,6 +364,7 @@ export const factory = {
|
||||
library: libraryFactory,
|
||||
memory: memoryFactory,
|
||||
partner: partnerFactory,
|
||||
queueStatistics: queueStatisticsFactory,
|
||||
session: sessionFactory,
|
||||
stack: stackFactory,
|
||||
user: userFactory,
|
||||
|
||||
@@ -1 +1 @@
|
||||
24.11.0
|
||||
24.11.1
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
"preview": "vite preview",
|
||||
"check:svelte": "svelte-check --no-tsconfig --fail-on-warnings",
|
||||
"check:typescript": "tsc --noEmit",
|
||||
"check:watch": "npm run check:svelte -- --watch",
|
||||
"check:code": "npm run format && npm run lint:p && npm run check:svelte && npm run check:typescript",
|
||||
"check:all": "npm run check:code && npm run test:cov",
|
||||
"check:watch": "pnpm run check:svelte --watch",
|
||||
"check:code": "pnpm run format && pnpm run lint && pnpm run check:svelte && pnpm run check:typescript",
|
||||
"check:all": "pnpm run check:code && pnpm run test:cov",
|
||||
"lint": "eslint . --max-warnings 0 --concurrency 4",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"lint:fix": "pnpm run lint --fix",
|
||||
"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",
|
||||
"test": "vitest --run",
|
||||
"test:cov": "vitest --coverage",
|
||||
@@ -28,7 +28,7 @@
|
||||
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
||||
"@immich/justified-layout-wasm": "^0.4.3",
|
||||
"@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",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@photo-sphere-viewer/core": "^5.14.0",
|
||||
@@ -97,7 +97,7 @@
|
||||
"prettier-plugin-sort-json": "^4.1.1",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"rollup-plugin-visualizer": "^6.0.0",
|
||||
"svelte": "5.43.0",
|
||||
"svelte": "5.43.12",
|
||||
"svelte-check": "^4.1.5",
|
||||
"svelte-eslint-parser": "^1.3.3",
|
||||
"tailwindcss": "^4.1.7",
|
||||
@@ -107,6 +107,6 @@
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"volta": {
|
||||
"node": "24.11.0"
|
||||
"node": "24.11.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,14 +76,6 @@
|
||||
--immich-dark-gray: 33 33 33;
|
||||
}
|
||||
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: rgb(var(--immich-ui-default-border));
|
||||
}
|
||||
|
||||
button:not(:disabled),
|
||||
[role='button']:not(:disabled) {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
<script lang="ts">
|
||||
import type { ActionItem } from '$lib/types';
|
||||
import { IconButton, type IconButtonProps } from '@immich/ui';
|
||||
import { IconButton, type ActionItem } from '@immich/ui';
|
||||
|
||||
type Props = {
|
||||
action: ActionItem;
|
||||
};
|
||||
|
||||
const { action }: Props = $props();
|
||||
const { title, icon, color = 'secondary', props: other = {}, onSelect } = $derived(action);
|
||||
const onclick = (event: Event) => onSelect?.({ event, item: action });
|
||||
const { title, icon, color = 'secondary', onAction } = $derived(action);
|
||||
</script>
|
||||
|
||||
{#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}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
<script lang="ts">
|
||||
import type { ActionItem } from '$lib/types';
|
||||
import { Button, type ButtonProps, Text } from '@immich/ui';
|
||||
import { type ActionItem, Button, Text } from '@immich/ui';
|
||||
|
||||
type Props = {
|
||||
action: ActionItem;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
const { action }: Props = $props();
|
||||
const { title, icon, color = 'secondary', props: other = {}, onSelect } = $derived(action);
|
||||
const onclick = (event: Event) => onSelect?.({ event, item: action });
|
||||
const { action, title: titleAttr }: Props = $props();
|
||||
const { title, icon, color = 'secondary', onAction } = $derived(action);
|
||||
</script>
|
||||
|
||||
{#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>
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
<script lang="ts">
|
||||
import type { ActionItem } from '$lib/types';
|
||||
import { IconButton, type IconButtonProps } from '@immich/ui';
|
||||
import { IconButton, type ActionItem } from '@immich/ui';
|
||||
|
||||
type Props = {
|
||||
action: ActionItem;
|
||||
};
|
||||
|
||||
const { action }: Props = $props();
|
||||
const { title, icon, props: other = {}, onSelect } = $derived(action);
|
||||
const onclick = (event: Event) => onSelect?.({ event, item: action });
|
||||
const { title, icon, onAction } = $derived(action);
|
||||
</script>
|
||||
|
||||
{#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}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
.filter(Boolean)
|
||||
.join(' • ');
|
||||
|
||||
const SharedLinkActions = $derived(getSharedLinkActions($t, sharedLink));
|
||||
const { ViewQrCode, Copy } = $derived(getSharedLinkActions($t, sharedLink));
|
||||
</script>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
@@ -41,7 +41,7 @@
|
||||
<Text size="tiny" color="muted">{getShareProperties()}</Text>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<ActionButton action={SharedLinkActions.ViewQrCode} />
|
||||
<ActionButton action={SharedLinkActions.Copy} />
|
||||
<ActionButton action={ViewQrCode} />
|
||||
<ActionButton action={Copy} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
import { groupBy } from 'lodash-es';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { run } from 'svelte/legacy';
|
||||
|
||||
interface Props {
|
||||
ownedAlbums?: AlbumResponseDto[];
|
||||
@@ -128,65 +127,45 @@
|
||||
},
|
||||
};
|
||||
|
||||
let albums: AlbumResponseDto[] = $state([]);
|
||||
let filteredAlbums: AlbumResponseDto[] = $state([]);
|
||||
let groupedAlbums: AlbumGroup[] = $state([]);
|
||||
let albums = $derived.by(() => {
|
||||
switch (userSettings.filter) {
|
||||
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 selectedAlbum: AlbumResponseDto | undefined = $state();
|
||||
let isOpen = $state(false);
|
||||
|
||||
// Step 1: Filter between Owned and Shared albums, or both.
|
||||
run(() => {
|
||||
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 }),
|
||||
}));
|
||||
|
||||
// TODO get rid of this
|
||||
$effect(() => {
|
||||
albumGroupIds = groupedAlbums.map(({ id }) => id);
|
||||
});
|
||||
|
||||
@@ -231,7 +210,7 @@
|
||||
const result = await modalManager.show(AlbumShareModal, { album: selectedAlbum });
|
||||
switch (result?.action) {
|
||||
case 'sharedUsers': {
|
||||
await handleAddUsers(result.data);
|
||||
await handleAddUsers(selectedAlbum, result.data);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -300,22 +279,17 @@
|
||||
updateRecentAlbumInfo(album);
|
||||
};
|
||||
|
||||
const handleAddUsers = async (albumUsers: AlbumUserAddDto[]) => {
|
||||
if (!albumToShare) {
|
||||
return;
|
||||
}
|
||||
const handleAddUsers = async (album: AlbumResponseDto, albumUsers: AlbumUserAddDto[]) => {
|
||||
try {
|
||||
const album = await addUsersToAlbum({
|
||||
id: albumToShare.id,
|
||||
const updatedAlbum = await addUsersToAlbum({
|
||||
id: album.id,
|
||||
addUsersDto: {
|
||||
albumUsers,
|
||||
},
|
||||
});
|
||||
updateAlbumInfo(album);
|
||||
updateAlbumInfo(updatedAlbum);
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_add_album_users'));
|
||||
} finally {
|
||||
albumToShare = null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
let innerHeight: number = $state(0);
|
||||
let activityHeight: 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 message = $state('');
|
||||
let isSendingMessage = $state(false);
|
||||
@@ -96,11 +96,7 @@
|
||||
}
|
||||
isSendingMessage = false;
|
||||
};
|
||||
$effect(() => {
|
||||
if (innerHeight && activityHeight) {
|
||||
divHeight = innerHeight - activityHeight;
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (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
|
||||
// It is used to highlight the search query in the album name
|
||||
$effect(() => {
|
||||
const albumNameArray: string[] = $derived.by(() => {
|
||||
let { albumName } = album;
|
||||
let findIndex = normalizeSearchString(albumName).indexOf(normalizeSearchString(searchQuery));
|
||||
let findLength = searchQuery.length;
|
||||
albumNameArray = [
|
||||
return [
|
||||
albumName.slice(0, findIndex),
|
||||
albumName.slice(findIndex, findIndex + findLength),
|
||||
albumName.slice(findIndex + findLength),
|
||||
|
||||
@@ -395,12 +395,12 @@
|
||||
}
|
||||
});
|
||||
|
||||
let currentAssetId = $derived(asset.id);
|
||||
// primarily, this is reactive on `asset`
|
||||
$effect(() => {
|
||||
if (currentAssetId) {
|
||||
untrack(() => handlePromiseError(handleGetAllAlbums()));
|
||||
ocrManager.clear();
|
||||
handlePromiseError(ocrManager.getAssetOcr(currentAssetId));
|
||||
handlePromiseError(handleGetAllAlbums());
|
||||
ocrManager.clear();
|
||||
if (!sharedLink) {
|
||||
handlePromiseError(ocrManager.getAssetOcr(asset.id));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
import { Icon, IconButton, LoadingSpinner, modalManager } from '@immich/ui';
|
||||
import {
|
||||
mdiCalendar,
|
||||
mdiCamera,
|
||||
mdiCameraIris,
|
||||
mdiClose,
|
||||
mdiEye,
|
||||
@@ -372,9 +373,9 @@
|
||||
</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><Icon icon={mdiCameraIris} size="24" /></div>
|
||||
<div><Icon icon={mdiCamera} size="24" /></div>
|
||||
|
||||
<div>
|
||||
{#if asset.exifInfo?.make || asset.exifInfo?.model}
|
||||
@@ -395,20 +396,34 @@
|
||||
</p>
|
||||
{/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}
|
||||
<div class="flex gap-2 text-sm">
|
||||
<p>
|
||||
<a
|
||||
href={resolve(
|
||||
`${AppRoute.SEARCH}?${getMetadataSearchQuery({ lensModel: asset.exifInfo.lensModel })}`,
|
||||
)}
|
||||
title="{$t('search_for')} {asset.exifInfo.lensModel}"
|
||||
class="hover:text-primary line-clamp-1"
|
||||
>
|
||||
{asset.exifInfo.lensModel}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
<a
|
||||
href={resolve(`${AppRoute.SEARCH}?${getMetadataSearchQuery({ lensModel: asset.exifInfo.lensModel })}`)}
|
||||
title="{$t('search_for')} {asset.exifInfo.lensModel}"
|
||||
class="hover:text-primary line-clamp-1"
|
||||
>
|
||||
{asset.exifInfo.lensModel}
|
||||
</a>
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-2 text-sm">
|
||||
@@ -416,19 +431,9 @@
|
||||
<p>ƒ/{asset.exifInfo.fNumber.toLocaleString($locale)}</p>
|
||||
{/if}
|
||||
|
||||
{#if asset.exifInfo.exposureTime}
|
||||
<p>{`${asset.exifInfo.exposureTime} s`}</p>
|
||||
{/if}
|
||||
|
||||
{#if asset.exifInfo.focalLength}
|
||||
<p>{`${asset.exifInfo.focalLength.toLocaleString($locale)} mm`}</p>
|
||||
{/if}
|
||||
|
||||
{#if asset.exifInfo.iso}
|
||||
<p>
|
||||
{`ISO ${asset.exifInfo.iso}`}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -171,7 +171,6 @@
|
||||
|
||||
$effect(() => {
|
||||
if (assetFileUrl) {
|
||||
// this can't be in an async context with $effect
|
||||
void cast(assetFileUrl);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -43,7 +43,9 @@
|
||||
|
||||
let videoPlayer: HTMLVideoElement | undefined = $state();
|
||||
let isLoading = $state(true);
|
||||
let assetFileUrl = $state('');
|
||||
let assetFileUrl = $derived(
|
||||
playOriginalVideo ? getAssetOriginalUrl({ id: assetId, cacheKey }) : getAssetPlaybackUrl({ id: assetId, cacheKey }),
|
||||
);
|
||||
let isScrubbing = $state(false);
|
||||
let showVideo = $state(false);
|
||||
|
||||
@@ -53,11 +55,9 @@
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
assetFileUrl = playOriginalVideo
|
||||
? getAssetOriginalUrl({ id: assetId, cacheKey })
|
||||
: getAssetPlaybackUrl({ id: assetId, cacheKey });
|
||||
if (videoPlayer) {
|
||||
videoPlayer.load();
|
||||
// reactive on `assetFileUrl` changes
|
||||
if (assetFileUrl) {
|
||||
videoPlayer?.load();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
|
||||
$effect(() => {
|
||||
if (assetFileUrl) {
|
||||
// this can't be in an async context with $effect
|
||||
void cast(assetFileUrl);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { getAssetOriginalUrl, getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils';
|
||||
import { timeToSeconds } from '$lib/utils/date-time';
|
||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||
import { AssetMediaSize, AssetVisibility } from '@immich/sdk';
|
||||
import { AssetMediaSize, AssetVisibility, type UserResponseDto } from '@immich/sdk';
|
||||
import {
|
||||
mdiArchiveArrowDownOutline,
|
||||
mdiCameraBurst,
|
||||
@@ -46,6 +46,7 @@
|
||||
imageClass?: ClassValue;
|
||||
brokenAssetClass?: ClassValue;
|
||||
dimmed?: boolean;
|
||||
albumUsers?: UserResponseDto[];
|
||||
onClick?: (asset: TimelineAsset) => void;
|
||||
onSelect?: (asset: TimelineAsset) => void;
|
||||
onMouseEvent?: (event: { isMouseOver: boolean; selectedGroupIndex: number }) => void;
|
||||
@@ -64,6 +65,7 @@
|
||||
readonly = false,
|
||||
showArchiveIcon = false,
|
||||
showStackedIcon = true,
|
||||
albumUsers = [],
|
||||
onClick = undefined,
|
||||
onSelect = undefined,
|
||||
onMouseEvent = undefined,
|
||||
@@ -85,6 +87,8 @@
|
||||
let width = $derived(thumbnailSize || thumbnailWidth || 235);
|
||||
let height = $derived(thumbnailSize || thumbnailHeight || 235);
|
||||
|
||||
let assetOwner = $derived(albumUsers?.find((user) => user.id === asset.ownerId) ?? null);
|
||||
|
||||
const onIconClickedHandler = (e?: MouseEvent) => {
|
||||
e?.stopPropagation();
|
||||
e?.preventDefault();
|
||||
@@ -220,6 +224,8 @@
|
||||
bind:this={element}
|
||||
data-asset={asset.id}
|
||||
data-thumbnail-focus-container
|
||||
data-selected={selected || undefined}
|
||||
data-disabled={disabled || undefined}
|
||||
tabindex={0}
|
||||
role="link"
|
||||
>
|
||||
@@ -264,20 +270,28 @@
|
||||
<!-- Favorite asset star -->
|
||||
{#if !authManager.isSharedLink && asset.isFavorite}
|
||||
<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>
|
||||
{/if}
|
||||
|
||||
{#if !!assetOwner}
|
||||
<div class="absolute bottom-1 end-2 max-w-[50%]">
|
||||
<p class="text-xs font-medium text-white drop-shadow-lg max-w-[100%] truncate">
|
||||
{assetOwner.name}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !authManager.isSharedLink && showArchiveIcon && asset.visibility === AssetVisibility.Archive}
|
||||
<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>
|
||||
{/if}
|
||||
|
||||
{#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">
|
||||
<span class="pe-2 pt-2">
|
||||
<Icon icon={mdiRotate360} size="24" />
|
||||
<Icon data-icon-equirectangular icon={mdiRotate360} size="24" />
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -285,7 +299,7 @@
|
||||
{#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">
|
||||
<span class="pe-2 pt-2">
|
||||
<Icon icon={mdiFileGifBox} size="24" />
|
||||
<Icon data-icon-playable icon={mdiFileGifBox} size="24" />
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -300,7 +314,7 @@
|
||||
>
|
||||
<span class="pe-2 pt-2 flex place-items-center gap-1">
|
||||
<p>{asset.stack.assetCount.toLocaleString($locale)}</p>
|
||||
<Icon icon={mdiCameraBurst} size="24" />
|
||||
<Icon data-icon-stack icon={mdiCameraBurst} size="24" />
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -366,7 +380,7 @@
|
||||
/>
|
||||
<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">
|
||||
<Icon icon={mdiMotionPauseOutline} size="24" />
|
||||
<Icon data-icon-playable-pause icon={mdiMotionPauseOutline} size="24" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -406,13 +420,13 @@
|
||||
{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}
|
||||
<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>
|
||||
{: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}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Badge from '$lib/elements/Badge.svelte';
|
||||
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 {
|
||||
mdiAlertCircle,
|
||||
@@ -23,7 +23,7 @@
|
||||
subtitle: string | undefined;
|
||||
description: Component | undefined;
|
||||
statistics: QueueStatisticsDto;
|
||||
queueStatus: QueueStatusDto;
|
||||
queueStatus: QueueStatusLegacyDto;
|
||||
icon: string;
|
||||
disabled?: boolean;
|
||||
allText: string | undefined;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
QueueCommand,
|
||||
type QueueCommandDto,
|
||||
QueueName,
|
||||
type QueuesResponseDto,
|
||||
type QueuesResponseLegacyDto,
|
||||
runQueueCommandLegacy,
|
||||
} from '@immich/sdk';
|
||||
import { modalManager, toastManager } from '@immich/ui';
|
||||
@@ -29,7 +29,7 @@
|
||||
import StorageMigrationDescription from './StorageMigrationDescription.svelte';
|
||||
|
||||
interface Props {
|
||||
jobs: QueuesResponseDto;
|
||||
jobs: QueuesResponseLegacyDto;
|
||||
}
|
||||
|
||||
let { jobs = $bindable() }: Props = $props();
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
import NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
|
||||
import AdminSidebar from '$lib/sidebars/AdminSidebar.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';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
breadcrumbs: BreadcrumbItem[];
|
||||
buttons?: Snippet;
|
||||
children?: Snippet;
|
||||
};
|
||||
|
||||
let { title, buttons, children }: Props = $props();
|
||||
let { breadcrumbs, buttons, children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<AppShell>
|
||||
@@ -24,7 +24,7 @@
|
||||
<AdminSidebar />
|
||||
</AppShellSidebar>
|
||||
|
||||
<TitleLayout {title} {buttons}>
|
||||
<TitleLayout {breadcrumbs} {buttons}>
|
||||
<Scrollable class="grow">
|
||||
<PageContent>
|
||||
{@render children?.()}
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
<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';
|
||||
|
||||
interface Props {
|
||||
id?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
type Props = {
|
||||
breadcrumbs: BreadcrumbItem[];
|
||||
buttons?: Snippet;
|
||||
children?: Snippet;
|
||||
}
|
||||
};
|
||||
|
||||
let { id, title, description, buttons, children }: Props = $props();
|
||||
let { breadcrumbs, buttons, children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<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 gap-1">
|
||||
<div class="font-medium outline-none" tabindex="-1" {id}>{title}</div>
|
||||
{#if description}
|
||||
<Text color="muted">{description}</Text>
|
||||
{/if}
|
||||
</div>
|
||||
<Breadcrumbs items={breadcrumbs} separator={mdiSlashForward} />
|
||||
{@render buttons?.()}
|
||||
</div>
|
||||
{@render children?.()}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
import { type PlacesGroup, getSelectedPlacesGroupOption } from '$lib/utils/places-utils';
|
||||
import { Icon } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { run } from 'svelte/legacy';
|
||||
|
||||
interface Props {
|
||||
places?: AssetResponseDto[];
|
||||
@@ -70,39 +69,27 @@
|
||||
},
|
||||
};
|
||||
|
||||
let filteredPlaces: AssetResponseDto[] = $state([]);
|
||||
let groupedPlaces: PlacesGroup[] = $state([]);
|
||||
const filteredPlaces = $derived.by(() => {
|
||||
const searchQueryNormalized = normalizeSearchString(searchQuery);
|
||||
return searchQueryNormalized
|
||||
? places.filter((place) => normalizeSearchString(place.exifInfo?.city ?? '').includes(searchQueryNormalized))
|
||||
: places;
|
||||
});
|
||||
|
||||
let placesGroupOption: string = $state(PlacesGroupBy.None);
|
||||
|
||||
let hasPlaces = $derived(places.length > 0);
|
||||
|
||||
// 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;
|
||||
}
|
||||
const placesGroupOption: string = $derived(getSelectedPlacesGroupOption(userSettings));
|
||||
const groupingFunction = $derived(groupOptions[placesGroupOption] ?? groupOptions[PlacesGroupBy.None]);
|
||||
const groupedPlaces: PlacesGroup[] = $derived(groupingFunction(filteredPlaces));
|
||||
|
||||
$effect(() => {
|
||||
searchResultCount = filteredPlaces.length;
|
||||
});
|
||||
|
||||
// Step 2: Group places.
|
||||
run(() => {
|
||||
placesGroupOption = getSelectedPlacesGroupOption(userSettings);
|
||||
const groupFunc = groupOptions[placesGroupOption] ?? groupOptions[PlacesGroupBy.None];
|
||||
groupedPlaces = groupFunc(filteredPlaces);
|
||||
|
||||
$effect(() => {
|
||||
placesGroupIds = groupedPlaces.map(({ id }) => id);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if hasPlaces}
|
||||
{#if places.length > 0}
|
||||
<!-- Album Cards -->
|
||||
{#if placesGroupOption === PlacesGroupBy.None}
|
||||
<PlacesCardGroup places={groupedPlaces[0].places} />
|
||||
|
||||
@@ -7,20 +7,11 @@
|
||||
import { mdiCameraIris, mdiChartPie, mdiPlayCircle } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
stats?: ServerStatsResponseDto;
|
||||
}
|
||||
type Props = {
|
||||
stats: ServerStatsResponseDto;
|
||||
};
|
||||
|
||||
let {
|
||||
stats = {
|
||||
photos: 0,
|
||||
videos: 0,
|
||||
usage: 0,
|
||||
usagePhotos: 0,
|
||||
usageVideos: 0,
|
||||
usageByUser: [],
|
||||
},
|
||||
}: Props = $props();
|
||||
const { stats }: Props = $props();
|
||||
|
||||
const zeros = (value: number) => {
|
||||
const maxLength = 13;
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
let { asset = undefined, point: initialPoint, onClose }: Props = $props();
|
||||
|
||||
let places: PlacesResponseDto[] = $state([]);
|
||||
let suggestedPlaces: PlacesResponseDto[] = $state([]);
|
||||
let suggestedPlaces: PlacesResponseDto[] = $derived(places.slice(0, 5));
|
||||
let searchWord: string = $state('');
|
||||
let latestSearchTimeout: number;
|
||||
let showLoadingSpinner = $state(false);
|
||||
@@ -52,9 +52,6 @@
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (places) {
|
||||
suggestedPlaces = places.slice(0, 5);
|
||||
}
|
||||
if (searchWord === '') {
|
||||
suggestedPlaces = [];
|
||||
}
|
||||
|
||||
@@ -33,37 +33,36 @@
|
||||
children,
|
||||
}: Props = $props();
|
||||
|
||||
let left: number = $state(0);
|
||||
let top: number = $state(0);
|
||||
const swap = (direction: string) => (direction === 'left' ? 'right' : 'left');
|
||||
|
||||
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
|
||||
// of zero when starting the 'slide' animation.
|
||||
let height: number = $state(0);
|
||||
|
||||
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>
|
||||
|
||||
<div
|
||||
bind:clientHeight={height}
|
||||
class="fixed min-w-50 w-max max-w-75 overflow-hidden rounded-lg shadow-lg z-1"
|
||||
style:left="{left}px"
|
||||
style:top="{top}px"
|
||||
style:left="{position.left}px"
|
||||
style:top="{position.top}px"
|
||||
transition:slide={{ duration: 250, easing: quintOut }}
|
||||
use:clickOutside={{ onOutclick: onClose }}
|
||||
onintroend={() => {
|
||||
|
||||
@@ -64,10 +64,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
let makeFilter = $derived(filters.make);
|
||||
let modelFilter = $derived(filters.model);
|
||||
let lensModelFilter = $derived(filters.lensModel);
|
||||
const makeFilter = $derived(filters.make);
|
||||
const modelFilter = $derived(filters.model);
|
||||
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(() => {
|
||||
handlePromiseError(updateMakes());
|
||||
});
|
||||
|
||||
@@ -7,11 +7,10 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy';
|
||||
|
||||
import Combobox, { asComboboxOptions, asSelectedOption } from '$lib/components/shared-components/combobox.svelte';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { getSearchSuggestions, SearchSuggestionType } from '@immich/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
@@ -66,15 +65,12 @@
|
||||
}
|
||||
let countryFilter = $derived(filters.country);
|
||||
let stateFilter = $derived(filters.state);
|
||||
run(() => {
|
||||
handlePromiseError(updateCountries());
|
||||
});
|
||||
run(() => {
|
||||
handlePromiseError(updateStates(countryFilter));
|
||||
});
|
||||
run(() => {
|
||||
handlePromiseError(updateCities(countryFilter, stateFilter));
|
||||
});
|
||||
|
||||
// TODO replace by async $derived, at the latest when it's in stable https://svelte.dev/docs/svelte/await-expressions
|
||||
$effect(() => handlePromiseError(updateStates(countryFilter)));
|
||||
$effect(() => handlePromiseError(updateCities(countryFilter, stateFilter)));
|
||||
|
||||
onMount(() => updateCountries());
|
||||
</script>
|
||||
|
||||
<div id="location-selection">
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
let { sharedLink, preload = false, class: className = '' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="relative shrink-0 size-24">
|
||||
<div class="relative shrink-0 size-22">
|
||||
{#if sharedLink?.album}
|
||||
<AlbumCover album={sharedLink.album} class={className} {preload} />
|
||||
{:else if sharedLink.assets[0]}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
import ActionButton from '$lib/components/ActionButton.svelte';
|
||||
import ShareCover from '$lib/components/sharedlinks-page/covers/share-cover.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import Badge from '$lib/elements/Badge.svelte';
|
||||
import { getSharedLinkActions } from '$lib/services/shared-link.service';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk';
|
||||
import { Badge, ContextMenuButton, MenuItemType, Text } from '@immich/ui';
|
||||
import { DateTime, type ToRelativeUnit } from 'luxon';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
}
|
||||
};
|
||||
|
||||
const SharedLinkActions = $derived(getSharedLinkActions($t, sharedLink));
|
||||
const { Edit, Copy, Delete } = $derived(getSharedLinkActions($t, sharedLink));
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -44,64 +44,65 @@
|
||||
>
|
||||
<ShareCover class="transition-all duration-300 hover:shadow-lg" {sharedLink} />
|
||||
|
||||
<div class="flex flex-col justify-between">
|
||||
<div class="info-top">
|
||||
<div class="font-mono text-xs font-semibold text-gray-500 dark:text-gray-400">
|
||||
{#if isExpired}
|
||||
<p class="font-bold text-red-600 dark:text-red-400">{$t('expired')}</p>
|
||||
{:else if expiresAt}
|
||||
<p>
|
||||
{$t('expires_date', { values: { date: getCountDownExpirationDate(expiresAt, now) } })}
|
||||
</p>
|
||||
{:else}
|
||||
<p>{$t('expires_date', { values: { date: '∞' } })}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Text size="large" color="primary" class="flex place-items-center gap-2 break-all">
|
||||
{#if sharedLink.type === SharedLinkType.Album}
|
||||
{sharedLink.album?.albumName}
|
||||
{:else if sharedLink.type === SharedLinkType.Individual}
|
||||
{$t('individual_share')}
|
||||
{/if}
|
||||
</Text>
|
||||
|
||||
<div class="text-sm pb-2">
|
||||
<p class="flex place-items-center gap-2 text-primary break-all uppercase">
|
||||
{#if sharedLink.type === SharedLinkType.Album}
|
||||
{sharedLink.album?.albumName}
|
||||
{:else if sharedLink.type === SharedLinkType.Individual}
|
||||
{$t('individual_share')}
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
<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>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#if isExpired}
|
||||
<Badge size="small" color="danger">{$t('expired')}</Badge>
|
||||
{:else if expiresAt}
|
||||
<Badge size="small" color="secondary">
|
||||
{$t('expires_date', { values: { date: getCountDownExpirationDate(expiresAt, now) } })}
|
||||
</Badge>
|
||||
{:else}
|
||||
<Badge size="small" color="secondary">{$t('expires_date', { values: { date: '∞' } })}</Badge>
|
||||
{/if}
|
||||
|
||||
{#if sharedLink.allowDownload}
|
||||
<Badge rounded="full"><span class="text-xs px-1">{$t('download')}</span></Badge>
|
||||
{#if sharedLink.slug}
|
||||
<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 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 sharedLink.password}
|
||||
<Badge rounded="full"><span class="text-xs px-1">{$t('password')}</span></Badge>
|
||||
{/if}
|
||||
{#if sharedLink.slug}
|
||||
<Badge rounded="full"><span class="text-xs px-1">{$t('custom_url')}</span></Badge>
|
||||
<Badge size="small" color="secondary">{$t('password')}</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if sharedLink.description}
|
||||
<Text size="small" class="line-clamp-1">{sharedLink.description}</Text>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:element>
|
||||
<div class="flex flex-auto flex-col place-content-center place-items-end text-end ms-4">
|
||||
<div class="sm:flex hidden">
|
||||
<ActionButton action={SharedLinkActions.Edit} />
|
||||
<ActionButton action={SharedLinkActions.Copy} />
|
||||
<ActionButton action={SharedLinkActions.Delete} />
|
||||
<ActionButton action={Edit} />
|
||||
<ActionButton action={Copy} />
|
||||
<ActionButton action={Delete} />
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user