diff --git a/README.md b/README.md index 30a8f01..252b07e 100644 --- a/README.md +++ b/README.md @@ -295,7 +295,6 @@ Will return an object with an array of events including the specified number of 'image_url':'https://images.metadata.sky.com/pd-image/57a11caf-1ebd-4c01-a40b-7fdfe5c5fad0/16-9', 'channelname':'BBC One South', 'status':'LIVE', - 'pvrid':'n/a', 'eid':'E4b8-19b' }, { @@ -308,7 +307,6 @@ Will return an object with an array of events including the specified number of 'image_url':'https://images.metadata.sky.com/pd-image/d2d67048-673a-4ea8-8a32-3ad386e306d2/16-9', 'channelname':'BBC One South', 'status':'LIVE', - 'pvrid':'n/a', 'eid':'E4b8-19b' }, {...} @@ -345,7 +343,6 @@ Will return a JSON structure with an array of events including the specified num "image_url":"https://images.metadata.sky.com/pd-image/62ad0457-1a6a-4b45-9ef7-6e144639d734/16-9", "channelname":"BBC One South", "status":"LIVE", - "pvrid":"n/a", "eid":"E4b8-19b" } }, @@ -361,7 +358,6 @@ Will return a JSON structure with an array of events including the specified num "image_url":"https://images.metadata.sky.com/pd-image/a975bdeb-c19b-4de2-9557-c6d2757bdae7/16-9", "channelname":"BBC One South" "status":"LIVE", - "pvrid":"n/a", "eid":"E4b8-19b" } }, @@ -400,7 +396,6 @@ Will return an object such as below: 'image_url':'https://images.metadata.sky.com/pd-image/9fbdcefe-312c-4681-b996-00637e85313a/16-9', 'channelname':'Channel 5 HD', 'status':'LIVE', - 'pvrid':'n/a', 'eid':'E4b8-19b' } ``` @@ -427,7 +422,6 @@ Will return a JSON structure such as below: "image_url":"https://images.metadata.sky.com/pd-image/e11d9e93-0eec-4855-88f5-6ade9946d5dd/16-9", "channelname":"BBC ONE HD", "status":"LIVE", - "pvrid":"n/a", "eid':"E4b8-19b" } } @@ -458,7 +452,6 @@ Will return an object such as below: 'image_url':'https://images.metadata.sky.com/pd-image/9fbdcefe-312c-4681-b996-00637e85313a/16-9', 'channelname':'Channel 5 HD', 'status':'LIVE', - 'pvrid':'n/a', 'eid':'E4b8-19b' } ``` @@ -483,7 +476,6 @@ Will return a JSON structure such as below: "image_url":"https://images.metadata.sky.com/pd-image/e11d9e93-0eec-4855-88f5-6ade9946d5dd/16-9", "channelname":"BBC ONE HD", "status":"LIVE", - "pvrid":"n/a", "eid":"E4b8-19b" } } @@ -510,8 +502,10 @@ Will return an object such as below for the number of recordings specified by li 'image_url':'https://images.metadata.sky.com/pd-image/54bfc205-c56e-4583-b03f-59c31f97f8c7/16-9', 'channelname':'E4 HD', 'status':'RECORDED', + 'deletetime': '2020-09-02T20:00:59Z', + 'failurereason': None, 'pvrid':'P29014192', - 'eid':'E869-67b1' + 'eid':'E869-67b1', }, { 'programmeuuid':'af9ecd2c-5026-4050-9c15-37598fe26713', @@ -523,6 +517,23 @@ Will return an object such as below for the number of recordings specified by li 'image_url':'https://images.metadata.sky.com/pd-image/af9ecd2c-5026-4050-9c15-37598fe26713/16-9', 'channelname':'Channel 5 HD', 'status':'SCHEDULED', + 'deletetime': None, + 'failurereason': None, + 'pvrid':'P29014192', + 'eid':'E869-67b1' + }, + { + 'programmeuuid':'575736fd-0719-4249-88cc-babd6e232bfa', + 'starttime':'2020-08-02T19:58:00Z', + 'endtime':'2020-08-02T21:01:59Z', + 'title':'Lorraine', + 'season':35, + 'episode':4, + 'image_url':'https://images.metadata.sky.com/pd-image/575736fd-0719-4249-88cc-babd6e232bfa/16-9', + 'channelname':'ITV HD', + 'status':'PART REC', + 'deletetime': None, + 'failurereason': 'Start Missed', 'pvrid':'P29014192', 'eid':'E869-67b1' }, @@ -546,9 +557,9 @@ Will return an object such as below for the number of recordings specified by li "attributes":{ }, - "programmes":[ + "recordings":[ { - "__type__":"__programme__", + "__type__":"__recording__", "attributes":{ "programmeuuid":"54bfc205-c56e-4583-b03f-59c31f97f8c7", "starttime":"2020-08-02T19:58:00Z", @@ -559,12 +570,14 @@ Will return an object such as below for the number of recordings specified by li "image_url":"https://images.metadata.sky.com/pd-image/54bfc205-c56e-4583-b03f-59c31f97f8c7/16-9", "channelname":"E4 HD", "status":"RECORDED", + "deletetime": "2020-09-02T20:00:59Z", + "failurereason": null, "pvrid":"P29014192", "eid":"E869-67b1" } }, { - "__type__":"__programme__", + "__type__":"__recording__", "attributes":{ "programmeuuid":"af9ecd2c-5026-4050-9c15-37598fe26713", "starttime":"null", @@ -575,9 +588,28 @@ Will return an object such as below for the number of recordings specified by li "image_url":"https://images.metadata.sky.com/pd-image/af9ecd2c-5026-4050-9c15-37598fe26713/16-9", "channelname":"Channel 5 HD", "status":"SCHEDULED", + "deletetime": null, + "failurereason": null, "pvrid":"P29014192", "eid":"E869-67b1" }, + { + "__type__":"__recording__", + "attributes":{ + "programmeuuid":"af9ecd2c-5026-4050-9c15-37598fe26713", + "starttime":"2020-08-02T19:58:00Z", + "endtime":"2020-08-02T21:01:59Z", + "title":"Home and Away", + "season":35, + "episode":4, + "image_url":"https://images.metadata.sky.com/pd-image/af9ecd2c-5026-4050-9c15-37598fe26713/16-9", + "channelname":"Channel 5 HD", + "status":"PART REC", + "deletetime": null, + "failurereason": "Start Missed", + "pvrid":"P29014192", + "eid":"E869-67b1" + }, {...} } ] @@ -603,6 +635,8 @@ Will return an object such as below: 'episode':5, 'image_url':'https://images.metadata.sky.com/pd-image/ddcd727f-487f-4558-8365-7bed4fe41c87/16-9', 'status':'RECORDED', + 'deletetime': None, + 'failurereason': None, 'pvrid':'P29014192', 'eid':'E869-67b1' } @@ -628,6 +662,8 @@ Will return an object such as below: "episode":null, "image_url":"https://images.metadata.sky.com/pd-image/e11d9e93-0eec-4855-88f5-6ade9946d5dd/16-9", "status":"RECORDED", + "deletetime": null, + "failurereason": null, "pvrid":"P29014192", "eid":"E869-67b1" } diff --git a/pyskyqremote/classes/channelepg.py b/pyskyqremote/classes/channelepg.py index fdbfd70..fcb3681 100644 --- a/pyskyqremote/classes/channelepg.py +++ b/pyskyqremote/classes/channelepg.py @@ -8,7 +8,13 @@ import requests -from ..const import LIVE_IMAGE_URL, RESPONSE_OK, SCHEDULE_URL, SKY_STATUS_LIVE +from ..const import ( + EPG_TIMEOUT, + LIVE_IMAGE_URL, + RESPONSE_OK, + SCHEDULE_URL, + SKY_STATUS_LIVE, +) from .channel import ChannelInformation, build_channel_image_url from .programme import Programme @@ -118,7 +124,6 @@ def _get_data(self, sid, channel_name, epg_date): image_url, channel_name, SKY_STATUS_LIVE, - "n/a", eid, ) programmes.add(programme) @@ -135,7 +140,7 @@ def _get_day_epg_data(self, sid, epg_date): "x-skyott-proposition": "SKYQ", } _LOGGER.debug("Channel Call - %s - %s", self._remote_config.host, epg_url) - resp = requests.get(epg_url, headers=headers) + resp = requests.get(epg_url, headers=headers, timeout=EPG_TIMEOUT) return resp.json()["schedule"] if resp.status_code == RESPONSE_OK else None def _get_channel_node(self, sid): diff --git a/pyskyqremote/classes/programme.py b/pyskyqremote/classes/programme.py index 6c3c0e4..f6c392c 100644 --- a/pyskyqremote/classes/programme.py +++ b/pyskyqremote/classes/programme.py @@ -54,7 +54,6 @@ class Programme: repr=True, compare=False, ) - pvrid: str = "n/a" eid: str = "n/a" def __hash__(self): diff --git a/pyskyqremote/classes/recordings.py b/pyskyqremote/classes/recordings.py index b1e018d..92c0f57 100644 --- a/pyskyqremote/classes/recordings.py +++ b/pyskyqremote/classes/recordings.py @@ -32,7 +32,6 @@ REST_SERIES_LINK, REST_SERIES_UNLINK, ) -from .programme import Programme _LOGGER = logging.getLogger(__name__) @@ -242,27 +241,54 @@ def _build_recording(self, recording): self._remote_config.territory, ) + status = recording["status"] + starttimestamp = 0 - if "ast" in recording: + endtimestamp = 0 + if status == "SCHEDULED": + if "st" in recording: + starttimestamp = recording["st"] + endtimestamp = ( + starttimestamp + recording["schd"] + if "schd" in recording + else starttimestamp + ) + elif status == "RECORDING": starttimestamp = recording["ast"] - elif "st" in recording: - starttimestamp = recording["st"] - starttime = datetime.utcfromtimestamp(starttimestamp) - - endtime = None - if "finald" in recording: - endtime = datetime.utcfromtimestamp(starttimestamp + recording["finald"]) - elif "schd" in recording: - endtime = datetime.utcfromtimestamp(starttimestamp + recording["schd"]) + if recording["fr"] == "N/A": + usedtimestamp = ( + recording["ast"] + if recording["ast"] > recording["st"] + else recording["st"] + ) + endtimestamp = usedtimestamp + recording["schd"] + else: + endtimestamp = recording["st"] + recording["schd"] + # elif status == "RECORDED" or status == "PART REC": + # starttimestamp = recording["ast"] + # endtimestamp = starttimestamp + recording["finald"] else: - endtime = starttime + starttimestamp = recording["ast"] if "ast" in recording else recording["st"] + if "finald" in recording: + endtimestamp = starttimestamp + recording["finald"] + elif "schd" in recording: + endtimestamp = starttimestamp + recording["schd"] + else: + endtimestamp = starttimestamp + + starttime = datetime.utcfromtimestamp(starttimestamp) + endtime = datetime.utcfromtimestamp(endtimestamp) pvrid = recording["pvrid"] eid = recording["oeid"] if "oeid" in recording else None - status = recording["status"] - return Programme( + deletetime = ( + datetime.utcfromtimestamp(recording["del"]) if "del" in recording else None + ) + failurereason = recording["fr"] if "fr" in recording else None + + return Recording( programmeuuid, starttime, endtime, @@ -272,6 +298,8 @@ def _build_recording(self, recording): image_url, channel, status, + deletetime, + failurereason, pvrid, eid, ) @@ -281,7 +309,7 @@ def _build_recording(self, recording): class Recordings: """SkyQ Channel EPG Class.""" - programmes: set = field( + recordings: set = field( init=True, repr=True, compare=False, @@ -297,7 +325,7 @@ def recordings_decoder(obj): recordings = json.loads(obj, object_hook=_json_decoder_hook) if "__type__" in recordings and recordings["__type__"] == "__recordings__": return Recordings( - programmes=recordings["programmes"], **recordings["attributes"] + recordings=recordings["recordings"], **recordings["attributes"] ) return recordings @@ -308,8 +336,10 @@ def _json_decoder_hook(obj): obj["starttime"] = datetime.strptime(obj["starttime"], "%Y-%m-%dT%H:%M:%SZ") if "endtime" in obj: obj["endtime"] = datetime.strptime(obj["endtime"], "%Y-%m-%dT%H:%M:%SZ") - if "__type__" in obj and obj["__type__"] == "__programme__": - obj = Programme(**obj["attributes"]) + if "deletetime" in obj: + obj["deletetime"] = datetime.strptime(obj["deletetime"], "%Y-%m-%dT%H:%M:%SZ") + if "__type__" in obj and obj["__type__"] == "__recording__": + obj = Recording(**obj["attributes"]) return obj @@ -317,31 +347,120 @@ class _RecordingsJSONEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, Recordings): type_ = "__recordings__" - programmes = o.programmes - attributes = {k: v for k, v in vars(o).items() if k not in {"programmes"}} + recordings = o.recordings + attributes = {k: v for k, v in vars(o).items() if k not in {"recordings"}} return { "__type__": type_, "attributes": attributes, - "programmes": programmes, + "recordings": recordings, } if isinstance(o, set): return list(o) - if isinstance(o, Programme): + if isinstance(o, Recording): attributes = {} for k, val in vars(o).items(): if isinstance(val, datetime): val = val.strftime("%Y-%m-%dT%H:%M:%SZ") attributes[k] = val return { - "__type__": "__programme__", + "__type__": "__recording__", "attributes": attributes, } json.JSONEncoder.default(self, o) # pragma: no cover +@dataclass(order=True) +class Recording: + """SkyQ Recording Class.""" + + programmeuuid: str = field( + init=True, + repr=True, + compare=False, + ) + starttime: datetime = field( + init=True, + repr=True, + compare=True, + ) + endtime: datetime = field( + init=True, + repr=True, + compare=False, + ) + title: str = field( + init=True, + repr=True, + compare=True, + ) + season: str = field( + init=True, + repr=True, + compare=False, + ) + episode: str = field( + init=True, + repr=True, + compare=False, + ) + image_url: str = field( + init=True, + repr=True, + compare=False, + ) + channelname: str = field( + init=True, + repr=True, + compare=False, + ) + status: str = field( + init=True, + repr=True, + compare=False, + ) + deletetime: datetime = field( + init=True, + repr=True, + compare=False, + ) + failurereason: str = field(init=True, repr=True, compare=False) + pvrid: str = "n/a" + eid: str = "n/a" + + def __hash__(self): + """Calculate the hash of this object.""" + return hash(self.starttime) + + def as_json(self) -> str: + """Return a JSON string representing this Recording.""" + return json.dumps(self, cls=_RecordingJSONEncoder) + + +def recordingdecoder(obj): + """Decode recording object from json.""" + recording = json.loads(obj, object_hook=_json_decoder_hook) + if "__type__" in recording and recording["__type__"] == "__recording__": + return Recording(**recording["attributes"]) + return recording + + +class _RecordingJSONEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, Recording): + attributes = {} + for k, val in vars(o).items(): + if isinstance(val, datetime): + val = val.strftime("%Y-%m-%dT%H:%M:%SZ") + attributes[k] = val + return { + "__type__": "__recording__", + "attributes": attributes, + } + + @dataclass class Quota: """SkyQ Quota Class.""" diff --git a/pyskyqremote/const.py b/pyskyqremote/const.py index 4d7b5a4..2e6b2c3 100644 --- a/pyskyqremote/const.py +++ b/pyskyqremote/const.py @@ -136,6 +136,7 @@ RESPONSE_OK = 200 CONNECT_TIMEOUT = 1000 +EPG_TIMEOUT = 60 HTTP_TIMEOUT = 6 SOAP_TIMEOUT = 2 diff --git a/tests/bash_sky_test.py b/tests/bash_sky_test.py index 424e6ec..d1858c6 100644 --- a/tests/bash_sky_test.py +++ b/tests/bash_sky_test.py @@ -91,8 +91,8 @@ # epgJSON = sky.get_epg_data(sid, queryDate).as_json() # print(epgJSON) -# print("----------- Get scheduled recordings") -# print(sky.get_recordings("SCHEDULED").as_json()) +print("----------- Get scheduled recordings") +print(sky.get_recordings("PART REC").as_json()) print("----------- Get quota info") print(sky.get_quota().as_json()) @@ -106,7 +106,7 @@ # pvrid = next( # ( # recording.pvrid -# for recording in recordings.programmes +# for recording in recordings.recordings # if recording.eid == eid # ), # None,