diff --git a/libs/portal/shared/util/src/lib/navigation/discover-facet-navigation.spec.ts b/libs/portal/shared/util/src/lib/navigation/discover-facet-navigation.spec.ts index 00efb9b3a..24eeace49 100644 --- a/libs/portal/shared/util/src/lib/navigation/discover-facet-navigation.spec.ts +++ b/libs/portal/shared/util/src/lib/navigation/discover-facet-navigation.spec.ts @@ -102,6 +102,17 @@ describe('createDiscoverFacetNavigation', () => { expect(navigate).not.toHaveBeenCalled(); }); + it('refuses the zero-date placeholder providers ship for "no date"', () => { + const discover = create(xtreamMovie); + + // '0000-00-00' reads as a four-digit year but filters by nothing + expect(discover.canOpenYear(603, '0000-00-00')).toBe(false); + expect(discover.canOpenYear(603, '0000')).toBe(false); + + discover.openYear(603, '0000-00-00'); + expect(navigate).not.toHaveBeenCalled(); + }); + it('does not navigate while the host cannot resolve its playlist', () => { const discover = create(() => null); diff --git a/libs/portal/shared/util/src/lib/navigation/discover-facet-navigation.ts b/libs/portal/shared/util/src/lib/navigation/discover-facet-navigation.ts index 12fc80970..673f6776f 100644 --- a/libs/portal/shared/util/src/lib/navigation/discover-facet-navigation.ts +++ b/libs/portal/shared/util/src/lib/navigation/discover-facet-navigation.ts @@ -4,6 +4,7 @@ import { TmdbCountryFacet, TmdbGenreFacet, TmdbMediaType, + isTmdbYearFacet, } from '@iptvnator/shared/interfaces'; import { discoverLink } from './discover-link.util'; @@ -31,11 +32,13 @@ export interface DiscoverFacetNavigation { * Year stated by a provider date field, whatever shape it arrives in — * `1976`, `1999-03-31` and `31-03-1999` all resolve. Reads the first * four-digit run rather than a fixed slice, which the day-first form - * would otherwise turn into `NaN`. + * would otherwise turn into `NaN`, and rejects the `0000-00-00` + * placeholder rather than offering a chip that filters by nothing. */ function facetYear(releaseDate: string | null | undefined): number | null { const match = releaseDate?.match(/\d{4}/); - return match ? Number(match[0]) : null; + const year = match ? Number(match[0]) : null; + return year !== null && isTmdbYearFacet(year) ? year : null; } /** diff --git a/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.html b/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.html index c6f16718c..9105cdddf 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.html +++ b/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.html @@ -16,30 +16,39 @@ }} } @if (serial.info.releasedate) { - {{ serial.info.releasedate }} + @if ( + discover.canOpenYear( + serial.info.tmdb_id, + serial.info.releasedate + ) + ) { + + } @else { + {{ + serial.info.releasedate + }} + } } @if (serial.info.tmdb_genres?.length) { @for (genre of serial.info.tmdb_genres; track genre.id) { - {{ genre.name }} + {{ genre.name }} + } } @else if (serial.info.genre) { {{ serial.info.genre }} @@ -59,11 +68,13 @@ country of serial.info.tmdb_countries; track country.code ) { - {{ country.name }} + {{ country.name }} + } } diff --git a/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.html b/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.html index 0dde0ab5a..24d943a74 100644 --- a/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.html +++ b/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.html @@ -17,22 +17,31 @@ }} } @if (info.releaseDate) { - {{ info.releaseDate | slice: 0 : 4 }} + @if (discover.canOpenYear(info.tmdb_id, info.releaseDate)) { + + } @else { + {{ + info.releaseDate | slice: 0 : 4 + }} + } } @if (info.tmdb_genres?.length) { @for (genre of info.tmdb_genres; track genre.id) { - {{ genre.name }} + {{ genre.name }} + } } @else if (info.genre) { {{ info.genre }} @@ -49,11 +58,13 @@ } @if (info.tmdb_countries?.length) { @for (country of info.tmdb_countries; track country.code) { - {{ country.name }} + {{ country.name }} + } } diff --git a/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.html b/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.html index 53702cdf0..3d177f1a6 100644 --- a/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.html +++ b/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.html @@ -295,22 +295,31 @@ > @if (info.releasedate) { - {{ info.releasedate | slice: 0 : 4 }} + @if (discover.canOpenYear(info.tmdb_id, info.releasedate)) { + + } @else { + {{ + info.releasedate | slice: 0 : 4 + }} + } } @if (info.tmdb_genres?.length) { @for (genre of info.tmdb_genres; track genre.id) { - {{ genre.name }} + {{ genre.name }} + } } @else if (info.genre) { {{ info.genre }} @@ -325,11 +334,13 @@ } @if (info.tmdb_countries?.length) { @for (country of info.tmdb_countries; track country.code) { - {{ country.name }} + {{ country.name }} + } } @else if (info.country) { {{ info.country }} diff --git a/libs/shared/interfaces/src/lib/tmdb.interface.ts b/libs/shared/interfaces/src/lib/tmdb.interface.ts index f723d997a..1f4e81936 100644 --- a/libs/shared/interfaces/src/lib/tmdb.interface.ts +++ b/libs/shared/interfaces/src/lib/tmdb.interface.ts @@ -119,6 +119,18 @@ export interface TmdbCountryFacet { name: string; } +/** + * Whether a number is a year the Discover page can actually filter by. + * + * Providers ship `0000-00-00` as their "no date" placeholder, which reads + * as a four-digit year and would otherwise produce a `0000` chip that + * filters by nothing. Shared by the chip side and the route-param side so + * a deep link cannot smuggle in what a chip refuses to offer. + */ +export function isTmdbYearFacet(year: number): boolean { + return Number.isInteger(year) && year >= 1000 && year <= 9999; +} + /** * One cached TMDB lookup. Two kinds of rows share the table, discriminated * by the `lookupKey` prefix: diff --git a/libs/ui/components/src/lib/content-hero/content-hero.component.scss b/libs/ui/components/src/lib/content-hero/content-hero.component.scss index e5abface0..24e42bc79 100644 --- a/libs/ui/components/src/lib/content-hero/content-hero.component.scss +++ b/libs/ui/components/src/lib/content-hero/content-hero.component.scss @@ -320,8 +320,15 @@ border-color: rgba(255, 215, 0, 0.3); } + // Rendered as a + } @else { + {{ meta.year }} + } } @if (meta.tmdbGenres?.length) { @for (genre of meta.tmdbGenres; track genre.id) { - {{ genre.name }} + {{ genre.name }} + } } @else if (meta.genre) { {{ meta.genre }} @@ -42,11 +47,13 @@ } @if (meta.tmdbCountries?.length) { @for (country of meta.tmdbCountries; track country.code) { - {{ country.name }} + {{ country.name }} + } } @else if (meta.country) { {{ meta.country }} diff --git a/libs/ui/shared-portals/src/lib/discover-view/discover-params.spec.ts b/libs/ui/shared-portals/src/lib/discover-view/discover-params.spec.ts index 7f45ab8ee..a47693c57 100644 --- a/libs/ui/shared-portals/src/lib/discover-view/discover-params.spec.ts +++ b/libs/ui/shared-portals/src/lib/discover-view/discover-params.spec.ts @@ -38,6 +38,15 @@ describe('parseDiscoverParams', () => { expect(parseDiscoverParams({ year: '1990' }).year).toBe(1990); }); + it('rejects a zero year a deep link could smuggle in', () => { + // The chips refuse '0000'; the route must refuse it too, or the + // request drops the filter and returns unfiltered popular titles + expect(parseDiscoverParams({ year: '0000' }).year).toBeNull(); + expect(hasDiscoverFacet(parseDiscoverParams({ year: '0000' }))).toBe( + false + ); + }); + it('drops a genre label without a valid genre id', () => { const facets = parseDiscoverParams({ genre: 'drama', diff --git a/libs/ui/shared-portals/src/lib/discover-view/discover-params.ts b/libs/ui/shared-portals/src/lib/discover-view/discover-params.ts index ad869365c..47ea52486 100644 --- a/libs/ui/shared-portals/src/lib/discover-view/discover-params.ts +++ b/libs/ui/shared-portals/src/lib/discover-view/discover-params.ts @@ -1,4 +1,4 @@ -import { TmdbMediaType } from '@iptvnator/shared/interfaces'; +import { TmdbMediaType, isTmdbYearFacet } from '@iptvnator/shared/interfaces'; /** * Facets of a portal Discover page, parsed from route query params. @@ -30,8 +30,13 @@ export function parseDiscoverParams(params: RawParams): DiscoverRouteParams { const type: TmdbMediaType = rawString(params, 'type') === 'tv' ? 'tv' : 'movie'; + // `0000` parses as four digits but filters by nothing — a deep link + // must not reach a state the chips themselves refuse to offer const rawYear = rawString(params, 'year'); - const year = rawYear && /^\d{4}$/.test(rawYear) ? Number(rawYear) : null; + const parsedYear = + rawYear && /^\d{4}$/.test(rawYear) ? Number(rawYear) : null; + const year = + parsedYear !== null && isTmdbYearFacet(parsedYear) ? parsedYear : null; const rawGenre = rawString(params, 'genre'); const parsedGenre = rawGenre && /^\d+$/.test(rawGenre) ? Number(rawGenre) : null;