Explorar el Código

refactor(server,web): time buckets for main timeline, archived, and favorites (1) (#3537)

* refactor: time buckets

* feat(web): use new time bucket api

* feat(web): use asset grid in archive/favorites

* chore: open api

* chore: clean up uuid validation

* refactor(web): move memory lane to photos page

* Update web/src/routes/(user)/archive/+page.svelte

Co-authored-by: Sergey Kondrikov <sergey.kondrikov@gmail.com>

* fix: hide archived photos on main timeline

* fix: select exif info

---------

Co-authored-by: Sergey Kondrikov <sergey.kondrikov@gmail.com>
Jason Rasmussen hace 1 año
padre
commit
c6abef186c
Se han modificado 51 ficheros con 1455 adiciones y 1801 borrados
  1. 326 250
      cli/src/api/open-api/api.ts
  2. 6 15
      mobile/openapi/.openapi-generator/FILES
  3. 4 7
      mobile/openapi/README.md
  4. 113 91
      mobile/openapi/doc/AssetApi.md
  5. 0 16
      mobile/openapi/doc/AssetCountByTimeBucketResponseDto.md
  6. 0 17
      mobile/openapi/doc/GetAssetByTimeBucketDto.md
  7. 0 17
      mobile/openapi/doc/GetAssetCountByTimeBucketDto.md
  8. 1 1
      mobile/openapi/doc/TimeBucketResponseDto.md
  9. 1 1
      mobile/openapi/doc/TimeBucketSize.md
  10. 2 5
      mobile/openapi/lib/api.dart
  11. 179 97
      mobile/openapi/lib/api/asset_api.dart
  12. 4 10
      mobile/openapi/lib/api_client.dart
  13. 2 2
      mobile/openapi/lib/api_helper.dart
  14. 0 106
      mobile/openapi/lib/model/asset_count_by_time_bucket_response_dto.dart
  15. 0 135
      mobile/openapi/lib/model/get_asset_by_time_bucket_dto.dart
  16. 0 133
      mobile/openapi/lib/model/get_asset_count_by_time_bucket_dto.dart
  17. 18 18
      mobile/openapi/lib/model/time_bucket_response_dto.dart
  18. 24 24
      mobile/openapi/lib/model/time_bucket_size.dart
  19. 10 10
      mobile/openapi/test/asset_api_test.dart
  20. 0 32
      mobile/openapi/test/asset_count_by_time_bucket_response_dto_test.dart
  21. 0 38
      mobile/openapi/test/get_asset_by_time_bucket_dto_test.dart
  22. 0 38
      mobile/openapi/test/get_asset_count_by_time_bucket_dto_test.dart
  23. 3 3
      mobile/openapi/test/time_bucket_response_dto_test.dart
  24. 2 2
      mobile/openapi/test/time_bucket_size_test.dart
  25. 182 136
      server/immich-openapi-specs.json
  26. 19 0
      server/src/domain/asset/asset.repository.ts
  27. 26 2
      server/src/domain/asset/asset.service.ts
  28. 1 0
      server/src/domain/asset/dto/index.ts
  29. 34 0
      server/src/domain/asset/dto/time-bucket.dto.ts
  30. 1 0
      server/src/domain/asset/response-dto/index.ts
  31. 9 0
      server/src/domain/asset/response-dto/time-bucket-response.dto.ts
  32. 0 56
      server/src/immich/api-v1/asset/asset-repository.ts
  33. 0 21
      server/src/immich/api-v1/asset/asset.controller.ts
  34. 0 31
      server/src/immich/api-v1/asset/asset.service.spec.ts
  35. 0 23
      server/src/immich/api-v1/asset/asset.service.ts
  36. 0 28
      server/src/immich/api-v1/asset/dto/get-asset-by-time-bucket.dto.ts
  37. 0 32
      server/src/immich/api-v1/asset/dto/get-asset-count-by-time-bucket.dto.ts
  38. 0 23
      server/src/immich/api-v1/asset/response-dto/asset-count-by-time-group-response.dto.ts
  39. 16 0
      server/src/immich/controllers/asset.controller.ts
  40. 51 0
      server/src/infra/repositories/asset.repository.ts
  41. 3 4
      server/test/repositories/asset.repository.mock.ts
  42. 337 250
      web/src/api/open-api/api.ts
  43. 2 2
      web/src/lib/components/album-page/asset-selection.svelte
  44. 4 1
      web/src/lib/components/layouts/user-page-layout.svelte
  45. 1 1
      web/src/lib/components/photos-page/asset-grid.svelte
  46. 1 1
      web/src/lib/components/shared-components/empty-placeholder.svelte
  47. 6 14
      web/src/lib/stores/assets.store.ts
  48. 34 49
      web/src/routes/(user)/archive/+page.svelte
  49. 24 42
      web/src/routes/(user)/favorites/+page.svelte
  50. 2 2
      web/src/routes/(user)/partners/[userId]/+page.svelte
  51. 7 15
      web/src/routes/(user)/photos/+page.svelte

+ 326 - 250
cli/src/api/open-api/api.ts

@@ -410,44 +410,6 @@ export const AssetBulkUploadCheckResultReasonEnum = {
 
 export type AssetBulkUploadCheckResultReasonEnum = typeof AssetBulkUploadCheckResultReasonEnum[keyof typeof AssetBulkUploadCheckResultReasonEnum];
 
-/**
- * 
- * @export
- * @interface AssetCountByTimeBucket
- */
-export interface AssetCountByTimeBucket {
-    /**
-     * 
-     * @type {number}
-     * @memberof AssetCountByTimeBucket
-     */
-    'count': number;
-    /**
-     * 
-     * @type {string}
-     * @memberof AssetCountByTimeBucket
-     */
-    'timeBucket': string;
-}
-/**
- * 
- * @export
- * @interface AssetCountByTimeBucketResponseDto
- */
-export interface AssetCountByTimeBucketResponseDto {
-    /**
-     * 
-     * @type {Array<AssetCountByTimeBucket>}
-     * @memberof AssetCountByTimeBucketResponseDto
-     */
-    'buckets': Array<AssetCountByTimeBucket>;
-    /**
-     * 
-     * @type {number}
-     * @memberof AssetCountByTimeBucketResponseDto
-     */
-    'totalCount': number;
-}
 /**
  * 
  * @export
@@ -1286,58 +1248,6 @@ export interface ExifResponseDto {
      */
     'timeZone'?: string | null;
 }
-/**
- * 
- * @export
- * @interface GetAssetByTimeBucketDto
- */
-export interface GetAssetByTimeBucketDto {
-    /**
-     * 
-     * @type {Array<string>}
-     * @memberof GetAssetByTimeBucketDto
-     */
-    'timeBucket': Array<string>;
-    /**
-     * 
-     * @type {string}
-     * @memberof GetAssetByTimeBucketDto
-     */
-    'userId'?: string;
-    /**
-     * Include assets without thumbnails
-     * @type {boolean}
-     * @memberof GetAssetByTimeBucketDto
-     */
-    'withoutThumbs'?: boolean;
-}
-/**
- * 
- * @export
- * @interface GetAssetCountByTimeBucketDto
- */
-export interface GetAssetCountByTimeBucketDto {
-    /**
-     * 
-     * @type {TimeGroupEnum}
-     * @memberof GetAssetCountByTimeBucketDto
-     */
-    'timeGroup': TimeGroupEnum;
-    /**
-     * 
-     * @type {string}
-     * @memberof GetAssetCountByTimeBucketDto
-     */
-    'userId'?: string;
-    /**
-     * Include assets without thumbnails
-     * @type {boolean}
-     * @memberof GetAssetCountByTimeBucketDto
-     */
-    'withoutThumbs'?: boolean;
-}
-
-
 /**
  * 
  * @export
@@ -2850,18 +2760,37 @@ export const ThumbnailFormat = {
 export type ThumbnailFormat = typeof ThumbnailFormat[keyof typeof ThumbnailFormat];
 
 
+/**
+ * 
+ * @export
+ * @interface TimeBucketResponseDto
+ */
+export interface TimeBucketResponseDto {
+    /**
+     * 
+     * @type {number}
+     * @memberof TimeBucketResponseDto
+     */
+    'count': number;
+    /**
+     * 
+     * @type {string}
+     * @memberof TimeBucketResponseDto
+     */
+    'timeBucket': string;
+}
 /**
  * 
  * @export
  * @enum {string}
  */
 
-export const TimeGroupEnum = {
-    Day: 'day',
-    Month: 'month'
+export const TimeBucketSize = {
+    Day: 'DAY',
+    Month: 'MONTH'
 } as const;
 
-export type TimeGroupEnum = typeof TimeGroupEnum[keyof typeof TimeGroupEnum];
+export type TimeBucketSize = typeof TimeBucketSize[keyof typeof TimeBucketSize];
 
 
 /**
@@ -5040,14 +4969,11 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
         },
         /**
          * 
-         * @param {GetAssetByTimeBucketDto} getAssetByTimeBucketDto 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        getAssetByTimeBucket: async (getAssetByTimeBucketDto: GetAssetByTimeBucketDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
-            // verify required parameter 'getAssetByTimeBucketDto' is not null or undefined
-            assertParamExists('getAssetByTimeBucket', 'getAssetByTimeBucketDto', getAssetByTimeBucketDto)
-            const localVarPath = `/asset/time-bucket`;
+        getAssetSearchTerms: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/asset/search-terms`;
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
             let baseOptions;
@@ -5055,7 +4981,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
                 baseOptions = configuration.baseOptions;
             }
 
-            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
+            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
             const localVarHeaderParameter = {} as any;
             const localVarQueryParameter = {} as any;
 
@@ -5070,12 +4996,9 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
 
 
     
-            localVarHeaderParameter['Content-Type'] = 'application/json';
-
             setSearchParams(localVarUrlObj, localVarQueryParameter);
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
-            localVarRequestOptions.data = serializeDataIfNeeded(getAssetByTimeBucketDto, localVarRequestOptions, configuration)
 
             return {
                 url: toPathString(localVarUrlObj),
@@ -5084,14 +5007,13 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
         },
         /**
          * 
-         * @param {GetAssetCountByTimeBucketDto} getAssetCountByTimeBucketDto 
+         * @param {boolean} [isArchived] 
+         * @param {boolean} [isFavorite] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        getAssetCountByTimeBucket: async (getAssetCountByTimeBucketDto: GetAssetCountByTimeBucketDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
-            // verify required parameter 'getAssetCountByTimeBucketDto' is not null or undefined
-            assertParamExists('getAssetCountByTimeBucket', 'getAssetCountByTimeBucketDto', getAssetCountByTimeBucketDto)
-            const localVarPath = `/asset/count-by-time-bucket`;
+        getAssetStats: async (isArchived?: boolean, isFavorite?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/asset/statistics`;
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
             let baseOptions;
@@ -5099,7 +5021,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
                 baseOptions = configuration.baseOptions;
             }
 
-            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
+            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
             const localVarHeaderParameter = {} as any;
             const localVarQueryParameter = {} as any;
 
@@ -5112,14 +5034,19 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
             // http bearer authentication required
             await setBearerAuthToObject(localVarHeaderParameter, configuration)
 
+            if (isArchived !== undefined) {
+                localVarQueryParameter['isArchived'] = isArchived;
+            }
 
-    
-            localVarHeaderParameter['Content-Type'] = 'application/json';
+            if (isFavorite !== undefined) {
+                localVarQueryParameter['isFavorite'] = isFavorite;
+            }
 
+
+    
             setSearchParams(localVarUrlObj, localVarQueryParameter);
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
-            localVarRequestOptions.data = serializeDataIfNeeded(getAssetCountByTimeBucketDto, localVarRequestOptions, configuration)
 
             return {
                 url: toPathString(localVarUrlObj),
@@ -5128,11 +5055,17 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
         },
         /**
          * 
+         * @param {string} id 
+         * @param {ThumbnailFormat} [format] 
+         * @param {string} [key] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        getAssetSearchTerms: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
-            const localVarPath = `/asset/search-terms`;
+        getAssetThumbnail: async (id: string, format?: ThumbnailFormat, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'id' is not null or undefined
+            assertParamExists('getAssetThumbnail', 'id', id)
+            const localVarPath = `/asset/thumbnail/{id}`
+                .replace(`{${"id"}}`, encodeURIComponent(String(id)));
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
             let baseOptions;
@@ -5153,6 +5086,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
             // http bearer authentication required
             await setBearerAuthToObject(localVarHeaderParameter, configuration)
 
+            if (format !== undefined) {
+                localVarQueryParameter['format'] = format;
+            }
+
+            if (key !== undefined) {
+                localVarQueryParameter['key'] = key;
+            }
+
 
     
             setSearchParams(localVarUrlObj, localVarQueryParameter);
@@ -5166,13 +5107,22 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
         },
         /**
          * 
+         * @param {TimeBucketSize} size 
+         * @param {string} timeBucket 
+         * @param {string} [userId] 
+         * @param {string} [albumId] 
          * @param {boolean} [isArchived] 
          * @param {boolean} [isFavorite] 
+         * @param {string} [key] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        getAssetStats: async (isArchived?: boolean, isFavorite?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
-            const localVarPath = `/asset/statistics`;
+        getByTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'size' is not null or undefined
+            assertParamExists('getByTimeBucket', 'size', size)
+            // verify required parameter 'timeBucket' is not null or undefined
+            assertParamExists('getByTimeBucket', 'timeBucket', timeBucket)
+            const localVarPath = `/asset/time-bucket`;
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
             let baseOptions;
@@ -5193,60 +5143,28 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
             // http bearer authentication required
             await setBearerAuthToObject(localVarHeaderParameter, configuration)
 
-            if (isArchived !== undefined) {
-                localVarQueryParameter['isArchived'] = isArchived;
+            if (size !== undefined) {
+                localVarQueryParameter['size'] = size;
             }
 
-            if (isFavorite !== undefined) {
-                localVarQueryParameter['isFavorite'] = isFavorite;
+            if (userId !== undefined) {
+                localVarQueryParameter['userId'] = userId;
             }
 
-
-    
-            setSearchParams(localVarUrlObj, localVarQueryParameter);
-            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
-            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
-
-            return {
-                url: toPathString(localVarUrlObj),
-                options: localVarRequestOptions,
-            };
-        },
-        /**
-         * 
-         * @param {string} id 
-         * @param {ThumbnailFormat} [format] 
-         * @param {string} [key] 
-         * @param {*} [options] Override http request option.
-         * @throws {RequiredError}
-         */
-        getAssetThumbnail: async (id: string, format?: ThumbnailFormat, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
-            // verify required parameter 'id' is not null or undefined
-            assertParamExists('getAssetThumbnail', 'id', id)
-            const localVarPath = `/asset/thumbnail/{id}`
-                .replace(`{${"id"}}`, encodeURIComponent(String(id)));
-            // use dummy base URL string because the URL constructor only accepts absolute URLs.
-            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
-            let baseOptions;
-            if (configuration) {
-                baseOptions = configuration.baseOptions;
+            if (albumId !== undefined) {
+                localVarQueryParameter['albumId'] = albumId;
             }
 
-            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
-            const localVarHeaderParameter = {} as any;
-            const localVarQueryParameter = {} as any;
-
-            // authentication cookie required
-
-            // authentication api_key required
-            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
+            if (isArchived !== undefined) {
+                localVarQueryParameter['isArchived'] = isArchived;
+            }
 
-            // authentication bearer required
-            // http bearer authentication required
-            await setBearerAuthToObject(localVarHeaderParameter, configuration)
+            if (isFavorite !== undefined) {
+                localVarQueryParameter['isFavorite'] = isFavorite;
+            }
 
-            if (format !== undefined) {
-                localVarQueryParameter['format'] = format;
+            if (timeBucket !== undefined) {
+                localVarQueryParameter['timeBucket'] = timeBucket;
             }
 
             if (key !== undefined) {
@@ -5498,6 +5416,76 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
 
 
     
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @param {TimeBucketSize} size 
+         * @param {string} [userId] 
+         * @param {string} [albumId] 
+         * @param {boolean} [isArchived] 
+         * @param {boolean} [isFavorite] 
+         * @param {string} [key] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'size' is not null or undefined
+            assertParamExists('getTimeBuckets', 'size', size)
+            const localVarPath = `/asset/time-buckets`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+
+            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication cookie required
+
+            // authentication api_key required
+            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
+
+            // authentication bearer required
+            // http bearer authentication required
+            await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+            if (size !== undefined) {
+                localVarQueryParameter['size'] = size;
+            }
+
+            if (userId !== undefined) {
+                localVarQueryParameter['userId'] = userId;
+            }
+
+            if (albumId !== undefined) {
+                localVarQueryParameter['albumId'] = albumId;
+            }
+
+            if (isArchived !== undefined) {
+                localVarQueryParameter['isArchived'] = isArchived;
+            }
+
+            if (isFavorite !== undefined) {
+                localVarQueryParameter['isFavorite'] = isFavorite;
+            }
+
+            if (key !== undefined) {
+                localVarQueryParameter['key'] = key;
+            }
+
+
+    
             setSearchParams(localVarUrlObj, localVarQueryParameter);
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@@ -5960,26 +5948,6 @@ export const AssetApiFp = function(configuration?: Configuration) {
             const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetById(id, key, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
-        /**
-         * 
-         * @param {GetAssetByTimeBucketDto} getAssetByTimeBucketDto 
-         * @param {*} [options] Override http request option.
-         * @throws {RequiredError}
-         */
-        async getAssetByTimeBucket(getAssetByTimeBucketDto: GetAssetByTimeBucketDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
-            const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetByTimeBucket(getAssetByTimeBucketDto, options);
-            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
-        },
-        /**
-         * 
-         * @param {GetAssetCountByTimeBucketDto} getAssetCountByTimeBucketDto 
-         * @param {*} [options] Override http request option.
-         * @throws {RequiredError}
-         */
-        async getAssetCountByTimeBucket(getAssetCountByTimeBucketDto: GetAssetCountByTimeBucketDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetCountByTimeBucketResponseDto>> {
-            const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetCountByTimeBucket(getAssetCountByTimeBucketDto, options);
-            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
-        },
         /**
          * 
          * @param {*} [options] Override http request option.
@@ -6012,6 +5980,22 @@ export const AssetApiFp = function(configuration?: Configuration) {
             const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetThumbnail(id, format, key, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
+        /**
+         * 
+         * @param {TimeBucketSize} size 
+         * @param {string} timeBucket 
+         * @param {string} [userId] 
+         * @param {string} [albumId] 
+         * @param {boolean} [isArchived] 
+         * @param {boolean} [isFavorite] 
+         * @param {string} [key] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.getByTimeBucket(size, timeBucket, userId, albumId, isArchived, isFavorite, key, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
         /**
          * 
          * @param {*} [options] Override http request option.
@@ -6066,6 +6050,21 @@ export const AssetApiFp = function(configuration?: Configuration) {
             const localVarAxiosArgs = await localVarAxiosParamCreator.getMemoryLane(timestamp, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
+        /**
+         * 
+         * @param {TimeBucketSize} size 
+         * @param {string} [userId] 
+         * @param {string} [albumId] 
+         * @param {boolean} [isArchived] 
+         * @param {boolean} [isFavorite] 
+         * @param {string} [key] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TimeBucketResponseDto>>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, isArchived, isFavorite, key, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
         /**
          * Get all asset of a device that are in the database, ID only.
          * @param {string} deviceId 
@@ -6224,24 +6223,6 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
         getAssetById(requestParameters: AssetApiGetAssetByIdRequest, options?: AxiosRequestConfig): AxiosPromise<AssetResponseDto> {
             return localVarFp.getAssetById(requestParameters.id, requestParameters.key, options).then((request) => request(axios, basePath));
         },
-        /**
-         * 
-         * @param {AssetApiGetAssetByTimeBucketRequest} requestParameters Request parameters.
-         * @param {*} [options] Override http request option.
-         * @throws {RequiredError}
-         */
-        getAssetByTimeBucket(requestParameters: AssetApiGetAssetByTimeBucketRequest, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> {
-            return localVarFp.getAssetByTimeBucket(requestParameters.getAssetByTimeBucketDto, options).then((request) => request(axios, basePath));
-        },
-        /**
-         * 
-         * @param {AssetApiGetAssetCountByTimeBucketRequest} requestParameters Request parameters.
-         * @param {*} [options] Override http request option.
-         * @throws {RequiredError}
-         */
-        getAssetCountByTimeBucket(requestParameters: AssetApiGetAssetCountByTimeBucketRequest, options?: AxiosRequestConfig): AxiosPromise<AssetCountByTimeBucketResponseDto> {
-            return localVarFp.getAssetCountByTimeBucket(requestParameters.getAssetCountByTimeBucketDto, options).then((request) => request(axios, basePath));
-        },
         /**
          * 
          * @param {*} [options] Override http request option.
@@ -6268,6 +6249,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
         getAssetThumbnail(requestParameters: AssetApiGetAssetThumbnailRequest, options?: AxiosRequestConfig): AxiosPromise<File> {
             return localVarFp.getAssetThumbnail(requestParameters.id, requestParameters.format, requestParameters.key, options).then((request) => request(axios, basePath));
         },
+        /**
+         * 
+         * @param {AssetApiGetByTimeBucketRequest} requestParameters Request parameters.
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> {
+            return localVarFp.getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(axios, basePath));
+        },
         /**
          * 
          * @param {*} [options] Override http request option.
@@ -6311,6 +6301,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
         getMemoryLane(requestParameters: AssetApiGetMemoryLaneRequest, options?: AxiosRequestConfig): AxiosPromise<Array<MemoryLaneResponseDto>> {
             return localVarFp.getMemoryLane(requestParameters.timestamp, options).then((request) => request(axios, basePath));
         },
+        /**
+         * 
+         * @param {AssetApiGetTimeBucketsRequest} requestParameters Request parameters.
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig): AxiosPromise<Array<TimeBucketResponseDto>> {
+            return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(axios, basePath));
+        },
         /**
          * Get all asset of a device that are in the database, ID only.
          * @param {AssetApiGetUserAssetsByDeviceIdRequest} requestParameters Request parameters.
@@ -6543,34 +6542,6 @@ export interface AssetApiGetAssetByIdRequest {
     readonly key?: string
 }
 
-/**
- * Request parameters for getAssetByTimeBucket operation in AssetApi.
- * @export
- * @interface AssetApiGetAssetByTimeBucketRequest
- */
-export interface AssetApiGetAssetByTimeBucketRequest {
-    /**
-     * 
-     * @type {GetAssetByTimeBucketDto}
-     * @memberof AssetApiGetAssetByTimeBucket
-     */
-    readonly getAssetByTimeBucketDto: GetAssetByTimeBucketDto
-}
-
-/**
- * Request parameters for getAssetCountByTimeBucket operation in AssetApi.
- * @export
- * @interface AssetApiGetAssetCountByTimeBucketRequest
- */
-export interface AssetApiGetAssetCountByTimeBucketRequest {
-    /**
-     * 
-     * @type {GetAssetCountByTimeBucketDto}
-     * @memberof AssetApiGetAssetCountByTimeBucket
-     */
-    readonly getAssetCountByTimeBucketDto: GetAssetCountByTimeBucketDto
-}
-
 /**
  * Request parameters for getAssetStats operation in AssetApi.
  * @export
@@ -6620,6 +6591,62 @@ export interface AssetApiGetAssetThumbnailRequest {
     readonly key?: string
 }
 
+/**
+ * Request parameters for getByTimeBucket operation in AssetApi.
+ * @export
+ * @interface AssetApiGetByTimeBucketRequest
+ */
+export interface AssetApiGetByTimeBucketRequest {
+    /**
+     * 
+     * @type {TimeBucketSize}
+     * @memberof AssetApiGetByTimeBucket
+     */
+    readonly size: TimeBucketSize
+
+    /**
+     * 
+     * @type {string}
+     * @memberof AssetApiGetByTimeBucket
+     */
+    readonly timeBucket: string
+
+    /**
+     * 
+     * @type {string}
+     * @memberof AssetApiGetByTimeBucket
+     */
+    readonly userId?: string
+
+    /**
+     * 
+     * @type {string}
+     * @memberof AssetApiGetByTimeBucket
+     */
+    readonly albumId?: string
+
+    /**
+     * 
+     * @type {boolean}
+     * @memberof AssetApiGetByTimeBucket
+     */
+    readonly isArchived?: boolean
+
+    /**
+     * 
+     * @type {boolean}
+     * @memberof AssetApiGetByTimeBucket
+     */
+    readonly isFavorite?: boolean
+
+    /**
+     * 
+     * @type {string}
+     * @memberof AssetApiGetByTimeBucket
+     */
+    readonly key?: string
+}
+
 /**
  * Request parameters for getDownloadInfo operation in AssetApi.
  * @export
@@ -6704,6 +6731,55 @@ export interface AssetApiGetMemoryLaneRequest {
     readonly timestamp: string
 }
 
+/**
+ * Request parameters for getTimeBuckets operation in AssetApi.
+ * @export
+ * @interface AssetApiGetTimeBucketsRequest
+ */
+export interface AssetApiGetTimeBucketsRequest {
+    /**
+     * 
+     * @type {TimeBucketSize}
+     * @memberof AssetApiGetTimeBuckets
+     */
+    readonly size: TimeBucketSize
+
+    /**
+     * 
+     * @type {string}
+     * @memberof AssetApiGetTimeBuckets
+     */
+    readonly userId?: string
+
+    /**
+     * 
+     * @type {string}
+     * @memberof AssetApiGetTimeBuckets
+     */
+    readonly albumId?: string
+
+    /**
+     * 
+     * @type {boolean}
+     * @memberof AssetApiGetTimeBuckets
+     */
+    readonly isArchived?: boolean
+
+    /**
+     * 
+     * @type {boolean}
+     * @memberof AssetApiGetTimeBuckets
+     */
+    readonly isFavorite?: boolean
+
+    /**
+     * 
+     * @type {string}
+     * @memberof AssetApiGetTimeBuckets
+     */
+    readonly key?: string
+}
+
 /**
  * Request parameters for getUserAssetsByDeviceId operation in AssetApi.
  * @export
@@ -6995,28 +7071,6 @@ export class AssetApi extends BaseAPI {
         return AssetApiFp(this.configuration).getAssetById(requestParameters.id, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
     }
 
-    /**
-     * 
-     * @param {AssetApiGetAssetByTimeBucketRequest} requestParameters Request parameters.
-     * @param {*} [options] Override http request option.
-     * @throws {RequiredError}
-     * @memberof AssetApi
-     */
-    public getAssetByTimeBucket(requestParameters: AssetApiGetAssetByTimeBucketRequest, options?: AxiosRequestConfig) {
-        return AssetApiFp(this.configuration).getAssetByTimeBucket(requestParameters.getAssetByTimeBucketDto, options).then((request) => request(this.axios, this.basePath));
-    }
-
-    /**
-     * 
-     * @param {AssetApiGetAssetCountByTimeBucketRequest} requestParameters Request parameters.
-     * @param {*} [options] Override http request option.
-     * @throws {RequiredError}
-     * @memberof AssetApi
-     */
-    public getAssetCountByTimeBucket(requestParameters: AssetApiGetAssetCountByTimeBucketRequest, options?: AxiosRequestConfig) {
-        return AssetApiFp(this.configuration).getAssetCountByTimeBucket(requestParameters.getAssetCountByTimeBucketDto, options).then((request) => request(this.axios, this.basePath));
-    }
-
     /**
      * 
      * @param {*} [options] Override http request option.
@@ -7049,6 +7103,17 @@ export class AssetApi extends BaseAPI {
         return AssetApiFp(this.configuration).getAssetThumbnail(requestParameters.id, requestParameters.format, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
     }
 
+    /**
+     * 
+     * @param {AssetApiGetByTimeBucketRequest} requestParameters Request parameters.
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof AssetApi
+     */
+    public getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig) {
+        return AssetApiFp(this.configuration).getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
+    }
+
     /**
      * 
      * @param {*} [options] Override http request option.
@@ -7102,6 +7167,17 @@ export class AssetApi extends BaseAPI {
         return AssetApiFp(this.configuration).getMemoryLane(requestParameters.timestamp, options).then((request) => request(this.axios, this.basePath));
     }
 
+    /**
+     * 
+     * @param {AssetApiGetTimeBucketsRequest} requestParameters Request parameters.
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof AssetApi
+     */
+    public getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig) {
+        return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
+    }
+
     /**
      * Get all asset of a device that are in the database, ID only.
      * @param {AssetApiGetUserAssetsByDeviceIdRequest} requestParameters Request parameters.

+ 6 - 15
mobile/openapi/.openapi-generator/FILES

@@ -19,8 +19,6 @@ doc/AssetBulkUploadCheckDto.md
 doc/AssetBulkUploadCheckItem.md
 doc/AssetBulkUploadCheckResponseDto.md
 doc/AssetBulkUploadCheckResult.md
-doc/AssetCountByTimeBucket.md
-doc/AssetCountByTimeBucketResponseDto.md
 doc/AssetFileUploadResponseDto.md
 doc/AssetIdsDto.md
 doc/AssetIdsResponseDto.md
@@ -49,8 +47,6 @@ doc/DeleteAssetStatus.md
 doc/DownloadArchiveInfo.md
 doc/DownloadResponseDto.md
 doc/ExifResponseDto.md
-doc/GetAssetByTimeBucketDto.md
-doc/GetAssetCountByTimeBucketDto.md
 doc/ImportAssetDto.md
 doc/JobApi.md
 doc/JobCommand.md
@@ -112,7 +108,8 @@ doc/TagApi.md
 doc/TagResponseDto.md
 doc/TagTypeEnum.md
 doc/ThumbnailFormat.md
-doc/TimeGroupEnum.md
+doc/TimeBucketResponseDto.md
+doc/TimeBucketSize.md
 doc/TranscodeHWAccel.md
 doc/TranscodePolicy.md
 doc/UpdateAlbumDto.md
@@ -162,8 +159,6 @@ lib/model/asset_bulk_upload_check_dto.dart
 lib/model/asset_bulk_upload_check_item.dart
 lib/model/asset_bulk_upload_check_response_dto.dart
 lib/model/asset_bulk_upload_check_result.dart
-lib/model/asset_count_by_time_bucket.dart
-lib/model/asset_count_by_time_bucket_response_dto.dart
 lib/model/asset_file_upload_response_dto.dart
 lib/model/asset_ids_dto.dart
 lib/model/asset_ids_response_dto.dart
@@ -191,8 +186,6 @@ lib/model/delete_asset_status.dart
 lib/model/download_archive_info.dart
 lib/model/download_response_dto.dart
 lib/model/exif_response_dto.dart
-lib/model/get_asset_by_time_bucket_dto.dart
-lib/model/get_asset_count_by_time_bucket_dto.dart
 lib/model/import_asset_dto.dart
 lib/model/job_command.dart
 lib/model/job_command_dto.dart
@@ -245,7 +238,8 @@ lib/model/system_config_template_storage_option_dto.dart
 lib/model/tag_response_dto.dart
 lib/model/tag_type_enum.dart
 lib/model/thumbnail_format.dart
-lib/model/time_group_enum.dart
+lib/model/time_bucket_response_dto.dart
+lib/model/time_bucket_size.dart
 lib/model/transcode_hw_accel.dart
 lib/model/transcode_policy.dart
 lib/model/update_album_dto.dart
@@ -274,8 +268,6 @@ test/asset_bulk_upload_check_dto_test.dart
 test/asset_bulk_upload_check_item_test.dart
 test/asset_bulk_upload_check_response_dto_test.dart
 test/asset_bulk_upload_check_result_test.dart
-test/asset_count_by_time_bucket_response_dto_test.dart
-test/asset_count_by_time_bucket_test.dart
 test/asset_file_upload_response_dto_test.dart
 test/asset_ids_dto_test.dart
 test/asset_ids_response_dto_test.dart
@@ -304,8 +296,6 @@ test/delete_asset_status_test.dart
 test/download_archive_info_test.dart
 test/download_response_dto_test.dart
 test/exif_response_dto_test.dart
-test/get_asset_by_time_bucket_dto_test.dart
-test/get_asset_count_by_time_bucket_dto_test.dart
 test/import_asset_dto_test.dart
 test/job_api_test.dart
 test/job_command_dto_test.dart
@@ -367,7 +357,8 @@ test/tag_api_test.dart
 test/tag_response_dto_test.dart
 test/tag_type_enum_test.dart
 test/thumbnail_format_test.dart
-test/time_group_enum_test.dart
+test/time_bucket_response_dto_test.dart
+test/time_bucket_size_test.dart
 test/transcode_hw_accel_test.dart
 test/transcode_policy_test.dart
 test/update_album_dto_test.dart

+ 4 - 7
mobile/openapi/README.md

@@ -95,16 +95,16 @@ Class | Method | HTTP request | Description
 *AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **POST** /asset/download/{id} | 
 *AssetApi* | [**getAllAssets**](doc//AssetApi.md#getallassets) | **GET** /asset | 
 *AssetApi* | [**getAssetById**](doc//AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} | 
-*AssetApi* | [**getAssetByTimeBucket**](doc//AssetApi.md#getassetbytimebucket) | **POST** /asset/time-bucket | 
-*AssetApi* | [**getAssetCountByTimeBucket**](doc//AssetApi.md#getassetcountbytimebucket) | **POST** /asset/count-by-time-bucket | 
 *AssetApi* | [**getAssetSearchTerms**](doc//AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms | 
 *AssetApi* | [**getAssetStats**](doc//AssetApi.md#getassetstats) | **GET** /asset/statistics | 
 *AssetApi* | [**getAssetThumbnail**](doc//AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{id} | 
+*AssetApi* | [**getByTimeBucket**](doc//AssetApi.md#getbytimebucket) | **GET** /asset/time-bucket | 
 *AssetApi* | [**getCuratedLocations**](doc//AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations | 
 *AssetApi* | [**getCuratedObjects**](doc//AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects | 
 *AssetApi* | [**getDownloadInfo**](doc//AssetApi.md#getdownloadinfo) | **GET** /asset/download | 
 *AssetApi* | [**getMapMarkers**](doc//AssetApi.md#getmapmarkers) | **GET** /asset/map-marker | 
 *AssetApi* | [**getMemoryLane**](doc//AssetApi.md#getmemorylane) | **GET** /asset/memory-lane | 
+*AssetApi* | [**getTimeBuckets**](doc//AssetApi.md#gettimebuckets) | **GET** /asset/time-buckets | 
 *AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} | 
 *AssetApi* | [**importFile**](doc//AssetApi.md#importfile) | **POST** /asset/import | 
 *AssetApi* | [**searchAsset**](doc//AssetApi.md#searchasset) | **POST** /asset/search | 
@@ -191,8 +191,6 @@ Class | Method | HTTP request | Description
  - [AssetBulkUploadCheckItem](doc//AssetBulkUploadCheckItem.md)
  - [AssetBulkUploadCheckResponseDto](doc//AssetBulkUploadCheckResponseDto.md)
  - [AssetBulkUploadCheckResult](doc//AssetBulkUploadCheckResult.md)
- - [AssetCountByTimeBucket](doc//AssetCountByTimeBucket.md)
- - [AssetCountByTimeBucketResponseDto](doc//AssetCountByTimeBucketResponseDto.md)
  - [AssetFileUploadResponseDto](doc//AssetFileUploadResponseDto.md)
  - [AssetIdsDto](doc//AssetIdsDto.md)
  - [AssetIdsResponseDto](doc//AssetIdsResponseDto.md)
@@ -220,8 +218,6 @@ Class | Method | HTTP request | Description
  - [DownloadArchiveInfo](doc//DownloadArchiveInfo.md)
  - [DownloadResponseDto](doc//DownloadResponseDto.md)
  - [ExifResponseDto](doc//ExifResponseDto.md)
- - [GetAssetByTimeBucketDto](doc//GetAssetByTimeBucketDto.md)
- - [GetAssetCountByTimeBucketDto](doc//GetAssetCountByTimeBucketDto.md)
  - [ImportAssetDto](doc//ImportAssetDto.md)
  - [JobCommand](doc//JobCommand.md)
  - [JobCommandDto](doc//JobCommandDto.md)
@@ -274,7 +270,8 @@ Class | Method | HTTP request | Description
  - [TagResponseDto](doc//TagResponseDto.md)
  - [TagTypeEnum](doc//TagTypeEnum.md)
  - [ThumbnailFormat](doc//ThumbnailFormat.md)
- - [TimeGroupEnum](doc//TimeGroupEnum.md)
+ - [TimeBucketResponseDto](doc//TimeBucketResponseDto.md)
+ - [TimeBucketSize](doc//TimeBucketSize.md)
  - [TranscodeHWAccel](doc//TranscodeHWAccel.md)
  - [TranscodePolicy](doc//TranscodePolicy.md)
  - [UpdateAlbumDto](doc//UpdateAlbumDto.md)

+ 113 - 91
mobile/openapi/doc/AssetApi.md

@@ -17,16 +17,16 @@ Method | HTTP request | Description
 [**downloadFile**](AssetApi.md#downloadfile) | **POST** /asset/download/{id} | 
 [**getAllAssets**](AssetApi.md#getallassets) | **GET** /asset | 
 [**getAssetById**](AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} | 
-[**getAssetByTimeBucket**](AssetApi.md#getassetbytimebucket) | **POST** /asset/time-bucket | 
-[**getAssetCountByTimeBucket**](AssetApi.md#getassetcountbytimebucket) | **POST** /asset/count-by-time-bucket | 
 [**getAssetSearchTerms**](AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms | 
 [**getAssetStats**](AssetApi.md#getassetstats) | **GET** /asset/statistics | 
 [**getAssetThumbnail**](AssetApi.md#getassetthumbnail) | **GET** /asset/thumbnail/{id} | 
+[**getByTimeBucket**](AssetApi.md#getbytimebucket) | **GET** /asset/time-bucket | 
 [**getCuratedLocations**](AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations | 
 [**getCuratedObjects**](AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects | 
 [**getDownloadInfo**](AssetApi.md#getdownloadinfo) | **GET** /asset/download | 
 [**getMapMarkers**](AssetApi.md#getmapmarkers) | **GET** /asset/map-marker | 
 [**getMemoryLane**](AssetApi.md#getmemorylane) | **GET** /asset/memory-lane | 
+[**getTimeBuckets**](AssetApi.md#gettimebuckets) | **GET** /asset/time-buckets | 
 [**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} | 
 [**importFile**](AssetApi.md#importfile) | **POST** /asset/import | 
 [**searchAsset**](AssetApi.md#searchasset) | **POST** /asset/search | 
@@ -503,8 +503,8 @@ Name | Type | Description  | Notes
 
 [[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)
 
-# **getAssetByTimeBucket**
-> List<AssetResponseDto> getAssetByTimeBucket(getAssetByTimeBucketDto)
+# **getAssetSearchTerms**
+> List<String> getAssetSearchTerms()
 
 
 
@@ -527,25 +527,21 @@ import 'package:openapi/api.dart';
 //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
 
 final api_instance = AssetApi();
-final getAssetByTimeBucketDto = GetAssetByTimeBucketDto(); // GetAssetByTimeBucketDto | 
 
 try {
-    final result = api_instance.getAssetByTimeBucket(getAssetByTimeBucketDto);
+    final result = api_instance.getAssetSearchTerms();
     print(result);
 } catch (e) {
-    print('Exception when calling AssetApi->getAssetByTimeBucket: $e\n');
+    print('Exception when calling AssetApi->getAssetSearchTerms: $e\n');
 }
 ```
 
 ### Parameters
-
-Name | Type | Description  | Notes
-------------- | ------------- | ------------- | -------------
- **getAssetByTimeBucketDto** | [**GetAssetByTimeBucketDto**](GetAssetByTimeBucketDto.md)|  | 
+This endpoint does not need any parameter.
 
 ### Return type
 
-[**List<AssetResponseDto>**](AssetResponseDto.md)
+**List<String>**
 
 ### Authorization
 
@@ -553,13 +549,13 @@ Name | Type | Description  | Notes
 
 ### HTTP request headers
 
- - **Content-Type**: application/json
+ - **Content-Type**: Not defined
  - **Accept**: application/json
 
 [[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)
 
-# **getAssetCountByTimeBucket**
-> AssetCountByTimeBucketResponseDto getAssetCountByTimeBucket(getAssetCountByTimeBucketDto)
+# **getAssetStats**
+> AssetStatsResponseDto getAssetStats(isArchived, isFavorite)
 
 
 
@@ -582,13 +578,14 @@ import 'package:openapi/api.dart';
 //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
 
 final api_instance = AssetApi();
-final getAssetCountByTimeBucketDto = GetAssetCountByTimeBucketDto(); // GetAssetCountByTimeBucketDto | 
+final isArchived = true; // bool | 
+final isFavorite = true; // bool | 
 
 try {
-    final result = api_instance.getAssetCountByTimeBucket(getAssetCountByTimeBucketDto);
+    final result = api_instance.getAssetStats(isArchived, isFavorite);
     print(result);
 } catch (e) {
-    print('Exception when calling AssetApi->getAssetCountByTimeBucket: $e\n');
+    print('Exception when calling AssetApi->getAssetStats: $e\n');
 }
 ```
 
@@ -596,62 +593,12 @@ try {
 
 Name | Type | Description  | Notes
 ------------- | ------------- | ------------- | -------------
- **getAssetCountByTimeBucketDto** | [**GetAssetCountByTimeBucketDto**](GetAssetCountByTimeBucketDto.md)|  | 
-
-### Return type
-
-[**AssetCountByTimeBucketResponseDto**](AssetCountByTimeBucketResponseDto.md)
-
-### Authorization
-
-[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
-
-### HTTP request headers
-
- - **Content-Type**: application/json
- - **Accept**: application/json
-
-[[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)
-
-# **getAssetSearchTerms**
-> List<String> getAssetSearchTerms()
-
-
-
-### Example
-```dart
-import 'package:openapi/api.dart';
-// TODO Configure API key authorization: cookie
-//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
-// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
-// TODO Configure API key authorization: api_key
-//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
-// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
-//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
-// TODO Configure HTTP Bearer authorization: bearer
-// Case 1. Use String Token
-//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
-// Case 2. Use Function which generate token.
-// String yourTokenGeneratorFunction() { ... }
-//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
-
-final api_instance = AssetApi();
-
-try {
-    final result = api_instance.getAssetSearchTerms();
-    print(result);
-} catch (e) {
-    print('Exception when calling AssetApi->getAssetSearchTerms: $e\n');
-}
-```
-
-### Parameters
-This endpoint does not need any parameter.
+ **isArchived** | **bool**|  | [optional] 
+ **isFavorite** | **bool**|  | [optional] 
 
 ### Return type
 
-**List<String>**
+[**AssetStatsResponseDto**](AssetStatsResponseDto.md)
 
 ### Authorization
 
@@ -664,8 +611,8 @@ 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)
 
-# **getAssetStats**
-> AssetStatsResponseDto getAssetStats(isArchived, isFavorite)
+# **getAssetThumbnail**
+> MultipartFile getAssetThumbnail(id, format, key)
 
 
 
@@ -688,14 +635,15 @@ import 'package:openapi/api.dart';
 //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
 
 final api_instance = AssetApi();
-final isArchived = true; // bool | 
-final isFavorite = true; // bool | 
+final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | 
+final format = ; // ThumbnailFormat | 
+final key = key_example; // String | 
 
 try {
-    final result = api_instance.getAssetStats(isArchived, isFavorite);
+    final result = api_instance.getAssetThumbnail(id, format, key);
     print(result);
 } catch (e) {
-    print('Exception when calling AssetApi->getAssetStats: $e\n');
+    print('Exception when calling AssetApi->getAssetThumbnail: $e\n');
 }
 ```
 
@@ -703,12 +651,13 @@ try {
 
 Name | Type | Description  | Notes
 ------------- | ------------- | ------------- | -------------
- **isArchived** | **bool**|  | [optional] 
- **isFavorite** | **bool**|  | [optional] 
+ **id** | **String**|  | 
+ **format** | [**ThumbnailFormat**](.md)|  | [optional] 
+ **key** | **String**|  | [optional] 
 
 ### Return type
 
-[**AssetStatsResponseDto**](AssetStatsResponseDto.md)
+[**MultipartFile**](MultipartFile.md)
 
 ### Authorization
 
@@ -717,12 +666,12 @@ Name | Type | Description  | Notes
 ### HTTP request headers
 
  - **Content-Type**: Not defined
- - **Accept**: application/json
+ - **Accept**: image/jpeg, image/webp
 
 [[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)
 
-# **getAssetThumbnail**
-> MultipartFile getAssetThumbnail(id, format, key)
+# **getByTimeBucket**
+> List<AssetResponseDto> getByTimeBucket(size, timeBucket, userId, albumId, isArchived, isFavorite, key)
 
 
 
@@ -745,15 +694,19 @@ import 'package:openapi/api.dart';
 //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
 
 final api_instance = AssetApi();
-final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | 
-final format = ; // ThumbnailFormat | 
+final size = ; // TimeBucketSize | 
+final timeBucket = timeBucket_example; // String | 
+final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | 
+final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | 
+final isArchived = true; // bool | 
+final isFavorite = true; // bool | 
 final key = key_example; // String | 
 
 try {
-    final result = api_instance.getAssetThumbnail(id, format, key);
+    final result = api_instance.getByTimeBucket(size, timeBucket, userId, albumId, isArchived, isFavorite, key);
     print(result);
 } catch (e) {
-    print('Exception when calling AssetApi->getAssetThumbnail: $e\n');
+    print('Exception when calling AssetApi->getByTimeBucket: $e\n');
 }
 ```
 
@@ -761,13 +714,17 @@ try {
 
 Name | Type | Description  | Notes
 ------------- | ------------- | ------------- | -------------
- **id** | **String**|  | 
- **format** | [**ThumbnailFormat**](.md)|  | [optional] 
+ **size** | [**TimeBucketSize**](.md)|  | 
+ **timeBucket** | **String**|  | 
+ **userId** | **String**|  | [optional] 
+ **albumId** | **String**|  | [optional] 
+ **isArchived** | **bool**|  | [optional] 
+ **isFavorite** | **bool**|  | [optional] 
  **key** | **String**|  | [optional] 
 
 ### Return type
 
-[**MultipartFile**](MultipartFile.md)
+[**List<AssetResponseDto>**](AssetResponseDto.md)
 
 ### Authorization
 
@@ -776,7 +733,7 @@ Name | Type | Description  | Notes
 ### HTTP request headers
 
  - **Content-Type**: Not defined
- - **Accept**: image/jpeg, image/webp
+ - **Accept**: application/json
 
 [[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)
 
@@ -1059,6 +1016,71 @@ Name | Type | Description  | Notes
 
 [[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)
 
+# **getTimeBuckets**
+> List<TimeBucketResponseDto> getTimeBuckets(size, userId, albumId, isArchived, isFavorite, key)
+
+
+
+### Example
+```dart
+import 'package:openapi/api.dart';
+// TODO Configure API key authorization: cookie
+//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
+// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
+//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
+// TODO Configure API key authorization: api_key
+//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
+// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
+//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
+// TODO Configure HTTP Bearer authorization: bearer
+// Case 1. Use String Token
+//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
+// Case 2. Use Function which generate token.
+// String yourTokenGeneratorFunction() { ... }
+//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
+
+final api_instance = AssetApi();
+final size = ; // TimeBucketSize | 
+final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | 
+final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | 
+final isArchived = true; // bool | 
+final isFavorite = true; // bool | 
+final key = key_example; // String | 
+
+try {
+    final result = api_instance.getTimeBuckets(size, userId, albumId, isArchived, isFavorite, key);
+    print(result);
+} catch (e) {
+    print('Exception when calling AssetApi->getTimeBuckets: $e\n');
+}
+```
+
+### Parameters
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+ **size** | [**TimeBucketSize**](.md)|  | 
+ **userId** | **String**|  | [optional] 
+ **albumId** | **String**|  | [optional] 
+ **isArchived** | **bool**|  | [optional] 
+ **isFavorite** | **bool**|  | [optional] 
+ **key** | **String**|  | [optional] 
+
+### Return type
+
+[**List<TimeBucketResponseDto>**](TimeBucketResponseDto.md)
+
+### Authorization
+
+[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
+
+### HTTP request headers
+
+ - **Content-Type**: Not defined
+ - **Accept**: application/json
+
+[[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)
+
 # **getUserAssetsByDeviceId**
 > List<String> getUserAssetsByDeviceId(deviceId)
 

+ 0 - 16
mobile/openapi/doc/AssetCountByTimeBucketResponseDto.md

@@ -1,16 +0,0 @@
-# openapi.model.AssetCountByTimeBucketResponseDto
-
-## Load the model package
-```dart
-import 'package:openapi/api.dart';
-```
-
-## Properties
-Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
-**buckets** | [**List<AssetCountByTimeBucket>**](AssetCountByTimeBucket.md) |  | [default to const []]
-**totalCount** | **int** |  | 
-
-[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
-
-

+ 0 - 17
mobile/openapi/doc/GetAssetByTimeBucketDto.md

@@ -1,17 +0,0 @@
-# openapi.model.GetAssetByTimeBucketDto
-
-## Load the model package
-```dart
-import 'package:openapi/api.dart';
-```
-
-## Properties
-Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
-**timeBucket** | **List<String>** |  | [default to const []]
-**userId** | **String** |  | [optional] 
-**withoutThumbs** | **bool** | Include assets without thumbnails | [optional] 
-
-[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
-
-

+ 0 - 17
mobile/openapi/doc/GetAssetCountByTimeBucketDto.md

@@ -1,17 +0,0 @@
-# openapi.model.GetAssetCountByTimeBucketDto
-
-## Load the model package
-```dart
-import 'package:openapi/api.dart';
-```
-
-## Properties
-Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
-**timeGroup** | [**TimeGroupEnum**](TimeGroupEnum.md) |  | 
-**userId** | **String** |  | [optional] 
-**withoutThumbs** | **bool** | Include assets without thumbnails | [optional] 
-
-[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
-
-

+ 1 - 1
mobile/openapi/doc/AssetCountByTimeBucket.md → mobile/openapi/doc/TimeBucketResponseDto.md

@@ -1,4 +1,4 @@
-# openapi.model.AssetCountByTimeBucket
+# openapi.model.TimeBucketResponseDto
 
 ## Load the model package
 ```dart

+ 1 - 1
mobile/openapi/doc/TimeGroupEnum.md → mobile/openapi/doc/TimeBucketSize.md

@@ -1,4 +1,4 @@
-# openapi.model.TimeGroupEnum
+# openapi.model.TimeBucketSize
 
 ## Load the model package
 ```dart

+ 2 - 5
mobile/openapi/lib/api.dart

@@ -56,8 +56,6 @@ part 'model/asset_bulk_upload_check_dto.dart';
 part 'model/asset_bulk_upload_check_item.dart';
 part 'model/asset_bulk_upload_check_response_dto.dart';
 part 'model/asset_bulk_upload_check_result.dart';
-part 'model/asset_count_by_time_bucket.dart';
-part 'model/asset_count_by_time_bucket_response_dto.dart';
 part 'model/asset_file_upload_response_dto.dart';
 part 'model/asset_ids_dto.dart';
 part 'model/asset_ids_response_dto.dart';
@@ -85,8 +83,6 @@ part 'model/delete_asset_status.dart';
 part 'model/download_archive_info.dart';
 part 'model/download_response_dto.dart';
 part 'model/exif_response_dto.dart';
-part 'model/get_asset_by_time_bucket_dto.dart';
-part 'model/get_asset_count_by_time_bucket_dto.dart';
 part 'model/import_asset_dto.dart';
 part 'model/job_command.dart';
 part 'model/job_command_dto.dart';
@@ -139,7 +135,8 @@ part 'model/system_config_template_storage_option_dto.dart';
 part 'model/tag_response_dto.dart';
 part 'model/tag_type_enum.dart';
 part 'model/thumbnail_format.dart';
-part 'model/time_group_enum.dart';
+part 'model/time_bucket_response_dto.dart';
+part 'model/time_bucket_size.dart';
 part 'model/transcode_hw_accel.dart';
 part 'model/transcode_policy.dart';
 part 'model/update_album_dto.dart';

+ 179 - 97
mobile/openapi/lib/api/asset_api.dart

@@ -501,103 +501,6 @@ class AssetApi {
     return null;
   }
 
-  /// Performs an HTTP 'POST /asset/time-bucket' operation and returns the [Response].
-  /// Parameters:
-  ///
-  /// * [GetAssetByTimeBucketDto] getAssetByTimeBucketDto (required):
-  Future<Response> getAssetByTimeBucketWithHttpInfo(GetAssetByTimeBucketDto getAssetByTimeBucketDto,) async {
-    // ignore: prefer_const_declarations
-    final path = r'/asset/time-bucket';
-
-    // ignore: prefer_final_locals
-    Object? postBody = getAssetByTimeBucketDto;
-
-    final queryParams = <QueryParam>[];
-    final headerParams = <String, String>{};
-    final formParams = <String, String>{};
-
-    const contentTypes = <String>['application/json'];
-
-
-    return apiClient.invokeAPI(
-      path,
-      'POST',
-      queryParams,
-      postBody,
-      headerParams,
-      formParams,
-      contentTypes.isEmpty ? null : contentTypes.first,
-    );
-  }
-
-  /// Parameters:
-  ///
-  /// * [GetAssetByTimeBucketDto] getAssetByTimeBucketDto (required):
-  Future<List<AssetResponseDto>?> getAssetByTimeBucket(GetAssetByTimeBucketDto getAssetByTimeBucketDto,) async {
-    final response = await getAssetByTimeBucketWithHttpInfo(getAssetByTimeBucketDto,);
-    if (response.statusCode >= HttpStatus.badRequest) {
-      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
-    }
-    // When a remote server returns no body with a status of 204, we shall not decode it.
-    // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
-    // FormatException when trying to decode an empty string.
-    if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
-      final responseBody = await _decodeBodyBytes(response);
-      return (await apiClient.deserializeAsync(responseBody, 'List<AssetResponseDto>') as List)
-        .cast<AssetResponseDto>()
-        .toList();
-
-    }
-    return null;
-  }
-
-  /// Performs an HTTP 'POST /asset/count-by-time-bucket' operation and returns the [Response].
-  /// Parameters:
-  ///
-  /// * [GetAssetCountByTimeBucketDto] getAssetCountByTimeBucketDto (required):
-  Future<Response> getAssetCountByTimeBucketWithHttpInfo(GetAssetCountByTimeBucketDto getAssetCountByTimeBucketDto,) async {
-    // ignore: prefer_const_declarations
-    final path = r'/asset/count-by-time-bucket';
-
-    // ignore: prefer_final_locals
-    Object? postBody = getAssetCountByTimeBucketDto;
-
-    final queryParams = <QueryParam>[];
-    final headerParams = <String, String>{};
-    final formParams = <String, String>{};
-
-    const contentTypes = <String>['application/json'];
-
-
-    return apiClient.invokeAPI(
-      path,
-      'POST',
-      queryParams,
-      postBody,
-      headerParams,
-      formParams,
-      contentTypes.isEmpty ? null : contentTypes.first,
-    );
-  }
-
-  /// Parameters:
-  ///
-  /// * [GetAssetCountByTimeBucketDto] getAssetCountByTimeBucketDto (required):
-  Future<AssetCountByTimeBucketResponseDto?> getAssetCountByTimeBucket(GetAssetCountByTimeBucketDto getAssetCountByTimeBucketDto,) async {
-    final response = await getAssetCountByTimeBucketWithHttpInfo(getAssetCountByTimeBucketDto,);
-    if (response.statusCode >= HttpStatus.badRequest) {
-      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
-    }
-    // When a remote server returns no body with a status of 204, we shall not decode it.
-    // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
-    // FormatException when trying to decode an empty string.
-    if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
-      return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetCountByTimeBucketResponseDto',) as AssetCountByTimeBucketResponseDto;
-    
-    }
-    return null;
-  }
-
   /// Performs an HTTP 'GET /asset/search-terms' operation and returns the [Response].
   Future<Response> getAssetSearchTermsWithHttpInfo() async {
     // ignore: prefer_const_declarations
@@ -763,6 +666,98 @@ class AssetApi {
     return null;
   }
 
+  /// Performs an HTTP 'GET /asset/time-bucket' operation and returns the [Response].
+  /// Parameters:
+  ///
+  /// * [TimeBucketSize] size (required):
+  ///
+  /// * [String] timeBucket (required):
+  ///
+  /// * [String] userId:
+  ///
+  /// * [String] albumId:
+  ///
+  /// * [bool] isArchived:
+  ///
+  /// * [bool] isFavorite:
+  ///
+  /// * [String] key:
+  Future<Response> getByTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? userId, String? albumId, bool? isArchived, bool? isFavorite, String? key, }) async {
+    // ignore: prefer_const_declarations
+    final path = r'/asset/time-bucket';
+
+    // ignore: prefer_final_locals
+    Object? postBody;
+
+    final queryParams = <QueryParam>[];
+    final headerParams = <String, String>{};
+    final formParams = <String, String>{};
+
+      queryParams.addAll(_queryParams('', 'size', size));
+    if (userId != null) {
+      queryParams.addAll(_queryParams('', 'userId', userId));
+    }
+    if (albumId != null) {
+      queryParams.addAll(_queryParams('', 'albumId', albumId));
+    }
+    if (isArchived != null) {
+      queryParams.addAll(_queryParams('', 'isArchived', isArchived));
+    }
+    if (isFavorite != null) {
+      queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
+    }
+      queryParams.addAll(_queryParams('', 'timeBucket', timeBucket));
+    if (key != null) {
+      queryParams.addAll(_queryParams('', 'key', key));
+    }
+
+    const contentTypes = <String>[];
+
+
+    return apiClient.invokeAPI(
+      path,
+      'GET',
+      queryParams,
+      postBody,
+      headerParams,
+      formParams,
+      contentTypes.isEmpty ? null : contentTypes.first,
+    );
+  }
+
+  /// Parameters:
+  ///
+  /// * [TimeBucketSize] size (required):
+  ///
+  /// * [String] timeBucket (required):
+  ///
+  /// * [String] userId:
+  ///
+  /// * [String] albumId:
+  ///
+  /// * [bool] isArchived:
+  ///
+  /// * [bool] isFavorite:
+  ///
+  /// * [String] key:
+  Future<List<AssetResponseDto>?> getByTimeBucket(TimeBucketSize size, String timeBucket, { String? userId, String? albumId, bool? isArchived, bool? isFavorite, String? key, }) async {
+    final response = await getByTimeBucketWithHttpInfo(size, timeBucket,  userId: userId, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, key: key, );
+    if (response.statusCode >= HttpStatus.badRequest) {
+      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+    }
+    // When a remote server returns no body with a status of 204, we shall not decode it.
+    // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
+    // FormatException when trying to decode an empty string.
+    if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
+      final responseBody = await _decodeBodyBytes(response);
+      return (await apiClient.deserializeAsync(responseBody, 'List<AssetResponseDto>') as List)
+        .cast<AssetResponseDto>()
+        .toList();
+
+    }
+    return null;
+  }
+
   /// Performs an HTTP 'GET /asset/curated-locations' operation and returns the [Response].
   Future<Response> getCuratedLocationsWithHttpInfo() async {
     // ignore: prefer_const_declarations
@@ -1052,6 +1047,93 @@ class AssetApi {
     return null;
   }
 
+  /// Performs an HTTP 'GET /asset/time-buckets' operation and returns the [Response].
+  /// Parameters:
+  ///
+  /// * [TimeBucketSize] size (required):
+  ///
+  /// * [String] userId:
+  ///
+  /// * [String] albumId:
+  ///
+  /// * [bool] isArchived:
+  ///
+  /// * [bool] isFavorite:
+  ///
+  /// * [String] key:
+  Future<Response> getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? userId, String? albumId, bool? isArchived, bool? isFavorite, String? key, }) async {
+    // ignore: prefer_const_declarations
+    final path = r'/asset/time-buckets';
+
+    // ignore: prefer_final_locals
+    Object? postBody;
+
+    final queryParams = <QueryParam>[];
+    final headerParams = <String, String>{};
+    final formParams = <String, String>{};
+
+      queryParams.addAll(_queryParams('', 'size', size));
+    if (userId != null) {
+      queryParams.addAll(_queryParams('', 'userId', userId));
+    }
+    if (albumId != null) {
+      queryParams.addAll(_queryParams('', 'albumId', albumId));
+    }
+    if (isArchived != null) {
+      queryParams.addAll(_queryParams('', 'isArchived', isArchived));
+    }
+    if (isFavorite != null) {
+      queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
+    }
+    if (key != null) {
+      queryParams.addAll(_queryParams('', 'key', key));
+    }
+
+    const contentTypes = <String>[];
+
+
+    return apiClient.invokeAPI(
+      path,
+      'GET',
+      queryParams,
+      postBody,
+      headerParams,
+      formParams,
+      contentTypes.isEmpty ? null : contentTypes.first,
+    );
+  }
+
+  /// Parameters:
+  ///
+  /// * [TimeBucketSize] size (required):
+  ///
+  /// * [String] userId:
+  ///
+  /// * [String] albumId:
+  ///
+  /// * [bool] isArchived:
+  ///
+  /// * [bool] isFavorite:
+  ///
+  /// * [String] key:
+  Future<List<TimeBucketResponseDto>?> getTimeBuckets(TimeBucketSize size, { String? userId, String? albumId, bool? isArchived, bool? isFavorite, String? key, }) async {
+    final response = await getTimeBucketsWithHttpInfo(size,  userId: userId, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, key: key, );
+    if (response.statusCode >= HttpStatus.badRequest) {
+      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+    }
+    // When a remote server returns no body with a status of 204, we shall not decode it.
+    // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
+    // FormatException when trying to decode an empty string.
+    if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
+      final responseBody = await _decodeBodyBytes(response);
+      return (await apiClient.deserializeAsync(responseBody, 'List<TimeBucketResponseDto>') as List)
+        .cast<TimeBucketResponseDto>()
+        .toList();
+
+    }
+    return null;
+  }
+
   /// Get all asset of a device that are in the database, ID only.
   ///
   /// Note: This method returns the HTTP [Response].

+ 4 - 10
mobile/openapi/lib/api_client.dart

@@ -207,10 +207,6 @@ class ApiClient {
           return AssetBulkUploadCheckResponseDto.fromJson(value);
         case 'AssetBulkUploadCheckResult':
           return AssetBulkUploadCheckResult.fromJson(value);
-        case 'AssetCountByTimeBucket':
-          return AssetCountByTimeBucket.fromJson(value);
-        case 'AssetCountByTimeBucketResponseDto':
-          return AssetCountByTimeBucketResponseDto.fromJson(value);
         case 'AssetFileUploadResponseDto':
           return AssetFileUploadResponseDto.fromJson(value);
         case 'AssetIdsDto':
@@ -265,10 +261,6 @@ class ApiClient {
           return DownloadResponseDto.fromJson(value);
         case 'ExifResponseDto':
           return ExifResponseDto.fromJson(value);
-        case 'GetAssetByTimeBucketDto':
-          return GetAssetByTimeBucketDto.fromJson(value);
-        case 'GetAssetCountByTimeBucketDto':
-          return GetAssetCountByTimeBucketDto.fromJson(value);
         case 'ImportAssetDto':
           return ImportAssetDto.fromJson(value);
         case 'JobCommand':
@@ -373,8 +365,10 @@ class ApiClient {
           return TagTypeEnumTypeTransformer().decode(value);
         case 'ThumbnailFormat':
           return ThumbnailFormatTypeTransformer().decode(value);
-        case 'TimeGroupEnum':
-          return TimeGroupEnumTypeTransformer().decode(value);
+        case 'TimeBucketResponseDto':
+          return TimeBucketResponseDto.fromJson(value);
+        case 'TimeBucketSize':
+          return TimeBucketSizeTypeTransformer().decode(value);
         case 'TranscodeHWAccel':
           return TranscodeHWAccelTypeTransformer().decode(value);
         case 'TranscodePolicy':

+ 2 - 2
mobile/openapi/lib/api_helper.dart

@@ -79,8 +79,8 @@ String parameterToString(dynamic value) {
   if (value is ThumbnailFormat) {
     return ThumbnailFormatTypeTransformer().encode(value).toString();
   }
-  if (value is TimeGroupEnum) {
-    return TimeGroupEnumTypeTransformer().encode(value).toString();
+  if (value is TimeBucketSize) {
+    return TimeBucketSizeTypeTransformer().encode(value).toString();
   }
   if (value is TranscodeHWAccel) {
     return TranscodeHWAccelTypeTransformer().encode(value).toString();

+ 0 - 106
mobile/openapi/lib/model/asset_count_by_time_bucket_response_dto.dart

@@ -1,106 +0,0 @@
-//
-// AUTO-GENERATED FILE, DO NOT MODIFY!
-//
-// @dart=2.12
-
-// ignore_for_file: unused_element, unused_import
-// ignore_for_file: always_put_required_named_parameters_first
-// ignore_for_file: constant_identifier_names
-// ignore_for_file: lines_longer_than_80_chars
-
-part of openapi.api;
-
-class AssetCountByTimeBucketResponseDto {
-  /// Returns a new [AssetCountByTimeBucketResponseDto] instance.
-  AssetCountByTimeBucketResponseDto({
-    this.buckets = const [],
-    required this.totalCount,
-  });
-
-  List<AssetCountByTimeBucket> buckets;
-
-  int totalCount;
-
-  @override
-  bool operator ==(Object other) => identical(this, other) || other is AssetCountByTimeBucketResponseDto &&
-     other.buckets == buckets &&
-     other.totalCount == totalCount;
-
-  @override
-  int get hashCode =>
-    // ignore: unnecessary_parenthesis
-    (buckets.hashCode) +
-    (totalCount.hashCode);
-
-  @override
-  String toString() => 'AssetCountByTimeBucketResponseDto[buckets=$buckets, totalCount=$totalCount]';
-
-  Map<String, dynamic> toJson() {
-    final json = <String, dynamic>{};
-      json[r'buckets'] = this.buckets;
-      json[r'totalCount'] = this.totalCount;
-    return json;
-  }
-
-  /// Returns a new [AssetCountByTimeBucketResponseDto] instance and imports its values from
-  /// [value] if it's a [Map], null otherwise.
-  // ignore: prefer_constructors_over_static_methods
-  static AssetCountByTimeBucketResponseDto? fromJson(dynamic value) {
-    if (value is Map) {
-      final json = value.cast<String, dynamic>();
-
-      return AssetCountByTimeBucketResponseDto(
-        buckets: AssetCountByTimeBucket.listFromJson(json[r'buckets']),
-        totalCount: mapValueOfType<int>(json, r'totalCount')!,
-      );
-    }
-    return null;
-  }
-
-  static List<AssetCountByTimeBucketResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
-    final result = <AssetCountByTimeBucketResponseDto>[];
-    if (json is List && json.isNotEmpty) {
-      for (final row in json) {
-        final value = AssetCountByTimeBucketResponseDto.fromJson(row);
-        if (value != null) {
-          result.add(value);
-        }
-      }
-    }
-    return result.toList(growable: growable);
-  }
-
-  static Map<String, AssetCountByTimeBucketResponseDto> mapFromJson(dynamic json) {
-    final map = <String, AssetCountByTimeBucketResponseDto>{};
-    if (json is Map && json.isNotEmpty) {
-      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
-      for (final entry in json.entries) {
-        final value = AssetCountByTimeBucketResponseDto.fromJson(entry.value);
-        if (value != null) {
-          map[entry.key] = value;
-        }
-      }
-    }
-    return map;
-  }
-
-  // maps a json object with a list of AssetCountByTimeBucketResponseDto-objects as value to a dart map
-  static Map<String, List<AssetCountByTimeBucketResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
-    final map = <String, List<AssetCountByTimeBucketResponseDto>>{};
-    if (json is Map && json.isNotEmpty) {
-      // ignore: parameter_assignments
-      json = json.cast<String, dynamic>();
-      for (final entry in json.entries) {
-        map[entry.key] = AssetCountByTimeBucketResponseDto.listFromJson(entry.value, growable: growable,);
-      }
-    }
-    return map;
-  }
-
-  /// The list of required keys that must be present in a JSON.
-  static const requiredKeys = <String>{
-    'buckets',
-    'totalCount',
-  };
-}
-

+ 0 - 135
mobile/openapi/lib/model/get_asset_by_time_bucket_dto.dart

@@ -1,135 +0,0 @@
-//
-// AUTO-GENERATED FILE, DO NOT MODIFY!
-//
-// @dart=2.12
-
-// ignore_for_file: unused_element, unused_import
-// ignore_for_file: always_put_required_named_parameters_first
-// ignore_for_file: constant_identifier_names
-// ignore_for_file: lines_longer_than_80_chars
-
-part of openapi.api;
-
-class GetAssetByTimeBucketDto {
-  /// Returns a new [GetAssetByTimeBucketDto] instance.
-  GetAssetByTimeBucketDto({
-    this.timeBucket = const [],
-    this.userId,
-    this.withoutThumbs,
-  });
-
-  List<String> timeBucket;
-
-  ///
-  /// Please note: This property should have been non-nullable! Since the specification file
-  /// does not include a default value (using the "default:" property), however, the generated
-  /// source code must fall back to having a nullable type.
-  /// Consider adding a "default:" property in the specification file to hide this note.
-  ///
-  String? userId;
-
-  /// Include assets without thumbnails
-  ///
-  /// Please note: This property should have been non-nullable! Since the specification file
-  /// does not include a default value (using the "default:" property), however, the generated
-  /// source code must fall back to having a nullable type.
-  /// Consider adding a "default:" property in the specification file to hide this note.
-  ///
-  bool? withoutThumbs;
-
-  @override
-  bool operator ==(Object other) => identical(this, other) || other is GetAssetByTimeBucketDto &&
-     other.timeBucket == timeBucket &&
-     other.userId == userId &&
-     other.withoutThumbs == withoutThumbs;
-
-  @override
-  int get hashCode =>
-    // ignore: unnecessary_parenthesis
-    (timeBucket.hashCode) +
-    (userId == null ? 0 : userId!.hashCode) +
-    (withoutThumbs == null ? 0 : withoutThumbs!.hashCode);
-
-  @override
-  String toString() => 'GetAssetByTimeBucketDto[timeBucket=$timeBucket, userId=$userId, withoutThumbs=$withoutThumbs]';
-
-  Map<String, dynamic> toJson() {
-    final json = <String, dynamic>{};
-      json[r'timeBucket'] = this.timeBucket;
-    if (this.userId != null) {
-      json[r'userId'] = this.userId;
-    } else {
-    //  json[r'userId'] = null;
-    }
-    if (this.withoutThumbs != null) {
-      json[r'withoutThumbs'] = this.withoutThumbs;
-    } else {
-    //  json[r'withoutThumbs'] = null;
-    }
-    return json;
-  }
-
-  /// Returns a new [GetAssetByTimeBucketDto] instance and imports its values from
-  /// [value] if it's a [Map], null otherwise.
-  // ignore: prefer_constructors_over_static_methods
-  static GetAssetByTimeBucketDto? fromJson(dynamic value) {
-    if (value is Map) {
-      final json = value.cast<String, dynamic>();
-
-      return GetAssetByTimeBucketDto(
-        timeBucket: json[r'timeBucket'] is Iterable
-            ? (json[r'timeBucket'] as Iterable).cast<String>().toList(growable: false)
-            : const [],
-        userId: mapValueOfType<String>(json, r'userId'),
-        withoutThumbs: mapValueOfType<bool>(json, r'withoutThumbs'),
-      );
-    }
-    return null;
-  }
-
-  static List<GetAssetByTimeBucketDto> listFromJson(dynamic json, {bool growable = false,}) {
-    final result = <GetAssetByTimeBucketDto>[];
-    if (json is List && json.isNotEmpty) {
-      for (final row in json) {
-        final value = GetAssetByTimeBucketDto.fromJson(row);
-        if (value != null) {
-          result.add(value);
-        }
-      }
-    }
-    return result.toList(growable: growable);
-  }
-
-  static Map<String, GetAssetByTimeBucketDto> mapFromJson(dynamic json) {
-    final map = <String, GetAssetByTimeBucketDto>{};
-    if (json is Map && json.isNotEmpty) {
-      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
-      for (final entry in json.entries) {
-        final value = GetAssetByTimeBucketDto.fromJson(entry.value);
-        if (value != null) {
-          map[entry.key] = value;
-        }
-      }
-    }
-    return map;
-  }
-
-  // maps a json object with a list of GetAssetByTimeBucketDto-objects as value to a dart map
-  static Map<String, List<GetAssetByTimeBucketDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
-    final map = <String, List<GetAssetByTimeBucketDto>>{};
-    if (json is Map && json.isNotEmpty) {
-      // ignore: parameter_assignments
-      json = json.cast<String, dynamic>();
-      for (final entry in json.entries) {
-        map[entry.key] = GetAssetByTimeBucketDto.listFromJson(entry.value, growable: growable,);
-      }
-    }
-    return map;
-  }
-
-  /// The list of required keys that must be present in a JSON.
-  static const requiredKeys = <String>{
-    'timeBucket',
-  };
-}
-

+ 0 - 133
mobile/openapi/lib/model/get_asset_count_by_time_bucket_dto.dart

@@ -1,133 +0,0 @@
-//
-// AUTO-GENERATED FILE, DO NOT MODIFY!
-//
-// @dart=2.12
-
-// ignore_for_file: unused_element, unused_import
-// ignore_for_file: always_put_required_named_parameters_first
-// ignore_for_file: constant_identifier_names
-// ignore_for_file: lines_longer_than_80_chars
-
-part of openapi.api;
-
-class GetAssetCountByTimeBucketDto {
-  /// Returns a new [GetAssetCountByTimeBucketDto] instance.
-  GetAssetCountByTimeBucketDto({
-    required this.timeGroup,
-    this.userId,
-    this.withoutThumbs,
-  });
-
-  TimeGroupEnum timeGroup;
-
-  ///
-  /// Please note: This property should have been non-nullable! Since the specification file
-  /// does not include a default value (using the "default:" property), however, the generated
-  /// source code must fall back to having a nullable type.
-  /// Consider adding a "default:" property in the specification file to hide this note.
-  ///
-  String? userId;
-
-  /// Include assets without thumbnails
-  ///
-  /// Please note: This property should have been non-nullable! Since the specification file
-  /// does not include a default value (using the "default:" property), however, the generated
-  /// source code must fall back to having a nullable type.
-  /// Consider adding a "default:" property in the specification file to hide this note.
-  ///
-  bool? withoutThumbs;
-
-  @override
-  bool operator ==(Object other) => identical(this, other) || other is GetAssetCountByTimeBucketDto &&
-     other.timeGroup == timeGroup &&
-     other.userId == userId &&
-     other.withoutThumbs == withoutThumbs;
-
-  @override
-  int get hashCode =>
-    // ignore: unnecessary_parenthesis
-    (timeGroup.hashCode) +
-    (userId == null ? 0 : userId!.hashCode) +
-    (withoutThumbs == null ? 0 : withoutThumbs!.hashCode);
-
-  @override
-  String toString() => 'GetAssetCountByTimeBucketDto[timeGroup=$timeGroup, userId=$userId, withoutThumbs=$withoutThumbs]';
-
-  Map<String, dynamic> toJson() {
-    final json = <String, dynamic>{};
-      json[r'timeGroup'] = this.timeGroup;
-    if (this.userId != null) {
-      json[r'userId'] = this.userId;
-    } else {
-    //  json[r'userId'] = null;
-    }
-    if (this.withoutThumbs != null) {
-      json[r'withoutThumbs'] = this.withoutThumbs;
-    } else {
-    //  json[r'withoutThumbs'] = null;
-    }
-    return json;
-  }
-
-  /// Returns a new [GetAssetCountByTimeBucketDto] instance and imports its values from
-  /// [value] if it's a [Map], null otherwise.
-  // ignore: prefer_constructors_over_static_methods
-  static GetAssetCountByTimeBucketDto? fromJson(dynamic value) {
-    if (value is Map) {
-      final json = value.cast<String, dynamic>();
-
-      return GetAssetCountByTimeBucketDto(
-        timeGroup: TimeGroupEnum.fromJson(json[r'timeGroup'])!,
-        userId: mapValueOfType<String>(json, r'userId'),
-        withoutThumbs: mapValueOfType<bool>(json, r'withoutThumbs'),
-      );
-    }
-    return null;
-  }
-
-  static List<GetAssetCountByTimeBucketDto> listFromJson(dynamic json, {bool growable = false,}) {
-    final result = <GetAssetCountByTimeBucketDto>[];
-    if (json is List && json.isNotEmpty) {
-      for (final row in json) {
-        final value = GetAssetCountByTimeBucketDto.fromJson(row);
-        if (value != null) {
-          result.add(value);
-        }
-      }
-    }
-    return result.toList(growable: growable);
-  }
-
-  static Map<String, GetAssetCountByTimeBucketDto> mapFromJson(dynamic json) {
-    final map = <String, GetAssetCountByTimeBucketDto>{};
-    if (json is Map && json.isNotEmpty) {
-      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
-      for (final entry in json.entries) {
-        final value = GetAssetCountByTimeBucketDto.fromJson(entry.value);
-        if (value != null) {
-          map[entry.key] = value;
-        }
-      }
-    }
-    return map;
-  }
-
-  // maps a json object with a list of GetAssetCountByTimeBucketDto-objects as value to a dart map
-  static Map<String, List<GetAssetCountByTimeBucketDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
-    final map = <String, List<GetAssetCountByTimeBucketDto>>{};
-    if (json is Map && json.isNotEmpty) {
-      // ignore: parameter_assignments
-      json = json.cast<String, dynamic>();
-      for (final entry in json.entries) {
-        map[entry.key] = GetAssetCountByTimeBucketDto.listFromJson(entry.value, growable: growable,);
-      }
-    }
-    return map;
-  }
-
-  /// The list of required keys that must be present in a JSON.
-  static const requiredKeys = <String>{
-    'timeGroup',
-  };
-}
-

+ 18 - 18
mobile/openapi/lib/model/asset_count_by_time_bucket.dart → mobile/openapi/lib/model/time_bucket_response_dto.dart

@@ -10,9 +10,9 @@
 
 part of openapi.api;
 
-class AssetCountByTimeBucket {
-  /// Returns a new [AssetCountByTimeBucket] instance.
-  AssetCountByTimeBucket({
+class TimeBucketResponseDto {
+  /// Returns a new [TimeBucketResponseDto] instance.
+  TimeBucketResponseDto({
     required this.count,
     required this.timeBucket,
   });
@@ -22,7 +22,7 @@ class AssetCountByTimeBucket {
   String timeBucket;
 
   @override
-  bool operator ==(Object other) => identical(this, other) || other is AssetCountByTimeBucket &&
+  bool operator ==(Object other) => identical(this, other) || other is TimeBucketResponseDto &&
      other.count == count &&
      other.timeBucket == timeBucket;
 
@@ -33,7 +33,7 @@ class AssetCountByTimeBucket {
     (timeBucket.hashCode);
 
   @override
-  String toString() => 'AssetCountByTimeBucket[count=$count, timeBucket=$timeBucket]';
+  String toString() => 'TimeBucketResponseDto[count=$count, timeBucket=$timeBucket]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
@@ -42,14 +42,14 @@ class AssetCountByTimeBucket {
     return json;
   }
 
-  /// Returns a new [AssetCountByTimeBucket] instance and imports its values from
+  /// Returns a new [TimeBucketResponseDto] instance and imports its values from
   /// [value] if it's a [Map], null otherwise.
   // ignore: prefer_constructors_over_static_methods
-  static AssetCountByTimeBucket? fromJson(dynamic value) {
+  static TimeBucketResponseDto? fromJson(dynamic value) {
     if (value is Map) {
       final json = value.cast<String, dynamic>();
 
-      return AssetCountByTimeBucket(
+      return TimeBucketResponseDto(
         count: mapValueOfType<int>(json, r'count')!,
         timeBucket: mapValueOfType<String>(json, r'timeBucket')!,
       );
@@ -57,11 +57,11 @@ class AssetCountByTimeBucket {
     return null;
   }
 
-  static List<AssetCountByTimeBucket> listFromJson(dynamic json, {bool growable = false,}) {
-    final result = <AssetCountByTimeBucket>[];
+  static List<TimeBucketResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <TimeBucketResponseDto>[];
     if (json is List && json.isNotEmpty) {
       for (final row in json) {
-        final value = AssetCountByTimeBucket.fromJson(row);
+        final value = TimeBucketResponseDto.fromJson(row);
         if (value != null) {
           result.add(value);
         }
@@ -70,12 +70,12 @@ class AssetCountByTimeBucket {
     return result.toList(growable: growable);
   }
 
-  static Map<String, AssetCountByTimeBucket> mapFromJson(dynamic json) {
-    final map = <String, AssetCountByTimeBucket>{};
+  static Map<String, TimeBucketResponseDto> mapFromJson(dynamic json) {
+    final map = <String, TimeBucketResponseDto>{};
     if (json is Map && json.isNotEmpty) {
       json = json.cast<String, dynamic>(); // ignore: parameter_assignments
       for (final entry in json.entries) {
-        final value = AssetCountByTimeBucket.fromJson(entry.value);
+        final value = TimeBucketResponseDto.fromJson(entry.value);
         if (value != null) {
           map[entry.key] = value;
         }
@@ -84,14 +84,14 @@ class AssetCountByTimeBucket {
     return map;
   }
 
-  // maps a json object with a list of AssetCountByTimeBucket-objects as value to a dart map
-  static Map<String, List<AssetCountByTimeBucket>> mapListFromJson(dynamic json, {bool growable = false,}) {
-    final map = <String, List<AssetCountByTimeBucket>>{};
+  // maps a json object with a list of TimeBucketResponseDto-objects as value to a dart map
+  static Map<String, List<TimeBucketResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
+    final map = <String, List<TimeBucketResponseDto>>{};
     if (json is Map && json.isNotEmpty) {
       // ignore: parameter_assignments
       json = json.cast<String, dynamic>();
       for (final entry in json.entries) {
-        map[entry.key] = AssetCountByTimeBucket.listFromJson(entry.value, growable: growable,);
+        map[entry.key] = TimeBucketResponseDto.listFromJson(entry.value, growable: growable,);
       }
     }
     return map;

+ 24 - 24
mobile/openapi/lib/model/time_group_enum.dart → mobile/openapi/lib/model/time_bucket_size.dart

@@ -11,9 +11,9 @@
 part of openapi.api;
 
 
-class TimeGroupEnum {
+class TimeBucketSize {
   /// Instantiate a new enum with the provided [value].
-  const TimeGroupEnum._(this.value);
+  const TimeBucketSize._(this.value);
 
   /// The underlying value of this enum member.
   final String value;
@@ -23,22 +23,22 @@ class TimeGroupEnum {
 
   String toJson() => value;
 
-  static const day = TimeGroupEnum._(r'day');
-  static const month = TimeGroupEnum._(r'month');
+  static const DAY = TimeBucketSize._(r'DAY');
+  static const MONTH = TimeBucketSize._(r'MONTH');
 
-  /// List of all possible values in this [enum][TimeGroupEnum].
-  static const values = <TimeGroupEnum>[
-    day,
-    month,
+  /// List of all possible values in this [enum][TimeBucketSize].
+  static const values = <TimeBucketSize>[
+    DAY,
+    MONTH,
   ];
 
-  static TimeGroupEnum? fromJson(dynamic value) => TimeGroupEnumTypeTransformer().decode(value);
+  static TimeBucketSize? fromJson(dynamic value) => TimeBucketSizeTypeTransformer().decode(value);
 
-  static List<TimeGroupEnum>? listFromJson(dynamic json, {bool growable = false,}) {
-    final result = <TimeGroupEnum>[];
+  static List<TimeBucketSize>? listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <TimeBucketSize>[];
     if (json is List && json.isNotEmpty) {
       for (final row in json) {
-        final value = TimeGroupEnum.fromJson(row);
+        final value = TimeBucketSize.fromJson(row);
         if (value != null) {
           result.add(value);
         }
@@ -48,16 +48,16 @@ class TimeGroupEnum {
   }
 }
 
-/// Transformation class that can [encode] an instance of [TimeGroupEnum] to String,
-/// and [decode] dynamic data back to [TimeGroupEnum].
-class TimeGroupEnumTypeTransformer {
-  factory TimeGroupEnumTypeTransformer() => _instance ??= const TimeGroupEnumTypeTransformer._();
+/// Transformation class that can [encode] an instance of [TimeBucketSize] to String,
+/// and [decode] dynamic data back to [TimeBucketSize].
+class TimeBucketSizeTypeTransformer {
+  factory TimeBucketSizeTypeTransformer() => _instance ??= const TimeBucketSizeTypeTransformer._();
 
-  const TimeGroupEnumTypeTransformer._();
+  const TimeBucketSizeTypeTransformer._();
 
-  String encode(TimeGroupEnum data) => data.value;
+  String encode(TimeBucketSize data) => data.value;
 
-  /// Decodes a [dynamic value][data] to a TimeGroupEnum.
+  /// Decodes a [dynamic value][data] to a TimeBucketSize.
   ///
   /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
   /// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
@@ -65,11 +65,11 @@ class TimeGroupEnumTypeTransformer {
   ///
   /// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
   /// and users are still using an old app with the old code.
-  TimeGroupEnum? decode(dynamic data, {bool allowNull = true}) {
+  TimeBucketSize? decode(dynamic data, {bool allowNull = true}) {
     if (data != null) {
       switch (data) {
-        case r'day': return TimeGroupEnum.day;
-        case r'month': return TimeGroupEnum.month;
+        case r'DAY': return TimeBucketSize.DAY;
+        case r'MONTH': return TimeBucketSize.MONTH;
         default:
           if (!allowNull) {
             throw ArgumentError('Unknown enum value to decode: $data');
@@ -79,7 +79,7 @@ class TimeGroupEnumTypeTransformer {
     return null;
   }
 
-  /// Singleton [TimeGroupEnumTypeTransformer] instance.
-  static TimeGroupEnumTypeTransformer? _instance;
+  /// Singleton [TimeBucketSizeTypeTransformer] instance.
+  static TimeBucketSizeTypeTransformer? _instance;
 }
 

+ 10 - 10
mobile/openapi/test/asset_api_test.dart

@@ -67,16 +67,6 @@ void main() {
       // TODO
     });
 
-    //Future<List<AssetResponseDto>> getAssetByTimeBucket(GetAssetByTimeBucketDto getAssetByTimeBucketDto) async
-    test('test getAssetByTimeBucket', () async {
-      // TODO
-    });
-
-    //Future<AssetCountByTimeBucketResponseDto> getAssetCountByTimeBucket(GetAssetCountByTimeBucketDto getAssetCountByTimeBucketDto) async
-    test('test getAssetCountByTimeBucket', () async {
-      // TODO
-    });
-
     //Future<List<String>> getAssetSearchTerms() async
     test('test getAssetSearchTerms', () async {
       // TODO
@@ -92,6 +82,11 @@ void main() {
       // TODO
     });
 
+    //Future<List<AssetResponseDto>> getByTimeBucket(TimeBucketSize size, String timeBucket, { String userId, String albumId, bool isArchived, bool isFavorite, String key }) async
+    test('test getByTimeBucket', () async {
+      // TODO
+    });
+
     //Future<List<CuratedLocationsResponseDto>> getCuratedLocations() async
     test('test getCuratedLocations', () async {
       // TODO
@@ -117,6 +112,11 @@ void main() {
       // TODO
     });
 
+    //Future<List<TimeBucketResponseDto>> getTimeBuckets(TimeBucketSize size, { String userId, String albumId, bool isArchived, bool isFavorite, String key }) async
+    test('test getTimeBuckets', () async {
+      // TODO
+    });
+
     // Get all asset of a device that are in the database, ID only.
     //
     //Future<List<String>> getUserAssetsByDeviceId(String deviceId) async

+ 0 - 32
mobile/openapi/test/asset_count_by_time_bucket_response_dto_test.dart

@@ -1,32 +0,0 @@
-//
-// AUTO-GENERATED FILE, DO NOT MODIFY!
-//
-// @dart=2.12
-
-// ignore_for_file: unused_element, unused_import
-// ignore_for_file: always_put_required_named_parameters_first
-// ignore_for_file: constant_identifier_names
-// ignore_for_file: lines_longer_than_80_chars
-
-import 'package:openapi/api.dart';
-import 'package:test/test.dart';
-
-// tests for AssetCountByTimeBucketResponseDto
-void main() {
-  // final instance = AssetCountByTimeBucketResponseDto();
-
-  group('test AssetCountByTimeBucketResponseDto', () {
-    // List<AssetCountByTimeBucket> buckets (default value: const [])
-    test('to test the property `buckets`', () async {
-      // TODO
-    });
-
-    // int totalCount
-    test('to test the property `totalCount`', () async {
-      // TODO
-    });
-
-
-  });
-
-}

+ 0 - 38
mobile/openapi/test/get_asset_by_time_bucket_dto_test.dart

@@ -1,38 +0,0 @@
-//
-// AUTO-GENERATED FILE, DO NOT MODIFY!
-//
-// @dart=2.12
-
-// ignore_for_file: unused_element, unused_import
-// ignore_for_file: always_put_required_named_parameters_first
-// ignore_for_file: constant_identifier_names
-// ignore_for_file: lines_longer_than_80_chars
-
-import 'package:openapi/api.dart';
-import 'package:test/test.dart';
-
-// tests for GetAssetByTimeBucketDto
-void main() {
-  // final instance = GetAssetByTimeBucketDto();
-
-  group('test GetAssetByTimeBucketDto', () {
-    // List<String> timeBucket (default value: const [])
-    test('to test the property `timeBucket`', () async {
-      // TODO
-    });
-
-    // String userId
-    test('to test the property `userId`', () async {
-      // TODO
-    });
-
-    // Include assets without thumbnails
-    // bool withoutThumbs
-    test('to test the property `withoutThumbs`', () async {
-      // TODO
-    });
-
-
-  });
-
-}

+ 0 - 38
mobile/openapi/test/get_asset_count_by_time_bucket_dto_test.dart

@@ -1,38 +0,0 @@
-//
-// AUTO-GENERATED FILE, DO NOT MODIFY!
-//
-// @dart=2.12
-
-// ignore_for_file: unused_element, unused_import
-// ignore_for_file: always_put_required_named_parameters_first
-// ignore_for_file: constant_identifier_names
-// ignore_for_file: lines_longer_than_80_chars
-
-import 'package:openapi/api.dart';
-import 'package:test/test.dart';
-
-// tests for GetAssetCountByTimeBucketDto
-void main() {
-  // final instance = GetAssetCountByTimeBucketDto();
-
-  group('test GetAssetCountByTimeBucketDto', () {
-    // TimeGroupEnum timeGroup
-    test('to test the property `timeGroup`', () async {
-      // TODO
-    });
-
-    // String userId
-    test('to test the property `userId`', () async {
-      // TODO
-    });
-
-    // Include assets without thumbnails
-    // bool withoutThumbs
-    test('to test the property `withoutThumbs`', () async {
-      // TODO
-    });
-
-
-  });
-
-}

+ 3 - 3
mobile/openapi/test/asset_count_by_time_bucket_test.dart → mobile/openapi/test/time_bucket_response_dto_test.dart

@@ -11,11 +11,11 @@
 import 'package:openapi/api.dart';
 import 'package:test/test.dart';
 
-// tests for AssetCountByTimeBucket
+// tests for TimeBucketResponseDto
 void main() {
-  // final instance = AssetCountByTimeBucket();
+  // final instance = TimeBucketResponseDto();
 
-  group('test AssetCountByTimeBucket', () {
+  group('test TimeBucketResponseDto', () {
     // int count
     test('to test the property `count`', () async {
       // TODO

+ 2 - 2
mobile/openapi/test/time_group_enum_test.dart → mobile/openapi/test/time_bucket_size_test.dart

@@ -11,10 +11,10 @@
 import 'package:openapi/api.dart';
 import 'package:test/test.dart';
 
-// tests for TimeGroupEnum
+// tests for TimeBucketSize
 void main() {
 
-  group('test TimeGroupEnum', () {
+  group('test TimeBucketSize', () {
 
   });
 

+ 182 - 136
server/immich-openapi-specs.json

@@ -948,48 +948,6 @@
         ]
       }
     },
-    "/asset/count-by-time-bucket": {
-      "post": {
-        "operationId": "getAssetCountByTimeBucket",
-        "parameters": [],
-        "requestBody": {
-          "content": {
-            "application/json": {
-              "schema": {
-                "$ref": "#/components/schemas/GetAssetCountByTimeBucketDto"
-              }
-            }
-          },
-          "required": true
-        },
-        "responses": {
-          "200": {
-            "content": {
-              "application/json": {
-                "schema": {
-                  "$ref": "#/components/schemas/AssetCountByTimeBucketResponseDto"
-                }
-              }
-            },
-            "description": ""
-          }
-        },
-        "security": [
-          {
-            "bearer": []
-          },
-          {
-            "cookie": []
-          },
-          {
-            "api_key": []
-          }
-        ],
-        "tags": [
-          "Asset"
-        ]
-      }
-    },
     "/asset/curated-locations": {
       "get": {
         "operationId": "getCuratedLocations",
@@ -1697,19 +1655,68 @@
       }
     },
     "/asset/time-bucket": {
-      "post": {
-        "operationId": "getAssetByTimeBucket",
-        "parameters": [],
-        "requestBody": {
-          "content": {
-            "application/json": {
-              "schema": {
-                "$ref": "#/components/schemas/GetAssetByTimeBucketDto"
-              }
+      "get": {
+        "operationId": "getByTimeBucket",
+        "parameters": [
+          {
+            "name": "size",
+            "required": true,
+            "in": "query",
+            "schema": {
+              "$ref": "#/components/schemas/TimeBucketSize"
             }
           },
-          "required": true
-        },
+          {
+            "name": "userId",
+            "required": false,
+            "in": "query",
+            "schema": {
+              "format": "uuid",
+              "type": "string"
+            }
+          },
+          {
+            "name": "albumId",
+            "required": false,
+            "in": "query",
+            "schema": {
+              "format": "uuid",
+              "type": "string"
+            }
+          },
+          {
+            "name": "isArchived",
+            "required": false,
+            "in": "query",
+            "schema": {
+              "type": "boolean"
+            }
+          },
+          {
+            "name": "isFavorite",
+            "required": false,
+            "in": "query",
+            "schema": {
+              "type": "boolean"
+            }
+          },
+          {
+            "name": "timeBucket",
+            "required": true,
+            "in": "query",
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "key",
+            "required": false,
+            "in": "query",
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
         "responses": {
           "200": {
             "content": {
@@ -1726,6 +1733,110 @@
           }
         },
         "security": [
+          {
+            "bearer": []
+          },
+          {
+            "cookie": []
+          },
+          {
+            "api_key": []
+          },
+          {
+            "bearer": []
+          },
+          {
+            "cookie": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "tags": [
+          "Asset"
+        ]
+      }
+    },
+    "/asset/time-buckets": {
+      "get": {
+        "operationId": "getTimeBuckets",
+        "parameters": [
+          {
+            "name": "size",
+            "required": true,
+            "in": "query",
+            "schema": {
+              "$ref": "#/components/schemas/TimeBucketSize"
+            }
+          },
+          {
+            "name": "userId",
+            "required": false,
+            "in": "query",
+            "schema": {
+              "format": "uuid",
+              "type": "string"
+            }
+          },
+          {
+            "name": "albumId",
+            "required": false,
+            "in": "query",
+            "schema": {
+              "format": "uuid",
+              "type": "string"
+            }
+          },
+          {
+            "name": "isArchived",
+            "required": false,
+            "in": "query",
+            "schema": {
+              "type": "boolean"
+            }
+          },
+          {
+            "name": "isFavorite",
+            "required": false,
+            "in": "query",
+            "schema": {
+              "type": "boolean"
+            }
+          },
+          {
+            "name": "key",
+            "required": false,
+            "in": "query",
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "items": {
+                    "$ref": "#/components/schemas/TimeBucketResponseDto"
+                  },
+                  "type": "array"
+                }
+              }
+            },
+            "description": ""
+          }
+        },
+        "security": [
+          {
+            "bearer": []
+          },
+          {
+            "cookie": []
+          },
+          {
+            "api_key": []
+          },
           {
             "bearer": []
           },
@@ -4787,39 +4898,6 @@
         ],
         "type": "object"
       },
-      "AssetCountByTimeBucket": {
-        "properties": {
-          "count": {
-            "type": "integer"
-          },
-          "timeBucket": {
-            "type": "string"
-          }
-        },
-        "required": [
-          "timeBucket",
-          "count"
-        ],
-        "type": "object"
-      },
-      "AssetCountByTimeBucketResponseDto": {
-        "properties": {
-          "buckets": {
-            "items": {
-              "$ref": "#/components/schemas/AssetCountByTimeBucket"
-            },
-            "type": "array"
-          },
-          "totalCount": {
-            "type": "integer"
-          }
-        },
-        "required": [
-          "totalCount",
-          "buckets"
-        ],
-        "type": "object"
-      },
       "AssetFileUploadResponseDto": {
         "properties": {
           "duplicate": {
@@ -5554,53 +5632,6 @@
         },
         "type": "object"
       },
-      "GetAssetByTimeBucketDto": {
-        "properties": {
-          "timeBucket": {
-            "example": [
-              "2015-06-01T00:00:00.000Z",
-              "2016-02-01T00:00:00.000Z",
-              "2016-03-01T00:00:00.000Z"
-            ],
-            "items": {
-              "type": "string"
-            },
-            "title": "Array of date time buckets",
-            "type": "array"
-          },
-          "userId": {
-            "format": "uuid",
-            "type": "string"
-          },
-          "withoutThumbs": {
-            "description": "Include assets without thumbnails",
-            "type": "boolean"
-          }
-        },
-        "required": [
-          "timeBucket"
-        ],
-        "type": "object"
-      },
-      "GetAssetCountByTimeBucketDto": {
-        "properties": {
-          "timeGroup": {
-            "$ref": "#/components/schemas/TimeGroupEnum"
-          },
-          "userId": {
-            "format": "uuid",
-            "type": "string"
-          },
-          "withoutThumbs": {
-            "description": "Include assets without thumbnails",
-            "type": "boolean"
-          }
-        },
-        "required": [
-          "timeGroup"
-        ],
-        "type": "object"
-      },
       "ImportAssetDto": {
         "properties": {
           "assetPath": {
@@ -6806,10 +6837,25 @@
         ],
         "type": "string"
       },
-      "TimeGroupEnum": {
+      "TimeBucketResponseDto": {
+        "properties": {
+          "count": {
+            "type": "integer"
+          },
+          "timeBucket": {
+            "type": "string"
+          }
+        },
+        "required": [
+          "timeBucket",
+          "count"
+        ],
+        "type": "object"
+      },
+      "TimeBucketSize": {
         "enum": [
-          "day",
-          "month"
+          "DAY",
+          "MONTH"
         ],
         "type": "string"
       },

+ 19 - 0
server/src/domain/asset/asset.repository.ts

@@ -47,6 +47,23 @@ export enum WithProperty {
   SIDECAR = 'sidecar',
 }
 
+export enum TimeBucketSize {
+  DAY = 'DAY',
+  MONTH = 'MONTH',
+}
+
+export interface TimeBucketOptions {
+  size: TimeBucketSize;
+  isArchived?: boolean;
+  isFavorite?: boolean;
+  albumId?: string;
+}
+
+export interface TimeBucketItem {
+  timeBucket: string;
+  count: number;
+}
+
 export const IAssetRepository = 'IAssetRepository';
 
 export interface IAssetRepository {
@@ -64,4 +81,6 @@ export interface IAssetRepository {
   findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | null>;
   getMapMarkers(ownerId: string, options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
   getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>;
+  getTimeBuckets(userId: string, options: TimeBucketOptions): Promise<TimeBucketItem[]>;
+  getByTimeBucket(userId: string, timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]>;
 }

+ 26 - 2
server/src/domain/asset/asset.service.ts

@@ -10,11 +10,20 @@ import { mimeTypes } from '../domain.constant';
 import { HumanReadableSize, usePagination } from '../domain.util';
 import { ImmichReadStream, IStorageRepository, StorageCore, StorageFolder } from '../storage';
 import { IAssetRepository } from './asset.repository';
-import { AssetIdsDto, DownloadArchiveInfo, DownloadDto, DownloadResponseDto, MemoryLaneDto } from './dto';
+import {
+  AssetIdsDto,
+  DownloadArchiveInfo,
+  DownloadDto,
+  DownloadResponseDto,
+  MemoryLaneDto,
+  TimeBucketAssetDto,
+  TimeBucketDto,
+} from './dto';
 import { AssetStatsDto, mapStats } from './dto/asset-statistics.dto';
 import { MapMarkerDto } from './dto/map-marker.dto';
-import { mapAsset, MapMarkerResponseDto } from './response-dto';
+import { AssetResponseDto, mapAsset, MapMarkerResponseDto } from './response-dto';
 import { MemoryLaneResponseDto } from './response-dto/memory-lane-response.dto';
+import { TimeBucketResponseDto } from './response-dto/time-bucket-response.dto';
 
 export enum UploadFieldName {
   ASSET_DATA = 'assetData',
@@ -135,6 +144,21 @@ export class AssetService {
     return Promise.all(requests).then((results) => results.filter((result) => result.assets.length > 0));
   }
 
+  async getTimeBuckets(authUser: AuthUserDto, dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
+    const { userId, ...options } = dto;
+    const targetId = userId || authUser.id;
+    await this.access.requirePermission(authUser, Permission.LIBRARY_READ, [targetId]);
+    return this.assetRepository.getTimeBuckets(targetId, options);
+  }
+
+  async getByTimeBucket(authUser: AuthUserDto, dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> {
+    const { userId, timeBucket, ...options } = dto;
+    const targetId = userId || authUser.id;
+    await this.access.requirePermission(authUser, Permission.LIBRARY_READ, [targetId]);
+    const assets = await this.assetRepository.getByTimeBucket(targetId, timeBucket, options);
+    return assets.map(mapAsset);
+  }
+
   async downloadFile(authUser: AuthUserDto, id: string): Promise<ImmichReadStream> {
     await this.access.requirePermission(authUser, Permission.ASSET_DOWNLOAD, id);
 

+ 1 - 0
server/src/domain/asset/dto/index.ts

@@ -3,3 +3,4 @@ export * from './asset-statistics.dto';
 export * from './download.dto';
 export * from './map-marker.dto';
 export * from './memory-lane.dto';
+export * from './time-bucket.dto';

+ 34 - 0
server/src/domain/asset/dto/time-bucket.dto.ts

@@ -0,0 +1,34 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { Transform } from 'class-transformer';
+import { IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
+import { toBoolean, ValidateUUID } from '../../domain.util';
+import { TimeBucketSize } from '../asset.repository';
+
+export class TimeBucketDto {
+  @IsNotEmpty()
+  @IsEnum(TimeBucketSize)
+  @ApiProperty({ enum: TimeBucketSize, enumName: 'TimeBucketSize' })
+  size!: TimeBucketSize;
+
+  @ValidateUUID({ optional: true })
+  userId?: string;
+
+  @ValidateUUID({ optional: true })
+  albumId?: string;
+
+  @IsOptional()
+  @IsBoolean()
+  @Transform(toBoolean)
+  isArchived?: boolean;
+
+  @IsOptional()
+  @IsBoolean()
+  @Transform(toBoolean)
+  isFavorite?: boolean;
+}
+
+export class TimeBucketAssetDto extends TimeBucketDto {
+  @IsString()
+  @IsNotEmpty()
+  timeBucket!: string;
+}

+ 1 - 0
server/src/domain/asset/response-dto/index.ts

@@ -3,3 +3,4 @@ export * from './asset-response.dto';
 export * from './exif-response.dto';
 export * from './map-marker-response.dto';
 export * from './smart-info-response.dto';
+export * from './time-bucket-response.dto';

+ 9 - 0
server/src/domain/asset/response-dto/time-bucket-response.dto.ts

@@ -0,0 +1,9 @@
+import { ApiProperty } from '@nestjs/swagger';
+
+export class TimeBucketResponseDto {
+  @ApiProperty({ type: 'string' })
+  timeBucket!: string;
+
+  @ApiProperty({ type: 'integer' })
+  count!: number;
+}

+ 0 - 56
server/src/immich/api-v1/asset/asset-repository.ts

@@ -6,11 +6,8 @@ import { In } from 'typeorm/find-options/operator/In';
 import { Repository } from 'typeorm/repository/Repository';
 import { AssetSearchDto } from './dto/asset-search.dto';
 import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
-import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
-import { GetAssetCountByTimeBucketDto, TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
 import { SearchPropertiesDto } from './dto/search-properties.dto';
 import { UpdateAssetDto } from './dto/update-asset.dto';
-import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group-response.dto';
 import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
 import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
 
@@ -36,8 +33,6 @@ export interface IAssetRepository {
   getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]>;
   getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>;
   getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]>;
-  getAssetCountByTimeBucket(userId: string, dto: GetAssetCountByTimeBucketDto): Promise<AssetCountByTimeBucket[]>;
-  getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]>;
   getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise<AssetCheck[]>;
   getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]>;
   getByOriginalPath(originalPath: string): Promise<AssetOwnerCheck | null>;
@@ -52,57 +47,6 @@ export class AssetRepository implements IAssetRepository {
     @InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
   ) {}
 
-  async getAssetByTimeBucket(userId: string, dto: GetAssetByTimeBucketDto): Promise<AssetEntity[]> {
-    // Get asset entity from a list of time buckets
-    let builder = this.assetRepository
-      .createQueryBuilder('asset')
-      .leftJoinAndSelect('asset.exifInfo', 'exifInfo')
-      .where('asset.ownerId = :userId', { userId: userId })
-      .andWhere(`date_trunc('month', "fileCreatedAt") IN (:...buckets)`, {
-        buckets: [...dto.timeBucket],
-      })
-      .andWhere('asset.isVisible = true')
-      .andWhere('asset.isArchived = false')
-      .orderBy('asset.fileCreatedAt', 'DESC');
-
-    if (!dto.withoutThumbs) {
-      builder = builder.andWhere('asset.resizePath is not NULL');
-    }
-
-    return builder.getMany();
-  }
-
-  async getAssetCountByTimeBucket(
-    userId: string,
-    dto: GetAssetCountByTimeBucketDto,
-  ): Promise<AssetCountByTimeBucket[]> {
-    const builder = this.assetRepository
-      .createQueryBuilder('asset')
-      .select(`COUNT(asset.id)::int`, 'count')
-      .where('"ownerId" = :userId', { userId: userId })
-      .andWhere('asset.isVisible = true')
-      .andWhere('asset.isArchived = false');
-
-    // Using a parameter for this doesn't work https://github.com/typeorm/typeorm/issues/7308
-    if (dto.timeGroup === TimeGroupEnum.Month) {
-      builder
-        .addSelect(`date_trunc('month', "fileCreatedAt")`, 'timeBucket')
-        .groupBy(`date_trunc('month', "fileCreatedAt")`)
-        .orderBy(`date_trunc('month', "fileCreatedAt")`, 'DESC');
-    } else if (dto.timeGroup === TimeGroupEnum.Day) {
-      builder
-        .addSelect(`date_trunc('day', "fileCreatedAt")`, 'timeBucket')
-        .groupBy(`date_trunc('day', "fileCreatedAt")`)
-        .orderBy(`date_trunc('day', "fileCreatedAt")`, 'DESC');
-    }
-
-    if (!dto.withoutThumbs) {
-      builder.andWhere('asset.resizePath is not NULL');
-    }
-
-    return builder.getRawMany();
-  }
-
   getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]> {
     return this.assetRepository
       .createQueryBuilder('asset')

+ 0 - 21
server/src/immich/api-v1/asset/asset.controller.ts

@@ -30,14 +30,11 @@ import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
 import { CreateAssetDto, ImportAssetDto } from './dto/create-asset.dto';
 import { DeleteAssetDto } from './dto/delete-asset.dto';
 import { DeviceIdDto } from './dto/device-id.dto';
-import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
-import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto';
 import { GetAssetThumbnailDto } from './dto/get-asset-thumbnail.dto';
 import { SearchAssetDto } from './dto/search-asset.dto';
 import { ServeFileDto } from './dto/serve-file.dto';
 import { UpdateAssetDto } from './dto/update-asset.dto';
 import { AssetBulkUploadCheckResponseDto } from './response-dto/asset-check-response.dto';
-import { AssetCountByTimeBucketResponseDto } from './response-dto/asset-count-by-time-group-response.dto';
 import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
 import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
 import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
@@ -163,15 +160,6 @@ export class AssetController {
     return this.assetService.searchAsset(authUser, dto);
   }
 
-  @Post('/count-by-time-bucket')
-  @HttpCode(HttpStatus.OK)
-  getAssetCountByTimeBucket(
-    @AuthUser() authUser: AuthUserDto,
-    @Body(ValidationPipe) dto: GetAssetCountByTimeBucketDto,
-  ): Promise<AssetCountByTimeBucketResponseDto> {
-    return this.assetService.getAssetCountByTimeBucket(authUser, dto);
-  }
-
   /**
    * Get all AssetEntity belong to the user
    */
@@ -189,15 +177,6 @@ export class AssetController {
     return this.assetService.getAllAssets(authUser, dto);
   }
 
-  @Post('/time-bucket')
-  @HttpCode(HttpStatus.OK)
-  getAssetByTimeBucket(
-    @AuthUser() authUser: AuthUserDto,
-    @Body(ValidationPipe) dto: GetAssetByTimeBucketDto,
-  ): Promise<AssetResponseDto[]> {
-    return this.assetService.getAssetByTimeBucket(authUser, dto);
-  }
-
   /**
    * Get all asset of a device that are in the database, ID only.
    */

+ 0 - 31
server/src/immich/api-v1/asset/asset.service.spec.ts

@@ -16,9 +16,7 @@ import { QueryFailedError, Repository } from 'typeorm';
 import { IAssetRepository } from './asset-repository';
 import { AssetService } from './asset.service';
 import { CreateAssetDto } from './dto/create-asset.dto';
-import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
 import { AssetRejectReason, AssetUploadAction } from './response-dto/asset-check-response.dto';
-import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group-response.dto';
 
 const _getCreateAssetDto = (): CreateAssetDto => {
   const createAssetDto = new CreateAssetDto();
@@ -83,18 +81,6 @@ const _getAssets = () => {
   return [_getAsset_1(), _getAsset_2()];
 };
 
-const _getAssetCountByTimeBucket = (): AssetCountByTimeBucket[] => {
-  const result1 = new AssetCountByTimeBucket();
-  result1.count = 2;
-  result1.timeBucket = '2022-06-01T00:00:00.000Z';
-
-  const result2 = new AssetCountByTimeBucket();
-  result1.count = 5;
-  result1.timeBucket = '2022-07-01T00:00:00.000Z';
-
-  return [result1, result2];
-};
-
 describe('AssetService', () => {
   let sut: AssetService;
   let a: Repository<AssetEntity>; // TO BE DELETED AFTER FINISHED REFACTORING
@@ -113,12 +99,10 @@ describe('AssetService', () => {
       update: jest.fn(),
       getAllByUserId: jest.fn(),
       getAllByDeviceId: jest.fn(),
-      getAssetCountByTimeBucket: jest.fn(),
       getById: jest.fn(),
       getDetectedObjectsByUserId: jest.fn(),
       getLocationsByUserId: jest.fn(),
       getSearchPropertiesByUserId: jest.fn(),
-      getAssetByTimeBucket: jest.fn(),
       getAssetsByChecksums: jest.fn(),
       getExistingAssets: jest.fn(),
       getByOriginalPath: jest.fn(),
@@ -221,21 +205,6 @@ describe('AssetService', () => {
     expect(result).toEqual(assets.map((asset) => asset.deviceAssetId));
   });
 
-  it('get assets count by time bucket', async () => {
-    const assetCountByTimeBucket = _getAssetCountByTimeBucket();
-
-    assetRepositoryMock.getAssetCountByTimeBucket.mockImplementation(() =>
-      Promise.resolve<AssetCountByTimeBucket[]>(assetCountByTimeBucket),
-    );
-
-    const result = await sut.getAssetCountByTimeBucket(authStub.user1, {
-      timeGroup: TimeGroupEnum.Month,
-    });
-
-    expect(result.totalCount).toEqual(assetCountByTimeBucket.reduce((a, b) => a + b.count, 0));
-    expect(result.buckets.length).toEqual(2);
-  });
-
   describe('deleteAll', () => {
     it('should return failed status when an asset is missing', async () => {
       assetRepositoryMock.get.mockResolvedValue(null);

+ 0 - 23
server/src/immich/api-v1/asset/asset.service.ts

@@ -37,8 +37,6 @@ import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
 import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
 import { CreateAssetDto, ImportAssetDto } from './dto/create-asset.dto';
 import { DeleteAssetDto } from './dto/delete-asset.dto';
-import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
-import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto';
 import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto';
 import { SearchAssetDto } from './dto/search-asset.dto';
 import { SearchPropertiesDto } from './dto/search-properties.dto';
@@ -49,10 +47,6 @@ import {
   AssetRejectReason,
   AssetUploadAction,
 } from './response-dto/asset-check-response.dto';
-import {
-  AssetCountByTimeBucketResponseDto,
-  mapAssetCountByTimeBucket,
-} from './response-dto/asset-count-by-time-group-response.dto';
 import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
 import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
 import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
@@ -195,13 +189,6 @@ export class AssetService {
     return assets.map((asset) => mapAsset(asset));
   }
 
-  public async getAssetByTimeBucket(authUser: AuthUserDto, dto: GetAssetByTimeBucketDto): Promise<AssetResponseDto[]> {
-    const userId = dto.userId || authUser.id;
-    await this.access.requirePermission(authUser, Permission.LIBRARY_READ, userId);
-    const assets = await this._assetRepository.getAssetByTimeBucket(userId, dto);
-    return assets.map((asset) => mapAsset(asset));
-  }
-
   public async getAssetById(authUser: AuthUserDto, assetId: string): Promise<AssetResponseDto> {
     await this.access.requirePermission(authUser, Permission.ASSET_READ, assetId);
 
@@ -457,16 +444,6 @@ export class AssetService {
     };
   }
 
-  async getAssetCountByTimeBucket(
-    authUser: AuthUserDto,
-    dto: GetAssetCountByTimeBucketDto,
-  ): Promise<AssetCountByTimeBucketResponseDto> {
-    const userId = dto.userId || authUser.id;
-    await this.access.requirePermission(authUser, Permission.LIBRARY_READ, userId);
-    const result = await this._assetRepository.getAssetCountByTimeBucket(userId, dto);
-    return mapAssetCountByTimeBucket(result);
-  }
-
   getExifPermission(authUser: AuthUserDto) {
     return !authUser.isPublicUser || authUser.isShowExif;
   }

+ 0 - 28
server/src/immich/api-v1/asset/dto/get-asset-by-time-bucket.dto.ts

@@ -1,28 +0,0 @@
-import { toBoolean } from '@app/domain';
-import { ApiProperty } from '@nestjs/swagger';
-import { Transform } from 'class-transformer';
-import { IsBoolean, IsNotEmpty, IsOptional, IsUUID } from 'class-validator';
-
-export class GetAssetByTimeBucketDto {
-  @IsNotEmpty()
-  @ApiProperty({
-    isArray: true,
-    type: String,
-    title: 'Array of date time buckets',
-    example: ['2015-06-01T00:00:00.000Z', '2016-02-01T00:00:00.000Z', '2016-03-01T00:00:00.000Z'],
-  })
-  timeBucket!: string[];
-
-  @IsOptional()
-  @IsUUID('4')
-  @ApiProperty({ format: 'uuid' })
-  userId?: string;
-
-  /**
-   * Include assets without thumbnails
-   */
-  @IsOptional()
-  @IsBoolean()
-  @Transform(toBoolean)
-  withoutThumbs?: boolean;
-}

+ 0 - 32
server/src/immich/api-v1/asset/dto/get-asset-count-by-time-bucket.dto.ts

@@ -1,32 +0,0 @@
-import { toBoolean } from '@app/domain';
-import { ApiProperty } from '@nestjs/swagger';
-import { Transform } from 'class-transformer';
-import { IsBoolean, IsNotEmpty, IsOptional, IsUUID } from 'class-validator';
-
-export enum TimeGroupEnum {
-  Day = 'day',
-  Month = 'month',
-}
-
-export class GetAssetCountByTimeBucketDto {
-  @IsNotEmpty()
-  @ApiProperty({
-    type: String,
-    enum: TimeGroupEnum,
-    enumName: 'TimeGroupEnum',
-  })
-  timeGroup!: TimeGroupEnum;
-
-  @IsOptional()
-  @IsUUID('4')
-  @ApiProperty({ format: 'uuid' })
-  userId?: string;
-
-  /**
-   * Include assets without thumbnails
-   */
-  @IsOptional()
-  @IsBoolean()
-  @Transform(toBoolean)
-  withoutThumbs?: boolean;
-}

+ 0 - 23
server/src/immich/api-v1/asset/response-dto/asset-count-by-time-group-response.dto.ts

@@ -1,23 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-
-export class AssetCountByTimeBucket {
-  @ApiProperty({ type: 'string' })
-  timeBucket!: string;
-
-  @ApiProperty({ type: 'integer' })
-  count!: number;
-}
-
-export class AssetCountByTimeBucketResponseDto {
-  buckets!: AssetCountByTimeBucket[];
-
-  @ApiProperty({ type: 'integer' })
-  totalCount!: number;
-}
-
-export function mapAssetCountByTimeBucket(result: AssetCountByTimeBucket[]): AssetCountByTimeBucketResponseDto {
-  return {
-    buckets: result,
-    totalCount: result.map((group) => group.count).reduce((a, b) => a + b, 0),
-  };
-}

+ 16 - 0
server/src/immich/controllers/asset.controller.ts

@@ -1,5 +1,6 @@
 import {
   AssetIdsDto,
+  AssetResponseDto,
   AssetService,
   AssetStatsDto,
   AssetStatsResponseDto,
@@ -8,6 +9,9 @@ import {
   DownloadResponseDto,
   MapMarkerResponseDto,
   MemoryLaneDto,
+  TimeBucketAssetDto,
+  TimeBucketDto,
+  TimeBucketResponseDto,
 } from '@app/domain';
 import { MapMarkerDto } from '@app/domain/asset/dto/map-marker.dto';
 import { MemoryLaneResponseDto } from '@app/domain/asset/response-dto/memory-lane-response.dto';
@@ -60,4 +64,16 @@ export class AssetController {
   getAssetStats(@AuthUser() authUser: AuthUserDto, @Query() dto: AssetStatsDto): Promise<AssetStatsResponseDto> {
     return this.service.getStatistics(authUser, dto);
   }
+
+  @Authenticated({ isShared: true })
+  @Get('time-buckets')
+  getTimeBuckets(@AuthUser() authUser: AuthUserDto, @Query() dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
+    return this.service.getTimeBuckets(authUser, dto);
+  }
+
+  @Authenticated({ isShared: true })
+  @Get('time-bucket')
+  getByTimeBucket(@AuthUser() authUser: AuthUserDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> {
+    return this.service.getByTimeBucket(authUser, dto);
+  }
 }

+ 51 - 0
server/src/infra/repositories/asset.repository.ts

@@ -8,6 +8,9 @@ import {
   MapMarkerSearchOptions,
   Paginated,
   PaginationOptions,
+  TimeBucketItem,
+  TimeBucketOptions,
+  TimeBucketSize,
   WithoutProperty,
   WithProperty,
 } from '@app/domain';
@@ -19,6 +22,11 @@ import { AssetEntity, AssetType } from '../entities';
 import OptionalBetween from '../utils/optional-between.util';
 import { paginate } from '../utils/pagination.util';
 
+const truncateMap: Record<TimeBucketSize, string> = {
+  [TimeBucketSize.DAY]: 'day',
+  [TimeBucketSize.MONTH]: 'month',
+};
+
 @Injectable()
 export class AssetRepository implements IAssetRepository {
   constructor(@InjectRepository(AssetEntity) private repository: Repository<AssetEntity>) {}
@@ -357,4 +365,47 @@ export class AssetRepository implements IAssetRepository {
 
     return result;
   }
+
+  getTimeBuckets(userId: string, options: TimeBucketOptions): Promise<TimeBucketItem[]> {
+    const truncateValue = truncateMap[options.size];
+
+    return this.getBuilder(userId, options)
+      .select(`COUNT(asset.id)::int`, 'count')
+      .addSelect(`date_trunc('${truncateValue}', "fileCreatedAt")`, 'timeBucket')
+      .groupBy(`date_trunc('${truncateValue}', "fileCreatedAt")`)
+      .orderBy(`date_trunc('${truncateValue}', "fileCreatedAt")`, 'DESC')
+      .getRawMany();
+  }
+
+  getByTimeBucket(userId: string, timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]> {
+    const truncateValue = truncateMap[options.size];
+    return this.getBuilder(userId, options)
+      .andWhere(`date_trunc('${truncateValue}', "fileCreatedAt") = :timeBucket`, { timeBucket })
+      .orderBy('asset.fileCreatedAt', 'DESC')
+      .getMany();
+  }
+
+  private getBuilder(userId: string, options: TimeBucketOptions) {
+    const { isArchived, isFavorite, albumId } = options;
+
+    let builder = this.repository
+      .createQueryBuilder('asset')
+      .where('asset.ownerId = :userId', { userId })
+      .andWhere('asset.isVisible = true')
+      .leftJoinAndSelect('asset.exifInfo', 'exifInfo');
+
+    if (albumId) {
+      builder = builder.leftJoin('asset.albums', 'album').andWhere('album.id = :albumId', { albumId });
+    }
+
+    if (isArchived != undefined) {
+      builder = builder.andWhere('asset.isArchived = :isArchived', { isArchived });
+    }
+
+    if (isFavorite !== undefined) {
+      builder = builder.andWhere('asset.isFavorite = :isFavorite', { isFavorite });
+    }
+
+    return builder;
+  }
 }

+ 3 - 4
server/test/repositories/asset.repository.mock.ts

@@ -10,14 +10,13 @@ export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => {
     getWith: jest.fn(),
     getFirstAssetForAlbumId: jest.fn(),
     getLastUpdatedAssetForAlbumId: jest.fn(),
-    getAll: jest.fn().mockResolvedValue({
-      items: [],
-      hasNextPage: false,
-    }),
+    getAll: jest.fn().mockResolvedValue({ items: [], hasNextPage: false }),
     deleteAll: jest.fn(),
     save: jest.fn(),
     findLivePhotoMatch: jest.fn(),
     getMapMarkers: jest.fn(),
     getStatistics: jest.fn(),
+    getByTimeBucket: jest.fn(),
+    getTimeBuckets: jest.fn(),
   };
 };

+ 337 - 250
web/src/api/open-api/api.ts

@@ -410,44 +410,6 @@ export const AssetBulkUploadCheckResultReasonEnum = {
 
 export type AssetBulkUploadCheckResultReasonEnum = typeof AssetBulkUploadCheckResultReasonEnum[keyof typeof AssetBulkUploadCheckResultReasonEnum];
 
-/**
- * 
- * @export
- * @interface AssetCountByTimeBucket
- */
-export interface AssetCountByTimeBucket {
-    /**
-     * 
-     * @type {number}
-     * @memberof AssetCountByTimeBucket
-     */
-    'count': number;
-    /**
-     * 
-     * @type {string}
-     * @memberof AssetCountByTimeBucket
-     */
-    'timeBucket': string;
-}
-/**
- * 
- * @export
- * @interface AssetCountByTimeBucketResponseDto
- */
-export interface AssetCountByTimeBucketResponseDto {
-    /**
-     * 
-     * @type {Array<AssetCountByTimeBucket>}
-     * @memberof AssetCountByTimeBucketResponseDto
-     */
-    'buckets': Array<AssetCountByTimeBucket>;
-    /**
-     * 
-     * @type {number}
-     * @memberof AssetCountByTimeBucketResponseDto
-     */
-    'totalCount': number;
-}
 /**
  * 
  * @export
@@ -1286,58 +1248,6 @@ export interface ExifResponseDto {
      */
     'timeZone'?: string | null;
 }
-/**
- * 
- * @export
- * @interface GetAssetByTimeBucketDto
- */
-export interface GetAssetByTimeBucketDto {
-    /**
-     * 
-     * @type {Array<string>}
-     * @memberof GetAssetByTimeBucketDto
-     */
-    'timeBucket': Array<string>;
-    /**
-     * 
-     * @type {string}
-     * @memberof GetAssetByTimeBucketDto
-     */
-    'userId'?: string;
-    /**
-     * Include assets without thumbnails
-     * @type {boolean}
-     * @memberof GetAssetByTimeBucketDto
-     */
-    'withoutThumbs'?: boolean;
-}
-/**
- * 
- * @export
- * @interface GetAssetCountByTimeBucketDto
- */
-export interface GetAssetCountByTimeBucketDto {
-    /**
-     * 
-     * @type {TimeGroupEnum}
-     * @memberof GetAssetCountByTimeBucketDto
-     */
-    'timeGroup': TimeGroupEnum;
-    /**
-     * 
-     * @type {string}
-     * @memberof GetAssetCountByTimeBucketDto
-     */
-    'userId'?: string;
-    /**
-     * Include assets without thumbnails
-     * @type {boolean}
-     * @memberof GetAssetCountByTimeBucketDto
-     */
-    'withoutThumbs'?: boolean;
-}
-
-
 /**
  * 
  * @export
@@ -2850,18 +2760,37 @@ export const ThumbnailFormat = {
 export type ThumbnailFormat = typeof ThumbnailFormat[keyof typeof ThumbnailFormat];
 
 
+/**
+ * 
+ * @export
+ * @interface TimeBucketResponseDto
+ */
+export interface TimeBucketResponseDto {
+    /**
+     * 
+     * @type {number}
+     * @memberof TimeBucketResponseDto
+     */
+    'count': number;
+    /**
+     * 
+     * @type {string}
+     * @memberof TimeBucketResponseDto
+     */
+    'timeBucket': string;
+}
 /**
  * 
  * @export
  * @enum {string}
  */
 
-export const TimeGroupEnum = {
-    Day: 'day',
-    Month: 'month'
+export const TimeBucketSize = {
+    Day: 'DAY',
+    Month: 'MONTH'
 } as const;
 
-export type TimeGroupEnum = typeof TimeGroupEnum[keyof typeof TimeGroupEnum];
+export type TimeBucketSize = typeof TimeBucketSize[keyof typeof TimeBucketSize];
 
 
 /**
@@ -5049,14 +4978,11 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
         },
         /**
          * 
-         * @param {GetAssetByTimeBucketDto} getAssetByTimeBucketDto 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        getAssetByTimeBucket: async (getAssetByTimeBucketDto: GetAssetByTimeBucketDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
-            // verify required parameter 'getAssetByTimeBucketDto' is not null or undefined
-            assertParamExists('getAssetByTimeBucket', 'getAssetByTimeBucketDto', getAssetByTimeBucketDto)
-            const localVarPath = `/asset/time-bucket`;
+        getAssetSearchTerms: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/asset/search-terms`;
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
             let baseOptions;
@@ -5064,7 +4990,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
                 baseOptions = configuration.baseOptions;
             }
 
-            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
+            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
             const localVarHeaderParameter = {} as any;
             const localVarQueryParameter = {} as any;
 
@@ -5079,12 +5005,9 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
 
 
     
-            localVarHeaderParameter['Content-Type'] = 'application/json';
-
             setSearchParams(localVarUrlObj, localVarQueryParameter);
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
-            localVarRequestOptions.data = serializeDataIfNeeded(getAssetByTimeBucketDto, localVarRequestOptions, configuration)
 
             return {
                 url: toPathString(localVarUrlObj),
@@ -5093,14 +5016,13 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
         },
         /**
          * 
-         * @param {GetAssetCountByTimeBucketDto} getAssetCountByTimeBucketDto 
+         * @param {boolean} [isArchived] 
+         * @param {boolean} [isFavorite] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        getAssetCountByTimeBucket: async (getAssetCountByTimeBucketDto: GetAssetCountByTimeBucketDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
-            // verify required parameter 'getAssetCountByTimeBucketDto' is not null or undefined
-            assertParamExists('getAssetCountByTimeBucket', 'getAssetCountByTimeBucketDto', getAssetCountByTimeBucketDto)
-            const localVarPath = `/asset/count-by-time-bucket`;
+        getAssetStats: async (isArchived?: boolean, isFavorite?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/asset/statistics`;
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
             let baseOptions;
@@ -5108,7 +5030,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
                 baseOptions = configuration.baseOptions;
             }
 
-            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
+            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
             const localVarHeaderParameter = {} as any;
             const localVarQueryParameter = {} as any;
 
@@ -5121,14 +5043,19 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
             // http bearer authentication required
             await setBearerAuthToObject(localVarHeaderParameter, configuration)
 
+            if (isArchived !== undefined) {
+                localVarQueryParameter['isArchived'] = isArchived;
+            }
 
-    
-            localVarHeaderParameter['Content-Type'] = 'application/json';
+            if (isFavorite !== undefined) {
+                localVarQueryParameter['isFavorite'] = isFavorite;
+            }
 
+
+    
             setSearchParams(localVarUrlObj, localVarQueryParameter);
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
-            localVarRequestOptions.data = serializeDataIfNeeded(getAssetCountByTimeBucketDto, localVarRequestOptions, configuration)
 
             return {
                 url: toPathString(localVarUrlObj),
@@ -5137,11 +5064,17 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
         },
         /**
          * 
+         * @param {string} id 
+         * @param {ThumbnailFormat} [format] 
+         * @param {string} [key] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        getAssetSearchTerms: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
-            const localVarPath = `/asset/search-terms`;
+        getAssetThumbnail: async (id: string, format?: ThumbnailFormat, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'id' is not null or undefined
+            assertParamExists('getAssetThumbnail', 'id', id)
+            const localVarPath = `/asset/thumbnail/{id}`
+                .replace(`{${"id"}}`, encodeURIComponent(String(id)));
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
             let baseOptions;
@@ -5162,6 +5095,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
             // http bearer authentication required
             await setBearerAuthToObject(localVarHeaderParameter, configuration)
 
+            if (format !== undefined) {
+                localVarQueryParameter['format'] = format;
+            }
+
+            if (key !== undefined) {
+                localVarQueryParameter['key'] = key;
+            }
+
 
     
             setSearchParams(localVarUrlObj, localVarQueryParameter);
@@ -5175,13 +5116,22 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
         },
         /**
          * 
+         * @param {TimeBucketSize} size 
+         * @param {string} timeBucket 
+         * @param {string} [userId] 
+         * @param {string} [albumId] 
          * @param {boolean} [isArchived] 
          * @param {boolean} [isFavorite] 
+         * @param {string} [key] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        getAssetStats: async (isArchived?: boolean, isFavorite?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
-            const localVarPath = `/asset/statistics`;
+        getByTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'size' is not null or undefined
+            assertParamExists('getByTimeBucket', 'size', size)
+            // verify required parameter 'timeBucket' is not null or undefined
+            assertParamExists('getByTimeBucket', 'timeBucket', timeBucket)
+            const localVarPath = `/asset/time-bucket`;
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
             let baseOptions;
@@ -5202,60 +5152,28 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
             // http bearer authentication required
             await setBearerAuthToObject(localVarHeaderParameter, configuration)
 
-            if (isArchived !== undefined) {
-                localVarQueryParameter['isArchived'] = isArchived;
+            if (size !== undefined) {
+                localVarQueryParameter['size'] = size;
             }
 
-            if (isFavorite !== undefined) {
-                localVarQueryParameter['isFavorite'] = isFavorite;
+            if (userId !== undefined) {
+                localVarQueryParameter['userId'] = userId;
             }
 
-
-    
-            setSearchParams(localVarUrlObj, localVarQueryParameter);
-            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
-            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
-
-            return {
-                url: toPathString(localVarUrlObj),
-                options: localVarRequestOptions,
-            };
-        },
-        /**
-         * 
-         * @param {string} id 
-         * @param {ThumbnailFormat} [format] 
-         * @param {string} [key] 
-         * @param {*} [options] Override http request option.
-         * @throws {RequiredError}
-         */
-        getAssetThumbnail: async (id: string, format?: ThumbnailFormat, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
-            // verify required parameter 'id' is not null or undefined
-            assertParamExists('getAssetThumbnail', 'id', id)
-            const localVarPath = `/asset/thumbnail/{id}`
-                .replace(`{${"id"}}`, encodeURIComponent(String(id)));
-            // use dummy base URL string because the URL constructor only accepts absolute URLs.
-            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
-            let baseOptions;
-            if (configuration) {
-                baseOptions = configuration.baseOptions;
+            if (albumId !== undefined) {
+                localVarQueryParameter['albumId'] = albumId;
             }
 
-            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
-            const localVarHeaderParameter = {} as any;
-            const localVarQueryParameter = {} as any;
-
-            // authentication cookie required
-
-            // authentication api_key required
-            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
+            if (isArchived !== undefined) {
+                localVarQueryParameter['isArchived'] = isArchived;
+            }
 
-            // authentication bearer required
-            // http bearer authentication required
-            await setBearerAuthToObject(localVarHeaderParameter, configuration)
+            if (isFavorite !== undefined) {
+                localVarQueryParameter['isFavorite'] = isFavorite;
+            }
 
-            if (format !== undefined) {
-                localVarQueryParameter['format'] = format;
+            if (timeBucket !== undefined) {
+                localVarQueryParameter['timeBucket'] = timeBucket;
             }
 
             if (key !== undefined) {
@@ -5507,6 +5425,76 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
 
 
     
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @param {TimeBucketSize} size 
+         * @param {string} [userId] 
+         * @param {string} [albumId] 
+         * @param {boolean} [isArchived] 
+         * @param {boolean} [isFavorite] 
+         * @param {string} [key] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'size' is not null or undefined
+            assertParamExists('getTimeBuckets', 'size', size)
+            const localVarPath = `/asset/time-buckets`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+
+            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication cookie required
+
+            // authentication api_key required
+            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
+
+            // authentication bearer required
+            // http bearer authentication required
+            await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+            if (size !== undefined) {
+                localVarQueryParameter['size'] = size;
+            }
+
+            if (userId !== undefined) {
+                localVarQueryParameter['userId'] = userId;
+            }
+
+            if (albumId !== undefined) {
+                localVarQueryParameter['albumId'] = albumId;
+            }
+
+            if (isArchived !== undefined) {
+                localVarQueryParameter['isArchived'] = isArchived;
+            }
+
+            if (isFavorite !== undefined) {
+                localVarQueryParameter['isFavorite'] = isFavorite;
+            }
+
+            if (key !== undefined) {
+                localVarQueryParameter['key'] = key;
+            }
+
+
+    
             setSearchParams(localVarUrlObj, localVarQueryParameter);
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@@ -5969,26 +5957,6 @@ export const AssetApiFp = function(configuration?: Configuration) {
             const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetById(id, key, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
-        /**
-         * 
-         * @param {GetAssetByTimeBucketDto} getAssetByTimeBucketDto 
-         * @param {*} [options] Override http request option.
-         * @throws {RequiredError}
-         */
-        async getAssetByTimeBucket(getAssetByTimeBucketDto: GetAssetByTimeBucketDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
-            const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetByTimeBucket(getAssetByTimeBucketDto, options);
-            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
-        },
-        /**
-         * 
-         * @param {GetAssetCountByTimeBucketDto} getAssetCountByTimeBucketDto 
-         * @param {*} [options] Override http request option.
-         * @throws {RequiredError}
-         */
-        async getAssetCountByTimeBucket(getAssetCountByTimeBucketDto: GetAssetCountByTimeBucketDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetCountByTimeBucketResponseDto>> {
-            const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetCountByTimeBucket(getAssetCountByTimeBucketDto, options);
-            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
-        },
         /**
          * 
          * @param {*} [options] Override http request option.
@@ -6021,6 +5989,22 @@ export const AssetApiFp = function(configuration?: Configuration) {
             const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetThumbnail(id, format, key, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
+        /**
+         * 
+         * @param {TimeBucketSize} size 
+         * @param {string} timeBucket 
+         * @param {string} [userId] 
+         * @param {string} [albumId] 
+         * @param {boolean} [isArchived] 
+         * @param {boolean} [isFavorite] 
+         * @param {string} [key] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.getByTimeBucket(size, timeBucket, userId, albumId, isArchived, isFavorite, key, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
         /**
          * 
          * @param {*} [options] Override http request option.
@@ -6075,6 +6059,21 @@ export const AssetApiFp = function(configuration?: Configuration) {
             const localVarAxiosArgs = await localVarAxiosParamCreator.getMemoryLane(timestamp, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
+        /**
+         * 
+         * @param {TimeBucketSize} size 
+         * @param {string} [userId] 
+         * @param {string} [albumId] 
+         * @param {boolean} [isArchived] 
+         * @param {boolean} [isFavorite] 
+         * @param {string} [key] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TimeBucketResponseDto>>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, isArchived, isFavorite, key, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
         /**
          * Get all asset of a device that are in the database, ID only.
          * @param {string} deviceId 
@@ -6242,24 +6241,6 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
         getAssetById(id: string, key?: string, options?: any): AxiosPromise<AssetResponseDto> {
             return localVarFp.getAssetById(id, key, options).then((request) => request(axios, basePath));
         },
-        /**
-         * 
-         * @param {GetAssetByTimeBucketDto} getAssetByTimeBucketDto 
-         * @param {*} [options] Override http request option.
-         * @throws {RequiredError}
-         */
-        getAssetByTimeBucket(getAssetByTimeBucketDto: GetAssetByTimeBucketDto, options?: any): AxiosPromise<Array<AssetResponseDto>> {
-            return localVarFp.getAssetByTimeBucket(getAssetByTimeBucketDto, options).then((request) => request(axios, basePath));
-        },
-        /**
-         * 
-         * @param {GetAssetCountByTimeBucketDto} getAssetCountByTimeBucketDto 
-         * @param {*} [options] Override http request option.
-         * @throws {RequiredError}
-         */
-        getAssetCountByTimeBucket(getAssetCountByTimeBucketDto: GetAssetCountByTimeBucketDto, options?: any): AxiosPromise<AssetCountByTimeBucketResponseDto> {
-            return localVarFp.getAssetCountByTimeBucket(getAssetCountByTimeBucketDto, options).then((request) => request(axios, basePath));
-        },
         /**
          * 
          * @param {*} [options] Override http request option.
@@ -6289,6 +6270,21 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
         getAssetThumbnail(id: string, format?: ThumbnailFormat, key?: string, options?: any): AxiosPromise<File> {
             return localVarFp.getAssetThumbnail(id, format, key, options).then((request) => request(axios, basePath));
         },
+        /**
+         * 
+         * @param {TimeBucketSize} size 
+         * @param {string} timeBucket 
+         * @param {string} [userId] 
+         * @param {string} [albumId] 
+         * @param {boolean} [isArchived] 
+         * @param {boolean} [isFavorite] 
+         * @param {string} [key] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: any): AxiosPromise<Array<AssetResponseDto>> {
+            return localVarFp.getByTimeBucket(size, timeBucket, userId, albumId, isArchived, isFavorite, key, options).then((request) => request(axios, basePath));
+        },
         /**
          * 
          * @param {*} [options] Override http request option.
@@ -6338,6 +6334,20 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
         getMemoryLane(timestamp: string, options?: any): AxiosPromise<Array<MemoryLaneResponseDto>> {
             return localVarFp.getMemoryLane(timestamp, options).then((request) => request(axios, basePath));
         },
+        /**
+         * 
+         * @param {TimeBucketSize} size 
+         * @param {string} [userId] 
+         * @param {string} [albumId] 
+         * @param {boolean} [isArchived] 
+         * @param {boolean} [isFavorite] 
+         * @param {string} [key] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: any): AxiosPromise<Array<TimeBucketResponseDto>> {
+            return localVarFp.getTimeBuckets(size, userId, albumId, isArchived, isFavorite, key, options).then((request) => request(axios, basePath));
+        },
         /**
          * Get all asset of a device that are in the database, ID only.
          * @param {string} deviceId 
@@ -6586,34 +6596,6 @@ export interface AssetApiGetAssetByIdRequest {
     readonly key?: string
 }
 
-/**
- * Request parameters for getAssetByTimeBucket operation in AssetApi.
- * @export
- * @interface AssetApiGetAssetByTimeBucketRequest
- */
-export interface AssetApiGetAssetByTimeBucketRequest {
-    /**
-     * 
-     * @type {GetAssetByTimeBucketDto}
-     * @memberof AssetApiGetAssetByTimeBucket
-     */
-    readonly getAssetByTimeBucketDto: GetAssetByTimeBucketDto
-}
-
-/**
- * Request parameters for getAssetCountByTimeBucket operation in AssetApi.
- * @export
- * @interface AssetApiGetAssetCountByTimeBucketRequest
- */
-export interface AssetApiGetAssetCountByTimeBucketRequest {
-    /**
-     * 
-     * @type {GetAssetCountByTimeBucketDto}
-     * @memberof AssetApiGetAssetCountByTimeBucket
-     */
-    readonly getAssetCountByTimeBucketDto: GetAssetCountByTimeBucketDto
-}
-
 /**
  * Request parameters for getAssetStats operation in AssetApi.
  * @export
@@ -6663,6 +6645,62 @@ export interface AssetApiGetAssetThumbnailRequest {
     readonly key?: string
 }
 
+/**
+ * Request parameters for getByTimeBucket operation in AssetApi.
+ * @export
+ * @interface AssetApiGetByTimeBucketRequest
+ */
+export interface AssetApiGetByTimeBucketRequest {
+    /**
+     * 
+     * @type {TimeBucketSize}
+     * @memberof AssetApiGetByTimeBucket
+     */
+    readonly size: TimeBucketSize
+
+    /**
+     * 
+     * @type {string}
+     * @memberof AssetApiGetByTimeBucket
+     */
+    readonly timeBucket: string
+
+    /**
+     * 
+     * @type {string}
+     * @memberof AssetApiGetByTimeBucket
+     */
+    readonly userId?: string
+
+    /**
+     * 
+     * @type {string}
+     * @memberof AssetApiGetByTimeBucket
+     */
+    readonly albumId?: string
+
+    /**
+     * 
+     * @type {boolean}
+     * @memberof AssetApiGetByTimeBucket
+     */
+    readonly isArchived?: boolean
+
+    /**
+     * 
+     * @type {boolean}
+     * @memberof AssetApiGetByTimeBucket
+     */
+    readonly isFavorite?: boolean
+
+    /**
+     * 
+     * @type {string}
+     * @memberof AssetApiGetByTimeBucket
+     */
+    readonly key?: string
+}
+
 /**
  * Request parameters for getDownloadInfo operation in AssetApi.
  * @export
@@ -6747,6 +6785,55 @@ export interface AssetApiGetMemoryLaneRequest {
     readonly timestamp: string
 }
 
+/**
+ * Request parameters for getTimeBuckets operation in AssetApi.
+ * @export
+ * @interface AssetApiGetTimeBucketsRequest
+ */
+export interface AssetApiGetTimeBucketsRequest {
+    /**
+     * 
+     * @type {TimeBucketSize}
+     * @memberof AssetApiGetTimeBuckets
+     */
+    readonly size: TimeBucketSize
+
+    /**
+     * 
+     * @type {string}
+     * @memberof AssetApiGetTimeBuckets
+     */
+    readonly userId?: string
+
+    /**
+     * 
+     * @type {string}
+     * @memberof AssetApiGetTimeBuckets
+     */
+    readonly albumId?: string
+
+    /**
+     * 
+     * @type {boolean}
+     * @memberof AssetApiGetTimeBuckets
+     */
+    readonly isArchived?: boolean
+
+    /**
+     * 
+     * @type {boolean}
+     * @memberof AssetApiGetTimeBuckets
+     */
+    readonly isFavorite?: boolean
+
+    /**
+     * 
+     * @type {string}
+     * @memberof AssetApiGetTimeBuckets
+     */
+    readonly key?: string
+}
+
 /**
  * Request parameters for getUserAssetsByDeviceId operation in AssetApi.
  * @export
@@ -7038,28 +7125,6 @@ export class AssetApi extends BaseAPI {
         return AssetApiFp(this.configuration).getAssetById(requestParameters.id, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
     }
 
-    /**
-     * 
-     * @param {AssetApiGetAssetByTimeBucketRequest} requestParameters Request parameters.
-     * @param {*} [options] Override http request option.
-     * @throws {RequiredError}
-     * @memberof AssetApi
-     */
-    public getAssetByTimeBucket(requestParameters: AssetApiGetAssetByTimeBucketRequest, options?: AxiosRequestConfig) {
-        return AssetApiFp(this.configuration).getAssetByTimeBucket(requestParameters.getAssetByTimeBucketDto, options).then((request) => request(this.axios, this.basePath));
-    }
-
-    /**
-     * 
-     * @param {AssetApiGetAssetCountByTimeBucketRequest} requestParameters Request parameters.
-     * @param {*} [options] Override http request option.
-     * @throws {RequiredError}
-     * @memberof AssetApi
-     */
-    public getAssetCountByTimeBucket(requestParameters: AssetApiGetAssetCountByTimeBucketRequest, options?: AxiosRequestConfig) {
-        return AssetApiFp(this.configuration).getAssetCountByTimeBucket(requestParameters.getAssetCountByTimeBucketDto, options).then((request) => request(this.axios, this.basePath));
-    }
-
     /**
      * 
      * @param {*} [options] Override http request option.
@@ -7092,6 +7157,17 @@ export class AssetApi extends BaseAPI {
         return AssetApiFp(this.configuration).getAssetThumbnail(requestParameters.id, requestParameters.format, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
     }
 
+    /**
+     * 
+     * @param {AssetApiGetByTimeBucketRequest} requestParameters Request parameters.
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof AssetApi
+     */
+    public getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig) {
+        return AssetApiFp(this.configuration).getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
+    }
+
     /**
      * 
      * @param {*} [options] Override http request option.
@@ -7145,6 +7221,17 @@ export class AssetApi extends BaseAPI {
         return AssetApiFp(this.configuration).getMemoryLane(requestParameters.timestamp, options).then((request) => request(this.axios, this.basePath));
     }
 
+    /**
+     * 
+     * @param {AssetApiGetTimeBucketsRequest} requestParameters Request parameters.
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof AssetApi
+     */
+    public getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig) {
+        return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
+    }
+
     /**
      * Get all asset of a device that are in the database, ID only.
      * @param {AssetApiGetUserAssetsByDeviceIdRequest} requestParameters Request parameters.

+ 2 - 2
web/src/lib/components/album-page/asset-selection.svelte

@@ -3,7 +3,7 @@
   import { AssetStore } from '$lib/stores/assets.store';
   import { locale } from '$lib/stores/preferences.store';
   import { openFileUploadDialog } from '$lib/utils/file-uploader';
-  import { TimeGroupEnum, type AssetResponseDto } from '@api';
+  import { TimeBucketSize, type AssetResponseDto } from '@api';
   import { createEventDispatcher, onMount } from 'svelte';
   import { quintOut } from 'svelte/easing';
   import { fly } from 'svelte/transition';
@@ -13,7 +13,7 @@
 
   const dispatch = createEventDispatcher();
 
-  const assetStore = new AssetStore({ timeGroup: TimeGroupEnum.Month });
+  const assetStore = new AssetStore({ size: TimeBucketSize.Month });
   const assetInteractionStore = createAssetInteractionStore();
   const { selectedAssets, assetsInAlbumState } = assetInteractionStore;
 

+ 4 - 1
web/src/lib/components/layouts/user-page-layout.svelte

@@ -7,6 +7,9 @@
   export let hideNavbar = false;
   export let showUploadButton = false;
   export let title: string | undefined = undefined;
+  export let scrollbar = true;
+
+  $: scrollbarClass = scrollbar ? 'immich-scrollbar p-4 pb-8' : 'scrollbar-hidden pl-4';
 </script>
 
 <header>
@@ -32,7 +35,7 @@
           <slot name="buttons" />
         </div>
 
-        <div class="immich-scrollbar absolute top-16 h-[calc(100%-theme(spacing.16))] w-full overflow-y-auto p-4 pb-8">
+        <div class="{scrollbarClass} absolute top-16 h-[calc(100%-theme(spacing.16))] w-full overflow-y-auto">
           <slot />
         </div>
       </section>

+ 1 - 1
web/src/lib/components/photos-page/asset-grid.svelte

@@ -274,7 +274,7 @@
 <!-- Right margin MUST be equal to the width of immich-scrubbable-scrollbar -->
 <section
   id="asset-grid"
-  class="scrollbar-hidden ml-4 mr-[60px] overflow-y-auto pb-4"
+  class="scrollbar-hidden ml-4 mr-[60px] h-full overflow-y-auto pb-4"
   bind:clientHeight={viewport.height}
   bind:clientWidth={viewport.width}
   bind:this={element}

+ 1 - 1
web/src/lib/components/shared-components/empty-placeholder.svelte

@@ -1,7 +1,7 @@
 <script lang="ts">
   import empty1Url from '$lib/assets/empty-1.svg';
 
-  export let actionHandler: undefined | (() => Promise<void>) = undefined;
+  export let actionHandler: undefined | (() => unknown) = undefined;
   export let text = '';
   export let alt = '';
 

+ 6 - 14
web/src/lib/stores/assets.store.ts

@@ -1,4 +1,4 @@
-import { api, AssetResponseDto, GetAssetCountByTimeBucketDto } from '@api';
+import { api, AssetApiGetTimeBucketsRequest, AssetResponseDto } from '@api';
 import { writable } from 'svelte/store';
 import { handleError } from '../utils/handle-error';
 
@@ -9,7 +9,7 @@ export enum BucketPosition {
   Unknown = 'unknown',
 }
 
-export type AssetStoreOptions = GetAssetCountByTimeBucketDto;
+export type AssetStoreOptions = AssetApiGetTimeBucketsRequest;
 
 export interface Viewport {
   width: number;
@@ -51,11 +51,9 @@ export class AssetStore {
   subscribe = this.store$.subscribe;
 
   async init(viewport: Viewport) {
-    const { data } = await api.assetApi.getAssetCountByTimeBucket({
-      getAssetCountByTimeBucketDto: { ...this.options, withoutThumbs: true },
-    });
+    const { data: buckets } = await api.assetApi.getTimeBuckets(this.options);
 
-    this.buckets = data.buckets.map((bucket) => {
+    this.buckets = buckets.map((bucket) => {
       const unwrappedWidth = (3 / 2) * bucket.count * THUMBNAIL_HEIGHT * (7 / 10);
       const rows = Math.ceil(unwrappedWidth / viewport.width);
       const height = rows * THUMBNAIL_HEIGHT;
@@ -101,14 +99,8 @@ export class AssetStore {
 
       bucket.cancelToken = new AbortController();
 
-      const { data: assets } = await api.assetApi.getAssetByTimeBucket(
-        {
-          getAssetByTimeBucketDto: {
-            timeBucket: [bucketDate],
-            ...this.options,
-            withoutThumbs: true,
-          },
-        },
+      const { data: assets } = await api.assetApi.getByTimeBucket(
+        { ...this.options, timeBucket: bucketDate },
         { signal: bucket.cancelToken.signal },
       );
 

+ 34 - 49
web/src/routes/(user)/archive/+page.svelte

@@ -6,70 +6,55 @@
   import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
   import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
   import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
+  import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
+  import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
   import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte';
   import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
   import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
-  import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
-  import SelectAll from 'svelte-material-icons/SelectAll.svelte';
-  import { archivedAsset } from '$lib/stores/archived-asset.store';
-  import { handleError } from '$lib/utils/handle-error';
-  import { api, AssetResponseDto } from '@api';
+  import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
+  import { AssetStore } from '$lib/stores/assets.store';
+  import { api, TimeBucketSize } from '@api';
   import { onMount } from 'svelte';
   import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
   import Plus from 'svelte-material-icons/Plus.svelte';
   import type { PageData } from './$types';
-  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
 
   export let data: PageData;
+  let assetCount = 1;
 
-  let selectedAssets: Set<AssetResponseDto> = new Set();
-  $: isMultiSelectionMode = selectedAssets.size > 0;
-  $: isAllFavorite = Array.from(selectedAssets).every((asset) => asset.isFavorite);
+  const assetStore = new AssetStore({ size: TimeBucketSize.Month, isArchived: true });
+  const assetInteractionStore = createAssetInteractionStore();
+  const { isMultiSelectState, selectedAssets } = assetInteractionStore;
+
+  $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
 
   onMount(async () => {
-    try {
-      const { data: assets } = await api.assetApi.getAllAssets({
-        isArchived: true,
-        withoutThumbs: true,
-      });
-      $archivedAsset = assets;
-    } catch {
-      handleError(Error, 'Unable to load archived assets');
-    }
+    const { data: stats } = await api.assetApi.getAssetStats({ isArchived: true });
+    assetCount = stats.total;
   });
-
-  const onAssetDelete = (assetId: string) => {
-    $archivedAsset = $archivedAsset.filter((a) => a.id !== assetId);
-  };
-  const handleSelectAll = () => {
-    selectedAssets = new Set($archivedAsset);
-  };
 </script>
 
-<UserPageLayout user={data.user} hideNavbar={isMultiSelectionMode} title={data.meta.title}>
-  <!-- Empty Message -->
-  {#if $archivedAsset.length === 0}
+{#if $isMultiSelectState}
+  <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}>
+    <ArchiveAction unarchive onAssetArchive={(asset) => assetStore.removeAsset(asset.id)} />
+    <CreateSharedLink />
+    <SelectAllAssets {assetStore} {assetInteractionStore} />
+    <AssetSelectContextMenu icon={Plus} title="Add">
+      <AddToAlbum />
+      <AddToAlbum shared />
+    </AssetSelectContextMenu>
+    <DeleteAssets onAssetDelete={(assetId) => assetStore.removeAsset(assetId)} />
+    <AssetSelectContextMenu icon={DotsVertical} title="Add">
+      <DownloadAction menuItem />
+      <FavoriteAction menuItem removeFavorite={isAllFavorite} />
+    </AssetSelectContextMenu>
+  </AssetSelectControlBar>
+{/if}
+
+<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} title={data.meta.title} scrollbar={!assetCount}>
+  {#if assetCount}
+    <AssetGrid {assetStore} {assetInteractionStore} />
+  {:else}
     <EmptyPlaceholder text="Archive photos and videos to hide them from your Photos view" alt="Empty archive" />
   {/if}
-
-  <svelte:fragment slot="header">
-    {#if isMultiSelectionMode}
-      <AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
-        <ArchiveAction unarchive onAssetArchive={(asset) => onAssetDelete(asset.id)} />
-        <CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} />
-        <CreateSharedLink />
-        <AssetSelectContextMenu icon={Plus} title="Add">
-          <AddToAlbum />
-          <AddToAlbum shared />
-        </AssetSelectContextMenu>
-        <DeleteAssets {onAssetDelete} />
-        <AssetSelectContextMenu icon={DotsVertical} title="Add">
-          <DownloadAction menuItem />
-          <FavoriteAction menuItem removeFavorite={isAllFavorite} />
-        </AssetSelectContextMenu>
-      </AssetSelectControlBar>
-    {/if}
-  </svelte:fragment>
-
-  <GalleryViewer assets={$archivedAsset} bind:selectedAssets viewFrom="archive-page" />
 </UserPageLayout>

+ 24 - 42
web/src/routes/(user)/favorites/+page.svelte

@@ -6,60 +6,45 @@
   import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
   import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
   import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
+  import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
+  import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
   import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte';
   import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
   import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
-  import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
-  import { handleError } from '$lib/utils/handle-error';
-  import { api, AssetResponseDto } from '@api';
+  import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
+  import { AssetStore } from '$lib/stores/assets.store';
+  import { api, TimeBucketSize } from '@api';
   import { onMount } from 'svelte';
   import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
   import Plus from 'svelte-material-icons/Plus.svelte';
-  import Error from '../../+error.svelte';
   import type { PageData } from './$types';
-  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
-  import SelectAll from 'svelte-material-icons/SelectAll.svelte';
-
-  let favorites: AssetResponseDto[] = [];
-  let selectedAssets: Set<AssetResponseDto> = new Set();
 
   export let data: PageData;
+  let assetCount = 1;
+
+  const assetStore = new AssetStore({ size: TimeBucketSize.Month, isFavorite: true });
+  const assetInteractionStore = createAssetInteractionStore();
+  const { isMultiSelectState, selectedAssets } = assetInteractionStore;
 
-  $: isMultiSelectionMode = selectedAssets.size > 0;
-  $: isAllArchive = Array.from(selectedAssets).every((asset) => asset.isArchived);
+  $: isAllArchive = Array.from($selectedAssets).every((asset) => asset.isArchived);
 
   onMount(async () => {
-    try {
-      const { data: assets } = await api.assetApi.getAllAssets({
-        isFavorite: true,
-        withoutThumbs: true,
-      });
-      favorites = assets;
-    } catch {
-      handleError(Error, 'Unable to load favorites');
-    }
+    const { data: stats } = await api.assetApi.getAssetStats({ isFavorite: true });
+    assetCount = stats.total;
   });
-
-  const handleSelectAll = () => {
-    selectedAssets = new Set(favorites);
-  };
-
-  const onAssetDelete = (assetId: string) => {
-    favorites = favorites.filter((a) => a.id !== assetId);
-  };
 </script>
 
 <!-- Multiselection mode app bar -->
-{#if isMultiSelectionMode}
-  <AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
-    <FavoriteAction removeFavorite onAssetFavorite={(asset) => onAssetDelete(asset.id)} />
+{#if $isMultiSelectState}
+  <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}>
+    <FavoriteAction removeFavorite onAssetFavorite={(asset) => assetStore.removeAsset(asset.id)} />
     <CreateSharedLink />
-    <CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} />
+    <SelectAllAssets {assetStore} {assetInteractionStore} />
     <AssetSelectContextMenu icon={Plus} title="Add">
       <AddToAlbum />
       <AddToAlbum shared />
     </AssetSelectContextMenu>
-    <DeleteAssets {onAssetDelete} />
+    <DeleteAssets onAssetDelete={(assetId) => assetStore.removeAsset(assetId)} />
     <AssetSelectContextMenu icon={DotsVertical} title="Menu">
       <DownloadAction menuItem />
       <ArchiveAction menuItem unarchive={isAllArchive} />
@@ -67,13 +52,10 @@
   </AssetSelectControlBar>
 {/if}
 
-<UserPageLayout user={data.user} hideNavbar={isMultiSelectionMode} title={data.meta.title}>
-  <section>
-    <!-- Empty Message -->
-    {#if favorites.length === 0}
-      <EmptyPlaceholder text="Add favorites to quickly find your best pictures and videos" alt="Empty favorites" />
-    {/if}
-
-    <GalleryViewer assets={favorites} bind:selectedAssets viewFrom="favorites-page" />
-  </section>
+<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} title={data.meta.title} scrollbar={!assetCount}>
+  {#if assetCount}
+    <AssetGrid {assetStore} {assetInteractionStore} />
+  {:else}
+    <EmptyPlaceholder text="Add favorites to quickly find your best pictures and videos" alt="Empty favorites" />
+  {/if}
 </UserPageLayout>

+ 2 - 2
web/src/routes/(user)/partners/[userId]/+page.svelte

@@ -10,7 +10,7 @@
   import { AppRoute } from '$lib/constants';
   import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
   import { AssetStore } from '$lib/stores/assets.store';
-  import { TimeGroupEnum } from '@api';
+  import { TimeBucketSize } from '@api';
   import { onDestroy } from 'svelte';
   import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
   import Plus from 'svelte-material-icons/Plus.svelte';
@@ -18,7 +18,7 @@
 
   export let data: PageData;
 
-  const assetStore = new AssetStore({ timeGroup: TimeGroupEnum.Month, userId: data.partner.id });
+  const assetStore = new AssetStore({ size: TimeBucketSize.Month, userId: data.partner.id });
   const assetInteractionStore = createAssetInteractionStore();
   const { isMultiSelectState, selectedAssets } = assetInteractionStore;
 

+ 7 - 15
web/src/routes/(user)/photos/+page.svelte

@@ -15,8 +15,8 @@
   import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
   import { AssetStore } from '$lib/stores/assets.store';
   import { openFileUploadDialog } from '$lib/utils/file-uploader';
-  import { TimeGroupEnum, api } from '@api';
-  import { onDestroy, onMount } from 'svelte';
+  import { TimeBucketSize, api } from '@api';
+  import { onMount } from 'svelte';
   import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
   import Plus from 'svelte-material-icons/Plus.svelte';
   import type { PageData } from './$types';
@@ -24,30 +24,22 @@
   export let data: PageData;
   let assetCount = 1;
 
-  const assetStore = new AssetStore({ timeGroup: TimeGroupEnum.Month });
+  const assetStore = new AssetStore({ size: TimeBucketSize.Month, isArchived: false });
   const assetInteractionStore = createAssetInteractionStore();
   const { isMultiSelectState, selectedAssets } = assetInteractionStore;
 
+  $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
+
   onMount(async () => {
     const { data: stats } = await api.assetApi.getAssetStats();
     assetCount = stats.total;
   });
-
-  onDestroy(() => {
-    assetInteractionStore.clearMultiselect();
-  });
-
-  $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
-
-  const handleUpload = async () => {
-    openFileUploadDialog();
-  };
 </script>
 
 <UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} showUploadButton>
   <svelte:fragment slot="header">
     {#if $isMultiSelectState}
-      <AssetSelectControlBar assets={$selectedAssets} clearSelect={assetInteractionStore.clearMultiselect}>
+      <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}>
         <CreateSharedLink />
         <SelectAllAssets {assetStore} {assetInteractionStore} />
         <AssetSelectContextMenu icon={Plus} title="Add">
@@ -69,7 +61,7 @@
         <MemoryLane />
       </AssetGrid>
     {:else}
-      <EmptyPlaceholder text="CLICK TO UPLOAD YOUR FIRST PHOTO" actionHandler={handleUpload} />
+      <EmptyPlaceholder text="CLICK TO UPLOAD YOUR FIRST PHOTO" actionHandler={() => openFileUploadDialog()} />
     {/if}
   </svelte:fragment>
 </UserPageLayout>