Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ jobs:
machine-learning:
name: Build and Push ML
needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).machine-learning == true || startsWith(github.base_ref, 'release/') }}
if: ${{ fromJSON(needs.pre-job.outputs.should_run).machine-learning == true }}
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -184,7 +184,7 @@ jobs:
server:
name: Build and Push Server
needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true || startsWith(github.base_ref, 'release/') }}
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }}
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@4d420cf8e5ea499f99b2c2849ff9a219d5f7b178 # multi-runner-build-workflow-v4.0.0
permissions:
contents: read
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/partials/_mobile-app-download.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ The mobile app can be downloaded from the following places:
- [Google Play Store](https://play.google.com/store/apps/details?id=app.alextran.immich)
- [GitHub Releases (APK)](https://github.com/immich-app/immich/releases)
- Obtainium: You can get your Obtainium config link from the [Utilities page of your Immich server](https://my.immich.app/utilities).
- [F-Droid](https://app.futo.org/fdroid/repo/)
- [FUTO F-Droid](https://get.immich.app/fdroid)
34 changes: 33 additions & 1 deletion open-api/immich-openapi-specs.json
Original file line number Diff line number Diff line change
Expand Up @@ -19889,12 +19889,14 @@
},
"latitude": {
"description": "Latitude coordinate",
"format": "double",
"maximum": 90,
"minimum": -90,
"type": "number"
},
"longitude": {
"description": "Longitude coordinate",
"format": "double",
"maximum": 180,
"minimum": -180,
"type": "number"
Expand Down Expand Up @@ -22131,6 +22133,7 @@
"fNumber": {
"default": null,
"description": "F-number (aperture)",
"format": "double",
"nullable": true,
"type": "number"
},
Expand All @@ -22145,6 +22148,7 @@
"focalLength": {
"default": null,
"description": "Focal length in mm",
"format": "double",
"nullable": true,
"type": "number"
},
Expand All @@ -22159,6 +22163,7 @@
"latitude": {
"default": null,
"description": "GPS latitude",
"format": "double",
"nullable": true,
"type": "number"
},
Expand All @@ -22171,6 +22176,7 @@
"longitude": {
"default": null,
"description": "GPS longitude",
"format": "double",
"nullable": true,
"type": "number"
},
Expand Down Expand Up @@ -24287,32 +24293,40 @@
"NumberFilter": {
"properties": {
"eq": {
"format": "double",
"type": "number"
},
"gt": {
"format": "double",
"type": "number"
},
"gte": {
"format": "double",
"type": "number"
},
"in": {
"items": {
"format": "double",
"type": "number"
},
"minItems": 1,
"type": "array"
},
"lt": {
"format": "double",
"type": "number"
},
"lte": {
"format": "double",
"type": "number"
},
"ne": {
"format": "double",
"type": "number"
},
"notIn": {
"items": {
"format": "double",
"type": "number"
},
"minItems": 1,
Expand All @@ -24324,34 +24338,42 @@
"NumberFilterNullable": {
"properties": {
"eq": {
"format": "double",
"nullable": true,
"type": "number"
},
"gt": {
"format": "double",
"type": "number"
},
"gte": {
"format": "double",
"type": "number"
},
"in": {
"items": {
"format": "double",
"type": "number"
},
"minItems": 1,
"type": "array"
},
"lt": {
"format": "double",
"type": "number"
},
"lte": {
"format": "double",
"type": "number"
},
"ne": {
"format": "double",
"nullable": true,
"type": "number"
},
"notIn": {
"items": {
"format": "double",
"type": "number"
},
"minItems": 1,
Expand Down Expand Up @@ -25096,10 +25118,12 @@
},
"latitude": {
"description": "Latitude coordinate",
"format": "double",
"type": "number"
},
"longitude": {
"description": "Longitude coordinate",
"format": "double",
"type": "number"
},
"name": {
Expand Down Expand Up @@ -26489,7 +26513,9 @@
"properties": {
"angle": {
"description": "Rotation angle in degrees",
"type": "number"
"maximum": 270,
"minimum": 0,
"type": "integer"
}
},
"required": [
Expand Down Expand Up @@ -31554,6 +31580,7 @@
"latitude": {
"description": "Array of latitude coordinates extracted from EXIF GPS data",
"items": {
"format": "double",
"nullable": true,
"type": "number"
},
Expand All @@ -31570,13 +31597,15 @@
"localOffsetHours": {
"description": "Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective.",
"items": {
"format": "double",
"type": "number"
},
"type": "array"
},
"longitude": {
"description": "Array of longitude coordinates extracted from EXIF GPS data",
"items": {
"format": "double",
"nullable": true,
"type": "number"
},
Expand All @@ -31600,6 +31629,7 @@
"ratio": {
"description": "Array of aspect ratios (width/height) for each asset",
"items": {
"format": "double",
"type": "number"
},
"type": "array"
Expand Down Expand Up @@ -31783,6 +31813,7 @@
},
"latitude": {
"description": "Latitude coordinate",
"format": "double",
"maximum": 90,
"minimum": -90,
"type": "number"
Expand All @@ -31796,6 +31827,7 @@
},
"longitude": {
"description": "Longitude coordinate",
"format": "double",
"maximum": 180,
"minimum": -180,
"type": "number"
Expand Down
4 changes: 3 additions & 1 deletion server/src/dtos/editing.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ const CropParametersSchema = z
const RotateParametersSchema = z
.object({
angle: z
.number()
.int()
.min(0)
.max(270)
.refine((v) => [0, 90, 180, 270].includes(v), {
error: 'Angle must be one of the following values: 0, 90, 180, 270',
})
Expand Down
8 changes: 4 additions & 4 deletions server/src/dtos/exif.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ export const ExifResponseSchema = z
modifyDate: z.string().meta({ format: 'date-time' }).nullish().default(null).describe('Modification date/time'),
timeZone: z.string().nullish().default(null).describe('Time zone'),
lensModel: z.string().nullish().default(null).describe('Lens model'),
fNumber: z.number().nullish().default(null).describe('F-number (aperture)'),
focalLength: z.number().nullish().default(null).describe('Focal length in mm'),
fNumber: z.number().meta({ format: 'double' }).nullish().default(null).describe('F-number (aperture)'),
focalLength: z.number().meta({ format: 'double' }).nullish().default(null).describe('Focal length in mm'),
iso: z.int().nullish().default(null).describe('ISO sensitivity'),
exposureTime: z.string().nullish().default(null).describe('Exposure time'),
latitude: z.number().nullish().default(null).describe('GPS latitude'),
longitude: z.number().nullish().default(null).describe('GPS longitude'),
latitude: z.number().meta({ format: 'double' }).nullish().default(null).describe('GPS latitude'),
longitude: z.number().meta({ format: 'double' }).nullish().default(null).describe('GPS longitude'),
city: z.string().nullish().default(null).describe('City name'),
state: z.string().nullish().default(null).describe('State/province name'),
country: z.string().nullish().default(null).describe('Country name'),
Expand Down
24 changes: 12 additions & 12 deletions server/src/dtos/search.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ const SearchPeopleSchema = z
const PlacesResponseSchema = z
.object({
name: z.string().describe('Place name'),
latitude: z.number().describe('Latitude coordinate'),
longitude: z.number().describe('Longitude coordinate'),
latitude: z.number().meta({ format: 'double' }).describe('Latitude coordinate'),
longitude: z.number().meta({ format: 'double' }).describe('Longitude coordinate'),
admin1name: z.string().optional().describe('Administrative level 1 name (state/province)'),
admin2name: z.string().optional().describe('Administrative level 2 name (county/district)'),
})
Expand Down Expand Up @@ -174,23 +174,23 @@ const StringPatternFilterSchema = nonEmptyPartial({
}).meta({ id: 'StringPatternFilter' });

const numberRangeShape = {
lt: z.number(),
lte: z.number(),
gt: z.number(),
gte: z.number(),
in: z.array(z.number()).min(1),
notIn: z.array(z.number()).min(1),
lt: z.number().meta({ format: 'double' }),
lte: z.number().meta({ format: 'double' }),
gt: z.number().meta({ format: 'double' }),
gte: z.number().meta({ format: 'double' }),
in: z.array(z.number().meta({ format: 'double' })).min(1),
notIn: z.array(z.number().meta({ format: 'double' })).min(1),
};

const NumberFilterSchema = nonEmptyPartial({
eq: z.number(),
ne: z.number(),
eq: z.number().meta({ format: 'double' }),
ne: z.number().meta({ format: 'double' }),
...numberRangeShape,
}).meta({ id: 'NumberFilter' });

const NumberFilterNullableSchema = nonEmptyPartial({
eq: z.number().nullable(),
ne: z.number().nullable(),
eq: z.number().meta({ format: 'double' }).nullable(),
ne: z.number().meta({ format: 'double' }).nullable(),
...numberRangeShape,
}).meta({ id: 'NumberFilterNullable' });

Expand Down
10 changes: 6 additions & 4 deletions server/src/dtos/time-bucket.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ const TimeBucketAssetResponseSchema = z
.object({
id: z.array(z.string()).describe('Array of asset IDs in the time bucket'),
ownerId: z.array(z.string()).describe('Array of owner IDs for each asset'),
ratio: z.array(z.number()).describe('Array of aspect ratios (width/height) for each asset'),
ratio: z
.array(z.number().meta({ format: 'double' }))
.describe('Array of aspect ratios (width/height) for each asset'),
isFavorite: z.array(z.boolean()).describe('Array indicating whether each asset is favorited'),
visibility: z
.array(AssetVisibilitySchema)
Expand All @@ -90,7 +92,7 @@ const TimeBucketAssetResponseSchema = z
.describe('Array of UTC timestamps when each asset was originally uploaded to Immich'),
fileCreatedAt: z.array(z.string()).describe('Array of file creation timestamps in UTC'),
localOffsetHours: z
.array(z.number())
.array(z.number().meta({ format: 'double' }))
.describe(
"Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective.",
),
Expand All @@ -110,11 +112,11 @@ const TimeBucketAssetResponseSchema = z
city: z.array(z.string().nullable()).optional().describe('Array of city names extracted from EXIF GPS data'),
country: z.array(z.string().nullable()).optional().describe('Array of country names extracted from EXIF GPS data'),
latitude: z
.array(z.number().nullable())
.array(z.number().meta({ format: 'double' }).nullable())
.optional()
.describe('Array of latitude coordinates extracted from EXIF GPS data'),
longitude: z
.array(z.number().nullable())
.array(z.number().meta({ format: 'double' }).nullable())
.optional()
.describe('Array of longitude coordinates extracted from EXIF GPS data'),
})
Expand Down
29 changes: 25 additions & 4 deletions server/src/utils/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,23 +225,44 @@ const patchOpenAPI = (document: OpenAPIObject) => {

document.components.schemas = sortKeys(schemas);

const errors: string[] = [];

for (const [schemaName, schema] of Object.entries(schemas)) {
if (!schema.properties) {
continue;
}

schema.properties = sortKeys(schema.properties);

for (const [key, value] of Object.entries(schema.properties)) {
if (typeof value === 'string') {
for (const [key, initialValue] of Object.entries(schema.properties)) {
if (typeof initialValue === 'string' || !isSchema(initialValue)) {
continue;
}

if (isSchema(value) && value.type === 'number' && value.format === 'float') {
throw new Error(`Invalid number format: ${schemaName}.${key}=float (use double instead). `);
// check array types
let value: SchemaObject | ReferenceObject = initialValue;
if (value.type === 'array' && value.items) {
value = value.items;
}

if (isSchema(value) && value.type === 'number') {
if (value.format === 'float') {
errors.push(`Invalid number format: ${schemaName}.${key}=float (use double instead). `);
}

// verify it was meant to be a number (and not an integer)
if (!value.format) {
errors.push(
`${schemaName}.${key} is a number (not an integer) and requires a format (e.g .meta({ format: 'double' })). `,
);
}
}
}
schema.required?.sort();

if (errors.length > 0) {
throw new Error(`Schema validation failed:\n ${errors.join('\n ')}`);
}
}
}

Expand Down
Loading
Loading