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 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;