diff --git a/i18n/en.json b/i18n/en.json index 97f4575567..76110d074a 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1386,6 +1386,7 @@ "leave": "Leave", "leave_album": "Leave album", "lens_model": "Lens model", + "less": "Less", "let_others_respond": "Let others respond", "level": "Level", "library": "Library", @@ -2392,6 +2393,12 @@ "updated_at": "Updated", "updated_password": "Updated password", "upload": "Upload", + "upload_activity": "Upload activity", + "upload_activity_day_count": "{date}: {count, plural, one {# upload} other {# uploads}}", + "upload_activity_day_friday": "Fri", + "upload_activity_day_monday": "Mon", + "upload_activity_day_wednesday": "Wed", + "upload_activity_total_count": "{count, plural, one {# upload} other {# uploads}}", "upload_concurrency": "Upload concurrency", "upload_details": "Upload Details", "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index badf9ce25d..4e8b366890 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -14477,6 +14477,77 @@ "x-immich-state": "Stable" } }, + "/users/me/stats/uploads": { + "get": { + "description": "Retrieve daily upload counts for the current user.", + "operationId": "getMyUploadStatistics", + "parameters": [ + { + "name": "from", + "required": false, + "in": "query", + "description": "Start date in UTC", + "schema": { + "format": "date", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$", + "example": "2024-01-01", + "type": "string" + } + }, + { + "name": "to", + "required": false, + "in": "query", + "description": "End date in UTC", + "schema": { + "format": "date", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$", + "example": "2024-01-01", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserUploadStatsResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Get current user upload statistics", + "tags": [ + "Users" + ], + "x-immich-history": [ + { + "version": "v3", + "state": "Added" + }, + { + "version": "v3", + "state": "Stable" + } + ], + "x-immich-permission": "user.read", + "x-immich-state": "Stable" + } + }, "/users/profile-image": { "delete": { "description": "Delete the profile image of the current user.", @@ -25949,6 +26020,71 @@ }, "type": "object" }, + "UserUploadStatsResponseDto": { + "properties": { + "from": { + "description": "Start date in UTC", + "example": "2024-01-01", + "type": "string" + }, + "series": { + "items": { + "properties": { + "count": { + "description": "Number of uploads", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "date": { + "description": "Date in UTC", + "example": "2024-01-01", + "type": "string" + } + }, + "required": [ + "date", + "count" + ], + "type": "object" + }, + "type": "array" + }, + "summary": { + "properties": { + "totalCount": { + "description": "Total number of uploads", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + } + }, + "required": [ + "totalCount" + ], + "type": "object" + }, + "to": { + "description": "End date in UTC", + "example": "2024-12-31", + "type": "string" + }, + "userId": { + "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, + "required": [ + "from", + "series", + "summary", + "to", + "userId" + ], + "type": "object" + }, "ValidateAccessTokenResponseDto": { "properties": { "authStatus": { diff --git a/server/src/controllers/user.controller.ts b/server/src/controllers/user.controller.ts index 1613c0d860..2961f28802 100644 --- a/server/src/controllers/user.controller.ts +++ b/server/src/controllers/user.controller.ts @@ -72,12 +72,9 @@ export class UserController { @Endpoint({ summary: 'Get current user upload statistics', description: 'Retrieve daily upload counts for the current user.', - history: new HistoryBuilder().added('v2'), + history: new HistoryBuilder().added('v3').stable('v3'), }) - getMyUploadStatistics( - @Auth() auth: AuthDto, - @Query() dto: UserUploadStatsDto, - ): Promise { + getMyUploadStatistics(@Auth() auth: AuthDto, @Query() dto: UserUploadStatsDto): Promise { return this.service.getUploadStatistics(auth, dto); } diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index 4d90bbf0d5..618501885b 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -339,6 +339,22 @@ where limit $3 +-- AssetRepository.getUploadStatistics +select + date_trunc('day', "createdAt" AT TIME ZONE 'UTC') AT TIME ZONE 'UTC' as "date", + count(*) as "count" +from + "asset" +where + "ownerId" = $1::uuid + and "createdAt" >= $2 + and "createdAt" < $3 + and "deletedAt" is null +group by + date_trunc('day', "createdAt" AT TIME ZONE 'UTC') AT TIME ZONE 'UTC' +order by + "date" asc + -- AssetRepository.getTimeBuckets with "asset" as ( diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 28caec4512..799325c0ae 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -97,16 +97,6 @@ export interface TimeBucketItem { count: number; } -export interface UploadStatisticsOptions { - from: Date; - to: Date; -} - -export interface UploadStatisticsItem { - date: string; - count: number; -} - export interface YearMonthDay { day: number; month: number; @@ -717,12 +707,12 @@ export class AssetRepository { } @GenerateSql({ params: [DummyValue.UUID, { from: DummyValue.DATE, to: DummyValue.DATE }] }) - getUploadStatistics(ownerId: string, options: UploadStatisticsOptions): Promise { + getUploadStatistics(ownerId: string, options: { from: Date; to: Date }) { const uploadDate = sql`date_trunc('day', "createdAt" AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'`; return this.db .selectFrom('asset') - .select(sql`(${uploadDate} AT TIME ZONE 'UTC')::date::text`.as('date')) + .select(uploadDate.as('date')) .select((eb) => eb.fn.countAll().as('count')) .where('ownerId', '=', asUuid(ownerId)) .where('createdAt', '>=', options.from) diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index 2700f470ae..f08b6dca20 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -54,15 +54,16 @@ export class UserService extends BaseService { } async getUploadStatistics(auth: AuthDto, dto: UserUploadStatsDto): Promise { + const formatUploadDate = (date: Date) => date.toISOString().slice(0, 10); const toDate = DateTime.fromJSDate(dto.to ?? new Date(), { zone: 'utc' }).startOf('day'); - const fromDate = DateTime.fromJSDate(dto.from ?? toDate.minus({ weeks: 52 }).plus({ days: 1 }).toJSDate(), { - zone: 'utc', - }).startOf('day'); + const fromDate = ( + dto.from ? DateTime.fromJSDate(dto.from, { zone: 'utc' }) : toDate.minus({ weeks: 52 }).plus({ days: 1 }) + ).startOf('day'); const uploadCounts = await this.assetRepository.getUploadStatistics(auth.user.id, { from: fromDate.toJSDate(), to: toDate.plus({ days: 1 }).toJSDate(), }); - const countsByDate = new Map(uploadCounts.map((item) => [item.date, item.count])); + const countsByDate = new Map(uploadCounts.map((item) => [formatUploadDate(item.date), item.count])); const series: UserUploadStatsResponseDto['series'] = []; for (let date = fromDate; date <= toDate; date = date.plus({ days: 1 })) { diff --git a/web/src/routes/(user)/user-settings/UserUsageStatistic.svelte b/web/src/routes/(user)/user-settings/UserUsageStatistic.svelte index de878e1d27..f74ab0df51 100644 --- a/web/src/routes/(user)/user-settings/UserUsageStatistic.svelte +++ b/web/src/routes/(user)/user-settings/UserUsageStatistic.svelte @@ -160,7 +160,7 @@ - Upload activity + {$t('upload_activity')}
@@ -172,11 +172,11 @@
-
Mon
+
{$t('upload_activity_day_monday')}
-
Wed
+
{$t('upload_activity_day_wednesday')}
-
Fri
+
{$t('upload_activity_day_friday')}
@@ -186,8 +186,8 @@ {#each week as day}
{/each}
@@ -196,14 +196,16 @@
- Less + {$t('less')} - More - {uploadStats.summary.totalCount.toLocaleString($locale)} uploads + {$t('more')} + {$t('upload_activity_total_count', { values: { count: uploadStats.summary.totalCount } })}