-
Notifications
You must be signed in to change notification settings - Fork 200
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
base: master
Are you sure you want to change the base?
Changes from 6 commits
360a781
1e8b3ef
35aea18
adea3d3
11fea1c
d86ba0a
2d88f34
c1b77a3
c7453d1
4aecf28
7a7d6f2
28e1faf
afc7c72
ead72e9
eb97cb1
faa59da
a1f88b4
a8b1b03
963df3a
605319c
b660c3b
6cbf9df
e395f9e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,232 @@ | ||
# -*- coding: utf-8 -*- | ||
import os | ||
from urllib.parse import quote_plus, urlencode | ||
import requests | ||
|
||
from plexapi import media, utils, settings, library | ||
from plexapi.base import PlexObject, Playable, PlexPartialObject | ||
from plexapi.exceptions import BadRequest, NotFound | ||
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.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 | ||
nwithan8 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
super().__init__(server, data) | ||
|
||
def _loadData(self, data): | ||
""" Load attribute values from Plex XML response. """ | ||
self._data = data | ||
self.cloud_key = data.attrib.get('machineIdentifier') | ||
|
||
def _get_cloud_key(self): | ||
url = self._server.url(key='/tv.plex.providers.epg.cloud', includeToken=True) | ||
data = requests.get(url=url).json() | ||
nwithan8 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if data: | ||
self.cloud_key = data.get('MediaContainer').get('Directory')[1].get('title') | ||
nwithan8 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return self.cloud_key | ||
return None | ||
|
||
def dvrs(self): | ||
""" 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 | ||
|
||
def sessions(self): | ||
""" Returns a list of all active live tv session (currently playing) media objects. | ||
""" | ||
return self.fetchItems('/livetv/sessions') | ||
|
||
def directories(self): | ||
""" Returns a list of all :class:`~plexapi.livetv.Directory` objects available to your server. | ||
""" | ||
return self._server.fetchItems(self.cloud_key + '/hubs/discover') | ||
|
||
def _guide_items(self, grid_type: int, beginsAt: int = None, endsAt: int = None): | ||
nwithan8 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
key = self.cloud_key + '/grid?type=' + str(grid_type) | ||
nwithan8 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if beginsAt: | ||
key += '&beginsAt%3C=' + str(beginsAt) # %3C is <, so <= | ||
if endsAt: | ||
key += '&endsAt%3E=' + str(endsAt) # %3E is >, so >= | ||
return self._server.fetchItems(key) | ||
|
||
def movies(self, beginsAt: int = None, endsAt: int = None): | ||
""" Returns a list of all :class:`~plexapi.video.Movie` items on the guide. | ||
|
||
Parameters: | ||
grid_type (int): 1 for movies, 4 for shows | ||
beginsAt (int): Limit results to beginning after UNIX timestamp (epoch). | ||
endsAt (int): Limit results to ending before UNIX timestamp (epoch). | ||
""" | ||
return self._guide_items(grid_type=1, beginsAt=beginsAt, endsAt=endsAt) | ||
|
||
def shows(self, beginsAt: int = None, endsAt: int = None): | ||
""" Returns a list of all :class:`~plexapi.video.Show` items on the guide. | ||
|
||
Parameters: | ||
beginsAt (int): Limit results to beginning after UNIX timestamp (epoch). | ||
endsAt (int): Limit results to ending before UNIX timestamp (epoch). | ||
""" | ||
return self._guide_items(grid_type=4, beginsAt=beginsAt, endsAt=endsAt) | ||
|
||
def guide(self, beginsAt: int = None, endsAt: int = 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 (int): Limit results to beginning after UNIX timestamp (epoch). | ||
endsAt (int): 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 | ||
|
||
def recordings(self): | ||
return self.fetchItems('/media/subscriptions/scheduled') | ||
|
||
def scheduled(self): | ||
return self.fetchItems('/media/subscriptions') |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -55,6 +55,15 @@ def _loadData(self, data): | |
self.optimizedForStreaming = cast(bool, data.attrib.get('optimizedForStreaming')) | ||
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') | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ | |
from plexapi import media, utils, settings, library | ||
from plexapi.base import Playable, PlexPartialObject | ||
from plexapi.exceptions import BadRequest, NotFound | ||
from plexapi.media import Media | ||
|
||
|
||
class Video(PlexPartialObject): | ||
|
@@ -32,6 +33,8 @@ def _loadData(self, data): | |
""" Load attribute values from Plex XML response. """ | ||
self._data = data | ||
self.listType = 'video' | ||
self.guid = data.attrib.get('guid') | ||
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. Im not sold on the changed to the video class. This is getting reusing many places. Would it be better for subclass used own class for live tv/recordings) ? Im not sure. 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. Hasn't given me an issue, but I can subclass it just to make sure 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. Strike that, the TAG and type are the same for Live videos and non-live videos, so I can't register a new Plex object. Plex doesn't make a distinction between them, so I think it's safe to just add the guid and live attributes, and the record method (checks if video is live first) |
||
self.year = data.attrib.get('year') | ||
self.addedAt = utils.toDatetime(data.attrib.get('addedAt')) | ||
self.key = data.attrib.get('key', '') | ||
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt')) | ||
|
@@ -55,7 +58,7 @@ def thumbUrl(self): | |
""" Return the first first thumbnail url starting on | ||
the most specific thumbnail for that item. | ||
""" | ||
thumb = self.firstAttr('thumb', 'parentThumb', 'granparentThumb') | ||
thumb = self.firstAttr('thumb', 'parentThumb', 'grandparentThumb') | ||
return self._server.url(thumb, includeToken=True) if thumb else None | ||
|
||
@property | ||
|
@@ -294,7 +297,7 @@ def _loadData(self, data): | |
self.guid = data.attrib.get('guid') | ||
self.originalTitle = data.attrib.get('originalTitle') | ||
self.originallyAvailableAt = utils.toDatetime( | ||
data.attrib.get('originallyAvailableAt'), '%Y-%m-%d') | ||
data.attrib.get('originallyAvailableAt'), format='%Y-%m-%d %H:%M%S') | ||
self.primaryExtraKey = data.attrib.get('primaryExtraKey') | ||
self.rating = utils.cast(float, data.attrib.get('rating')) | ||
self.ratingImage = data.attrib.get('ratingImage') | ||
|
@@ -703,6 +706,7 @@ def _loadData(self, data): | |
self.rating = utils.cast(float, data.attrib.get('rating')) | ||
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0)) | ||
self.year = utils.cast(int, data.attrib.get('year')) | ||
self.live = utils.cast(int, data.attrib.get('live', '0')) | ||
self.directors = self.findItems(data, media.Director) | ||
self.media = self.findItems(data, media.Media) | ||
self.writers = self.findItems(data, media.Writer) | ||
|
@@ -760,6 +764,12 @@ def _defaultSyncTitle(self): | |
""" Returns str, default title for a new syncItem. """ | ||
return '%s - %s - (%s) %s' % (self.grandparentTitle, self.parentTitle, self.seasonEpisode, self.title) | ||
|
||
def record(self): | ||
# TODO | ||
if self.live: | ||
return False | ||
return False | ||
|
||
|
||
@utils.registerPlexObject | ||
class Clip(Playable, Video): | ||
|
Uh oh!
There was an error while loading. Please reload this page.