Skip to content

Commit 1b04aa6

Browse files
PiratesIRCclaude
andcommitted
v1.26.1430910 — alias matching, per-channel logos, show status, accuracy fixes
Major features ported from sibling plugin Lineuparr: * Alias Stage 0 — 200+ curated channel-name variant map (aliases.py). FNC→Fox News Channel, CSPAN 2→C-SPAN2, CA: RDS→Réseau des Sports (RDS) HD, etc. O(1) lookup runs before fuzzy, short-circuits noisy provider naming. Users can extend via set_user_aliases() at runtime. * Per-channel logos (logo_matcher.py + Apply Per-Channel Logos action) — fuzzy-matches each channel without a logo against the tv-logo/tv-logos GitHub repo, creates Logo records, bulk-assigns. Per-session filelist cache prevents GitHub rate-limit churn on re-runs. * Show Status action (progress_status.py) — persistent progress file at /data/channel_mapparr_progress.json. ProgressTracker now writes status, ETA, and completion summary so the new "Show Status" button can report live progress without watching container logs. Matching accuracy (fuzzy_matcher.py): * Callsign denylist (50 K/W-shaped English words) prevents extracting "WITH"/"WATCH"/"WWE"/"KING" as callsigns from program titles. * Callsign extraction now returns (callsign, is_high_confidence) and is cached. Loose mid-name extractions are flagged low-confidence. * normalize_name: CamelCase splitting (JusticeCentral → Justice Central), number-word folding (BBC Three ↔ BBC 3), dot-between-words splitting, East/West parenthetical promotion ((W) → "West") so zoned variants survive normalization, multi-token country-prefix stripping (CA FR:). * _has_token_overlap majority mode now applies subset, divergent, and numeric-sibling guards. Catches BBC One vs BBC Two, Sky Cinema Disney vs Sky Cinema Decades, ABC News vs BBC News. * Trailing-number guard rejects ESPN 1 vs ESPN 2 / HBO 1 vs HBO 2 fuzzy collisions. Guards now apply inside the per-candidate loop so a high-scoring guard-rejected candidate doesn't suppress lower-scoring valid ones. UI / settings: * Every action has a proper button_label (Dispatcharr rendered "Run" otherwise). Astral-plane emojis swapped to BMP symbols since the plugin loader rejects surrogate pairs. * help_text on every settings field. * Action list expanded: Apply Per-Channel Logos, Show Status. User-reported fixes (Canadian French sports): * CA: RDS / RDS 1 → Réseau des Sports (RDS) HD via alias (canonical name has a parenthesized abbreviation that normalization was stripping). * CA FR: prefix variants now strip cleanly. * TSN n RAW / TSN n BK → TSN n HD via alias. * User's full failing case set: 14/14. Internal: * Test harness at .wolf/test_matching.py (not shipped) covers 46 curated cases plus the 14-case user-report regression set. * All Lineuparr accuracy patterns adopted: see Lineuparr fuzzy_matcher.py for the reference implementation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 166a667 commit 1b04aa6

7 files changed

Lines changed: 1285 additions & 147 deletions

File tree

Channel-Maparr/aliases.py

Lines changed: 311 additions & 0 deletions
Large diffs are not rendered by default.

Channel-Maparr/fuzzy_matcher.py

Lines changed: 345 additions & 79 deletions
Large diffs are not rendered by default.

Channel-Maparr/logo_matcher.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
"""
2+
Logo matching utilities for matching channel names to tv-logo/tv-logos filenames.
3+
4+
Uses simple fuzzy matching (not the full FuzzyMatcher pipeline) since we're
5+
comparing clean channel names against structured filenames.
6+
"""
7+
8+
import re
9+
import urllib.request
10+
import json
11+
import logging
12+
13+
try:
14+
from rapidfuzz import fuzz
15+
except ImportError:
16+
try:
17+
from thefuzz import fuzz
18+
except ImportError:
19+
# Pure-Python fallback: Levenshtein ratio via difflib
20+
import difflib
21+
22+
class _FuzzFallback:
23+
@staticmethod
24+
def ratio(a, b):
25+
return difflib.SequenceMatcher(None, a, b).ratio() * 100
26+
27+
fuzz = _FuzzFallback()
28+
29+
LOGGER = logging.getLogger("plugins.channel_maparr.logo_matcher")
30+
31+
# Threshold for fuzzy matching channel names to logo filenames
32+
LOGO_MATCH_THRESHOLD = 85
33+
34+
35+
def normalize_logo_filename(filename, country_suffix):
36+
"""Normalize a tv-logos filename for comparison.
37+
38+
'cnn-us.png' with country_suffix='us' -> 'cnn'
39+
'fox-news-us.png' with country_suffix='us' -> 'fox news'
40+
"""
41+
# Strip extension
42+
name = re.sub(r'\.(png|svg|jpg|jpeg|gif|webp)$', '', filename, flags=re.IGNORECASE)
43+
# Strip country suffix (e.g., '-us' at end)
44+
suffix_pattern = rf'-{re.escape(country_suffix)}$'
45+
name = re.sub(suffix_pattern, '', name, flags=re.IGNORECASE)
46+
# Replace hyphens with spaces
47+
name = name.replace('-', ' ')
48+
return name.lower().strip()
49+
50+
51+
def normalize_channel_name(name):
52+
"""Normalize a channel name for comparison against logo filenames.
53+
54+
'A&E' -> 'a and e'
55+
'Fox News' -> 'fox news'
56+
'CNN HD' -> 'cnn'
57+
'Discovery Channel' -> 'discovery'
58+
"""
59+
name = name.lower().strip()
60+
# Replace & with 'and'
61+
name = name.replace('&', ' and ')
62+
# Remove special characters except spaces
63+
name = re.sub(r'[^\w\s]', '', name)
64+
# Strip common suffixes that tv-logos filenames don't include
65+
name = re.sub(r'\s+(hd|sd|uhd|4k|fhd|network|channel|tv)\s*$', '', name)
66+
# Collapse whitespace
67+
name = re.sub(r'\s+', ' ', name).strip()
68+
return name
69+
70+
71+
def match_channel_to_logo(channel_name, logo_filenames, country_suffix, threshold=LOGO_MATCH_THRESHOLD):
72+
"""Match a channel name against a list of tv-logos filenames.
73+
74+
Returns the matching filename or None if no match meets the threshold.
75+
"""
76+
normalized_channel = normalize_channel_name(channel_name)
77+
if not normalized_channel:
78+
return None
79+
80+
best_score = 0
81+
best_file = None
82+
83+
for filename in logo_filenames:
84+
normalized_logo = normalize_logo_filename(filename, country_suffix)
85+
if not normalized_logo:
86+
continue
87+
88+
score = fuzz.ratio(normalized_channel, normalized_logo)
89+
if score > best_score:
90+
best_score = score
91+
best_file = filename
92+
if best_score == 100:
93+
break
94+
95+
if best_score >= threshold:
96+
return best_file
97+
return None
98+
99+
100+
_IMAGE_EXTS = ('.png', '.svg', '.jpg', '.jpeg', '.gif', '.webp')
101+
102+
103+
def fetch_tv_logos_filelist(repo, branch, country_dir):
104+
"""Fetch logo filenames from the tv-logos GitHub repo.
105+
106+
Uses the Git Trees API with recursive=1 so directories with more than
107+
1000 files (united-states, for example) return complete results — the
108+
Contents API silently caps at 1000.
109+
"""
110+
url = f"https://api.github.com/repos/{repo}/git/trees/{branch}?recursive=1"
111+
try:
112+
req = urllib.request.Request(url, headers={"Accept": "application/vnd.github.v3+json"})
113+
with urllib.request.urlopen(req, timeout=15) as resp:
114+
data = json.loads(resp.read().decode())
115+
except urllib.error.HTTPError as e:
116+
if e.code == 403:
117+
LOGGER.warning(
118+
f"GitHub rate limit hit fetching tv-logos for {country_dir}. "
119+
f"Anonymous API is 60 req/hr/IP — try again later."
120+
)
121+
else:
122+
LOGGER.warning(f"GitHub HTTP {e.code} fetching tv-logos for {country_dir}: {e.reason}")
123+
return []
124+
except Exception as e:
125+
LOGGER.warning(f"Failed to fetch tv-logos file list for {country_dir}: {e}")
126+
return []
127+
128+
if data.get("truncated"):
129+
LOGGER.warning(f"GitHub tree response was truncated for {repo}@{branch}; some logos missing.")
130+
131+
prefix = f"countries/{country_dir}/"
132+
files = []
133+
for entry in data.get("tree", []):
134+
if entry.get("type") != "blob":
135+
continue
136+
path = entry.get("path", "")
137+
if not path.startswith(prefix):
138+
continue
139+
name = path[len(prefix):]
140+
if "/" in name: # skip nested subdirectories
141+
continue
142+
if name.lower().endswith(_IMAGE_EXTS):
143+
files.append(name)
144+
return files
145+
146+
147+
def build_logo_url(repo, branch, country_dir, filename):
148+
"""Build a raw GitHub URL for a logo file."""
149+
return f"https://raw.githubusercontent.com/{repo}/{branch}/countries/{country_dir}/{filename}"

Channel-Maparr/plugin.json

Lines changed: 74 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "Channel Mapparr",
3-
"version": "1.26.1001200",
3+
"version": "1.26.1430910",
44
"description": "Standardizes broadcast (OTA) and premium/cable channel names using network data and channel lists. Supports M3U stream import, category organization, and fuzzy matching across 42K+ channels in 11 countries.",
55
"author": "PiratesIRC",
66
"license": "MIT",
@@ -18,158 +18,195 @@
1818
"id": "channel_databases",
1919
"label": "Channel Databases",
2020
"type": "string",
21-
"default": "US"
21+
"default": "US",
22+
"help_text": "Comma-separated country codes to load. Supported: US, UK, CA, AU, BR, DE, ES, FR, MX, NL, IN. Example: 'US, UK, CA'. Drives which channel JSON files are used for matching AND which tv-logos directories the per-channel logo action queries."
2223
},
2324
{
2425
"id": "match_sensitivity",
2526
"label": "Match Sensitivity",
2627
"type": "select",
2728
"default": "normal",
2829
"options": [
29-
{"value": "relaxed", "label": "Relaxed \u2014 more matches, more false positives"},
30-
{"value": "normal", "label": "Normal \u2014 balanced"},
31-
{"value": "strict", "label": "Strict \u2014 fewer matches, high confidence"},
32-
{"value": "exact", "label": "Exact \u2014 near-exact matches only"}
33-
]
30+
{"value": "relaxed", "label": "Relaxed — more matches, more false positives"},
31+
{"value": "normal", "label": "Normal — balanced"},
32+
{"value": "strict", "label": "Strict — fewer matches, high confidence"},
33+
{"value": "exact", "label": "Exact — near-exact matches only"}
34+
],
35+
"help_text": "Minimum similarity score required for a stream/channel to be considered a match. 'Normal' (80) suits most providers. Bump to 'Strict' if you see wrong-channel matches; drop to 'Relaxed' only on clean, well-named lineups."
3436
},
3537
{
3638
"id": "selected_groups",
3739
"label": "Channel Groups to Process",
3840
"type": "string",
39-
"default": ""
41+
"default": "",
42+
"help_text": "Comma-separated Dispatcharr channel group names. Leave blank to process ALL groups. Example: 'Sports, News, Movies'."
4043
},
4144
{
4245
"id": "category_groups",
4346
"label": "Category Organization Groups",
4447
"type": "string",
45-
"default": ""
48+
"default": "",
49+
"help_text": "Comma-separated group names the Organize-by-Category action is allowed to create or move channels into. Leave blank to use the categories defined in the channel database JSON."
4650
},
4751
{
4852
"id": "m3u_sources",
4953
"label": "M3U Source",
5054
"type": "select",
51-
"default": "_all"
55+
"default": "_all",
56+
"help_text": "Which M3U account(s) the M3U import reads streams from. '_all' uses every active source; otherwise pick one. Dropdown is populated from Dispatcharr's M3U Accounts at load time."
5257
},
5358
{
5459
"id": "m3u_group_filter",
5560
"label": "M3U Group Filter",
5661
"type": "string",
57-
"default": ""
62+
"default": "",
63+
"help_text": "Comma-separated M3U group-title values to include during import. Leave blank to import all groups."
5864
},
5965
{
6066
"id": "m3u_category_filter",
6167
"label": "Category Filter",
6268
"type": "string",
63-
"default": ""
69+
"default": "",
70+
"help_text": "Comma-separated category names (from the channel database) to import. Leave blank to import all categories matched by streams."
6471
},
6572
{
6673
"id": "m3u_custom_group_name",
6774
"label": "Custom Import Group Name",
6875
"type": "string",
69-
"default": ""
76+
"default": "",
77+
"help_text": "Optional group name to assign to channels created by M3U import. Leave blank to use category-based naming."
7078
},
7179
{
7280
"id": "ota_format",
7381
"label": "OTA Name Format",
7482
"type": "string",
75-
"default": "{NETWORK} - {STATE} {CITY} ({CALLSIGN})"
83+
"default": "{NETWORK} - {STATE} {CITY} ({CALLSIGN})",
84+
"help_text": "Template for broadcast (over-the-air) channel names. Available tokens: {NETWORK}, {STATE}, {CITY}, {CALLSIGN}. Example: 'ABC - NY New York (WABC-TV)'."
7685
},
7786
{
7887
"id": "unknown_suffix",
7988
"label": "Unknown Channel Suffix",
8089
"type": "string",
81-
"default": " [Unk]"
90+
"default": " [Unk]",
91+
"help_text": "Suffix appended to channels the matcher couldn't identify. Used by the Tag Unknown Channels action. Include a leading space if you want one."
8292
},
8393
{
8494
"id": "ignored_tags",
8595
"label": "Ignored Tags",
8696
"type": "string",
87-
"default": "[4K], [FHD], [HD], [SD], [Unknown], [Unk], [Slow], [Dead]"
97+
"default": "[4K], [FHD], [HD], [SD], [Unknown], [Unk], [Slow], [Dead]",
98+
"help_text": "Comma-separated tags stripped from stream/channel names before matching. Use brackets/parens for literal matches (e.g. '[HD]', '(PRIME)'); bare words are matched on word boundaries."
8899
},
89100
{
90101
"id": "default_logo",
91102
"label": "Default Logo",
92103
"type": "string",
93-
"default": ""
104+
"default": "",
105+
"help_text": "Name (case-insensitive) of a logo in Dispatcharr's Logo Manager. Used by the Apply Default Logo action. For per-channel logos use the tv-logos action instead."
94106
},
95107
{
96108
"id": "dry_run_mode",
97109
"label": "Dry Run Mode",
98110
"type": "boolean",
99-
"default": false
111+
"default": false,
112+
"help_text": "When ON, mutating actions (Rename, Organize, Import) export a CSV preview to /data/exports/ instead of writing to the database. Recommended for first-time use of new settings."
100113
},
101114
{
102115
"id": "rate_limiting",
103116
"label": "Rate Limiting",
104117
"type": "select",
105118
"default": "none",
106119
"options": [
107-
{"value": "none", "label": "None \u2014 fastest"},
108-
{"value": "low", "label": "Low \u2014 slight delay"},
109-
{"value": "medium", "label": "Medium \u2014 moderate delay"},
110-
{"value": "high", "label": "High \u2014 gentlest on database"}
111-
]
120+
{"value": "none", "label": "None — fastest"},
121+
{"value": "low", "label": "Low — slight delay"},
122+
{"value": "medium", "label": "Medium — moderate delay"},
123+
{"value": "high", "label": "High — gentlest on database"}
124+
],
125+
"help_text": "Throttles database writes during bulk operations. 'None' is safe for SSD/local DBs; raise if you see DB lock errors or are running on shared/slow storage."
112126
}
113127
],
114128
"actions": [
115129
{
116130
"id": "validate_settings",
117-
"label": "\u2705 Validate Settings",
118-
"description": "Check database connectivity, channel databases, and settings",
131+
"label": "Validate Settings",
132+
"description": "Check database connectivity, channel databases, and settings before running any action.",
133+
"button_label": "✅ Validate",
119134
"button_variant": "outline",
120135
"button_color": "blue"
121136
},
122137
{
123138
"id": "load_and_process_channels",
124-
"label": "\u25b6 Load & Process Channels",
125-
"description": "Scan channel groups and determine standardized names",
139+
"label": "Load & Process Channels",
140+
"description": "Scan channel groups and determine standardized names.",
141+
"button_label": "▶ Load & Process",
126142
"button_variant": "filled",
127143
"button_color": "green"
128144
},
129145
{
130146
"id": "rename_channels",
131-
"label": "\u270f\ufe0f Rename Channels",
132-
"description": "Apply standardized names. Dry Run exports a CSV preview instead.",
147+
"label": "Rename Channels",
148+
"description": "Apply standardized names to processed channels. Dry Run exports a CSV preview instead.",
149+
"button_label": "✏️ Rename",
133150
"button_variant": "filled",
134151
"button_color": "green",
135152
"confirm": {"message": "This will rename channels to the standardized format. This action is irreversible. Continue?"}
136153
},
137154
{
138155
"id": "rename_unknown_channels",
139-
"label": "\u2696 Tag Unknown Channels",
140-
"description": "Append suffix to unmatched OTA and premium channels",
156+
"label": "Tag Unknown Channels",
157+
"description": "Append the configured suffix to unmatched OTA and premium channels.",
158+
"button_label": "⚖ Tag Unknowns",
141159
"button_variant": "filled",
142160
"button_color": "green",
143161
"confirm": {"message": "This will append the configured suffix to unmatched channels. Continue?"}
144162
},
145163
{
146164
"id": "apply_logos",
147-
"label": "\u2b50 Apply Logos",
148-
"description": "Assign the default logo to channels that don't have one",
165+
"label": "Apply Default Logo",
166+
"description": "Assign the configured default logo to channels that don't have one.",
167+
"button_label": "⭐ Apply Default Logo",
149168
"button_variant": "filled",
150169
"button_color": "green",
151170
"confirm": {"message": "This will apply the default logo to channels without a logo. Continue?"}
152171
},
172+
{
173+
"id": "apply_tv_logos",
174+
"label": "Apply Per-Channel Logos (tv-logos)",
175+
"description": "Fuzzy-match each channel name to the tv-logo/tv-logos GitHub repo and assign per-channel logos. Uses the country codes from Channel Databases. Channels with an existing logo are left alone.",
176+
"button_label": "🎨 Apply Per-Channel Logos",
177+
"button_variant": "filled",
178+
"button_color": "green",
179+
"confirm": {"message": "This will fetch tv-logos and assign per-channel logos to channels without one. Continue?"}
180+
},
153181
{
154182
"id": "organize_by_category",
155-
"label": "\u2630 Organize by Category",
183+
"label": "Organize by Category",
156184
"description": "Move channels into category-based groups. Dry Run exports a CSV preview.",
185+
"button_label": "☰ Organize",
157186
"button_variant": "filled",
158187
"button_color": "green",
159188
"confirm": {"message": "This will create new groups (if needed) and move channels to category-based groups. Continue?"}
160189
},
161190
{
162191
"id": "import_m3u_streams",
163-
"label": "\u21e9 Import M3U Streams",
192+
"label": "Import M3U Streams",
164193
"description": "Create channels from M3U streams organized by category. Runs in background. Dry Run exports a CSV preview.",
194+
"button_label": "⇩ Import Streams",
165195
"button_variant": "filled",
166196
"button_color": "violet",
167197
"confirm": {"message": "This will create new channels from M3U streams and organize them into groups. Duplicates get suffixes. Continue?"}
168198
},
199+
{
200+
"id": "plugin_status",
201+
"label": "Show Status",
202+
"description": "Show live progress and ETA for the most recent or running operation. Reads a persistent progress file so you can check without watching container logs.",
203+
"button_label": "📊 Status"
204+
},
169205
{
170206
"id": "clear_csv_exports",
171-
"label": "\u2717 Clear CSV Exports",
172-
"description": "Delete all CSV export files created by this plugin",
207+
"label": "Clear CSV Exports",
208+
"description": "Delete all CSV export files created by this plugin.",
209+
"button_label": "✗ Clear CSVs",
173210
"button_variant": "outline",
174211
"button_color": "red",
175212
"confirm": {"message": "Delete all Channel Mapparr CSV exports?"}

0 commit comments

Comments
 (0)