@@ -341,8 +341,54 @@ def _load_channel_databases(self):
341341 self .logger .error (f"Error loading { channel_file } : { e } " )
342342
343343 self .logger .info (f"Total channels loaded: { total_broadcast } broadcast, { total_premium } premium" )
344+ self ._load_broadcast_stations ()
344345 return True
345346
347+ def _load_broadcast_stations (self ):
348+ """Load the FCC station table (networks.json) into the OTA lookup.
349+
350+ The per-country *_channels.json databases ship only premium
351+ (National/Regional) entries — no ``broadcast`` type, no ``callsign``
352+ field — so OTA/callsign matching relies on this US station table:
353+ callsign -> {network_affiliation, community_served_city,
354+ community_served_state, ...}. Each station is appended to
355+ ``broadcast_channels`` and indexed in ``channel_lookup`` by both its
356+ full callsign (``WEWS-TV``) and base callsign (``WEWS``) so a stream
357+ citing either form resolves. A missing file is non-fatal. Ported from
358+ Channel-Maparr to revive OTA matching after the US database lost its
359+ broadcast entries. bug-063.
360+
361+ Returns the number of stations loaded.
362+ """
363+ stations_path = os .path .join (self .plugin_dir , "networks.json" )
364+ if not os .path .exists (stations_path ):
365+ self .logger .info ("No networks.json present — OTA station table not loaded" )
366+ return 0
367+
368+ try :
369+ with open (stations_path , 'r' , encoding = 'utf-8' ) as f :
370+ stations = json .load (f )
371+ except Exception as e :
372+ self .logger .error (f"Error loading networks.json: { e } " )
373+ return 0
374+
375+ loaded = 0
376+ for station in stations :
377+ callsign = (station .get ('callsign' ) or '' ).strip ().upper ()
378+ if not callsign :
379+ continue
380+ self .broadcast_channels .append (station )
381+ # setdefault: keep the first (primary) station for a given key so a
382+ # later subchannel entry can't clobber the main affiliate.
383+ self .channel_lookup .setdefault (callsign , station )
384+ base_callsign = re .sub (r'-(?:TV|CD|LP|DT|LD)$' , '' , callsign )
385+ if base_callsign != callsign :
386+ self .channel_lookup .setdefault (base_callsign , station )
387+ loaded += 1
388+
389+ self .logger .info (f"Loaded { loaded } OTA broadcast stations from networks.json" )
390+ return loaded
391+
346392 def reload_databases (self , country_codes = None ):
347393 """
348394 Reload channel databases with specific country codes.
@@ -429,6 +475,7 @@ def reload_databases(self, country_codes=None):
429475 self .logger .error (f"Error loading { channel_file } : { e } " )
430476
431477 self .logger .info (f"Total channels loaded: { total_broadcast } broadcast, { total_premium } premium" )
478+ self ._load_broadcast_stations ()
432479 return True
433480
434481 def extract_callsign (self , channel_name ):
@@ -446,7 +493,15 @@ def extract_callsign(self, channel_name):
446493 callsign = paren_match .group (1 ).upper ()
447494 if callsign not in ['WEST' , 'EAST' , 'KIDS' , 'WOMEN' , 'WILD' , 'WORLD' ]:
448495 return callsign
449-
496+
497+ # Priority 1b: grandfathered 3-letter callsigns in parentheses without a suffix
498+ # (WWL/WJZ/KYW/WRC). Suffixed forms fall through to Priority 2. bug-062.
499+ paren3_match = re .search (r'\(([KW][A-Z]{2})\)' , channel_name , re .IGNORECASE )
500+ if paren3_match :
501+ callsign = paren3_match .group (1 ).upper ()
502+ if callsign not in ['WEST' , 'EAST' , 'KIDS' , 'WOMEN' , 'WILD' , 'WORLD' ]:
503+ return callsign
504+
450505 # Priority 2: Callsigns with suffix in parentheses
451506 paren_suffix_match = re .search (r'\(([KW][A-Z]{2,4}-(?:TV|CD|LP|DT|LD))\)' , channel_name , re .IGNORECASE )
452507 if paren_suffix_match :
0 commit comments