refactor: star rating (#26357)

* refactor: star rating

* transform rating 0 to null in controller dto

* migrate rating 0 to null

* deprecate rating -1

* rating type annotation

* update Rating type
This commit is contained in:
Mees Frensel
2026-02-26 14:54:20 +01:00
committed by GitHub
parent 4c79c3c902
commit e454c3566b
24 changed files with 294 additions and 127 deletions
+1 -1
View File
@@ -516,7 +516,7 @@ export class AssetService extends BaseService {
dateTimeOriginal?: string;
latitude?: number;
longitude?: number;
rating?: number;
rating?: number | null;
}) {
const { id, description, dateTimeOriginal, latitude, longitude, rating } = dto;
const writes = _.omitBy(
@@ -1423,6 +1423,20 @@ describe(MetadataService.name, () => {
);
});
it('should handle 0 as unrated -> null', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mockReadTags({ Rating: 0 });
await sut.handleMetadataExtraction({ id: asset.id });
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
expect.objectContaining({
rating: null,
}),
{ lockedPropertiesBehavior: 'skip' },
);
});
it('should handle valid negative rating value', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
@@ -1780,6 +1794,28 @@ describe(MetadataService.name, () => {
'timeZone',
]);
});
it('should write rating', async () => {
const asset = factory.jobAssets.sidecarWrite();
asset.exifInfo.rating = 4;
mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue(['rating']);
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(asset);
await expect(sut.handleSidecarWrite({ id: asset.id })).resolves.toBe(JobStatus.Success);
expect(mocks.metadata.writeTags).toHaveBeenCalledWith(asset.files[0].path, { Rating: 4 });
expect(mocks.asset.unlockProperties).toHaveBeenCalledWith(asset.id, ['rating']);
});
it('should write null rating as 0', async () => {
const asset = factory.jobAssets.sidecarWrite();
asset.exifInfo.rating = null;
mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue(['rating']);
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(asset);
await expect(sut.handleSidecarWrite({ id: asset.id })).resolves.toBe(JobStatus.Success);
expect(mocks.metadata.writeTags).toHaveBeenCalledWith(asset.files[0].path, { Rating: 0 });
expect(mocks.asset.unlockProperties).toHaveBeenCalledWith(asset.id, ['rating']);
});
});
describe('firstDateTime', () => {
+2 -2
View File
@@ -301,7 +301,7 @@ export class MetadataService extends BaseService {
// comments
description: String(exifTags.ImageDescription || exifTags.Description || '').trim(),
profileDescription: exifTags.ProfileDescription || null,
rating: validateRange(exifTags.Rating, -1, 5),
rating: exifTags.Rating === 0 ? null : validateRange(exifTags.Rating, -1, 5),
// grouping
livePhotoCID: (exifTags.ContentIdentifier || exifTags.MediaGroupUUID) ?? null,
@@ -451,7 +451,7 @@ export class MetadataService extends BaseService {
dateTimeOriginal: asset.exifInfo.dateTimeOriginal as string | null,
latitude: asset.exifInfo.latitude,
longitude: asset.exifInfo.longitude,
rating: asset.exifInfo.rating,
rating: asset.exifInfo.rating ?? 0,
tags: asset.exifInfo.tags,
timeZone: asset.exifInfo.timeZone,
},