diff --git a/docs/docs/features/searching.md b/docs/docs/features/searching.md index 13547f6ba..eed5faa6f 100644 --- a/docs/docs/features/searching.md +++ b/docs/docs/features/searching.md @@ -31,6 +31,7 @@ The filters smart search allows you to search by include: - Not in any album - Archived - Favorited + - Rating diff --git a/i18n/en.json b/i18n/en.json index 72559d450..b6f75ce4f 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1134,6 +1134,7 @@ "search_timezone": "Search timezone...", "search_type": "Search type", "search_your_photos": "Search your photos", + "search_rating": "Search by rating...", "searching_locales": "Searching locales...", "second": "Second", "see_all_people": "See all people", diff --git a/mobile/openapi/lib/model/metadata_search_dto.dart b/mobile/openapi/lib/model/metadata_search_dto.dart index 3a3c14144..3fb003d16 100644 Binary files a/mobile/openapi/lib/model/metadata_search_dto.dart and b/mobile/openapi/lib/model/metadata_search_dto.dart differ diff --git a/mobile/openapi/lib/model/random_search_dto.dart b/mobile/openapi/lib/model/random_search_dto.dart index c63d7e82f..10727ec10 100644 Binary files a/mobile/openapi/lib/model/random_search_dto.dart and b/mobile/openapi/lib/model/random_search_dto.dart differ diff --git a/mobile/openapi/lib/model/smart_search_dto.dart b/mobile/openapi/lib/model/smart_search_dto.dart index c81e1519b..f377c23f2 100644 Binary files a/mobile/openapi/lib/model/smart_search_dto.dart and b/mobile/openapi/lib/model/smart_search_dto.dart differ diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 5b5c3a150..14245e11b 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -9956,6 +9956,11 @@ "previewPath": { "type": "string" }, + "rating": { + "maximum": 5, + "minimum": -1, + "type": "number" + }, "size": { "maximum": 1000, "minimum": 1, @@ -10613,6 +10618,11 @@ }, "type": "array" }, + "rating": { + "maximum": 5, + "minimum": -1, + "type": "number" + }, "size": { "maximum": 1000, "minimum": 1, @@ -11563,6 +11573,11 @@ "query": { "type": "string" }, + "rating": { + "maximum": 5, + "minimum": -1, + "type": "number" + }, "size": { "maximum": 1000, "minimum": 1, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index d4b36a04f..9ff35331f 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -811,6 +811,7 @@ export type MetadataSearchDto = { page?: number; personIds?: string[]; previewPath?: string; + rating?: number; size?: number; state?: string | null; tagIds?: string[]; @@ -878,6 +879,7 @@ export type RandomSearchDto = { make?: string; model?: string | null; personIds?: string[]; + rating?: number; size?: number; state?: string | null; tagIds?: string[]; @@ -914,6 +916,7 @@ export type SmartSearchDto = { page?: number; personIds?: string[]; query: string; + rating?: number; size?: number; state?: string | null; tagIds?: string[]; diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts index 6cf34debe..3589331c7 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -114,6 +114,12 @@ class BaseSearchDto { @ValidateUUID({ each: true, optional: true }) tagIds?: string[]; + + @Optional() + @IsInt() + @Max(5) + @Min(-1) + rating?: number; } export class RandomSearchDto extends BaseSearchDto { diff --git a/server/src/entities/asset.entity.ts b/server/src/entities/asset.entity.ts index 8ff4130ed..fd69673eb 100644 --- a/server/src/entities/asset.entity.ts +++ b/server/src/entities/asset.entity.ts @@ -387,6 +387,11 @@ export function searchAssetBuilder(kysely: Kysely, options: AssetSearchBuild .innerJoin('exif', 'assets.id', 'exif.assetId') .where('exif.lensModel', options.lensModel === null ? 'is' : '=', options.lensModel!), ) + .$if(options.rating !== undefined, (qb) => + qb + .innerJoin('exif', 'assets.id', 'exif.assetId') + .where('exif.rating', options.rating === null ? 'is' : '=', options.rating!), + ) .$if(!!options.checksum, (qb) => qb.where('assets.checksum', '=', options.checksum!)) .$if(!!options.deviceAssetId, (qb) => qb.where('assets.deviceAssetId', '=', options.deviceAssetId!)) .$if(!!options.deviceId, (qb) => qb.where('assets.deviceId', '=', options.deviceId!)) diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index a6eb5c7a8..2f313aa08 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -109,6 +109,7 @@ export interface SearchExifOptions { model?: string | null; state?: string | null; description?: string | null; + rating?: number | null; } export interface SearchEmbeddingOptions { diff --git a/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte b/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte index 817001033..4fc646b20 100644 --- a/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte @@ -14,6 +14,7 @@ date: SearchDateFilter; display: SearchDisplayFilters; mediaType: MediaType; + rating?: number; }; @@ -26,6 +27,7 @@ import SearchCameraSection, { type SearchCameraFilter } from './search-camera-section.svelte'; import SearchDateSection from './search-date-section.svelte'; import SearchMediaSection from './search-media-section.svelte'; + import SearchRatingsSection from './search-ratings-section.svelte'; import { parseUtcDate } from '$lib/utils/date-time'; import SearchDisplaySection from './search-display-section.svelte'; import SearchTextSection from './search-text-section.svelte'; @@ -34,6 +36,7 @@ import { mdiTune } from '@mdi/js'; import { generateId } from '$lib/utils/generate-id'; import { SvelteSet } from 'svelte/reactivity'; + import { preferences } from '$lib/stores/user.store'; interface Props { searchQuery: MetadataSearchDto | SmartSearchDto; @@ -81,6 +84,7 @@ : searchQuery.type === AssetTypeEnum.Video ? MediaType.Video : MediaType.All, + rating: searchQuery.rating, }); const resetForm = () => { @@ -94,6 +98,7 @@ date: {}, display: {}, mediaType: MediaType.All, + rating: undefined, }; }; @@ -124,6 +129,7 @@ personIds: filter.personIds.size > 0 ? [...filter.personIds] : undefined, tagIds: filter.tagIds.size > 0 ? [...filter.tagIds] : undefined, type, + rating: filter.rating, }; onSearch(payload); @@ -161,6 +167,11 @@ + + {#if $preferences?.ratings.enabled} + + {/if} +
diff --git a/web/src/lib/components/shared-components/search-bar/search-ratings-section.svelte b/web/src/lib/components/shared-components/search-bar/search-ratings-section.svelte new file mode 100644 index 000000000..00e622380 --- /dev/null +++ b/web/src/lib/components/shared-components/search-bar/search-ratings-section.svelte @@ -0,0 +1,31 @@ + + +
+ +