fix(downloads): preserve episode snapshot identity

This commit is contained in:
4gray committed 2026-08-01 16:29:51 +02:00
1 parent 9ba45ed443
commit ce0f563735
13 files changed
+524 -41

No files matched your search

@@ -0,0 +1,58 @@
import { createStalkerSeriesDownloadSnapshot } from './stalker-series-download-metadata';
describe('createStalkerSeriesDownloadSnapshot', () => {
it('preserves zero coordinates from the download row', () => {
const snapshot = createStalkerSeriesDownloadSnapshot({
item: {
id: '50',
category_id: '18',
info: {
name: 'Signal House',
description: 'Parent plot',
actors: 'Sienna Wave',
director: 'Cora Bell',
},
} as never,
episode: {
title: 'The Call',
info: { plot: 'Episode plot', movie_image: 'still.jpg' },
} as never,
language: 'en',
seriesTitle: 'Signal House',
seasonNumber: 0,
episodeNumber: '0',
});
expect(snapshot).toEqual(
expect.objectContaining({
mediaKind: 'series',
title: 'Signal House',
cast: [{ name: 'Sienna Wave' }],
creators: [{ name: 'Cora Bell' }],
episode: expect.objectContaining({
seasonNumber: 0,
episodeNumber: 0,
title: 'The Call',
}),
})
);
});
it.each([
[Number.NaN, 2],
[1, -1],
[1.5, 2],
])('omits episode metadata for malformed row coordinates', (season, ep) => {
const snapshot = createStalkerSeriesDownloadSnapshot({
item: { id: '50', info: { name: 'Signal House' } } as never,
episode: { title: 'The Call', info: [] } as never,
language: 'en',
seriesTitle: 'Signal House',
seasonNumber: season,
episodeNumber: ep,
});
expect(snapshot.mediaKind).toBe('series');
expect(snapshot.episode).toBeUndefined();
});
});
@@ -0,0 +1,102 @@
import type {
DownloadMetadataPerson,
DownloadMetadataSnapshot,
TmdbEnrichedCastMember,
XtreamSerieEpisode,
XtreamSerieEpisodeInfo,
} from '@iptvnator/shared/interfaces';
import type { StalkerSelectedVodItem } from '@iptvnator/portal/stalker/data-access';
import {
createMovieDownloadSnapshot,
createSeriesEpisodeDownloadSnapshot,
} from '@iptvnator/portal/shared/util';
interface StalkerSeriesDownloadSnapshotInput {
item: StalkerSelectedVodItem;
episode: XtreamSerieEpisode;
language: string;
seriesTitle: string;
/** Values already selected for the persisted download row. */
seasonNumber: unknown;
episodeNumber: unknown;
}
function textList(value: string | undefined): string[] | undefined {
const entries = value
?.split(',')
.map((entry) => entry.trim())
.filter(Boolean);
return entries?.length ? entries : undefined;
}
function finiteNumber(value: string | number | undefined) {
if (value === undefined || String(value).trim() === '') return undefined;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
function releaseYear(value: string | undefined): number | undefined {
const match = value?.match(/(?:^|\D)((?:19|20)\d{2})(?:\D|$)/);
return match ? Number(match[1]) : undefined;
}
function people(
enriched: TmdbEnrichedCastMember[] | undefined,
fallback: string | undefined
): DownloadMetadataPerson[] | undefined {
return enriched?.length
? enriched.map((person) => ({
name: person.name,
role: person.character,
profileUrl: person.profileUrl ?? undefined,
tmdbPersonId: person.tmdbPersonId,
}))
: textList(fallback)?.map((name) => ({ name }));
}
function rowCoordinate(value: unknown): number | undefined {
const parsed = Number(value);
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : undefined;
}
export function createStalkerSeriesDownloadSnapshot(
input: StalkerSeriesDownloadSnapshotInput
): DownloadMetadataSnapshot {
const info = input.item.info;
const common = {
language: input.language,
title: input.seriesTitle.trim() || 'Series',
originalTitle: info?.o_name,
plot: info?.description,
releaseDate: info?.releasedate,
year: releaseYear(info?.releasedate),
genres: textList(info?.genre),
rating: finiteNumber(info?.rating_imdb),
status: info?.tmdb_status,
posterUrl: info?.movie_image,
backdropUrl: info?.tmdb_backdrop,
tmdbId: info?.tmdb_id,
providerCategoryId: input.item.category_id,
cast: people(info?.tmdb_cast, info?.actors),
creators: people(info?.tmdb_directors, info?.director),
};
const seasonNumber = rowCoordinate(input.seasonNumber);
const episodeNumber = rowCoordinate(input.episodeNumber);
if (seasonNumber === undefined || episodeNumber === undefined) {
return { ...createMovieDownloadSnapshot(common), mediaKind: 'series' };
}
const episodeInfo = Array.isArray(input.episode.info)
? undefined
: (input.episode.info as XtreamSerieEpisodeInfo);
return createSeriesEpisodeDownloadSnapshot({
...common,
episode: {
seasonNumber,
episodeNumber,
title: input.episode.title,
plot: episodeInfo?.plot,
stillUrl: episodeInfo?.movie_image,
},
});
}
@@ -368,7 +368,7 @@ describe('StalkerSeriesViewComponent', () => {
await fixture.whenStable();
await fixture.componentInstance.downloadEpisode({
episode_num: -3,
episode_num: '0',
title: 'The Call',
info: {
plot: 'Episode-specific plot',
@@ -385,8 +385,8 @@ describe('StalkerSeriesViewComponent', () => {
playlistType: 'stalker',
seriesXtreamId: 50001,
seasonNumber: 0,
episodeNumber: -3,
title: 'Signal House - S00E-3 - The Call',
episodeNumber: '0',
title: 'Signal House - S00E00 - The Call',
metadataSnapshot: expect.objectContaining({
language: 'en',
mediaKind: 'series',
@@ -398,8 +398,8 @@ describe('StalkerSeriesViewComponent', () => {
cast: [{ name: 'Sienna Wave' }],
creators: [{ name: 'Cora Bell' }],
episode: {
seasonNumber: 1,
episodeNumber: 1,
seasonNumber: 0,
episodeNumber: 0,
title: 'The Call',
plot: 'Episode-specific plot',
stillUrl:
@@ -37,7 +37,6 @@ import {
PORTAL_EXTERNAL_PLAYBACK,
PORTAL_PLAYBACK_POSITIONS,
PORTAL_PLAYER,
createSeriesEpisodeDownloadSnapshot,
createLogger,
getStalkerReturnToState,
} from '@iptvnator/portal/shared/util';
@@ -84,6 +83,7 @@ import {
saveStalkerSeriesPosition,
StalkerSeriesPositionPartialSaveError,
} from './stalker-series-position-compatibility';
import { createStalkerSeriesDownloadSnapshot } from './stalker-series-download-metadata';
interface SeriesPositionContext {
readonly generation: number;
@@ -1109,9 +1109,6 @@ export class StalkerSeriesViewComponent implements OnDestroy {
const episodeNum = episode.episode_num || 1;
const seriesTitle =
item.info?.name || this.displayItem()?.info?.name || 'Series';
const snapshotSeriesTitle = seriesTitle.trim() || 'Series';
const snapshotSeasonNum = positiveCoordinate(episode.season);
const snapshotEpisodeNum = positiveCoordinate(episode.episode_num);
const episodeTitle = `${seriesTitle} - S${String(seasonNum).padStart(
2,
'0'
@@ -1124,35 +1121,16 @@ export class StalkerSeriesViewComponent implements OnDestroy {
title: episodeTitle,
url,
posterUrl,
metadataSnapshot: createSeriesEpisodeDownloadSnapshot({
metadataSnapshot: createStalkerSeriesDownloadSnapshot({
item,
episode,
language:
this.translateService.currentLang ||
this.translateService.defaultLang ||
'en',
title: snapshotSeriesTitle,
originalTitle: item.info?.o_name,
plot: item.info?.description,
releaseDate: item.info?.releasedate,
year: snapshotYear(item.info?.releasedate),
genres: snapshotTextList(item.info?.genre),
rating: snapshotNumber(item.info?.rating_imdb),
status: item.info?.tmdb_status,
posterUrl: item.info?.movie_image,
backdropUrl: item.info?.tmdb_backdrop,
tmdbId: item.info?.tmdb_id,
providerCategoryId: item.category_id,
cast: snapshotPeople(item.info?.tmdb_cast, item.info?.actors),
creators: snapshotPeople(
item.info?.tmdb_directors,
item.info?.director
),
episode: {
seasonNumber: snapshotSeasonNum,
episodeNumber: snapshotEpisodeNum,
title: episode.title,
plot: episodeInfo?.plot,
stillUrl: episodeInfo?.movie_image,
},
seriesTitle,
seasonNumber: seasonNum,
episodeNumber: episodeNum,
}),
seriesXtreamId: this.toSeriesId(item.id),
seasonNumber: seasonNum,
@@ -206,6 +206,7 @@
[seriesTitle]="info.name"
[playbackPositions]="episodePlaybackPositions()"
[xtreamDownloadContext]="xtreamDownloadContext()"
[downloadMetadataContext]="downloadMetadataContext()"
[downloadsEnabled]="!providerOnly()"
[openingEpisodeId]="openingEpisodeId()"
[activeEpisodeId]="activeEpisodeId()"
@@ -38,6 +38,7 @@ class StubSeasonContainerComponent {
readonly seriesTitle = input<string | undefined>(undefined);
readonly playbackPositions = input<unknown>(null);
readonly xtreamDownloadContext = input<unknown>(null);
readonly downloadMetadataContext = input<unknown>(null);
readonly downloadsEnabled = input(true);
readonly openingEpisodeId = input<number | null>(null);
readonly activeEpisodeId = input<number | null>(null);
@@ -115,6 +116,9 @@ describe('SerialDetailsComponent', () => {
cover: 'cover.jpg',
backdrop_path: [],
genre: 'Drama',
category_id: '3',
tmdb_id: 901,
tmdb_cast: [{ name: 'Sienna Wave', character: 'Mara' }],
},
episodes: {
'1': [
@@ -352,6 +356,22 @@ describe('SerialDetailsComponent', () => {
],
});
expect(seasonContainer?.downloadsEnabled()).toBe(true);
expect(seasonContainer?.downloadMetadataContext()).toEqual(
expect.objectContaining({
language: 'en',
title: 'Series One',
plot: 'Series plot',
genres: ['Drama'],
providerCategoryId: '3',
tmdbId: 901,
cast: expect.arrayContaining([
expect.objectContaining({
name: 'Sienna Wave',
role: 'Mara',
}),
]),
})
);
});
it('keeps every provider episode but disables download presentation in provider-only mode', async () => {
@@ -21,6 +21,7 @@ import {
PortalDetailShellComponent,
SeasonContainerComponent,
SeasonContainerPlaybackToggleRequest,
type SeasonContainerDownloadMetadataContext,
SeasonContainerXtreamDownloadContext,
} from '@iptvnator/ui/components';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
@@ -51,6 +52,7 @@ import {
SimilarCatalogItem,
matchRecommendationsToCatalog,
} from '../tmdb-similar.util';
import { createXtreamSeriesDownloadMetadataContext } from './serial-download-metadata';
@Component({
selector: 'app-serial-details',
@@ -102,6 +104,18 @@ export class SerialDetailsComponent implements OnInit, OnDestroy {
readonly currentPlaylistId = signal('');
readonly xtreamDownloadContext =
signal<SeasonContainerXtreamDownloadContext | null>(null);
readonly downloadMetadataContext =
computed<SeasonContainerDownloadMetadataContext | null>(() => {
const info = this.selectedItem()?.info;
return info
? createXtreamSeriesDownloadMetadataContext(
info,
this.translateService.currentLang ||
this.translateService.defaultLang ||
'en'
)
: null;
});
/** `playlistId:categoryId:serialId` of the last initialized view */
private readonly lastInitKey = signal<string | null>(null);
private readonly backdropBackfillKey = signal<string | null>(null);
@@ -0,0 +1,61 @@
import { createXtreamSeriesDownloadMetadataContext } from './serial-download-metadata';
describe('createXtreamSeriesDownloadMetadataContext', () => {
it('maps already-enriched series metadata to the shared context', () => {
const context = createXtreamSeriesDownloadMetadataContext(
{
name: 'Signal House',
plot: 'Series plot',
cover: 'poster.jpg',
genre: 'Drama, Mystery',
releaseDate: '2024-04-10',
episode_run_time: '47',
rating: '8.2',
category_id: '7',
backdrop_path: ['backdrop.jpg'],
tmdb_id: 123,
tmdb_cast: [
{
name: 'Sienna Wave',
character: 'Mara',
tmdbPersonId: 10,
},
],
tmdb_directors: [{ name: 'Cora Bell', tmdbPersonId: 11 }],
} as never,
'de'
);
expect(context).toEqual(
expect.objectContaining({
language: 'de',
title: 'Signal House',
plot: 'Series plot',
year: 2024,
durationMinutes: 47,
genres: ['Drama', 'Mystery'],
rating: 8.2,
tmdbId: 123,
cast: [
{
name: 'Sienna Wave',
role: 'Mara',
tmdbPersonId: 10,
},
],
creators: [{ name: 'Cora Bell', tmdbPersonId: 11 }],
})
);
});
it('keeps sparse provider data valid', () => {
const context = createXtreamSeriesDownloadMetadataContext(
{ name: 'Sparse Show' } as never,
'en'
);
expect(context.language).toBe('en');
expect(context.title).toBe('Sparse Show');
expect(context.cast).toBeUndefined();
});
});
@@ -0,0 +1,66 @@
import type { SeasonContainerDownloadMetadataContext } from '@iptvnator/ui/components';
import type {
DownloadMetadataPerson,
TmdbEnrichedCastMember,
XtreamSerieInfo,
} from '@iptvnator/shared/interfaces';
function textList(value: string | undefined): string[] | undefined {
const entries = value
?.split(',')
.map((entry) => entry.trim())
.filter(Boolean);
return entries?.length ? entries : undefined;
}
function finiteNumber(value: number | string | undefined) {
if (value === undefined || String(value).trim() === '') return undefined;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
function releaseYear(value: string | undefined): number | undefined {
const match = value?.match(/(?:^|\D)((?:19|20)\d{2})(?:\D|$)/);
return match ? Number(match[1]) : undefined;
}
function people(
enriched: TmdbEnrichedCastMember[] | undefined,
fallback: string | undefined
): DownloadMetadataPerson[] | undefined {
return enriched?.length
? enriched.map((person) => ({
name: person.name,
role: person.character,
profileUrl: person.profileUrl ?? undefined,
tmdbPersonId: person.tmdbPersonId,
}))
: textList(fallback)?.map((name) => ({ name }));
}
export function createXtreamSeriesDownloadMetadataContext(
info: XtreamSerieInfo,
language: string
): SeasonContainerDownloadMetadataContext {
const runtimeMinutes = finiteNumber(info.episode_run_time);
return {
language,
title: info.name?.trim() || 'Series',
plot: info.plot,
releaseDate: info.releaseDate,
year: releaseYear(info.releaseDate),
durationMinutes:
runtimeMinutes && runtimeMinutes > 0
? Math.round(runtimeMinutes)
: undefined,
genres: textList(info.genre),
rating: finiteNumber(info.rating),
status: info.tmdb_status,
posterUrl: info.cover,
backdropUrl: info.backdrop_path?.[0],
tmdbId: finiteNumber(info.tmdb_id),
providerCategoryId: info.category_id,
cast: people(info.tmdb_cast, info.cast),
creators: people(info.tmdb_directors, info.director),
};
}
@@ -1,4 +1,13 @@
import { XtreamSerieEpisode } from '@iptvnator/shared/interfaces';
import {
DownloadMetadataSnapshot,
XtreamSerieEpisode,
XtreamSerieEpisodeInfo,
} from '@iptvnator/shared/interfaces';
import {
createMovieDownloadSnapshot,
createSeriesEpisodeDownloadSnapshot,
type DownloadMovieSnapshotInput,
} from '@iptvnator/portal/shared/util';
/**
* Pure helpers for mapping episodes (Xtream and Stalker-mapped) to the
@@ -12,6 +21,9 @@ export interface XtreamEpisodeDownloadContext {
password?: string;
}
/** Provider-neutral metadata already loaded by the detail host. */
export type SeasonContainerDownloadMetadataContext = DownloadMovieSnapshotInput;
export interface XtreamEpisodeDownloadRequest {
playlistId: string;
xtreamId: number;
@@ -22,6 +34,42 @@ export interface XtreamEpisodeDownloadRequest {
seriesXtreamId: number;
seasonNumber: number;
episodeNumber: number;
metadataSnapshot?: DownloadMetadataSnapshot;
}
function rowCoordinate(value: unknown): number | undefined {
const parsed = Number(value);
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : undefined;
}
function buildMetadataSnapshot(
metadata: SeasonContainerDownloadMetadataContext,
episode: XtreamSerieEpisode,
seasonNumber: unknown,
episodeNumber: unknown
): DownloadMetadataSnapshot {
const normalizedSeason = rowCoordinate(seasonNumber);
const normalizedEpisode = rowCoordinate(episodeNumber);
if (normalizedSeason === undefined || normalizedEpisode === undefined) {
return {
...createMovieDownloadSnapshot(metadata),
mediaKind: 'series',
};
}
const info = Array.isArray(episode.info)
? undefined
: (episode.info as XtreamSerieEpisodeInfo);
return createSeriesEpisodeDownloadSnapshot({
...metadata,
episode: {
seasonNumber: normalizedSeason,
episodeNumber: normalizedEpisode,
title: episode.title,
plot: info?.plot,
stillUrl: info?.movie_image,
},
});
}
export function hashString(str: string): number {
@@ -75,6 +123,7 @@ export function buildXtreamEpisodeDownloadRequest(options: {
seriesTitle: string;
fallbackSeasonKey: string | undefined;
posterUrl?: string;
metadataContext?: SeasonContainerDownloadMetadataContext;
}): XtreamEpisodeDownloadRequest {
const { episode, context, playlistId, seriesId, seriesTitle } = options;
const serverUrl = context.serverUrl?.replace(/\/$/, '') || '';
@@ -84,7 +133,9 @@ export function buildXtreamEpisodeDownloadRequest(options: {
const seasonNumber =
episode.season || Number(options.fallbackSeasonKey) || 1;
const episodeNumber = episode.episode_num || 1;
const title = `${seriesTitle || 'Series'} - S${String(seasonNumber).padStart(
const title = `${seriesTitle || 'Series'} - S${String(
seasonNumber
).padStart(
2,
'0'
)}E${String(episodeNumber).padStart(2, '0')} - ${episode.title}`;
@@ -99,5 +150,15 @@ export function buildXtreamEpisodeDownloadRequest(options: {
seriesXtreamId: seriesId,
seasonNumber,
episodeNumber,
...(options.metadataContext
? {
metadataSnapshot: buildMetadataSnapshot(
options.metadataContext,
episode,
seasonNumber,
episodeNumber
),
}
: {}),
};
}
@@ -66,10 +66,90 @@ describe('episode-download.util', () => {
fallbackSeasonKey: '1',
});
expect(request.url).toBe('http://host/series/u/p/55.mkv');
expect(request.title).toBe('Show - S02E03 - The One');
expect(request.seasonNumber).toBe(2);
expect(request.seriesXtreamId).toBe(900);
expect(request).toEqual({
playlistId: 'pl-1',
xtreamId: 55,
contentType: 'episode',
title: 'Show - S02E03 - The One',
url: 'http://host/series/u/p/55.mkv',
posterUrl: undefined,
seriesXtreamId: 900,
seasonNumber: 2,
episodeNumber: 3,
});
});
it('adds the loaded series and episode metadata when provided', () => {
const request = buildXtreamEpisodeDownloadRequest({
episode: episode({
id: '55',
episode_num: 3,
season: 2,
title: 'The One',
info: {
plot: 'Episode plot',
movie_image: 'https://images.test/stills/one.jpg',
},
}),
context: { serverUrl: 'http://host' },
playlistId: 'pl-1',
seriesId: 900,
seriesTitle: 'Show',
fallbackSeasonKey: '1',
metadataContext: {
language: 'de',
title: 'Show',
plot: 'Series plot',
genres: ['Drama'],
posterUrl: 'poster.jpg',
providerCategoryId: '7',
cast: [{ name: 'Actor' }],
},
});
expect(request.metadataSnapshot).toEqual(
expect.objectContaining({
language: 'de',
mediaKind: 'series',
title: 'Show',
plot: 'Series plot',
genres: ['Drama'],
providerCategoryId: '7',
cast: [{ name: 'Actor' }],
episode: {
seasonNumber: 2,
episodeNumber: 3,
title: 'The One',
plot: 'Episode plot',
stillUrl: 'https://images.test/stills/one.jpg',
},
})
);
});
it('creates a valid sparse snapshot without provider enrichment', () => {
const request = buildXtreamEpisodeDownloadRequest({
episode: episode({ season: 1, info: [] }),
context: {},
playlistId: 'pl-1',
seriesId: 900,
seriesTitle: 'Show',
fallbackSeasonKey: undefined,
metadataContext: { language: 'en', title: 'Show' },
});
expect(request.metadataSnapshot).toEqual(
expect.objectContaining({
language: 'en',
mediaKind: 'series',
title: 'Show',
episode: {
seasonNumber: 1,
episodeNumber: 1,
title: 'Ep',
},
})
);
});
});
@@ -12,7 +12,7 @@ import { SeasonContainerComponent } from './season-container.component';
const downloadsServiceStub = {
isAvailable: signal(false),
downloads: () => [],
startDownload: async () => undefined,
startDownload: jest.fn().mockResolvedValue(undefined),
isDownloaded: () => false,
isDownloading: () => false,
getDownloadedFilePath: () => '',
@@ -59,6 +59,7 @@ describe('SeasonContainerComponent', () => {
beforeEach(async () => {
downloadsServiceStub.isAvailable.set(false);
downloadsServiceStub.startDownload.mockClear();
await TestBed.configureTestingModule({
imports: [
NoopAnimationsModule,
@@ -148,6 +149,41 @@ describe('SeasonContainerComponent', () => {
).toBe(0);
});
it('forwards the optional series metadata into the Xtream request', async () => {
const target = createEpisode();
setRequiredInputs({ '1': [target] });
fixture.componentRef.setInput('seriesTitle', 'Signal House');
fixture.componentRef.setInput('xtreamDownloadContext', {
serverUrl: 'http://host',
username: 'u',
password: 'p',
});
fixture.componentRef.setInput('downloadMetadataContext', {
language: 'en',
title: 'Signal House',
plot: 'Series plot',
});
fixture.detectChanges();
await component.downloadEpisode(new Event('click'), target);
expect(downloadsServiceStub.startDownload).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Signal House - S01E01 - Pilot',
metadataSnapshot: expect.objectContaining({
mediaKind: 'series',
title: 'Signal House',
plot: 'Series plot',
episode: expect.objectContaining({
seasonNumber: 1,
episodeNumber: 1,
title: 'Pilot',
}),
}),
})
);
});
it('renders the season-level placeholder when the selected season has no episodes', () => {
setRequiredInputs({ '1': [] });
fixture.detectChanges();
@@ -35,6 +35,7 @@ import {
buildXtreamEpisodeDownloadRequest,
getEpisodeDownloadId,
isStalkerEpisode,
type SeasonContainerDownloadMetadataContext,
} from './episode-download.util';
import {
EPISODE_INFO_PLAY,
@@ -56,6 +57,8 @@ export interface SeasonContainerXtreamDownloadContext {
password?: string;
}
export type { SeasonContainerDownloadMetadataContext } from './episode-download.util';
export interface SeasonContainerPlaybackToggleRequest {
contentXtreamId: number;
nextPosition: PlaybackPositionData | null;
@@ -94,6 +97,8 @@ export class SeasonContainerComponent implements OnInit {
);
readonly xtreamDownloadContext =
input<SeasonContainerXtreamDownloadContext | null>(null);
readonly downloadMetadataContext =
input<SeasonContainerDownloadMetadataContext | null>(null);
readonly downloadsEnabled = input(true);
readonly openingEpisodeId = input<number | null>(null);
/** Episode currently playing in an EXTERNAL player session. */
@@ -384,6 +389,7 @@ export class SeasonContainerComponent implements OnInit {
seriesTitle: this.seriesTitle(),
fallbackSeasonKey: this.selectedSeason(),
posterUrl: this.getEpisodeInfo(episode)?.movie_image,
metadataContext: this.downloadMetadataContext() ?? undefined,
})
);
}