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 logging import getLogger
|
||||||
from Queue import Queue, Empty
|
from Queue import Queue, Empty
|
||||||
from shutil import rmtree
|
|
||||||
from urllib import quote_plus, unquote
|
from urllib import quote_plus, unquote
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from os import makedirs
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
import xbmc
|
import xbmc
|
||||||
from xbmcvfs import exists
|
|
||||||
|
|
||||||
|
from . import path_ops
|
||||||
from . import utils
|
from . import utils
|
||||||
from . import state
|
from . import state
|
||||||
|
|
||||||
|
@ -202,10 +200,9 @@ class Artwork():
|
||||||
if utils.dialog('yesno', "Image Texture Cache", utils.lang(39251)):
|
if utils.dialog('yesno', "Image Texture Cache", utils.lang(39251)):
|
||||||
LOG.info("Resetting all cache data first")
|
LOG.info("Resetting all cache data first")
|
||||||
# Remove all existing textures first
|
# Remove all existing textures first
|
||||||
path = utils.try_decode(
|
path = path_ops.translate_path('special://thumbnails/')
|
||||||
xbmc.translatePath("special://thumbnails/"))
|
if path_ops.exists(path):
|
||||||
if utils.exists_dir(path):
|
path_ops.rmtree(path, ignore_errors=True)
|
||||||
rmtree(path, ignore_errors=True)
|
|
||||||
self.restore_cache_directories()
|
self.restore_cache_directories()
|
||||||
|
|
||||||
# remove all existing data from texture DB
|
# remove all existing data from texture DB
|
||||||
|
@ -321,10 +318,11 @@ class Artwork():
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
# Delete thumbnail as well as the entry
|
# Delete thumbnail as well as the entry
|
||||||
path = xbmc.translatePath("special://thumbnails/%s" % cachedurl)
|
path = path_ops.translate_path("special://thumbnails/%s"
|
||||||
|
% cachedurl)
|
||||||
LOG.debug("Deleting cached thumbnail: %s", path)
|
LOG.debug("Deleting cached thumbnail: %s", path)
|
||||||
if exists(path):
|
if path_ops.exists(path):
|
||||||
rmtree(utils.try_decode(path), ignore_errors=True)
|
path_ops.rmtree(path, ignore_errors=True)
|
||||||
cursor.execute("DELETE FROM texture WHERE url = ?", (url,))
|
cursor.execute("DELETE FROM texture WHERE url = ?", (url,))
|
||||||
connection.commit()
|
connection.commit()
|
||||||
finally:
|
finally:
|
||||||
|
@ -337,8 +335,8 @@ class Artwork():
|
||||||
"a", "b", "c", "d", "e", "f",
|
"a", "b", "c", "d", "e", "f",
|
||||||
"Video", "plex")
|
"Video", "plex")
|
||||||
for path in paths:
|
for path in paths:
|
||||||
makedirs(utils.try_decode(
|
new_path = path_ops.translate_path("special://thumbnails/%s" % path)
|
||||||
xbmc.translatePath("special://thumbnails/%s" % path)))
|
path_ops.makedirs(utils.encode_path(new_path))
|
||||||
|
|
||||||
|
|
||||||
class ArtworkSyncMessage(object):
|
class ArtworkSyncMessage(object):
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
###############################################################################
|
###############################################################################
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from os.path import join
|
|
||||||
import xbmcgui
|
import xbmcgui
|
||||||
from xbmcaddon import Addon
|
|
||||||
|
|
||||||
from . import utils
|
from . import utils
|
||||||
|
from . import path_ops
|
||||||
|
from . import variables as v
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
LOG = getLogger('PLEX.context')
|
LOG = getLogger('PLEX.context')
|
||||||
ADDON = Addon('plugin.video.plexkodiconnect')
|
|
||||||
|
|
||||||
ACTION_PARENT_DIR = 9
|
ACTION_PARENT_DIR = 9
|
||||||
ACTION_PREVIOUS_MENU = 10
|
ACTION_PREVIOUS_MENU = 10
|
||||||
|
@ -65,10 +64,11 @@ class ContextMenu(xbmcgui.WindowXMLDialog):
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def _add_editcontrol(self, x, y, height, width, password=None):
|
def _add_editcontrol(self, x, y, height, width, password=None):
|
||||||
media = join(ADDON.getAddonInfo('path'),
|
media = path_ops.path.join(
|
||||||
'resources', 'skins', 'default', 'media')
|
v.ADDON_PATH, 'resources', 'skins', 'default', 'media')
|
||||||
|
filename = utils.try_encode(path_ops.path.join(media, 'white.png'))
|
||||||
control = xbmcgui.ControlImage(0, 0, 0, 0,
|
control = xbmcgui.ControlImage(0, 0, 0, 0,
|
||||||
filename=join(media, "white.png"),
|
filename=filename,
|
||||||
aspectRatio=0,
|
aspectRatio=0,
|
||||||
colorDiffuse="ff111111")
|
colorDiffuse="ff111111")
|
||||||
control.setPosition(x, y)
|
control.setPosition(x, y)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
###############################################################################
|
###############################################################################
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from xbmcaddon import Addon
|
|
||||||
import xbmc
|
import xbmc
|
||||||
import xbmcgui
|
import xbmcgui
|
||||||
|
|
||||||
|
@ -103,7 +102,7 @@ class ContextMenu(object):
|
||||||
options.append(OPTIONS['Addon'])
|
options.append(OPTIONS['Addon'])
|
||||||
context_menu = context.ContextMenu(
|
context_menu = context.ContextMenu(
|
||||||
"script-plex-context.xml",
|
"script-plex-context.xml",
|
||||||
Addon('plugin.video.plexkodiconnect').getAddonInfo('path'),
|
utils.try_encode(v.ADDON_PATH),
|
||||||
"default",
|
"default",
|
||||||
"1080i")
|
"1080i")
|
||||||
context_menu.set_options(options)
|
context_menu.set_options(options)
|
||||||
|
|
|
@ -5,16 +5,14 @@
|
||||||
#
|
#
|
||||||
###############################################################################
|
###############################################################################
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from shutil import copyfile
|
|
||||||
from os import walk, makedirs
|
|
||||||
from os.path import basename, join
|
|
||||||
from sys import argv
|
from sys import argv
|
||||||
from urllib import urlencode
|
from urllib import urlencode
|
||||||
import xbmcplugin
|
import xbmcplugin
|
||||||
from xbmc import sleep, executebuiltin, translatePath
|
from xbmc import sleep, executebuiltin
|
||||||
from xbmcgui import ListItem
|
from xbmcgui import ListItem
|
||||||
|
|
||||||
from . import utils
|
from . import utils
|
||||||
|
from . import path_ops
|
||||||
from .downloadutils import DownloadUtils as DU
|
from .downloadutils import DownloadUtils as DU
|
||||||
from .plex_api import API
|
from .plex_api import API
|
||||||
from . import plex_functions as PF
|
from . import plex_functions as PF
|
||||||
|
@ -441,7 +439,7 @@ def get_video_files(plex_id, params):
|
||||||
|
|
||||||
item = PF.GetPlexMetadata(plex_id)
|
item = PF.GetPlexMetadata(plex_id)
|
||||||
try:
|
try:
|
||||||
path = item[0][0][0].attrib['file']
|
path = utils.try_decode(item[0][0][0].attrib['file'])
|
||||||
except (TypeError, IndexError, AttributeError, KeyError):
|
except (TypeError, IndexError, AttributeError, KeyError):
|
||||||
LOG.error('Could not get file path for item %s', plex_id)
|
LOG.error('Could not get file path for item %s', plex_id)
|
||||||
return xbmcplugin.endOfDirectory(HANDLE)
|
return xbmcplugin.endOfDirectory(HANDLE)
|
||||||
|
@ -453,18 +451,19 @@ def get_video_files(plex_id, params):
|
||||||
elif '\\' in path:
|
elif '\\' in path:
|
||||||
path = path.replace('\\', '\\\\')
|
path = path.replace('\\', '\\\\')
|
||||||
# Directory only, get rid of filename
|
# Directory only, get rid of filename
|
||||||
path = path.replace(basename(path), '')
|
path = path.replace(path_ops.path.basename(path), '')
|
||||||
if utils.exists_dir(path):
|
if path_ops.exists(path):
|
||||||
for root, dirs, files in walk(path):
|
for root, dirs, files in path_ops.walk(path):
|
||||||
for directory in dirs:
|
for directory in dirs:
|
||||||
item_path = utils.try_encode(join(root, directory))
|
item_path = utils.try_encode(path_ops.path.join(root,
|
||||||
|
directory))
|
||||||
listitem = ListItem(item_path, path=item_path)
|
listitem = ListItem(item_path, path=item_path)
|
||||||
xbmcplugin.addDirectoryItem(handle=HANDLE,
|
xbmcplugin.addDirectoryItem(handle=HANDLE,
|
||||||
url=item_path,
|
url=item_path,
|
||||||
listitem=listitem,
|
listitem=listitem,
|
||||||
isFolder=True)
|
isFolder=True)
|
||||||
for file in files:
|
for file in files:
|
||||||
item_path = utils.try_encode(join(root, file))
|
item_path = utils.try_encode(path_ops.path.join(root, file))
|
||||||
listitem = ListItem(item_path, path=item_path)
|
listitem = ListItem(item_path, path=item_path)
|
||||||
xbmcplugin.addDirectoryItem(handle=HANDLE,
|
xbmcplugin.addDirectoryItem(handle=HANDLE,
|
||||||
url=file,
|
url=file,
|
||||||
|
@ -492,11 +491,11 @@ def extra_fanart(plex_id, plex_path):
|
||||||
|
|
||||||
# We need to store the images locally for this to work
|
# We need to store the images locally for this to work
|
||||||
# because of the caching system in xbmc
|
# because of the caching system in xbmc
|
||||||
fanart_dir = utils.try_decode(translatePath(
|
fanart_dir = path_ops.translate_path("special://thumbnails/plex/%s/"
|
||||||
"special://thumbnails/plex/%s/" % plex_id))
|
% plex_id)
|
||||||
if not utils.exists_dir(fanart_dir):
|
if not path_ops.exists(fanart_dir):
|
||||||
# Download the images to the cache directory
|
# Download the images to the cache directory
|
||||||
makedirs(fanart_dir)
|
path_ops.makedirs(fanart_dir)
|
||||||
xml = PF.GetPlexMetadata(plex_id)
|
xml = PF.GetPlexMetadata(plex_id)
|
||||||
if xml is None:
|
if xml is None:
|
||||||
LOG.error('Could not download metadata for %s', plex_id)
|
LOG.error('Could not download metadata for %s', plex_id)
|
||||||
|
@ -506,20 +505,23 @@ def extra_fanart(plex_id, plex_path):
|
||||||
backdrops = api.artwork()['Backdrop']
|
backdrops = api.artwork()['Backdrop']
|
||||||
for count, backdrop in enumerate(backdrops):
|
for count, backdrop in enumerate(backdrops):
|
||||||
# Same ordering as in artwork
|
# Same ordering as in artwork
|
||||||
art_file = utils.try_encode(join(fanart_dir,
|
art_file = utils.try_encode(path_ops.path.join(
|
||||||
"fanart%.3d.jpg" % count))
|
fanart_dir, "fanart%.3d.jpg" % count))
|
||||||
listitem = ListItem("%.3d" % count, path=art_file)
|
listitem = ListItem("%.3d" % count, path=art_file)
|
||||||
xbmcplugin.addDirectoryItem(
|
xbmcplugin.addDirectoryItem(
|
||||||
handle=HANDLE,
|
handle=HANDLE,
|
||||||
url=art_file,
|
url=art_file,
|
||||||
listitem=listitem)
|
listitem=listitem)
|
||||||
copyfile(backdrop, utils.try_decode(art_file))
|
path_ops.copyfile(backdrop, utils.try_decode(art_file))
|
||||||
else:
|
else:
|
||||||
LOG.info("Found cached backdrop.")
|
LOG.info("Found cached backdrop.")
|
||||||
# Use existing cached images
|
# Use existing cached images
|
||||||
for root, _, files in walk(fanart_dir):
|
fanart_dir = utils.try_decode(fanart_dir)
|
||||||
|
for root, _, files in path_ops.walk(fanart_dir):
|
||||||
|
root = utils.decode_path(root)
|
||||||
for file in files:
|
for file in files:
|
||||||
art_file = utils.try_encode(join(root, file))
|
file = utils.decode_path(file)
|
||||||
|
art_file = utils.try_encode(path_ops.path.join(root, file))
|
||||||
listitem = ListItem(file, path=art_file)
|
listitem = ListItem(file, path=art_file)
|
||||||
xbmcplugin.addDirectoryItem(handle=HANDLE,
|
xbmcplugin.addDirectoryItem(handle=HANDLE,
|
||||||
url=art_file,
|
url=art_file,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import xml.etree.ElementTree as etree
|
||||||
from xbmc import executebuiltin, translatePath
|
from xbmc import executebuiltin, translatePath
|
||||||
|
|
||||||
from . import utils
|
from . import utils
|
||||||
|
from . import path_ops
|
||||||
from . import migration
|
from . import migration
|
||||||
from .downloadutils import DownloadUtils as DU
|
from .downloadutils import DownloadUtils as DU
|
||||||
from . import videonodes
|
from . import videonodes
|
||||||
|
@ -25,6 +26,9 @@ LOG = getLogger('PLEX.initialsetup')
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
if not path_ops.exists(v.EXTERNAL_SUBTITLE_TEMP_PATH):
|
||||||
|
path_ops.makedirs(v.EXTERNAL_SUBTITLE_TEMP_PATH)
|
||||||
|
|
||||||
|
|
||||||
WINDOW_PROPERTIES = (
|
WINDOW_PROPERTIES = (
|
||||||
"plex_online", "plex_serverStatus", "plex_shouldStop", "plex_dbScan",
|
"plex_online", "plex_serverStatus", "plex_shouldStop", "plex_dbScan",
|
||||||
|
|
|
@ -1025,23 +1025,11 @@ class LibrarySync(Thread):
|
||||||
do with "process_" methods
|
do with "process_" methods
|
||||||
"""
|
"""
|
||||||
if message['type'] == 'playing':
|
if message['type'] == 'playing':
|
||||||
try:
|
|
||||||
self.process_playing(message['PlaySessionStateNotification'])
|
self.process_playing(message['PlaySessionStateNotification'])
|
||||||
except KeyError:
|
|
||||||
LOG.error('Received invalid PMS message for playstate: %s',
|
|
||||||
message)
|
|
||||||
elif message['type'] == 'timeline':
|
elif message['type'] == 'timeline':
|
||||||
try:
|
|
||||||
self.process_timeline(message['TimelineEntry'])
|
self.process_timeline(message['TimelineEntry'])
|
||||||
except (KeyError, ValueError):
|
|
||||||
LOG.error('Received invalid PMS message for timeline: %s',
|
|
||||||
message)
|
|
||||||
elif message['type'] == 'activity':
|
elif message['type'] == 'activity':
|
||||||
try:
|
|
||||||
self.process_activity(message['ActivityNotification'])
|
self.process_activity(message['ActivityNotification'])
|
||||||
except KeyError:
|
|
||||||
LOG.error('Received invalid PMS message for activity: %s',
|
|
||||||
message)
|
|
||||||
|
|
||||||
def multi_delete(self, liste, delete_list):
|
def multi_delete(self, liste, delete_list):
|
||||||
"""
|
"""
|
||||||
|
@ -1196,7 +1184,7 @@ class LibrarySync(Thread):
|
||||||
continue
|
continue
|
||||||
playlists.process_websocket(plex_id=str(item['itemID']),
|
playlists.process_websocket(plex_id=str(item['itemID']),
|
||||||
updated_at=str(item['updatedAt']),
|
updated_at=str(item['updatedAt']),
|
||||||
state=status)
|
status=status)
|
||||||
elif status == 9:
|
elif status == 9:
|
||||||
# Immediately and always process deletions (as the PMS will
|
# Immediately and always process deletions (as the PMS will
|
||||||
# send additional message with other codes)
|
# send additional message with other codes)
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from re import compile as re_compile
|
|
||||||
from xml.etree.ElementTree import ParseError
|
from xml.etree.ElementTree import ParseError
|
||||||
|
|
||||||
from . import utils
|
from . import utils
|
||||||
|
@ -9,8 +8,6 @@ from . import variables as v
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
LOG = getLogger('PLEX.music')
|
LOG = getLogger('PLEX.music')
|
||||||
|
|
||||||
REGEX_MUSICPATH = re_compile(r'''^\^(.+)\$$''')
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
|
192
resources/lib/path_ops.py
Normal file
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 logging import getLogger
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from os.path import join
|
|
||||||
from xbmc import Player, sleep
|
from xbmc import Player, sleep
|
||||||
|
|
||||||
from .plex_api import API
|
from .plex_api import API
|
||||||
|
@ -25,8 +24,6 @@ from . import state
|
||||||
LOG = getLogger('PLEX.playback')
|
LOG = getLogger('PLEX.playback')
|
||||||
# Do we need to return ultimately with a setResolvedUrl?
|
# Do we need to return ultimately with a setResolvedUrl?
|
||||||
RESOLVE = True
|
RESOLVE = True
|
||||||
# We're "failing" playback with a video of 0 length
|
|
||||||
NULL_VIDEO = join(v.ADDON_FOLDER, 'addons', v.ADDON_ID, 'empty_video.mp4')
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
@ -273,7 +270,7 @@ def _ensure_resolve(abort=False):
|
||||||
state.PKC_CAUSED_STOP_DONE = False
|
state.PKC_CAUSED_STOP_DONE = False
|
||||||
if not abort:
|
if not abort:
|
||||||
result = pickler.Playback_Successful()
|
result = pickler.Playback_Successful()
|
||||||
result.listitem = PKCListItem(path=NULL_VIDEO)
|
result.listitem = PKCListItem(path=v.NULL_VIDEO)
|
||||||
pickler.pickle_me(result)
|
pickler.pickle_me(result)
|
||||||
else:
|
else:
|
||||||
# Shows PKC error message
|
# Shows PKC error message
|
||||||
|
|
|
@ -3,10 +3,8 @@
|
||||||
Collection of functions associated with Kodi and Plex playlists and playqueues
|
Collection of functions associated with Kodi and Plex playlists and playqueues
|
||||||
"""
|
"""
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
import os
|
|
||||||
import urllib
|
import urllib
|
||||||
from urlparse import parse_qsl, urlsplit
|
from urlparse import parse_qsl, urlsplit
|
||||||
from re import compile as re_compile
|
|
||||||
|
|
||||||
from .plex_api import API
|
from .plex_api import API
|
||||||
from . import plex_functions as PF
|
from . import plex_functions as PF
|
||||||
|
@ -14,6 +12,7 @@ from . import plexdb_functions as plexdb
|
||||||
from . import kodidb_functions as kodidb
|
from . import kodidb_functions as kodidb
|
||||||
from .downloadutils import DownloadUtils as DU
|
from .downloadutils import DownloadUtils as DU
|
||||||
from . import utils
|
from . import utils
|
||||||
|
from . import path_ops
|
||||||
from . import json_rpc as js
|
from . import json_rpc as js
|
||||||
from . import variables as v
|
from . import variables as v
|
||||||
|
|
||||||
|
@ -21,7 +20,6 @@ from . import variables as v
|
||||||
|
|
||||||
LOG = getLogger('PLEX.playlist_func')
|
LOG = getLogger('PLEX.playlist_func')
|
||||||
|
|
||||||
REGEX = re_compile(r'''metadata%2F(\d+)''')
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,9 +40,9 @@ class PlaylistObjectBaseclase(object):
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
"""
|
"""
|
||||||
Print the playlist, e.g. to log. Returns utf-8 encoded string
|
Print the playlist, e.g. to log. Returns unicode
|
||||||
"""
|
"""
|
||||||
answ = u'{\'%s\': {\'id\': %s, ' % (self.__class__.__name__, self.id)
|
answ = '{\'%s\': {\'id\': %s, ' % (self.__class__.__name__, self.id)
|
||||||
# For some reason, can't use dir directly
|
# For some reason, can't use dir directly
|
||||||
for key in self.__dict__:
|
for key in self.__dict__:
|
||||||
if key in ('id', 'kodi_pl'):
|
if key in ('id', 'kodi_pl'):
|
||||||
|
@ -58,7 +56,7 @@ class PlaylistObjectBaseclase(object):
|
||||||
else:
|
else:
|
||||||
# e.g. int
|
# e.g. int
|
||||||
answ += '\'%s\': %s, ' % (key, unicode(getattr(self, key)))
|
answ += '\'%s\': %s, ' % (key, unicode(getattr(self, key)))
|
||||||
return utils.try_encode(answ + '}}')
|
return answ + '}}'
|
||||||
|
|
||||||
|
|
||||||
class Playlist_Object(PlaylistObjectBaseclase):
|
class Playlist_Object(PlaylistObjectBaseclase):
|
||||||
|
@ -82,7 +80,9 @@ class Playlist_Object(PlaylistObjectBaseclase):
|
||||||
|
|
||||||
@kodi_path.setter
|
@kodi_path.setter
|
||||||
def kodi_path(self, path):
|
def kodi_path(self, path):
|
||||||
file = os.path.basename(path)
|
if not isinstance(path, unicode):
|
||||||
|
raise RuntimeError('Path is %s, not unicode!' % type(path))
|
||||||
|
file = path_ops.path.basename(path)
|
||||||
try:
|
try:
|
||||||
self.kodi_filename, self.kodi_extension = file.split('.', 1)
|
self.kodi_filename, self.kodi_extension = file.split('.', 1)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -220,9 +220,9 @@ class Playlist_Item(object):
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
"""
|
"""
|
||||||
Print the playlist item, e.g. to log. Returns utf-8 encoded string
|
Print the playlist item, e.g. to log. Returns unicode
|
||||||
"""
|
"""
|
||||||
answ = (u'{\'%s\': {\'id\': \'%s\', \'plex_id\': \'%s\', '
|
answ = ('{\'%s\': {\'id\': \'%s\', \'plex_id\': \'%s\', '
|
||||||
% (self.__class__.__name__, self.id, self.plex_id))
|
% (self.__class__.__name__, self.id, self.plex_id))
|
||||||
for key in self.__dict__:
|
for key in self.__dict__:
|
||||||
if key in ('id', 'plex_id', 'xml'):
|
if key in ('id', 'plex_id', 'xml'):
|
||||||
|
@ -240,7 +240,7 @@ class Playlist_Item(object):
|
||||||
answ += '\'xml\': None}}'
|
answ += '\'xml\': None}}'
|
||||||
else:
|
else:
|
||||||
answ += '\'xml\': \'%s\'}}' % self.xml.tag
|
answ += '\'xml\': \'%s\'}}' % self.xml.tag
|
||||||
return utils.try_encode(answ)
|
return answ
|
||||||
|
|
||||||
def plex_stream_index(self, kodi_stream_index, stream_type):
|
def plex_stream_index(self, kodi_stream_index, stream_type):
|
||||||
"""
|
"""
|
||||||
|
@ -865,7 +865,8 @@ def get_plextype_from_xml(xml):
|
||||||
returns None if unsuccessful
|
returns None if unsuccessful
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
plex_id = REGEX.findall(xml.attrib['playQueueSourceURI'])[0]
|
plex_id = utils.REGEX_PLEX_ID_FROM_URL.findall(
|
||||||
|
xml.attrib['playQueueSourceURI'])[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
LOG.error('Could not get plex_id from xml: %s', xml.attrib)
|
LOG.error('Could not get plex_id from xml: %s', xml.attrib)
|
||||||
return
|
return
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from xbmcvfs import exists
|
|
||||||
|
|
||||||
from .watchdog.events import FileSystemEventHandler
|
from .watchdog.events import FileSystemEventHandler
|
||||||
from .watchdog.observers import Observer
|
from .watchdog.observers import Observer
|
||||||
|
@ -11,6 +8,7 @@ from .plex_api import API
|
||||||
from . import kodidb_functions as kodidb
|
from . import kodidb_functions as kodidb
|
||||||
from . import plexdb_functions as plexdb
|
from . import plexdb_functions as plexdb
|
||||||
from . import utils
|
from . import utils
|
||||||
|
from . import path_ops
|
||||||
from . import variables as v
|
from . import variables as v
|
||||||
from . import state
|
from . import state
|
||||||
|
|
||||||
|
@ -32,12 +30,6 @@ EVENT_TYPE_DELETED = 'deleted'
|
||||||
EVENT_TYPE_CREATED = 'created'
|
EVENT_TYPE_CREATED = 'created'
|
||||||
EVENT_TYPE_MODIFIED = 'modified'
|
EVENT_TYPE_MODIFIED = 'modified'
|
||||||
|
|
||||||
# m3u files do not have encoding specified
|
|
||||||
if v.PLATFORM == 'Windows':
|
|
||||||
ENCODING = 'mbcs'
|
|
||||||
else:
|
|
||||||
ENCODING = sys.getdefaultencoding()
|
|
||||||
|
|
||||||
|
|
||||||
def create_plex_playlist(playlist):
|
def create_plex_playlist(playlist):
|
||||||
"""
|
"""
|
||||||
|
@ -99,19 +91,20 @@ def create_kodi_playlist(plex_id=None, updated_at=None):
|
||||||
playlist.plex_updatedat = updated_at
|
playlist.plex_updatedat = updated_at
|
||||||
LOG.debug('Creating new Kodi playlist from Plex playlist: %s', playlist)
|
LOG.debug('Creating new Kodi playlist from Plex playlist: %s', playlist)
|
||||||
name = utils.valid_filename(playlist.plex_name)
|
name = utils.valid_filename(playlist.plex_name)
|
||||||
path = os.path.join(v.PLAYLIST_PATH, playlist.type, '%s.m3u' % name)
|
path = path_ops.path.join(v.PLAYLIST_PATH, playlist.type, '%s.m3u' % name)
|
||||||
while exists(path) or playlist_object_from_db(path=path):
|
while path_ops.exists(path) or playlist_object_from_db(path=path):
|
||||||
# In case the Plex playlist names are not unique
|
# In case the Plex playlist names are not unique
|
||||||
occurance = utils.REGEX_FILE_NUMBERING.search(path)
|
occurance = utils.REGEX_FILE_NUMBERING.search(path)
|
||||||
if not occurance:
|
if not occurance:
|
||||||
path = os.path.join(v.PLAYLIST_PATH,
|
path = path_ops.path.join(v.PLAYLIST_PATH,
|
||||||
playlist.type,
|
playlist.type,
|
||||||
'%s_01.m3u' % name[:min(len(name), 248)])
|
'%s_01.m3u' % name[:min(len(name), 248)])
|
||||||
else:
|
else:
|
||||||
occurance = int(occurance.group(1)) + 1
|
occurance = int(occurance.group(1)) + 1
|
||||||
path = os.path.join(v.PLAYLIST_PATH,
|
path = path_ops.path.join(v.PLAYLIST_PATH,
|
||||||
playlist.type,
|
playlist.type,
|
||||||
'%s_%02d.m3u' % (name[:min(len(name), 248)],
|
'%s_%02d.m3u' % (name[:min(len(name),
|
||||||
|
248)],
|
||||||
occurance))
|
occurance))
|
||||||
LOG.debug('Kodi playlist path: %s', path)
|
LOG.debug('Kodi playlist path: %s', path)
|
||||||
playlist.kodi_path = path
|
playlist.kodi_path = path
|
||||||
|
@ -130,7 +123,7 @@ def delete_kodi_playlist(playlist):
|
||||||
Returns None or raises PL.PlaylistError
|
Returns None or raises PL.PlaylistError
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
os.remove(playlist.kodi_path)
|
path_ops.remove(playlist.kodi_path)
|
||||||
except (OSError, IOError) as err:
|
except (OSError, IOError) as err:
|
||||||
LOG.error('Could not delete Kodi playlist file %s. Error:\n %s: %s',
|
LOG.error('Could not delete Kodi playlist file %s. Error:\n %s: %s',
|
||||||
playlist, err.errno, err.strerror)
|
playlist, err.errno, err.strerror)
|
||||||
|
@ -180,10 +173,10 @@ def m3u_to_plex_ids(playlist):
|
||||||
Adapter to process *.m3u playlist files. Encoding is not uniform!
|
Adapter to process *.m3u playlist files. Encoding is not uniform!
|
||||||
"""
|
"""
|
||||||
plex_ids = list()
|
plex_ids = list()
|
||||||
with open(utils.encode_path(playlist.kodi_path), 'rb') as f:
|
with open(path_ops.encode_path(playlist.kodi_path), 'rb') as f:
|
||||||
text = f.read()
|
text = f.read()
|
||||||
try:
|
try:
|
||||||
text = text.decode(ENCODING)
|
text = text.decode(v.M3U_ENCODING)
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
LOG.warning('Fallback to ISO-8859-1 decoding for %s', playlist)
|
LOG.warning('Fallback to ISO-8859-1 decoding for %s', playlist)
|
||||||
text = text.decode('ISO-8859-1')
|
text = text.decode('ISO-8859-1')
|
||||||
|
@ -210,15 +203,15 @@ def _write_playlist_to_file(playlist, xml):
|
||||||
Feed with playlist [Playlist_Object]. Will write the playlist to a m3u file
|
Feed with playlist [Playlist_Object]. Will write the playlist to a m3u file
|
||||||
Returns None or raises PL.PlaylistError
|
Returns None or raises PL.PlaylistError
|
||||||
"""
|
"""
|
||||||
text = u'#EXTCPlayListM3U::M3U\n'
|
text = '#EXTCPlayListM3U::M3U\n'
|
||||||
for element in xml:
|
for element in xml:
|
||||||
api = API(element)
|
api = API(element)
|
||||||
text += (u'#EXTINF:%s,%s\n%s\n'
|
text += ('#EXTINF:%s,%s\n%s\n'
|
||||||
% (api.runtime(), api.title(), api.path()))
|
% (api.runtime(), api.title(), api.path()))
|
||||||
text += '\n'
|
text += '\n'
|
||||||
text = text.encode(ENCODING, 'ignore')
|
text = text.encode(v.M3U_ENCODING, 'strict')
|
||||||
try:
|
try:
|
||||||
with open(utils.encode_path(playlist.kodi_path), 'wb') as f:
|
with open(path_ops.encode_path(playlist.kodi_path), 'wb') as f:
|
||||||
f.write(text)
|
f.write(text)
|
||||||
except (OSError, IOError) as err:
|
except (OSError, IOError) as err:
|
||||||
LOG.error('Could not write Kodi playlist file: %s', playlist)
|
LOG.error('Could not write Kodi playlist file: %s', playlist)
|
||||||
|
@ -267,15 +260,15 @@ def _kodi_playlist_identical(xml_element):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def process_websocket(plex_id, updated_at, state):
|
def process_websocket(plex_id, updated_at, status):
|
||||||
"""
|
"""
|
||||||
Hit by librarysync to process websocket messages concerning playlists
|
Hit by librarysync to process websocket messages concerning playlists
|
||||||
"""
|
"""
|
||||||
create = False
|
create = False
|
||||||
playlist = playlist_object_from_db(plex_id=plex_id)
|
|
||||||
with state.LOCK_PLAYLISTS:
|
with state.LOCK_PLAYLISTS:
|
||||||
|
playlist = playlist_object_from_db(plex_id=plex_id)
|
||||||
try:
|
try:
|
||||||
if playlist and state == 9:
|
if playlist and status == 9:
|
||||||
LOG.debug('Plex deletion of playlist detected: %s', playlist)
|
LOG.debug('Plex deletion of playlist detected: %s', playlist)
|
||||||
delete_kodi_playlist(playlist)
|
delete_kodi_playlist(playlist)
|
||||||
elif playlist and playlist.plex_updatedat == updated_at:
|
elif playlist and playlist.plex_updatedat == updated_at:
|
||||||
|
@ -285,7 +278,7 @@ def process_websocket(plex_id, updated_at, state):
|
||||||
LOG.debug('Change of Plex playlist detected: %s', playlist)
|
LOG.debug('Change of Plex playlist detected: %s', playlist)
|
||||||
delete_kodi_playlist(playlist)
|
delete_kodi_playlist(playlist)
|
||||||
create = True
|
create = True
|
||||||
elif not playlist and not state == 9:
|
elif not playlist and not status == 9:
|
||||||
LOG.debug('Creation of new Plex playlist detected: %s',
|
LOG.debug('Creation of new Plex playlist detected: %s',
|
||||||
plex_id)
|
plex_id)
|
||||||
create = True
|
create = True
|
||||||
|
@ -333,7 +326,7 @@ def _full_sync():
|
||||||
elif playlist.plex_updatedat != api.updated_at():
|
elif playlist.plex_updatedat != api.updated_at():
|
||||||
LOG.debug('Detected changed Plex playlist %s: %s',
|
LOG.debug('Detected changed Plex playlist %s: %s',
|
||||||
api.plex_id(), api.title())
|
api.plex_id(), api.title())
|
||||||
if exists(playlist.kodi_path):
|
if path_ops.exists(playlist.kodi_path):
|
||||||
delete_kodi_playlist(playlist)
|
delete_kodi_playlist(playlist)
|
||||||
else:
|
else:
|
||||||
update_plex_table(playlist, delete=True)
|
update_plex_table(playlist, delete=True)
|
||||||
|
@ -361,17 +354,19 @@ def _full_sync():
|
||||||
if state.ENABLE_MUSIC:
|
if state.ENABLE_MUSIC:
|
||||||
master_paths.append(v.PLAYLIST_PATH_MUSIC)
|
master_paths.append(v.PLAYLIST_PATH_MUSIC)
|
||||||
for master_path in master_paths:
|
for master_path in master_paths:
|
||||||
for root, _, files in os.walk(utils.encode_path(master_path)):
|
for root, _, files in path_ops.walk(master_path):
|
||||||
root = utils.decode_path(root)
|
|
||||||
for file in files:
|
for file in files:
|
||||||
file = utils.decode_path(file)
|
|
||||||
try:
|
try:
|
||||||
extension = file.rsplit('.', 1)[1]
|
extension = file.rsplit('.', 1)[1]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
continue
|
continue
|
||||||
if extension not in SUPPORTED_FILETYPES:
|
if extension not in SUPPORTED_FILETYPES:
|
||||||
continue
|
continue
|
||||||
path = os.path.join(root, file)
|
LOG.debug('root: %s', root)
|
||||||
|
LOG.debug('type: %s', type(root))
|
||||||
|
LOG.debug('file: %s', file)
|
||||||
|
LOG.debug('type: %s', type(file))
|
||||||
|
path = path_ops.path.join(root, file)
|
||||||
kodi_hash = utils.generate_file_md5(path)
|
kodi_hash = utils.generate_file_md5(path)
|
||||||
playlist = playlist_object_from_db(kodi_hash=kodi_hash)
|
playlist = playlist_object_from_db(kodi_hash=kodi_hash)
|
||||||
playlist_2 = playlist_object_from_db(path=path)
|
playlist_2 = playlist_object_from_db(path=path)
|
||||||
|
|
|
@ -3,7 +3,6 @@ Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly
|
||||||
"""
|
"""
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from re import compile as re_compile
|
|
||||||
import xbmc
|
import xbmc
|
||||||
|
|
||||||
from . import utils
|
from . import utils
|
||||||
|
@ -18,7 +17,6 @@ from . import state
|
||||||
LOG = getLogger('PLEX.playqueue')
|
LOG = getLogger('PLEX.playqueue')
|
||||||
|
|
||||||
PLUGIN = 'plugin://%s' % v.ADDON_ID
|
PLUGIN = 'plugin://%s' % v.ADDON_ID
|
||||||
REGEX = re_compile(r'''plex_id=(\d+)''')
|
|
||||||
|
|
||||||
# Our PKC playqueues (3 instances of Playqueue_Object())
|
# Our PKC playqueues (3 instances of Playqueue_Object())
|
||||||
PLAYQUEUES = []
|
PLAYQUEUES = []
|
||||||
|
@ -133,7 +131,7 @@ class PlayqueueMonitor(Thread):
|
||||||
old_item.kodi_type == new_item['type'])
|
old_item.kodi_type == new_item['type'])
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
plex_id = REGEX.findall(new_item['file'])[0]
|
plex_id = utils.REGEX_PLEX_ID.findall(new_item['file'])[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
LOG.debug('Comparing paths directly as a fallback')
|
LOG.debug('Comparing paths directly as a fallback')
|
||||||
identical = old_item.file == new_item['file']
|
identical = old_item.file == new_item['file']
|
||||||
|
|
|
@ -30,15 +30,15 @@ http://stackoverflow.com/questions/111945/is-there-any-way-to-do-http-put-in-pyt
|
||||||
(and others...)
|
(and others...)
|
||||||
"""
|
"""
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from re import compile as re_compile, sub
|
from re import sub
|
||||||
from urllib import urlencode, unquote
|
from urllib import urlencode, unquote
|
||||||
import os
|
|
||||||
from xbmcgui import ListItem
|
from xbmcgui import ListItem
|
||||||
from xbmcvfs import exists
|
from xbmcvfs import exists
|
||||||
|
|
||||||
from .downloadutils import DownloadUtils as DU
|
from .downloadutils import DownloadUtils as DU
|
||||||
from . import clientinfo
|
from . import clientinfo
|
||||||
from . import utils
|
from . import utils
|
||||||
|
from . import path_ops
|
||||||
from . import plex_functions as PF
|
from . import plex_functions as PF
|
||||||
from . import plexdb_functions as plexdb
|
from . import plexdb_functions as plexdb
|
||||||
from . import kodidb_functions as kodidb
|
from . import kodidb_functions as kodidb
|
||||||
|
@ -48,12 +48,6 @@ from . import state
|
||||||
###############################################################################
|
###############################################################################
|
||||||
LOG = getLogger('PLEX.plex_api')
|
LOG = getLogger('PLEX.plex_api')
|
||||||
|
|
||||||
REGEX_IMDB = re_compile(r'''/(tt\d+)''')
|
|
||||||
REGEX_TVDB = re_compile(r'''thetvdb:\/\/(.+?)\?''')
|
|
||||||
|
|
||||||
if not utils.exists_dir(v.EXTERNAL_SUBTITLE_TEMP_PATH):
|
|
||||||
os.makedirs(v.EXTERNAL_SUBTITLE_TEMP_PATH)
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
@ -438,10 +432,10 @@ class API(object):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if providername == 'imdb':
|
if providername == 'imdb':
|
||||||
regex = REGEX_IMDB
|
regex = utils.REGEX_IMDB
|
||||||
elif providername == 'tvdb':
|
elif providername == 'tvdb':
|
||||||
# originally e.g. com.plexapp.agents.thetvdb://276564?lang=en
|
# originally e.g. com.plexapp.agents.thetvdb://276564?lang=en
|
||||||
regex = REGEX_TVDB
|
regex = utils.REGEX_TVDB
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -1246,7 +1240,7 @@ class API(object):
|
||||||
# Get additional info (filename / languages)
|
# Get additional info (filename / languages)
|
||||||
filename = None
|
filename = None
|
||||||
if 'file' in entry[0].attrib:
|
if 'file' in entry[0].attrib:
|
||||||
filename = os.path.basename(entry[0].attrib['file'])
|
filename = path_ops.path.basename(entry[0].attrib['file'])
|
||||||
# Languages of audio streams
|
# Languages of audio streams
|
||||||
languages = []
|
languages = []
|
||||||
for stream in entry[0]:
|
for stream in entry[0]:
|
||||||
|
@ -1401,7 +1395,7 @@ class API(object):
|
||||||
|
|
||||||
Returns the path to the downloaded subtitle or None
|
Returns the path to the downloaded subtitle or None
|
||||||
"""
|
"""
|
||||||
path = os.path.join(v.EXTERNAL_SUBTITLE_TEMP_PATH, filename)
|
path = path_ops.path.join(v.EXTERNAL_SUBTITLE_TEMP_PATH, filename)
|
||||||
response = DU().downloadUrl(url, return_response=True)
|
response = DU().downloadUrl(url, return_response=True)
|
||||||
try:
|
try:
|
||||||
response.status_code
|
response.status_code
|
||||||
|
@ -1410,7 +1404,7 @@ class API(object):
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
LOG.debug('Writing temp subtitle to %s', path)
|
LOG.debug('Writing temp subtitle to %s', path)
|
||||||
with open(utils.encode_path(path), 'wb') as filer:
|
with open(path_ops.encode_path(path), 'wb') as filer:
|
||||||
filer.write(response.content)
|
filer.write(response.content)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
@ -1603,17 +1597,18 @@ class API(object):
|
||||||
check = exists(utils.try_encode(path))
|
check = exists(utils.try_encode(path))
|
||||||
else:
|
else:
|
||||||
# directories
|
# directories
|
||||||
if "\\" in path:
|
checkpath = utils.try_encode(path)
|
||||||
if not path.endswith('\\'):
|
if b"\\" in checkpath:
|
||||||
|
if not checkpath.endswith('\\'):
|
||||||
# Add the missing backslash
|
# Add the missing backslash
|
||||||
check = utils.exists_dir(path + "\\")
|
check = utils.exists_dir(checkpath + "\\")
|
||||||
else:
|
else:
|
||||||
check = utils.exists_dir(path)
|
check = utils.exists_dir(checkpath)
|
||||||
else:
|
else:
|
||||||
if not path.endswith('/'):
|
if not checkpath.endswith('/'):
|
||||||
check = utils.exists_dir(path + "/")
|
check = utils.exists_dir(checkpath + "/")
|
||||||
else:
|
else:
|
||||||
check = utils.exists_dir(path)
|
check = utils.exists_dir(checkpath)
|
||||||
|
|
||||||
if not check:
|
if not check:
|
||||||
if force_check is False:
|
if force_check is False:
|
||||||
|
|
|
@ -3,7 +3,6 @@ from logging import getLogger
|
||||||
from urllib import urlencode, quote_plus
|
from urllib import urlencode, quote_plus
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
from urlparse import urlparse, parse_qsl
|
from urlparse import urlparse, parse_qsl
|
||||||
from re import compile as re_compile
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from time import time
|
from time import time
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
@ -18,8 +17,6 @@ from . import variables as v
|
||||||
LOG = getLogger('PLEX.plex_functions')
|
LOG = getLogger('PLEX.plex_functions')
|
||||||
|
|
||||||
CONTAINERSIZE = int(utils.settings('limitindex'))
|
CONTAINERSIZE = int(utils.settings('limitindex'))
|
||||||
REGEX_PLEX_KEY = re_compile(r'''/(.+)/(\d+)$''')
|
|
||||||
REGEX_PLEX_DIRECT = re_compile(r'''\.plex\.direct:\d+$''')
|
|
||||||
|
|
||||||
# For discovery of PMS in the local LAN
|
# For discovery of PMS in the local LAN
|
||||||
PLEX_GDM_IP = '239.0.0.250' # multicast to PMS
|
PLEX_GDM_IP = '239.0.0.250' # multicast to PMS
|
||||||
|
@ -47,7 +44,7 @@ def GetPlexKeyNumber(plexKey):
|
||||||
Returns ('','') if nothing is found
|
Returns ('','') if nothing is found
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
result = REGEX_PLEX_KEY.findall(plexKey)[0]
|
result = utils.REGEX_END_DIGITS.findall(plexKey)[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
result = ('', '')
|
result = ('', '')
|
||||||
return result
|
return result
|
||||||
|
@ -411,7 +408,7 @@ def _pms_list_from_plex_tv(token):
|
||||||
def _poke_pms(pms, queue):
|
def _poke_pms(pms, queue):
|
||||||
data = pms['connections'][0].attrib
|
data = pms['connections'][0].attrib
|
||||||
url = data['uri']
|
url = data['uri']
|
||||||
if data['local'] == '1' and REGEX_PLEX_DIRECT.findall(url):
|
if data['local'] == '1' and utils.REGEX_PLEX_DIRECT.findall(url):
|
||||||
# In case DNS resolve of plex.direct does not work, append a new
|
# In case DNS resolve of plex.direct does not work, append a new
|
||||||
# connection that will directly access the local IP (e.g. internet down)
|
# connection that will directly access the local IP (e.g. internet down)
|
||||||
conn = deepcopy(pms['connections'][0])
|
conn = deepcopy(pms['connections'][0])
|
||||||
|
|
|
@ -55,6 +55,7 @@ class Service():
|
||||||
utils.settings('useDirectPaths') == '1')
|
utils.settings('useDirectPaths') == '1')
|
||||||
LOG.info("Number of sync threads: %s",
|
LOG.info("Number of sync threads: %s",
|
||||||
utils.settings('syncThreadNumber'))
|
utils.settings('syncThreadNumber'))
|
||||||
|
LOG.info('Playlist m3u encoding: %s', v.M3U_ENCODING)
|
||||||
LOG.info("Full sys.argv received: %s", sys.argv)
|
LOG.info("Full sys.argv received: %s", sys.argv)
|
||||||
self.monitor = xbmc.Monitor()
|
self.monitor = xbmc.Monitor()
|
||||||
# Load/Reset PKC entirely - important for user/Kodi profile switch
|
# Load/Reset PKC entirely - important for user/Kodi profile switch
|
||||||
|
|
|
@ -3,14 +3,14 @@
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
from xbmc import sleep, executebuiltin, translatePath
|
from xbmc import sleep, executebuiltin
|
||||||
import xbmcaddon
|
|
||||||
from xbmcvfs import exists
|
|
||||||
|
|
||||||
from .downloadutils import DownloadUtils as DU
|
from .downloadutils import DownloadUtils as DU
|
||||||
from . import utils
|
from . import utils
|
||||||
|
from . import path_ops
|
||||||
from . import plex_tv
|
from . import plex_tv
|
||||||
from . import plex_functions as PF
|
from . import plex_functions as PF
|
||||||
|
from . import variables as v
|
||||||
from . import state
|
from . import state
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
@ -44,7 +44,6 @@ class UserClient(Thread):
|
||||||
self.ssl = None
|
self.ssl = None
|
||||||
self.sslcert = None
|
self.sslcert = None
|
||||||
|
|
||||||
self.addon = xbmcaddon.Addon()
|
|
||||||
self.do_utils = None
|
self.do_utils = None
|
||||||
|
|
||||||
Thread.__init__(self)
|
Thread.__init__(self)
|
||||||
|
@ -197,11 +196,8 @@ class UserClient(Thread):
|
||||||
'Addon.Openutils.settings(plugin.video.plexkodiconnect)')
|
'Addon.Openutils.settings(plugin.video.plexkodiconnect)')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Get /profile/addon_data
|
|
||||||
addondir = translatePath(self.addon.getAddonInfo('profile'))
|
|
||||||
|
|
||||||
# If there's no settings.xml
|
# If there's no settings.xml
|
||||||
if not exists("%ssettings.xml" % addondir):
|
if not path_ops.exists("%ssettings.xml" % v.ADDON_PROFILE):
|
||||||
LOG.error("Error, no settings.xml found.")
|
LOG.error("Error, no settings.xml found.")
|
||||||
self.auth = False
|
self.auth = False
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -4,11 +4,6 @@ Various functions and decorators for PKC
|
||||||
"""
|
"""
|
||||||
###############################################################################
|
###############################################################################
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
import xbmc
|
|
||||||
import xbmcaddon
|
|
||||||
import xbmcgui
|
|
||||||
from xbmcvfs import exists, delete
|
|
||||||
import os
|
|
||||||
from cProfile import Profile
|
from cProfile import Profile
|
||||||
from pstats import Stats
|
from pstats import Stats
|
||||||
from sqlite3 import connect, OperationalError
|
from sqlite3 import connect, OperationalError
|
||||||
|
@ -18,13 +13,14 @@ from time import localtime, strftime
|
||||||
from unicodedata import normalize
|
from unicodedata import normalize
|
||||||
import xml.etree.ElementTree as etree
|
import xml.etree.ElementTree as etree
|
||||||
from functools import wraps, partial
|
from functools import wraps, partial
|
||||||
from shutil import rmtree
|
|
||||||
from urllib import quote_plus
|
from urllib import quote_plus
|
||||||
import hashlib
|
import hashlib
|
||||||
import re
|
import re
|
||||||
import unicodedata
|
import xbmc
|
||||||
|
import xbmcaddon
|
||||||
|
import xbmcgui
|
||||||
|
|
||||||
from .watchdog.utils import unicode_paths
|
from . import path_ops
|
||||||
from . import variables as v
|
from . import variables as v
|
||||||
from . import state
|
from . import state
|
||||||
|
|
||||||
|
@ -36,9 +32,19 @@ WINDOW = xbmcgui.Window(10000)
|
||||||
ADDON = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
|
ADDON = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
|
||||||
EPOCH = datetime.utcfromtimestamp(0)
|
EPOCH = datetime.utcfromtimestamp(0)
|
||||||
|
|
||||||
|
# Grab Plex id from '...plex_id=XXXX....'
|
||||||
REGEX_PLEX_ID = re.compile(r'''plex_id=(\d+)''')
|
REGEX_PLEX_ID = re.compile(r'''plex_id=(\d+)''')
|
||||||
|
# Return the numbers at the end of an url like '.../.../XXXX'
|
||||||
|
REGEX_END_DIGITS = re.compile(r'''/(.+)/(\d+)$''')
|
||||||
|
REGEX_PLEX_DIRECT = re.compile(r'''\.plex\.direct:\d+$''')
|
||||||
REGEX_FILE_NUMBERING = re.compile(r'''_(\d+)\.\w+$''')
|
REGEX_FILE_NUMBERING = re.compile(r'''_(\d+)\.\w+$''')
|
||||||
|
# Plex API
|
||||||
|
REGEX_IMDB = re.compile(r'''/(tt\d+)''')
|
||||||
|
REGEX_TVDB = re.compile(r'''thetvdb:\/\/(.+?)\?''')
|
||||||
|
# Plex music
|
||||||
|
REGEX_MUSICPATH = re.compile(r'''^\^(.+)\$$''')
|
||||||
|
# Grab Plex id from an URL-encoded string
|
||||||
|
REGEX_PLEX_ID_FROM_URL = re.compile(r'''metadata%2F(\d+)''')
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Main methods
|
# Main methods
|
||||||
|
@ -106,30 +112,6 @@ def settings(setting, value=None):
|
||||||
return try_decode(addon.getSetting(setting))
|
return try_decode(addon.getSetting(setting))
|
||||||
|
|
||||||
|
|
||||||
def exists_dir(path):
|
|
||||||
"""
|
|
||||||
Safe way to check whether the directory path exists already (broken in Kodi
|
|
||||||
<17)
|
|
||||||
|
|
||||||
Feed with encoded string or unicode
|
|
||||||
"""
|
|
||||||
if v.KODIVERSION >= 17:
|
|
||||||
answ = exists(try_encode(path))
|
|
||||||
else:
|
|
||||||
dummyfile = os.path.join(try_decode(path), 'dummyfile.txt')
|
|
||||||
try:
|
|
||||||
with open(encode_path(dummyfile), 'w') as filer:
|
|
||||||
filer.write('text')
|
|
||||||
except IOError:
|
|
||||||
# folder does not exist yet
|
|
||||||
answ = 0
|
|
||||||
else:
|
|
||||||
# Folder exists. Delete file again.
|
|
||||||
delete(try_encode(dummyfile))
|
|
||||||
answ = 1
|
|
||||||
return answ
|
|
||||||
|
|
||||||
|
|
||||||
def lang(stringid):
|
def lang(stringid):
|
||||||
"""
|
"""
|
||||||
Central string retrieval from strings.po
|
Central string retrieval from strings.po
|
||||||
|
@ -248,26 +230,6 @@ def kodi_time_to_millis(time):
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def encode_path(path):
|
|
||||||
"""
|
|
||||||
Filenames and paths are not necessarily utf-8 encoded. Use this function
|
|
||||||
instead of try_encode/trydecode if working with filenames and paths!
|
|
||||||
(os.walk only feeds on encoded paths. sys.getfilesystemencoding returns None
|
|
||||||
for Raspberry Pi)
|
|
||||||
"""
|
|
||||||
return unicode_paths.encode(path)
|
|
||||||
|
|
||||||
|
|
||||||
def decode_path(path):
|
|
||||||
"""
|
|
||||||
Filenames and paths are not necessarily utf-8 encoded. Use this function
|
|
||||||
instead of try_encode/trydecode if working with filenames and paths!
|
|
||||||
(os.walk only feeds on encoded paths. sys.getfilesystemencoding returns None
|
|
||||||
for Raspberry Pi)
|
|
||||||
"""
|
|
||||||
return unicode_paths.decode(path)
|
|
||||||
|
|
||||||
|
|
||||||
def try_encode(input_str, encoding='utf-8'):
|
def try_encode(input_str, encoding='utf-8'):
|
||||||
"""
|
"""
|
||||||
Will try to encode input_str (in unicode) to encoding. This possibly
|
Will try to encode input_str (in unicode) to encoding. This possibly
|
||||||
|
@ -333,10 +295,6 @@ def valid_filename(text):
|
||||||
else:
|
else:
|
||||||
# Linux
|
# Linux
|
||||||
text = re.sub(r'/', '', text)
|
text = re.sub(r'/', '', text)
|
||||||
if not os.path.supports_unicode_filenames:
|
|
||||||
text = unicodedata.normalize('NFKD', text)
|
|
||||||
text = text.encode('ascii', 'ignore')
|
|
||||||
text = text.decode('ascii')
|
|
||||||
# Ensure that filename length is at most 255 chars (including 3 chars for
|
# Ensure that filename length is at most 255 chars (including 3 chars for
|
||||||
# filename extension and 1 dot to separate the extension)
|
# filename extension and 1 dot to separate the extension)
|
||||||
text = text[:min(len(text), 251)]
|
text = text[:min(len(text), 251)]
|
||||||
|
@ -485,15 +443,15 @@ def wipe_database():
|
||||||
# Delete all synced playlists
|
# Delete all synced playlists
|
||||||
for path in playlist_paths:
|
for path in playlist_paths:
|
||||||
try:
|
try:
|
||||||
os.remove(path)
|
path_ops.remove(path)
|
||||||
except (OSError, IOError):
|
except (OSError, IOError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
LOG.info("Resetting all cached artwork.")
|
LOG.info("Resetting all cached artwork.")
|
||||||
# Remove all existing textures first
|
# Remove all existing textures first
|
||||||
path = xbmc.translatePath("special://thumbnails/")
|
path = path_ops.translate_path("special://thumbnails/")
|
||||||
if exists(path):
|
if path_ops.exists(path):
|
||||||
rmtree(try_decode(path), ignore_errors=True)
|
path_ops.rmtree(path, ignore_errors=True)
|
||||||
# remove all existing data from texture DB
|
# remove all existing data from texture DB
|
||||||
connection = kodi_sql('texture')
|
connection = kodi_sql('texture')
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
|
@ -547,10 +505,8 @@ def reset(ask_user=True):
|
||||||
heading='{plex} %s ' % lang(30132),
|
heading='{plex} %s ' % lang(30132),
|
||||||
line1=lang(39603)):
|
line1=lang(39603)):
|
||||||
# Delete the settings
|
# Delete the settings
|
||||||
addon = xbmcaddon.Addon()
|
|
||||||
addondir = try_decode(xbmc.translatePath(addon.getAddonInfo('profile')))
|
|
||||||
LOG.info("Deleting: settings.xml")
|
LOG.info("Deleting: settings.xml")
|
||||||
os.remove("%ssettings.xml" % addondir)
|
path_ops.remove("%ssettings.xml" % v.ADDON_PROFILE)
|
||||||
reboot_kodi()
|
reboot_kodi()
|
||||||
|
|
||||||
|
|
||||||
|
@ -612,29 +568,6 @@ def compare_version(current, minimum):
|
||||||
return curr_patch >= min_patch
|
return curr_patch >= min_patch
|
||||||
|
|
||||||
|
|
||||||
def normalize_nodes(text):
|
|
||||||
"""
|
|
||||||
For video nodes
|
|
||||||
"""
|
|
||||||
text = text.replace(":", "")
|
|
||||||
text = text.replace("/", "-")
|
|
||||||
text = text.replace("\\", "-")
|
|
||||||
text = text.replace("<", "")
|
|
||||||
text = text.replace(">", "")
|
|
||||||
text = text.replace("*", "")
|
|
||||||
text = text.replace("?", "")
|
|
||||||
text = text.replace('|', "")
|
|
||||||
text = text.replace('(', "")
|
|
||||||
text = text.replace(')', "")
|
|
||||||
text = text.strip()
|
|
||||||
# Remove dots from the last character as windows can not have directories
|
|
||||||
# with dots at the end
|
|
||||||
text = text.rstrip('.')
|
|
||||||
text = try_encode(normalize('NFKD', unicode(text, 'utf-8')))
|
|
||||||
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_string(text):
|
def normalize_string(text):
|
||||||
"""
|
"""
|
||||||
For theme media, do not modify unless modified in TV Tunes
|
For theme media, do not modify unless modified in TV Tunes
|
||||||
|
@ -656,6 +589,28 @@ def normalize_string(text):
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_nodes(text):
|
||||||
|
"""
|
||||||
|
For video nodes. Returns unicode
|
||||||
|
"""
|
||||||
|
text = text.replace(":", "")
|
||||||
|
text = text.replace("/", "-")
|
||||||
|
text = text.replace("\\", "-")
|
||||||
|
text = text.replace("<", "")
|
||||||
|
text = text.replace(">", "")
|
||||||
|
text = text.replace("*", "")
|
||||||
|
text = text.replace("?", "")
|
||||||
|
text = text.replace('|', "")
|
||||||
|
text = text.replace('(', "")
|
||||||
|
text = text.replace(')', "")
|
||||||
|
text = text.strip()
|
||||||
|
# Remove dots from the last character as windows can not have directories
|
||||||
|
# with dots at the end
|
||||||
|
text = text.rstrip('.')
|
||||||
|
text = normalize('NFKD', unicode(text, 'utf-8'))
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def indent(elem, level=0):
|
def indent(elem, level=0):
|
||||||
"""
|
"""
|
||||||
Prettifies xml trees. Pass the etree root in
|
Prettifies xml trees. Pass the etree root in
|
||||||
|
@ -702,9 +657,9 @@ class XmlKodiSetting(object):
|
||||||
top_element=None):
|
top_element=None):
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
if path is None:
|
if path is None:
|
||||||
self.path = os.path.join(v.KODI_PROFILE, filename)
|
self.path = path_ops.path.join(v.KODI_PROFILE, filename)
|
||||||
else:
|
else:
|
||||||
self.path = os.path.join(path, filename)
|
self.path = path_ops.path.join(path, filename)
|
||||||
self.force_create = force_create
|
self.force_create = force_create
|
||||||
self.top_element = top_element
|
self.top_element = top_element
|
||||||
self.tree = None
|
self.tree = None
|
||||||
|
@ -869,7 +824,7 @@ def passwords_xml():
|
||||||
"""
|
"""
|
||||||
To add network credentials to Kodi's password xml
|
To add network credentials to Kodi's password xml
|
||||||
"""
|
"""
|
||||||
path = try_decode(xbmc.translatePath("special://userdata/"))
|
path = path_ops.translate_path('special://userdata/')
|
||||||
xmlpath = "%spasswords.xml" % path
|
xmlpath = "%spasswords.xml" % path
|
||||||
try:
|
try:
|
||||||
xmlparse = etree.parse(xmlpath)
|
xmlparse = etree.parse(xmlpath)
|
||||||
|
@ -990,7 +945,7 @@ def playlist_xsp(mediatype, tagname, viewid, viewtype="", delete=False):
|
||||||
"""
|
"""
|
||||||
Feed with tagname as unicode
|
Feed with tagname as unicode
|
||||||
"""
|
"""
|
||||||
path = try_decode(xbmc.translatePath("special://profile/playlists/video/"))
|
path = path_ops.translate_path("special://profile/playlists/video/")
|
||||||
if viewtype == "mixed":
|
if viewtype == "mixed":
|
||||||
plname = "%s - %s" % (tagname, mediatype)
|
plname = "%s - %s" % (tagname, mediatype)
|
||||||
xsppath = "%sPlex %s - %s.xsp" % (path, viewid, mediatype)
|
xsppath = "%sPlex %s - %s.xsp" % (path, viewid, mediatype)
|
||||||
|
@ -999,15 +954,15 @@ def playlist_xsp(mediatype, tagname, viewid, viewtype="", delete=False):
|
||||||
xsppath = "%sPlex %s.xsp" % (path, viewid)
|
xsppath = "%sPlex %s.xsp" % (path, viewid)
|
||||||
|
|
||||||
# Create the playlist directory
|
# Create the playlist directory
|
||||||
if not exists(try_encode(path)):
|
if not path_ops.exists(path):
|
||||||
LOG.info("Creating directory: %s", path)
|
LOG.info("Creating directory: %s", path)
|
||||||
os.makedirs(path)
|
path_ops.makedirs(path)
|
||||||
|
|
||||||
# Only add the playlist if it doesn't already exists
|
# Only add the playlist if it doesn't already exists
|
||||||
if exists(try_encode(xsppath)):
|
if path_ops.exists(xsppath):
|
||||||
LOG.info('Path %s does exist', xsppath)
|
LOG.info('Path %s does exist', xsppath)
|
||||||
if delete:
|
if delete:
|
||||||
os.remove(xsppath)
|
path_ops.remove(xsppath)
|
||||||
LOG.info("Successfully removed playlist: %s.", tagname)
|
LOG.info("Successfully removed playlist: %s.", tagname)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -1019,7 +974,7 @@ def playlist_xsp(mediatype, tagname, viewid, viewtype="", delete=False):
|
||||||
'show': 'tvshows'
|
'show': 'tvshows'
|
||||||
}
|
}
|
||||||
LOG.info("Writing playlist file to: %s", xsppath)
|
LOG.info("Writing playlist file to: %s", xsppath)
|
||||||
with open(encode_path(xsppath), 'wb') as filer:
|
with open(path_ops.encode_path(xsppath), 'wb') as filer:
|
||||||
filer.write(try_encode(
|
filer.write(try_encode(
|
||||||
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n'
|
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n'
|
||||||
'<smartplaylist type="%s">\n\t'
|
'<smartplaylist type="%s">\n\t'
|
||||||
|
@ -1037,22 +992,22 @@ def delete_playlists():
|
||||||
"""
|
"""
|
||||||
Clean up the playlists
|
Clean up the playlists
|
||||||
"""
|
"""
|
||||||
path = try_decode(xbmc.translatePath("special://profile/playlists/video/"))
|
path = path_ops.translate_path('special://profile/playlists/video/')
|
||||||
for root, _, files in os.walk(path):
|
for root, _, files in path_ops.walk(path):
|
||||||
for file in files:
|
for file in files:
|
||||||
if file.startswith('Plex'):
|
if file.startswith('Plex'):
|
||||||
os.remove(os.path.join(root, file))
|
path_ops.remove(path_ops.path.join(root, file))
|
||||||
|
|
||||||
|
|
||||||
def delete_nodes():
|
def delete_nodes():
|
||||||
"""
|
"""
|
||||||
Clean up video nodes
|
Clean up video nodes
|
||||||
"""
|
"""
|
||||||
path = try_decode(xbmc.translatePath("special://profile/library/video/"))
|
path = path_ops.translate_path("special://profile/library/video/")
|
||||||
for root, dirs, _ in os.walk(path):
|
for root, dirs, _ in path_ops.walk(path):
|
||||||
for directory in dirs:
|
for directory in dirs:
|
||||||
if directory.startswith('Plex-'):
|
if directory.startswith('Plex-'):
|
||||||
rmtree(os.path.join(root, directory))
|
path_ops.rmtree(path_ops.path.join(root, directory))
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
|
@ -1065,7 +1020,7 @@ def generate_file_md5(path):
|
||||||
"""
|
"""
|
||||||
m = hashlib.md5()
|
m = hashlib.md5()
|
||||||
m.update(path.encode('utf-8'))
|
m.update(path.encode('utf-8'))
|
||||||
with open(encode_path(path), 'rb') as f:
|
with open(path_ops.encode_path(path), 'rb') as f:
|
||||||
while True:
|
while True:
|
||||||
piece = f.read(32768)
|
piece = f.read(32768)
|
||||||
if not piece:
|
if not piece:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import xbmc
|
import xbmc
|
||||||
from xbmcaddon import Addon
|
from xbmcaddon import Addon
|
||||||
|
|
||||||
|
@ -34,7 +35,9 @@ _ADDON = Addon()
|
||||||
ADDON_NAME = 'PlexKodiConnect'
|
ADDON_NAME = 'PlexKodiConnect'
|
||||||
ADDON_ID = 'plugin.video.plexkodiconnect'
|
ADDON_ID = 'plugin.video.plexkodiconnect'
|
||||||
ADDON_VERSION = _ADDON.getAddonInfo('version')
|
ADDON_VERSION = _ADDON.getAddonInfo('version')
|
||||||
|
ADDON_PATH = try_decode(_ADDON.getAddonInfo('path'))
|
||||||
ADDON_FOLDER = try_decode(xbmc.translatePath('special://home'))
|
ADDON_FOLDER = try_decode(xbmc.translatePath('special://home'))
|
||||||
|
ADDON_PROFILE = try_decode(_ADDON.getAddonInfo('profile'))
|
||||||
|
|
||||||
KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1)
|
KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1)
|
||||||
KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
|
KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
|
||||||
|
@ -81,10 +84,6 @@ MIN_DB_VERSION = '2.0.27'
|
||||||
|
|
||||||
# Database paths
|
# Database paths
|
||||||
_DB_VIDEO_VERSION = {
|
_DB_VIDEO_VERSION = {
|
||||||
13: 78, # Gotham
|
|
||||||
14: 90, # Helix
|
|
||||||
15: 93, # Isengard
|
|
||||||
16: 99, # Jarvis
|
|
||||||
17: 107, # Krypton
|
17: 107, # Krypton
|
||||||
18: 109 # Leia
|
18: 109 # Leia
|
||||||
}
|
}
|
||||||
|
@ -92,10 +91,6 @@ DB_VIDEO_PATH = try_decode(xbmc.translatePath(
|
||||||
"special://database/MyVideos%s.db" % _DB_VIDEO_VERSION[KODIVERSION]))
|
"special://database/MyVideos%s.db" % _DB_VIDEO_VERSION[KODIVERSION]))
|
||||||
|
|
||||||
_DB_MUSIC_VERSION = {
|
_DB_MUSIC_VERSION = {
|
||||||
13: 46, # Gotham
|
|
||||||
14: 48, # Helix
|
|
||||||
15: 52, # Isengard
|
|
||||||
16: 56, # Jarvis
|
|
||||||
17: 60, # Krypton
|
17: 60, # Krypton
|
||||||
18: 70 # Leia
|
18: 70 # Leia
|
||||||
}
|
}
|
||||||
|
@ -103,10 +98,6 @@ DB_MUSIC_PATH = try_decode(xbmc.translatePath(
|
||||||
"special://database/MyMusic%s.db" % _DB_MUSIC_VERSION[KODIVERSION]))
|
"special://database/MyMusic%s.db" % _DB_MUSIC_VERSION[KODIVERSION]))
|
||||||
|
|
||||||
_DB_TEXTURE_VERSION = {
|
_DB_TEXTURE_VERSION = {
|
||||||
13: 13, # Gotham
|
|
||||||
14: 13, # Helix
|
|
||||||
15: 13, # Isengard
|
|
||||||
16: 13, # Jarvis
|
|
||||||
17: 13, # Krypton
|
17: 13, # Krypton
|
||||||
18: 13 # Leia
|
18: 13 # Leia
|
||||||
}
|
}
|
||||||
|
@ -122,6 +113,12 @@ EXTERNAL_SUBTITLE_TEMP_PATH = try_decode(xbmc.translatePath(
|
||||||
# Multiply Plex time by this factor to receive Kodi time
|
# Multiply Plex time by this factor to receive Kodi time
|
||||||
PLEX_TO_KODI_TIMEFACTOR = 1.0 / 1000.0
|
PLEX_TO_KODI_TIMEFACTOR = 1.0 / 1000.0
|
||||||
|
|
||||||
|
# We're "failing" playback with a video of 0 length
|
||||||
|
NULL_VIDEO = os.path.join(ADDON_FOLDER,
|
||||||
|
'addons',
|
||||||
|
ADDON_ID,
|
||||||
|
'empty_video.mp4')
|
||||||
|
|
||||||
# Playlist stuff
|
# Playlist stuff
|
||||||
PLAYLIST_PATH = os.path.join(KODI_PROFILE, 'playlists')
|
PLAYLIST_PATH = os.path.join(KODI_PROFILE, 'playlists')
|
||||||
PLAYLIST_PATH_MIXED = os.path.join(PLAYLIST_PATH, 'mixed')
|
PLAYLIST_PATH_MIXED = os.path.join(PLAYLIST_PATH, 'mixed')
|
||||||
|
@ -512,3 +509,14 @@ PLEX_STREAM_TYPE_FROM_STREAM_TYPE = {
|
||||||
'audio': '2',
|
'audio': '2',
|
||||||
'subtitle': '3'
|
'subtitle': '3'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Encoding to be used for our m3u playlist files
|
||||||
|
# m3u files do not have encoding specified by definition, unfortunately.
|
||||||
|
if PLATFORM == 'Windows':
|
||||||
|
M3U_ENCODING = 'mbcs'
|
||||||
|
else:
|
||||||
|
M3U_ENCODING = sys.getfilesystemencoding()
|
||||||
|
if (not M3U_ENCODING or
|
||||||
|
M3U_ENCODING == 'ascii' or
|
||||||
|
M3U_ENCODING == 'ANSI_X3.4-1968'):
|
||||||
|
M3U_ENCODING = 'utf-8'
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
###############################################################################
|
###############################################################################
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from distutils import dir_util
|
|
||||||
import xml.etree.ElementTree as etree
|
import xml.etree.ElementTree as etree
|
||||||
from os import makedirs
|
|
||||||
import xbmc
|
import xbmc
|
||||||
from xbmcvfs import exists
|
|
||||||
|
|
||||||
from . import utils
|
from . import utils
|
||||||
|
from . import path_ops
|
||||||
from . import variables as v
|
from . import variables as v
|
||||||
from . import state
|
from . import state
|
||||||
|
|
||||||
|
@ -16,7 +14,6 @@ from . import state
|
||||||
LOG = getLogger('PLEX.videonodes')
|
LOG = getLogger('PLEX.videonodes')
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Paths are strings, NOT unicode!
|
|
||||||
|
|
||||||
|
|
||||||
class VideoNodes(object):
|
class VideoNodes(object):
|
||||||
|
@ -66,33 +63,30 @@ class VideoNodes(object):
|
||||||
dirname = viewid
|
dirname = viewid
|
||||||
|
|
||||||
# Returns strings
|
# Returns strings
|
||||||
path = utils.try_decode(xbmc.translatePath(
|
path = path_ops.translate_path('special://profile/library/video/')
|
||||||
"special://profile/library/video/"))
|
nodepath = path_ops.translate_path(
|
||||||
nodepath = utils.try_decode(xbmc.translatePath(
|
'special://profile/library/video/Plex-%s/' % dirname)
|
||||||
"special://profile/library/video/Plex-%s/" % dirname))
|
|
||||||
|
|
||||||
if delete:
|
if delete:
|
||||||
if utils.exists_dir(nodepath):
|
if path_ops.exists(nodepath):
|
||||||
from shutil import rmtree
|
path_ops.rmtree(nodepath)
|
||||||
rmtree(nodepath)
|
|
||||||
LOG.info("Sucessfully removed videonode: %s." % tagname)
|
LOG.info("Sucessfully removed videonode: %s." % tagname)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Verify the video directory
|
# Verify the video directory
|
||||||
if not utils.exists_dir(path):
|
if not path_ops.exists(path):
|
||||||
dir_util.copy_tree(
|
path_ops.copy_tree(
|
||||||
src=utils.try_decode(
|
src=path_ops.translate_path(
|
||||||
xbmc.translatePath("special://xbmc/system/library/video")),
|
'special://xbmc/system/library/video'),
|
||||||
dst=utils.try_decode(
|
dst=path_ops.translate_path('special://profile/library/video'),
|
||||||
xbmc.translatePath("special://profile/library/video")),
|
|
||||||
preserve_mode=0) # do not copy permission bits!
|
preserve_mode=0) # do not copy permission bits!
|
||||||
|
|
||||||
# Create the node directory
|
# Create the node directory
|
||||||
if mediatype != "photos":
|
if mediatype != "photos":
|
||||||
if not utils.exists_dir(nodepath):
|
if not path_ops.exists(nodepath):
|
||||||
# folder does not exist yet
|
# folder does not exist yet
|
||||||
LOG.debug('Creating folder %s' % nodepath)
|
LOG.debug('Creating folder %s' % nodepath)
|
||||||
makedirs(nodepath)
|
path_ops.makedirs(nodepath)
|
||||||
|
|
||||||
# Create index entry
|
# Create index entry
|
||||||
nodeXML = "%sindex.xml" % nodepath
|
nodeXML = "%sindex.xml" % nodepath
|
||||||
|
@ -298,7 +292,7 @@ class VideoNodes(object):
|
||||||
# kodi picture sources somehow
|
# kodi picture sources somehow
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if exists(utils.try_encode(nodeXML)):
|
if path_ops.exists(nodeXML):
|
||||||
# Don't recreate xml if already exists
|
# Don't recreate xml if already exists
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -403,13 +397,12 @@ class VideoNodes(object):
|
||||||
utils.indent(root)
|
utils.indent(root)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
etree.ElementTree(root).write(nodeXML, encoding="UTF-8")
|
etree.ElementTree(root).write(path_ops.encode_path(nodeXML),
|
||||||
|
encoding="UTF-8")
|
||||||
|
|
||||||
def singleNode(self, indexnumber, tagname, mediatype, itemtype):
|
def singleNode(self, indexnumber, tagname, mediatype, itemtype):
|
||||||
tagname = utils.try_encode(tagname)
|
cleantagname = utils.normalize_nodes(tagname)
|
||||||
cleantagname = utils.try_decode(utils.normalize_nodes(tagname))
|
nodepath = path_ops.translate_path('special://profile/library/video/')
|
||||||
nodepath = utils.try_decode(xbmc.translatePath(
|
|
||||||
"special://profile/library/video/"))
|
|
||||||
nodeXML = "%splex_%s.xml" % (nodepath, cleantagname)
|
nodeXML = "%splex_%s.xml" % (nodepath, cleantagname)
|
||||||
path = "library://video/plex_%s.xml" % cleantagname
|
path = "library://video/plex_%s.xml" % cleantagname
|
||||||
if v.KODIVERSION >= 17:
|
if v.KODIVERSION >= 17:
|
||||||
|
@ -419,13 +412,12 @@ class VideoNodes(object):
|
||||||
windowpath = "ActivateWindow(Video,%s,return)" % path
|
windowpath = "ActivateWindow(Video,%s,return)" % path
|
||||||
|
|
||||||
# Create the video node directory
|
# Create the video node directory
|
||||||
if not utils.exists_dir(nodepath):
|
if not path_ops.exists(nodepath):
|
||||||
# We need to copy over the default items
|
# We need to copy over the default items
|
||||||
dir_util.copy_tree(
|
path_ops.copy_tree(
|
||||||
src=utils.try_decode(
|
src=path_ops.translate_path(
|
||||||
xbmc.translatePath("special://xbmc/system/library/video")),
|
'special://xbmc/system/library/video'),
|
||||||
dst=utils.try_decode(
|
dst=path_ops.translate_path('special://profile/library/video'),
|
||||||
xbmc.translatePath("special://profile/library/video")),
|
|
||||||
preserve_mode=0) # do not copy permission bits!
|
preserve_mode=0) # do not copy permission bits!
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
|
@ -440,7 +432,7 @@ class VideoNodes(object):
|
||||||
utils.window('%s.content' % embynode, value=path)
|
utils.window('%s.content' % embynode, value=path)
|
||||||
utils.window('%s.type' % embynode, value=itemtype)
|
utils.window('%s.type' % embynode, value=itemtype)
|
||||||
|
|
||||||
if exists(utils.try_encode(nodeXML)):
|
if path_ops.exists(nodeXML):
|
||||||
# Don't recreate xml if already exists
|
# Don't recreate xml if already exists
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue