mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
feat: show shared link view count
This commit is contained in:
parent
7bf142dc43
commit
8b0684ee9c
@ -204,6 +204,12 @@ describe('/shared-links', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should increment the view count', async () => {
|
||||
const request1 = await request(app).get('/shared-links/me').query({ key: linkWithAlbum.key });
|
||||
const request2 = await request(app).get('/shared-links/me').query({ key: linkWithAlbum.key });
|
||||
expect(request2.body.viewCount).toBe(request1.body.viewCount + 1);
|
||||
});
|
||||
|
||||
it('should return unauthorized for incorrect shared link', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/shared-links/me')
|
||||
|
@ -816,6 +816,7 @@
|
||||
"invite_people": "Invite People",
|
||||
"invite_to_album": "Invite to album",
|
||||
"items_count": "{count, plural, one {# item} other {# items}}",
|
||||
"views_count": "{count, plural, one {# view} other {# views}}",
|
||||
"jobs": "Jobs",
|
||||
"keep": "Keep",
|
||||
"keep_all": "Keep All",
|
||||
|
@ -13,6 +13,13 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
||||
addDefault(value, 'sharedLinks', SharedLinksResponse().toJson());
|
||||
}
|
||||
break;
|
||||
|
||||
case 'SharedLinkResponseDto':
|
||||
if (value is Map) {
|
||||
addDefault(value, 'viewCount', 0);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ServerConfigDto':
|
||||
if (value is Map) {
|
||||
addDefault(
|
||||
@ -26,11 +33,14 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
||||
'https://tiles.immich.cloud/v1/style/dark.json',
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'UserResponseDto':
|
||||
if (value is Map) {
|
||||
addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String());
|
||||
}
|
||||
break;
|
||||
|
||||
case 'UserAdminResponseDto':
|
||||
if (value is Map) {
|
||||
addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String());
|
||||
|
@ -27,6 +27,7 @@ class SharedLinkResponseDto {
|
||||
this.token,
|
||||
required this.type,
|
||||
required this.userId,
|
||||
required this.viewCount,
|
||||
});
|
||||
|
||||
///
|
||||
@ -63,6 +64,8 @@ class SharedLinkResponseDto {
|
||||
|
||||
String userId;
|
||||
|
||||
num viewCount;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SharedLinkResponseDto &&
|
||||
other.album == album &&
|
||||
@ -78,7 +81,8 @@ class SharedLinkResponseDto {
|
||||
other.showMetadata == showMetadata &&
|
||||
other.token == token &&
|
||||
other.type == type &&
|
||||
other.userId == userId;
|
||||
other.userId == userId &&
|
||||
other.viewCount == viewCount;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
@ -96,10 +100,11 @@ class SharedLinkResponseDto {
|
||||
(showMetadata.hashCode) +
|
||||
(token == null ? 0 : token!.hashCode) +
|
||||
(type.hashCode) +
|
||||
(userId.hashCode);
|
||||
(userId.hashCode) +
|
||||
(viewCount.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SharedLinkResponseDto[album=$album, allowDownload=$allowDownload, allowUpload=$allowUpload, assets=$assets, createdAt=$createdAt, description=$description, expiresAt=$expiresAt, id=$id, key=$key, password=$password, showMetadata=$showMetadata, token=$token, type=$type, userId=$userId]';
|
||||
String toString() => 'SharedLinkResponseDto[album=$album, allowDownload=$allowDownload, allowUpload=$allowUpload, assets=$assets, createdAt=$createdAt, description=$description, expiresAt=$expiresAt, id=$id, key=$key, password=$password, showMetadata=$showMetadata, token=$token, type=$type, userId=$userId, viewCount=$viewCount]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@ -137,6 +142,7 @@ class SharedLinkResponseDto {
|
||||
}
|
||||
json[r'type'] = this.type;
|
||||
json[r'userId'] = this.userId;
|
||||
json[r'viewCount'] = this.viewCount;
|
||||
return json;
|
||||
}
|
||||
|
||||
@ -163,6 +169,7 @@ class SharedLinkResponseDto {
|
||||
token: mapValueOfType<String>(json, r'token'),
|
||||
type: SharedLinkType.fromJson(json[r'type'])!,
|
||||
userId: mapValueOfType<String>(json, r'userId')!,
|
||||
viewCount: num.parse('${json[r'viewCount']}'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@ -222,6 +229,7 @@ class SharedLinkResponseDto {
|
||||
'showMetadata',
|
||||
'type',
|
||||
'userId',
|
||||
'viewCount',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -11415,6 +11415,9 @@
|
||||
},
|
||||
"userId": {
|
||||
"type": "string"
|
||||
},
|
||||
"viewCount": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@ -11429,7 +11432,8 @@
|
||||
"password",
|
||||
"showMetadata",
|
||||
"type",
|
||||
"userId"
|
||||
"userId",
|
||||
"viewCount"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
|
@ -1060,6 +1060,7 @@ export type SharedLinkResponseDto = {
|
||||
token?: string | null;
|
||||
"type": SharedLinkType;
|
||||
userId: string;
|
||||
viewCount: number;
|
||||
};
|
||||
export type SharedLinkCreateDto = {
|
||||
albumId?: string;
|
||||
|
1
server/src/db.d.ts
vendored
1
server/src/db.d.ts
vendored
@ -312,6 +312,7 @@ export interface SharedLinks {
|
||||
showExif: Generated<boolean>;
|
||||
type: string;
|
||||
userId: string;
|
||||
viewCount: Generated<number>;
|
||||
}
|
||||
|
||||
export interface SmartSearch {
|
||||
|
@ -94,6 +94,7 @@ export class SharedLinkResponseDto {
|
||||
type!: SharedLinkType;
|
||||
createdAt!: Date;
|
||||
expiresAt!: Date | null;
|
||||
viewCount!: number;
|
||||
assets!: AssetResponseDto[];
|
||||
album?: AlbumResponseDto;
|
||||
allowUpload!: boolean;
|
||||
@ -102,7 +103,7 @@ export class SharedLinkResponseDto {
|
||||
showMetadata!: boolean;
|
||||
}
|
||||
|
||||
export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseDto {
|
||||
const map = (sharedLink: SharedLinkEntity, { stripMetadata }: { stripMetadata: boolean }) => {
|
||||
const linkAssets = sharedLink.assets || [];
|
||||
const albumAssets = (sharedLink?.album?.assets || []).map((asset) => asset);
|
||||
|
||||
@ -115,35 +116,16 @@ export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseD
|
||||
userId: sharedLink.userId,
|
||||
key: sharedLink.key.toString('base64url'),
|
||||
type: sharedLink.type,
|
||||
viewCount: sharedLink.viewCount,
|
||||
createdAt: sharedLink.createdAt,
|
||||
expiresAt: sharedLink.expiresAt,
|
||||
assets: assets.map((asset) => mapAsset(asset)),
|
||||
album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined,
|
||||
allowUpload: sharedLink.allowUpload,
|
||||
allowDownload: sharedLink.allowDownload,
|
||||
showMetadata: sharedLink.showExif,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapSharedLinkWithoutMetadata(sharedLink: SharedLinkEntity): SharedLinkResponseDto {
|
||||
const linkAssets = sharedLink.assets || [];
|
||||
const albumAssets = (sharedLink?.album?.assets || []).map((asset) => asset);
|
||||
|
||||
const assets = _.uniqBy([...linkAssets, ...albumAssets], (asset) => asset.id);
|
||||
|
||||
return {
|
||||
id: sharedLink.id,
|
||||
description: sharedLink.description,
|
||||
password: sharedLink.password,
|
||||
userId: sharedLink.userId,
|
||||
key: sharedLink.key.toString('base64url'),
|
||||
type: sharedLink.type,
|
||||
createdAt: sharedLink.createdAt,
|
||||
expiresAt: sharedLink.expiresAt,
|
||||
assets: assets.map((asset) => mapAsset(asset, { stripMetadata: true })) as AssetResponseDto[],
|
||||
assets: assets.map((asset) => mapAsset(asset, { stripMetadata })),
|
||||
album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined,
|
||||
allowUpload: sharedLink.allowUpload,
|
||||
allowDownload: sharedLink.allowDownload,
|
||||
showMetadata: sharedLink.showExif,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const mapSharedLink = (sharedLink: SharedLinkEntity) => map(sharedLink, { stripMetadata: false });
|
||||
export const mapSharedLinkWithoutMetadata = (sharedLink: SharedLinkEntity) => map(sharedLink, { stripMetadata: true });
|
||||
|
@ -62,4 +62,7 @@ export class SharedLinkEntity {
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
albumId!: string | null;
|
||||
|
||||
@Column({ default: 0 })
|
||||
viewCount!: number;
|
||||
}
|
||||
|
14
server/src/migrations/1739916305466-AddViewCount.ts
Normal file
14
server/src/migrations/1739916305466-AddViewCount.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddViewCount1739916305466 implements MigrationInterface {
|
||||
name = 'AddViewCount1739916305466'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "shared_links" ADD "viewCount" integer NOT NULL DEFAULT '0'`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "shared_links" DROP COLUMN "viewCount"`);
|
||||
}
|
||||
|
||||
}
|
@ -194,3 +194,12 @@ where
|
||||
"shared_links"."type" = $2
|
||||
or "albums"."id" is not null
|
||||
)
|
||||
|
||||
-- SharedLinkRepository.incrementViewCount
|
||||
update "shared_links"
|
||||
set
|
||||
"viewCount" = viewCount + 1
|
||||
where
|
||||
"shared_links"."id" = $1
|
||||
returning
|
||||
*
|
||||
|
@ -216,6 +216,15 @@ export class SharedLinkRepository {
|
||||
await this.db.deleteFrom('shared_links').where('shared_links.id', '=', entity.id).execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async incrementViewCount(id: string) {
|
||||
await this.db
|
||||
.updateTable('shared_links')
|
||||
.set('viewCount', sql<number>`"viewCount" + 1`)
|
||||
.where('shared_links.id', '=', id)
|
||||
.execute();
|
||||
}
|
||||
|
||||
private getSharedLinks(id: string) {
|
||||
return this.db
|
||||
.selectFrom('shared_links')
|
||||
|
@ -35,6 +35,9 @@ export class SharedLinkService extends BaseService {
|
||||
response.token = this.validateAndRefreshToken(sharedLink, dto);
|
||||
}
|
||||
|
||||
await this.sharedLinkRepository.incrementViewCount(sharedLink.id);
|
||||
sharedLink.viewCount++;
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@ -195,7 +198,7 @@ export class SharedLinkService extends BaseService {
|
||||
};
|
||||
}
|
||||
|
||||
private mapToSharedLink(sharedLink: SharedLinkEntity, { withExif }: { withExif: boolean }) {
|
||||
private mapToSharedLink(sharedLink: SharedLinkEntity, { withExif }: { withExif: boolean }): SharedLinkResponseDto {
|
||||
return withExif ? mapSharedLink(sharedLink) : mapSharedLinkWithoutMetadata(sharedLink);
|
||||
}
|
||||
|
||||
|
@ -4,14 +4,19 @@
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
viewCount?: number;
|
||||
album: AlbumResponseDto;
|
||||
}
|
||||
|
||||
let { album }: Props = $props();
|
||||
let { album, viewCount }: Props = $props();
|
||||
</script>
|
||||
|
||||
<span class="my-2 flex gap-2 text-sm font-medium text-gray-500" data-testid="album-details">
|
||||
<span>{getAlbumDateRange(album)}</span>
|
||||
<span>•</span>
|
||||
<span>{$t('items_count', { values: { count: album.assetCount } })}</span>
|
||||
{#if viewCount}
|
||||
<span>•</span>
|
||||
<span>{$t('views_count', { values: { count: viewCount } })}</span>
|
||||
{/if}
|
||||
</span>
|
||||
|
@ -111,7 +111,7 @@
|
||||
</h1>
|
||||
|
||||
{#if album.assetCount > 0}
|
||||
<AlbumSummary {album} />
|
||||
<AlbumSummary {album} viewCount={sharedLink.viewCount} />
|
||||
{/if}
|
||||
|
||||
<!-- ALBUM DESCRIPTION -->
|
||||
|
Loading…
x
Reference in New Issue
Block a user