Fix encoding of file and path operations

This commit is contained in:
Croneter 2018-06-23 18:25:18 +02:00
parent 074c439e99
commit 1234f61fc0
19 changed files with 405 additions and 290 deletions

View file

@ -3,15 +3,13 @@
############################################################################### ###############################################################################
from logging import getLogger from logging import getLogger
from Queue import Queue, Empty from Queue import Queue, Empty
from shutil import rmtree
from urllib import quote_plus, unquote from urllib import quote_plus, unquote
from threading import Thread from threading import Thread
from os import makedirs
import requests import requests
import xbmc import xbmc
from xbmcvfs import exists
from . import path_ops
from . import utils from . import utils
from . import state from . import state
@ -202,10 +200,9 @@ class Artwork():
if utils.dialog('yesno', "Image Texture Cache", utils.lang(39251)): if utils.dialog('yesno', "Image Texture Cache", utils.lang(39251)):
LOG.info("Resetting all cache data first") LOG.info("Resetting all cache data first")
# Remove all existing textures first # Remove all existing textures first
path = utils.try_decode( path = path_ops.translate_path('special://thumbnails/')
xbmc.translatePath("special://thumbnails/")) if path_ops.exists(path):
if utils.exists_dir(path): path_ops.rmtree(path, ignore_errors=True)
rmtree(path, ignore_errors=True)
self.restore_cache_directories() self.restore_cache_directories()
# remove all existing data from texture DB # remove all existing data from texture DB
@ -321,10 +318,11 @@ class Artwork():
pass pass
else: else:
# Delete thumbnail as well as the entry # Delete thumbnail as well as the entry
path = xbmc.translatePath("special://thumbnails/%s" % cachedurl) path = path_ops.translate_path("special://thumbnails/%s"
% cachedurl)
LOG.debug("Deleting cached thumbnail: %s", path) LOG.debug("Deleting cached thumbnail: %s", path)
if exists(path): if path_ops.exists(path):
rmtree(utils.try_decode(path), ignore_errors=True) path_ops.rmtree(path, ignore_errors=True)
cursor.execute("DELETE FROM texture WHERE url = ?", (url,)) cursor.execute("DELETE FROM texture WHERE url = ?", (url,))
connection.commit() connection.commit()
finally: finally:
@ -337,8 +335,8 @@ class Artwork():
"a", "b", "c", "d", "e", "f", "a", "b", "c", "d", "e", "f",
"Video", "plex") "Video", "plex")
for path in paths: for path in paths:
makedirs(utils.try_decode( new_path = path_ops.translate_path("special://thumbnails/%s" % path)
xbmc.translatePath("special://thumbnails/%s" % path))) path_ops.makedirs(utils.encode_path(new_path))
class ArtworkSyncMessage(object): class ArtworkSyncMessage(object):

View file

@ -1,16 +1,15 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
############################################################################### ###############################################################################
from logging import getLogger from logging import getLogger
from os.path import join
import xbmcgui import xbmcgui
from xbmcaddon import Addon
from . import utils from . import utils
from . import path_ops
from . import variables as v
############################################################################### ###############################################################################
LOG = getLogger('PLEX.context') LOG = getLogger('PLEX.context')
ADDON = Addon('plugin.video.plexkodiconnect')
ACTION_PARENT_DIR = 9 ACTION_PARENT_DIR = 9
ACTION_PREVIOUS_MENU = 10 ACTION_PREVIOUS_MENU = 10
@ -65,10 +64,11 @@ class ContextMenu(xbmcgui.WindowXMLDialog):
self.close() self.close()
def _add_editcontrol(self, x, y, height, width, password=None): def _add_editcontrol(self, x, y, height, width, password=None):
media = join(ADDON.getAddonInfo('path'), media = path_ops.path.join(
'resources', 'skins', 'default', 'media') v.ADDON_PATH, 'resources', 'skins', 'default', 'media')
filename = utils.try_encode(path_ops.path.join(media, 'white.png'))
control = xbmcgui.ControlImage(0, 0, 0, 0, control = xbmcgui.ControlImage(0, 0, 0, 0,
filename=join(media, "white.png"), filename=filename,
aspectRatio=0, aspectRatio=0,
colorDiffuse="ff111111") colorDiffuse="ff111111")
control.setPosition(x, y) control.setPosition(x, y)

View file

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
############################################################################### ###############################################################################
from logging import getLogger from logging import getLogger
from xbmcaddon import Addon
import xbmc import xbmc
import xbmcgui import xbmcgui
@ -103,7 +102,7 @@ class ContextMenu(object):
options.append(OPTIONS['Addon']) options.append(OPTIONS['Addon'])
context_menu = context.ContextMenu( context_menu = context.ContextMenu(
"script-plex-context.xml", "script-plex-context.xml",
Addon('plugin.video.plexkodiconnect').getAddonInfo('path'), utils.try_encode(v.ADDON_PATH),
"default", "default",
"1080i") "1080i")
context_menu.set_options(options) context_menu.set_options(options)

View file

@ -5,16 +5,14 @@
# #
############################################################################### ###############################################################################
from logging import getLogger from logging import getLogger
from shutil import copyfile
from os import walk, makedirs
from os.path import basename, join
from sys import argv from sys import argv
from urllib import urlencode from urllib import urlencode
import xbmcplugin import xbmcplugin
from xbmc import sleep, executebuiltin, translatePath from xbmc import sleep, executebuiltin
from xbmcgui import ListItem from xbmcgui import ListItem
from . import utils from . import utils
from . import path_ops
from .downloadutils import DownloadUtils as DU from .downloadutils import DownloadUtils as DU
from .plex_api import API from .plex_api import API
from . import plex_functions as PF from . import plex_functions as PF
@ -441,7 +439,7 @@ def get_video_files(plex_id, params):
item = PF.GetPlexMetadata(plex_id) item = PF.GetPlexMetadata(plex_id)
try: try:
path = item[0][0][0].attrib['file'] path = utils.try_decode(item[0][0][0].attrib['file'])
except (TypeError, IndexError, AttributeError, KeyError): except (TypeError, IndexError, AttributeError, KeyError):
LOG.error('Could not get file path for item %s', plex_id) LOG.error('Could not get file path for item %s', plex_id)
return xbmcplugin.endOfDirectory(HANDLE) return xbmcplugin.endOfDirectory(HANDLE)
@ -453,18 +451,19 @@ def get_video_files(plex_id, params):
elif '\\' in path: elif '\\' in path:
path = path.replace('\\', '\\\\') path = path.replace('\\', '\\\\')
# Directory only, get rid of filename # Directory only, get rid of filename
path = path.replace(basename(path), '') path = path.replace(path_ops.path.basename(path), '')
if utils.exists_dir(path): if path_ops.exists(path):
for root, dirs, files in walk(path): for root, dirs, files in path_ops.walk(path):
for directory in dirs: for directory in dirs:
item_path = utils.try_encode(join(root, directory)) item_path = utils.try_encode(path_ops.path.join(root,
directory))
listitem = ListItem(item_path, path=item_path) listitem = ListItem(item_path, path=item_path)
xbmcplugin.addDirectoryItem(handle=HANDLE, xbmcplugin.addDirectoryItem(handle=HANDLE,
url=item_path, url=item_path,
listitem=listitem, listitem=listitem,
isFolder=True) isFolder=True)
for file in files: for file in files:
item_path = utils.try_encode(join(root, file)) item_path = utils.try_encode(path_ops.path.join(root, file))
listitem = ListItem(item_path, path=item_path) listitem = ListItem(item_path, path=item_path)
xbmcplugin.addDirectoryItem(handle=HANDLE, xbmcplugin.addDirectoryItem(handle=HANDLE,
url=file, url=file,
@ -492,11 +491,11 @@ def extra_fanart(plex_id, plex_path):
# We need to store the images locally for this to work # We need to store the images locally for this to work
# because of the caching system in xbmc # because of the caching system in xbmc
fanart_dir = utils.try_decode(translatePath( fanart_dir = path_ops.translate_path("special://thumbnails/plex/%s/"
"special://thumbnails/plex/%s/" % plex_id)) % plex_id)
if not utils.exists_dir(fanart_dir): if not path_ops.exists(fanart_dir):
# Download the images to the cache directory # Download the images to the cache directory
makedirs(fanart_dir) path_ops.makedirs(fanart_dir)
xml = PF.GetPlexMetadata(plex_id) xml = PF.GetPlexMetadata(plex_id)
if xml is None: if xml is None:
LOG.error('Could not download metadata for %s', plex_id) LOG.error('Could not download metadata for %s', plex_id)
@ -506,20 +505,23 @@ def extra_fanart(plex_id, plex_path):
backdrops = api.artwork()['Backdrop'] backdrops = api.artwork()['Backdrop']
for count, backdrop in enumerate(backdrops): for count, backdrop in enumerate(backdrops):
# Same ordering as in artwork # Same ordering as in artwork
art_file = utils.try_encode(join(fanart_dir, art_file = utils.try_encode(path_ops.path.join(
"fanart%.3d.jpg" % count)) fanart_dir, "fanart%.3d.jpg" % count))
listitem = ListItem("%.3d" % count, path=art_file) listitem = ListItem("%.3d" % count, path=art_file)
xbmcplugin.addDirectoryItem( xbmcplugin.addDirectoryItem(
handle=HANDLE, handle=HANDLE,
url=art_file, url=art_file,
listitem=listitem) listitem=listitem)
copyfile(backdrop, utils.try_decode(art_file)) path_ops.copyfile(backdrop, utils.try_decode(art_file))
else: else:
LOG.info("Found cached backdrop.") LOG.info("Found cached backdrop.")
# Use existing cached images # Use existing cached images
for root, _, files in walk(fanart_dir): fanart_dir = utils.try_decode(fanart_dir)
for root, _, files in path_ops.walk(fanart_dir):
root = utils.decode_path(root)
for file in files: for file in files:
art_file = utils.try_encode(join(root, file)) file = utils.decode_path(file)
art_file = utils.try_encode(path_ops.path.join(root, file))
listitem = ListItem(file, path=art_file) listitem = ListItem(file, path=art_file)
xbmcplugin.addDirectoryItem(handle=HANDLE, xbmcplugin.addDirectoryItem(handle=HANDLE,
url=art_file, url=art_file,

View file

@ -7,6 +7,7 @@ import xml.etree.ElementTree as etree
from xbmc import executebuiltin, translatePath from xbmc import executebuiltin, translatePath
from . import utils from . import utils
from . import path_ops
from . import migration from . import migration
from .downloadutils import DownloadUtils as DU from .downloadutils import DownloadUtils as DU
from . import videonodes from . import videonodes
@ -25,6 +26,9 @@ LOG = getLogger('PLEX.initialsetup')
############################################################################### ###############################################################################
if not path_ops.exists(v.EXTERNAL_SUBTITLE_TEMP_PATH):
path_ops.makedirs(v.EXTERNAL_SUBTITLE_TEMP_PATH)
WINDOW_PROPERTIES = ( WINDOW_PROPERTIES = (
"plex_online", "plex_serverStatus", "plex_shouldStop", "plex_dbScan", "plex_online", "plex_serverStatus", "plex_shouldStop", "plex_dbScan",

View file

@ -1025,23 +1025,11 @@ class LibrarySync(Thread):
do with "process_" methods do with "process_" methods
""" """
if message['type'] == 'playing': if message['type'] == 'playing':
try:
self.process_playing(message['PlaySessionStateNotification']) self.process_playing(message['PlaySessionStateNotification'])
except KeyError:
LOG.error('Received invalid PMS message for playstate: %s',
message)
elif message['type'] == 'timeline': elif message['type'] == 'timeline':
try:
self.process_timeline(message['TimelineEntry']) self.process_timeline(message['TimelineEntry'])
except (KeyError, ValueError):
LOG.error('Received invalid PMS message for timeline: %s',
message)
elif message['type'] == 'activity': elif message['type'] == 'activity':
try:
self.process_activity(message['ActivityNotification']) self.process_activity(message['ActivityNotification'])
except KeyError:
LOG.error('Received invalid PMS message for activity: %s',
message)
def multi_delete(self, liste, delete_list): def multi_delete(self, liste, delete_list):
""" """
@ -1196,7 +1184,7 @@ class LibrarySync(Thread):
continue continue
playlists.process_websocket(plex_id=str(item['itemID']), playlists.process_websocket(plex_id=str(item['itemID']),
updated_at=str(item['updatedAt']), updated_at=str(item['updatedAt']),
state=status) status=status)
elif status == 9: elif status == 9:
# Immediately and always process deletions (as the PMS will # Immediately and always process deletions (as the PMS will
# send additional message with other codes) # send additional message with other codes)

View file

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from logging import getLogger from logging import getLogger
from re import compile as re_compile
from xml.etree.ElementTree import ParseError from xml.etree.ElementTree import ParseError
from . import utils from . import utils
@ -9,8 +8,6 @@ from . import variables as v
############################################################################### ###############################################################################
LOG = getLogger('PLEX.music') LOG = getLogger('PLEX.music')
REGEX_MUSICPATH = re_compile(r'''^\^(.+)\$$''')
############################################################################### ###############################################################################

192
resources/lib/path_ops.py Normal file
View file

@ -0,0 +1,192 @@
# -*- coding: utf-8 -*-
# File and Path operations
#
# Kodi xbmc*.*() functions usually take utf-8 encoded commands, thus try_encode
# works.
# Unfortunatly, working with filenames and paths seems to require an encoding in
# the OS' getfilesystemencoding - it will NOT always work with unicode paths.
# However, sys.getfilesystemencoding might return None.
# Feed unicode to all the functions below and you're fine.
import shutil
import os
from os import path # allows to use path_ops.path.join, for example
from distutils import dir_util
import xbmc
import xbmcvfs
from .watchdog.utils import unicode_paths
# Kodi seems to encode in utf-8 in ALL cases (unlike e.g. the OS filesystem)
KODI_ENCODING = 'utf-8'
def encode_path(path):
"""
Filenames and paths are not necessarily utf-8 encoded. Use this function
instead of try_encode/trydecode if working with filenames and paths!
(os.walk only feeds on encoded paths. sys.getfilesystemencoding returns None
for Raspberry Pi)
"""
return unicode_paths.encode(path)
def decode_path(path):
"""
Filenames and paths are not necessarily utf-8 encoded. Use this function
instead of try_encode/trydecode if working with filenames and paths!
(os.walk only feeds on encoded paths. sys.getfilesystemencoding returns None
for Raspberry Pi)
"""
return unicode_paths.decode(path)
def translate_path(path):
"""
Returns the XBMC translated path [unicode]
e.g. Converts 'special://masterprofile/script_data'
-> '/home/user/XBMC/UserData/script_data' on Linux.
"""
translated = xbmc.translatePath(path.encode(KODI_ENCODING, 'strict'))
return translated.decode(KODI_ENCODING, 'strict')
def exists(path):
"""Returns True if the path [unicode] exists"""
return xbmcvfs.exists(path.encode(KODI_ENCODING, 'strict'))
def rmtree(path, *args, **kwargs):
"""Recursively delete a directory tree.
If ignore_errors is set, errors are ignored; otherwise, if onerror
is set, it is called to handle the error with arguments (func,
path, exc_info) where func is os.listdir, os.remove, or os.rmdir;
path is the argument to that function that caused it to fail; and
exc_info is a tuple returned by sys.exc_info(). If ignore_errors
is false and onerror is None, an exception is raised.
"""
return shutil.rmtree(encode_path(path), *args, **kwargs)
def copyfile(src, dst):
"""Copy data from src to dst"""
return shutil.copyfile(encode_path(src), encode_path(dst))
def makedirs(path, *args, **kwargs):
"""makedirs(path [, mode=0777])
Super-mkdir; create a leaf directory and all intermediate ones. Works like
mkdir, except that any intermediate path segment (not just the rightmost)
will be created if it does not exist. This is recursive.
"""
return os.makedirs(encode_path(path), *args, **kwargs)
def remove(path):
"""
Remove (delete) the file path. If path is a directory, OSError is raised;
see rmdir() below to remove a directory. This is identical to the unlink()
function documented below. On Windows, attempting to remove a file that is
in use causes an exception to be raised; on Unix, the directory entry is
removed but the storage allocated to the file is not made available until
the original file is no longer in use.
"""
return os.remove(encode_path(path))
def walk(top, topdown=True, onerror=None, followlinks=False):
"""
Directory tree generator.
For each directory in the directory tree rooted at top (including top
itself, but excluding '.' and '..'), yields a 3-tuple
dirpath, dirnames, filenames
dirpath is a string, the path to the directory. dirnames is a list of
the names of the subdirectories in dirpath (excluding '.' and '..').
filenames is a list of the names of the non-directory files in dirpath.
Note that the names in the lists are just names, with no path components.
To get a full path (which begins with top) to a file or directory in
dirpath, do os.path.join(dirpath, name).
If optional arg 'topdown' is true or not specified, the triple for a
directory is generated before the triples for any of its subdirectories
(directories are generated top down). If topdown is false, the triple
for a directory is generated after the triples for all of its
subdirectories (directories are generated bottom up).
When topdown is true, the caller can modify the dirnames list in-place
(e.g., via del or slice assignment), and walk will only recurse into the
subdirectories whose names remain in dirnames; this can be used to prune the
search, or to impose a specific order of visiting. Modifying dirnames when
topdown is false is ineffective, since the directories in dirnames have
already been generated by the time dirnames itself is generated. No matter
the value of topdown, the list of subdirectories is retrieved before the
tuples for the directory and its subdirectories are generated.
By default errors from the os.listdir() call are ignored. If
optional arg 'onerror' is specified, it should be a function; it
will be called with one argument, an os.error instance. It can
report the error to continue with the walk, or raise the exception
to abort the walk. Note that the filename is available as the
filename attribute of the exception object.
By default, os.walk does not follow symbolic links to subdirectories on
systems that support them. In order to get this functionality, set the
optional argument 'followlinks' to true.
Caution: if you pass a relative pathname for top, don't change the
current working directory between resumptions of walk. walk never
changes the current directory, and assumes that the client doesn't
either.
Example:
import os
from os.path import join, getsize
for root, dirs, files in os.walk('python/Lib/email'):
print root, "consumes",
print sum([getsize(join(root, name)) for name in files]),
print "bytes in", len(files), "non-directory files"
if 'CVS' in dirs:
dirs.remove('CVS') # don't visit CVS directories
"""
# Get all the results from os.walk and store them in a list
walker = list(os.walk(encode_path(top),
topdown,
onerror,
followlinks))
for top, dirs, nondirs in walker:
yield (decode_path(top),
[decode_path(x) for x in dirs],
[decode_path(x) for x in nondirs])
def copy_tree(src, dst, *args, **kwargs):
"""
Copy an entire directory tree 'src' to a new location 'dst'.
Both 'src' and 'dst' must be directory names. If 'src' is not a
directory, raise DistutilsFileError. If 'dst' does not exist, it is
created with 'mkpath()'. The end result of the copy is that every
file in 'src' is copied to 'dst', and directories under 'src' are
recursively copied to 'dst'. Return the list of files that were
copied or might have been copied, using their output name. The
return value is unaffected by 'update' or 'dry_run': it is simply
the list of all files under 'src', with the names changed to be
under 'dst'.
'preserve_mode' and 'preserve_times' are the same as for
'copy_file'; note that they only apply to regular files, not to
directories. If 'preserve_symlinks' is true, symlinks will be
copied as symlinks (on platforms that support them!); otherwise
(the default), the destination of the symlink will be copied.
'update' and 'verbose' are the same as for 'copy_file'.
"""
src = encode_path(src)
dst = encode_path(dst)
return dir_util.copy_tree(src, dst, *args, **kwargs)

View file

@ -3,7 +3,6 @@ Used to kick off Kodi playback
""" """
from logging import getLogger from logging import getLogger
from threading import Thread from threading import Thread
from os.path import join
from xbmc import Player, sleep from xbmc import Player, sleep
from .plex_api import API from .plex_api import API
@ -25,8 +24,6 @@ from . import state
LOG = getLogger('PLEX.playback') LOG = getLogger('PLEX.playback')
# Do we need to return ultimately with a setResolvedUrl? # Do we need to return ultimately with a setResolvedUrl?
RESOLVE = True RESOLVE = True
# We're "failing" playback with a video of 0 length
NULL_VIDEO = join(v.ADDON_FOLDER, 'addons', v.ADDON_ID, 'empty_video.mp4')
############################################################################### ###############################################################################
@ -273,7 +270,7 @@ def _ensure_resolve(abort=False):
state.PKC_CAUSED_STOP_DONE = False state.PKC_CAUSED_STOP_DONE = False
if not abort: if not abort:
result = pickler.Playback_Successful() result = pickler.Playback_Successful()
result.listitem = PKCListItem(path=NULL_VIDEO) result.listitem = PKCListItem(path=v.NULL_VIDEO)
pickler.pickle_me(result) pickler.pickle_me(result)
else: else:
# Shows PKC error message # Shows PKC error message

View file

@ -3,10 +3,8 @@
Collection of functions associated with Kodi and Plex playlists and playqueues Collection of functions associated with Kodi and Plex playlists and playqueues
""" """
from logging import getLogger from logging import getLogger
import os
import urllib import urllib
from urlparse import parse_qsl, urlsplit from urlparse import parse_qsl, urlsplit
from re import compile as re_compile
from .plex_api import API from .plex_api import API
from . import plex_functions as PF from . import plex_functions as PF
@ -14,6 +12,7 @@ from . import plexdb_functions as plexdb
from . import kodidb_functions as kodidb from . import kodidb_functions as kodidb
from .downloadutils import DownloadUtils as DU from .downloadutils import DownloadUtils as DU
from . import utils from . import utils
from . import path_ops
from . import json_rpc as js from . import json_rpc as js
from . import variables as v from . import variables as v
@ -21,7 +20,6 @@ from . import variables as v
LOG = getLogger('PLEX.playlist_func') LOG = getLogger('PLEX.playlist_func')
REGEX = re_compile(r'''metadata%2F(\d+)''')
############################################################################### ###############################################################################
@ -42,9 +40,9 @@ class PlaylistObjectBaseclase(object):
def __repr__(self): def __repr__(self):
""" """
Print the playlist, e.g. to log. Returns utf-8 encoded string Print the playlist, e.g. to log. Returns unicode
""" """
answ = u'{\'%s\': {\'id\': %s, ' % (self.__class__.__name__, self.id) answ = '{\'%s\': {\'id\': %s, ' % (self.__class__.__name__, self.id)
# For some reason, can't use dir directly # For some reason, can't use dir directly
for key in self.__dict__: for key in self.__dict__:
if key in ('id', 'kodi_pl'): if key in ('id', 'kodi_pl'):
@ -58,7 +56,7 @@ class PlaylistObjectBaseclase(object):
else: else:
# e.g. int # e.g. int
answ += '\'%s\': %s, ' % (key, unicode(getattr(self, key))) answ += '\'%s\': %s, ' % (key, unicode(getattr(self, key)))
return utils.try_encode(answ + '}}') return answ + '}}'
class Playlist_Object(PlaylistObjectBaseclase): class Playlist_Object(PlaylistObjectBaseclase):
@ -82,7 +80,9 @@ class Playlist_Object(PlaylistObjectBaseclase):
@kodi_path.setter @kodi_path.setter
def kodi_path(self, path): def kodi_path(self, path):
file = os.path.basename(path) if not isinstance(path, unicode):
raise RuntimeError('Path is %s, not unicode!' % type(path))
file = path_ops.path.basename(path)
try: try:
self.kodi_filename, self.kodi_extension = file.split('.', 1) self.kodi_filename, self.kodi_extension = file.split('.', 1)
except ValueError: except ValueError:
@ -220,9 +220,9 @@ class Playlist_Item(object):
def __repr__(self): def __repr__(self):
""" """
Print the playlist item, e.g. to log. Returns utf-8 encoded string Print the playlist item, e.g. to log. Returns unicode
""" """
answ = (u'{\'%s\': {\'id\': \'%s\', \'plex_id\': \'%s\', ' answ = ('{\'%s\': {\'id\': \'%s\', \'plex_id\': \'%s\', '
% (self.__class__.__name__, self.id, self.plex_id)) % (self.__class__.__name__, self.id, self.plex_id))
for key in self.__dict__: for key in self.__dict__:
if key in ('id', 'plex_id', 'xml'): if key in ('id', 'plex_id', 'xml'):
@ -240,7 +240,7 @@ class Playlist_Item(object):
answ += '\'xml\': None}}' answ += '\'xml\': None}}'
else: else:
answ += '\'xml\': \'%s\'}}' % self.xml.tag answ += '\'xml\': \'%s\'}}' % self.xml.tag
return utils.try_encode(answ) return answ
def plex_stream_index(self, kodi_stream_index, stream_type): def plex_stream_index(self, kodi_stream_index, stream_type):
""" """
@ -865,7 +865,8 @@ def get_plextype_from_xml(xml):
returns None if unsuccessful returns None if unsuccessful
""" """
try: try:
plex_id = REGEX.findall(xml.attrib['playQueueSourceURI'])[0] plex_id = utils.REGEX_PLEX_ID_FROM_URL.findall(
xml.attrib['playQueueSourceURI'])[0]
except IndexError: except IndexError:
LOG.error('Could not get plex_id from xml: %s', xml.attrib) LOG.error('Could not get plex_id from xml: %s', xml.attrib)
return return

View file

@ -1,8 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from logging import getLogger from logging import getLogger
import os
import sys
from xbmcvfs import exists
from .watchdog.events import FileSystemEventHandler from .watchdog.events import FileSystemEventHandler
from .watchdog.observers import Observer from .watchdog.observers import Observer
@ -11,6 +8,7 @@ from .plex_api import API
from . import kodidb_functions as kodidb from . import kodidb_functions as kodidb
from . import plexdb_functions as plexdb from . import plexdb_functions as plexdb
from . import utils from . import utils
from . import path_ops
from . import variables as v from . import variables as v
from . import state from . import state
@ -32,12 +30,6 @@ EVENT_TYPE_DELETED = 'deleted'
EVENT_TYPE_CREATED = 'created' EVENT_TYPE_CREATED = 'created'
EVENT_TYPE_MODIFIED = 'modified' EVENT_TYPE_MODIFIED = 'modified'
# m3u files do not have encoding specified
if v.PLATFORM == 'Windows':
ENCODING = 'mbcs'
else:
ENCODING = sys.getdefaultencoding()
def create_plex_playlist(playlist): def create_plex_playlist(playlist):
""" """
@ -99,19 +91,20 @@ def create_kodi_playlist(plex_id=None, updated_at=None):
playlist.plex_updatedat = updated_at playlist.plex_updatedat = updated_at
LOG.debug('Creating new Kodi playlist from Plex playlist: %s', playlist) LOG.debug('Creating new Kodi playlist from Plex playlist: %s', playlist)
name = utils.valid_filename(playlist.plex_name) name = utils.valid_filename(playlist.plex_name)
path = os.path.join(v.PLAYLIST_PATH, playlist.type, '%s.m3u' % name) path = path_ops.path.join(v.PLAYLIST_PATH, playlist.type, '%s.m3u' % name)
while exists(path) or playlist_object_from_db(path=path): while path_ops.exists(path) or playlist_object_from_db(path=path):
# In case the Plex playlist names are not unique # In case the Plex playlist names are not unique
occurance = utils.REGEX_FILE_NUMBERING.search(path) occurance = utils.REGEX_FILE_NUMBERING.search(path)
if not occurance: if not occurance:
path = os.path.join(v.PLAYLIST_PATH, path = path_ops.path.join(v.PLAYLIST_PATH,
playlist.type, playlist.type,
'%s_01.m3u' % name[:min(len(name), 248)]) '%s_01.m3u' % name[:min(len(name), 248)])
else: else:
occurance = int(occurance.group(1)) + 1 occurance = int(occurance.group(1)) + 1
path = os.path.join(v.PLAYLIST_PATH, path = path_ops.path.join(v.PLAYLIST_PATH,
playlist.type, playlist.type,
'%s_%02d.m3u' % (name[:min(len(name), 248)], '%s_%02d.m3u' % (name[:min(len(name),
248)],
occurance)) occurance))
LOG.debug('Kodi playlist path: %s', path) LOG.debug('Kodi playlist path: %s', path)
playlist.kodi_path = path playlist.kodi_path = path
@ -130,7 +123,7 @@ def delete_kodi_playlist(playlist):
Returns None or raises PL.PlaylistError Returns None or raises PL.PlaylistError
""" """
try: try:
os.remove(playlist.kodi_path) path_ops.remove(playlist.kodi_path)
except (OSError, IOError) as err: except (OSError, IOError) as err:
LOG.error('Could not delete Kodi playlist file %s. Error:\n %s: %s', LOG.error('Could not delete Kodi playlist file %s. Error:\n %s: %s',
playlist, err.errno, err.strerror) playlist, err.errno, err.strerror)
@ -180,10 +173,10 @@ def m3u_to_plex_ids(playlist):
Adapter to process *.m3u playlist files. Encoding is not uniform! Adapter to process *.m3u playlist files. Encoding is not uniform!
""" """
plex_ids = list() plex_ids = list()
with open(utils.encode_path(playlist.kodi_path), 'rb') as f: with open(path_ops.encode_path(playlist.kodi_path), 'rb') as f:
text = f.read() text = f.read()
try: try:
text = text.decode(ENCODING) text = text.decode(v.M3U_ENCODING)
except UnicodeDecodeError: except UnicodeDecodeError:
LOG.warning('Fallback to ISO-8859-1 decoding for %s', playlist) LOG.warning('Fallback to ISO-8859-1 decoding for %s', playlist)
text = text.decode('ISO-8859-1') text = text.decode('ISO-8859-1')
@ -210,15 +203,15 @@ def _write_playlist_to_file(playlist, xml):
Feed with playlist [Playlist_Object]. Will write the playlist to a m3u file Feed with playlist [Playlist_Object]. Will write the playlist to a m3u file
Returns None or raises PL.PlaylistError Returns None or raises PL.PlaylistError
""" """
text = u'#EXTCPlayListM3U::M3U\n' text = '#EXTCPlayListM3U::M3U\n'
for element in xml: for element in xml:
api = API(element) api = API(element)
text += (u'#EXTINF:%s,%s\n%s\n' text += ('#EXTINF:%s,%s\n%s\n'
% (api.runtime(), api.title(), api.path())) % (api.runtime(), api.title(), api.path()))
text += '\n' text += '\n'
text = text.encode(ENCODING, 'ignore') text = text.encode(v.M3U_ENCODING, 'strict')
try: try:
with open(utils.encode_path(playlist.kodi_path), 'wb') as f: with open(path_ops.encode_path(playlist.kodi_path), 'wb') as f:
f.write(text) f.write(text)
except (OSError, IOError) as err: except (OSError, IOError) as err:
LOG.error('Could not write Kodi playlist file: %s', playlist) LOG.error('Could not write Kodi playlist file: %s', playlist)
@ -267,15 +260,15 @@ def _kodi_playlist_identical(xml_element):
pass pass
def process_websocket(plex_id, updated_at, state): def process_websocket(plex_id, updated_at, status):
""" """
Hit by librarysync to process websocket messages concerning playlists Hit by librarysync to process websocket messages concerning playlists
""" """
create = False create = False
playlist = playlist_object_from_db(plex_id=plex_id)
with state.LOCK_PLAYLISTS: with state.LOCK_PLAYLISTS:
playlist = playlist_object_from_db(plex_id=plex_id)
try: try:
if playlist and state == 9: if playlist and status == 9:
LOG.debug('Plex deletion of playlist detected: %s', playlist) LOG.debug('Plex deletion of playlist detected: %s', playlist)
delete_kodi_playlist(playlist) delete_kodi_playlist(playlist)
elif playlist and playlist.plex_updatedat == updated_at: elif playlist and playlist.plex_updatedat == updated_at:
@ -285,7 +278,7 @@ def process_websocket(plex_id, updated_at, state):
LOG.debug('Change of Plex playlist detected: %s', playlist) LOG.debug('Change of Plex playlist detected: %s', playlist)
delete_kodi_playlist(playlist) delete_kodi_playlist(playlist)
create = True create = True
elif not playlist and not state == 9: elif not playlist and not status == 9:
LOG.debug('Creation of new Plex playlist detected: %s', LOG.debug('Creation of new Plex playlist detected: %s',
plex_id) plex_id)
create = True create = True
@ -333,7 +326,7 @@ def _full_sync():
elif playlist.plex_updatedat != api.updated_at(): elif playlist.plex_updatedat != api.updated_at():
LOG.debug('Detected changed Plex playlist %s: %s', LOG.debug('Detected changed Plex playlist %s: %s',
api.plex_id(), api.title()) api.plex_id(), api.title())
if exists(playlist.kodi_path): if path_ops.exists(playlist.kodi_path):
delete_kodi_playlist(playlist) delete_kodi_playlist(playlist)
else: else:
update_plex_table(playlist, delete=True) update_plex_table(playlist, delete=True)
@ -361,17 +354,19 @@ def _full_sync():
if state.ENABLE_MUSIC: if state.ENABLE_MUSIC:
master_paths.append(v.PLAYLIST_PATH_MUSIC) master_paths.append(v.PLAYLIST_PATH_MUSIC)
for master_path in master_paths: for master_path in master_paths:
for root, _, files in os.walk(utils.encode_path(master_path)): for root, _, files in path_ops.walk(master_path):
root = utils.decode_path(root)
for file in files: for file in files:
file = utils.decode_path(file)
try: try:
extension = file.rsplit('.', 1)[1] extension = file.rsplit('.', 1)[1]
except IndexError: except IndexError:
continue continue
if extension not in SUPPORTED_FILETYPES: if extension not in SUPPORTED_FILETYPES:
continue continue
path = os.path.join(root, file) LOG.debug('root: %s', root)
LOG.debug('type: %s', type(root))
LOG.debug('file: %s', file)
LOG.debug('type: %s', type(file))
path = path_ops.path.join(root, file)
kodi_hash = utils.generate_file_md5(path) kodi_hash = utils.generate_file_md5(path)
playlist = playlist_object_from_db(kodi_hash=kodi_hash) playlist = playlist_object_from_db(kodi_hash=kodi_hash)
playlist_2 = playlist_object_from_db(path=path) playlist_2 = playlist_object_from_db(path=path)

View file

@ -3,7 +3,6 @@ Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly
""" """
from logging import getLogger from logging import getLogger
from threading import Thread from threading import Thread
from re import compile as re_compile
import xbmc import xbmc
from . import utils from . import utils
@ -18,7 +17,6 @@ from . import state
LOG = getLogger('PLEX.playqueue') LOG = getLogger('PLEX.playqueue')
PLUGIN = 'plugin://%s' % v.ADDON_ID PLUGIN = 'plugin://%s' % v.ADDON_ID
REGEX = re_compile(r'''plex_id=(\d+)''')
# Our PKC playqueues (3 instances of Playqueue_Object()) # Our PKC playqueues (3 instances of Playqueue_Object())
PLAYQUEUES = [] PLAYQUEUES = []
@ -133,7 +131,7 @@ class PlayqueueMonitor(Thread):
old_item.kodi_type == new_item['type']) old_item.kodi_type == new_item['type'])
else: else:
try: try:
plex_id = REGEX.findall(new_item['file'])[0] plex_id = utils.REGEX_PLEX_ID.findall(new_item['file'])[0]
except IndexError: except IndexError:
LOG.debug('Comparing paths directly as a fallback') LOG.debug('Comparing paths directly as a fallback')
identical = old_item.file == new_item['file'] identical = old_item.file == new_item['file']

View file

@ -30,15 +30,15 @@ http://stackoverflow.com/questions/111945/is-there-any-way-to-do-http-put-in-pyt
(and others...) (and others...)
""" """
from logging import getLogger from logging import getLogger
from re import compile as re_compile, sub from re import sub
from urllib import urlencode, unquote from urllib import urlencode, unquote
import os
from xbmcgui import ListItem from xbmcgui import ListItem
from xbmcvfs import exists from xbmcvfs import exists
from .downloadutils import DownloadUtils as DU from .downloadutils import DownloadUtils as DU
from . import clientinfo from . import clientinfo
from . import utils from . import utils
from . import path_ops
from . import plex_functions as PF from . import plex_functions as PF
from . import plexdb_functions as plexdb from . import plexdb_functions as plexdb
from . import kodidb_functions as kodidb from . import kodidb_functions as kodidb
@ -48,12 +48,6 @@ from . import state
############################################################################### ###############################################################################
LOG = getLogger('PLEX.plex_api') LOG = getLogger('PLEX.plex_api')
REGEX_IMDB = re_compile(r'''/(tt\d+)''')
REGEX_TVDB = re_compile(r'''thetvdb:\/\/(.+?)\?''')
if not utils.exists_dir(v.EXTERNAL_SUBTITLE_TEMP_PATH):
os.makedirs(v.EXTERNAL_SUBTITLE_TEMP_PATH)
############################################################################### ###############################################################################
@ -438,10 +432,10 @@ class API(object):
return None return None
if providername == 'imdb': if providername == 'imdb':
regex = REGEX_IMDB regex = utils.REGEX_IMDB
elif providername == 'tvdb': elif providername == 'tvdb':
# originally e.g. com.plexapp.agents.thetvdb://276564?lang=en # originally e.g. com.plexapp.agents.thetvdb://276564?lang=en
regex = REGEX_TVDB regex = utils.REGEX_TVDB
else: else:
return None return None
@ -1246,7 +1240,7 @@ class API(object):
# Get additional info (filename / languages) # Get additional info (filename / languages)
filename = None filename = None
if 'file' in entry[0].attrib: if 'file' in entry[0].attrib:
filename = os.path.basename(entry[0].attrib['file']) filename = path_ops.path.basename(entry[0].attrib['file'])
# Languages of audio streams # Languages of audio streams
languages = [] languages = []
for stream in entry[0]: for stream in entry[0]:
@ -1401,7 +1395,7 @@ class API(object):
Returns the path to the downloaded subtitle or None Returns the path to the downloaded subtitle or None
""" """
path = os.path.join(v.EXTERNAL_SUBTITLE_TEMP_PATH, filename) path = path_ops.path.join(v.EXTERNAL_SUBTITLE_TEMP_PATH, filename)
response = DU().downloadUrl(url, return_response=True) response = DU().downloadUrl(url, return_response=True)
try: try:
response.status_code response.status_code
@ -1410,7 +1404,7 @@ class API(object):
return return
else: else:
LOG.debug('Writing temp subtitle to %s', path) LOG.debug('Writing temp subtitle to %s', path)
with open(utils.encode_path(path), 'wb') as filer: with open(path_ops.encode_path(path), 'wb') as filer:
filer.write(response.content) filer.write(response.content)
return path return path
@ -1603,17 +1597,18 @@ class API(object):
check = exists(utils.try_encode(path)) check = exists(utils.try_encode(path))
else: else:
# directories # directories
if "\\" in path: checkpath = utils.try_encode(path)
if not path.endswith('\\'): if b"\\" in checkpath:
if not checkpath.endswith('\\'):
# Add the missing backslash # Add the missing backslash
check = utils.exists_dir(path + "\\") check = utils.exists_dir(checkpath + "\\")
else: else:
check = utils.exists_dir(path) check = utils.exists_dir(checkpath)
else: else:
if not path.endswith('/'): if not checkpath.endswith('/'):
check = utils.exists_dir(path + "/") check = utils.exists_dir(checkpath + "/")
else: else:
check = utils.exists_dir(path) check = utils.exists_dir(checkpath)
if not check: if not check:
if force_check is False: if force_check is False:

View file

@ -3,7 +3,6 @@ from logging import getLogger
from urllib import urlencode, quote_plus from urllib import urlencode, quote_plus
from ast import literal_eval from ast import literal_eval
from urlparse import urlparse, parse_qsl from urlparse import urlparse, parse_qsl
from re import compile as re_compile
from copy import deepcopy from copy import deepcopy
from time import time from time import time
from threading import Thread from threading import Thread
@ -18,8 +17,6 @@ from . import variables as v
LOG = getLogger('PLEX.plex_functions') LOG = getLogger('PLEX.plex_functions')
CONTAINERSIZE = int(utils.settings('limitindex')) CONTAINERSIZE = int(utils.settings('limitindex'))
REGEX_PLEX_KEY = re_compile(r'''/(.+)/(\d+)$''')
REGEX_PLEX_DIRECT = re_compile(r'''\.plex\.direct:\d+$''')
# For discovery of PMS in the local LAN # For discovery of PMS in the local LAN
PLEX_GDM_IP = '239.0.0.250' # multicast to PMS PLEX_GDM_IP = '239.0.0.250' # multicast to PMS
@ -47,7 +44,7 @@ def GetPlexKeyNumber(plexKey):
Returns ('','') if nothing is found Returns ('','') if nothing is found
""" """
try: try:
result = REGEX_PLEX_KEY.findall(plexKey)[0] result = utils.REGEX_END_DIGITS.findall(plexKey)[0]
except IndexError: except IndexError:
result = ('', '') result = ('', '')
return result return result
@ -411,7 +408,7 @@ def _pms_list_from_plex_tv(token):
def _poke_pms(pms, queue): def _poke_pms(pms, queue):
data = pms['connections'][0].attrib data = pms['connections'][0].attrib
url = data['uri'] url = data['uri']
if data['local'] == '1' and REGEX_PLEX_DIRECT.findall(url): if data['local'] == '1' and utils.REGEX_PLEX_DIRECT.findall(url):
# In case DNS resolve of plex.direct does not work, append a new # In case DNS resolve of plex.direct does not work, append a new
# connection that will directly access the local IP (e.g. internet down) # connection that will directly access the local IP (e.g. internet down)
conn = deepcopy(pms['connections'][0]) conn = deepcopy(pms['connections'][0])

View file

@ -55,6 +55,7 @@ class Service():
utils.settings('useDirectPaths') == '1') utils.settings('useDirectPaths') == '1')
LOG.info("Number of sync threads: %s", LOG.info("Number of sync threads: %s",
utils.settings('syncThreadNumber')) utils.settings('syncThreadNumber'))
LOG.info('Playlist m3u encoding: %s', v.M3U_ENCODING)
LOG.info("Full sys.argv received: %s", sys.argv) LOG.info("Full sys.argv received: %s", sys.argv)
self.monitor = xbmc.Monitor() self.monitor = xbmc.Monitor()
# Load/Reset PKC entirely - important for user/Kodi profile switch # Load/Reset PKC entirely - important for user/Kodi profile switch

View file

@ -3,14 +3,14 @@
from logging import getLogger from logging import getLogger
from threading import Thread from threading import Thread
from xbmc import sleep, executebuiltin, translatePath from xbmc import sleep, executebuiltin
import xbmcaddon
from xbmcvfs import exists
from .downloadutils import DownloadUtils as DU from .downloadutils import DownloadUtils as DU
from . import utils from . import utils
from . import path_ops
from . import plex_tv from . import plex_tv
from . import plex_functions as PF from . import plex_functions as PF
from . import variables as v
from . import state from . import state
############################################################################### ###############################################################################
@ -44,7 +44,6 @@ class UserClient(Thread):
self.ssl = None self.ssl = None
self.sslcert = None self.sslcert = None
self.addon = xbmcaddon.Addon()
self.do_utils = None self.do_utils = None
Thread.__init__(self) Thread.__init__(self)
@ -197,11 +196,8 @@ class UserClient(Thread):
'Addon.Openutils.settings(plugin.video.plexkodiconnect)') 'Addon.Openutils.settings(plugin.video.plexkodiconnect)')
return False return False
# Get /profile/addon_data
addondir = translatePath(self.addon.getAddonInfo('profile'))
# If there's no settings.xml # If there's no settings.xml
if not exists("%ssettings.xml" % addondir): if not path_ops.exists("%ssettings.xml" % v.ADDON_PROFILE):
LOG.error("Error, no settings.xml found.") LOG.error("Error, no settings.xml found.")
self.auth = False self.auth = False
return False return False

View file

@ -4,11 +4,6 @@ Various functions and decorators for PKC
""" """
############################################################################### ###############################################################################
from logging import getLogger from logging import getLogger
import xbmc
import xbmcaddon
import xbmcgui
from xbmcvfs import exists, delete
import os
from cProfile import Profile from cProfile import Profile
from pstats import Stats from pstats import Stats
from sqlite3 import connect, OperationalError from sqlite3 import connect, OperationalError
@ -18,13 +13,14 @@ from time import localtime, strftime
from unicodedata import normalize from unicodedata import normalize
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
from functools import wraps, partial from functools import wraps, partial
from shutil import rmtree
from urllib import quote_plus from urllib import quote_plus
import hashlib import hashlib
import re import re
import unicodedata import xbmc
import xbmcaddon
import xbmcgui
from .watchdog.utils import unicode_paths from . import path_ops
from . import variables as v from . import variables as v
from . import state from . import state
@ -36,9 +32,19 @@ WINDOW = xbmcgui.Window(10000)
ADDON = xbmcaddon.Addon(id='plugin.video.plexkodiconnect') ADDON = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
EPOCH = datetime.utcfromtimestamp(0) EPOCH = datetime.utcfromtimestamp(0)
# Grab Plex id from '...plex_id=XXXX....'
REGEX_PLEX_ID = re.compile(r'''plex_id=(\d+)''') REGEX_PLEX_ID = re.compile(r'''plex_id=(\d+)''')
# Return the numbers at the end of an url like '.../.../XXXX'
REGEX_END_DIGITS = re.compile(r'''/(.+)/(\d+)$''')
REGEX_PLEX_DIRECT = re.compile(r'''\.plex\.direct:\d+$''')
REGEX_FILE_NUMBERING = re.compile(r'''_(\d+)\.\w+$''') REGEX_FILE_NUMBERING = re.compile(r'''_(\d+)\.\w+$''')
# Plex API
REGEX_IMDB = re.compile(r'''/(tt\d+)''')
REGEX_TVDB = re.compile(r'''thetvdb:\/\/(.+?)\?''')
# Plex music
REGEX_MUSICPATH = re.compile(r'''^\^(.+)\$$''')
# Grab Plex id from an URL-encoded string
REGEX_PLEX_ID_FROM_URL = re.compile(r'''metadata%2F(\d+)''')
############################################################################### ###############################################################################
# Main methods # Main methods
@ -106,30 +112,6 @@ def settings(setting, value=None):
return try_decode(addon.getSetting(setting)) return try_decode(addon.getSetting(setting))
def exists_dir(path):
"""
Safe way to check whether the directory path exists already (broken in Kodi
<17)
Feed with encoded string or unicode
"""
if v.KODIVERSION >= 17:
answ = exists(try_encode(path))
else:
dummyfile = os.path.join(try_decode(path), 'dummyfile.txt')
try:
with open(encode_path(dummyfile), 'w') as filer:
filer.write('text')
except IOError:
# folder does not exist yet
answ = 0
else:
# Folder exists. Delete file again.
delete(try_encode(dummyfile))
answ = 1
return answ
def lang(stringid): def lang(stringid):
""" """
Central string retrieval from strings.po Central string retrieval from strings.po
@ -248,26 +230,6 @@ def kodi_time_to_millis(time):
return ret return ret
def encode_path(path):
"""
Filenames and paths are not necessarily utf-8 encoded. Use this function
instead of try_encode/trydecode if working with filenames and paths!
(os.walk only feeds on encoded paths. sys.getfilesystemencoding returns None
for Raspberry Pi)
"""
return unicode_paths.encode(path)
def decode_path(path):
"""
Filenames and paths are not necessarily utf-8 encoded. Use this function
instead of try_encode/trydecode if working with filenames and paths!
(os.walk only feeds on encoded paths. sys.getfilesystemencoding returns None
for Raspberry Pi)
"""
return unicode_paths.decode(path)
def try_encode(input_str, encoding='utf-8'): def try_encode(input_str, encoding='utf-8'):
""" """
Will try to encode input_str (in unicode) to encoding. This possibly Will try to encode input_str (in unicode) to encoding. This possibly
@ -333,10 +295,6 @@ def valid_filename(text):
else: else:
# Linux # Linux
text = re.sub(r'/', '', text) text = re.sub(r'/', '', text)
if not os.path.supports_unicode_filenames:
text = unicodedata.normalize('NFKD', text)
text = text.encode('ascii', 'ignore')
text = text.decode('ascii')
# Ensure that filename length is at most 255 chars (including 3 chars for # Ensure that filename length is at most 255 chars (including 3 chars for
# filename extension and 1 dot to separate the extension) # filename extension and 1 dot to separate the extension)
text = text[:min(len(text), 251)] text = text[:min(len(text), 251)]
@ -485,15 +443,15 @@ def wipe_database():
# Delete all synced playlists # Delete all synced playlists
for path in playlist_paths: for path in playlist_paths:
try: try:
os.remove(path) path_ops.remove(path)
except (OSError, IOError): except (OSError, IOError):
pass pass
LOG.info("Resetting all cached artwork.") LOG.info("Resetting all cached artwork.")
# Remove all existing textures first # Remove all existing textures first
path = xbmc.translatePath("special://thumbnails/") path = path_ops.translate_path("special://thumbnails/")
if exists(path): if path_ops.exists(path):
rmtree(try_decode(path), ignore_errors=True) path_ops.rmtree(path, ignore_errors=True)
# remove all existing data from texture DB # remove all existing data from texture DB
connection = kodi_sql('texture') connection = kodi_sql('texture')
cursor = connection.cursor() cursor = connection.cursor()
@ -547,10 +505,8 @@ def reset(ask_user=True):
heading='{plex} %s ' % lang(30132), heading='{plex} %s ' % lang(30132),
line1=lang(39603)): line1=lang(39603)):
# Delete the settings # Delete the settings
addon = xbmcaddon.Addon()
addondir = try_decode(xbmc.translatePath(addon.getAddonInfo('profile')))
LOG.info("Deleting: settings.xml") LOG.info("Deleting: settings.xml")
os.remove("%ssettings.xml" % addondir) path_ops.remove("%ssettings.xml" % v.ADDON_PROFILE)
reboot_kodi() reboot_kodi()
@ -612,29 +568,6 @@ def compare_version(current, minimum):
return curr_patch >= min_patch return curr_patch >= min_patch
def normalize_nodes(text):
"""
For video nodes
"""
text = text.replace(":", "")
text = text.replace("/", "-")
text = text.replace("\\", "-")
text = text.replace("<", "")
text = text.replace(">", "")
text = text.replace("*", "")
text = text.replace("?", "")
text = text.replace('|', "")
text = text.replace('(', "")
text = text.replace(')', "")
text = text.strip()
# Remove dots from the last character as windows can not have directories
# with dots at the end
text = text.rstrip('.')
text = try_encode(normalize('NFKD', unicode(text, 'utf-8')))
return text
def normalize_string(text): def normalize_string(text):
""" """
For theme media, do not modify unless modified in TV Tunes For theme media, do not modify unless modified in TV Tunes
@ -656,6 +589,28 @@ def normalize_string(text):
return text return text
def normalize_nodes(text):
"""
For video nodes. Returns unicode
"""
text = text.replace(":", "")
text = text.replace("/", "-")
text = text.replace("\\", "-")
text = text.replace("<", "")
text = text.replace(">", "")
text = text.replace("*", "")
text = text.replace("?", "")
text = text.replace('|', "")
text = text.replace('(', "")
text = text.replace(')', "")
text = text.strip()
# Remove dots from the last character as windows can not have directories
# with dots at the end
text = text.rstrip('.')
text = normalize('NFKD', unicode(text, 'utf-8'))
return text
def indent(elem, level=0): def indent(elem, level=0):
""" """
Prettifies xml trees. Pass the etree root in Prettifies xml trees. Pass the etree root in
@ -702,9 +657,9 @@ class XmlKodiSetting(object):
top_element=None): top_element=None):
self.filename = filename self.filename = filename
if path is None: if path is None:
self.path = os.path.join(v.KODI_PROFILE, filename) self.path = path_ops.path.join(v.KODI_PROFILE, filename)
else: else:
self.path = os.path.join(path, filename) self.path = path_ops.path.join(path, filename)
self.force_create = force_create self.force_create = force_create
self.top_element = top_element self.top_element = top_element
self.tree = None self.tree = None
@ -869,7 +824,7 @@ def passwords_xml():
""" """
To add network credentials to Kodi's password xml To add network credentials to Kodi's password xml
""" """
path = try_decode(xbmc.translatePath("special://userdata/")) path = path_ops.translate_path('special://userdata/')
xmlpath = "%spasswords.xml" % path xmlpath = "%spasswords.xml" % path
try: try:
xmlparse = etree.parse(xmlpath) xmlparse = etree.parse(xmlpath)
@ -990,7 +945,7 @@ def playlist_xsp(mediatype, tagname, viewid, viewtype="", delete=False):
""" """
Feed with tagname as unicode Feed with tagname as unicode
""" """
path = try_decode(xbmc.translatePath("special://profile/playlists/video/")) path = path_ops.translate_path("special://profile/playlists/video/")
if viewtype == "mixed": if viewtype == "mixed":
plname = "%s - %s" % (tagname, mediatype) plname = "%s - %s" % (tagname, mediatype)
xsppath = "%sPlex %s - %s.xsp" % (path, viewid, mediatype) xsppath = "%sPlex %s - %s.xsp" % (path, viewid, mediatype)
@ -999,15 +954,15 @@ def playlist_xsp(mediatype, tagname, viewid, viewtype="", delete=False):
xsppath = "%sPlex %s.xsp" % (path, viewid) xsppath = "%sPlex %s.xsp" % (path, viewid)
# Create the playlist directory # Create the playlist directory
if not exists(try_encode(path)): if not path_ops.exists(path):
LOG.info("Creating directory: %s", path) LOG.info("Creating directory: %s", path)
os.makedirs(path) path_ops.makedirs(path)
# Only add the playlist if it doesn't already exists # Only add the playlist if it doesn't already exists
if exists(try_encode(xsppath)): if path_ops.exists(xsppath):
LOG.info('Path %s does exist', xsppath) LOG.info('Path %s does exist', xsppath)
if delete: if delete:
os.remove(xsppath) path_ops.remove(xsppath)
LOG.info("Successfully removed playlist: %s.", tagname) LOG.info("Successfully removed playlist: %s.", tagname)
return return
@ -1019,7 +974,7 @@ def playlist_xsp(mediatype, tagname, viewid, viewtype="", delete=False):
'show': 'tvshows' 'show': 'tvshows'
} }
LOG.info("Writing playlist file to: %s", xsppath) LOG.info("Writing playlist file to: %s", xsppath)
with open(encode_path(xsppath), 'wb') as filer: with open(path_ops.encode_path(xsppath), 'wb') as filer:
filer.write(try_encode( filer.write(try_encode(
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n' '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n'
'<smartplaylist type="%s">\n\t' '<smartplaylist type="%s">\n\t'
@ -1037,22 +992,22 @@ def delete_playlists():
""" """
Clean up the playlists Clean up the playlists
""" """
path = try_decode(xbmc.translatePath("special://profile/playlists/video/")) path = path_ops.translate_path('special://profile/playlists/video/')
for root, _, files in os.walk(path): for root, _, files in path_ops.walk(path):
for file in files: for file in files:
if file.startswith('Plex'): if file.startswith('Plex'):
os.remove(os.path.join(root, file)) path_ops.remove(path_ops.path.join(root, file))
def delete_nodes(): def delete_nodes():
""" """
Clean up video nodes Clean up video nodes
""" """
path = try_decode(xbmc.translatePath("special://profile/library/video/")) path = path_ops.translate_path("special://profile/library/video/")
for root, dirs, _ in os.walk(path): for root, dirs, _ in path_ops.walk(path):
for directory in dirs: for directory in dirs:
if directory.startswith('Plex-'): if directory.startswith('Plex-'):
rmtree(os.path.join(root, directory)) path_ops.rmtree(path_ops.path.join(root, directory))
break break
@ -1065,7 +1020,7 @@ def generate_file_md5(path):
""" """
m = hashlib.md5() m = hashlib.md5()
m.update(path.encode('utf-8')) m.update(path.encode('utf-8'))
with open(encode_path(path), 'rb') as f: with open(path_ops.encode_path(path), 'rb') as f:
while True: while True:
piece = f.read(32768) piece = f.read(32768)
if not piece: if not piece:

View file

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os import os
import sys
import xbmc import xbmc
from xbmcaddon import Addon from xbmcaddon import Addon
@ -34,7 +35,9 @@ _ADDON = Addon()
ADDON_NAME = 'PlexKodiConnect' ADDON_NAME = 'PlexKodiConnect'
ADDON_ID = 'plugin.video.plexkodiconnect' ADDON_ID = 'plugin.video.plexkodiconnect'
ADDON_VERSION = _ADDON.getAddonInfo('version') ADDON_VERSION = _ADDON.getAddonInfo('version')
ADDON_PATH = try_decode(_ADDON.getAddonInfo('path'))
ADDON_FOLDER = try_decode(xbmc.translatePath('special://home')) ADDON_FOLDER = try_decode(xbmc.translatePath('special://home'))
ADDON_PROFILE = try_decode(_ADDON.getAddonInfo('profile'))
KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1) KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1)
KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2]) KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
@ -81,10 +84,6 @@ MIN_DB_VERSION = '2.0.27'
# Database paths # Database paths
_DB_VIDEO_VERSION = { _DB_VIDEO_VERSION = {
13: 78, # Gotham
14: 90, # Helix
15: 93, # Isengard
16: 99, # Jarvis
17: 107, # Krypton 17: 107, # Krypton
18: 109 # Leia 18: 109 # Leia
} }
@ -92,10 +91,6 @@ DB_VIDEO_PATH = try_decode(xbmc.translatePath(
"special://database/MyVideos%s.db" % _DB_VIDEO_VERSION[KODIVERSION])) "special://database/MyVideos%s.db" % _DB_VIDEO_VERSION[KODIVERSION]))
_DB_MUSIC_VERSION = { _DB_MUSIC_VERSION = {
13: 46, # Gotham
14: 48, # Helix
15: 52, # Isengard
16: 56, # Jarvis
17: 60, # Krypton 17: 60, # Krypton
18: 70 # Leia 18: 70 # Leia
} }
@ -103,10 +98,6 @@ DB_MUSIC_PATH = try_decode(xbmc.translatePath(
"special://database/MyMusic%s.db" % _DB_MUSIC_VERSION[KODIVERSION])) "special://database/MyMusic%s.db" % _DB_MUSIC_VERSION[KODIVERSION]))
_DB_TEXTURE_VERSION = { _DB_TEXTURE_VERSION = {
13: 13, # Gotham
14: 13, # Helix
15: 13, # Isengard
16: 13, # Jarvis
17: 13, # Krypton 17: 13, # Krypton
18: 13 # Leia 18: 13 # Leia
} }
@ -122,6 +113,12 @@ EXTERNAL_SUBTITLE_TEMP_PATH = try_decode(xbmc.translatePath(
# Multiply Plex time by this factor to receive Kodi time # Multiply Plex time by this factor to receive Kodi time
PLEX_TO_KODI_TIMEFACTOR = 1.0 / 1000.0 PLEX_TO_KODI_TIMEFACTOR = 1.0 / 1000.0
# We're "failing" playback with a video of 0 length
NULL_VIDEO = os.path.join(ADDON_FOLDER,
'addons',
ADDON_ID,
'empty_video.mp4')
# Playlist stuff # Playlist stuff
PLAYLIST_PATH = os.path.join(KODI_PROFILE, 'playlists') PLAYLIST_PATH = os.path.join(KODI_PROFILE, 'playlists')
PLAYLIST_PATH_MIXED = os.path.join(PLAYLIST_PATH, 'mixed') PLAYLIST_PATH_MIXED = os.path.join(PLAYLIST_PATH, 'mixed')
@ -512,3 +509,14 @@ PLEX_STREAM_TYPE_FROM_STREAM_TYPE = {
'audio': '2', 'audio': '2',
'subtitle': '3' 'subtitle': '3'
} }
# Encoding to be used for our m3u playlist files
# m3u files do not have encoding specified by definition, unfortunately.
if PLATFORM == 'Windows':
M3U_ENCODING = 'mbcs'
else:
M3U_ENCODING = sys.getfilesystemencoding()
if (not M3U_ENCODING or
M3U_ENCODING == 'ascii' or
M3U_ENCODING == 'ANSI_X3.4-1968'):
M3U_ENCODING = 'utf-8'

View file

@ -1,13 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
############################################################################### ###############################################################################
from logging import getLogger from logging import getLogger
from distutils import dir_util
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
from os import makedirs
import xbmc import xbmc
from xbmcvfs import exists
from . import utils from . import utils
from . import path_ops
from . import variables as v from . import variables as v
from . import state from . import state
@ -16,7 +14,6 @@ from . import state
LOG = getLogger('PLEX.videonodes') LOG = getLogger('PLEX.videonodes')
############################################################################### ###############################################################################
# Paths are strings, NOT unicode!
class VideoNodes(object): class VideoNodes(object):
@ -66,33 +63,30 @@ class VideoNodes(object):
dirname = viewid dirname = viewid
# Returns strings # Returns strings
path = utils.try_decode(xbmc.translatePath( path = path_ops.translate_path('special://profile/library/video/')
"special://profile/library/video/")) nodepath = path_ops.translate_path(
nodepath = utils.try_decode(xbmc.translatePath( 'special://profile/library/video/Plex-%s/' % dirname)
"special://profile/library/video/Plex-%s/" % dirname))
if delete: if delete:
if utils.exists_dir(nodepath): if path_ops.exists(nodepath):
from shutil import rmtree path_ops.rmtree(nodepath)
rmtree(nodepath)
LOG.info("Sucessfully removed videonode: %s." % tagname) LOG.info("Sucessfully removed videonode: %s." % tagname)
return return
# Verify the video directory # Verify the video directory
if not utils.exists_dir(path): if not path_ops.exists(path):
dir_util.copy_tree( path_ops.copy_tree(
src=utils.try_decode( src=path_ops.translate_path(
xbmc.translatePath("special://xbmc/system/library/video")), 'special://xbmc/system/library/video'),
dst=utils.try_decode( dst=path_ops.translate_path('special://profile/library/video'),
xbmc.translatePath("special://profile/library/video")),
preserve_mode=0) # do not copy permission bits! preserve_mode=0) # do not copy permission bits!
# Create the node directory # Create the node directory
if mediatype != "photos": if mediatype != "photos":
if not utils.exists_dir(nodepath): if not path_ops.exists(nodepath):
# folder does not exist yet # folder does not exist yet
LOG.debug('Creating folder %s' % nodepath) LOG.debug('Creating folder %s' % nodepath)
makedirs(nodepath) path_ops.makedirs(nodepath)
# Create index entry # Create index entry
nodeXML = "%sindex.xml" % nodepath nodeXML = "%sindex.xml" % nodepath
@ -298,7 +292,7 @@ class VideoNodes(object):
# kodi picture sources somehow # kodi picture sources somehow
continue continue
if exists(utils.try_encode(nodeXML)): if path_ops.exists(nodeXML):
# Don't recreate xml if already exists # Don't recreate xml if already exists
continue continue
@ -403,13 +397,12 @@ class VideoNodes(object):
utils.indent(root) utils.indent(root)
except: except:
pass pass
etree.ElementTree(root).write(nodeXML, encoding="UTF-8") etree.ElementTree(root).write(path_ops.encode_path(nodeXML),
encoding="UTF-8")
def singleNode(self, indexnumber, tagname, mediatype, itemtype): def singleNode(self, indexnumber, tagname, mediatype, itemtype):
tagname = utils.try_encode(tagname) cleantagname = utils.normalize_nodes(tagname)
cleantagname = utils.try_decode(utils.normalize_nodes(tagname)) nodepath = path_ops.translate_path('special://profile/library/video/')
nodepath = utils.try_decode(xbmc.translatePath(
"special://profile/library/video/"))
nodeXML = "%splex_%s.xml" % (nodepath, cleantagname) nodeXML = "%splex_%s.xml" % (nodepath, cleantagname)
path = "library://video/plex_%s.xml" % cleantagname path = "library://video/plex_%s.xml" % cleantagname
if v.KODIVERSION >= 17: if v.KODIVERSION >= 17:
@ -419,13 +412,12 @@ class VideoNodes(object):
windowpath = "ActivateWindow(Video,%s,return)" % path windowpath = "ActivateWindow(Video,%s,return)" % path
# Create the video node directory # Create the video node directory
if not utils.exists_dir(nodepath): if not path_ops.exists(nodepath):
# We need to copy over the default items # We need to copy over the default items
dir_util.copy_tree( path_ops.copy_tree(
src=utils.try_decode( src=path_ops.translate_path(
xbmc.translatePath("special://xbmc/system/library/video")), 'special://xbmc/system/library/video'),
dst=utils.try_decode( dst=path_ops.translate_path('special://profile/library/video'),
xbmc.translatePath("special://profile/library/video")),
preserve_mode=0) # do not copy permission bits! preserve_mode=0) # do not copy permission bits!
labels = { labels = {
@ -440,7 +432,7 @@ class VideoNodes(object):
utils.window('%s.content' % embynode, value=path) utils.window('%s.content' % embynode, value=path)
utils.window('%s.type' % embynode, value=itemtype) utils.window('%s.type' % embynode, value=itemtype)
if exists(utils.try_encode(nodeXML)): if path_ops.exists(nodeXML):
# Don't recreate xml if already exists # Don't recreate xml if already exists
return return