feat(video): link and stream Immich videos in the journey gallery

Immich timeline and album listings no longer filter out videos; each asset now carries its media type, which the provider picker forwards when linking. A linked video streams through Immich's transcoded /video/playback endpoint, and the asset proxy forwards the viewer's Range header (and passes 206/Content-Range back) so the player can seek. Synology video stays excluded until its stream API is verified.

Adds media_type/media_types to the provider-photos request contract.
This commit is contained in:
Maurice
2026-06-30 11:05:07 +02:00
committed by Maurice
parent 8713443665
commit 1abc9b2bc7
13 changed files with 80 additions and 45 deletions
@@ -164,7 +164,7 @@ describe('JourneyController', () => {
it('provider-photos batch passes the passphrase through when present', () => {
const addProviderPhoto = vi.fn().mockReturnValue({ id: 1 });
new JourneyController(svc({ addProviderPhoto } as Partial<JourneyService>)).providerPhotos(user, '3', { provider: 'immich', asset_ids: ['a'], caption: 'cap', passphrase: 'secret' });
expect(addProviderPhoto).toHaveBeenCalledWith(3, 1, 'immich', 'a', 'cap', 'secret');
expect(addProviderPhoto).toHaveBeenCalledWith(3, 1, 'immich', 'a', 'cap', 'secret', 'image');
// single-photo success path
expect(new JourneyController(svc({ addProviderPhoto: vi.fn().mockReturnValue({ id: 2 }) } as Partial<JourneyService>)).providerPhotos(user, '3', { provider: 'immich', asset_id: 'a' })).toEqual({ id: 2 });
});
@@ -191,7 +191,7 @@ describe('JourneyController', () => {
const addProviderPhotoToGallery = vi.fn().mockReturnValue({ id: 1 });
const batch = new JourneyController(svc({ addProviderPhotoToGallery } as Partial<JourneyService>));
expect(batch.galleryProviderPhotos(user, '9', { provider: 'immich', asset_ids: ['a', 'b'], passphrase: 'pw' })).toEqual({ photos: [{ id: 1 }, { id: 1 }], added: 2 });
expect(addProviderPhotoToGallery).toHaveBeenCalledWith(9, 1, 'immich', 'a', undefined, 'pw');
expect(addProviderPhotoToGallery).toHaveBeenCalledWith(9, 1, 'immich', 'a', undefined, 'pw', 'image');
expect(thrown(() => new JourneyController(svc()).galleryProviderPhotos(user, '9', { provider: 'immich' }))).toEqual({ status: 400, body: { error: 'provider and asset_id required' } });
expect(thrown(() => new JourneyController(svc({ addProviderPhotoToGallery: vi.fn().mockReturnValue(null) } as Partial<JourneyService>)).galleryProviderPhotos(user, '9', { provider: 'immich', asset_id: 'a' }))).toEqual({ status: 403, body: { error: 'Not allowed or duplicate' } });
expect(new JourneyController(svc({ addProviderPhotoToGallery: vi.fn().mockReturnValue({ id: 3 }) } as Partial<JourneyService>)).galleryProviderPhotos(user, '9', { provider: 'immich', asset_id: 'a' })).toEqual({ id: 3 });