def __init__(self, media=None, partIndex=0): self.media = media self.part = None self.forceTranscode = False self.isDirectPlayable = False self.videoStream = None self.audioStream = None self.subtitleStream = None self.isSelected = False self.subtitleDecision = self.SUBTITLES_DEFAULT self.sorts = util.AttributeDict() if media: self.indirectHeaders = media.indirectHeaders self.part = media.parts[partIndex] if self.part: # We generally just rely on PMS to have told us selected streams, so # initialize our streams accordingly. self.videoStream = self.part.getSelectedStreamOfType(plexstream.PlexStream.TYPE_VIDEO) self.audioStream = self.part.getSelectedStreamOfType(plexstream.PlexStream.TYPE_AUDIO) self.subtitleStream = self.part.getSelectedStreamOfType(plexstream.PlexStream.TYPE_SUBTITLE) else: util.WARN_LOG("Media does not contain a valid part") util.LOG("Choice media: {0} part:{1}".format(media, partIndex)) for streamType in ("videoStream", "audioStream", "subtitleStream"): attr = getattr(self, streamType) if attr: util.LOG("Choice {0}: {1}".format(streamType, repr(attr))) else: util.WARN_LOG("Could not create media choice for invalid media")
def getServerDecision(self): directPlay = not (self.metadata and self.metadata.isTranscoded) decisionPath = self.getDecisionPath(directPlay) newDecision = None if decisionPath: server = self.metadata.transcodeServer or self.item.getServer() request = plexrequest.PlexRequest(server, decisionPath) response = request.getWithTimeout(10) if response.isSuccess() and response.container: decision = serverdecision.ServerDecision(self, response, self) if decision.isSuccess(): util.LOG("MDE: Server was happy with client's original decision. {0}".format(decision)) elif decision.isDecision(True): util.WARN_LOG("MDE: Server was unhappy with client's original decision. {0}".format(decision)) return decision.getDecision() else: util.LOG("MDE: Server was unbiased about the decision. {0}".format(decision)) # Check if the server has provided a new media item to use it. If # there is no item, then we'll continue along as if there was no # decision made. newDecision = decision.getDecision(False) else: util.WARN_LOG("MDE: Server failed to provide a decision") else: util.WARN_LOG("MDE: Server or item does not support decisions") return newDecision or self
def getPostWithTimeout(self, seconds=10, body=None): if self._cancel: return self.logRequest(body, seconds, False) try: if self.method == 'PUT': res = self.session.put(self.url, timeout=seconds, stream=True) elif self.method == 'DELETE': res = self.session.delete(self.url, timeout=seconds, stream=True) elif self.method == 'HEAD': res = self.session.head(self.url, timeout=seconds, stream=True) elif self.method == 'POST' or body is not None: res = self.session.post(self.url, data=body, timeout=seconds, stream=True) else: res = self.session.get(self.url, timeout=seconds, stream=True) self.currentResponse = res if self._cancel: return None util.LOG("Got a {0} from {1}".format(res.status_code, util.cleanToken(self.url))) # self.event = msg return res except Exception, e: util.WARN_LOG( "Request to {0} errored out after {1} ms: {2}".format( self.url, seconds, e.message))
def chooseMedia(self, item, forceUpdate=False): # If we've already evaluated this item, use our previous choice. if not forceUpdate and item.mediaChoice is not None and item.mediaChoice.media is not None and not item.mediaChoice.media.isIndirect( ): return item.mediaChoice # See if we're missing media/stream details for this item. if item.isLibraryItem() and item.isVideoItem() and len( item.media) > 0 and not item.media[0].hasStreams(): # TODO(schuyler): Fetch the details util.WARN_LOG("Can't make media choice, missing details") # Take a first pass through the media items to create an array of candidates # that we'll evaluate more completely. If we find a forced item, we use it. # If we find an indirect, we only keep a single candidate. indirect = False candidates = [] maxResolution = item.settings.getMaxResolution(item.getQualityType()) for mediaIndex in range(len(item.media)): media = item.media[mediaIndex] media.mediaIndex = mediaIndex if media.isSelected(): candidates = [] candidates.append(media) break if media.isIndirect(): # Only add indirect media if the resolution fits. We cannot # exit early as the user may have selected media. indirect = True if media.getVideoResolution() <= maxResolution: candidates.append(media) elif media.isAccessible(): # Only consider testing available media candidates.append(media) # Only use the first indirect media item if indirect and candidates: candidates = candidates[0] # Make sure we have at least one valid item, regardless of availability if len(candidates) == 0: candidates.append(item.media[0]) # Now that we have an array of candidates, evaluate them completely. choices = [] for media in candidates: choice = None if media is not None: if item.isVideoItem(): choice = self.evaluateMediaVideo(item, media) elif item.isMusicItem(): choice = self.evaluateMediaMusic(item, media) else: choice = mediachoice.MediaChoice(media) choices.append(choice) item.mediaChoice = self.sortChoices(choices)[-1] util.LOG("MDE: MediaChoice: {0}".format(item.mediaChoice)) return item.mediaChoice
def getImageTranscodeURL(self, path, width, height, **extraOpts): if not path: return '' params = ("&width=%s&height=%s" % (width, height)) + ''.join( ["&%s=%s" % (key, extraOpts[key]) for key in extraOpts]) if "://" in path: imageUrl = self.convertUrlToLoopBack(path) else: imageUrl = "http://127.0.0.1:" + self.getLocalServerPort() + path path = "/photo/:/transcode?url=" + compat.quote_plus(imageUrl) + params # Try to use a better server to transcode for synced servers if self.synced: import plexservermanager selectedServer = plexservermanager.MANAGER.getTranscodeServer( "photo") if selectedServer: return selectedServer.buildUrl(path, True) if self.activeConnection: return self.activeConnection.simpleBuildUrl(self, path) else: util.WARN_LOG("Server connection is None, returning an empty url") return ""
def query(self, path, method=None, **kwargs): method = method or self.session.get url = self.buildUrl(path, includeToken=True) # If URL is empty, try refresh resources and return empty set for now if not url: util.WARN_LOG( "Empty server url, returning None and refreshing resources") plexapp.refreshResources(True) return None util.LOG('{0} {1}'.format( method.__name__.upper(), re.sub('X-Plex-Token=[^&]+', 'X-Plex-Token=****', url))) try: response = method(url, **kwargs) if response.status_code not in (200, 201): codename = http.status_codes.get(response.status_code, ['Unknown'])[0] raise exceptions.BadRequest('({0}) {1}'.format( response.status_code, codename)) data = response.text.encode('utf8') except asyncadapter.TimeoutException: util.ERROR() plexapp.refreshResources(True) return None except http.requests.ConnectionError: util.ERROR() return None return ElementTree.fromstring(data) if data else None
def sendTimelineToServer(self, timelineType, timeline, time): if not hasattr(timeline.item, 'getServer') or not timeline.item.getServer(): return serverTimeline = self.getServerTimeline(timelineType) # Only send timeline if it's the first, item changes, playstate changes or timer pops itemsEqual = timeline.item and serverTimeline.item and timeline.item.ratingKey == serverTimeline.item.ratingKey if itemsEqual and timeline.state == serverTimeline.state and not serverTimeline.isExpired( ): return serverTimeline.reset() serverTimeline.item = timeline.item serverTimeline.state = timeline.state # Ignore sending timelines for multi part media with no duration obj = timeline.choice if obj and obj.part and obj.part.duration.asInt( ) == 0 and obj.media.parts and len(obj.media.parts) > 1: util.WARN_LOG( "Timeline not supported: the current part doesn't have a valid duration" ) return # It's possible with timers and in player seeking for the time to be greater than the # duration, which causes a 400, so in that case we'll set the time to the duration. duration = timeline.item.duration.asInt() or timeline.duration if time > duration: time = duration params = util.AttributeDict() params["time"] = time params["duration"] = duration params["state"] = timeline.state params["guid"] = timeline.item.guid params["ratingKey"] = timeline.item.ratingKey params["url"] = timeline.item.url params["key"] = timeline.item.key params["containerKey"] = timeline.item.container.address if timeline.playQueue: params["playQueueItemID"] = timeline.playQueue.selectedId path = "/:/timeline" for paramKey in params: if params[paramKey]: path = http.addUrlParam( path, paramKey + "=" + urllib.quote(str(params[paramKey]))) request = plexrequest.PlexRequest(timeline.item.getServer(), path) context = request.createRequestContext( "timelineUpdate", callback.Callable(self.onTimelineResponse)) context.playQueue = timeline.playQueue plexapp.APP.startRequest(request, context)
def onRequestTimeout(self, context): requestID = context.request.getIdentity() if requestID not in self.pendingRequests: return context.request.cancel() util.WARN_LOG("Request to {0} timed out after {1} sec".format(util.cleanToken(context.request.url), context.timeout)) if context.callback: context.callback(None, context)
def evaluateMediaMusic(self, item, media): # Resolve indirects before doing anything else. if media.isIndirect(): util.LOG("Resolve indirect media for {0}".format(item)) media = media.resolveIndirect() choice = mediachoice.MediaChoice(media) if media is None: return choice # Verify the server supports audio transcoding, otherwise force direct play if not item.getServer().supportsAudioTranscoding: util.LOG( "MDE: force direct play because the server does not support audio transcoding" ) choice.isDirectPlayable = True return choice # See if this part has a server decision to transcode and obey it if choice.part and choice.part.get( "decision", serverdecision.ServerDecision.DECISION_DIRECT_PLAY ) != serverdecision.ServerDecision.DECISION_DIRECT_PLAY: util.WARN_LOG("MDE: Server has decided this cannot direct play") return choice # Verify the codec and container are compatible codec = media.audioCodec container = media.get('container') canPlayCodec = item.settings.supportsAudioStream( codec, media.audioChannels.asInt()) canPlayContainer = ( codec == container) or True # (container in ("mp4", "mka", "mkv")) choice.isDirectPlayable = (canPlayCodec and canPlayContainer) if choice.isDirectPlayable: # Inspect the audio stream attributes if the codec/container can direct # play. For now we only need to verify the sample rate. if choice.audioStream is not None and choice.audioStream.samplingRate.asInt( ) >= 192000: util.LOG("MDE: sampling rate is not compatible") choice.isDirectPlayable = False else: util.LOG("MDE: container or codec is incompatible") return choice
def getDecision(self, requireDecision=True): if not self.item: # Return no decision. The player will either continue with the original # or terminate if a valid decision was required. if requireDecision: # Terminate the player by default if there was no decision returned. code = self.decisionsCodes["generalDecision"] reason = ' '.join([self.decisionsTexts["transcodeDecision"], self.decisionsTexts["generalDecision"]]) raise DecisionFailure(code, reason) return None # Rebuild the original item with the new item. util.WARN_LOG("Server requested new playback decision: {0}".format(self)) self.original.rebuild(self.item, self) return self.original
def getPostWithTimeout(self, seconds=DEFAULT_TIMEOUT, body=None): if self._cancel: return self.logRequest(body, seconds, False) try: if self.method == 'PUT': res = self.session.put(self.url, timeout=seconds, stream=True) elif self.method == 'DELETE': res = self.session.delete(self.url, timeout=seconds, stream=True) elif self.method == 'HEAD': res = self.session.head(self.url, timeout=seconds, stream=True) elif self.method == 'POST' or body is not None: res = self.session.post(self.url, data=body, timeout=seconds, stream=True) else: res = self.session.get(self.url, timeout=seconds, stream=True) self.currentResponse = res if self._cancel: return None util.LOG("Got a {0} from {1}".format(res.status_code, util.cleanToken(self.url))) # self.event = msg return res except Exception, e: info = traceback.extract_tb(sys.exc_info()[2])[-1] util.WARN_LOG( "Request errored out - URL: {0} File: {1} Line: {2} Msg: {3}". format(util.cleanToken(self.url), os.path.basename(info[0]), info[1], e.message))
def onAccountResponse(self, request, response, context): oldId = self.ID if response.isSuccess(): data = response.getBodyXml() # The user is signed in self.isSignedIn = True self.isOffline = False self.ID = data.attrib.get('id') self.title = data.attrib.get('title') self.username = data.attrib.get('username') self.email = data.attrib.get('email') self.thumb = data.attrib.get('thumb') self.authToken = data.attrib.get('authenticationToken') self.isPlexPass = ( data.find('subscription') is not None and data.find('subscription').attrib.get('active') == '1') self.isManaged = data.attrib.get('restricted') == '1' self.isSecure = data.attrib.get('secure') == '1' self.hasQueue = bool(data.attrib.get('queueEmail')) # PIN if data.attrib.get('pin'): self.pin = data.attrib.get('pin') else: self.pin = None self.isProtected = bool(self.pin) # update the list of users in the home self.updateHomeUsers() # set admin attribute for the user self.isAdmin = False if self.homeUsers: for user in self.homeUsers: if self.ID == user.id: self.isAdmin = str(user.admin) == "1" break if self.isAdmin and self.isPlexPass: self.adminHasPlexPass = True # consider a single, unprotected user authenticated if not self.isAuthenticated and not self.isProtected and len( self.homeUsers) <= 1: self.isAuthenticated = True self.logState() self.saveState() plexapp.MANAGER.publish() plexapp.refreshResources() elif response.getStatus() >= 400 and response.getStatus() < 500: # The user is specifically unauthorized, clear everything util.WARN_LOG("Sign Out: User is unauthorized") self.signOut(True) else: # Unexpected error, keep using whatever we read from the registry util.WARN_LOG( "Unexpected response from plex.tv ({0}), switching to OFFLINE mode" .format(response.getStatus())) self.logState() self.isOffline = True # consider a single, unprotected user authenticated if not self.isAuthenticated and not self.isProtected: self.isAuthenticated = True plexapp.APP.clearInitializer("myplex") # Logger().UpdateSyslogHeader() # TODO: ------------------------------------------------------------------------------------------------------IMPLEMENT if oldId != self.ID or self.switchUser: self.switchUser = None plexapp.APP.trigger("change:user", account=self, reallyChanged=oldId != self.ID) plexapp.APP.trigger("account:response")
def buildUrl(self, path, includeToken=False): if self.activeConnection: return self.activeConnection.buildUrl(self, path, includeToken) else: util.WARN_LOG("Server connection is None, returning an empty url") return ""