Преглед изворни кода

feat(web/server): Search by panorama photos (#3470)

* Add panorama filter

* Add generated api changes

* Fix naming
PyKen пре 1 година
родитељ
комит
51cfe10c28

+ 18 - 5
cli/src/api/open-api/api.ts

@@ -9489,6 +9489,7 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
          * @param {string} [exifInfoCountry] 
          * @param {string} [exifInfoCountry] 
          * @param {string} [exifInfoMake] 
          * @param {string} [exifInfoMake] 
          * @param {string} [exifInfoModel] 
          * @param {string} [exifInfoModel] 
+         * @param {string} [exifInfoProjectionType] 
          * @param {Array<string>} [smartInfoObjects] 
          * @param {Array<string>} [smartInfoObjects] 
          * @param {Array<string>} [smartInfoTags] 
          * @param {Array<string>} [smartInfoTags] 
          * @param {boolean} [recent] 
          * @param {boolean} [recent] 
@@ -9496,7 +9497,7 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
          * @param {*} [options] Override http request option.
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          * @throws {RequiredError}
          */
          */
-        search: async (q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, isArchived?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+        search: async (q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, isArchived?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, exifInfoProjectionType?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
             const localVarPath = `/search`;
             const localVarPath = `/search`;
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -9562,6 +9563,10 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
                 localVarQueryParameter['exifInfo.model'] = exifInfoModel;
                 localVarQueryParameter['exifInfo.model'] = exifInfoModel;
             }
             }
 
 
+            if (exifInfoProjectionType !== undefined) {
+                localVarQueryParameter['exifInfo.projectionType'] = exifInfoProjectionType;
+            }
+
             if (smartInfoObjects) {
             if (smartInfoObjects) {
                 localVarQueryParameter['smartInfo.objects'] = smartInfoObjects;
                 localVarQueryParameter['smartInfo.objects'] = smartInfoObjects;
             }
             }
@@ -9630,6 +9635,7 @@ export const SearchApiFp = function(configuration?: Configuration) {
          * @param {string} [exifInfoCountry] 
          * @param {string} [exifInfoCountry] 
          * @param {string} [exifInfoMake] 
          * @param {string} [exifInfoMake] 
          * @param {string} [exifInfoModel] 
          * @param {string} [exifInfoModel] 
+         * @param {string} [exifInfoProjectionType] 
          * @param {Array<string>} [smartInfoObjects] 
          * @param {Array<string>} [smartInfoObjects] 
          * @param {Array<string>} [smartInfoTags] 
          * @param {Array<string>} [smartInfoTags] 
          * @param {boolean} [recent] 
          * @param {boolean} [recent] 
@@ -9637,8 +9643,8 @@ export const SearchApiFp = function(configuration?: Configuration) {
          * @param {*} [options] Override http request option.
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          * @throws {RequiredError}
          */
          */
-        async search(q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, isArchived?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SearchResponseDto>> {
-            const localVarAxiosArgs = await localVarAxiosParamCreator.search(q, query, clip, type, isFavorite, isArchived, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, recent, motion, options);
+        async search(q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, isArchived?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, exifInfoProjectionType?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SearchResponseDto>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.search(q, query, clip, type, isFavorite, isArchived, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, exifInfoProjectionType, smartInfoObjects, smartInfoTags, recent, motion, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
         },
     }
     }
@@ -9674,7 +9680,7 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat
          * @throws {RequiredError}
          * @throws {RequiredError}
          */
          */
         search(requestParameters: SearchApiSearchRequest = {}, options?: AxiosRequestConfig): AxiosPromise<SearchResponseDto> {
         search(requestParameters: SearchApiSearchRequest = {}, options?: AxiosRequestConfig): AxiosPromise<SearchResponseDto> {
-            return localVarFp.search(requestParameters.q, requestParameters.query, requestParameters.clip, requestParameters.type, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.exifInfoCity, requestParameters.exifInfoState, requestParameters.exifInfoCountry, requestParameters.exifInfoMake, requestParameters.exifInfoModel, requestParameters.smartInfoObjects, requestParameters.smartInfoTags, requestParameters.recent, requestParameters.motion, options).then((request) => request(axios, basePath));
+            return localVarFp.search(requestParameters.q, requestParameters.query, requestParameters.clip, requestParameters.type, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.exifInfoCity, requestParameters.exifInfoState, requestParameters.exifInfoCountry, requestParameters.exifInfoMake, requestParameters.exifInfoModel, requestParameters.exifInfoProjectionType, requestParameters.smartInfoObjects, requestParameters.smartInfoTags, requestParameters.recent, requestParameters.motion, options).then((request) => request(axios, basePath));
         },
         },
     };
     };
 };
 };
@@ -9762,6 +9768,13 @@ export interface SearchApiSearchRequest {
      */
      */
     readonly exifInfoModel?: string
     readonly exifInfoModel?: string
 
 
+    /**
+     * 
+     * @type {string}
+     * @memberof SearchApiSearch
+     */
+    readonly exifInfoProjectionType?: string
+
     /**
     /**
      * 
      * 
      * @type {Array<string>}
      * @type {Array<string>}
@@ -9826,7 +9839,7 @@ export class SearchApi extends BaseAPI {
      * @memberof SearchApi
      * @memberof SearchApi
      */
      */
     public search(requestParameters: SearchApiSearchRequest = {}, options?: AxiosRequestConfig) {
     public search(requestParameters: SearchApiSearchRequest = {}, options?: AxiosRequestConfig) {
-        return SearchApiFp(this.configuration).search(requestParameters.q, requestParameters.query, requestParameters.clip, requestParameters.type, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.exifInfoCity, requestParameters.exifInfoState, requestParameters.exifInfoCountry, requestParameters.exifInfoMake, requestParameters.exifInfoModel, requestParameters.smartInfoObjects, requestParameters.smartInfoTags, requestParameters.recent, requestParameters.motion, options).then((request) => request(this.axios, this.basePath));
+        return SearchApiFp(this.configuration).search(requestParameters.q, requestParameters.query, requestParameters.clip, requestParameters.type, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.exifInfoCity, requestParameters.exifInfoState, requestParameters.exifInfoCountry, requestParameters.exifInfoMake, requestParameters.exifInfoModel, requestParameters.exifInfoProjectionType, requestParameters.smartInfoObjects, requestParameters.smartInfoTags, requestParameters.recent, requestParameters.motion, options).then((request) => request(this.axios, this.basePath));
     }
     }
 }
 }
 
 

+ 4 - 2
mobile/openapi/doc/SearchApi.md

@@ -117,7 +117,7 @@ This endpoint does not need any parameter.
 [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 
 
 # **search**
 # **search**
-> SearchResponseDto search(q, query, clip, type, isFavorite, isArchived, exifInfoPeriodCity, exifInfoPeriodState, exifInfoPeriodCountry, exifInfoPeriodMake, exifInfoPeriodModel, smartInfoPeriodObjects, smartInfoPeriodTags, recent, motion)
+> SearchResponseDto search(q, query, clip, type, isFavorite, isArchived, exifInfoPeriodCity, exifInfoPeriodState, exifInfoPeriodCountry, exifInfoPeriodMake, exifInfoPeriodModel, exifInfoPeriodProjectionType, smartInfoPeriodObjects, smartInfoPeriodTags, recent, motion)
 
 
 
 
 
 
@@ -151,13 +151,14 @@ final exifInfoPeriodState = exifInfoPeriodState_example; // String |
 final exifInfoPeriodCountry = exifInfoPeriodCountry_example; // String | 
 final exifInfoPeriodCountry = exifInfoPeriodCountry_example; // String | 
 final exifInfoPeriodMake = exifInfoPeriodMake_example; // String | 
 final exifInfoPeriodMake = exifInfoPeriodMake_example; // String | 
 final exifInfoPeriodModel = exifInfoPeriodModel_example; // String | 
 final exifInfoPeriodModel = exifInfoPeriodModel_example; // String | 
+final exifInfoPeriodProjectionType = exifInfoPeriodProjectionType_example; // String | 
 final smartInfoPeriodObjects = []; // List<String> | 
 final smartInfoPeriodObjects = []; // List<String> | 
 final smartInfoPeriodTags = []; // List<String> | 
 final smartInfoPeriodTags = []; // List<String> | 
 final recent = true; // bool | 
 final recent = true; // bool | 
 final motion = true; // bool | 
 final motion = true; // bool | 
 
 
 try {
 try {
-    final result = api_instance.search(q, query, clip, type, isFavorite, isArchived, exifInfoPeriodCity, exifInfoPeriodState, exifInfoPeriodCountry, exifInfoPeriodMake, exifInfoPeriodModel, smartInfoPeriodObjects, smartInfoPeriodTags, recent, motion);
+    final result = api_instance.search(q, query, clip, type, isFavorite, isArchived, exifInfoPeriodCity, exifInfoPeriodState, exifInfoPeriodCountry, exifInfoPeriodMake, exifInfoPeriodModel, exifInfoPeriodProjectionType, smartInfoPeriodObjects, smartInfoPeriodTags, recent, motion);
     print(result);
     print(result);
 } catch (e) {
 } catch (e) {
     print('Exception when calling SearchApi->search: $e\n');
     print('Exception when calling SearchApi->search: $e\n');
@@ -179,6 +180,7 @@ Name | Type | Description  | Notes
  **exifInfoPeriodCountry** | **String**|  | [optional] 
  **exifInfoPeriodCountry** | **String**|  | [optional] 
  **exifInfoPeriodMake** | **String**|  | [optional] 
  **exifInfoPeriodMake** | **String**|  | [optional] 
  **exifInfoPeriodModel** | **String**|  | [optional] 
  **exifInfoPeriodModel** | **String**|  | [optional] 
+ **exifInfoPeriodProjectionType** | **String**|  | [optional] 
  **smartInfoPeriodObjects** | [**List<String>**](String.md)|  | [optional] [default to const []]
  **smartInfoPeriodObjects** | [**List<String>**](String.md)|  | [optional] [default to const []]
  **smartInfoPeriodTags** | [**List<String>**](String.md)|  | [optional] [default to const []]
  **smartInfoPeriodTags** | [**List<String>**](String.md)|  | [optional] [default to const []]
  **recent** | **bool**|  | [optional] 
  **recent** | **bool**|  | [optional] 

+ 10 - 3
mobile/openapi/lib/api/search_api.dart

@@ -126,6 +126,8 @@ class SearchApi {
   ///
   ///
   /// * [String] exifInfoPeriodModel:
   /// * [String] exifInfoPeriodModel:
   ///
   ///
+  /// * [String] exifInfoPeriodProjectionType:
+  ///
   /// * [List<String>] smartInfoPeriodObjects:
   /// * [List<String>] smartInfoPeriodObjects:
   ///
   ///
   /// * [List<String>] smartInfoPeriodTags:
   /// * [List<String>] smartInfoPeriodTags:
@@ -133,7 +135,7 @@ class SearchApi {
   /// * [bool] recent:
   /// * [bool] recent:
   ///
   ///
   /// * [bool] motion:
   /// * [bool] motion:
-  Future<Response> searchWithHttpInfo({ String? q, String? query, bool? clip, String? type, bool? isFavorite, bool? isArchived, String? exifInfoPeriodCity, String? exifInfoPeriodState, String? exifInfoPeriodCountry, String? exifInfoPeriodMake, String? exifInfoPeriodModel, List<String>? smartInfoPeriodObjects, List<String>? smartInfoPeriodTags, bool? recent, bool? motion, }) async {
+  Future<Response> searchWithHttpInfo({ String? q, String? query, bool? clip, String? type, bool? isFavorite, bool? isArchived, String? exifInfoPeriodCity, String? exifInfoPeriodState, String? exifInfoPeriodCountry, String? exifInfoPeriodMake, String? exifInfoPeriodModel, String? exifInfoPeriodProjectionType, List<String>? smartInfoPeriodObjects, List<String>? smartInfoPeriodTags, bool? recent, bool? motion, }) async {
     // ignore: prefer_const_declarations
     // ignore: prefer_const_declarations
     final path = r'/search';
     final path = r'/search';
 
 
@@ -177,6 +179,9 @@ class SearchApi {
     if (exifInfoPeriodModel != null) {
     if (exifInfoPeriodModel != null) {
       queryParams.addAll(_queryParams('', 'exifInfo.model', exifInfoPeriodModel));
       queryParams.addAll(_queryParams('', 'exifInfo.model', exifInfoPeriodModel));
     }
     }
+    if (exifInfoPeriodProjectionType != null) {
+      queryParams.addAll(_queryParams('', 'exifInfo.projectionType', exifInfoPeriodProjectionType));
+    }
     if (smartInfoPeriodObjects != null) {
     if (smartInfoPeriodObjects != null) {
       queryParams.addAll(_queryParams('multi', 'smartInfo.objects', smartInfoPeriodObjects));
       queryParams.addAll(_queryParams('multi', 'smartInfo.objects', smartInfoPeriodObjects));
     }
     }
@@ -228,6 +233,8 @@ class SearchApi {
   ///
   ///
   /// * [String] exifInfoPeriodModel:
   /// * [String] exifInfoPeriodModel:
   ///
   ///
+  /// * [String] exifInfoPeriodProjectionType:
+  ///
   /// * [List<String>] smartInfoPeriodObjects:
   /// * [List<String>] smartInfoPeriodObjects:
   ///
   ///
   /// * [List<String>] smartInfoPeriodTags:
   /// * [List<String>] smartInfoPeriodTags:
@@ -235,8 +242,8 @@ class SearchApi {
   /// * [bool] recent:
   /// * [bool] recent:
   ///
   ///
   /// * [bool] motion:
   /// * [bool] motion:
-  Future<SearchResponseDto?> search({ String? q, String? query, bool? clip, String? type, bool? isFavorite, bool? isArchived, String? exifInfoPeriodCity, String? exifInfoPeriodState, String? exifInfoPeriodCountry, String? exifInfoPeriodMake, String? exifInfoPeriodModel, List<String>? smartInfoPeriodObjects, List<String>? smartInfoPeriodTags, bool? recent, bool? motion, }) async {
-    final response = await searchWithHttpInfo( q: q, query: query, clip: clip, type: type, isFavorite: isFavorite, isArchived: isArchived, exifInfoPeriodCity: exifInfoPeriodCity, exifInfoPeriodState: exifInfoPeriodState, exifInfoPeriodCountry: exifInfoPeriodCountry, exifInfoPeriodMake: exifInfoPeriodMake, exifInfoPeriodModel: exifInfoPeriodModel, smartInfoPeriodObjects: smartInfoPeriodObjects, smartInfoPeriodTags: smartInfoPeriodTags, recent: recent, motion: motion, );
+  Future<SearchResponseDto?> search({ String? q, String? query, bool? clip, String? type, bool? isFavorite, bool? isArchived, String? exifInfoPeriodCity, String? exifInfoPeriodState, String? exifInfoPeriodCountry, String? exifInfoPeriodMake, String? exifInfoPeriodModel, String? exifInfoPeriodProjectionType, List<String>? smartInfoPeriodObjects, List<String>? smartInfoPeriodTags, bool? recent, bool? motion, }) async {
+    final response = await searchWithHttpInfo( q: q, query: query, clip: clip, type: type, isFavorite: isFavorite, isArchived: isArchived, exifInfoPeriodCity: exifInfoPeriodCity, exifInfoPeriodState: exifInfoPeriodState, exifInfoPeriodCountry: exifInfoPeriodCountry, exifInfoPeriodMake: exifInfoPeriodMake, exifInfoPeriodModel: exifInfoPeriodModel, exifInfoPeriodProjectionType: exifInfoPeriodProjectionType, smartInfoPeriodObjects: smartInfoPeriodObjects, smartInfoPeriodTags: smartInfoPeriodTags, recent: recent, motion: motion, );
     if (response.statusCode >= HttpStatus.badRequest) {
     if (response.statusCode >= HttpStatus.badRequest) {
       throw ApiException(response.statusCode, await _decodeBodyBytes(response));
       throw ApiException(response.statusCode, await _decodeBodyBytes(response));
     }
     }

+ 1 - 1
mobile/openapi/test/search_api_test.dart

@@ -27,7 +27,7 @@ void main() {
       // TODO
       // TODO
     });
     });
 
 
-    //Future<SearchResponseDto> search({ String q, String query, bool clip, String type, bool isFavorite, bool isArchived, String exifInfoPeriodCity, String exifInfoPeriodState, String exifInfoPeriodCountry, String exifInfoPeriodMake, String exifInfoPeriodModel, List<String> smartInfoPeriodObjects, List<String> smartInfoPeriodTags, bool recent, bool motion }) async
+    //Future<SearchResponseDto> search({ String q, String query, bool clip, String type, bool isFavorite, bool isArchived, String exifInfoPeriodCity, String exifInfoPeriodState, String exifInfoPeriodCountry, String exifInfoPeriodMake, String exifInfoPeriodModel, String exifInfoPeriodProjectionType, List<String> smartInfoPeriodObjects, List<String> smartInfoPeriodTags, bool recent, bool motion }) async
     test('test search', () async {
     test('test search', () async {
       // TODO
       // TODO
     });
     });

+ 8 - 0
server/immich-openapi-specs.json

@@ -2924,6 +2924,14 @@
               "type": "string"
               "type": "string"
             }
             }
           },
           },
+          {
+            "name": "exifInfo.projectionType",
+            "required": false,
+            "in": "query",
+            "schema": {
+              "type": "string"
+            }
+          },
           {
           {
             "name": "smartInfo.objects",
             "name": "smartInfo.objects",
             "required": false,
             "required": false,

+ 5 - 0
server/src/domain/search/dto/search.dto.ts

@@ -58,6 +58,11 @@ export class SearchDto {
   @IsOptional()
   @IsOptional()
   'exifInfo.model'?: string;
   'exifInfo.model'?: string;
 
 
+  @IsString()
+  @IsNotEmpty()
+  @IsOptional()
+  'exifInfo.projectionType'?: string;
+
   @IsString({ each: true })
   @IsString({ each: true })
   @IsArray()
   @IsArray()
   @IsOptional()
   @IsOptional()

+ 2 - 1
server/src/infra/typesense-schemas/asset.schema.ts

@@ -1,6 +1,6 @@
 import { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections';
 import { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections';
 
 
-export const assetSchemaVersion = 7;
+export const assetSchemaVersion = 8;
 export const assetSchema: CollectionCreateSchema = {
 export const assetSchema: CollectionCreateSchema = {
   name: `assets-v${assetSchemaVersion}`,
   name: `assets-v${assetSchemaVersion}`,
   fields: [
   fields: [
@@ -23,6 +23,7 @@ export const assetSchema: CollectionCreateSchema = {
     { name: 'exifInfo.make', type: 'string', facet: true, optional: true },
     { name: 'exifInfo.make', type: 'string', facet: true, optional: true },
     { name: 'exifInfo.model', type: 'string', facet: true, optional: true },
     { name: 'exifInfo.model', type: 'string', facet: true, optional: true },
     { name: 'exifInfo.orientation', type: 'string', optional: true },
     { name: 'exifInfo.orientation', type: 'string', optional: true },
+    { name: 'exifInfo.projectionType', type: 'string', facet: true, optional: true },
 
 
     // smart info
     // smart info
     { name: 'smartInfo.objects', type: 'string[]', facet: true, optional: true },
     { name: 'smartInfo.objects', type: 'string[]', facet: true, optional: true },

+ 20 - 6
web/src/api/open-api/api.ts

@@ -9535,6 +9535,7 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
          * @param {string} [exifInfoCountry] 
          * @param {string} [exifInfoCountry] 
          * @param {string} [exifInfoMake] 
          * @param {string} [exifInfoMake] 
          * @param {string} [exifInfoModel] 
          * @param {string} [exifInfoModel] 
+         * @param {string} [exifInfoProjectionType] 
          * @param {Array<string>} [smartInfoObjects] 
          * @param {Array<string>} [smartInfoObjects] 
          * @param {Array<string>} [smartInfoTags] 
          * @param {Array<string>} [smartInfoTags] 
          * @param {boolean} [recent] 
          * @param {boolean} [recent] 
@@ -9542,7 +9543,7 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
          * @param {*} [options] Override http request option.
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          * @throws {RequiredError}
          */
          */
-        search: async (q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, isArchived?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+        search: async (q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, isArchived?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, exifInfoProjectionType?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
             const localVarPath = `/search`;
             const localVarPath = `/search`;
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -9608,6 +9609,10 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
                 localVarQueryParameter['exifInfo.model'] = exifInfoModel;
                 localVarQueryParameter['exifInfo.model'] = exifInfoModel;
             }
             }
 
 
+            if (exifInfoProjectionType !== undefined) {
+                localVarQueryParameter['exifInfo.projectionType'] = exifInfoProjectionType;
+            }
+
             if (smartInfoObjects) {
             if (smartInfoObjects) {
                 localVarQueryParameter['smartInfo.objects'] = smartInfoObjects;
                 localVarQueryParameter['smartInfo.objects'] = smartInfoObjects;
             }
             }
@@ -9676,6 +9681,7 @@ export const SearchApiFp = function(configuration?: Configuration) {
          * @param {string} [exifInfoCountry] 
          * @param {string} [exifInfoCountry] 
          * @param {string} [exifInfoMake] 
          * @param {string} [exifInfoMake] 
          * @param {string} [exifInfoModel] 
          * @param {string} [exifInfoModel] 
+         * @param {string} [exifInfoProjectionType] 
          * @param {Array<string>} [smartInfoObjects] 
          * @param {Array<string>} [smartInfoObjects] 
          * @param {Array<string>} [smartInfoTags] 
          * @param {Array<string>} [smartInfoTags] 
          * @param {boolean} [recent] 
          * @param {boolean} [recent] 
@@ -9683,8 +9689,8 @@ export const SearchApiFp = function(configuration?: Configuration) {
          * @param {*} [options] Override http request option.
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          * @throws {RequiredError}
          */
          */
-        async search(q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, isArchived?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SearchResponseDto>> {
-            const localVarAxiosArgs = await localVarAxiosParamCreator.search(q, query, clip, type, isFavorite, isArchived, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, recent, motion, options);
+        async search(q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, isArchived?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, exifInfoProjectionType?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SearchResponseDto>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.search(q, query, clip, type, isFavorite, isArchived, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, exifInfoProjectionType, smartInfoObjects, smartInfoTags, recent, motion, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
         },
     }
     }
@@ -9726,6 +9732,7 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat
          * @param {string} [exifInfoCountry] 
          * @param {string} [exifInfoCountry] 
          * @param {string} [exifInfoMake] 
          * @param {string} [exifInfoMake] 
          * @param {string} [exifInfoModel] 
          * @param {string} [exifInfoModel] 
+         * @param {string} [exifInfoProjectionType] 
          * @param {Array<string>} [smartInfoObjects] 
          * @param {Array<string>} [smartInfoObjects] 
          * @param {Array<string>} [smartInfoTags] 
          * @param {Array<string>} [smartInfoTags] 
          * @param {boolean} [recent] 
          * @param {boolean} [recent] 
@@ -9733,8 +9740,8 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat
          * @param {*} [options] Override http request option.
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          * @throws {RequiredError}
          */
          */
-        search(q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, isArchived?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options?: any): AxiosPromise<SearchResponseDto> {
-            return localVarFp.search(q, query, clip, type, isFavorite, isArchived, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, recent, motion, options).then((request) => request(axios, basePath));
+        search(q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, isArchived?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, exifInfoProjectionType?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options?: any): AxiosPromise<SearchResponseDto> {
+            return localVarFp.search(q, query, clip, type, isFavorite, isArchived, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, exifInfoProjectionType, smartInfoObjects, smartInfoTags, recent, motion, options).then((request) => request(axios, basePath));
         },
         },
     };
     };
 };
 };
@@ -9822,6 +9829,13 @@ export interface SearchApiSearchRequest {
      */
      */
     readonly exifInfoModel?: string
     readonly exifInfoModel?: string
 
 
+    /**
+     * 
+     * @type {string}
+     * @memberof SearchApiSearch
+     */
+    readonly exifInfoProjectionType?: string
+
     /**
     /**
      * 
      * 
      * @type {Array<string>}
      * @type {Array<string>}
@@ -9886,7 +9900,7 @@ export class SearchApi extends BaseAPI {
      * @memberof SearchApi
      * @memberof SearchApi
      */
      */
     public search(requestParameters: SearchApiSearchRequest = {}, options?: AxiosRequestConfig) {
     public search(requestParameters: SearchApiSearchRequest = {}, options?: AxiosRequestConfig) {
-        return SearchApiFp(this.configuration).search(requestParameters.q, requestParameters.query, requestParameters.clip, requestParameters.type, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.exifInfoCity, requestParameters.exifInfoState, requestParameters.exifInfoCountry, requestParameters.exifInfoMake, requestParameters.exifInfoModel, requestParameters.smartInfoObjects, requestParameters.smartInfoTags, requestParameters.recent, requestParameters.motion, options).then((request) => request(this.axios, this.basePath));
+        return SearchApiFp(this.configuration).search(requestParameters.q, requestParameters.query, requestParameters.clip, requestParameters.type, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.exifInfoCity, requestParameters.exifInfoState, requestParameters.exifInfoCountry, requestParameters.exifInfoMake, requestParameters.exifInfoModel, requestParameters.exifInfoProjectionType, requestParameters.smartInfoObjects, requestParameters.smartInfoTags, requestParameters.recent, requestParameters.motion, options).then((request) => request(this.axios, this.basePath));
     }
     }
 }
 }
 
 

+ 10 - 0
web/src/routes/(user)/explore/+page.svelte

@@ -8,6 +8,7 @@
   import HeartMultipleOutline from 'svelte-material-icons/HeartMultipleOutline.svelte';
   import HeartMultipleOutline from 'svelte-material-icons/HeartMultipleOutline.svelte';
   import MotionPlayOutline from 'svelte-material-icons/MotionPlayOutline.svelte';
   import MotionPlayOutline from 'svelte-material-icons/MotionPlayOutline.svelte';
   import PlayCircleOutline from 'svelte-material-icons/PlayCircleOutline.svelte';
   import PlayCircleOutline from 'svelte-material-icons/PlayCircleOutline.svelte';
+  import Rotate360Icon from 'svelte-material-icons/Rotate360.svelte';
   import type { PageData } from './$types';
   import type { PageData } from './$types';
 
 
   export let data: PageData;
   export let data: PageData;
@@ -149,6 +150,15 @@
             <span>Motion photos</span>
             <span>Motion photos</span>
           </a>
           </a>
         </div>
         </div>
+        <div>
+          <a
+            href="/search?exifInfo.projectionType=EQUIRECTANGULAR"
+            class="flex w-full items-center gap-2 text-sm font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary"
+          >
+            <Rotate360Icon size={24} />
+            <span>Panorama photos</span>
+          </a>
+        </div>
       </div>
       </div>
     </div>
     </div>
   </div>
   </div>