mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 11:06:16 -08:00
fix(tmdb): provide route params observable to inline collection details, linearize regexes
The global-collection inline detail host builds a fake ActivatedRoute for VodDetailsRouteComponent/SerialDetailsComponent with only snapshot.params. Since the detail components now read route.params via toSignal() (detail-> detail re-init), the missing observable crashed component construction and the content hero never rendered — broke dashboard-activation, favorites and recent Electron E2E on all platforms. Provide the params observable alongside the snapshot and assert it in the component spec. Also resolves both CodeQL js/polynomial-redos alerts: bracket-stripping in normalizeTitle now excludes opening delimiters inside the classes, and youtubeEmbedUrl extracts watch?v= ids with a linear two-pass match instead of "watch\?.*v=". Combining-diacritics range rewritten as explicit \u escapes (greptile note). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
600685d897
commit
60be8a2999
4 files changed
+43
-31
No files matched your search
@@ -8,7 +8,7 @@ import {
|
||||
} from '@iptvnator/portal/xtream/data-access';
|
||||
import { PlaylistsService } from '@iptvnator/services';
|
||||
import { Playlist } from '@iptvnator/shared/interfaces';
|
||||
import { of } from 'rxjs';
|
||||
import { firstValueFrom, of } from 'rxjs';
|
||||
import { SerialDetailsComponent } from './serial-details/serial-details.component';
|
||||
import { XtreamCollectionDetailComponent } from './xtream-collection-detail.component';
|
||||
|
||||
@@ -129,12 +129,18 @@ describe('XtreamCollectionDetailComponent', () => {
|
||||
expect(fixture.componentInstance.detailComponent()).toBe(
|
||||
SerialDetailsComponent
|
||||
);
|
||||
expect(
|
||||
fixture.componentInstance
|
||||
.detailInjector()
|
||||
?.get(ActivatedRoute)
|
||||
.snapshot.params
|
||||
).toEqual({
|
||||
const route = fixture.componentInstance
|
||||
.detailInjector()
|
||||
?.get(ActivatedRoute);
|
||||
expect(route?.snapshot.params).toEqual({
|
||||
categoryId: '3',
|
||||
serialId: '103',
|
||||
});
|
||||
// Regression: the detail components consume route.params via
|
||||
// toSignal(), so the fake route must expose the observable too —
|
||||
// otherwise the inline detail crashes on construction.
|
||||
expect(route?.params).toBeDefined();
|
||||
await expect(firstValueFrom(route!.params)).resolves.toEqual({
|
||||
categoryId: '3',
|
||||
serialId: '103',
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
} from '@iptvnator/portal/xtream/data-access';
|
||||
import { PlaylistsService } from '@iptvnator/services';
|
||||
import { Playlist } from '@iptvnator/shared/interfaces';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { firstValueFrom, of } from 'rxjs';
|
||||
import { SerialDetailsComponent } from './serial-details/serial-details.component';
|
||||
import { VodDetailsRouteComponent } from './vod-details/vod-details-route.component';
|
||||
|
||||
@@ -132,28 +132,26 @@ export class XtreamCollectionDetailComponent {
|
||||
? VodDetailsRouteComponent
|
||||
: SerialDetailsComponent
|
||||
);
|
||||
const routeParams =
|
||||
item.contentType === 'movie'
|
||||
? {
|
||||
categoryId: this.toPathSegment(item.categoryId),
|
||||
vodId: xtreamId,
|
||||
}
|
||||
: {
|
||||
categoryId: this.toPathSegment(item.categoryId),
|
||||
serialId: xtreamId,
|
||||
};
|
||||
this.detailInjector.set(
|
||||
Injector.create({
|
||||
providers: [
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
// The detail components read both snapshot.params and
|
||||
// the params observable (via toSignal) — provide both.
|
||||
useValue: {
|
||||
snapshot: {
|
||||
params:
|
||||
item.contentType === 'movie'
|
||||
? {
|
||||
categoryId: this.toPathSegment(
|
||||
item.categoryId
|
||||
),
|
||||
vodId: xtreamId,
|
||||
}
|
||||
: {
|
||||
categoryId: this.toPathSegment(
|
||||
item.categoryId
|
||||
),
|
||||
serialId: xtreamId,
|
||||
},
|
||||
},
|
||||
snapshot: { params: routeParams },
|
||||
params: of(routeParams),
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -46,10 +46,12 @@ export function normalizeTitle(raw: string | null | undefined): string {
|
||||
}
|
||||
|
||||
const cleaned = raw
|
||||
.replace(/\[[^\]]*\]|\([^)]*\)|\{[^}]*\}/g, ' ')
|
||||
// Inner classes exclude the opening delimiter too, so runaway
|
||||
// inputs like "[[[[[…" backtrack linearly (CodeQL js/polynomial-redos)
|
||||
.replace(/\[[^\][]*\]|\([^()]*\)|\{[^{}]*\}/g, ' ')
|
||||
.replace(LANGUAGE_PREFIX, '')
|
||||
.normalize('NFD')
|
||||
.replace(/[̀-ͯ]/g, '')
|
||||
.replace(/[\u0300-\u036F]/g, '')
|
||||
.toLowerCase()
|
||||
.replace(/[^\p{L}\p{N}]+/gu, ' ')
|
||||
.split(' ')
|
||||
|
||||
@@ -76,12 +76,18 @@ export function youtubeEmbedUrl(
|
||||
return null;
|
||||
}
|
||||
|
||||
// Two linear passes instead of one "watch\?.*v=" alternation, which
|
||||
// backtracks polynomially on hostile input (CodeQL js/polynomial-redos)
|
||||
let videoId = raw;
|
||||
const urlMatch = raw.match(
|
||||
/(?:youtube(?:-nocookie)?\.com\/(?:watch\?.*v=|embed\/|shorts\/)|youtu\.be\/)([A-Za-z0-9_-]{6,})/
|
||||
);
|
||||
if (urlMatch) {
|
||||
videoId = urlMatch[1];
|
||||
if (/youtube(?:-nocookie)?\.com\/watch\?/.test(raw)) {
|
||||
videoId = raw.match(/[?&]v=([A-Za-z0-9_-]{6,})/)?.[1] ?? '';
|
||||
} else {
|
||||
const urlMatch = raw.match(
|
||||
/(?:youtube(?:-nocookie)?\.com\/(?:embed|shorts)\/|youtu\.be\/)([A-Za-z0-9_-]{6,})/
|
||||
);
|
||||
if (urlMatch) {
|
||||
videoId = urlMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
return /^[A-Za-z0-9_-]{6,}$/.test(videoId)
|
||||
|
||||
Reference in new issue
Block a user