diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index c6256a63..07286f7f 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -3,15 +3,13 @@ ############################################################################### from logging import getLogger from Queue import Queue, Empty -from shutil import rmtree from urllib import quote_plus, unquote from threading import Thread -from os import makedirs import requests import xbmc -from xbmcvfs import exists +from . import path_ops from . import utils from . import state @@ -202,10 +200,9 @@ class Artwork(): if utils.dialog('yesno', "Image Texture Cache", utils.lang(39251)): LOG.info("Resetting all cache data first") # Remove all existing textures first - path = utils.try_decode( - xbmc.translatePath("special://thumbnails/")) - if utils.exists_dir(path): - rmtree(path, ignore_errors=True) + path = path_ops.translate_path('special://thumbnails/') + if path_ops.exists(path): + path_ops.rmtree(path, ignore_errors=True) self.restore_cache_directories() # remove all existing data from texture DB @@ -321,10 +318,11 @@ class Artwork(): pass else: # 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) - if exists(path): - rmtree(utils.try_decode(path), ignore_errors=True) + if path_ops.exists(path): + path_ops.rmtree(path, ignore_errors=True) cursor.execute("DELETE FROM texture WHERE url = ?", (url,)) connection.commit() finally: @@ -337,8 +335,8 @@ class Artwork(): "a", "b", "c", "d", "e", "f", "Video", "plex") for path in paths: - makedirs(utils.try_decode( - xbmc.translatePath("special://thumbnails/%s" % path))) + new_path = path_ops.translate_path("special://thumbnails/%s" % path) + path_ops.makedirs(utils.encode_path(new_path)) class ArtworkSyncMessage(object): diff --git a/resources/lib/context.py b/resources/lib/context.py index 3f629689..914e9a5d 100644 --- a/resources/lib/context.py +++ b/resources/lib/context.py @@ -1,16 +1,15 @@ # -*- coding: utf-8 -*- ############################################################################### from logging import getLogger -from os.path import join import xbmcgui -from xbmcaddon import Addon from . import utils +from . import path_ops +from . import variables as v ############################################################################### LOG = getLogger('PLEX.context') -ADDON = Addon('plugin.video.plexkodiconnect') ACTION_PARENT_DIR = 9 ACTION_PREVIOUS_MENU = 10 @@ -65,10 +64,11 @@ class ContextMenu(xbmcgui.WindowXMLDialog): self.close() def _add_editcontrol(self, x, y, height, width, password=None): - media = join(ADDON.getAddonInfo('path'), - 'resources', 'skins', 'default', 'media') + media = path_ops.path.join( + 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, - filename=join(media, "white.png"), + filename=filename, aspectRatio=0, colorDiffuse="ff111111") control.setPosition(x, y) diff --git a/resources/lib/context_entry.py b/resources/lib/context_entry.py index 65d80d80..373e52cf 100644 --- a/resources/lib/context_entry.py +++ b/resources/lib/context_entry.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- ############################################################################### from logging import getLogger -from xbmcaddon import Addon import xbmc import xbmcgui @@ -103,7 +102,7 @@ class ContextMenu(object): options.append(OPTIONS['Addon']) context_menu = context.ContextMenu( "script-plex-context.xml", - Addon('plugin.video.plexkodiconnect').getAddonInfo('path'), + utils.try_encode(v.ADDON_PATH), "default", "1080i") context_menu.set_options(options) diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 1c78436a..0d433727 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -5,16 +5,14 @@ # ############################################################################### from logging import getLogger -from shutil import copyfile -from os import walk, makedirs -from os.path import basename, join from sys import argv from urllib import urlencode import xbmcplugin -from xbmc import sleep, executebuiltin, translatePath +from xbmc import sleep, executebuiltin from xbmcgui import ListItem from . import utils +from . import path_ops from .downloadutils import DownloadUtils as DU from .plex_api import API from . import plex_functions as PF @@ -441,7 +439,7 @@ def get_video_files(plex_id, params): item = PF.GetPlexMetadata(plex_id) try: - path = item[0][0][0].attrib['file'] + path = utils.try_decode(item[0][0][0].attrib['file']) except (TypeError, IndexError, AttributeError, KeyError): LOG.error('Could not get file path for item %s', plex_id) return xbmcplugin.endOfDirectory(HANDLE) @@ -453,18 +451,19 @@ def get_video_files(plex_id, params): elif '\\' in path: path = path.replace('\\', '\\\\') # Directory only, get rid of filename - path = path.replace(basename(path), '') - if utils.exists_dir(path): - for root, dirs, files in walk(path): + path = path.replace(path_ops.path.basename(path), '') + if path_ops.exists(path): + for root, dirs, files in path_ops.walk(path): 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) xbmcplugin.addDirectoryItem(handle=HANDLE, url=item_path, listitem=listitem, isFolder=True) 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) xbmcplugin.addDirectoryItem(handle=HANDLE, url=file, @@ -492,11 +491,11 @@ def extra_fanart(plex_id, plex_path): # We need to store the images locally for this to work # because of the caching system in xbmc - fanart_dir = utils.try_decode(translatePath( - "special://thumbnails/plex/%s/" % plex_id)) - if not utils.exists_dir(fanart_dir): + fanart_dir = path_ops.translate_path("special://thumbnails/plex/%s/" + % plex_id) + if not path_ops.exists(fanart_dir): # Download the images to the cache directory - makedirs(fanart_dir) + path_ops.makedirs(fanart_dir) xml = PF.GetPlexMetadata(plex_id) if xml is None: 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'] for count, backdrop in enumerate(backdrops): # Same ordering as in artwork - art_file = utils.try_encode(join(fanart_dir, - "fanart%.3d.jpg" % count)) + art_file = utils.try_encode(path_ops.path.join( + fanart_dir, "fanart%.3d.jpg" % count)) listitem = ListItem("%.3d" % count, path=art_file) xbmcplugin.addDirectoryItem( handle=HANDLE, url=art_file, listitem=listitem) - copyfile(backdrop, utils.try_decode(art_file)) + path_ops.copyfile(backdrop, utils.try_decode(art_file)) else: LOG.info("Found cached backdrop.") # 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: - 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) xbmcplugin.addDirectoryItem(handle=HANDLE, url=art_file, diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 04c4cb4a..4f027ce7 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -7,6 +7,7 @@ import xml.etree.ElementTree as etree from xbmc import executebuiltin, translatePath from . import utils +from . import path_ops from . import migration from .downloadutils import DownloadUtils as DU 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 = ( "plex_online", "plex_serverStatus", "plex_shouldStop", "plex_dbScan", diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 931999ed..9a9e6373 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1025,23 +1025,11 @@ class LibrarySync(Thread): do with "process_" methods """ if message['type'] == 'playing': - try: - self.process_playing(message['PlaySessionStateNotification']) - except KeyError: - LOG.error('Received invalid PMS message for playstate: %s', - message) + self.process_playing(message['PlaySessionStateNotification']) elif message['type'] == 'timeline': - try: - self.process_timeline(message['TimelineEntry']) - except (KeyError, ValueError): - LOG.error('Received invalid PMS message for timeline: %s', - message) + self.process_timeline(message['TimelineEntry']) elif message['type'] == 'activity': - try: - self.process_activity(message['ActivityNotification']) - except KeyError: - LOG.error('Received invalid PMS message for activity: %s', - message) + self.process_activity(message['ActivityNotification']) def multi_delete(self, liste, delete_list): """ @@ -1196,7 +1184,7 @@ class LibrarySync(Thread): continue playlists.process_websocket(plex_id=str(item['itemID']), updated_at=str(item['updatedAt']), - state=status) + status=status) elif status == 9: # Immediately and always process deletions (as the PMS will # send additional message with other codes) diff --git a/resources/lib/music.py b/resources/lib/music.py index 518f4747..8731237c 100644 --- a/resources/lib/music.py +++ b/resources/lib/music.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- from logging import getLogger -from re import compile as re_compile from xml.etree.ElementTree import ParseError from . import utils @@ -9,8 +8,6 @@ from . import variables as v ############################################################################### LOG = getLogger('PLEX.music') - -REGEX_MUSICPATH = re_compile(r'''^\^(.+)\$$''') ############################################################################### diff --git a/resources/lib/path_ops.py b/resources/lib/path_ops.py new file mode 100644 index 00000000..a08f32e2 --- /dev/null +++ b/resources/lib/path_ops.py @@ -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) diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 7b41ad4c..0f3f3b63 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -3,7 +3,6 @@ Used to kick off Kodi playback """ from logging import getLogger from threading import Thread -from os.path import join from xbmc import Player, sleep from .plex_api import API @@ -25,8 +24,6 @@ from . import state LOG = getLogger('PLEX.playback') # Do we need to return ultimately with a setResolvedUrl? 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 if not abort: result = pickler.Playback_Successful() - result.listitem = PKCListItem(path=NULL_VIDEO) + result.listitem = PKCListItem(path=v.NULL_VIDEO) pickler.pickle_me(result) else: # Shows PKC error message diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 83ee8653..c688f857 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -3,10 +3,8 @@ Collection of functions associated with Kodi and Plex playlists and playqueues """ from logging import getLogger -import os import urllib from urlparse import parse_qsl, urlsplit -from re import compile as re_compile from .plex_api import API from . import plex_functions as PF @@ -14,6 +12,7 @@ from . import plexdb_functions as plexdb from . import kodidb_functions as kodidb from .downloadutils import DownloadUtils as DU from . import utils +from . import path_ops from . import json_rpc as js from . import variables as v @@ -21,7 +20,6 @@ from . import variables as v LOG = getLogger('PLEX.playlist_func') -REGEX = re_compile(r'''metadata%2F(\d+)''') ############################################################################### @@ -42,9 +40,9 @@ class PlaylistObjectBaseclase(object): 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 key in self.__dict__: if key in ('id', 'kodi_pl'): @@ -58,7 +56,7 @@ class PlaylistObjectBaseclase(object): else: # e.g. int answ += '\'%s\': %s, ' % (key, unicode(getattr(self, key))) - return utils.try_encode(answ + '}}') + return answ + '}}' class Playlist_Object(PlaylistObjectBaseclase): @@ -82,7 +80,9 @@ class Playlist_Object(PlaylistObjectBaseclase): @kodi_path.setter 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: self.kodi_filename, self.kodi_extension = file.split('.', 1) except ValueError: @@ -220,9 +220,9 @@ class Playlist_Item(object): 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)) for key in self.__dict__: if key in ('id', 'plex_id', 'xml'): @@ -240,7 +240,7 @@ class Playlist_Item(object): answ += '\'xml\': None}}' else: answ += '\'xml\': \'%s\'}}' % self.xml.tag - return utils.try_encode(answ) + return answ def plex_stream_index(self, kodi_stream_index, stream_type): """ @@ -865,7 +865,8 @@ def get_plextype_from_xml(xml): returns None if unsuccessful """ try: - plex_id = REGEX.findall(xml.attrib['playQueueSourceURI'])[0] + plex_id = utils.REGEX_PLEX_ID_FROM_URL.findall( + xml.attrib['playQueueSourceURI'])[0] except IndexError: LOG.error('Could not get plex_id from xml: %s', xml.attrib) return diff --git a/resources/lib/playlists.py b/resources/lib/playlists.py index 23b013ee..bfaeb163 100644 --- a/resources/lib/playlists.py +++ b/resources/lib/playlists.py @@ -1,8 +1,5 @@ # -*- coding: utf-8 -*- from logging import getLogger -import os -import sys -from xbmcvfs import exists from .watchdog.events import FileSystemEventHandler from .watchdog.observers import Observer @@ -11,6 +8,7 @@ from .plex_api import API from . import kodidb_functions as kodidb from . import plexdb_functions as plexdb from . import utils +from . import path_ops from . import variables as v from . import state @@ -32,12 +30,6 @@ EVENT_TYPE_DELETED = 'deleted' EVENT_TYPE_CREATED = 'created' 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): """ @@ -99,20 +91,21 @@ def create_kodi_playlist(plex_id=None, updated_at=None): playlist.plex_updatedat = updated_at LOG.debug('Creating new Kodi playlist from Plex playlist: %s', playlist) name = utils.valid_filename(playlist.plex_name) - path = os.path.join(v.PLAYLIST_PATH, playlist.type, '%s.m3u' % name) - while exists(path) or playlist_object_from_db(path=path): + path = path_ops.path.join(v.PLAYLIST_PATH, playlist.type, '%s.m3u' % name) + while path_ops.exists(path) or playlist_object_from_db(path=path): # In case the Plex playlist names are not unique occurance = utils.REGEX_FILE_NUMBERING.search(path) if not occurance: - path = os.path.join(v.PLAYLIST_PATH, - playlist.type, - '%s_01.m3u' % name[:min(len(name), 248)]) + path = path_ops.path.join(v.PLAYLIST_PATH, + playlist.type, + '%s_01.m3u' % name[:min(len(name), 248)]) else: occurance = int(occurance.group(1)) + 1 - path = os.path.join(v.PLAYLIST_PATH, - playlist.type, - '%s_%02d.m3u' % (name[:min(len(name), 248)], - occurance)) + path = path_ops.path.join(v.PLAYLIST_PATH, + playlist.type, + '%s_%02d.m3u' % (name[:min(len(name), + 248)], + occurance)) LOG.debug('Kodi playlist path: %s', path) playlist.kodi_path = path # Derive filename close to Plex playlist name @@ -130,7 +123,7 @@ def delete_kodi_playlist(playlist): Returns None or raises PL.PlaylistError """ try: - os.remove(playlist.kodi_path) + path_ops.remove(playlist.kodi_path) except (OSError, IOError) as err: LOG.error('Could not delete Kodi playlist file %s. Error:\n %s: %s', 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! """ 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() try: - text = text.decode(ENCODING) + text = text.decode(v.M3U_ENCODING) except UnicodeDecodeError: LOG.warning('Fallback to ISO-8859-1 decoding for %s', playlist) 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 Returns None or raises PL.PlaylistError """ - text = u'#EXTCPlayListM3U::M3U\n' + text = '#EXTCPlayListM3U::M3U\n' for element in xml: api = API(element) - text += (u'#EXTINF:%s,%s\n%s\n' + text += ('#EXTINF:%s,%s\n%s\n' % (api.runtime(), api.title(), api.path())) text += '\n' - text = text.encode(ENCODING, 'ignore') + text = text.encode(v.M3U_ENCODING, 'strict') 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) except (OSError, IOError) as err: LOG.error('Could not write Kodi playlist file: %s', playlist) @@ -267,15 +260,15 @@ def _kodi_playlist_identical(xml_element): 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 """ create = False - playlist = playlist_object_from_db(plex_id=plex_id) with state.LOCK_PLAYLISTS: + playlist = playlist_object_from_db(plex_id=plex_id) try: - if playlist and state == 9: + if playlist and status == 9: LOG.debug('Plex deletion of playlist detected: %s', playlist) delete_kodi_playlist(playlist) 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) delete_kodi_playlist(playlist) 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', plex_id) create = True @@ -333,7 +326,7 @@ def _full_sync(): elif playlist.plex_updatedat != api.updated_at(): LOG.debug('Detected changed Plex playlist %s: %s', api.plex_id(), api.title()) - if exists(playlist.kodi_path): + if path_ops.exists(playlist.kodi_path): delete_kodi_playlist(playlist) else: update_plex_table(playlist, delete=True) @@ -361,17 +354,19 @@ def _full_sync(): if state.ENABLE_MUSIC: master_paths.append(v.PLAYLIST_PATH_MUSIC) for master_path in master_paths: - for root, _, files in os.walk(utils.encode_path(master_path)): - root = utils.decode_path(root) + for root, _, files in path_ops.walk(master_path): for file in files: - file = utils.decode_path(file) try: extension = file.rsplit('.', 1)[1] except IndexError: continue if extension not in SUPPORTED_FILETYPES: 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) playlist = playlist_object_from_db(kodi_hash=kodi_hash) playlist_2 = playlist_object_from_db(path=path) diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 8654a143..5cead379 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -3,7 +3,6 @@ Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly """ from logging import getLogger from threading import Thread -from re import compile as re_compile import xbmc from . import utils @@ -18,7 +17,6 @@ from . import state LOG = getLogger('PLEX.playqueue') PLUGIN = 'plugin://%s' % v.ADDON_ID -REGEX = re_compile(r'''plex_id=(\d+)''') # Our PKC playqueues (3 instances of Playqueue_Object()) PLAYQUEUES = [] @@ -133,7 +131,7 @@ class PlayqueueMonitor(Thread): old_item.kodi_type == new_item['type']) else: try: - plex_id = REGEX.findall(new_item['file'])[0] + plex_id = utils.REGEX_PLEX_ID.findall(new_item['file'])[0] except IndexError: LOG.debug('Comparing paths directly as a fallback') identical = old_item.file == new_item['file'] diff --git a/resources/lib/plex_api.py b/resources/lib/plex_api.py index f913887c..f8c09c3a 100644 --- a/resources/lib/plex_api.py +++ b/resources/lib/plex_api.py @@ -30,15 +30,15 @@ http://stackoverflow.com/questions/111945/is-there-any-way-to-do-http-put-in-pyt (and others...) """ from logging import getLogger -from re import compile as re_compile, sub +from re import sub from urllib import urlencode, unquote -import os from xbmcgui import ListItem from xbmcvfs import exists from .downloadutils import DownloadUtils as DU from . import clientinfo from . import utils +from . import path_ops from . import plex_functions as PF from . import plexdb_functions as plexdb from . import kodidb_functions as kodidb @@ -48,12 +48,6 @@ from . import state ############################################################################### 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 if providername == 'imdb': - regex = REGEX_IMDB + regex = utils.REGEX_IMDB elif providername == 'tvdb': # originally e.g. com.plexapp.agents.thetvdb://276564?lang=en - regex = REGEX_TVDB + regex = utils.REGEX_TVDB else: return None @@ -1246,7 +1240,7 @@ class API(object): # Get additional info (filename / languages) filename = None 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 = [] for stream in entry[0]: @@ -1401,7 +1395,7 @@ class API(object): 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) try: response.status_code @@ -1410,7 +1404,7 @@ class API(object): return else: 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) return path @@ -1603,17 +1597,18 @@ class API(object): check = exists(utils.try_encode(path)) else: # directories - if "\\" in path: - if not path.endswith('\\'): + checkpath = utils.try_encode(path) + if b"\\" in checkpath: + if not checkpath.endswith('\\'): # Add the missing backslash - check = utils.exists_dir(path + "\\") + check = utils.exists_dir(checkpath + "\\") else: - check = utils.exists_dir(path) + check = utils.exists_dir(checkpath) else: - if not path.endswith('/'): - check = utils.exists_dir(path + "/") + if not checkpath.endswith('/'): + check = utils.exists_dir(checkpath + "/") else: - check = utils.exists_dir(path) + check = utils.exists_dir(checkpath) if not check: if force_check is False: diff --git a/resources/lib/plex_functions.py b/resources/lib/plex_functions.py index 89da000b..42b756bc 100644 --- a/resources/lib/plex_functions.py +++ b/resources/lib/plex_functions.py @@ -3,7 +3,6 @@ from logging import getLogger from urllib import urlencode, quote_plus from ast import literal_eval from urlparse import urlparse, parse_qsl -from re import compile as re_compile from copy import deepcopy from time import time from threading import Thread @@ -18,8 +17,6 @@ from . import variables as v LOG = getLogger('PLEX.plex_functions') 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 PLEX_GDM_IP = '239.0.0.250' # multicast to PMS @@ -47,7 +44,7 @@ def GetPlexKeyNumber(plexKey): Returns ('','') if nothing is found """ try: - result = REGEX_PLEX_KEY.findall(plexKey)[0] + result = utils.REGEX_END_DIGITS.findall(plexKey)[0] except IndexError: result = ('', '') return result @@ -411,7 +408,7 @@ def _pms_list_from_plex_tv(token): def _poke_pms(pms, queue): data = pms['connections'][0].attrib 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 # connection that will directly access the local IP (e.g. internet down) conn = deepcopy(pms['connections'][0]) diff --git a/resources/lib/service_entry.py b/resources/lib/service_entry.py index 8d38df2d..f6ccc3dc 100644 --- a/resources/lib/service_entry.py +++ b/resources/lib/service_entry.py @@ -55,6 +55,7 @@ class Service(): utils.settings('useDirectPaths') == '1') LOG.info("Number of sync threads: %s", utils.settings('syncThreadNumber')) + LOG.info('Playlist m3u encoding: %s', v.M3U_ENCODING) LOG.info("Full sys.argv received: %s", sys.argv) self.monitor = xbmc.Monitor() # Load/Reset PKC entirely - important for user/Kodi profile switch diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index d52450d9..5b74884c 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -3,14 +3,14 @@ from logging import getLogger from threading import Thread -from xbmc import sleep, executebuiltin, translatePath -import xbmcaddon -from xbmcvfs import exists +from xbmc import sleep, executebuiltin from .downloadutils import DownloadUtils as DU from . import utils +from . import path_ops from . import plex_tv from . import plex_functions as PF +from . import variables as v from . import state ############################################################################### @@ -44,7 +44,6 @@ class UserClient(Thread): self.ssl = None self.sslcert = None - self.addon = xbmcaddon.Addon() self.do_utils = None Thread.__init__(self) @@ -197,11 +196,8 @@ class UserClient(Thread): 'Addon.Openutils.settings(plugin.video.plexkodiconnect)') return False - # Get /profile/addon_data - addondir = translatePath(self.addon.getAddonInfo('profile')) - # 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.") self.auth = False return False diff --git a/resources/lib/utils.py b/resources/lib/utils.py index d2b08486..18586754 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -4,11 +4,6 @@ Various functions and decorators for PKC """ ############################################################################### from logging import getLogger -import xbmc -import xbmcaddon -import xbmcgui -from xbmcvfs import exists, delete -import os from cProfile import Profile from pstats import Stats from sqlite3 import connect, OperationalError @@ -18,13 +13,14 @@ from time import localtime, strftime from unicodedata import normalize import xml.etree.ElementTree as etree from functools import wraps, partial -from shutil import rmtree from urllib import quote_plus import hashlib 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 state @@ -36,9 +32,19 @@ WINDOW = xbmcgui.Window(10000) ADDON = xbmcaddon.Addon(id='plugin.video.plexkodiconnect') EPOCH = datetime.utcfromtimestamp(0) +# Grab Plex id from '...plex_id=XXXX....' 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+$''') - +# 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 @@ -106,30 +112,6 @@ def settings(setting, value=None): 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): """ Central string retrieval from strings.po @@ -248,26 +230,6 @@ def kodi_time_to_millis(time): 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'): """ Will try to encode input_str (in unicode) to encoding. This possibly @@ -333,10 +295,6 @@ def valid_filename(text): else: # Linux 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 # filename extension and 1 dot to separate the extension) text = text[:min(len(text), 251)] @@ -485,15 +443,15 @@ def wipe_database(): # Delete all synced playlists for path in playlist_paths: try: - os.remove(path) + path_ops.remove(path) except (OSError, IOError): pass LOG.info("Resetting all cached artwork.") # Remove all existing textures first - path = xbmc.translatePath("special://thumbnails/") - if exists(path): - rmtree(try_decode(path), ignore_errors=True) + path = path_ops.translate_path("special://thumbnails/") + if path_ops.exists(path): + path_ops.rmtree(path, ignore_errors=True) # remove all existing data from texture DB connection = kodi_sql('texture') cursor = connection.cursor() @@ -547,10 +505,8 @@ def reset(ask_user=True): heading='{plex} %s ' % lang(30132), line1=lang(39603)): # Delete the settings - addon = xbmcaddon.Addon() - addondir = try_decode(xbmc.translatePath(addon.getAddonInfo('profile'))) LOG.info("Deleting: settings.xml") - os.remove("%ssettings.xml" % addondir) + path_ops.remove("%ssettings.xml" % v.ADDON_PROFILE) reboot_kodi() @@ -612,29 +568,6 @@ def compare_version(current, minimum): 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): """ For theme media, do not modify unless modified in TV Tunes @@ -656,6 +589,28 @@ def normalize_string(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): """ Prettifies xml trees. Pass the etree root in @@ -702,9 +657,9 @@ class XmlKodiSetting(object): top_element=None): self.filename = filename if path is None: - self.path = os.path.join(v.KODI_PROFILE, filename) + self.path = path_ops.path.join(v.KODI_PROFILE, filename) else: - self.path = os.path.join(path, filename) + self.path = path_ops.path.join(path, filename) self.force_create = force_create self.top_element = top_element self.tree = None @@ -869,7 +824,7 @@ def passwords_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 try: xmlparse = etree.parse(xmlpath) @@ -990,7 +945,7 @@ def playlist_xsp(mediatype, tagname, viewid, viewtype="", delete=False): """ 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": plname = "%s - %s" % (tagname, 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) # Create the playlist directory - if not exists(try_encode(path)): + if not path_ops.exists(path): LOG.info("Creating directory: %s", path) - os.makedirs(path) + path_ops.makedirs(path) # 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) if delete: - os.remove(xsppath) + path_ops.remove(xsppath) LOG.info("Successfully removed playlist: %s.", tagname) return @@ -1019,7 +974,7 @@ def playlist_xsp(mediatype, tagname, viewid, viewtype="", delete=False): 'show': 'tvshows' } 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( '\n' '\n\t' @@ -1037,22 +992,22 @@ def delete_playlists(): """ Clean up the playlists """ - path = try_decode(xbmc.translatePath("special://profile/playlists/video/")) - for root, _, files in os.walk(path): + path = path_ops.translate_path('special://profile/playlists/video/') + for root, _, files in path_ops.walk(path): for file in files: if file.startswith('Plex'): - os.remove(os.path.join(root, file)) + path_ops.remove(path_ops.path.join(root, file)) def delete_nodes(): """ Clean up video nodes """ - path = try_decode(xbmc.translatePath("special://profile/library/video/")) - for root, dirs, _ in os.walk(path): + path = path_ops.translate_path("special://profile/library/video/") + for root, dirs, _ in path_ops.walk(path): for directory in dirs: if directory.startswith('Plex-'): - rmtree(os.path.join(root, directory)) + path_ops.rmtree(path_ops.path.join(root, directory)) break @@ -1065,7 +1020,7 @@ def generate_file_md5(path): """ m = hashlib.md5() 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: piece = f.read(32768) if not piece: diff --git a/resources/lib/variables.py b/resources/lib/variables.py index b1ef2d98..faf5e75e 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import os +import sys import xbmc from xbmcaddon import Addon @@ -34,7 +35,9 @@ _ADDON = Addon() ADDON_NAME = 'PlexKodiConnect' ADDON_ID = 'plugin.video.plexkodiconnect' ADDON_VERSION = _ADDON.getAddonInfo('version') +ADDON_PATH = try_decode(_ADDON.getAddonInfo('path')) ADDON_FOLDER = try_decode(xbmc.translatePath('special://home')) +ADDON_PROFILE = try_decode(_ADDON.getAddonInfo('profile')) KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1) KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2]) @@ -81,10 +84,6 @@ MIN_DB_VERSION = '2.0.27' # Database paths _DB_VIDEO_VERSION = { - 13: 78, # Gotham - 14: 90, # Helix - 15: 93, # Isengard - 16: 99, # Jarvis 17: 107, # Krypton 18: 109 # Leia } @@ -92,10 +91,6 @@ DB_VIDEO_PATH = try_decode(xbmc.translatePath( "special://database/MyVideos%s.db" % _DB_VIDEO_VERSION[KODIVERSION])) _DB_MUSIC_VERSION = { - 13: 46, # Gotham - 14: 48, # Helix - 15: 52, # Isengard - 16: 56, # Jarvis 17: 60, # Krypton 18: 70 # Leia } @@ -103,10 +98,6 @@ DB_MUSIC_PATH = try_decode(xbmc.translatePath( "special://database/MyMusic%s.db" % _DB_MUSIC_VERSION[KODIVERSION])) _DB_TEXTURE_VERSION = { - 13: 13, # Gotham - 14: 13, # Helix - 15: 13, # Isengard - 16: 13, # Jarvis 17: 13, # Krypton 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 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_PATH = os.path.join(KODI_PROFILE, 'playlists') PLAYLIST_PATH_MIXED = os.path.join(PLAYLIST_PATH, 'mixed') @@ -512,3 +509,14 @@ PLEX_STREAM_TYPE_FROM_STREAM_TYPE = { 'audio': '2', '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' diff --git a/resources/lib/videonodes.py b/resources/lib/videonodes.py index a960880d..b32cefad 100644 --- a/resources/lib/videonodes.py +++ b/resources/lib/videonodes.py @@ -1,13 +1,11 @@ # -*- coding: utf-8 -*- ############################################################################### from logging import getLogger -from distutils import dir_util import xml.etree.ElementTree as etree -from os import makedirs import xbmc -from xbmcvfs import exists from . import utils +from . import path_ops from . import variables as v from . import state @@ -16,7 +14,6 @@ from . import state LOG = getLogger('PLEX.videonodes') ############################################################################### -# Paths are strings, NOT unicode! class VideoNodes(object): @@ -66,33 +63,30 @@ class VideoNodes(object): dirname = viewid # Returns strings - path = utils.try_decode(xbmc.translatePath( - "special://profile/library/video/")) - nodepath = utils.try_decode(xbmc.translatePath( - "special://profile/library/video/Plex-%s/" % dirname)) + path = path_ops.translate_path('special://profile/library/video/') + nodepath = path_ops.translate_path( + 'special://profile/library/video/Plex-%s/' % dirname) if delete: - if utils.exists_dir(nodepath): - from shutil import rmtree - rmtree(nodepath) + if path_ops.exists(nodepath): + path_ops.rmtree(nodepath) LOG.info("Sucessfully removed videonode: %s." % tagname) return # Verify the video directory - if not utils.exists_dir(path): - dir_util.copy_tree( - src=utils.try_decode( - xbmc.translatePath("special://xbmc/system/library/video")), - dst=utils.try_decode( - xbmc.translatePath("special://profile/library/video")), + if not path_ops.exists(path): + path_ops.copy_tree( + src=path_ops.translate_path( + 'special://xbmc/system/library/video'), + dst=path_ops.translate_path('special://profile/library/video'), preserve_mode=0) # do not copy permission bits! # Create the node directory if mediatype != "photos": - if not utils.exists_dir(nodepath): + if not path_ops.exists(nodepath): # folder does not exist yet LOG.debug('Creating folder %s' % nodepath) - makedirs(nodepath) + path_ops.makedirs(nodepath) # Create index entry nodeXML = "%sindex.xml" % nodepath @@ -298,7 +292,7 @@ class VideoNodes(object): # kodi picture sources somehow continue - if exists(utils.try_encode(nodeXML)): + if path_ops.exists(nodeXML): # Don't recreate xml if already exists continue @@ -403,13 +397,12 @@ class VideoNodes(object): utils.indent(root) except: 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): - tagname = utils.try_encode(tagname) - cleantagname = utils.try_decode(utils.normalize_nodes(tagname)) - nodepath = utils.try_decode(xbmc.translatePath( - "special://profile/library/video/")) + cleantagname = utils.normalize_nodes(tagname) + nodepath = path_ops.translate_path('special://profile/library/video/') nodeXML = "%splex_%s.xml" % (nodepath, cleantagname) path = "library://video/plex_%s.xml" % cleantagname if v.KODIVERSION >= 17: @@ -419,13 +412,12 @@ class VideoNodes(object): windowpath = "ActivateWindow(Video,%s,return)" % path # 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 - dir_util.copy_tree( - src=utils.try_decode( - xbmc.translatePath("special://xbmc/system/library/video")), - dst=utils.try_decode( - xbmc.translatePath("special://profile/library/video")), + path_ops.copy_tree( + src=path_ops.translate_path( + 'special://xbmc/system/library/video'), + dst=path_ops.translate_path('special://profile/library/video'), preserve_mode=0) # do not copy permission bits! labels = { @@ -440,7 +432,7 @@ class VideoNodes(object): utils.window('%s.content' % embynode, value=path) 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 return