Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(server): "view all" for cities only showing 12 cities #8035

Merged
merged 19 commits into from
Mar 20, 2024
1 change: 1 addition & 0 deletions mobile/openapi/README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 52 additions & 0 deletions mobile/openapi/doc/AssetApi.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion mobile/openapi/doc/ValidateLibraryImportPathResponseDto.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions mobile/openapi/lib/api/asset_api.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions mobile/openapi/test/asset_api_test.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 37 additions & 1 deletion open-api/immich-openapi-specs.json
Original file line number Diff line number Diff line change
Expand Up @@ -1098,6 +1098,41 @@
]
}
},
"/asset/cities": {
"get": {
"operationId": "getAssetsByCity",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/AssetResponseDto"
},
"type": "array"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Asset"
]
}
},
"/asset/curated-locations": {
"get": {
"operationId": "getCuratedLocations",
Expand Down Expand Up @@ -10689,7 +10724,8 @@
}
},
"required": [
"importPath"
"importPath",
"isValid"
],
"type": "object"
},
Expand Down
10 changes: 9 additions & 1 deletion open-api/typescript-sdk/src/fetch-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ export type ValidateLibraryDto = {
};
export type ValidateLibraryImportPathResponseDto = {
importPath: string;
isValid?: boolean;
isValid: boolean;
message?: string;
};
export type ValidateLibraryResponseDto = {
Expand Down Expand Up @@ -1291,6 +1291,14 @@ export function checkBulkUpload({ assetBulkUploadCheckDto }: {
body: assetBulkUploadCheckDto
})));
}
export function getAssetsByCity(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: AssetResponseDto[];
}>("/asset/cities", {
...opts
}));
}
export function getCuratedLocations(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
Expand Down
5 changes: 5 additions & 0 deletions server/src/domain/asset/asset.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,11 @@ export class AssetService {
return this.assetRepository.getAllByDeviceId(auth.user.id, deviceId);
}

async getAssetsByCity(auth: AuthDto): Promise<AssetResponseDto[]> {
const items = await this.assetRepository.getAssetsByCity([auth.user.id]);
return items.map((a) => mapAsset(a, { auth }));
}

async get(auth: AuthDto, id: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
await this.access.requirePermission(auth, Permission.ASSET_READ, id);

Expand Down
1 change: 1 addition & 0 deletions server/src/domain/repositories/asset.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,5 +152,6 @@ export interface IAssetRepository {
upsertJobStatus(jobStatus: Partial<AssetJobStatusEntity>): Promise<void>;
getAssetIdByCity(userId: string, options: AssetExploreFieldOptions): Promise<SearchExploreItem<string>>;
getAssetIdByTag(userId: string, options: AssetExploreFieldOptions): Promise<SearchExploreItem<string>>;
getAssetsByCity(userIds: string[]): Promise<AssetEntity[]>;
searchMetadata(query: string, userIds: string[], options: MetadataSearchOptions): Promise<AssetEntity[]>;
}
5 changes: 5 additions & 0 deletions server/src/immich/controllers/asset.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ export class AssetController {
return this.service.getTimeBucket(auth, dto) as Promise<AssetResponseDto[]>;
}

@Get('cities')
getAssetsByCity(@Auth() auth: AuthDto): Promise<AssetResponseDto[]> {
return this.service.getAssetsByCity(auth);
}

@Post('jobs')
@HttpCode(HttpStatus.NO_CONTENT)
runAssetJobs(@Auth() auth: AuthDto, @Body() dto: AssetJobsDto): Promise<void> {
Expand Down
94 changes: 93 additions & 1 deletion server/src/infra/repositories/asset.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,7 @@ export class AssetRepository implements IAssetRepository {
const cte = this.exifRepository
.createQueryBuilder('e')
.select('city')
.addSelect('count(city)', 'count')
.groupBy('city')
.having('count(city) >= :minAssetsPerField', { minAssetsPerField });

Expand All @@ -644,11 +645,12 @@ export class AssetRepository implements IAssetRepository {
})
.select('c.city', 'value')
.addSelect('asset.id', 'data')
.distinctOn(['c.city'])
.distinctOn(['c.count', 'c.city'])
.innerJoin('exif', 'e', 'asset.id = e."assetId"')
.addCommonTableExpression(cte, 'cities')
.innerJoin('cities', 'c', 'c.city = e.city')
.limit(maxFields)
.orderBy('c.count', 'DESC')
.getRawMany();

return { fieldName: 'exifInfo.city', items };
Expand Down Expand Up @@ -683,6 +685,96 @@ export class AssetRepository implements IAssetRepository {
return { fieldName: 'smartInfo.tags', items };
}

@GenerateSql({ params: [[DummyValue.UUID]] })
async getAssetsByCity(userIds: string[]): Promise<AssetEntity[]> {
// the performance difference between this and the normal way is too huge to ignore, e.g. 3s vs 4ms
const parameters = [userIds.join(', '), true, false, AssetType.IMAGE];
const rawRes = await this.repository.query(
`
WITH RECURSIVE cte AS (
(
SELECT city, "assetId"
FROM exif
INNER JOIN assets ON exif."assetId" = assets.id
WHERE "ownerId" IN ($1) AND "isVisible" = $2 AND "isArchived" = $3 AND type = $4
ORDER BY city
LIMIT 1
)
UNION ALL
SELECT l.city, l."assetId"
FROM cte c
, LATERAL (
SELECT city, "assetId"
FROM exif
WHERE city > c.city
ORDER BY city
LIMIT 1
) l
INNER JOIN assets ON l."assetId" = assets.id
WHERE "ownerId" IN ($5) AND "isVisible" = $6 AND "isArchived" = $7 AND type = $8
)
SELECT assets.*, exif.*
FROM assets
INNER JOIN cte ON id = "assetId"
INNER JOIN exif ON assets.id = exif."assetId"
`,
[...parameters, ...parameters],
);

const items = rawRes.map(
({
country,
state,
city,
description,
model,
make,
dateTimeOriginal,
exifImageHeight,
exifImageWidth,
exposureTime,
fNumber,
fileSizeInByte,
focalLength,
iso,
latitude,
lensModel,
longitude,
modifyDate,
projectionType,
timeZone,
...assetInfo
}: any) =>
({
exifInfo: {
city,
country,
dateTimeOriginal,
description,
exifImageHeight,
exifImageWidth,
exposureTime,
fNumber,
fileSizeInByte,
focalLength,
iso,
latitude,
lensModel,
longitude,
make,
model,
modifyDate,
projectionType,
state,
timeZone,
},
...assetInfo,
}) as AssetEntity,
);

return items;
}

private getBuilder(options: AssetBuilderOptions) {
const { isArchived, isFavorite, isTrashed, albumId, personId, userIds, withStacked, exifInfo, assetType } = options;

Expand Down