From ad7ac3341e6ddaa6192a0ebf5067875858611571 Mon Sep 17 00:00:00 2001 From: brettlangdon Date: Mon, 26 Feb 2024 08:37:17 -0500 Subject: [PATCH] add zaptv epg guide data --- src/dlhdhr/config.py | 1 + src/dlhdhr/dlhd/channels.py | 48 +++++++++----- src/dlhdhr/epg/__init__.py | 5 +- src/dlhdhr/epg/zaptv.py | 127 ++++++++++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+), 18 deletions(-) create mode 100644 src/dlhdhr/epg/zaptv.py diff --git a/src/dlhdhr/config.py b/src/dlhdhr/config.py index 3a675b6..0c9de44 100644 --- a/src/dlhdhr/config.py +++ b/src/dlhdhr/config.py @@ -21,3 +21,4 @@ COUNTRY_EXCLUDE: set[str] | None = _set_or_none("DLHDHR_COUNTRY_EXCLUDE") COUNTRY_ALLOW: set[str] | None = _set_or_none("DLHDHR_COUNTRY_ALLOW") ZAP2IT_REFRESH_DELAY: int = int(os.getenv("DLHDHR_ZAP2IT_REFRESH_DELAY", "3600")) +ZAPTV_REFRESH_DELAY: int = int(os.getenv("DLHDHR_ZAPTV_REFRESH_DELAY", "3600")) diff --git a/src/dlhdhr/dlhd/channels.py b/src/dlhdhr/dlhd/channels.py index bcefdd3..a5cccd5 100644 --- a/src/dlhdhr/dlhd/channels.py +++ b/src/dlhdhr/dlhd/channels.py @@ -39,10 +39,18 @@ _CHANNELS = [ ), DLHDChannel(number="36", name="Sky Sports Arena UK", country_code="uk", xmltv_id="SkySportsArena.uk", call_sign=""), DLHDChannel( - number="37", name="Sky Sports Action UK", country_code="uk", xmltv_id="SkySportsAction.uk", call_sign="" + number="37", + name="Sky Sports Action UK", + country_code="uk", + xmltv_id="SkySportsAction.uk", + call_sign="sky-action", ), DLHDChannel( - number="38", name="Sky Sports Main Event", country_code="uk", xmltv_id="SkySportsMainEvent.uk", call_sign="" + number="38", + name="Sky Sports Main Event", + country_code="uk", + xmltv_id="SkySportsMainEvent.uk", + call_sign="sky-sports-main-event", ), DLHDChannel(number="39", name="Fox Sports 1 USA", country_code="us", xmltv_id="FoxSports1.us", call_sign=""), DLHDChannel(number="40", name="Tennis Channel", country_code="us", xmltv_id="TennisChannel.us", call_sign="TENNIS"), @@ -67,7 +75,9 @@ _CHANNELS = [ ), DLHDChannel(number="57", name="EuroSport 1 Poland", country_code="pl", xmltv_id="", call_sign=""), DLHDChannel(number="58", name="EuroSport 2 Poland", country_code="pl", xmltv_id="Eurosport2.pl", call_sign=""), - DLHDChannel(number="60", name="Sky Sports F1 UK", country_code="uk", xmltv_id="SkySportsF1.uk", call_sign=""), + DLHDChannel( + number="60", name="Sky Sports F1 UK", country_code="uk", xmltv_id="SkySportsF1.uk", call_sign="sky-sports-f1" + ), DLHDChannel(number="61", name="beIN Sports MENA English 1", country_code="", xmltv_id="", call_sign=""), DLHDChannel(number="62", name="beIN SPORTS 1 Turkey", country_code="tr", xmltv_id="beINSports1.tr", call_sign=""), DLHDChannel(number="63", name="beIN SPORTS 2 Turkey", country_code="tr", xmltv_id="beINSports2.tr", call_sign=""), @@ -77,7 +87,13 @@ _CHANNELS = [ ), DLHDChannel(number="66", name="TUDN USA", country_code="us", xmltv_id="TUDN.us", call_sign="TUDN"), DLHDChannel(number="67", name="beIN SPORTS 4 Turkey", country_code="tr", xmltv_id="beINSports4.tr", call_sign=""), - DLHDChannel(number="70", name="Sky Sports Golf UK", country_code="uk", xmltv_id="SkySportsGolf.uk", call_sign=""), + DLHDChannel( + number="70", + name="Sky Sports Golf UK", + country_code="uk", + xmltv_id="SkySportsGolf.uk", + call_sign="sky-sports-golf", + ), DLHDChannel( number="71", name="Eleven Sports 1 Poland", country_code="pl", xmltv_id="ElevenSport1.pl", call_sign="" ), @@ -151,7 +167,7 @@ _CHANNELS = [ name="Sky sports Premier League", country_code="uk", xmltv_id="SkySportsPremiereLeague.uk", - call_sign="", + call_sign="sky-sports-premier-league", ), DLHDChannel(number="131", name="Telemundo", country_code="us", xmltv_id="WKAQ.us", call_sign="WNJU"), DLHDChannel(number="132", name="Univision", country_code="ca", xmltv_id="UnivisionCanada.ca", call_sign=""), @@ -276,16 +292,16 @@ _CHANNELS = [ DLHDChannel(number="345", name="CNN USA", country_code="us", xmltv_id="CNN.us", call_sign="CNN"), DLHDChannel(number="346", name="Willow Cricket", country_code="", xmltv_id="WillowCricket.us", call_sign=""), DLHDChannel(number="347", name="Fox News", country_code="us", xmltv_id="FoxNews.us", call_sign=""), - DLHDChannel(number="348", name="Dave", country_code="uk", xmltv_id="Dave.uk", call_sign=""), - DLHDChannel(number="349", name="BBC News Channel HD", country_code="uk", xmltv_id="", call_sign=""), - DLHDChannel(number="350", name="ITV 1 UK", country_code="uk", xmltv_id="ITV1.uk", call_sign=""), - DLHDChannel(number="351", name="ITV 2 UK", country_code="uk", xmltv_id="ITV2.uk", call_sign=""), - DLHDChannel(number="352", name="ITV 3 UK", country_code="uk", xmltv_id="ITV3.uk", call_sign=""), - DLHDChannel(number="353", name="ITV 4 UK", country_code="uk", xmltv_id="ITV4.uk", call_sign=""), - DLHDChannel(number="354", name="Channel 4 UK", country_code="uk", xmltv_id="Channel4.uk", call_sign=""), - DLHDChannel(number="355", name="Channel 5 UK", country_code="uk", xmltv_id="Channel5.uk", call_sign=""), - DLHDChannel(number="356", name="BBC One UK", country_code="uk", xmltv_id="BBC1.uk", call_sign=""), - DLHDChannel(number="357", name="BBC Two UK", country_code="uk", xmltv_id="BBC2.uk", call_sign=""), + DLHDChannel(number="348", name="Dave", country_code="uk", xmltv_id="Dave.uk", call_sign="dave"), + DLHDChannel(number="349", name="BBC News Channel HD", country_code="uk", xmltv_id="", call_sign="bbc-news-channel"), + DLHDChannel(number="350", name="ITV 1 UK", country_code="uk", xmltv_id="ITV1.uk", call_sign="itv1"), + DLHDChannel(number="351", name="ITV 2 UK", country_code="uk", xmltv_id="ITV2.uk", call_sign="itv2"), + DLHDChannel(number="352", name="ITV 3 UK", country_code="uk", xmltv_id="ITV3.uk", call_sign="itv3"), + DLHDChannel(number="353", name="ITV 4 UK", country_code="uk", xmltv_id="ITV4.uk", call_sign="itv4"), + DLHDChannel(number="354", name="Channel 4 UK", country_code="uk", xmltv_id="Channel4.uk", call_sign="channel-4"), + DLHDChannel(number="355", name="Channel 5 UK", country_code="uk", xmltv_id="Channel5.uk", call_sign="channel-5"), + DLHDChannel(number="356", name="BBC One UK", country_code="uk", xmltv_id="BBC1.uk", call_sign="bbc-one"), + DLHDChannel(number="357", name="BBC Two UK", country_code="uk", xmltv_id="BBC2.uk", call_sign="bbc-two"), DLHDChannel(number="358", name="BBC Three UK", country_code="uk", xmltv_id="BBC3.uk", call_sign=""), DLHDChannel(number="359", name="BBC Four UK", country_code="uk", xmltv_id="BBC4.uk", call_sign=""), DLHDChannel(number="360", name="5 USA", country_code="us", xmltv_id="", call_sign=""), @@ -791,7 +807,7 @@ _CHANNELS = [ ), DLHDChannel(number="682", name="Sky Showcase UK", country_code="uk", xmltv_id="SkyShowcase.uk", call_sign=""), DLHDChannel(number="683", name="Sky Arts UK", country_code="uk", xmltv_id="SkyArts.uk", call_sign=""), - DLHDChannel(number="684", name="Sky Comedy UK", country_code="uk", xmltv_id="SkyComedy.uk", call_sign=""), + DLHDChannel(number="684", name="Sky Comedy UK", country_code="uk", xmltv_id="SkyComedy.uk", call_sign="sky-comedy"), DLHDChannel(number="685", name="Showtime SHOxBET USA", country_code="us", xmltv_id="ShowtimeXBet.us", call_sign=""), DLHDChannel(number="686", name="Sky History", country_code="gb", xmltv_id="HistoryChannel.uk", call_sign=""), DLHDChannel(number="687", name="Gold UK", country_code="gb", xmltv_id="StarGold.uk", call_sign=""), diff --git a/src/dlhdhr/epg/__init__.py b/src/dlhdhr/epg/__init__.py index 9fbea69..75caffa 100644 --- a/src/dlhdhr/epg/__init__.py +++ b/src/dlhdhr/epg/__init__.py @@ -6,18 +6,19 @@ from xml.etree.ElementTree import Element, tostring from dlhdhr.dlhd import DLHDChannel from dlhdhr.epg.zap2it import Zap2it from dlhdhr.epg.program import Program +from dlhdhr.epg.zaptv import ZapTV @dataclass() class EPG: zap2it: Zap2it = field(default_factory=Zap2it) + zaptv: ZapTV = field(default_factory=ZapTV) async def get_channel_programs(self, channel: DLHDChannel) -> list[Program]: if channel.country_code == "us": return await self.zap2it.get_channel_programs(channel) elif channel.country_code == "uk": - # TODO: TV24? TV Guide? - return [] + return await self.zaptv.get_channel_programs(channel) return [] diff --git a/src/dlhdhr/epg/zaptv.py b/src/dlhdhr/epg/zaptv.py new file mode 100644 index 0000000..f9d899f --- /dev/null +++ b/src/dlhdhr/epg/zaptv.py @@ -0,0 +1,127 @@ +import datetime +from dataclasses import dataclass, field +import time + +import httpx + +from dlhdhr import config +from dlhdhr.dlhd.channels import DLHDChannel +from dlhdhr.epg.program import Program +from dlhdhr.epg.program import Rating + + +@dataclass() +class ZapTV: + _BASE_URL = "https://www.zaptv.co.uk/api/" + _listings: dict[str, Program] = field(default_factory=dict) + _last_fetch: float = 0 + + def _get_client(self) -> httpx.AsyncClient: + return httpx.AsyncClient( + base_url=self._BASE_URL, + timeout=2.0, + verify=True, + max_redirects=1, + headers={ + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:120.0) Gecko/20100101 Firefox/120.0", + "Referer": "https://www.zaptv.co.uk/", + "Accept": "application/json", + }, + ) + + def _cleanup_listings(self) -> None: + now = datetime.datetime.now(datetime.UTC) + + updated: dict[str, list[Program]] = {} + for call_sign, programs in self._listings.items(): + updated_programs = [p for p in programs if p.end_time > now] + if updated_programs: + updated[call_sign] = updated_programs + self._listings = updated + + async def _fetch_listings(self) -> dict[str, list[Program]]: + listings: dict[str, list[Program]] = {} + now = datetime.datetime.now(datetime.UTC) + async with self._get_client() as client: + channels = set() + events = {} + + # TODO: Can we fetch tomorrows data as well? + res = await client.get(f"/schedules/today") + res.raise_for_status() + + data = res.json() + for d in data: + channel = d["channel"] + code = channel["code"] + channels.add(code) + + if code not in events: + events[code] = {} + + for broadcast in d["broadcasts"]: + events[code][broadcast["uid"]] = broadcast + + for code in channels: + programs = [] + for evt_data in events[code].values(): + end_time = datetime.datetime.fromisoformat(evt_data["endsAt"]) + if end_time < now: + continue + + ep_data = evt_data["metadata"].get("episode") or {} + season = ep_data.get("season") or None + if season is not None: + season = str(season) + episode = ep_data.get("number") or None + if episode is not None: + episode = str(episode) + + release_year = evt_data["metadata"].get("year") + if release_year is not None: + release_year = str(release_year) + + programs.append( + Program( + start_time=datetime.datetime.fromisoformat(evt_data["startsAt"]), + end_time=end_time, + title=evt_data["title"], + description="", + season=season, + episode=episode, + tags=[], + release_year=release_year, + thumbnail=evt_data["image"] or None, + rating=None, + ) + ) + + listings[code] = sorted(programs, key=lambda p: p.start_time, reverse=True) + + return listings + + async def _refresh_listings(self) -> dict[str, list[Program]]: + self._cleanup_listings() + + now = time.time() + if self._listings and now - self._last_fetch > config.ZAPTV_REFRESH_DELAY: + return self._listings + + programs = await self._fetch_listings() + for code, programs in programs.items(): + if code in self._listings: + self._listings[code].extend(programs) + else: + self._listings[code] = programs + return self._listings + + async def get_channel_programs(self, channel: DLHDChannel) -> list[Program]: + if not channel.call_sign: + return [] + + await self._refresh_listings() + + if channel.call_sign not in self._listings: + return [] + + return self._listings[channel.call_sign]