-
Notifications
You must be signed in to change notification settings - Fork 60
Feat: Video support #499
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Feat: Video support #499
Conversation
WalkthroughReplaces image-only flows with asset-aware logic: adds ShowVideos/PlayAudio settings, centralizes asset filtering (AssetHelper/AssetExtensionMethods), extends pools and logic to handle videos, renames GetImage → GetAsset (with optional type), updates API/OpenAPI and frontend to support image/video assets, and updates tests accordingly. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as Frontend (Home Page)
participant Logic as PooledImmichFrameLogic
participant Pool as AssetPool (All/Caching/Memory/...)
participant API as ImmichApi
participant Filter as AssetExtensionMethods.ApplyAccountFilters
participant Settings as AccountSettings
UI->>Logic: Request assets (getAssets)
Logic->>Pool: GetAssets()
Pool->>API: Search assets (Type=IMAGE if !ShowVideos → else allow videos)
API-->>Pool: Asset list
Pool->>API: GetExcludedAlbumAssets(...) (via AssetHelper)
API-->>Pool: Excluded assets
Pool->>Filter: ApplyAccountFilters(assets, settings, excluded)
Filter->>Settings: Evaluate ShowVideos, ShowArchived, date, rating
Filter-->>Pool: Filtered assets
Pool-->>Logic: Return assets
Logic-->>UI: Render asset list
rect rgb(220, 255, 220)
UI->>Logic: Play specific asset (GetAsset(id, assetType))
Logic->>Pool: GetAsset(id, assetType)
Pool->>API: Fetch asset binary/stream
API-->>Pool: Binary stream (image/video)
Pool-->>Logic: Stream + metadata
Logic-->>UI: Stream served (FileResult / blob)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
If you want to give this a try, use the |
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
|
@kdy1968 the setting is under |
|
This is mostly working really well, nice job! I do see some occasional hangs on Frameo (when first launched or often when switching images it just hangs on last image). This is likely due to it's limited CPU and WebView but hopefully there is something we can do to improve this. I also think a setting like video_display_length to only play the first XX seconds would be useful. |
|
seem to be running for 3-5 minute but eventually stops , refreshing browser will resume again , not seeing anything in the log file |
Ooh just found this - fantastic. I tried it, the only slight issue I have noticed so far is that if there is an image displayed next after a video clip, it seems to be shown for a much shorter time than in the settings. (My interval currently set to 3 seconds). |
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
immichFrame.Web/src/lib/components/home-page/home-page.svelte (1)
144-152: GuardhandleDoneagainst double invocation from ProgressBar and videoended
handleDoneis invoked both by theProgressBar(onDone={handleDone}) and byImageComponent’sendedevent:
- Line 144–152:
handleDoneperforms navigation + restart logic.- Line 429–431:
on:ended={() => handleDone(false, false)}onImageComponent.- Line 482–483:
onDone={handleDone}onProgressBar.For videos, both events can fire near-simultaneously at the end of playback, which means
handleDonecan run twice in parallel. That can cause behavior like:
- Skipping one asset ahead (two calls to
getNextAssets()).- The asset immediately after a video appearing only briefly, since a second transition is triggered almost immediately — this matches feedback about “post-video images” showing shorter than the configured interval.
A simple re-entrancy guard around
handleDonewill prevent this:- let progressBarStatus: ProgressBarStatus = $state(ProgressBarStatus.Playing); - let progressBar: ProgressBar = $state() as ProgressBar; - let imageComponent: ImageComponentInstance = $state() as ImageComponentInstance; - let currentDuration: number = $state($configStore.interval ?? 20); + let progressBarStatus: ProgressBarStatus = $state(ProgressBarStatus.Playing); + let progressBar: ProgressBar = $state() as ProgressBar; + let imageComponent: ImageComponentInstance = $state() as ImageComponentInstance; + let currentDuration: number = $state($configStore.interval ?? 20); + let isTransitioning: boolean = $state(false); - const handleDone = async (previous: boolean = false, instant: boolean = false) => { - progressBar.restart(false); - $instantTransition = instant; - if (previous) await getPreviousAssets(); - else await getNextAssets(); - await tick(); - await imageComponent?.play?.(); - await progressBar.play(); - }; + const handleDone = async (previous: boolean = false, instant: boolean = false) => { + if (isTransitioning) { + return; + } + + isTransitioning = true; + try { + progressBar.restart(false); + $instantTransition = instant; + if (previous) { + await getPreviousAssets(); + } else { + await getNextAssets(); + } + await tick(); + await imageComponent?.play?.(); + await progressBar.play(); + } finally { + isTransitioning = false; + } + };This keeps the existing UX while ensuring only one transition is processed per completion.
Also applies to: 429-431, 482-483
ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs (1)
99-154: Fix malformed MIME type when serving cached imagesIn the cached‑image branch you build the content type as:
var ex = Path.GetExtension(file); return (Path.GetFileName(file), $"image/{ex}", fs);
Path.GetExtensionincludes the leading dot (e.g.,.jpeg), so this yieldsimage/.jpeg, which is not a valid MIME type and may confuse some clients. A minimal fix:- var ex = Path.GetExtension(file); - return (Path.GetFileName(file), $"image/{ex}", fs); + var ext = Path.GetExtension(file).TrimStart('.').ToLowerInvariant(); + var contentType = ext == "webp" ? "image/webp" : "image/jpeg"; + return (Path.GetFileName(file), contentType, fs);This also keeps cached images consistent with the non‑cached branch.
♻️ Duplicate comments (1)
ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (1)
3-3: Same dependency concern as AllAssetsPool.This file also depends on
ImmichFrame.WebApi.Helpers. See the comment on AllAssetsPool.cs regarding the Core → WebApi.Helpers dependency direction.
🧹 Nitpick comments (11)
immichFrame.Web/src/lib/constants/asset-type.ts (1)
1-11: Confirm AssetType numeric mapping matches backend AssetTypeEnumThese helpers are clean and convenient, but their correctness depends on
IMAGE = 0,VIDEO = 1, etc. staying in sync with the backendAssetTypeEnumvalues from Immich’s OpenAPI schema.It’s worth double‑checking the enum mapping in the generated client/spec and adding a small test (e.g., asserting that at least one known image and one known video asset report the expected
.typevalue) so any future backend change is caught quickly.immichFrame.Web/src/lib/components/home-page/home-page.svelte (4)
95-121:updateAssetPromisesdoesn’t need to beasync
updateAssetPromisesdoesn’t contain anyawaitand only performs synchronous bookkeeping onassetPromisesDict. Marking itasyncjust wraps the return value in a resolvedPromise<void>and can be misleading about its behavior.You can simplify it to a plain function:
- async function updateAssetPromises() { + function updateAssetPromises() { for (let asset of displayingAssets) { if (!(asset.id in assetPromisesDict)) { assetPromisesDict[asset.id] = loadAsset(asset); } } for (let i = 0; i < PRELOAD_ASSETS; i++) { if (i >= assetBacklog.length) { break; } if (!(assetBacklog[i].id in assetPromisesDict)) { assetPromisesDict[assetBacklog[i].id] = loadAsset(assetBacklog[i]); } } for (let key in assetPromisesDict) { if ( !( displayingAssets.find((item) => item.id == key) || assetBacklog.find((item) => item.id == key) ) ) { delete assetPromisesDict[key]; } } }Call sites already ignore the returned promise, so this is a safe cleanup.
135-138: Confirm Immichdurationformat and hardenparseAssetDuration
getAssetDurationSeconds/parseAssetDurationcurrently assume a colon‑separated format (hh:mm[:ss[.fff]]) and return0for anything they can’t parse, which then falls back to$configStore.interval:
- Lines 257–263:
updateCurrentDurationderivescurrentDurationfrom parsed durations.- Lines 265–272:
getAssetDurationSecondsspecial‑cases videos, otherwise uses the configured interval.- Lines 274–298:
parseAssetDurationsplits on:and multiplies by 60 each step, returning0on NaN.Immich’s OpenAPI only documents
durationas astring, without constraining the format. If some assets have a plain numeric string (seconds) or another representation, this parser will silently treat them as “invalid” and revert to the fallback interval, which can make progress bar and actual video length diverge.To make this more robust, you can add a fast path for simple numeric strings before the colon‑based logic:
- function parseAssetDuration(duration?: string | null) { - if (!duration) { - return 0; - } - const parts = duration.split(':').map((value) => value.trim()); - if (!parts.length) { - return 0; - } + function parseAssetDuration(duration?: string | null) { + if (!duration) { + return 0; + } + + // Handle plain seconds (e.g. "37" or "37.5") before colon-based formats. + if (!duration.includes(':')) { + const numeric = parseFloat(duration.replace(',', '.')); + return Number.isNaN(numeric) ? 0 : numeric; + } + + const parts = duration.split(':').map((value) => value.trim()); + if (!parts.length) { + return 0; + } let total = 0; let multiplier = 1; while (parts.length) { const value = parts.pop(); if (!value) { continue; } const normalized = value.replace(',', '.'); const numeric = parseFloat(normalized); if (Number.isNaN(numeric)) { return 0; } total += numeric * multiplier; multiplier *= 60; } return total; }I’d also recommend adding a couple of unit tests around
parseAssetDurationthat use realdurationstrings from your Immich instance (short clip, longer clip, and a failure case) so regressions are caught early.Also applies to: 257-272, 274-298
135-139: Minor readability: reuseisSupportedAssetin backlog filterIn
loadAssets, you manually repeat the image/video checks:assetBacklog = assetRequest.data.filter( (asset) => isImageAsset(asset) || isVideoAsset(asset) );Given you already have
isSupportedAsset, you could make this a bit clearer and more future‑proof:- assetBacklog = assetRequest.data.filter( - (asset) => isImageAsset(asset) || isVideoAsset(asset) - ); + assetBacklog = assetRequest.data.filter(isSupportedAsset);This keeps all knowledge of “what we can render” in a single place.
327-362: Consider revoking object URLs when assets are evicted
loadAssetcreates an object URL for each asset blob:return [getObjectUrl(req.data), assetResponse, album];and
getObjectUrlwrapsURL.createObjectURL. When assets fall out ofdisplayingAssetsandassetBacklog,updateAssetPromisesdeletes their promises fromassetPromisesDict, but the corresponding object URLs are never revoked.This was already a minor leak for images; with video blobs added, the memory impact can grow over long runtimes.
Not urgent for this PR, but as a follow‑up you may want to:
- Track the URL per asset id (e.g. in a separate
assetUrlByIdmap or by resolving the promise before deletion), and- Call
URL.revokeObjectURL(url)insideupdateAssetPromiseswhen you remove an entry that’s no longer in history/backlog.That will keep the frame process footprint more stable during long-running slideshows.
Also applies to: 111-119
ImmichFrame.Core/Logic/Pool/MemoryAssetsPool.cs (1)
20-23: Video gating in MemoryAssetsPool is correct but could be centralizedFiltering
assetstoAssetTypeEnum.IMAGEwhenShowVideosis false is logically correct and avoids extra per-asset API calls for videos. If you’re standardizing on shared helpers likeApplyAccountFilterselsewhere, consider using the same helper here for consistency so account-level rules (videos, excluded albums/people, ratings) all live in one place.ImmichFrame.Core.Tests/Logic/Pool/FavoriteAssetsPoolTests.cs (1)
42-42: Leverage the newtypeparameter to cover ShowVideos=true behaviorThe extended
CreateAssethelper is good, but the tests in this file still only exercise the image-only path (implicitShowVideos == false). Consider adding at least one test that sets_mockAccountSettings.Setup(a => a.ShowVideos).Returns(true);and verifies that:
MetadataSearchDto.Typeis not forced toIMAGE, and- video assets (created via
CreateAsset(id, AssetTypeEnum.VIDEO)) are returned as expected.That will lock in the new behavior and guard against regressions.
ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs (1)
156-177: Align video file extension with actual content type (optional)
GetVideoAssetalways names the file{id}.mp4even if theContent-Typeheader is something else (e.g.,video/webm). That’s probably harmless for streaming but can be misleading if the filename is ever surfaced to clients. Consider deriving the extension fromcontentType(falling back to.mp4when unknown) so the name and MIME type match.ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs (1)
26-26: Consider parameterizing the hardcoded limit.The test helper now calls
GetAssets(25, ct)with a hardcoded limit of 25. While this is acceptable for tests, consider whether this value should be a constant or parameter to make test intent clearer.- public async Task<IEnumerable<AssetResponseDto>> TestLoadAssets(CancellationToken ct = default) => await base.GetAssets(25, ct); + private const int TestAssetLimit = 25; + public async Task<IEnumerable<AssetResponseDto>> TestLoadAssets(CancellationToken ct = default) => await base.GetAssets(TestAssetLimit, ct);ImmichFrame.Core.Tests/Logic/Pool/MemoryAssetsPoolTests.cs (1)
168-198: Simplify test setup by removing unused video memories.Lines 171, 177, and the videoMemoriesCount variable create video memories that are never used because
ShowVideosdefaults to false. These video assets are filtered out byApplyAccountFilters, making the test setup misleading.Apply this diff to remove the unnecessary video memory creation:
[Test] public async Task LoadAssets_AggregatesAssetsFromMultipleMemories() { - var imageMemoriesCount = 2; - var videoMemoriesCount = 2; + var memoryCount = 2; var assetsPerMemory = 2; // Arrange var memoryYear = DateTime.Now.Year - 3; - var memories = CreateSampleImageMemories(imageMemoriesCount, assetsPerMemory, true, memoryYear); // 2 memories, 2 assets each - memories.AddRange(CreateSampleVideoMemories(videoMemoriesCount, assetsPerMemory, true, memoryYear)); // 2 video memories, 2 assets each + var memories = CreateSampleImageMemories(memoryCount, assetsPerMemory, true, memoryYear); // 2 memories, 2 assets each _mockImmichApi.Setup(x => x.SearchMemoriesAsync(It.IsAny<DateTimeOffset>(), null, null, null, It.IsAny<CancellationToken>())) .ReturnsAsync(memories).Verifiable(Times.Once);openApi/swagger.json (1)
211-274: LGTM! New unified asset endpoint properly supports video playback.The new
/api/Asset/{id}/Assetendpoint consolidates image and video retrieval with proper content type support (image/jpeg, image/webp, video/mp4, video/quicktime) and an optionalassetTypeparameter for type-specific retrieval.The path
/api/Asset/{id}/Assethas a slightly redundant naming pattern. Consider/api/Asset/{id}or/api/Asset/{id}/Contentfor a cleaner API surface in a future refactor. However, the current pattern maintains consistency with existing endpoints like/api/Asset/{id}/Imageand/api/Asset/{id}/AssetInfo.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (32)
ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs(2 hunks)ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs(4 hunks)ImmichFrame.Core.Tests/Logic/Pool/CachingApiAssetsPoolTests.cs(11 hunks)ImmichFrame.Core.Tests/Logic/Pool/FavoriteAssetsPoolTests.cs(1 hunks)ImmichFrame.Core.Tests/Logic/Pool/MemoryAssetsPoolTests.cs(7 hunks)ImmichFrame.Core.Tests/Logic/Pool/PersonAssetsPoolTests.cs(4 hunks)ImmichFrame.Core/Helpers/AssetExtensionMethods.cs(1 hunks)ImmichFrame.Core/Interfaces/IImmichFrameLogic.cs(2 hunks)ImmichFrame.Core/Interfaces/IServerSettings.cs(1 hunks)ImmichFrame.Core/Logic/MultiImmichFrameLogicDelegate.cs(1 hunks)ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs(1 hunks)ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs(2 hunks)ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs(1 hunks)ImmichFrame.Core/Logic/Pool/FavoriteAssetsPool.cs(1 hunks)ImmichFrame.Core/Logic/Pool/MemoryAssetsPool.cs(2 hunks)ImmichFrame.Core/Logic/Pool/PeopleAssetsPool.cs(1 hunks)ImmichFrame.Core/Logic/Pool/QueuingAssetPool.cs(2 hunks)ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs(2 hunks)ImmichFrame.WebApi.Tests/Controllers/AssetControllerTests.cs(2 hunks)ImmichFrame.WebApi.Tests/Resources/TestV1.json(1 hunks)ImmichFrame.WebApi.Tests/Resources/TestV2.json(2 hunks)ImmichFrame.WebApi.Tests/Resources/TestV2.yml(2 hunks)ImmichFrame.WebApi.Tests/Resources/TestV2_NoGeneral.json(1 hunks)ImmichFrame.WebApi/Controllers/AssetController.cs(3 hunks)ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs(2 hunks)ImmichFrame.WebApi/Models/ServerSettings.cs(1 hunks)immichFrame.Web/src/lib/components/elements/image-component.svelte(5 hunks)immichFrame.Web/src/lib/components/elements/image.svelte(6 hunks)immichFrame.Web/src/lib/components/home-page/home-page.svelte(18 hunks)immichFrame.Web/src/lib/constants/asset-type.ts(1 hunks)immichFrame.Web/src/lib/immichFrameApi.ts(2 hunks)openApi/swagger.json(2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-09T16:12:49.488Z
Learnt from: JoeRu
Repo: immichFrame/ImmichFrame PR: 481
File: ImmichFrame.Core.Tests/Logic/Pool/ChronologicalAssetsPoolWrapperTests.cs:306-306
Timestamp: 2025-10-09T16:12:49.488Z
Learning: When testing the ChronologicalAssetsPoolWrapper in ImmichFrame.Core.Tests, use `Is.SupersetOf` rather than `Is.EquivalentTo` or `Is.EqualTo` assertions because the wrapper uses Fisher-Yates shuffle to randomize set order, making output non-deterministic between runs. The wrapper also uses a 10x fetch multiplier (capped at 1000) that may return more assets than requested, which is legitimate behavior.
Applied to files:
ImmichFrame.Core/Logic/Pool/MemoryAssetsPool.csImmichFrame.Core.Tests/Logic/Pool/CachingApiAssetsPoolTests.csImmichFrame.Core/Logic/Pool/FavoriteAssetsPool.csImmichFrame.Core/Logic/PooledImmichFrameLogic.csImmichFrame.WebApi.Tests/Controllers/AssetControllerTests.csImmichFrame.Core.Tests/Logic/Pool/PersonAssetsPoolTests.csImmichFrame.Core.Tests/Logic/Pool/FavoriteAssetsPoolTests.csImmichFrame.Core.Tests/Logic/Pool/MemoryAssetsPoolTests.csImmichFrame.Core/Logic/Pool/AlbumAssetsPool.csImmichFrame.Core/Logic/Pool/PeopleAssetsPool.csImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.csImmichFrame.Core/Helpers/AssetExtensionMethods.csImmichFrame.Core/Logic/Pool/AllAssetsPool.csImmichFrame.Core/Logic/Pool/QueuingAssetPool.csImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.csImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs
📚 Learning: 2025-10-09T16:12:49.488Z
Learnt from: JoeRu
Repo: immichFrame/ImmichFrame PR: 481
File: ImmichFrame.Core.Tests/Logic/Pool/ChronologicalAssetsPoolWrapperTests.cs:306-306
Timestamp: 2025-10-09T16:12:49.488Z
Learning: When testing algorithms with randomization (like Fisher-Yates shuffle), use order-independent assertions such as `Is.SupersetOf` to validate core functionality while maintaining test stability. Order-dependent assertions like `Is.EqualTo` or exact-count assertions like `Is.EquivalentTo` will cause flaky tests when the implementation legitimately produces non-deterministic output.
Applied to files:
ImmichFrame.Core.Tests/Logic/Pool/CachingApiAssetsPoolTests.cs
🔇 Additional comments (47)
ImmichFrame.WebApi.Tests/Resources/TestV2_NoGeneral.json (1)
30-30: Remove unverified claim about test coverage.The original review comment incorrectly asserts that tests exercise the ShowVideos configuration difference. However,
TestLoadConfigV2Json_NoGeneral()only validatesGeneralSettingsand does not callVerifyAccounts()orVerifyConfig(). The asymmetric ShowVideos configuration (Account1 without it, Account2 with it) has no test coverage in this test case.The test file itself is appropriate—the asymmetric configuration is intentional for testing different scenarios. However, the review comment's request to verify test coverage cannot be fulfilled because the relevant test does not exercise the Accounts configuration.
Likely an incorrect or invalid review comment.
ImmichFrame.Core/Logic/Pool/QueuingAssetPool.cs (1)
50-51: No issues found—QueuingAssetPool is confirmed to be unused in production.Verification confirms the TODO comment is accurate:
QueuingAssetPoolis never instantiated in production code. TheBuildPoolmethod only instantiatesAllAssetsPool,FavoriteAssetsPool,MemoryAssetsPool,AlbumAssetsPool,PersonAssetsPool, andMultiAssetPool. No DI registration or production-code references toQueuingAssetPoolexist outside the test suite. The missingShowVideosfilter is not a functional concern for unused code.ImmichFrame.WebApi.Tests/Resources/TestV2.json (1)
47-68: ShowVideos flag correctly wired into JSON test dataThe new
ShowVideosfields for both accounts align with the C#ServerAccountSettings.ShowVideosproperty and give you explicit coverage of the video-enabled path in v2 JSON config tests. No issues from a structure or naming perspective.ImmichFrame.WebApi/Models/ServerSettings.cs (1)
65-81: ServerAccountSettings.ShowVideos added with safe default
ShowVideosis added with a default offalse, which is a good backward-compatible choice for existing configs that don’t include the field. It also matches the casing used in your JSON/YAML resources.ImmichFrame.WebApi.Tests/Resources/TestV2.yml (1)
44-60: YAML test fixture kept consistent with JSON for ShowVideosAdding
ShowVideos: trueto both account entries mirrors the JSON resource and ensures YAML-based tests exercise the video-enabled configuration too. Looks consistent and correct.ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs (1)
15-15: V1 settings and adapter correctly extended with ShowVideosExtending
ServerSettingsV1andAccountSettingsV1AdapterwithShowVideoskeeps the v1 configuration path feature-complete with v2 while preserving a safe default offalsefor older configs. The adapter projection is straightforward and consistent with the other account flags.Also applies to: 68-83
ImmichFrame.Core/Interfaces/IImmichFrameLogic.cs (1)
8-15: GetImage → GetAsset interface change looks soundSwitching to
GetAsset(Guid id, AssetTypeEnum? assetType = null)is a sensible generalization for video support and retains the existing stream return shape. Just ensure all implementations and callers have been updated to the new name/signature (the compiler should catch any stragglers).immichFrame.Web/src/lib/immichFrameApi.ts (1)
224-235: getAssets/getAsset client wiring aligns with new API surfaceThe generated
getAssetsandgetAssethelpers correctly reflect the new/api/Assetlist and/api/Asset/{id}/Assetbinary endpoints, including the optionalassetTypequery parameter. This matches how the front-end now requests image/video blobs.Also applies to: 272-285
ImmichFrame.WebApi.Tests/Resources/TestV1.json (1)
15-15: ShowVideos test flag looks consistentAdding
ShowVideos: truealongside other boolean flags keeps the test config aligned with the new account setting; no issues here.ImmichFrame.Core/Interfaces/IServerSettings.cs (1)
16-16: Interface change requires all implementations to expose ShowVideosAdding
ShowVideostoIAccountSettingsis appropriate next to the otherShow*flags, but it does require every implementation (and mocks) ofIAccountSettingsto be updated. Please confirm all concrete settings classes (and configuration mappings) now populate this property with the intended default.ImmichFrame.Core/Logic/Pool/MemoryAssetsPool.cs (1)
47-49: DailyApiCache expiration at next midnight is fineSetting
AbsoluteExpirationtoDateTimeOffset.Now.Date.AddDays(1)keeps the cache aligned to calendar days; the behavior looks intentional and safe.ImmichFrame.Core/Logic/Pool/FavoriteAssetsPool.cs (1)
26-29: Conditional favorite-type filter matches ShowVideos semanticsOnly forcing
metadataBody.Type = AssetTypeEnum.IMAGEwhenShowVideosis false looks correct and aligns favorites with the per-account video toggle. Please just double‑check that the ImmichSearchAssetsAsyncAPI indeed treats a nullTypeas “all asset types”; if that ever changes, video inclusion here would break silently.ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs (1)
18-18: Album pool now relies entirely on upstream filteringReturning
albumAssetsdirectly keeps this pool simple. Given the previous excluded‑album handling, please confirm that album‑level exclusions (fromExcludedAlbums) are now enforced centrally (e.g., via a sharedApplyAccountFiltersstep inCachingApiAssetsPoolor similar) so behavior doesn’t regress for users relying on exclusions.ImmichFrame.Core/Logic/Pool/PeopleAssetsPool.cs (1)
28-31: Person-assets video gating matches the intended semanticsConditionally constraining
metadataBody.TypetoIMAGEwhenShowVideosis false is consistent with the other pools and keeps per-person results aligned with the account’s video preference. Looks good.ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs (1)
76-97: New GetAsset router cleanly separates image and video handlingThe
GetAssetentrypoint looks good: resolvingassetTypeviaGetAssetInfoAsyncwhen missing, then delegating to image/video helpers and guarding unsupported types with a clear exception. Please just ensure that all external callers passing a non‑nullassetTypeare using the real asset type from Immich (not UI guesses), otherwise you can end up calling the wrong handler and surfacing confusing 404s.ImmichFrame.WebApi.Tests/Controllers/AssetControllerTests.cs (1)
193-264: Commented-out test needs attention.The TODO comment indicates this video streaming test requires fixes before activation. Consider tracking this as a follow-up task to ensure video asset retrieval is properly tested.
Would you like me to open a new issue to track completing this test, or is this already covered by issue #502?
ImmichFrame.Core.Tests/Logic/Pool/PersonAssetsPoolTests.cs (1)
44-44: LGTM! Type-aware asset filtering properly tested.The addition of the optional
typeparameter with a sensible default (IMAGE) and the consistent verification of type filtering in mock setups aligns well with the video support feature.Also applies to: 61-84, 103-109
ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs (1)
34-38: LGTM! Proper cache mock setup.The mock setup correctly handles the async factory pattern for
GetOrAddAsync, ensuring the cache behavior is properly exercised in tests.ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (2)
11-21: LGTM! Video counting logic is correct.The conditional logic properly includes video assets in the count when
ShowVideosis enabled, falling back to images-only otherwise.
32-35: LGTM! Asset type filtering correctly respects ShowVideos setting.The logic appropriately sets
Type = IMAGEonly when videos are disabled, allowing mixed asset types whenShowVideosis true. The use ofApplyAccountFiltersconsolidates filtering logic.Also applies to: 64-66
ImmichFrame.Core.Tests/Logic/Pool/CachingApiAssetsPoolTests.cs (2)
62-62: LGTM! Comprehensive video filtering test coverage.The new tests properly validate video asset filtering behavior under different
ShowVideossettings. The test data includes a video asset, and assertions correctly verify filtering outcomes.Also applies to: 85-100, 134-148, 205-220
164-177: Good improvement: Dictionary-based cache store.Replacing the local cached-value pattern with a dictionary provides more robust simulation of actual caching behavior and better reflects how the cache operates in production.
ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (2)
23-26: LGTM! Sound two-step caching pattern.Caching excluded album assets separately from the final filtered assets is a good design that allows cache invalidation at the appropriate granularity. The cache keys are properly namespaced.
28-40: LGTM! Excluded assets aggregation is correct.The method properly iterates over excluded albums with null-safe handling and aggregates their assets. The null-coalescing operator on line 32 prevents null reference exceptions.
ImmichFrame.Core/Logic/MultiImmichFrameLogicDelegate.cs (1)
45-46: LGTM! API properly generalized for multi-asset-type support.Renaming
GetImagetoGetAssetwith an optionalassetTypeparameter appropriately reflects the expanded scope to support videos. The delegation correctly passes both parameters through.immichFrame.Web/src/lib/components/elements/image-component.svelte (2)
56-73: LGTM! Clean event handling and control API.The event dispatcher is properly typed, and the exported
pause/playmethods provide a clean API for external control. Null-safe optional chaining ensures the methods work correctly even if components aren't yet mounted.
115-116: LGTM! Proper component binding and event propagation.The
bind:thisdirectives correctly capture component references, and theon:endedhandlers properly propagate media-end events. The pattern handles both single and split-screen modes consistently.Also applies to: 132-133, 151-152
ImmichFrame.Core.Tests/Logic/Pool/MemoryAssetsPoolTests.cs (3)
30-47: LGTM! Asset type parameterization is well implemented.The helper now correctly sets file extensions and asset types based on the provided
AssetTypeEnum, enabling test scenarios for both images and videos.
49-57: LGTM! Type-specific helper methods improve test readability.The specialized helpers provide clear intent when creating image-only or video-only test data.
200-231: LGTM! Video aggregation test correctly validates mixed-asset behavior.The test properly sets
ShowVideos = trueand verifies that both image and video memories are aggregated, expecting the correct total count of 8 assets.ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs (5)
46-61: LGTM! Test helpers cleanly support multi-type and rating-aware asset creation.The helpers provide clear, type-safe wrappers for creating test assets with specific types and ratings, supporting comprehensive test scenarios.
64-95: LGTM! Asset counting tests correctly validate ShowVideos behavior.The tests properly verify that asset counts include only images when
ShowVideosis false (100 assets) and include both images and videos when enabled (140 assets).
98-156: LGTM! Parameter validation tests correctly verify API request construction.Both tests properly validate that
SearchRandomAsyncis called with the correct parameters:
- When
ShowVideosis false:Type = IMAGEand size reflects only images.- When
ShowVideosis true:Type = null(all types) and size includes both images and videos.
159-171: LGTM! Date filter test correctly validates ImagesFromDays behavior.The test properly verifies that the
TakenAfterparameter is set correctly based onImagesFromDaysconfiguration.
174-197: LGTM! Excluded albums test correctly validates asset filtering.The test properly verifies that assets from excluded albums are filtered out, using the updated image-specific helper for consistency.
openApi/swagger.json (2)
13-13: LGTM! OperationId rename improves API clarity.Renaming
GetAssettoGetAssetsfor the list endpoint accurately reflects that it returns multiple assets, improving API documentation clarity.
207-209: LGTM! Proper deprecation strategy maintains backward compatibility.Marking the
/Imageendpoint as deprecated while keeping it functional allows existing clients to continue working while signaling the need to migrate to the new/Assetendpoint.ImmichFrame.Core/Helpers/AssetExtensionMethods.cs (3)
9-12: LGTM! Supported asset type filtering is appropriate.The method correctly identifies IMAGE and VIDEO as supported asset types, which aligns with the PR's goal of adding video playback support. Other asset types (if any exist in
AssetTypeEnum) are intentionally filtered out.
14-17: LGTM! Async overload provides convenient filtering pipeline support.The async extension method allows seamless integration with async asset retrieval operations.
19-50: Date filtering excludes assets without EXIF DateTimeOriginal; consider whether fallback dates are intended.The filtering logic in lines 33 and 39 uses only
ExifInfo?.DateTimeOriginalwhen applying date range constraints. Verification confirms thatAssetResponseDtohas alternative date properties available—fileCreatedAt,fileModifiedAt, andlocalDateTime—but no fallback logic uses these fields. Assets without EXIF date information will be completely excluded from date-range filtering.This filtering behavior may be intentional (restricting to EXIF-sourced dates only) or may represent an oversight where date filters should fallback to file or computed timestamps. Verify this aligns with intended user behavior before merging.
immichFrame.Web/src/lib/components/elements/image.svelte (4)
2-47: LGTM! Video handling setup is properly structured.The component correctly imports necessary utilities, creates a typed event dispatcher for the
endedevent, derives video status, and maintains a reference to the video element for programmatic control.
52-53: LGTM! Zoom and pan are appropriately disabled for videos.Disabling these visual effects for videos prevents interference with video playback and maintains expected user experience.
137-151: LGTM! Video control methods are well-guarded against edge cases.Both
pause()andplay()properly check for video element existence before calling methods. The try-catch inplay()correctly handles autoplay blocking and other expected failures.
190-211: LGTM! Conditional rendering properly handles video and image assets.The video element is configured correctly for automatic slideshow playback:
autoplayandmutedenable automatic playbackplaysinlineprevents unwanted fullscreen on mobileposterprovides visual continuity while loadingon:endedevent enables progression to the next assetImmichFrame.WebApi/Controllers/AssetController.cs (3)
36-42: LGTM! Endpoint rename improves semantic clarity.Renaming to
GetAssets(plural) accurately reflects that the endpoint returns a list of assets, improving API consistency.
62-84: LGTM! Backward-compatible refactor with proper deprecation.The approach maintains backward compatibility while introducing video support:
- Old
GetImageendpoint delegates to newGetAssetwithIMAGEtype hint- Properly marked with
[Obsolete]attribute- New
GetAssetaccepts optionalassetTypeparameter for performance optimization- Logging includes type hint for debugging
95-95: LGTM! Random image endpoint updated to use new GetAsset method.The call correctly specifies
AssetTypeEnum.IMAGEsince this endpoint is specifically for image retrieval, as indicated by its name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (1)
ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (1)
28-40: Same null reference issue and duplication as in AllAssetsPool.cs.Line 36 has the same potential
ArgumentNullExceptionifalbumInfo.Assetsis null. This method is an exact duplicate of the implementation inAllAssetsPool.cs(lines 69-81).Apply the same fix as suggested for
AllAssetsPool.cs:- excludedAlbumAssets.AddRange(albumInfo.Assets); + if (albumInfo.Assets != null) + { + excludedAlbumAssets.AddRange(albumInfo.Assets); + }Consider extracting this duplicated logic to a shared helper as suggested in the review for
AllAssetsPool.cs.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs(5 hunks)ImmichFrame.Core/Helpers/AssetExtensionMethods.cs(1 hunks)ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs(3 hunks)ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- ImmichFrame.Core/Helpers/AssetExtensionMethods.cs
- ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-09T16:12:49.488Z
Learnt from: JoeRu
Repo: immichFrame/ImmichFrame PR: 481
File: ImmichFrame.Core.Tests/Logic/Pool/ChronologicalAssetsPoolWrapperTests.cs:306-306
Timestamp: 2025-10-09T16:12:49.488Z
Learning: When testing the ChronologicalAssetsPoolWrapper in ImmichFrame.Core.Tests, use `Is.SupersetOf` rather than `Is.EquivalentTo` or `Is.EqualTo` assertions because the wrapper uses Fisher-Yates shuffle to randomize set order, making output non-deterministic between runs. The wrapper also uses a 10x fetch multiplier (capped at 1000) that may return more assets than requested, which is legitimate behavior.
Applied to files:
ImmichFrame.Core/Logic/Pool/AllAssetsPool.csImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs
🧬 Code graph analysis (2)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (2)
ImmichFrame.Core/Helpers/AssetExtensionMethods.cs (2)
Task(13-16)IEnumerable(18-49)ImmichFrame.Core/Helpers/CollectionExtensionMethods.cs (4)
IEnumerable(7-14)IEnumerable(16-17)IEnumerable(19-20)IEnumerable(45-45)
ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (3)
ImmichFrame.Core/Helpers/AssetExtensionMethods.cs (2)
Task(13-16)IEnumerable(18-49)ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (3)
Task(9-21)Task(23-67)Task(69-81)ImmichFrame.Core/Helpers/CollectionExtensionMethods.cs (4)
IEnumerable(7-14)IEnumerable(16-17)IEnumerable(19-20)IEnumerable(45-45)
🔇 Additional comments (3)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (2)
9-21: LGTM! Video support in asset counting implemented correctly.The conditional logic properly includes videos in the count when
ShowVideosis enabled, aligning with the PR objectives.
23-67: LGTM! Asset retrieval with centralized filtering looks good.The refactored approach using
ApplyAccountFilterscentralizes the filtering logic and properly handles the video support feature. The conditional type restriction (lines 32-35) correctly allows both images and videos whenShowVideosis enabled.ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (1)
21-26: LGTM! Smart two-level caching strategy.The approach of caching excluded album assets separately (line 23) and then caching the filtered results (line 25) is efficient. This avoids re-fetching excluded albums on every cache miss for the main asset list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (1)
ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (1)
28-43: Extract duplicated method to shared helper.
GetExcludedAlbumAssetsis duplicated inAllAssetsPool.cs(lines 69-84). Extract this to a shared static helper inImmichFrame.Core.Helpers(e.g.,AssetHelper.GetExcludedAlbumAssets) to eliminate duplication and improve maintainability.Apply this refactor:
1. Create a new helper class:
// ImmichFrame.Core/Helpers/AssetHelper.cs namespace ImmichFrame.Core.Helpers; public static class AssetHelper { public static async Task<IEnumerable<AssetResponseDto>> GetExcludedAlbumAssets( ImmichApi immichApi, IAccountSettings accountSettings, CancellationToken ct = default) { var excludedAlbumAssets = new List<AssetResponseDto>(); foreach (var albumId in accountSettings?.ExcludedAlbums ?? new()) { var albumInfo = await immichApi.GetAlbumInfoAsync(albumId, null, null, ct); if (albumInfo.Assets != null) { excludedAlbumAssets.AddRange(albumInfo.Assets); } } return excludedAlbumAssets; } }2. Replace in CachingApiAssetsPool.cs:
- private async Task<IEnumerable<AssetResponseDto>> GetExcludedAlbumAssets(CancellationToken ct = default) - { - var excludedAlbumAssets = new List<AssetResponseDto>(); - - foreach (var albumId in accountSettings?.ExcludedAlbums ?? new()) - { - var albumInfo = await immichApi.GetAlbumInfoAsync(albumId, null, null, ct); - - if (albumInfo.Assets != null) - { - excludedAlbumAssets.AddRange(albumInfo.Assets); - } - } - - return excludedAlbumAssets; - } + private Task<IEnumerable<AssetResponseDto>> GetExcludedAlbumAssets(CancellationToken ct = default) + => AssetHelper.GetExcludedAlbumAssets(immichApi, accountSettings, ct);3. Replace in AllAssetsPool.cs similarly.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs(3 hunks)ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-09T16:12:49.488Z
Learnt from: JoeRu
Repo: immichFrame/ImmichFrame PR: 481
File: ImmichFrame.Core.Tests/Logic/Pool/ChronologicalAssetsPoolWrapperTests.cs:306-306
Timestamp: 2025-10-09T16:12:49.488Z
Learning: When testing the ChronologicalAssetsPoolWrapper in ImmichFrame.Core.Tests, use `Is.SupersetOf` rather than `Is.EquivalentTo` or `Is.EqualTo` assertions because the wrapper uses Fisher-Yates shuffle to randomize set order, making output non-deterministic between runs. The wrapper also uses a 10x fetch multiplier (capped at 1000) that may return more assets than requested, which is legitimate behavior.
Applied to files:
ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.csImmichFrame.Core/Logic/Pool/AllAssetsPool.cs
🧬 Code graph analysis (2)
ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (4)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (3)
Task(9-21)Task(23-67)Task(69-84)ImmichFrame.Core/Helpers/AssetExtensionMethods.cs (2)
Task(13-16)IEnumerable(18-49)ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs (2)
Task(8-19)AlbumAssetsPool(7-29)ImmichFrame.Core/Helpers/CollectionExtensionMethods.cs (4)
IEnumerable(7-14)IEnumerable(16-17)IEnumerable(19-20)IEnumerable(45-45)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (3)
ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (5)
Task(11-14)Task(16-19)Task(21-26)Task(28-43)Task(45-45)ImmichFrame.Core/Helpers/AssetExtensionMethods.cs (2)
Task(13-16)IEnumerable(18-49)ImmichFrame.Core/Helpers/CollectionExtensionMethods.cs (4)
IEnumerable(7-14)IEnumerable(16-17)IEnumerable(19-20)IEnumerable(45-45)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Analyze (csharp)
🔇 Additional comments (5)
ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (2)
2-2: LGTM: Correct dependency direction.The using directive correctly imports from
ImmichFrame.Core.Helpers, resolving the previously flagged upward dependency issue.
21-26: LGTM: Sound two-tier caching strategy.The separate cache keys for excluded album assets and main assets enable independent invalidation, improving cache efficiency when excluded albums change.
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (3)
2-2: LGTM!Required for
ApplyAccountFiltersextension method.
32-35: LGTM: Correct conditional video handling.The logic correctly restricts to
IMAGEtype only whenShowVideosis false, allowing both images and videos when enabled.
64-66: LGTM: Consistent filtering flow.The filtering pattern correctly retrieves excluded album assets and applies consolidated account filters, consistent with
CachingApiAssetsPool.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (1)
64-64: Consider caching excluded album assets.
GetExcludedAlbumAssetsis called on everyGetAssetsinvocation, which may be inefficient if the excluded albums list is large or API responses are slow.CachingApiAssetsPoolcaches this result (line 23 of that file), butAllAssetsPooldoes not.If
GetAssetsis called frequently, consider cachingexcludedAlbumAssetsusing the existingapiCacheinstance.Example approach:
- var excludedAlbumAssets = await AssetHelper.GetExcludedAlbumAssets(immichApi, accountSettings, ct); + var excludedAlbumAssets = await apiCache.GetOrAddAsync( + $"{nameof(AllAssetsPool)}_ExcludedAlbums", + () => AssetHelper.GetExcludedAlbumAssets(immichApi, accountSettings, ct));
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
ImmichFrame.Core/Helpers/AssetHelper.cs(1 hunks)ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs(3 hunks)ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-09T16:12:49.488Z
Learnt from: JoeRu
Repo: immichFrame/ImmichFrame PR: 481
File: ImmichFrame.Core.Tests/Logic/Pool/ChronologicalAssetsPoolWrapperTests.cs:306-306
Timestamp: 2025-10-09T16:12:49.488Z
Learning: When testing the ChronologicalAssetsPoolWrapper in ImmichFrame.Core.Tests, use `Is.SupersetOf` rather than `Is.EquivalentTo` or `Is.EqualTo` assertions because the wrapper uses Fisher-Yates shuffle to randomize set order, making output non-deterministic between runs. The wrapper also uses a 10x fetch multiplier (capped at 1000) that may return more assets than requested, which is legitimate behavior.
Applied to files:
ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.csImmichFrame.Core/Logic/Pool/AllAssetsPool.cs
🧬 Code graph analysis (3)
ImmichFrame.Core/Helpers/AssetHelper.cs (3)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (2)
Task(9-21)Task(23-67)ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (4)
Task(11-14)Task(16-19)Task(21-26)Task(28-28)ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs (2)
AlbumAssetsPool(7-29)Task(9-28)
ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (4)
ImmichFrame.Core/Helpers/AssetHelper.cs (2)
Task(9-23)AssetHelper(7-24)ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (2)
Task(9-21)Task(23-67)ImmichFrame.Core/Helpers/AssetExtensionMethods.cs (2)
Task(13-16)IEnumerable(18-49)ImmichFrame.Core/Helpers/CollectionExtensionMethods.cs (4)
IEnumerable(7-14)IEnumerable(16-17)IEnumerable(19-20)IEnumerable(45-45)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (4)
ImmichFrame.Core/Helpers/AssetHelper.cs (2)
Task(9-23)AssetHelper(7-24)ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (4)
Task(11-14)Task(16-19)Task(21-26)Task(28-28)ImmichFrame.Core/Helpers/AssetExtensionMethods.cs (2)
Task(13-16)IEnumerable(18-49)ImmichFrame.Core/Helpers/CollectionExtensionMethods.cs (4)
IEnumerable(7-14)IEnumerable(16-17)IEnumerable(19-20)IEnumerable(45-45)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Analyze (csharp)
🔇 Additional comments (4)
ImmichFrame.Core/Helpers/AssetHelper.cs (1)
7-24: LGTM! Clean extraction of excluded album logic.The helper properly centralizes excluded album asset retrieval, handles null
accountSettingsandAssetscollections safely, and supports cancellation. This eliminates the duplication flagged in previous reviews.ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (2)
11-20: GetAssetCount now respects the ShowVideos setting.The method correctly returns combined image and video counts when
ShowVideosis enabled, and image-only counts otherwise. Note that other filters (excluded albums, date ranges, rating) are not yet applied, as acknowledged by the TODO comment.
32-35: Video filtering and exclusion logic is correct.The implementation properly restricts
searchDto.TypetoIMAGEwhenShowVideosis false and correctly applies excluded album filters viaAssetHelperandApplyAccountFilters.Also applies to: 64-66
ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (1)
21-26: LGTM! Efficient caching strategy for excluded albums.Caching
excludedAlbumAssetsseparately under a dedicated key is a smart approach, as it allows independent cache invalidation. The filtering flow is clean and properly integrates the excluded assets intoApplyAccountFilters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
immichFrame.Web/src/lib/components/home-page/home-page.svelte (1)
144-152: GuardhandleDoneagainst overlapping invocations from progress bar, videoended, and controls
handleDoneis now the central transition handler, called from:
ProgressBarviaonDone={handleDone},ImageComponentviaon:ended={() => handleDone(false, false)},OverlayControlsnext/backactions.Because
handleDoneisasyncand mutatesassetBacklog,assetHistory,displayingAssets, and restarts bothimageComponentandprogressBar, concurrent or rapid-fire triggers (e.g., progress bar finishing while a video firesended, or user clicking “next” at the same time) can cause overlapping transitions, skipped assets, or inconsistent progress/video sync.Consider adding a simple reentrancy guard so only one transition runs at a time:
- const handleDone = async (previous: boolean = false, instant: boolean = false) => { + let isHandlingAssetTransition = false; + + const handleDone = async (previous: boolean = false, instant: boolean = false) => { + if (isHandlingAssetTransition) { + return; + } + isHandlingAssetTransition = true; + try { progressBar.restart(false); $instantTransition = instant; if (previous) await getPreviousAssets(); else await getNextAssets(); await tick(); await imageComponent?.play?.(); await progressBar.play(); - }; + } finally { + isHandlingAssetTransition = false; + } + };This keeps your nice wiring (restart/stop subscriptions,
OverlayControlspause/info toggles,on:ended, and progress baronDone) while preventing race conditions during navigation and playback.Also applies to: 379-382, 386-389, 429-432, 441-458, 460-468, 476-484
♻️ Duplicate comments (1)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (1)
9-21: Filter mismatch between GetAssetCount and GetAssets remains unfixed in AllAssetsPool.GetAssetCount (line 9-21) uses raw API statistics filtered only by ShowVideos, while GetAssets (line 24-69) applies ShowArchived, date ranges, and rating filters. This causes GetAssetCount to return inflated counts that don't match the actual filtered assets returned by GetAssets.
Example: With ShowArchived=false, GetAssetCount includes archived assets in raw stats, but GetAssets excludes them—counts don't match.
The inline TODO comment acknowledges this is temporary, but the discrepancy remains unresolved and can mislead callers like PooledImmichFrameLogic.GetTotalAssets().
🧹 Nitpick comments (7)
immichFrame.Web/src/lib/components/elements/image-component.svelte (1)
67-75: Consider error handling and caller feedback for playback controls.The exported
pause()andplay()functions use optional chaining, which gracefully handles unmounted components but silently fails without feedback to the caller. Additionally, errors from child component methods will propagate uncaught.Consider whether:
- Callers need to know if playback control succeeded
- Errors from child components should be caught and logged or re-thrown with context
Example with error handling:
export const pause = async () => { - await primaryImageComponent?.pause?.(); - await secondaryImageComponent?.pause?.(); + try { + await primaryImageComponent?.pause?.(); + await secondaryImageComponent?.pause?.(); + } catch (error) { + console.error('Failed to pause media:', error); + throw error; + } };ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (1)
32-68: Consider eliminating redundant filter checks.Filters are applied both server-side (via
searchDtoproperties at lines 32-61) and client-side (viaApplyAccountFiltersat line 68). Specifically:
ShowVideos: filtered viasearchDto.Type(line 34) and re-checked inApplyAccountFiltersShowArchived: filtered viasearchDto.Visibility(lines 37-44) and re-checked inApplyAccountFilters- Date ranges: filtered via
searchDto.TakenBefore/TakenAfter(lines 46-56) and re-checked inApplyAccountFiltersRating: filtered viasearchDto.Rating(lines 58-61) and re-checked inApplyAccountFiltersThe only unique client-side operations are
IsSupportedAsset()and excluded-album filtering. If the server-side API is trusted, consider creating a lighterApplyAccountFiltersvariant that skips redundant checks.ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs (1)
10-57: Legacy V1 settings correctly expose ShowVideos and PlayAudio via adaptersThe additions of
ShowVideosandPlayAudiotoServerSettingsV1, and their exposure through bothAccountSettingsV1AdapterandGeneralSettingsV1Adapter, preserve backward compatibility for the flat config while satisfying the newer interfaces. If you ever need per-accountPlayAudiodistinct from the general value in V1, that would likely warrant a new settings version, but the current approach is reasonable for legacy support.Also applies to: 69-85, 87-123
openApi/swagger.json (1)
211-275: New /api/Asset/{id}/Asset endpoint cleanly models binary image/video retrievalThe
GET /api/Asset/{id}/Assetdefinition—withassetTypereferencingAssetTypeEnumand multiple binary content types for images and videos—matches the intended asset-type-aware retrieval and the generated TSgetAssetfunction. Consider adding a short description clarifying howassetTypeis used (e.g., when it’s required vs inferred) for future readers of the API docs.immichFrame.Web/src/lib/components/home-page/home-page.svelte (3)
95-101: MakeupdateAssetPromisessynchronous (noasync) for clarity
updateAssetPromisescontains noawaitand is always called without being awaited, so marking itasyncis misleading and may confuse future maintainers about whether its completion needs to be sequenced with other work.You can simplify it as:
- async function updateAssetPromises() { + function updateAssetPromises() { for (let asset of displayingAssets) { if (!(asset.id in assetPromisesDict)) { assetPromisesDict[asset.id] = loadAsset(asset); } } // ... for (let key in assetPromisesDict) { if ( !( displayingAssets.find((item) => item.id == key) || assetBacklog.find((item) => item.id == key) ) ) { delete assetPromisesDict[key]; } } }Also applies to: 105-106, 111-120
257-263: Duration parsing andcurrentDurationderivation are reasonable; consider adding testsThe combination of:
parseAssetDurationhandlinghh:mm:ss-style strings (with,or.decimals),getAssetDurationSecondsfalling back to$configStore.intervalwhen the API duration is missing/invalid,updateCurrentDurationtaking the max per-asset duration (and falling back to interval when needed),- binding
ProgressBar’sdurationtocurrentDuration,is a solid approach for keeping the progress bar in sync with either video length or the configured image interval.
Given the subtle parsing and fallback rules, it would be valuable to add unit tests around
parseAssetDuration/getAssetDurationSecondsfor typical cases ('12','1:30','01:02:03.5', malformed values) to lock in behavior and avoid regressions if the backend duration format changes.Also applies to: 265-272, 274-298, 303-305, 312-313, 316-316, 478-478
327-331: Revoke object URLs when assets are evicted to avoid memory leaks
loadAssetcreates an object URL viaURL.createObjectURL(req.data)and stores only the string inassetPromisesDict. When entries are deleted in the cleanup loop, the URLs themselves are never revoked, so a long-running slideshow can accumulate a growing number of unreclaimed blobs.You can track URLs per asset ID and revoke them when removing from
assetPromisesDict:let assetPromisesDict: Record< string, Promise<[string, api.AssetResponseDto, api.AlbumResponseDto[]]> > = {}; +const objectUrls: Record<string, string> = {}; // ... for (let key in assetPromisesDict) { if ( !( displayingAssets.find((item) => item.id == key) || assetBacklog.find((item) => item.id == key) ) ) { - delete assetPromisesDict[key]; + const url = objectUrls[key]; + if (url) { + URL.revokeObjectURL(url); + delete objectUrls[key]; + } + delete assetPromisesDict[key]; } } // ... - return [getObjectUrl(req.data), assetResponse, album] as [ + const objectUrl = getObjectUrl(req.data); + objectUrls[assetResponse.id] = objectUrl; + return [objectUrl, assetResponse, album] as [ string, api.AssetResponseDto, api.AlbumResponseDto[] ];Optionally, you can also revoke any remaining URLs in
onDestroyto be extra safe.Also applies to: 353-357, 360-362, 111-120
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
ImmichFrame.Core/Interfaces/IServerSettings.cs(2 hunks)ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs(3 hunks)ImmichFrame.WebApi.Tests/Resources/TestV1.json(1 hunks)ImmichFrame.WebApi.Tests/Resources/TestV2.json(3 hunks)ImmichFrame.WebApi.Tests/Resources/TestV2.yml(3 hunks)ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs(4 hunks)ImmichFrame.WebApi/Models/ClientSettingsDto.cs(2 hunks)ImmichFrame.WebApi/Models/ServerSettings.cs(2 hunks)immichFrame.Web/src/lib/components/elements/image-component.svelte(6 hunks)immichFrame.Web/src/lib/components/elements/image.svelte(7 hunks)immichFrame.Web/src/lib/components/home-page/home-page.svelte(18 hunks)immichFrame.Web/src/lib/immichFrameApi.ts(3 hunks)openApi/swagger.json(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
- immichFrame.Web/src/lib/components/elements/image.svelte
- ImmichFrame.WebApi.Tests/Resources/TestV2.json
- ImmichFrame.Core/Interfaces/IServerSettings.cs
- ImmichFrame.WebApi.Tests/Resources/TestV1.json
- ImmichFrame.WebApi.Tests/Resources/TestV2.yml
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-09T16:12:49.488Z
Learnt from: JoeRu
Repo: immichFrame/ImmichFrame PR: 481
File: ImmichFrame.Core.Tests/Logic/Pool/ChronologicalAssetsPoolWrapperTests.cs:306-306
Timestamp: 2025-10-09T16:12:49.488Z
Learning: When testing the ChronologicalAssetsPoolWrapper in ImmichFrame.Core.Tests, use `Is.SupersetOf` rather than `Is.EquivalentTo` or `Is.EqualTo` assertions because the wrapper uses Fisher-Yates shuffle to randomize set order, making output non-deterministic between runs. The wrapper also uses a 10x fetch multiplier (capped at 1000) that may return more assets than requested, which is legitimate behavior.
Applied to files:
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs
🧬 Code graph analysis (4)
immichFrame.Web/src/lib/immichFrameApi.ts (2)
ImmichFrame.WebApi/Controllers/AssetController.cs (5)
ApiController(20-121)ImmichFrame(9-122)HttpGet(53-60)HttpGet(77-120)HttpGet(62-75)ImmichFrame.Core/Interfaces/IImmichFrameLogic.cs (1)
IImmichFrameLogic(6-15)
immichFrame.Web/src/lib/components/home-page/home-page.svelte (1)
ImmichFrame.Core/Interfaces/IImmichFrameLogic.cs (1)
IImmichFrameLogic(6-15)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (3)
ImmichFrame.Core/Helpers/AssetHelper.cs (2)
Task(9-23)AssetHelper(7-24)ImmichFrame.Core/Helpers/AssetExtensionMethods.cs (2)
Task(13-16)IEnumerable(18-49)ImmichFrame.Core/Helpers/CollectionExtensionMethods.cs (4)
IEnumerable(7-14)IEnumerable(16-17)IEnumerable(19-20)IEnumerable(45-45)
openApi/swagger.json (2)
ImmichFrame.WebApi/Controllers/AssetController.cs (7)
ApiController(20-121)ImmichFrame(9-122)HttpGet(53-60)HttpGet(62-75)HttpGet(77-120)HttpGet(36-42)HttpGet(44-51)ImmichFrame.Core/Interfaces/IImmichFrameLogic.cs (1)
IImmichFrameLogic(6-15)
🔇 Additional comments (16)
immichFrame.Web/src/lib/components/elements/image-component.svelte (3)
63-65: LGTM!The
handleMediaEndedfunction correctly forwards the ended event from child components to parent consumers, enabling slideshow auto-advance behavior.
32-32: LGTM! Consistent prop and event wiring across all Image instances.The
playAudioprop (defaulting tofalse) and event wiring (bind:this,on:ended) are consistently applied to all Image components in both split and default modes, enabling uniform playback control and event propagation.Also applies to: 51-51, 117-119, 135-137, 155-157
7-7: No issues found. Image component interface verified.The Image component correctly exports
pauseandplayas async functions, dispatches theendedevent with proper typing, and accepts theplayAudioprop. The type import and component wiring inimage-component.svelteare correct.ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (2)
2-2: LGTM! Dependency direction corrected.The using statement now correctly references
ImmichFrame.Core.Helpersinstead of the previously flaggedImmichFrame.WebApi.Helpers, maintaining proper layering.
64-66: LGTM! Effective caching of excluded album assets.The caching strategy appropriately reduces repeated API calls for excluded albums, and the cache key is sufficiently unique. The null-safety concern from previous reviews has been addressed in
AssetHelper.GetExcludedAlbumAssets.immichFrame.Web/src/lib/immichFrameApi.ts (2)
188-216: ClientSettingsDto.playAudio wiring looks correctThe new
playAudio?: booleanfield is consistent with the server-sideClientSettingsDto.PlayAudioand the OpenAPI schema, and its placement alongsideimageFill/layoutkeeps the client settings payload coherent.
225-236: New asset endpoints mapping (getAssets / getAsset) aligns with OpenAPI
getAssetscorrectly targetsGET /api/Assetand returnsAssetResponseDto[], and the newgetAsset(id, { clientIdentifier, assetType })usesfetchBlobagainstGET /api/Asset/{id}/Assetwith the optionalassetType: AssetTypeEnum, matching the swagger definitions and expected binary responses for images/videos. Just ensure all call sites that previously used the old list-stylegetAssethave been updated to the newgetAssetsname.Also applies to: 273-286
ImmichFrame.WebApi/Models/ClientSettingsDto.cs (1)
31-34: PlayAudio added and mapped cleanly from general settingsThe
PlayAudioproperty and its assignment inFromGeneralSettingsare consistent with the rest of the DTO mapping and with the new client-sideplayAudioflag; no issues here.Also applies to: 35-66
ImmichFrame.WebApi/Models/ServerSettings.cs (1)
54-64: New PlayAudio and ShowVideos settings are well-integratedAdding
PlayAudiotoGeneralSettingsandShowVideostoServerAccountSettings, both defaulting tofalse, fits the existing configuration model and provides a clear opt-in path for audio/video features without changing existing behavior.Also applies to: 66-82
openApi/swagger.json (3)
8-56: Renaming operationId to GetAssets avoids conflict and matches client namingChanging the
GET /api/AssetoperationId toGetAssetsclarifies its plural nature and aligns with the regenerated TypeScriptgetAssetshelper without affecting the on-the-wire API.
164-210: Deprecation of /api/Asset/{id}/Image is properly signaledMarking
GET /api/Asset/{id}/Imageas"deprecated": truewhile keeping its responses unchanged is a clean way to steer new clients toward the asset-aware endpoint without breaking existing consumers.
845-945: ClientSettingsDto schema now correctly exposes playAudioThe
playAudioboolean property in theClientSettingsDtoschema reflects the new server/client capability and aligns with both the C# DTO and the generated TypeScript type; this keeps the contract in sync across layers.immichFrame.Web/src/lib/components/home-page/home-page.svelte (4)
6-9: New imports and playback/preload state look consistentThe additions of
tick, theImageComponentInstancetype,PRELOAD_ASSETS,imageComponent,currentDuration, andassetPromisesDictintegrate cleanly with the existing state pattern and are typed appropriately for the upcoming video support and duration handling.Also applies to: 30-30, 41-42, 55-58
125-125: Asset loading and type filtering align with new asset modelUsing
api.getAssets()and then filtering toisImageAsset(asset) || isVideoAsset(asset)ensures the slideshow only handles supported visual assets while keeping the backend API general. This matches the new asset-type-aware model and should keep unsupported asset types out of the UI.Also applies to: 136-138
169-171: Split view and orientation guards correctly exclude videosUsing
isImageAsset/isVideoAssetingetNextAssets/getPreviousAssetsand short-circuitingisHorizontalfor videos ensures:
- split view only ever shows two images (never videos),
- video assets don’t go through EXIF-based orientation heuristics,
imagesState.splitis true only for pure image pairs.This is a good safety measure for layout, especially with mixed asset types.
Also applies to: 191-192, 204-205, 221-222, 226-228, 312-313
379-382: Playback wiring between stores,ImageComponent, andplayAudiolooks coherentSubscribing to
restartProgress/stopProgressto callimageComponent.play()/.pause(), bindingimageComponentviabind:this, forwardingon:ended, and passingplayAudio={$configStore.playAudio}intoImageComponentall line up well with the new video/audio features. Once the reentrancy guard aroundhandleDoneis in place, this should give predictable synchronized control over both visuals and audio.Also applies to: 386-389, 429-432, 431-431
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docker/Settings.example.yml (1)
37-52: Add ShowVideos to the Accounts section template.Per the PR objectives and your clarification in the comments, the correct configuration is
Accounts: ShowVideos: true. However, this setting is not present in the example configuration template. SinceShowVideosis a per-account setting (unlike the globalPlayAudio), it should be added to the Accounts section alongside other per-account toggles likeShowMemories,ShowFavorites, andShowArchived.Please add
ShowVideos: falseto each account entry in the Accounts section to ensure users have a complete example configuration:Accounts: - ImmichServerUrl: REQUIRED ApiKey: REQUIRED ImagesFromDate: null ShowMemories: false ShowFavorites: false ShowArchived: false + ShowVideos: false ImagesFromDays: null
🧹 Nitpick comments (9)
ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs (1)
103-131: Consider clarifying variable naming in test setup.The test logic is correct, but the variable
requestedVideoCountat line 108 may be misleading since videos are not actually requested whenShowVideosis false. The variable is used only to populate test data, possibly to verify that videos are properly filtered out even when present. Consider renaming tounusedVideoCountor adding a comment explaining that videos in test data verify filtering behavior.Example:
- var requestedVideoCount = 8; + var unusedVideoCount = 8; // Videos in test data verify they're properly filtered when ShowVideos=false var rating = 3; _mockAccountSettings.SetupGet(s => s.ShowArchived).Returns(true); _mockAccountSettings.SetupGet(s => s.Rating).Returns(3); var returnedAssets = CreateSampleImageAssets(requestedImageCount, rating: rating); - returnedAssets.AddRange(CreateSampleVideoAssets(requestedVideoCount, rating: rating)); + returnedAssets.AddRange(CreateSampleVideoAssets(unusedVideoCount, rating: rating));immichFrame.Web/src/lib/components/elements/image.svelte (3)
26-27: AlignplayAudioprop typing with parent usage and provide a default
image.svelterequiresplayAudio: booleaninPropsand destructures it without a default, whileimage-component.sveltetreatsplayAudioas optional withplayAudio = false. This mismatch can force call sites that useImagedirectly to always provideplayAudio, even if they don’t care about audio.Consider relaxing the prop and adding an explicit default for clarity and resilience:
interface Props { // ... - playAudio: boolean; + playAudio?: boolean; } let { // ... - playAudio + playAudio = false }: Props = $props();This keeps behavior unchanged for callers that already pass
playAudio, while avoiding stricter typing for any existing direct usages ofImage.Also applies to: 40-42
45-48: Video/image branching and zoom/pan disabling look correct; consider minor robustness tweaksThe
isVideo/videoElementwiring andenableZoom/enablePangating ensure that videos render via<video>without zoom/pan animations, while images keep the existing zoom/pan behavior. This is a good separation and avoids awkward Ken Burns–style transforms on video.Two small robustness/maintenance suggestions:
Guard thumbhash decoding and reuse the computed poster URL
You now compute
thumbHashToDataURL(decodeBase64(image[1].thumbhash ?? ''))both for the<video>posterand for the background<img>at the bottom of the file. Ifthumbhashis missing or malformed, this can throw in two places and is harder to adjust centrally.You can centralize and guard the computation:
const thumbhashUrl = $derived(() => { const hash = image[1].thumbhash; if (!hash) return ''; try { return thumbHashToDataURL(decodeBase64(hash)); } catch { return ''; } });Then use
thumbhashUrlin bothposter={thumbhashUrl}and the background<img src={thumbhashUrl}>.Clarify audio/autoplay expectations
muted={!playAudio}is a sensible default, but whenplayAudioistrue, autoplay may be blocked by browsers; theplay()helper already swallows the resulting promise rejection. It may be worth adding a short comment near themuted={!playAudio}line explaining that enabling audio can disable autoplay on some platforms so future maintainers understand this trade‑off.These are optional quality-of-life improvements; the current logic is functionally sound.
Also applies to: 52-53, 161-171, 190-201
137-151: Pause/play exports are safely guarded but could be generalized slightlyThe exported
pause/playhelpers correctly no-op when the current asset is not a video or thevideoElementref is not yet bound, which makes their use from parent components safe.If you expect to support other media types (e.g., audio-only assets) in future, you might consider:
- Relaxing the
isVideocheck to a more generic “has playable media element” abstraction, or- Adding a comment that these helpers are video-specific today to avoid confusion when new asset types are added.
No functional issue, just a small clarity consideration.
immichFrame.Web/src/lib/components/elements/image-component.svelte (1)
59-70: Pause/play delegation across primary/secondary images is sound; consider centralizing shared propsThe introduction of
primaryImageComponent/secondaryImageComponentand the exportedpause/playfunctions cleanly coordinate playback across split and non-split layouts. The use of optional chaining (?.pause?.(),?.play?.()) makes this safe regardless of which layout is active.One small maintainability suggestion: the
<Image>invocations repeat the same long prop list three times, now including{playAudio}andbind:showInfo. You could reduce duplication and the risk of future drift by extracting a small helper component or using a spread object for the shared props, e.g.:<!-- pseudo-code idea --> <Image {...baseImageProps} image={images[0]} bind:this={primaryImageComponent} bind:showInfo />Not required for correctness, but it will make future changes (like adding new shared props) less error-prone.
Also applies to: 101-115, 118-132, 137-151
immichFrame.Web/src/lib/components/home-page/home-page.svelte (4)
30-31: Asset preloading viaassetPromisesDictis well-structured; consider lifecycle cleanupThe new
PRELOAD_ASSETSconstant andassetPromisesDictlogic inupdateAssetPromises()do a good job of:
- Ensuring currently displayed assets and a small backlog are prefetched.
- Avoiding duplicate requests by reusing promises keyed by
asset.id.- Cleaning up entries whose assets are no longer in
displayingAssetsorassetBacklog.One improvement to consider is explicit lifecycle cleanup for the promise dictionary when the component is destroyed to avoid any lingering references:
onDestroy(() => { // existing unsubscribe logic... assetPromisesDict = {}; });This is minor in practice (since the component is long-lived), but it documents the intended lifecycle and prevents surprises if more state is added to the dictionary later.
Also applies to: 55-59, 95-121, 200-201, 231-231
125-139: Filtering to image/video assets is clear; verify behavior for any future asset types
loadAssets()now filtersassetBacklogtoisImageAsset(asset) || isVideoAsset(asset), which is correct for this PR’s video-focused scope and ensures non-displayable asset types are ignored.If the backend introduces additional playable asset types (e.g., audio-only) and you intend to support them in the slideshow, remember to extend this filter accordingly; otherwise those assets will silently never be shown.
144-161: Transition/playback coordination viahandleDoneand subscriptions is solidThe introduction of
isHandlingAssetTransitionplus thehandleDone()flow (restart progress, update assets, awaittick(), then kickimageComponent.play()andprogressBar.play()) provides a clear, serialized transition path and guards against concurrent navigation calls from the progress bar and overlay controls.Similarly, wiring the
restartProgress/stopProgresssubscriptions and the OverlayControlspause/showInfoactions toimageComponent.play()/pause()ensures the video element stays in sync with the progress bar and UI state.The only very minor tweak you might consider is awaiting the
imageComponent?.play?.()call insidehandleDonefor consistency with the overlay callbacks:await imageComponent?.play?.();Functionally this is not required, but it makes the behavior uniform across the different entry points.
Also applies to: 388-399, 449-477
266-307: Dynamic duration handling for videos is good; consider edge cases and documentationThe new
updateCurrentDuration,getAssetDurationSeconds, andparseAssetDurationfunctions, together withduration={currentDuration}onProgressBar, nicely align the progress bar duration with asset metadata:
- For videos, valid
asset.durationstrings (e.g.,HH:MM:SS,MM:SS, with optional fractional seconds and,decimals) are parsed; invalid or non-positive values fall back to$configStore.interval.- For images, you consistently use
$configStore.interval.- For split view, you take the max of the durations for the two assets, which keeps the pair on screen long enough for the longest member.
Two optional hardening/documentation suggestions:
Guard against extremely large or tiny durations
If misconfigured or malformed durations were ever to be very large (hours) or very small (< 1 second), the UI might behave oddly. You could clamp
currentDurationwithin reasonable bounds (e.g., min 1s, max some configured ceiling).Document expected duration format
A brief comment on
parseAssetDuration()describing the accepted formats (HH:MM:SS[.fraction],MM:SS[.fraction],SS[.fraction], allowing,as decimal separator) will help keep backend and frontend in sync if new formats are introduced.The core logic is solid; these are just safety/documentation improvements.
Also applies to: 312-327, 484-487
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs(4 hunks)docker/Settings.example.json(1 hunks)docker/Settings.example.yml(1 hunks)immichFrame.Web/src/lib/components/elements/image-component.svelte(6 hunks)immichFrame.Web/src/lib/components/elements/image.svelte(7 hunks)immichFrame.Web/src/lib/components/home-page/home-page.svelte(17 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-09T16:12:49.488Z
Learnt from: JoeRu
Repo: immichFrame/ImmichFrame PR: 481
File: ImmichFrame.Core.Tests/Logic/Pool/ChronologicalAssetsPoolWrapperTests.cs:306-306
Timestamp: 2025-10-09T16:12:49.488Z
Learning: When testing the ChronologicalAssetsPoolWrapper in ImmichFrame.Core.Tests, use `Is.SupersetOf` rather than `Is.EquivalentTo` or `Is.EqualTo` assertions because the wrapper uses Fisher-Yates shuffle to randomize set order, making output non-deterministic between runs. The wrapper also uses a 10x fetch multiplier (capped at 1000) that may return more assets than requested, which is legitimate behavior.
Applied to files:
ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs
🧬 Code graph analysis (2)
docker/Settings.example.json (2)
ImmichFrame.Core/Models/Weather.cs (2)
Weather(5-13)ImmichFrame(3-14)ImmichFrame.Core/Helpers/WeatherExtensions.cs (1)
ImmichFrame(5-24)
ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs (2)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (4)
Task(9-21)Task(23-69)AllAssetsPool(7-71)AllAssetsPool(6-78)ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (4)
Task(11-14)Task(16-19)Task(21-26)Task(28-28)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: publish_images
- GitHub Check: Analyze (csharp)
🔇 Additional comments (14)
docker/Settings.example.yml (1)
35-35: ✓ PlayAudio setting correctly added to General section.The addition follows the established conventions: appropriate placement, consistent naming with existing toggles, and safe default value (false).
docker/Settings.example.json (3)
33-33: Formatting consistency fix applied.The WeatherIconUrl value is now properly formatted and aligns with its usage in
WeatherExtensions.cs, whereIconIdis interpolated into the URL template.
40-60: Verify ShowVideos setting is present in Accounts section.Per the PR objectives and user clarification in comments, the configuration key
ShowVideosshould be documented underAccounts(e.g.,Accounts: ShowVideos: true). The provided excerpt shows only a partial Accounts structure; please confirm thatShowVideosis included in the full Accounts configuration example.If
ShowVideosis not currently in the Accounts section ofSettings.example.json, it should be added alongside other account-level filters (ShowMemories, ShowFavorites, ShowArchived) for completeness and user discoverability.
37-37: All PlayAudio integration verified and complete.The setting is properly wired end-to-end:
- Server settings — Defined in IServerSettings interface, ServerSettings model, and ServerSettingsV1 adapter with default
false- ClientSettingsDto mapping — PlayAudio properly mapped at line 62:
dto.PlayAudio = generalSettings.PlayAudio- Frontend consumption — Wired through all layers:
- Config API endpoint (ConfigController.GetConfig) returns mapped ClientSettingsDto
- Frontend config.store wraps ClientSettingsDto
- home-page.svelte passes
$configStore.playAudioto image component- image.svelte uses it to control audio:
muted={!playAudio}The feature flows from docker/Settings.example.json → backend models → API → frontend store → UI component where it actively controls audio playback muting.
ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs (6)
45-49: LGTM! Cache mock properly supports async asset enumeration.The new
GetOrAddAsyncsetup forIEnumerable<AssetResponseDto>correctly handles the factory pattern and aligns with the production code's use ofAssetHelper.GetExcludedAlbumAssets.
52-67: LGTM! Helper methods properly support asset type and rating.The helper methods correctly create typed assets with optional ratings, enabling comprehensive testing of the new video support and rating filters. The structure with a base method and type-specific wrappers promotes code reuse.
69-101: LGTM! Asset counting tests properly verify video support toggle.Both tests correctly validate that
GetAssetCountreturns only images whenShowVideosis false (default) and includes videos when enabled, matching the production implementation.
133-162: LGTM! Comprehensive test for mixed image and video retrieval.The test correctly verifies that when
ShowVideosis enabled, the search DTO hasType=null(no type filter) and returns both images and videos. The assertions properly validate the total count and search parameters.
164-177: LGTM! Date filter test correctly validates ImagesFromDays.The test properly verifies that
ImagesFromDaysis converted to aTakenAfterdate filter with the correct calculation.
179-203: LGTM! Excluded albums test properly validates filtering logic.The test correctly verifies that assets from excluded albums are filtered out. Good use of
CreateSampleImageAssetsat line 183 to ensure consistent asset typing.immichFrame.Web/src/lib/components/elements/image.svelte (1)
252-271: Updated pan/zoom keyframes look consistent with new scale variablesThe changes to the
panandzoom-pankeyframes to includescale(var(--start-scale))/scale(var(--end-scale))keep the scaling behavior consistent with the standalonezoomanimation, and they respect theenableZoom/enablePanclass gating you added above.No issues spotted here; the transforms compose correctly and should preserve the intended visual effects.
immichFrame.Web/src/lib/components/elements/image-component.svelte (1)
32-33:playAudiodefaulting and wiring through toImageare consistent
playAudiois optional here with a default offalse, and it is threaded through to allImageusages. This ensures the top-level config can control video audio behavior uniformly, without breaking existing callers.Looks good as-is; no changes needed.
Also applies to: 50-52
immichFrame.Web/src/lib/components/home-page/home-page.svelte (2)
174-187: Split-view and orientation logic correctly exclude videos from split layoutUsing
isImageAssetchecks in bothgetNextAssets()andgetPreviousAssets()to decide whether to take two assets for split-view, and treatingisVideoAsset(asset)as immediately non-horizontal inisHorizontal(), ensures:
- Split layout is only used when both assets are images.
- Videos are presented in a single-column layout, which avoids awkward side-by-side video/image combinations.
This is a sensible constraint for a first iteration of video support and matches typical expectations for a slideshow frame.
Also applies to: 209-232, 234-247
41-43: BindingimageComponentand forwardingplayAudiofrom config is coherentThe new
imageComponentref (bind:this={imageComponent}) combined withplayAudio={$configStore.playAudio}cleanly propagates the configuration down to the media layer and enables programmatic control from the home page.This wiring is consistent with the new API surface introduced in
image-component.svelteandimage.svelte. No issues here.Also applies to: 421-440
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
immichFrame.Web/src/lib/components/home-page/home-page.svelte (1)
84-87: AlignprovideClose()with overlayshowInfotoggling to resume video playback
OverlayControls.showInfoand thepausehandler both pause/resume both theimageComponentand theProgressBar. In contrast,provideClose()(used viasetContext('close', provideClose)) only resumes theProgressBar:async function provideClose() { infoVisible = false; await progressBar.play(); }If an info overlay triggers
provideClose()after having paused media, the bar will resume but the video (and audio) may remain paused, which is a subtle UX inconsistency.You can make this consistent with the other paths by also resuming the
imageComponent:async function provideClose() { infoVisible = false; - await progressBar.play(); + await imageComponent?.play?.(); + await progressBar.play(); }This keeps all “close/hide info” flows aligned: whenever the info UI is dismissed, both progress and media playback resume together.
Also applies to: 452-455, 474-492
🧹 Nitpick comments (1)
immichFrame.Web/src/lib/components/home-page/home-page.svelte (1)
95-122: Object URL lifecycle is now handled; consider hardening against rejected asset promisesYou’ve correctly addressed the earlier memory‑leak concern by:
- Ensuring every displayed/preloaded asset has an entry in
assetPromisesDict.- Revoking object URLs when entries fall out of both
displayingAssetsandassetBackloginupdateAssetPromises().- Revoking remaining URLs in
onDestroyand clearingassetPromisesDict.One edge case: if
loadAsset()ever rejects (network/transport error), the stored promise will reject. BothupdateAssetPromises()andonDestroy()currentlyawaitthose promises without atry/catch, which means:
- A single rejected promise can abort the cleanup loop.
Promise.all(revokes)inonDestroycan surface as an unhandled rejection.You can make cleanup more robust by swallowing those rejections while still deleting the entry, e.g.:
- for (let key in assetPromisesDict) { + for (let key in assetPromisesDict) { if ( !( displayingAssets.find((item) => item.id == key) || assetBacklog.find((item) => item.id == key) ) ) { - revokeObjectUrl((await assetPromisesDict[key])[0]); - delete assetPromisesDict[key]; + try { + const [url] = await assetPromisesDict[key]; + revokeObjectUrl(url); + } catch (err) { + console.warn('Failed to resolve asset during cleanup:', err); + } finally { + delete assetPromisesDict[key]; + } } }And similarly for destroy:
- const revokes = Object.values(assetPromisesDict).map(async (p) => - revokeObjectUrl((await p)[0]) - ); - await Promise.all(revokes); + const revokes = Object.values(assetPromisesDict).map(async (p) => { + try { + const [url] = await p; + revokeObjectUrl(url); + } catch (err) { + console.warn('Failed to resolve asset during destroy cleanup:', err); + } + }); + await Promise.allSettled(revokes); assetPromisesDict = {};This keeps the new URL lifecycle behavior while avoiding cleanup being derailed by a transient fetch failure.
Also applies to: 337-343, 350-363, 370-380, 419-433
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
immichFrame.Web/src/lib/components/home-page/home-page.svelte(18 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Analyze (csharp)
🔇 Additional comments (8)
immichFrame.Web/src/lib/components/home-page/home-page.svelte (8)
6-6: Imports, type-safeimageComponentbinding, and asset state wiring look consistentUsing
tickandsetContextalongsideonMount/onDestroy, the separateImageComponentInstancetype forbind:this, and theassetPromisesDict/currentDurationstate wiring all line up with how the rest of the file uses them (play/pause calls, duration updates, and promise lookups). I don’t see correctness issues here; this sets up the later logic cleanly.Also applies to: 9-9, 17-18, 30-30, 41-43, 55-58
126-127: Type-aware filtering inloadAssetsis aligned with image/video supportUsing
getAssets()once and then filtering toisImageAsset(asset) || isVideoAsset(asset)before populatingassetBacklogmatches the rest of the code’s expectations (splitview and orientation logic assume only images, while video handling is done viaisVideoAsset). This is a straightforward and correct way to scope what the front-end rotates through.Also applies to: 137-139
145-161: Re-entrancy guard around asset transitions is solid and avoids racey navigationThe
isHandlingAssetTransitionflag combined with thetry/finallyinhandleDone()is a good pattern here. It prevents overlapping calls from the ProgressBaronDoneand the overlay controls (next/back) from interleavinggetNextAssets()/getPreviousAssets()andupdateAssetPromises(), while still guaranteeing the flag is reset even if something throws. Theawait tick()beforeimageComponent?.play?.()also ensures the new asset DOM is ready before resuming playback and progress.Also applies to: 465-472, 499-507
175-185: Restricting splitview to images and treating videos as non-horizontal is coherentThe combination of:
isImageAsset(assetBacklog[0/1])andisImageAsset(assetHistory[...])checks before entering the splitview branch, andsplit: assets.length == 2 && assets.every(isImageAsset)inloadImages, andisHorizontal()early-returningfalseforisVideoAsset(asset)ensures videos never participate in splitview layout or orientation heuristics. That avoids trying to fit videos into layouts designed around EXIF dimensions, which seems like the right tradeoff for this PR.
Also applies to: 200-203, 210-233, 235-247, 322-323
267-308: Duration helpers provide a robust basis for progress timing across images and videos
updateCurrentDuration()+getAssetDurationSeconds()andparseAssetDuration()give you:
- Per-asset durations for videos based on
asset.duration, with a safe fallback to$configStore.interval.- Uniform interval-based durations for images.
- A max-of-durations policy for multi-asset displays.
- A clean reset to the configured fallback when
loadImagesfails.Binding
currentDurationinto<ProgressBar duration={currentDuration} />and updating it on every transition should address the “image after video displays too briefly” behavior without complicating the rest of the flow. The parsing logic (colon-separated or single-value with comma/decimal) also looks defensive enough for typical metadata formats.Also applies to: 313-314, 326-327, 499-502
337-343:loadAssetintegration withassetTypeand object URLs matches the new APIPassing
assetType: assetResponse.typeintoapi.getAssetand wrapping the blob withgetObjectUrl(req.data)aligns this component with the asset-type-aware backend changes. The album/people enrichment and the[url, assetResponse, album]tuple structure are consistent with howloadImagesconsumes these promises. No issues spotted here beyond the general cleanup concerns already mentioned for object URLs.Also applies to: 350-363, 370-371
397-408: Restart/stop subscriptions correctly keep playback and progress in syncThe
restartProgressandstopProgresssubscriptions now not only manipulate theProgressBarbut also callimageComponent?.play?.()/pause?.(). That keeps video (and any timed image animations) aligned with the bar state. Unsubscribing in the destroy hook completes the lifecycle; this looks well-wired.Also applies to: 400-401, 407-408
452-455:playAudioprop threading intoImageComponentPassing
playAudio={$configStore.playAudio}down toImageComponentis the right place to hook audio-enable config into the media layer; it keeps this component focused on layout/selection while the child handles the actual playback semantics.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
immichFrame.Web/src/lib/components/home-page/home-page.svelte (1)
344-375: Empty URL from failed API requests bypasses error handling, causing silent failures.The
loadAssetfunction returns['', assetResponse, []]when API requests fail (line 357) without throwing an exception. This causes:
loadImagescatch block never executes (no exception thrown)imagesState.errorremainsfalseandimagesState.loadedremainstrueImageComponentrenders withsrc=""instead of showing the error state- User sees blank images with no indication of failure
Both split view and normal modes are affected since both destructure
image[0]as the src attribute.Recommended fix: Throw an error in
loadAssetwhen the request fails:if (req.status != 200 || ($configStore.showAlbumName && album == null)) { - return ['', assetResponse, []] as [string, api.AssetResponseDto, api.AlbumResponseDto[]]; + throw new Error(`Failed to load asset ${assetResponse.id}: status ${req.status}`); }This ensures the exception propagates to
loadImagescatch block (line 334), which correctly setserror: trueand displays error feedback to the user.
🧹 Nitpick comments (2)
immichFrame.Web/src/lib/components/elements/image.svelte (1)
148-162: Consider narrowing the error handling in play().The
play()function catches all errors with a generic catch block and silently ignores them. While this is appropriate for handling autoplay blocking, it could also mask legitimate errors such as network failures or corrupted video files. Consider checking the error type or logging it for debugging.export const play = async () => { if (isVideo && videoElement) { try { await videoElement.play(); - } catch { - // Autoplay might be blocked; ignore. + } catch (error) { + // Autoplay might be blocked by browser policy; ignore NotAllowedError + if (error instanceof DOMException && error.name === 'NotAllowedError') { + // Expected when autoplay is blocked + } else { + console.warn('Video playback failed:', error); + } } } };immichFrame.Web/src/lib/components/home-page/home-page.svelte (1)
242-254: Confusing function name:isHorizontalchecks for portrait orientation.The function name suggests it checks if an image is horizontally oriented (landscape), but the logic
imageHeight > imageWidth(line 253) actually checks for portrait orientation (taller than wide).While the logic appears correct for split view (which displays portrait images side by side), the misleading name reduces code clarity.
Consider renaming to better reflect the actual check:
-function isHorizontal(asset: api.AssetResponseDto) { +function isPortraitOrientation(asset: api.AssetResponseDto) { if (isVideoAsset(asset)) { return false; } const isFlipped = (orientation: number) => [5, 6, 7, 8].includes(orientation); let imageHeight = asset.exifInfo?.exifImageHeight ?? 0; let imageWidth = asset.exifInfo?.exifImageWidth ?? 0; if (isFlipped(Number(asset.exifInfo?.orientation ?? 0))) { [imageHeight, imageWidth] = [imageWidth, imageHeight]; } - return imageHeight > imageWidth; // or imageHeight > imageWidth * 1.25; + return imageHeight > imageWidth; // Portrait: taller than wide }Then update all call sites (lines 188, 189, 223, 224).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
immichFrame.Web/src/lib/components/elements/image.svelte(8 hunks)immichFrame.Web/src/lib/components/home-page/home-page.svelte(19 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Analyze (csharp)
🔇 Additional comments (14)
immichFrame.Web/src/lib/components/elements/image.svelte (4)
175-176: LGTM: Correct conditional origin calculation.The origin calculations correctly exclude video assets from face-based centering, since zoom and pan effects are disabled for videos. This avoids unnecessary computation and maintains consistent behavior.
262-268: LGTM: Improved transform readability.The keyframe transform declarations have been reformatted for better readability by splitting onto multiple lines. No functional changes.
Also applies to: 273-279
44-44: LGTM: Correct reactive derivations.The
isVideo,enableZoom, andenablePanvalues correctly use$derivedto reactively compute based on the asset type and configuration props. The logic to disable zoom and pan for video assets is appropriate.Also applies to: 51-52
7-7: Given the web search results about Svelte 5 runes, I now have enough information to provide a comprehensive rewritten review comment. The search confirms that $derived should be used to compute values that depend on reactive state and automatically updates whenever its dependencies change.However, I need to verify the actual implementation in the file to be certain about the specific issue. Let me generate a final rewritten comment based on what we've verified so far.
Ensure the
isVideoAssetimport resolves correctly and is used consistently.The import of
isVideoAssetfrom'$lib/constants/asset-type'is correctly implemented. The function checks if the asset type is VIDEO and is properly exported. The usage patterns in the codebase are consistent.However, verify that the overall implementation with the video element follows best practices. Modern browsers require the muted attribute together with the autoplay option to allow autoplay video. Ensure that the
mutedattribute binding is properly connected to theplayAudioprop and that the video element has appropriate error handling and cleanup logic.immichFrame.Web/src/lib/components/home-page/home-page.svelte (10)
6-6: LGTM! Import additions support video playback integration.The new imports (
tick,ImageComponentInstance, asset type guards) and thePRELOAD_ASSETSrename correctly reflect the shift from image-only to multi-asset-type handling.Also applies to: 9-9, 17-17, 30-30
41-42: LGTM! State variables correctly support video playback.The
imageComponentbinding andcurrentDurationstate enable lifecycle coordination, and theassetPromisesDictrename accurately reflects the expanded scope.Also applies to: 55-58
96-129: LGTM! Object URL cleanup correctly implemented.The
updateAssetPromisesfunction now properly revokes object URLs before deleting entries fromassetPromisesDict. The try-catch-finally pattern ensures cleanup happens even when promise resolution fails.
152-169: LGTM! Transition guard prevents race conditions.The
isHandlingAssetTransitionflag with try-finally ensures only one transition happens at a time and the guard is always reset. Thetick()call correctly synchronizes DOM updates before playing.
171-240: LGTM! Split view correctly restricted to image pairs.The
isImageAssetguards in bothgetNextAssetsandgetPreviousAssetsensure that split view is only activated for pairs of images, correctly excluding videos.
274-315: LGTM! Duration parsing handles edge cases correctly.The
parseAssetDurationfunction correctly:
- Parses HH:MM:SS format by processing parts right-to-left
- Handles European decimal notation (comma → dot)
- Returns 0 for invalid input (empty strings, non-numeric values, missing parts)
The
updateCurrentDurationlogic usingMath.maxensures all displayed assets share the longest duration, which is appropriate for synchronized playback.
377-387: LGTM! Object URL helpers correctly implemented.The
getObjectUrlandrevokeObjectUrlhelpers appropriately wrap the browser APIs with error handling, ensuring graceful degradation if revocation fails.
389-445: LGTM! Lifecycle hooks correctly manage resources.The
onMountsubscriptions coordinateimageComponentplayback with progress bar state, and theonDestroycleanup comprehensively revokes all object URLs usingPromise.allSettled, ensuring no memory leaks even on errors.
448-522: LGTM! Template bindings correctly integrate video playback.The template correctly:
- Binds
imageComponentfor lifecycle management- Passes
playAudioconfiguration- Coordinates play/pause across overlay controls
- Uses dynamic
currentDurationfor the progress bar
131-150: Code structure is correct; no issues found.The
getAssets()API call is properly implemented with optional parameters, the return type correctly providesAssetResponseDto[], and the filter functions are correctly implemented:
isImageAssetchecksasset.type === 0(AssetType.IMAGE)isVideoAssetchecksasset.type === 1(AssetType.VIDEO)The filtering logic in
home-page.sveltecorrectly restricts to images and videos. Whether the backend returns video assets depends on backend configuration, not the client code—which is properly structured to handle them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (3)
immichFrame.Web/src/lib/components/elements/asset-component.svelte (1)
3-3: Remove unused import.The
createEventDispatcherimport is not used anywhere in this component.Apply this diff to remove the unused import:
- import { createEventDispatcher } from 'svelte';immichFrame.Web/src/lib/components/elements/asset.svelte (1)
211-224: Consider video looping and verify autoplay with audio behavior.A few observations:
Autoplay with audio: When
playAudio={true}, the video is unmuted. Most browsers block autoplay with audio unless the user has interacted with the page. The try-catch in theplay()method handles this, but users may experience videos not autoplaying when audio is enabled.Missing loop attribute: Videos currently play once and stop. Consider adding a
loopattribute if continuous playback is desired during the display interval.Poster fallback: When
thumbhashUrlis empty, the poster attribute will be set to an empty string, which is safe but means no placeholder will show while the video loads.Based on the PR comments, user "kdy1968" reported "videos run ~3–5 minutes then stop." This aligns with the missing
loopattribute. User "3rob3" suggested a configuration option to limit playback duration. Consider:
- Adding a
loopattribute to the video element for continuous playback- Or adding a configuration option to control loop behavior and playback duration limits
Do you want me to open an issue to track these enhancements?
immichFrame.Web/src/lib/components/home-page/home-page.svelte (1)
291-315: Consider simplifying duration parsing logic.The duration parsing works correctly but could be more readable. The while loop with
pop()and multiplier accumulation is a bit indirect.Consider this alternative for clarity:
function parseAssetDuration(duration?: string | null) { if (!duration) { return 0; } - const parts = duration.split(':').map((value) => value.trim()); - if (!parts.length) { + const parts = duration.split(':').map((value) => value.trim().replace(',', '.')); + if (parts.length === 0 || parts.length > 3) { return 0; } - let total = 0; - let multiplier = 1; - while (parts.length) { - const value = parts.pop(); - if (!value) { - continue; - } - const normalized = value.replace(',', '.'); - const numeric = parseFloat(normalized); - if (Number.isNaN(numeric)) { - return 0; - } - total += numeric * multiplier; - multiplier *= 60; + // Parse as [hours:]minutes:seconds + const multipliers = [3600, 60, 1]; // hours, minutes, seconds + const offset = multipliers.length - parts.length; + + let total = 0; + for (let i = 0; i < parts.length; i++) { + const numeric = parseFloat(parts[i]); + if (Number.isNaN(numeric)) { + return 0; + } + total += numeric * multipliers[offset + i]; } return total; }This makes the parsing direction explicit and avoids the
pop()loop.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
immichFrame.Web/src/lib/components/elements/asset-component.svelte(9 hunks)immichFrame.Web/src/lib/components/elements/asset.svelte(7 hunks)immichFrame.Web/src/lib/components/home-page/home-page.svelte(19 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Analyze (csharp)
🔇 Additional comments (9)
immichFrame.Web/src/lib/components/elements/asset-component.svelte (1)
59-70: LGTM! Playback control delegation is well-structured.The pause/play methods properly delegate to child asset components with safe optional chaining, enabling coordinated playback control across the component hierarchy.
immichFrame.Web/src/lib/components/elements/asset.svelte (3)
46-56: LGTM! Proper video cleanup on unmount.The effect correctly pauses the video and clears the src to release resources when the asset changes or the component unmounts.
64-74: LGTM! Safe thumbhash decoding with fallback.The try-catch properly handles invalid or missing thumbhash data, returning an empty string as a safe fallback.
158-172: LGTM! Autoplay restriction handling is correct.The play method correctly wraps
videoElement.play()in a try-catch to handle cases where autoplay might be blocked by the browser, preventing unhandled promise rejections.immichFrame.Web/src/lib/components/home-page/home-page.svelte (5)
119-126: ✅ Object URL leak fixed!This properly addresses the past review comment about object URL leaks. The cleanup now correctly awaits the promise, extracts the URL, and revokes it before deleting the dictionary entry.
152-169: LGTM! Race condition guard prevents concurrent transitions.The
isHandlingAssetTransitionflag properly serializes asset transitions, and the try-finally ensures the flag is always reset even if an error occurs. The use oftick()correctly waits for the UI to update before starting playback.
426-445: ✅ Object URL cleanup in onDestroy properly implemented!This correctly addresses the past review comment. The
onDestroyhandler now properly revokes all remaining object URLs usingPromise.allSettledto ensure all cleanup attempts complete, even if some fail. The error handling is appropriate.
274-289: Duration calculation correctly handles videos and images.The logic properly:
- Extracts duration from video metadata
- Falls back to configured interval for images or when video duration is unavailable
- Takes the maximum duration when multiple assets are displayed
344-359: Batch-level error handling prevents partial failure recovery; error details are lost.Verification confirms the error handling paths exist but have design limitations:
Partial failures block the batch: If any asset fails in
pickAssets, the entire catch block triggers (line 333), returning{error: true, assets: [], ...}. All successfully loaded assets are discarded, not displayed.Error details are lost:
loadAssetthrows descriptive errors with asset ID and status (line 358), butpickAssets' generic catch swallows these details. Users only see a generic error state; they don't know which asset failed or why.The code functions without crashing, but consider:
- Collecting per-asset results instead of batch-level catch to allow partial display
- Propagating specific error details to the UI error handler
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (6)
immichFrame.Web/src/lib/components/elements/asset-component.svelte (2)
5-7: Prefer plainletforbind:thiscomponent refs instead of$state
primaryAssetComponentandsecondaryAssetComponentare only used asbind:thisrefs and for calling instance exports (pause/play). In Svelte 5 runes mode,bind:thisis intended to target a normal variable (the docs showlet componentInstance: MyComponent; <MyComponent bind:this={componentInstance} />), and all instance exports are then available on that object. Wrapping these refs in$stateadds an unnecessary proxy layer and can make the typing/behavior of the component instance less clear, while not providing any reactive benefit here. Based on Svelte docs.Consider simplifying to something like:
-import type AssetComponent from './asset.svelte'; +import type AssetComponent from './asset.svelte'; - let primaryAssetComponent = $state<AssetComponent | undefined>(undefined); - let secondaryAssetComponent = $state<AssetComponent | undefined>(undefined); + let primaryAssetComponent: AssetComponent | undefined; + let secondaryAssetComponent: AssetComponent | undefined; export const pause = async () => { await primaryAssetComponent?.pause?.(); await secondaryAssetComponent?.pause?.(); }; export const play = async () => { await primaryAssetComponent?.play?.(); await secondaryAssetComponent?.play?.(); };The rest of the script changes (asset tuple typing,
playAudioprop/default, and thepause/playdelegation) look consistent with the new asset/video API.Also applies to: 16-17, 31-32, 35-36, 49-51, 58-69
91-154: Keying and split-mode assumptions aroundassetscould be tightenedThe template wiring to
<Asset>(includingasset={assets[0|1]}, forwardingplayAudio, and bindingshowInfoandbind:this) looks coherent, but there are a couple of details worth tightening:
{#key assets}relies on the array’s reference identity. If upstream logic ever mutates the array in place rather than replacing it, the key won’t change and transitions may not rerun as expected. Keying on something stable like the primary asset’s ID (or a slideshow sequence ID) would be more robust.- In the
splitbranch,assets[1]is used without a length check. If the calling code ever passes fewer than two items whilesplitis true, this will renderundefinedinto<Asset>. If that contract isn’t guaranteed elsewhere, consider guarding or asserting on the array length here.- When
splitis true,playAudiois forwarded to both<Asset>instances. If it’s possible for both sides to be videos and you don’t want dual audio, you may want to gate audio to a single side (e.g., only the primary) or by asset type.None of these are blockers, but clarifying these assumptions now will make the slideshow behavior easier to reason about as video support evolves.
immichFrame.Web/src/lib/components/home-page/home-page.svelte (4)
19-21: Align album tuple typing with possiblenullvalues
loadAsset()initializesalbumasapi.AlbumResponseDto[] | nulland returns it (via a cast) asapi.AlbumResponseDto[], whileAssetsState.assetsandassetPromisesDictalso declare the third tuple element asapi.AlbumResponseDto[]. At runtime this value isnullwhen$configStore.showAlbumNameis false, so the current types don’t match actual values and may hide bugs in consumers that assume a non‑null array.Consider either:
- Allowing
nullin the tuple type, e.g.api.AlbumResponseDto[] | null, or- Always returning an empty array
[]when album names are not requested.This would remove the need for the
as [...]cast and keep the types honest.Also applies to: 55-58, 342-347, 355-372
152-169: Transition guard and play/pause coordination are sensible; verify ProgressBar semanticsThe new
isHandlingAssetTransitionguard inhandleDone()plus the sequencing:
progressBar.restart(false)await getNextAssets()/getPreviousAssets()(which updatescurrentDurationviapickAssets)await tick()await assetComponent?.play?.();progressBar.play();should prevent overlapping transitions and keep playback roughly in sync. The restart in step 1 assumes
ProgressBarreads the effectivedurationonplay()rather than atrestart(), which seems intended but depends on its implementation.If
ProgressBarinstead latchesdurationinsiderestart(), you may want to move therestart(false)call to aftergetNextAssets()/getPreviousAssets()so it sees the updatedcurrentDuration.Also applies to: 402-407, 409-414, 474-483
243-245: ConfirmisHorizontalcondition vs. naming
isHorizontal()returnsfalsefor videos (good for splitview) but then treats an asset as “horizontal” whenassetHeight > assetWidth, which is more characteristic of a portrait/vertical image. Given it’s used to gate splitview layout, this naming/condition pair is a bit surprising.Please double‑check whether the intent is:
- “portrait‑friendly” assets (height > width), in which case renaming to something like
isPortraitwould be clearer, or- truly horizontal/landscape assets, in which case the condition likely wants
assetWidth > assetHeight(or the commented 1.25 ratio).Marking as a semantic check rather than a required change.
Also applies to: 248-249, 251-251, 253-253
274-280: Duration handling is sensible; clarify intended semantics for long videos
updateCurrentDuration()/getAssetDurationSeconds()/parseAssetDuration()together:
- Use parsed video durations when available.
- Fall back to
$configStore.interval ?? 20for images and invalid/absent video durations.- Take the max duration across assets in splitview.
- Bind
currentDurationinto<ProgressBar duration={currentDuration} />.This is a good, predictable strategy. One UX detail to confirm: currently, videos longer than the configured interval will play for their full duration. If the intended behavior is “interval applies only to images”, this is perfect; if the expectation is “videos should not exceed the interval” or “should be capped at some max video duration”, you might want to clamp in
getAssetDurationSeconds()(e.g.,Math.min(parsed, fallback)or a dedicatedmaxVideoDurationconfig).No functional issue, just ensuring the behavior matches the product intent.
Also applies to: 283-289, 291-313, 315-321, 324-324, 327-327, 331-331, 333-333, 511-511
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
immichFrame.Web/src/lib/components/elements/asset-component.svelte(9 hunks)immichFrame.Web/src/lib/components/home-page/home-page.svelte(19 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Analyze (csharp)
🔇 Additional comments (1)
immichFrame.Web/src/lib/components/home-page/home-page.svelte (1)
96-102: Object URL lifecycle & cleanup look robustThe combination of
assetPromisesDictpreloading, cleanup inupdateAssetPromises()(revoking URLs for assets no longer indisplayingAssets/assetBacklog), and the final sweep inonDestroycloses the previously noted object URL leak and should keep long‑running frames from accumulating Blob URLs. The try/catch around both cleanup paths also avoids unhandled rejections whenloadAsset()fails.No changes requested here; this is a solid improvement.
Also applies to: 106-109, 112-129, 368-385, 424-443
| "ImageZoom": true, | ||
| "ImagePan": false, | ||
| "ImageFill": false, | ||
| "PlayAudio": false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you forgot ShowVideos?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both should also be added to example.env
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
correct
| ImageZoom: true | ||
| ImagePan: false | ||
| ImageFill: false | ||
| PlayAudio: false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same, I think you forgot ShowVideos?
Closes #183
Summary by CodeRabbit
New Features
Refactor