Fix encoding of file and path operations
This commit is contained in:
parent
074c439e99
commit
1234f61fc0
19 changed files with 405 additions and 290 deletions
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'''^\^(.+)\$$''')
|
||||
###############################################################################
|
||||
|
||||
|
||||
|
|
192
resources/lib/path_ops.py
Normal file
192
resources/lib/path_ops.py
Normal 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)
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n'
|
||||
'<smartplaylist type="%s">\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:
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue