refactor(server): allow -1 rating again (#28886)

This commit is contained in:
Timon 2026-06-10 10:55:51 +02:00 committed by GitHub
parent 1ce961fbb3
commit 5c38373808
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 52 additions and 45 deletions

View file

@ -492,6 +492,20 @@ describe('/asset', () => {
expect(status).toEqual(200);
});
it('should set the negative rating', async () => {
const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ rating: -1 });
expect(body).toMatchObject({
id: user1Assets[0].id,
exifInfo: expect.objectContaining({
rating: -1,
}),
});
expect(status).toEqual(200);
});
it('should return tagged people', async () => {
const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`)

View file

@ -95,9 +95,9 @@ class AssetBulkUpdateDto {
///
Optional<num?> longitude;
/// Rating in range [1-5], or null for unrated
/// Rating in range [1-5] (starred), -1 (rejected), or null (unrated)
///
/// Minimum value: 1
/// Minimum value: -1
/// Maximum value: 5
Optional<int?> rating;

View file

@ -77,9 +77,9 @@ class UpdateAssetDto {
///
Optional<num?> longitude;
/// Rating in range [1-5], or null for unrated
/// Rating in range [1-5] (starred), -1 (rejected), or null (unrated)
///
/// Minimum value: 1
/// Minimum value: -1
/// Maximum value: 5
Optional<int?> rating;

View file

@ -16602,9 +16602,9 @@
"type": "number"
},
"rating": {
"description": "Rating in range [1-5], or null for unrated",
"description": "Rating in range [1-5] (starred), -1 (rejected), or null (unrated)",
"maximum": 5,
"minimum": 1,
"minimum": -1,
"nullable": true,
"type": "integer",
"x-immich-history": [
@ -16616,15 +16616,10 @@
"version": "v2",
"state": "Stable"
},
{
"version": "v2.6.0",
"state": "Updated",
"description": "Using -1 as a rating is deprecated and will be removed in the next major version."
},
{
"version": "v3",
"state": "Updated",
"description": "Using -1 as a rating is no longer valid."
"description": "Using 0 as a rating is no longer valid."
}
],
"x-immich-state": "Stable"
@ -26430,9 +26425,9 @@
"type": "number"
},
"rating": {
"description": "Rating in range [1-5], or null for unrated",
"description": "Rating in range [1-5] (starred), -1 (rejected), or null (unrated)",
"maximum": 5,
"minimum": 1,
"minimum": -1,
"nullable": true,
"type": "integer",
"x-immich-history": [
@ -26444,15 +26439,10 @@
"version": "v2",
"state": "Stable"
},
{
"version": "v2.6.0",
"state": "Updated",
"description": "Using -1 as a rating is deprecated and will be removed in the next major version."
},
{
"version": "v3",
"state": "Updated",
"description": "Using -1 as a rating is no longer valid."
"description": "Using 0 as a rating is no longer valid."
}
],
"x-immich-state": "Stable"

View file

@ -672,7 +672,7 @@ export type AssetBulkUpdateDto = {
latitude?: number;
/** Longitude coordinate */
longitude?: number;
/** Rating in range [1-5], or null for unrated */
/** Rating in range [1-5] (starred), -1 (rejected), or null (unrated) */
rating?: number | null;
/** Time zone (IANA timezone) */
timeZone?: string;
@ -919,7 +919,7 @@ export type UpdateAssetDto = {
livePhotoVideoId?: string | null;
/** Longitude coordinate */
longitude?: number;
/** Rating in range [1-5], or null for unrated */
/** Rating in range [1-5] (starred), -1 (rejected), or null (unrated) */
rating?: number | null;
visibility?: AssetVisibility;
};

View file

@ -240,7 +240,16 @@ describe(AssetController.name, () => {
for (const [test, errors] of [
[{ rating: 7 }, [{ path: ['rating'], message: 'Too big: expected number to be <=5' }]],
[{ rating: 3.5 }, [{ path: ['rating'], message: 'Invalid input: expected int, received number' }]],
[{ rating: -2 }, [{ path: ['rating'], message: 'Too small: expected number to be >=1' }]],
[{ rating: -2 }, [{ path: ['rating'], message: 'Too small: expected number to be >=-1' }]],
[
{ rating: 0 },
[
{
path: ['rating'],
message: 'Rating must be -1 (rejected), 15 (starred), or null (unrated); 0 is not valid',
},
],
],
] as const) {
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}`).send(test);
expect(status).toBe(400);

View file

@ -15,16 +15,18 @@ const UpdateAssetBaseSchema = z
longitude: longitudeSchema.optional().describe('Longitude coordinate'),
rating: z
.int()
.min(1)
.min(-1)
.max(5)
.nullish()
.describe('Rating in range [1-5], or null for unrated')
.refine((v) => v !== 0, {
error: 'Rating must be -1 (rejected), 15 (starred), or null (unrated); 0 is not valid',
})
.describe('Rating in range [1-5] (starred), -1 (rejected), or null (unrated)')
.meta({
...new HistoryBuilder()
.added('v1')
.stable('v2')
.updated('v2.6.0', 'Using -1 as a rating is deprecated and will be removed in the next major version.')
.updated('v3', 'Using -1 as a rating is no longer valid.')
.updated('v3', 'Using 0 as a rating is no longer valid.')
.getExtensions(),
}),
description: z.string().optional().describe('Asset description'),

View file

@ -1,7 +1,5 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`UPDATE "asset_exif" SET "rating" = NULL WHERE "rating" = -1;`.execute(db);
export async function up(): Promise<void> {
// await sql`UPDATE "asset_exif" SET "rating" = NULL WHERE "rating" = -1;`.execute(db);
}
export async function down(): Promise<void> {

View file

@ -13,9 +13,9 @@
let { asset, isOwner }: Props = $props();
let rating = $derived(asset.exifInfo?.rating || null) as Rating;
let rating = $derived(asset.exifInfo?.rating ?? null) as Rating;
const handleChangeRating = async (rating: number | null) => {
const handleChangeRating = async (rating: Rating) => {
try {
await updateAsset({ id: asset.id, updateAssetDto: { rating } });
} catch (error) {

View file

@ -6,7 +6,7 @@
import { mdiStar, mdiStarOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
export type Rating = 1 | 2 | 3 | 4 | 5 | null;
export type Rating = -1 | 1 | 2 | 3 | 4 | 5 | null;
interface Props {
count?: number;
@ -33,6 +33,7 @@
return;
}
ratingSelection = newRating;
onRating(newRating);
};
@ -70,7 +71,7 @@
<div class="flex flex-row" data-testid="star-container">
{#each { length: count } as _, index (index)}
{@const value = index + 1}
{@const filled = hoverRating === null ? (ratingSelection || 0) >= value : hoverRating >= value}
{@const filled = hoverRating === null ? (ratingSelection ?? 0) >= value : hoverRating >= value}
{@const starId = `${id}-${value}`}
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
@ -102,14 +103,7 @@
</div>
</fieldset>
{#if ratingSelection !== null && !readOnly}
<button
type="button"
onclick={() => {
ratingSelection = null;
handleSelect(ratingSelection);
}}
class="cursor-pointer text-xs text-primary"
>
<button type="button" onclick={() => handleSelect(null)} class="cursor-pointer text-xs text-primary">
{$t('rating_clear')}
</button>
{/if}

View file

@ -15,7 +15,7 @@ import {
fromTimelinePlainDate,
fromTimelinePlainDateTime,
fromTimelinePlainYearMonth,
fromISODateTimeUTC,
fromISODateTimeUTCToObject,
getTimes,
setDifference,
type TimelineDateTime,
@ -190,7 +190,7 @@ export class TimelineMonth {
isVideo: !bucketAssets.isImage[i],
livePhotoVideoId: bucketAssets.livePhotoVideoId[i],
localDateTime,
createdAt: fromISODateTimeUTC(bucketAssets.createdAt[i]).setZone('local'),
createdAt: fromISODateTimeUTCToObject(bucketAssets.createdAt[i]),
fileCreatedAt,
ownerId: bucketAssets.ownerId[i],
projectionType: bucketAssets.projectionType[i],