-
Notifications
You must be signed in to change notification settings - Fork 201
LiveTV support (both DVR and free Plex streaming/IPTV) - Requesting code review #543
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
nwithan8
wants to merge
23
commits into
pushingkarmaorg:master
Choose a base branch
from
nwithan8:master
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
360a781
Initial commit for Live TV support
nwithan8 1e8b3ef
Initial commit for Live TV support
nwithan8 35aea18
Merge pull request #1 from pkkid/master
nwithan8 adea3d3
New Directory class for IPTV channels
nwithan8 11fea1c
New iptv() method to get Plex Live TV channel hubs
nwithan8 d86ba0a
Initial commit for limited Live TV (DVR) and IPTV (Free Plex streams)…
nwithan8 2d88f34
new datetimeToTimestamp method, bug fix
nwithan8 c1b77a3
Line length fix for linter
nwithan8 c7453d1
Reused sessions, datetime rather than int in guide item methods, prop…
nwithan8 4aecf28
Grab 'art' attribute for IPTVChannel, bug fix for iptv()
nwithan8 7a7d6f2
Fix conflicts
nwithan8 28e1faf
Merge branch 'master' into master
nwithan8 afc7c72
Merge branch 'master' into master
nwithan8 ead72e9
Added return type documentation
nwithan8 eb97cb1
Made some attributes (news, tidal, iptv, etc) as properties rather th…
nwithan8 faa59da
Fixed LiveTV import
nwithan8 a1f88b4
Added helper methods for XML parsing, xmltodict
nwithan8 a8b1b03
Abstracted server.query() with private function to get the raw reques…
nwithan8 963df3a
Fixed getting cloud key (now livetv_key)
nwithan8 605319c
Items and size cached, can be reloaded manually
nwithan8 b660c3b
Moved response code check out of queryReturnResponse
nwithan8 6cbf9df
Handle both kinds of livetv keys (cloud (ZIP code guide) and xmltv (l…
nwithan8 e395f9e
Fixed error when grabbing guide
nwithan8 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,285 @@ | ||
# -*- coding: utf-8 -*- | ||
import os | ||
from typing import List | ||
from urllib.parse import quote_plus, urlencode | ||
from datetime import datetime | ||
import requests | ||
|
||
from plexapi import media, utils, settings, library | ||
from plexapi.base import PlexObject, Playable, PlexPartialObject | ||
from plexapi.exceptions import BadRequest, NotFound | ||
from plexapi.media import Session | ||
from plexapi.video import Video | ||
from requests.status_codes import _codes as codes | ||
|
||
|
||
@utils.registerPlexObject | ||
class IPTVChannel(Video): | ||
""" Represents a single IPTVChannel.""" | ||
|
||
TAG = 'Directory' | ||
TYPE = 'channel' | ||
METADATA_TYPE = 'channel' | ||
|
||
def _loadData(self, data): | ||
self._data = data | ||
self.art = data.attrib.get('art') | ||
self.guid = data.attrib.get('id') | ||
self.thumb = data.attrib.get('thumb') | ||
self.title = data.attrib.get('title') | ||
self.type = data.attrib.get('type') | ||
self.items = self.findItems(data) | ||
|
||
|
||
@utils.registerPlexObject | ||
class Recording(Video): | ||
""" Represents a single Recording.""" | ||
|
||
TAG = 'MediaSubscription' | ||
|
||
def _loadData(self, data): | ||
self._data = data | ||
self.key = data.attrib.key('key') | ||
self.type = data.attrib.key('type') | ||
self.targetLibrarySectionId = data.attrib.get('targetLibrarySectionId') | ||
self.createdAt = utils.toDatetime(data.attrib.get('createdAt')) | ||
self.title = data.attrib.get('title') | ||
self.items = self.findItems(data) | ||
|
||
def delete(self): | ||
self._server.query(key='/media/subscription/' + self.key, method=self._server._session.delete) | ||
|
||
|
||
@utils.registerPlexObject | ||
class ScheduledRecording(Video): | ||
""" Represents a single ScheduledRecording.""" | ||
|
||
TAG = 'MediaGrabOperation' | ||
|
||
def _loadData(self, data): | ||
self._data = data | ||
self.mediaSubscriptionID = data.attrib.get('mediaSubscriptionID') | ||
self.mediaIndex = data.attrib.get('mediaIndex') | ||
self.key = data.attrib.key('key') | ||
self.grabberIdentifier = data.attrib.get('grabberIdentifier') | ||
self.grabberProtocol = data.attrib.get('grabberProtocol') | ||
self.deviceID = data.attrib.get('deviceID') | ||
self.status = data.attrib.get('status') | ||
self.provider = data.attrib.get('provider') | ||
self.items = self.findItems(data) | ||
|
||
|
||
@utils.registerPlexObject | ||
class Setting(PlexObject): | ||
""" Represents a single DVRDevice Setting.""" | ||
|
||
TAG = 'Setting' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This conflicts with Setting(PlexObject) in settings.py |
||
|
||
def _loadData(self, data): | ||
self._data = data | ||
self.id = data.attrib.get('id') | ||
self.label = data.attrib.get('label') | ||
self.summary = data.attrib.get('summary') | ||
self.type = data.attrib.get('type') | ||
self.default = data.attrib.get('default') | ||
self.value = data.attrib.get('value') | ||
self.hidden = data.attrib.get('hidden') | ||
self.advanced = data.attrib.get('advanced') | ||
self.group = data.attrib.get('group') | ||
self.enumValues = data.attrib.get('enumValues') | ||
self.items = self.findItems(data) | ||
|
||
|
||
@utils.registerPlexObject | ||
class DVRChannel(PlexObject): | ||
""" Represents a single DVRDevice DVRChannel.""" | ||
|
||
TAG = 'ChannelMapping' | ||
|
||
def _loadData(self, data): | ||
self._data = data | ||
self.channelKey = data.attrib.get('channelKey') | ||
self.deviceIdentifier = data.attrib.get('deviceIdentifier') | ||
self.enabled = utils.cast(int, data.attrib.get('enabled')) | ||
self.lineupIdentifier = data.attrib.get('lineupIdentifier') | ||
self.items = self.findItems(data) | ||
|
||
|
||
@utils.registerPlexObject | ||
class DVRDevice(PlexObject): | ||
""" Represents a single DVRDevice.""" | ||
|
||
TAG = 'Device' | ||
|
||
def _loadData(self, data): | ||
self._data = data | ||
self.parentID = data.attrib.get('parentID') | ||
self.key = data.attrib.get('key', '') | ||
self.uuid = data.attrib.get('uuid') | ||
self.uri = data.attrib.get('uri') | ||
self.protocol = data.attrib.get('protocol') | ||
self.status = data.attrib.get('status') | ||
self.state = data.attrib.get('state') | ||
self.lastSeenAt = utils.toDatetime(data.attrib.get('lastSeenAt')) | ||
self.make = data.attrib.get('make') | ||
self.model = data.attrib.get('model') | ||
self.modelNumber = data.attrib.get('modelNumber') | ||
self.source = data.attrib.get('source') | ||
self.sources = data.attrib.get('sources') | ||
self.thumb = data.attrib.get('thumb') | ||
self.tuners = utils.cast(int, data.attrib.get('tuners')) | ||
self.items = self.findItems(data) | ||
|
||
|
||
@utils.registerPlexObject | ||
class DVR(DVRDevice): | ||
""" Represents a single DVR.""" | ||
|
||
TAG = 'Dvr' | ||
|
||
def _loadData(self, data): | ||
self._data = data | ||
self.key = utils.cast(int, data.attrib.get('key')) | ||
self.uuid = data.attrib.get('uuid') | ||
self.language = data.attrib.get('language') | ||
self.lineupURL = data.attrib.get('lineup') | ||
self.title = data.attrib.get('lineupTitle') | ||
self.country = data.attrib.get('country') | ||
self.refreshTime = utils.toDatetime(data.attrib.get('refreshedAt')) | ||
self.epgIdentifier = data.attrib.get('epgIdentifier') | ||
self.items = self.findItems(data) | ||
|
||
|
||
class LiveTV(PlexObject): | ||
def __init__(self, server, data, session=None, token=None): | ||
self._token = token | ||
self._session = session or requests.Session() | ||
self._server = server | ||
self._dvrs = [] # cached DVR objects | ||
self._cloud_key = None # used if cloud XML (zip code) | ||
self._xmltv_key = None # used if local XML (XML path) | ||
super().__init__(server, data) | ||
|
||
def _loadData(self, data): | ||
self._data = data | ||
|
||
def _parseXmlToDict(self, key: str): | ||
response = self._server._queryReturnResponse(key=key) | ||
if not response: | ||
return None | ||
return utils.parseXmlToDict(xml_data_string=response.text) | ||
|
||
@property | ||
def cloud_key(self): | ||
if not self._cloud_key: | ||
data = self._parseXmlToDict(key='/tv.plex.providers.epg.cloud') | ||
if not data: | ||
return None | ||
try: | ||
self._cloud_key = data['MediaContainer']['Directory'][1]['@title'] | ||
except: | ||
pass | ||
return self._cloud_key | ||
|
||
@property | ||
def xmltv_key(self): | ||
if not self._xmltv_key: | ||
data = self._parseXmlToDict(key='/tv.plex.providers.epg.xmltv') | ||
if not data: | ||
return None | ||
try: | ||
self._xmltv_key = data['MediaContainer']['Directory'][1]['@title'] | ||
except: | ||
pass | ||
return self._xmltv_key | ||
|
||
@property | ||
def dvrs(self) -> List[DVR]: | ||
""" Returns a list of :class:`~plexapi.livetv.DVR` objects available to your server. | ||
""" | ||
if not self._dvrs: | ||
self._dvrs = self.fetchItems('/livetv/dvrs') | ||
return self._dvrs | ||
|
||
@property | ||
def sessions(self) -> List[Session]: | ||
""" Returns a list of all active live tv session (currently playing) media objects. | ||
""" | ||
return self.fetchItems('/livetv/sessions') | ||
|
||
@property | ||
def hubs(self): | ||
""" Returns a list of all :class:`~plexapi.livetv.Hub` objects available to your server. | ||
""" | ||
hubs = [] | ||
if self.cloud_key: | ||
hubs.extend(self._server.fetchItems("/" + self.cloud_key + '/hubs/discover')) | ||
if self.xmltv_key: | ||
hubs.extend(self._server.fetchItems("/" + self.xmltv_key + '/hubs/discover')) | ||
return hubs | ||
|
||
@property | ||
def recordings(self): | ||
return self.fetchItems('/media/subscriptions/scheduled') | ||
|
||
@property | ||
def scheduled(self): | ||
return self.fetchItems('/media/subscriptions') | ||
|
||
def _guide_items(self, key, grid_type: int, beginsAt: datetime = None, endsAt: datetime = None): | ||
""" Returns a list of all guide items | ||
|
||
Parameters: | ||
key (str): cloud_key or xmltv_key | ||
grid_type (int): 1 for movies, 4 for shows | ||
beginsAt (datetime): Limit results to beginning after UNIX timestamp (epoch). | ||
endsAt (datetime): Limit results to ending before UNIX timestamp (epoch). | ||
""" | ||
key = '/%s/grid?type=%s' % (key, grid_type) | ||
if beginsAt: | ||
key += '&beginsAt%3C=%s' % utils.datetimeToEpoch(beginsAt) # %3C is <, so <= | ||
if endsAt: | ||
key += '&endsAt%3E=%s' % utils.datetimeToEpoch(endsAt) # %3E is >, so >= | ||
return self._server.fetchItems(key) | ||
|
||
def movies(self, beginsAt: datetime = None, endsAt: datetime = None): | ||
""" Returns a list of all :class:`~plexapi.video.Movie` items on the guide. | ||
|
||
Parameters: | ||
beginsAt (datetime): Limit results to beginning after UNIX timestamp (epoch). | ||
endsAt (datetime): Limit results to ending before UNIX timestamp (epoch). | ||
""" | ||
movies = [] | ||
if self.cloud_key: | ||
movies.extend(self._guide_items(key=self.cloud_key, grid_type=1, beginsAt=beginsAt, endsAt=endsAt)) | ||
if self.xmltv_key: | ||
movies.extend(self._guide_items(key=self.xmltv_key, grid_type=1, beginsAt=beginsAt, endsAt=endsAt)) | ||
return movies | ||
|
||
def shows(self, beginsAt: datetime = None, endsAt: datetime = None): | ||
""" Returns a list of all :class:`~plexapi.video.Show` items on the guide. | ||
|
||
Parameters: | ||
beginsAt (datetime): Limit results to beginning after UNIX timestamp (epoch). | ||
endsAt (datetime): Limit results to ending before UNIX timestamp (epoch). | ||
""" | ||
shows = [] | ||
if self.cloud_key: | ||
shows.extend(self._guide_items(key=self.cloud_key, grid_type=4, beginsAt=beginsAt, endsAt=endsAt)) | ||
if self.xmltv_key: | ||
shows.extend(self._guide_items(key=self.xmltv_key, grid_type=4, beginsAt=beginsAt, endsAt=endsAt)) | ||
return shows | ||
|
||
def guide(self, beginsAt: datetime = None, endsAt: datetime = None): | ||
""" Returns a list of all media items on the guide. Items may be any of | ||
:class:`~plexapi.video.Movie`, :class:`~plexapi.video.Show`. | ||
|
||
Parameters: | ||
beginsAt (datetime): Limit results to beginning after UNIX timestamp (epoch). | ||
endsAt (datetime): Limit results to ending before UNIX timestamp (epoch). | ||
""" | ||
all_movies = self.movies(beginsAt, endsAt) | ||
return all_movies | ||
# Potential show endpoint currently hanging, do not use | ||
# all_shows = self.shows(beginsAt, endsAt) | ||
# return all_movies + all_shows |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -66,6 +66,15 @@ def _loadData(self, data): | |
self.proxyType = cast(int, data.attrib.get('proxyType')) | ||
self.target = data.attrib.get('target') | ||
self.title = data.attrib.get('title') | ||
self.protocol = data.attrib.get('protocol') | ||
self.channelCallSign = data.attrib.get('channelCallSign') | ||
self.channelIdentifier = data.attrib.get('channelIdentifier') | ||
self.channelThumb = data.attrib.get('channelThumb') | ||
self.channelTitle = data.attrib.get('channelTitle') | ||
self.beginsAt = utils.toDatetime(data.attrib.get('beginsAt')) | ||
self.endsAt = utils.toDatetime(data.attrib.get('endsAt')) | ||
self.onAir = cast(int, data.attrib.get('onAir')) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. self.onAir = **utils.**cast(int, data.attrib.get('onAir')) |
||
self.channelID = data.attrib.get('channelID') | ||
self.videoCodec = data.attrib.get('videoCodec') | ||
self.videoFrameRate = data.attrib.get('videoFrameRate') | ||
self.videoProfile = data.attrib.get('videoProfile') | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.