Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/src/assets/i18n/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@
"RESULTS_FOUND": "تم العثور على {{count}} نتيجة",
"NO_RESULTS_FOR": "لم يتم العثور على نتائج لـ \"{{term}}\"",
"GROUP_BY_PLAYLIST": "تجميع النتائج حسب قائمة التشغيل",
"GROUP_SIMILAR_TITLES": "تجميع العناوين المتشابهة",
"VISIBLE_CATEGORIES_ONLY": "الفئات المرئية فقط",
"GLOBAL_SEARCH_SOURCE_NOTE": "Global search includes results from Xtream and M3U playlists.",
"GLOBAL_INITIAL_DESCRIPTION": "Enter at least 2 characters to search across Xtream and M3U playlists.",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/assets/i18n/ary.json
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@
"RESULTS_FOUND": "تلقاو {{count}} ديال النتايج",
"NO_RESULTS_FOR": "ما تلقاو حتى نتيجة لـ \"{{term}}\"",
"GROUP_BY_PLAYLIST": "جمع النتائج حسب قائمة التشغيل",
"GROUP_SIMILAR_TITLES": "جمع العناوين المتشابهة",
"VISIBLE_CATEGORIES_ONLY": "غير الفئات الظاهرة",
"GLOBAL_SEARCH_SOURCE_NOTE": "Global search includes results from Xtream and M3U playlists.",
"GLOBAL_INITIAL_DESCRIPTION": "Enter at least 2 characters to search across Xtream and M3U playlists.",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/assets/i18n/by.json
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@
"RESULTS_FOUND": "Знойдзена {{count}} вынікаў",
"NO_RESULTS_FOR": "Не знойдзена вынікаў для \"{{term}}\"",
"GROUP_BY_PLAYLIST": "Групаваць вынікі па плэйлісце",
"GROUP_SIMILAR_TITLES": "Групаваць падобныя назвы",
"VISIBLE_CATEGORIES_ONLY": "Толькі бачныя катэгорыі",
"GLOBAL_SEARCH_SOURCE_NOTE": "Global search includes results from Xtream and M3U playlists.",
"GLOBAL_INITIAL_DESCRIPTION": "Enter at least 2 characters to search across Xtream and M3U playlists.",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/assets/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@
"RESULTS_FOUND": "{{count}} Ergebnisse gefunden",
"NO_RESULTS_FOR": "Keine Ergebnisse für \"{{term}}\" gefunden",
"GROUP_BY_PLAYLIST": "Ergebnisse nach Playlist gruppieren",
"GROUP_SIMILAR_TITLES": "Ähnliche Titel gruppieren",
"VISIBLE_CATEGORIES_ONLY": "Nur sichtbare Kategorien",
"GLOBAL_SEARCH_SOURCE_NOTE": "Global search includes results from Xtream and M3U playlists.",
"GLOBAL_INITIAL_DESCRIPTION": "Enter at least 2 characters to search across Xtream and M3U playlists.",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/assets/i18n/el.json
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@
"RESULTS_FOUND": "Βρέθηκαν {{count}} αποτελέσματα",
"NO_RESULTS_FOR": "Δεν βρέθηκαν αποτελέσματα για \"{{term}}\"",
"GROUP_BY_PLAYLIST": "Ομαδοποίηση αποτελεσμάτων ανά λίστα αναπαραγωγής",
"GROUP_SIMILAR_TITLES": "Ομαδοποίηση παρόμοιων τίτλων",
"VISIBLE_CATEGORIES_ONLY": "Μόνο ορατές κατηγορίες",
"GLOBAL_SEARCH_SOURCE_NOTE": "Global search includes results from Xtream and M3U playlists.",
"GLOBAL_INITIAL_DESCRIPTION": "Enter at least 2 characters to search across Xtream and M3U playlists.",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@
"RESULTS_FOUND": "Found {{count}} results",
"NO_RESULTS_FOR": "No results found for \"{{term}}\"",
"GROUP_BY_PLAYLIST": "Group results by playlist",
"GROUP_SIMILAR_TITLES": "Group similar titles",
"VISIBLE_CATEGORIES_ONLY": "Visible categories only",
"GLOBAL_SEARCH_SOURCE_NOTE": "Global search includes results from Xtream and M3U playlists.",
"GLOBAL_INITIAL_DESCRIPTION": "Enter at least 2 characters to search across Xtream and M3U playlists.",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/assets/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@
"RESULTS_FOUND": "Se encontraron {{count}} resultados",
"NO_RESULTS_FOR": "No se encontraron resultados para \"{{term}}\"",
"GROUP_BY_PLAYLIST": "Agrupar resultados por lista de reproducción",
"GROUP_SIMILAR_TITLES": "Agrupar títulos similares",
"VISIBLE_CATEGORIES_ONLY": "Solo categorías visibles",
"GLOBAL_SEARCH_SOURCE_NOTE": "Global search includes results from Xtream and M3U playlists.",
"GLOBAL_INITIAL_DESCRIPTION": "Enter at least 2 characters to search across Xtream and M3U playlists.",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/assets/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@
"RESULTS_FOUND": "{{count}} résultats trouvés",
"NO_RESULTS_FOR": "Aucun résultat trouvé pour \"{{term}}\"",
"GROUP_BY_PLAYLIST": "Grouper les résultats par liste de lecture",
"GROUP_SIMILAR_TITLES": "Grouper les titres similaires",
"VISIBLE_CATEGORIES_ONLY": "Catégories visibles uniquement",
"GLOBAL_SEARCH_SOURCE_NOTE": "Global search includes results from Xtream and M3U playlists.",
"GLOBAL_INITIAL_DESCRIPTION": "Enter at least 2 characters to search across Xtream and M3U playlists.",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/assets/i18n/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@
"RESULTS_FOUND": "Trovati {{count}} risultati",
"NO_RESULTS_FOR": "Nessun risultato trovato per \"{{term}}\"",
"GROUP_BY_PLAYLIST": "Raggruppa risultati per playlist",
"GROUP_SIMILAR_TITLES": "Raggruppa titoli simili",
"VISIBLE_CATEGORIES_ONLY": "Solo categorie visibili",
"GLOBAL_SEARCH_SOURCE_NOTE": "Global search includes results from Xtream and M3U playlists.",
"GLOBAL_INITIAL_DESCRIPTION": "Enter at least 2 characters to search across Xtream and M3U playlists.",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/assets/i18n/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@
"RESULTS_FOUND": "{{count}}件の結果が見つかりました",
"NO_RESULTS_FOR": "「{{term}}」に一致する結果は見つかりませんでした",
"GROUP_BY_PLAYLIST": "プレイリストごとに結果をグループ化",
"GROUP_SIMILAR_TITLES": "類似タイトルをグループ化",
"VISIBLE_CATEGORIES_ONLY": "表示中のカテゴリーのみ",
"GLOBAL_SEARCH_SOURCE_NOTE": "Global search includes results from Xtream and M3U playlists.",
"GLOBAL_INITIAL_DESCRIPTION": "Enter at least 2 characters to search across Xtream and M3U playlists.",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/assets/i18n/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@
"RESULTS_FOUND": "{{count}}개의 결과를 찾았습니다",
"NO_RESULTS_FOR": "\"{{term}}\"에 대한 결과를 찾을 수 없습니다",
"GROUP_BY_PLAYLIST": "재생목록별로 결과 그룹화",
"GROUP_SIMILAR_TITLES": "유사한 제목 그룹화",
"VISIBLE_CATEGORIES_ONLY": "표시되는 카테고리만",
"GLOBAL_SEARCH_SOURCE_NOTE": "Global search includes results from Xtream and M3U playlists.",
"GLOBAL_INITIAL_DESCRIPTION": "Enter at least 2 characters to search across Xtream and M3U playlists.",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/assets/i18n/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@
"RESULTS_FOUND": "{{count}} resultaten gevonden",
"NO_RESULTS_FOR": "Geen resultaten gevonden voor \"{{term}}\"",
"GROUP_BY_PLAYLIST": "Resultaten groeperen op afspeellijst",
"GROUP_SIMILAR_TITLES": "Vergelijkbare titels groeperen",
"VISIBLE_CATEGORIES_ONLY": "Alleen zichtbare categorieën",
"GLOBAL_SEARCH_SOURCE_NOTE": "Global search includes results from Xtream and M3U playlists.",
"GLOBAL_INITIAL_DESCRIPTION": "Enter at least 2 characters to search across Xtream and M3U playlists.",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/assets/i18n/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@
"RESULTS_FOUND": "Znaleziono {{count}} wyników",
"NO_RESULTS_FOR": "Brak wyników dla „{{term}}”",
"GROUP_BY_PLAYLIST": "Grupuj wyniki według listy odtwarzania",
"GROUP_SIMILAR_TITLES": "Grupuj podobne tytuły",
"VISIBLE_CATEGORIES_ONLY": "Tylko widoczne kategorie",
"GLOBAL_SEARCH_SOURCE_NOTE": "Global search includes results from Xtream and M3U playlists.",
"GLOBAL_INITIAL_DESCRIPTION": "Enter at least 2 characters to search across Xtream and M3U playlists.",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/assets/i18n/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@
"RESULTS_FOUND": "{{count}} resultados encontrados",
"NO_RESULTS_FOR": "Nenhum resultado encontrado para \"{{term}}\"",
"GROUP_BY_PLAYLIST": "Agrupar resultados por playlist",
"GROUP_SIMILAR_TITLES": "Agrupar títulos semelhantes",
"VISIBLE_CATEGORIES_ONLY": "Apenas categorias visíveis",
"GLOBAL_SEARCH_SOURCE_NOTE": "Global search includes results from Xtream and M3U playlists.",
"GLOBAL_INITIAL_DESCRIPTION": "Enter at least 2 characters to search across Xtream and M3U playlists.",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/assets/i18n/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@
"RESULTS_FOUND": "Найдено {{count}} результатов",
"NO_RESULTS_FOR": "Нет результатов по запросу \"{{term}}\"",
"GROUP_BY_PLAYLIST": "Группировать результаты по плейлистам",
"GROUP_SIMILAR_TITLES": "Группировать похожие названия",
"VISIBLE_CATEGORIES_ONLY": "Только видимые категории",
"GLOBAL_SEARCH_SOURCE_NOTE": "Глобальный поиск включает результаты из плейлистов Xtream и M3U.",
"GLOBAL_INITIAL_DESCRIPTION": "Введите минимум 2 символа для поиска по плейлистам Xtream и M3U.",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/assets/i18n/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@
"RESULTS_FOUND": "{{count}} sonuç bulundu",
"NO_RESULTS_FOR": "\"{{term}}\" için sonuç bulunamadı",
"GROUP_BY_PLAYLIST": "Sonuçları oynatma listesine göre grupla",
"GROUP_SIMILAR_TITLES": "Benzer başlıkları grupla",
"VISIBLE_CATEGORIES_ONLY": "Sadece görünür kategoriler",
"GLOBAL_SEARCH_SOURCE_NOTE": "Global search includes results from Xtream and M3U playlists.",
"GLOBAL_INITIAL_DESCRIPTION": "Enter at least 2 characters to search across Xtream and M3U playlists.",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/assets/i18n/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@
"RESULTS_FOUND": "找到 {{count}} 条结果",
"NO_RESULTS_FOR": "未找到与“{{term}}”匹配的结果",
"GROUP_BY_PLAYLIST": "按播放列表分组结果",
"GROUP_SIMILAR_TITLES": "将相似标题分组",
"VISIBLE_CATEGORIES_ONLY": "仅可见分类",
"GLOBAL_SEARCH_SOURCE_NOTE": "Global search includes results from Xtream and M3U playlists.",
"GLOBAL_INITIAL_DESCRIPTION": "Enter at least 2 characters to search across Xtream and M3U playlists.",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/assets/i18n/zhtw.json
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@
"RESULTS_FOUND": "找到 {{count}} 筆結果",
"NO_RESULTS_FOR": "找不到與「{{term}}」相符的結果",
"GROUP_BY_PLAYLIST": "依播放清單分組顯示結果",
"GROUP_SIMILAR_TITLES": "將相似標題分組",
"VISIBLE_CATEGORIES_ONLY": "僅顯示可見類別",
"GLOBAL_SEARCH_SOURCE_NOTE": "Global search includes results from Xtream and M3U playlists.",
"GLOBAL_INITIAL_DESCRIPTION": "Enter at least 2 characters to search across Xtream and M3U playlists.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@
{{ type() }}
</span>
}

@if (countBadge() > 1) {
<span class="count-badge" aria-hidden="true">
×{{ countBadge() }}
</span>
}
</div>

<div class="card-info">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@
.type-badge {
@include grid.type-badge;
}

.count-badge {
position: absolute;
top: 6px;
right: 6px;
padding: 2px 7px;
border-radius: 999px;
font-size: 12px;
font-weight: 500;
line-height: 1.4;
color: #fff;
background: rgba(0, 0, 0, 0.72);
}
}

.card-info {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ export class ContentCardComponent {
/** Whether to render the type badge (live/movie/series) on the poster */
readonly showTypeBadge = input<boolean>(true);

/**
* Number of collapsed variants behind this card. Rendered as a small
* "×N" pill when greater than 1 (global-search variant grouping).
*/
readonly countBadge = input<number>(0);

/** Emitted when the card is clicked */
readonly cardClick = output<void>();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { XtreamSearchResultItem } from '@iptvnator/portal/xtream/data-access';
import { groupResultsByVariant } from './global-search-grouping.util';

function item(
title: string,
overrides: Partial<XtreamSearchResultItem> = {}
): XtreamSearchResultItem {
return {
id: title,
title,
type: 'movie',
...overrides,
} as unknown as XtreamSearchResultItem;
}

const displayType = (i: XtreamSearchResultItem): string =>
(i as { type?: string }).type ?? 'movie';

describe('groupResultsByVariant', () => {
it('collapses tagged variants of one title into a single group', () => {
const groups = groupResultsByVariant(
[
item('DE| The Pitt'),
item('4K-TR - The Pitt (2025) (US)'),
item('The Pitt (2025)'),
item('|ALB| The Pitt'),
],
displayType
);

expect(groups).toHaveLength(1);
expect(groups[0].items).toHaveLength(4);
expect(groups[0].representative.title).toBe('DE| The Pitt');
});

it('labels the group with the cleanest member title', () => {
const groups = groupResultsByVariant(
[item('DE| The Pitt'), item('The Pitt (2025)'), item('|ALB| The Pitt')],
displayType
);

expect(groups[0].displayTitle).toBe('The Pitt');
});

it('never merges different content types with the same title', () => {
const groups = groupResultsByVariant(
[
item('The Pitt', { type: 'series' }),
item('The Pitt', { type: 'movie' }),
],
displayType
);

expect(groups).toHaveLength(2);
});

it('keeps unrelated titles in separate groups', () => {
const groups = groupResultsByVariant(
[item('The Pitt'), item('The Pradeeps of Pittsburgh')],
displayType
);

expect(groups).toHaveLength(2);
});

it('preserves first-seen group order and ranked member order', () => {
const groups = groupResultsByVariant(
[
item('EN| Fallout'),
item('The Last of Us'),
item('DE| Fallout'),
],
displayType
);

expect(groups.map((g) => g.displayTitle)).toEqual([
'Fallout',
'The Last of Us',
]);
expect(groups[0].items.map((i) => i.title)).toEqual([
'EN| Fallout',
'DE| Fallout',
]);
});

it('gives titles that normalize to nothing their own group', () => {
const groups = groupResultsByVariant(
[item('|DE|', { id: 'a' }), item('|FR|', { id: 'b' })],
displayType
);

expect(groups).toHaveLength(2);
});

it('splits same-title remakes with different years', () => {
const groups = groupResultsByVariant(
[
item('Dune (1984)'),
item('DE| Dune (2021)'),
item('Dune (2021)'),
],
displayType
);

expect(groups).toHaveLength(2);
expect(groups.map((g) => g.items.length).sort()).toEqual([1, 2]);
});

it('keeps one group when only some variants carry the year', () => {
const groups = groupResultsByVariant(
[
item('DE| The Pitt'),
item('The Pitt (2025)'),
item('|ALB| The Pitt'),
],
displayType
);

expect(groups).toHaveLength(1);
expect(groups[0].items).toHaveLength(3);
});

it('prefers a representative that has a poster', () => {
const groups = groupResultsByVariant(
[
item('DE| The Pitt', { id: 1, poster_url: '' }),
item('The Pitt (2025)', { id: 2, poster_url: 'poster.jpg' }),
],
displayType
);

expect(groups[0].representative.poster_url).toBe('poster.jpg');
});

it('applies a key prefix so identical titles stay independent', () => {
const a = groupResultsByVariant([item('DE| The Pitt')], displayType, 'p1::');
const b = groupResultsByVariant([item('DE| The Pitt')], displayType, 'p2::');

expect(a[0].key).not.toBe(b[0].key);
expect(a[0].key.startsWith('p1::')).toBe(true);
});
});
Loading
Loading