From d602c573fb7de363959d04d88c22d8fff1583194 Mon Sep 17 00:00:00 2001 From: ajnart Date: Tue, 21 Jul 2026 09:53:02 +0200 Subject: [PATCH] feat(immich): show random photos without album --- .../widgets/immich-album-carousel/index.mdx | 3 +- .../widgets/immich-album-carousel/index.ts | 4 +- packages/api/src/router/widgets/immich.ts | 2 +- .../src/immich/immich-integration.ts | 76 +++++++++++++------ packages/request-handler/src/immich.ts | 2 +- packages/translation/src/lang/en.json | 7 +- .../src/immich/album-carousel/component.tsx | 22 +----- 7 files changed, 62 insertions(+), 54 deletions(-) diff --git a/apps/docs/docs/widgets/immich-album-carousel/index.mdx b/apps/docs/docs/widgets/immich-album-carousel/index.mdx index 59b1beeb9..703bd5a57 100644 --- a/apps/docs/docs/widgets/immich-album-carousel/index.mdx +++ b/apps/docs/docs/widgets/immich-album-carousel/index.mdx @@ -1,6 +1,6 @@ --- title: 'Immich Album Carousel' -description: 'Shows a slideshow of your album pictures from Immich' +description: 'Shows a slideshow of pictures from an Immich album or your full library' hide_title: true --- @@ -37,4 +37,3 @@ import screen from './img/screen.png'; ### Configuration - diff --git a/apps/docs/docs/widgets/immich-album-carousel/index.ts b/apps/docs/docs/widgets/immich-album-carousel/index.ts index f6c4b224c..7e956bae6 100644 --- a/apps/docs/docs/widgets/immich-album-carousel/index.ts +++ b/apps/docs/docs/widgets/immich-album-carousel/index.ts @@ -4,13 +4,13 @@ import { IconPhoto } from "@tabler/icons-react"; export const immichAlbumCarouselWidget: WidgetDefinition = { icon: IconPhoto, name: "Immich Album", - description: "Shows a slideshow of your album pictures from Immich", + description: "Shows a slideshow of pictures from an Immich album or your full library", path: "../../widgets/immich-album-carousel", configuration: { items: [ { name: "Album", - description: "Select the Immich album to show in the slideshow.", + description: "Select an Immich album, or leave empty to show random photos from your full library.", values: { type: "string" }, defaultValue: "", }, diff --git a/packages/api/src/router/widgets/immich.ts b/packages/api/src/router/widgets/immich.ts index 7e8c54d49..aa917271f 100644 --- a/packages/api/src/router/widgets/immich.ts +++ b/packages/api/src/router/widgets/immich.ts @@ -25,7 +25,7 @@ export const immichRouter = createTRPCRouter({ .concat(createImmichIntegrationMiddleware("query")) .input( z.object({ - albumId: z.string(), + albumId: z.string().optional(), }), ) .query(async ({ ctx, input }) => { diff --git a/packages/integrations/src/immich/immich-integration.ts b/packages/integrations/src/immich/immich-integration.ts index 6274802a5..9496482d6 100644 --- a/packages/integrations/src/immich/immich-integration.ts +++ b/packages/integrations/src/immich/immich-integration.ts @@ -1,11 +1,14 @@ import { AssetMediaSize, + AssetTypeEnum, + AssetVisibility, getAlbumInfo, getAllAlbums, getMyUser, getServerStatistics, init, searchAssets, + searchRandom, searchUsers, } from "@immich/sdk"; import type { AssetResponseDto, MetadataSearchDto } from "@immich/sdk"; @@ -43,6 +46,7 @@ export interface ImmichAsset { } const logger = createLogger({ module: "immich-integration" }); +const randomPhotoCount = 50; export class ImmichIntegration extends Integration { constructor(integration: IntegrationInput) { @@ -63,38 +67,36 @@ export class ImmichIntegration extends Integration { }; } - public async getAlbumAsync(albumId: string): Promise { + public async getAlbumAsync(albumId?: string): Promise { this.initClient(); const requestOptions = this.getRequestOptions(); + + if (!albumId) { + const assets = await searchRandom( + { + randomSearchDto: { + size: randomPhotoCount, + type: AssetTypeEnum.Image, + visibility: AssetVisibility.Timeline, + }, + }, + requestOptions, + ); + + return { + id: "", + albumName: "", + assets: await this.createAssetsAsync(assets), + }; + } + const [album, albumAssets] = await Promise.all([ getAlbumInfo({ id: albumId }, requestOptions), this.searchAlbumAssetsAsync(albumId), ]); - const imageProxy = new ImageProxy(); return { albumName: album.albumName, - assets: await Promise.all( - albumAssets.map(async (asset) => { - const publicLink = await imageProxy.createImageAsync( - // This is the URL generated by the SDK's viewAsset call. Preview images are - // browser-compatible while avoiding the large original asset download. - this.url(`/api/assets/${asset.id}/thumbnail`, { size: AssetMediaSize.Preview }).toString(), - { - "x-api-key": this.getSecretValue("apiKey"), - }, - ); - return { - id: asset.id, - type: asset.type, - thumbhash: asset.thumbhash, - fileCreatedAt: asset.fileCreatedAt, - fileModifiedAt: asset.fileModifiedAt, - originalPath: asset.originalPath, - updatedAt: asset.updatedAt, - publicLink, - }; - }), - ), + assets: await this.createAssetsAsync(albumAssets), id: album.id, }; } @@ -136,6 +138,32 @@ export class ImmichIntegration extends Integration { return assets; } + private async createAssetsAsync(assets: AssetResponseDto[]): Promise { + const imageProxy = new ImageProxy(); + return await Promise.all( + assets.map(async (asset) => { + const publicLink = await imageProxy.createImageAsync( + // This is the URL generated by the SDK's viewAsset call. Preview images are + // browser-compatible while avoiding the large original asset download. + this.url(`/api/assets/${asset.id}/thumbnail`, { size: AssetMediaSize.Preview }).toString(), + { + "x-api-key": this.getSecretValue("apiKey"), + }, + ); + return { + id: asset.id, + type: asset.type, + thumbhash: asset.thumbhash, + fileCreatedAt: asset.fileCreatedAt, + fileModifiedAt: asset.fileModifiedAt, + originalPath: asset.originalPath, + updatedAt: asset.updatedAt, + publicLink, + }; + }), + ); + } + private getRequestOptions(fetchAsync = fetchWithTrustedCertificatesAsync) { return { // Undici and node types are not the same diff --git a/packages/request-handler/src/immich.ts b/packages/request-handler/src/immich.ts index 56fc89bd0..a6e2a996c 100644 --- a/packages/request-handler/src/immich.ts +++ b/packages/request-handler/src/immich.ts @@ -33,7 +33,7 @@ export const immichAlbumsRequestHandler = createIntegrationRequestHandler< export const immichAlbumRequestHandler = createIntegrationRequestHandler< ImmichAlbum, IntegrationKindByCategory<"photoService">, - { albumId: string } + { albumId?: string } >({ async requestAsync(integration, input) { const integrationInstance = await createIntegrationAsync(integration); diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 1e48e4486..0ce813d33 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -3009,7 +3009,7 @@ "option": { "albumId": { "label": "Album", - "description": "Select the Immich album to show in the slideshow." + "description": "Select an Immich album, or leave empty to show random photos from your full library." }, "rotationIntervalSeconds": { "label": "Interval in seconds", @@ -3024,9 +3024,8 @@ "description": "Show photos in a random order instead of always starting with the same album order." } }, - "description": "Slideshow of pictures in your Immich album", - "noAlbumSelected": "No album selected, edit widget and choose an album", - "noPhotos": "No photos in this album" + "description": "Slideshow of pictures from an Immich album or your full library", + "noPhotos": "No photos found" }, "downloads": { "name": "Download Client", diff --git a/packages/widgets/src/immich/album-carousel/component.tsx b/packages/widgets/src/immich/album-carousel/component.tsx index ee7085757..85323b647 100644 --- a/packages/widgets/src/immich/album-carousel/component.tsx +++ b/packages/widgets/src/immich/album-carousel/component.tsx @@ -21,9 +21,9 @@ export default function ImmichAlbumCarouselWidget({ const { data: album } = clientApi.widget.immich.getAlbum.useQuery( { integrationId: integrationIds[0] ?? "", - albumId: options.albumId ?? "", + albumId: options.albumId || undefined, }, - { enabled: Boolean(options.albumId) }, + { enabled: integrationIds.length > 0 }, ); const photoAssets = useMemo(() => { @@ -35,10 +35,6 @@ export default function ImmichAlbumCarouselWidget({ setCurrentPhotoIndex(0); }, [photoAssets]); - if (!options.albumId) { - return ; - } - if (!album) return ; if (album.assets.length === 0) { @@ -117,20 +113,6 @@ function Carousel({ assets, currentIndex, setCurrentIndex, rotationInterval, sho ); } -function NoAlbumSelected() { - const t = useI18n(); - return ( -
- - - - {t("widget.immich-albumCarousel.noAlbumSelected")} - - -
- ); -} - function NoPhotosInAlbum() { const t = useI18n(); return (