diff --git a/public/index.html b/public/index.html
index 728663b..e6d5c5a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1214,7 +1214,7 @@
-
+
diff --git a/public/js/services/liveChannelProjection.js b/public/js/services/liveChannelProjection.js
index 763f0ef..c55feaa 100644
--- a/public/js/services/liveChannelProjection.js
+++ b/public/js/services/liveChannelProjection.js
@@ -6,7 +6,10 @@
function normalizeSearch(value) {
return String(value || '')
.normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
- .toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim()
+ .toLowerCase()
+ .replace(/([a-z])([0-9])/g, '$1 $2')
+ .replace(/([0-9])([a-z])/g, '$1 $2')
+ .replace(/[^a-z0-9]+/g, ' ').trim()
.split(/\s+/).filter(Boolean)
.map(token => token.length > 3 && token.endsWith('s') ? token.slice(0, -1) : token);
}
@@ -20,7 +23,10 @@
variant.name, variant.tvgId, variant.categoryName, variant.groupTitle
])
].filter(Boolean).join(' '));
- return wanted.every(token => corpus.some(candidate => candidate.includes(token)));
+ return wanted.every(token => corpus.some(candidate => {
+ if (/^\d+$/.test(token)) return /^\d+$/.test(candidate) && Number(candidate) === Number(token);
+ return candidate.includes(token);
+ }));
}
function normalizeLanguage(language) {
diff --git a/server/services/flexibleChannelSearch.js b/server/services/flexibleChannelSearch.js
index 54f519a..703246c 100644
--- a/server/services/flexibleChannelSearch.js
+++ b/server/services/flexibleChannelSearch.js
@@ -1,7 +1,10 @@
function normalizeSearch(value) {
return String(value || '')
.normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
- .toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim()
+ .toLowerCase()
+ .replace(/([a-z])([0-9])/g, '$1 $2')
+ .replace(/([0-9])([a-z])/g, '$1 $2')
+ .replace(/[^a-z0-9]+/g, ' ').trim()
.split(/\s+/).filter(Boolean)
.map(token => token.length > 3 && token.endsWith('s') ? token.slice(0, -1) : token);
}
@@ -13,7 +16,10 @@ function matchesSearch(channel, query) {
channel.displayName, channel.standardName, channel.fingerprint,
...(channel.variants || []).flatMap(variant => [variant.name, variant.tvgId, variant.categoryName])
].filter(Boolean).join(' '));
- return wanted.every(token => corpus.some(candidate => candidate.includes(token)));
+ return wanted.every(token => corpus.some(candidate => {
+ if (/^\d+$/.test(token)) return /^\d+$/.test(candidate) && Number(candidate) === Number(token);
+ return candidate.includes(token);
+ }));
}
module.exports = { matchesSearch, normalizeSearch };
diff --git a/test/t27BeinSearchRegression.test.js b/test/t27BeinSearchRegression.test.js
new file mode 100644
index 0000000..a857c63
--- /dev/null
+++ b/test/t27BeinSearchRegression.test.js
@@ -0,0 +1,39 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+
+const backendSearch = require('../server/services/flexibleChannelSearch');
+const frontendProjection = require('../public/js/services/liveChannelProjection.js');
+
+function channel(displayName) {
+ return {
+ displayName,
+ standardName: displayName,
+ fingerprint: displayName.toLowerCase(),
+ variants: [{
+ name: `${displayName} HD`,
+ tvgId: displayName.replace(/\s+/g, '.').toLowerCase(),
+ categoryName: 'Sports',
+ groupTitle: 'Sports'
+ }]
+ };
+}
+
+test('backend beIN search keeps singular/plural tolerance without matching Max channels', () => {
+ assert.equal(backendSearch.matchesSearch(channel('beIN Sports 1'), 'bein sports 1'), true);
+ assert.equal(backendSearch.matchesSearch(channel('beIN Sports 1'), 'Bein Sport 1'), true);
+ assert.equal(backendSearch.matchesSearch(channel('beIN Sports Max 10'), 'bein sports 1'), false);
+ assert.equal(backendSearch.matchesSearch(channel('beIN Sports Max 4'), 'bein sports 1'), false);
+ assert.equal(backendSearch.matchesSearch(channel('beIN Sports 2'), 'bein sports 1'), false);
+ assert.equal(backendSearch.matchesSearch(channel('beINSports1'), 'bein sports 1'), true);
+ assert.equal(backendSearch.matchesSearch(channel('beIN Sports 01'), 'bein sports 1'), true);
+});
+
+test('frontend local beIN search mirrors backend Max-channel exclusion', () => {
+ assert.equal(frontendProjection.matchesSearch(channel('beIN Sports 1'), 'bein sports 1'), true);
+ assert.equal(frontendProjection.matchesSearch(channel('beIN Sports 1'), 'Bein Sport 1'), true);
+ assert.equal(frontendProjection.matchesSearch(channel('beIN Sports Max 10'), 'bein sports 1'), false);
+ assert.equal(frontendProjection.matchesSearch(channel('beIN Sports Max 4'), 'bein sports 1'), false);
+ assert.equal(frontendProjection.matchesSearch(channel('beIN Sports 2'), 'bein sports 1'), false);
+ assert.equal(frontendProjection.matchesSearch(channel('beINSports1'), 'bein sports 1'), true);
+ assert.equal(frontendProjection.matchesSearch(channel('beIN Sports 01'), 'bein sports 1'), true);
+});