Skip to content

Commit

Permalink
[script.plexmod] 0.7.6
Browse files Browse the repository at this point in the history
  • Loading branch information
pannal committed Mar 5, 2024
1 parent adaa5a2 commit 8b6ee47
Show file tree
Hide file tree
Showing 31 changed files with 404 additions and 148 deletions.
4 changes: 2 additions & 2 deletions script.plexmod/addon.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="script.plexmod"
name="PM4K for Plex"
version="0.7.5"
version="0.7.6"
provider-name="pannal">
<requires>
<import addon="xbmc.python" version="3.0.0"/>
Expand Down Expand Up @@ -33,7 +33,7 @@
<source>https://github.com/pannal/plex-for-kodi</source>
<platform>all</platform>
<news>
- Based on 0.7.5-rev2
- Based on 0.7.6-rev1
</news>
<assets>
<icon>icon.png</icon>
Expand Down
30 changes: 30 additions & 0 deletions script.plexmod/changelog.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
[-0.7.6-]
- Core: Avoid DNS rebind protection issues for plex.direct
- Core: Support ipv6 plex.direct hosts when checking for locality/LAN
- Core: Network: Massively speed up local connection checks
- Core: Network: Skip local connection checks for plex.tv
- Core: Network: Add 172.16.0.0/12 as local network
- Core: MyPlex: Ignore non-server resources
- Core: Use correct default for LAN_REACHABILITY_TIMEOUT
- Core: Cache home users indefinitely; add refresh button to user select
- Core: Add support for adaptive readfactor from Kodi Omega BETA3 onwards; recommend it instead of the default range
- Core: Add setting to allow VC1 DirectPlay (default on; was on by default before)
- Core: MDE: Fix DirectStreaming when media has no deep analysis by setting the maximum bitrate to unlimited in maximum quality mode
- Core: Simplify video DirectStream codec decision handling
- Core: Make sure our home window opens, exit if it doesn't in time; unify waiting for window functionality
- Home: Add "Refresh Users" action to user dropdown to refresh available home users
- Home: Reduce unnecessary section refreshes if the section hasn't changed
- Playback: Fix PlayQueue crashing
- Userselect: Remove legacy thread/cache handling leftovers
- Userselect: Fix ACTION_PREVIOUS_MENU/ESC closing the addon instead of leaving the user selection
- SeekDialog: Avoid accidental seeking when navigating through player buttons after closing player settings without changing anything
- SeekDialog: Hide OSD fast after changing subtitle via settings
- SeekDialog: Show episode year in player overlay if possible
- SeekDialog: Widen episode/movie title line
- SeekDialog: Autoscroll episode/movie title lines for too long titles
- SeekDialog: Hide non-autoskipping marker into the OSD using NAV_BACK/PREVIOUS_MENU
- SeekDialog: Apply positive marker endtime offset to manually skipping markers as well (unifying with the autoskip handling), to avoid re-showing the marker occasionally after seeking
- Library: Show current total item count in title
- Settings: Add description for Direct Stream


[- 0.7.5-rev2 -]
- Core: asyncio Kodi compat
- Fix: transcoding is broken due to deepcopy usage
Expand Down
29 changes: 28 additions & 1 deletion script.plexmod/lib/_included_packages/plexnet/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,40 @@
from . import callback
from . import util


codes = requests.codes
status_codes = requests.status_codes._codes


DEFAULT_TIMEOUT = asyncadapter.AsyncTimeout(util.TIMEOUT).setConnectTimeout(util.TIMEOUT)

KNOWN_HOSTS = {}

_getaddrinfo = socket.getaddrinfo


def pgetaddrinfo(host, port, *args, **kwargs):
"""
"circumvent" DNS rebind protection
"""
if host.endswith("plex.direct"):
v6 = host.count("-") > 3
if host in KNOWN_HOSTS:
ip = KNOWN_HOSTS[host]
else:
base = host.split(".", 1)[0]
ip = KNOWN_HOSTS[host] = v6 and base.replace("-", ":") or base.replace("-", ".")
util.DEBUG_LOG("Dynamically resolving {} to {}".format(host, ip))

fam = v6 and socket.AF_INET6 or socket.AF_INET
stype = kwargs.get("type", kwargs.get("socktype", socket.SOCK_STREAM))
proto = kwargs.get("proto", socket.IPPROTO_TCP)

return [(fam, stype, proto, '', (ip, port))]
return _getaddrinfo(host, port, *args, **kwargs)


socket.getaddrinfo = pgetaddrinfo


def GET(*args, **kwargs):
return requests.get(*args, headers=util.BASE_HEADERS.copy(), timeout=util.TIMEOUT, **kwargs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ def evaluateMediaVideo(self, item, media, partIndex=0):

numVideoStreams = 0
problematicAudioStream = False
allowedVC = dict([(k, item.settings.getPreference("allow_{}".format(k), d)) for k, d in
(("hevc", True), ("av1", False), ("vc1", True))])
allowedVC["vp9"] = item.settings.getGlobal("vp9Support")
allowedVC["h264"] = True

if part.get('hasChapterVideoStream').asBool():
numVideoStreams = 1
Expand All @@ -148,13 +152,7 @@ def evaluateMediaVideo(self, item, media, partIndex=0):
if streamType == stream.TYPE_VIDEO:
numVideoStreams = numVideoStreams + 1

if stream.codec == "h264" or (
stream.codec == "hevc" and item.settings.getPreference("allow_hevc", True)
) or (
stream.codec == "av1" and item.settings.getPreference("allow_av1", False)
) or (
stream.codec == "vp9" and item.settings.getGlobal("vp9Support")
):
if stream.codec in allowedVC and allowedVC[stream.codec]:
choice.sorts.videoDS = 1
elif streamType == stream.TYPE_AUDIO:
# fixme: really?
Expand Down Expand Up @@ -295,6 +293,10 @@ def canDirectPlay(self, item, choice):
util.LOG("MDE: (DP) Codec is AV1, which is disabled")
return False

if choice.videoStream.codec == "vc1" and not item.settings.getPreference("allow_vc1", True):
util.LOG("MDE: (DP) Codec is VC1, which is disabled")
return False

return True

# container = choice.media.get('container')
Expand Down
72 changes: 50 additions & 22 deletions script.plexmod/lib/_included_packages/plexnet/myplexaccount.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ def saveState(self):
'isAdmin': self.isAdmin,
'isSecure': self.isSecure,
'adminHasPlexPass': self.adminHasPlexPass,
'thumb': self.thumb
'thumb': self.thumb,
'homeUsers': self.homeUsers,
'lastHomeUserUpdate': self.lastHomeUserUpdate
}

util.INTERFACE.setRegistry("MyPlexAccount", json.dumps(obj), "myplex")
Expand Down Expand Up @@ -99,6 +101,11 @@ def loadState(self):
self.isProtected = bool(obj.get('pin'))
self.adminHasPlexPass = obj.get('adminHasPlexPass') or self.adminHasPlexPass
self.thumb = obj.get('thumb')
self.lastHomeUserUpdate = obj.get('lastHomeUserUpdate')
self.homeUsers = [HomeUser(data) for data in obj.get('homeUsers', [])]
if self.homeUsers:
util.LOG("cached home users: {0} (last update: {1})".format(self.homeUsers,
self.lastHomeUserUpdate))

def verifyAccount(self):
if self.authToken:
Expand Down Expand Up @@ -148,15 +155,14 @@ def onAccountResponse(self, request, response, context):
self.isProtected = bool(self.pin)

# update the list of users in the home
self.updateHomeUsers()
# Cache home users forever
epoch = time.time()

# 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.lastHomeUserUpdate:
util.DEBUG_LOG(
"Skipping home user update (updated {0} seconds ago)".format(epoch - self.lastHomeUserUpdate))
else:
self.updateHomeUsers(use_async=bool(self.homeUsers))

if self.isAdmin and self.isPlexPass:
self.adminHasPlexPass = True
Expand Down Expand Up @@ -207,6 +213,7 @@ def signOut(self, expired=False):
self.authToken = None
self.pin = None
self.lastHomeUserUpdate = None
self.homeUsers = []

# Booleans
self.isSignedIn = False
Expand Down Expand Up @@ -248,7 +255,7 @@ def refreshAccount(self):
return
self.validateToken(self.authToken, False)

def updateHomeUsers(self):
def updateHomeUsers(self, use_async=False):
# Ignore request and clear any home users we are not signed in
if not self.isSignedIn:
self.homeUsers = []
Expand All @@ -258,17 +265,28 @@ def updateHomeUsers(self):
self.lastHomeUserUpdate = None
return

# Cache home users for 60 seconds, mainly to stop back to back tests
epoch = time.time()
if not self.lastHomeUserUpdate:
self.lastHomeUserUpdate = epoch
elif self.lastHomeUserUpdate + 60 > epoch:
util.DEBUG_LOG("Skipping home user update (updated {0} seconds ago)".format(epoch - self.lastHomeUserUpdate))
return

req = myplexrequest.MyPlexRequest("/api/home/users")
xml = req.getToStringWithTimeout(seconds=util.LONG_TIMEOUT)
data = ElementTree.fromstring(xml)
if use_async:
context = req.createRequestContext("home_users", callback.Callable(self.onHomeUsersUpdateResponse),
timeout=util.LONG_TIMEOUT)
if self.isOffline:
context.timeout = self.isOffline and asyncadapter.AsyncTimeout(1).setConnectTimeout(1)
util.APP.startRequest(req, context)
else:
self.onHomeUsersUpdateResponse(req, None, None)

def onHomeUsersUpdateResponse(self, request, response, context):
"""
this can either be called with a given request, which will lead to a synchronous request, or as a
completionCallback from an async request
"""
if response:
data = response.getBodyXml()
else:
xml = request.getToStringWithTimeout(seconds=util.LONG_TIMEOUT)
data = ElementTree.fromstring(xml)

oldHU = self.homeUsers[:]
if data.attrib.get('size') and data.find('User') is not None:
self.homeUsers = []
for user in data.findall('User'):
Expand All @@ -278,9 +296,19 @@ def updateHomeUsers(self):
homeUser.isProtected = homeUser.protected == "1"
self.homeUsers.append(homeUser)

self.lastHomeUserUpdate = epoch
# 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 oldHU != self.homeUsers:
util.LOG("home users: {0}".format(self.homeUsers))

util.LOG("home users: {0}".format(self.homeUsers))
self.lastHomeUserUpdate = time.time()
self.saveState()

def switchHomeUser(self, userId, pin=''):
if userId == self.ID and self.isAuthenticated:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ def __init__(self):
plexserver.PlexServer.__init__(self)
self.uuid = 'myplex'
self.name = 'plex.tv'
conn = plexconnection.PlexConnection(plexresource.ResourceConnection.SOURCE_MYPLEX, "https://plex.tv", False, None)
conn = plexconnection.PlexConnection(plexresource.ResourceConnection.SOURCE_MYPLEX, "https://plex.tv", False,
None, skipLocalCheck=True)
self.connections.append(conn)
self.activeConnection = conn

Expand Down
2 changes: 2 additions & 0 deletions script.plexmod/lib/_included_packages/plexnet/plexapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ def init():
global MANAGER, SERVERMANAGER, ACCOUNT
from . import myplexaccount
ACCOUNT = myplexaccount.ACCOUNT
util.DEBUG_LOG("Waiting for account initialization...")
ACCOUNT.init()
from . import plexservermanager
SERVERMANAGER = plexservermanager.MANAGER
from . import myplexmanager
util.MANAGER = MANAGER = myplexmanager.MANAGER
util.DEBUG_LOG("Verifying account...")
ACCOUNT.verifyAccount()


Expand Down
64 changes: 45 additions & 19 deletions script.plexmod/lib/_included_packages/plexnet/plexconnection.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@

# local networks
LOCAL_NETWORKS = {
4: [IPv4Network('10.0.0.0/8'), IPv4Network('192.168.0.0/16'), IPv4Network('127.0.0.0/8')],
4: [IPv4Network('10.0.0.0/8'), IPv4Network('192.168.0.0/16'), IPv4Network('172.16.0.0/12'),
IPv4Network('127.0.0.0/8')],
6: [IPv6Network('fd00::/8')]
}

LOCALS_SEEN = {}


class ConnectionSource(int):
def init(self, name):
Expand Down Expand Up @@ -122,32 +125,55 @@ def ipInLocalNet(self, ip):
def checkLocal(self):
pUrl = urlparse(self.address)
hostname = pUrl.hostname
try:
ips = resolve(hostname)
except (socket.gaierror, ICMPLibError):
return False

for ip in ips:
if ip == hostname:
continue

network = self.ipInLocalNet(ip)
if not network:
continue
if hostname.endswith("plex.direct"):
util.DEBUG_LOG("Using shortcut for hostname IP detection due to plex.direct host: {}".format(hostname))
v6 = hostname.count("-") > 3
base = hostname.split(".", 1)[0]
ips = [v6 and base.replace("-", ":") or base.replace("-", ".")]

else:
try:
host = ping(ip, count=1, interval=1, timeout=util.LAN_REACHABILITY_TIMEOUT, privileged=False)
except:
continue
ips = resolve(hostname)
except (socket.gaierror, ICMPLibError):
util.DEBUG_LOG("Couldn't resolve hostname: {}".format(hostname))
return False

if host.is_alive:
for ip in ips:
local_and_alive = False
if ip in LOCALS_SEEN:
local, network, host = LOCALS_SEEN[ip]
if local:
util.DEBUG_LOG("We've already verified {} ({}) as local, skipping".format(hostname, ip))
else:
util.DEBUG_LOG("We've already verified {} ({}) as remote, skipping".format(hostname, ip))
continue
local_and_alive = True

else:
network = self.ipInLocalNet(ip)
if not network:
LOCALS_SEEN[ip] = (False, network, None)
continue

try:
host = ping(ip, count=1, interval=1, timeout=util.LAN_REACHABILITY_TIMEOUT, privileged=False)
except:
util.DEBUG_LOG("IP {} didn't answer in time ({}s)".format(ip, util.LAN_REACHABILITY_TIMEOUT))
LOCALS_SEEN[ip] = (False, network, None)
continue

if local_and_alive or host.is_alive:
self.isLocal = True
util.LOG("Found IP {0} in local network ({1}) when checking {2}. Ping: {3}ms (max: {4}ms)"
.format(ip, network, self.address, host.max_rtt, int(util.LAN_REACHABILITY_TIMEOUT * 1000)))
if not local_and_alive:
util.LOG("Found IP {0} in local network ({1}) when checking {2}. Ping: {3}ms (max: {4}s)"
.format(ip, network, self.address, host.max_rtt, util.LAN_REACHABILITY_TIMEOUT))
LOCALS_SEEN[ip] = (True, network, host)

if self.isSecure:
# alert the server that we've found the IP locally, so we can test non-secure connectivity
self.isSecureButLocal = (ip, pUrl.port)
return True

return False

Expand Down Expand Up @@ -210,7 +236,7 @@ def testReachability(self, server, allowFallback=False):
context.server = server
util.addPlexHeaders(self.request, server.getToken())
self.hasPendingRequest = util.APP.startRequest(self.request, context)
util.DEBUG_LOG("Testing insecure connection test for: {0}".format(server))
util.DEBUG_LOG("Testing insecure connection for: {0}".format(server))
return True

return False
Expand Down
Loading

0 comments on commit 8b6ee47

Please sign in to comment.