Playlist sync support, part 2
This commit is contained in:
parent
ac8b8e6153
commit
e38f99f088
7 changed files with 297 additions and 66 deletions
|
@ -85,6 +85,19 @@ class API(object):
|
||||||
"""
|
"""
|
||||||
return self.item.get('type')
|
return self.item.get('type')
|
||||||
|
|
||||||
|
def playlist_type(self):
|
||||||
|
"""
|
||||||
|
Returns the playlist type ('video', 'audio') or None
|
||||||
|
"""
|
||||||
|
return self.item.get('playlistType')
|
||||||
|
|
||||||
|
def updated_at(self):
|
||||||
|
"""
|
||||||
|
Returns the last time this item was updated as unicode, e.g.
|
||||||
|
'1524739868', or None
|
||||||
|
"""
|
||||||
|
return self.item.get('updatedAt')
|
||||||
|
|
||||||
def checksum(self):
|
def checksum(self):
|
||||||
"""
|
"""
|
||||||
Returns a string, not int.
|
Returns a string, not int.
|
||||||
|
|
|
@ -1527,7 +1527,7 @@ class LibrarySync(Thread):
|
||||||
last_time_sync = utils.unix_timestamp()
|
last_time_sync = utils.unix_timestamp()
|
||||||
window('plex_dbScan', clear=True)
|
window('plex_dbScan', clear=True)
|
||||||
state.DB_SCAN = False
|
state.DB_SCAN = False
|
||||||
kodi_playlist_monitor = None
|
playlist_monitor = None
|
||||||
|
|
||||||
while not self.stopped():
|
while not self.stopped():
|
||||||
# In the event the server goes offline
|
# In the event the server goes offline
|
||||||
|
@ -1554,7 +1554,8 @@ class LibrarySync(Thread):
|
||||||
kodi_db_version_checked = True
|
kodi_db_version_checked = True
|
||||||
last_sync = utils.unix_timestamp()
|
last_sync = utils.unix_timestamp()
|
||||||
self.fanartthread.start()
|
self.fanartthread.start()
|
||||||
kodi_playlist_monitor = playlists.kodi_playlist_monitor()
|
if playlists.full_sync():
|
||||||
|
playlist_monitor = playlists.kodi_playlist_monitor()
|
||||||
else:
|
else:
|
||||||
LOG.error('Initial start-up full sync unsuccessful')
|
LOG.error('Initial start-up full sync unsuccessful')
|
||||||
xbmc.executebuiltin('InhibitIdleShutdown(false)')
|
xbmc.executebuiltin('InhibitIdleShutdown(false)')
|
||||||
|
@ -1600,11 +1601,12 @@ class LibrarySync(Thread):
|
||||||
LOG.info('Done initial sync on Kodi startup')
|
LOG.info('Done initial sync on Kodi startup')
|
||||||
artwork.Artwork().cache_major_artwork()
|
artwork.Artwork().cache_major_artwork()
|
||||||
self.fanartthread.start()
|
self.fanartthread.start()
|
||||||
|
if playlists.full_sync():
|
||||||
|
playlist_monitor = playlists.kodi_playlist_monitor()
|
||||||
else:
|
else:
|
||||||
LOG.info('Startup sync has not yet been successful')
|
LOG.info('Startup sync has not yet been successful')
|
||||||
window('plex_dbScan', clear=True)
|
window('plex_dbScan', clear=True)
|
||||||
state.DB_SCAN = False
|
state.DB_SCAN = False
|
||||||
kodi_playlist_monitor = playlists.kodi_playlist_monitor()
|
|
||||||
|
|
||||||
# Currently no db scan, so we can start a new scan
|
# Currently no db scan, so we can start a new scan
|
||||||
elif state.DB_SCAN is False:
|
elif state.DB_SCAN is False:
|
||||||
|
@ -1665,8 +1667,8 @@ class LibrarySync(Thread):
|
||||||
continue
|
continue
|
||||||
xbmc.sleep(100)
|
xbmc.sleep(100)
|
||||||
# Shut down playlist monitoring
|
# Shut down playlist monitoring
|
||||||
if kodi_playlist_monitor:
|
if playlist_monitor:
|
||||||
kodi_playlist_monitor.stop()
|
playlist_monitor.stop()
|
||||||
# doUtils could still have a session open due to interrupted sync
|
# doUtils could still have a session open due to interrupted sync
|
||||||
try:
|
try:
|
||||||
DU().stopSession()
|
DU().stopSession()
|
||||||
|
|
|
@ -139,9 +139,9 @@ class Playlist_Object(PlaylistObjectBaseclase):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise PlaylistError('Invalid path: %s' % path)
|
raise PlaylistError('Invalid path: %s' % path)
|
||||||
if path.startswith(v.PLAYLIST_PATH_VIDEO):
|
if path.startswith(v.PLAYLIST_PATH_VIDEO):
|
||||||
self.type = 'video'
|
self.type = v.KODI_TYPE_VIDEO_PLAYLIST
|
||||||
elif path.startswith(v.PLAYLIST_PATH_MUSIC):
|
elif path.startswith(v.PLAYLIST_PATH_MUSIC):
|
||||||
self.type = 'music'
|
self.type = v.KODI_TYPE_AUDIO_PLAYLIST
|
||||||
else:
|
else:
|
||||||
raise PlaylistError('Playlist type not supported: %s' % path)
|
raise PlaylistError('Playlist type not supported: %s' % path)
|
||||||
if not self.plex_name:
|
if not self.plex_name:
|
||||||
|
@ -670,6 +670,7 @@ def get_all_playlists():
|
||||||
try:
|
try:
|
||||||
xml.attrib
|
xml.attrib
|
||||||
except (AttributeError, TypeError):
|
except (AttributeError, TypeError):
|
||||||
|
LOG.error('Could not download a list of all playlists')
|
||||||
xml = None
|
xml = None
|
||||||
return xml
|
return xml
|
||||||
|
|
||||||
|
@ -682,9 +683,7 @@ def get_PMS_playlist(playlist, playlist_id=None):
|
||||||
Returns None if something went wrong
|
Returns None if something went wrong
|
||||||
"""
|
"""
|
||||||
playlist_id = playlist_id if playlist_id else playlist.id
|
playlist_id = playlist_id if playlist_id else playlist.id
|
||||||
xml = DU().downloadUrl(
|
xml = DU().downloadUrl("{server}/%ss/%s" % (playlist.kind, playlist_id))
|
||||||
"{server}/%ss/%s" % (playlist.kind, playlist_id),
|
|
||||||
headerOptions={'Accept': 'application/xml'})
|
|
||||||
try:
|
try:
|
||||||
xml.attrib['%sID' % playlist.kind]
|
xml.attrib['%sID' % playlist.kind]
|
||||||
except (AttributeError, KeyError):
|
except (AttributeError, KeyError):
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import re
|
|
||||||
|
from xbmcvfs import exists
|
||||||
|
|
||||||
import watchdog
|
import watchdog
|
||||||
import playlist_func as PL
|
import playlist_func as PL
|
||||||
|
@ -11,15 +12,12 @@ import kodidb_functions as kodidb
|
||||||
import plexdb_functions as plexdb
|
import plexdb_functions as plexdb
|
||||||
import utils
|
import utils
|
||||||
import variables as v
|
import variables as v
|
||||||
|
import state
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
LOG = getLogger("PLEX." + __name__)
|
LOG = getLogger("PLEX." + __name__)
|
||||||
|
|
||||||
# Our PKC playlists. Keys: ID [int] of plex DB table playlists. Values:
|
|
||||||
# playlist_func.Playlist_Object()
|
|
||||||
PLAYLISTS = {}
|
|
||||||
|
|
||||||
# Which playlist formates are supported by PKC?
|
# Which playlist formates are supported by PKC?
|
||||||
SUPPORTED_FILETYPES = (
|
SUPPORTED_FILETYPES = (
|
||||||
'm3u',
|
'm3u',
|
||||||
|
@ -28,12 +26,11 @@ SUPPORTED_FILETYPES = (
|
||||||
# 'cue',
|
# 'cue',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# m3u files do not have encoding specified
|
||||||
DEFAULT_ENCODING = sys.getdefaultencoding()
|
DEFAULT_ENCODING = sys.getdefaultencoding()
|
||||||
|
|
||||||
REGEX_PLEX_ID = re.compile(r'''plex_id=(\d+)''')
|
|
||||||
|
|
||||||
|
def create_plex_playlist(playlist=None, path=None):
|
||||||
def create_plex_playlist(playlist):
|
|
||||||
"""
|
"""
|
||||||
Adds the playlist [Playlist_Object] to the PMS. If playlist.plex_id is
|
Adds the playlist [Playlist_Object] to the PMS. If playlist.plex_id is
|
||||||
not None the existing Plex playlist will be overwritten; otherwise a new
|
not None the existing Plex playlist will be overwritten; otherwise a new
|
||||||
|
@ -43,6 +40,9 @@ def create_plex_playlist(playlist):
|
||||||
|
|
||||||
Returns None or raises PL.PlaylistError
|
Returns None or raises PL.PlaylistError
|
||||||
"""
|
"""
|
||||||
|
if not playlist:
|
||||||
|
playlist = PL.Playlist_Object()
|
||||||
|
playlist.kodi_path = path
|
||||||
LOG.info('Creating Plex playlist from Kodi file: %s', playlist.kodi_path)
|
LOG.info('Creating Plex playlist from Kodi file: %s', playlist.kodi_path)
|
||||||
plex_ids = _playlist_file_to_plex_ids(playlist)
|
plex_ids = _playlist_file_to_plex_ids(playlist)
|
||||||
for pos, plex_id in enumerate(plex_ids):
|
for pos, plex_id in enumerate(plex_ids):
|
||||||
|
@ -64,40 +64,50 @@ def delete_plex_playlist(playlist):
|
||||||
LOG.info('Deleting playlist %s from the PMS', playlist.plex_name)
|
LOG.info('Deleting playlist %s from the PMS', playlist.plex_name)
|
||||||
try:
|
try:
|
||||||
PL.delete_playlist_from_pms(playlist)
|
PL.delete_playlist_from_pms(playlist)
|
||||||
except PL.PlaylistError as err:
|
except PL.PlaylistError:
|
||||||
LOG.error('Could not delete Plex playlist: %s', err.strerror)
|
pass
|
||||||
else:
|
else:
|
||||||
update_plex_table(playlist, delete=True)
|
update_plex_table(playlist, delete=True)
|
||||||
|
|
||||||
|
|
||||||
def create_kodi_playlist(plex_id):
|
def create_kodi_playlist(plex_id=None):
|
||||||
"""
|
"""
|
||||||
Creates a new Kodi playlist file. Will also add (or modify an existing) Plex
|
Creates a new Kodi playlist file. Will also add (or modify an existing) Plex
|
||||||
playlist table entry.
|
playlist table entry.
|
||||||
Assumes that the Plex playlist is indeed new. A NEW Kodi playlist will be
|
Assumes that the Plex playlist is indeed new. A NEW Kodi playlist will be
|
||||||
created in any case (not replaced)
|
created in any case (not replaced). Thus make sure that the "same" playlist
|
||||||
|
is deleted from both disk and the Plex database.
|
||||||
Returns the playlist or raises PL.PlaylistError
|
Returns the playlist or raises PL.PlaylistError
|
||||||
"""
|
"""
|
||||||
LOG.info('Creating new Kodi playlist from Plex playlist %s', plex_id)
|
xml = PL.get_PMS_playlist(playlist_id=plex_id)
|
||||||
playlist = PL.Playlist_Object()
|
|
||||||
playlist.id = plex_id
|
|
||||||
xml = PL.get_PMS_playlist(playlist)
|
|
||||||
if not xml:
|
if not xml:
|
||||||
LOG.error('Could not create Kodi playlist for %s', plex_id)
|
LOG.error('Could not get Plex playlist %s', plex_id)
|
||||||
return
|
return
|
||||||
PL.get_playlist_details_from_xml(playlist, xml)
|
api = API(xml)
|
||||||
if xml.get('playlistType') == 'audio':
|
playlist = PL.Playlist_Object()
|
||||||
playlist.type = 'music'
|
playlist.id = api.plex_id()
|
||||||
elif xml.get('playlistType') == 'video':
|
playlist.type = v.KODI_PLAYLIST_TYPE_FROM_PLEX[api.playlist_type()]
|
||||||
playlist.type = 'video'
|
playlist.plex_name = api.title()
|
||||||
else:
|
playlist.plex_updatedat = api.updated_at()
|
||||||
raise RuntimeError('Plex playlist type unknown: %s'
|
LOG.info('Creating new Kodi playlist from Plex playlist %s: %s',
|
||||||
% xml.get('playlistType'))
|
playlist.id, playlist.plex_name)
|
||||||
playlist.plex_name = xml.get('title')
|
name = utils.valid_filename(playlist.plex_name)
|
||||||
name = utils.slugify(playlist.plex_name)
|
path = os.join(v.PLAYLIST_PATH, playlist.type, '%s.m3u8' % name)
|
||||||
playlist.kodi_path = os.join(v.PLAYLIST_PATH,
|
while exists(path) or playlist_object_from_db(path=path):
|
||||||
playlist.type,
|
# In case the Plex playlist names are not unique
|
||||||
'%s.m3u8' % name)
|
occurance = utils.REGEX_FILE_NUMBERING.search(path)
|
||||||
|
if not occurance:
|
||||||
|
path = os.join(v.PLAYLIST_PATH,
|
||||||
|
playlist.type,
|
||||||
|
'%s_01.m3u8' % name[:min(len(name), 247)])
|
||||||
|
else:
|
||||||
|
occurance = int(occurance.group(1)) + 1
|
||||||
|
path = os.join(v.PLAYLIST_PATH,
|
||||||
|
playlist.type,
|
||||||
|
'%s_%02d.m3u8' % (name[:min(len(name), 247)],
|
||||||
|
occurance))
|
||||||
|
LOG.debug('Kodi playlist path: %s', path)
|
||||||
|
playlist.kodi_path = path
|
||||||
# Derive filename close to Plex playlist name
|
# Derive filename close to Plex playlist name
|
||||||
_write_playlist_to_file(playlist, xml)
|
_write_playlist_to_file(playlist, xml)
|
||||||
update_plex_table(playlist, update_kodi_hash=True)
|
update_plex_table(playlist, update_kodi_hash=True)
|
||||||
|
@ -114,9 +124,10 @@ def delete_kodi_playlist(playlist):
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
os.remove(playlist.kodi_path)
|
os.remove(playlist.kodi_path)
|
||||||
except OSError 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.kodi_path, err.errno, err.strerror)
|
playlist.kodi_path, err.errno, err.strerror)
|
||||||
|
raise PL.PlaylistError('Could not delete %s' % playlist.kodi_path)
|
||||||
else:
|
else:
|
||||||
update_plex_table(playlist, delete=True)
|
update_plex_table(playlist, delete=True)
|
||||||
|
|
||||||
|
@ -181,7 +192,7 @@ def m3u_to_plex_ids(playlist):
|
||||||
playlist.kodi_path)
|
playlist.kodi_path)
|
||||||
text = text.decode('ISO-8859-1')
|
text = text.decode('ISO-8859-1')
|
||||||
for entry in _m3u_iterator(text):
|
for entry in _m3u_iterator(text):
|
||||||
plex_id = REGEX_PLEX_ID.search(entry)
|
plex_id = utils.REGEX_PLEX_ID.search(entry)
|
||||||
if plex_id:
|
if plex_id:
|
||||||
plex_id = plex_id.group(1)
|
plex_id = plex_id.group(1)
|
||||||
plex_ids.append(plex_id)
|
plex_ids.append(plex_id)
|
||||||
|
@ -210,8 +221,14 @@ def _write_playlist_to_file(playlist, xml):
|
||||||
% (api.runtime(), api.title(), api.path()))
|
% (api.runtime(), api.title(), api.path()))
|
||||||
text += '\n'
|
text += '\n'
|
||||||
text = text.encode('utf-8')
|
text = text.encode('utf-8')
|
||||||
with open(playlist.kodi_path, 'wb') as f:
|
try:
|
||||||
f.write(text)
|
with open(playlist.kodi_path, 'wb') as f:
|
||||||
|
f.write(text)
|
||||||
|
except (OSError, IOError) as err:
|
||||||
|
LOG.error('Could not write Kodi playlist file: %s', playlist.kodi_path)
|
||||||
|
LOG.error('Error message %s: %s', err.errno, err.strerror)
|
||||||
|
raise PL.PlaylistError('Cannot write Kodi playlist to path %s'
|
||||||
|
% playlist.kodi_path)
|
||||||
|
|
||||||
|
|
||||||
def change_plex_playlist_name(playlist, new_name):
|
def change_plex_playlist_name(playlist, new_name):
|
||||||
|
@ -233,24 +250,118 @@ def plex_id_from_playlist_path(path):
|
||||||
return plex_id
|
return plex_id
|
||||||
|
|
||||||
|
|
||||||
def playlist_object_from_db(path=None):
|
def playlist_object_from_db(path=None, kodi_hash=None, plex_id=None):
|
||||||
"""
|
"""
|
||||||
Returns the playlist as a Playlist_Object for path [unicode] from the Plex
|
Returns the playlist as a Playlist_Object for either the plex_id, path or
|
||||||
playlists table or None if not found.
|
kodi_hash. kodi_hash will be more reliable as it includes path and file
|
||||||
|
content.
|
||||||
"""
|
"""
|
||||||
playlist = PL.Playlist_Object()
|
playlist = PL.Playlist_Object()
|
||||||
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
|
playlist = plex_db.retrieve_playlist(playlist, plex_id, path, kodi_hash)
|
||||||
return playlist
|
return playlist
|
||||||
|
|
||||||
|
|
||||||
|
def _kodi_playlist_identical(xml_element):
|
||||||
|
"""
|
||||||
|
Feed with one playlist xml element from the PMS. Will return True if PKC
|
||||||
|
already synced this playlist, False if not or if the Play playlist has
|
||||||
|
changed in the meantime
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def full_sync():
|
def full_sync():
|
||||||
"""
|
"""
|
||||||
Full sync of playlists between Kodi and Plex. Returns True is successful,
|
Full sync of playlists between Kodi and Plex. Returns True is successful,
|
||||||
False otherwise
|
False otherwise
|
||||||
"""
|
"""
|
||||||
|
LOG.info('Starting playlist full sync')
|
||||||
|
# Get all Plex playlists
|
||||||
xml = PL.get_all_playlists()
|
xml = PL.get_all_playlists()
|
||||||
if not xml:
|
if not xml:
|
||||||
return False
|
return False
|
||||||
for entry in xml:
|
# For each playlist, check Plex database to see whether we already synced
|
||||||
|
# before. If yes, make sure that hashes are identical. If not, sync it.
|
||||||
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
|
old_plex_ids = plex_db.plex_ids_all_playlists()
|
||||||
|
for xml_playlist in xml:
|
||||||
|
api = API(xml_playlist)
|
||||||
|
if (not state.ENABLE_MUSIC and
|
||||||
|
api.playlist_type() == v.PLEX_TYPE_AUDIO_PLAYLIST):
|
||||||
|
continue
|
||||||
|
playlist = playlist_object_from_db(plex_id=api.plex_id())
|
||||||
|
try:
|
||||||
|
if not playlist:
|
||||||
|
LOG.debug('New Plex playlist %s discovered: %s',
|
||||||
|
api.plex_id(), api.title())
|
||||||
|
create_kodi_playlist(api.plex_id())
|
||||||
|
continue
|
||||||
|
elif playlist.plex_updatedat != api.updated_at():
|
||||||
|
LOG.debug('Detected changed Plex playlist %s: %s',
|
||||||
|
api.plex_id(), api.title())
|
||||||
|
delete_kodi_playlist(playlist)
|
||||||
|
create_kodi_playlist(api.plex_id())
|
||||||
|
except PL.PlaylistError:
|
||||||
|
LOG.info('Skipping playlist %s: %s', api.plex_id(), api.title())
|
||||||
|
old_plex_ids.remove(api.plex_id())
|
||||||
|
# Get rid of old Plex playlists that were deleted on the Plex side
|
||||||
|
for plex_id in old_plex_ids:
|
||||||
|
playlist = playlist_object_from_db(plex_id=api.plex_id())
|
||||||
|
if playlist:
|
||||||
|
LOG.debug('Removing outdated Plex playlist %s from %s',
|
||||||
|
playlist.plex_name, playlist.kodi_path)
|
||||||
|
try:
|
||||||
|
delete_kodi_playlist(playlist)
|
||||||
|
except PL.PlaylistError:
|
||||||
|
pass
|
||||||
|
# Look at all supported Kodi playlists. Check whether they are in the DB.
|
||||||
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
|
old_kodi_hashes = plex_db.kodi_hashes_all_playlists()
|
||||||
|
master_paths = [v.PLAYLIST_PATH_VIDEO]
|
||||||
|
if state.ENABLE_MUSIC:
|
||||||
|
master_paths.append(v.PLAYLIST_PATH_MUSIC)
|
||||||
|
for master_path in master_paths:
|
||||||
|
for root, _, files in os.walk(master_path):
|
||||||
|
for file in files:
|
||||||
|
try:
|
||||||
|
extension = file.rsplit('.', 1)[1]
|
||||||
|
except IndexError:
|
||||||
|
continue
|
||||||
|
if extension not in SUPPORTED_FILETYPES:
|
||||||
|
continue
|
||||||
|
path = os.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)
|
||||||
|
if playlist:
|
||||||
|
# Nothing changed at all - neither path nor content
|
||||||
|
old_kodi_hashes.remove(kodi_hash)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if playlist_2:
|
||||||
|
LOG.debug('Changed Kodi playlist %s detected: %s',
|
||||||
|
playlist_2.plex_name, path)
|
||||||
|
playlist = PL.Playlist_Object()
|
||||||
|
playlist.id = playlist_2.id
|
||||||
|
playlist.kodi_path = path
|
||||||
|
playlist.plex_name = playlist_2.plex_name
|
||||||
|
delete_plex_playlist(playlist_2)
|
||||||
|
create_plex_playlist(playlist)
|
||||||
|
else:
|
||||||
|
LOG.debug('New Kodi playlist detected: %s', path)
|
||||||
|
# Make sure that we delete any playlist with other hash
|
||||||
|
create_plex_playlist(path=path)
|
||||||
|
except PL.PlaylistError:
|
||||||
|
LOG.info('Skipping Kodi playlist %s', path)
|
||||||
|
for kodi_hash in old_kodi_hashes:
|
||||||
|
playlist = playlist_object_from_db(kodi_hash=kodi_hash)
|
||||||
|
if playlist:
|
||||||
|
try:
|
||||||
|
delete_plex_playlist(playlist)
|
||||||
|
except PL.PlaylistError:
|
||||||
|
pass
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class PlaylistEventhandler(watchdog.events.FileSystemEventHandler):
|
class PlaylistEventhandler(watchdog.events.FileSystemEventHandler):
|
||||||
|
|
|
@ -406,6 +406,60 @@ class Plex_DB_Functions():
|
||||||
plex_id = None
|
plex_id = None
|
||||||
return plex_id
|
return plex_id
|
||||||
|
|
||||||
|
def plex_ids_all_playlists(self):
|
||||||
|
"""
|
||||||
|
Returns a list of all Plex ids of playlists.
|
||||||
|
"""
|
||||||
|
answ = []
|
||||||
|
self.plexcursor.execute('SELECT plex_id FROM playlists')
|
||||||
|
for entry in self.plexcursor.fetchall():
|
||||||
|
answ.append(entry[0])
|
||||||
|
return answ
|
||||||
|
|
||||||
|
def kodi_hashes_all_playlists(self):
|
||||||
|
"""
|
||||||
|
Returns a list of all Kodi hashes of playlists.
|
||||||
|
"""
|
||||||
|
answ = []
|
||||||
|
self.plexcursor.execute('SELECT kodi_hash FROM playlists')
|
||||||
|
for entry in self.plexcursor.fetchall():
|
||||||
|
answ.append(entry[0])
|
||||||
|
return answ
|
||||||
|
|
||||||
|
def retrieve_playlist(self, playlist, plex_id=None, path=None,
|
||||||
|
kodi_hash=None):
|
||||||
|
"""
|
||||||
|
Returns a complete Playlist_Object (empty one passed in via playlist)
|
||||||
|
for the entry with plex_id. Or None if not found
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
SELECT plex_id, plex_name, plex_updatedat, kodi_path, kodi_type,
|
||||||
|
kodi_hash
|
||||||
|
FROM playlists
|
||||||
|
WHERE %s = ?
|
||||||
|
LIMIT 1
|
||||||
|
'''
|
||||||
|
if plex_id:
|
||||||
|
query = query % 'plex_id'
|
||||||
|
var = plex_id
|
||||||
|
elif kodi_hash:
|
||||||
|
query = query % 'kodi_hash'
|
||||||
|
var = kodi_hash
|
||||||
|
else:
|
||||||
|
query = query % 'kodi_path'
|
||||||
|
var = path
|
||||||
|
self.plexcursor.execute(query, (var, ))
|
||||||
|
answ = self.plexcursor.fetchone()
|
||||||
|
if not answ:
|
||||||
|
return
|
||||||
|
playlist.plex_id = answ[0]
|
||||||
|
playlist.plex_name = answ[1]
|
||||||
|
playlist.plex_updatedat = answ[2]
|
||||||
|
playlist.kodi_path = answ[3]
|
||||||
|
playlist.kodi_type = answ[4]
|
||||||
|
playlist.kodi_hash = answ[5]
|
||||||
|
return playlist
|
||||||
|
|
||||||
def insert_playlist_entry(self, playlist):
|
def insert_playlist_entry(self, playlist):
|
||||||
"""
|
"""
|
||||||
Inserts or modifies an existing entry in the Plex playlists table.
|
Inserts or modifies an existing entry in the Plex playlists table.
|
||||||
|
|
|
@ -4,6 +4,7 @@ Various functions and decorators for PKC
|
||||||
"""
|
"""
|
||||||
###############################################################################
|
###############################################################################
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
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
|
||||||
|
@ -13,11 +14,11 @@ 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 os.path import join
|
|
||||||
from os import remove, walk, makedirs
|
|
||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
from urllib import quote_plus
|
from urllib import quote_plus
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import re
|
||||||
|
import unicodedata
|
||||||
|
|
||||||
import xbmc
|
import xbmc
|
||||||
import xbmcaddon
|
import xbmcaddon
|
||||||
|
@ -35,6 +36,10 @@ 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)
|
||||||
|
|
||||||
|
REGEX_PLEX_ID = re.compile(r'''plex_id=(\d+)''')
|
||||||
|
REGEX_FILE_NUMBERING = re.compile(r'''_(\d+)\.\w+$''')
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Main methods
|
# Main methods
|
||||||
|
|
||||||
|
@ -111,7 +116,7 @@ def exists_dir(path):
|
||||||
if v.KODIVERSION >= 17:
|
if v.KODIVERSION >= 17:
|
||||||
answ = exists(try_encode(path))
|
answ = exists(try_encode(path))
|
||||||
else:
|
else:
|
||||||
dummyfile = join(try_decode(path), 'dummyfile.txt')
|
dummyfile = os.path.join(try_decode(path), 'dummyfile.txt')
|
||||||
try:
|
try:
|
||||||
with open(dummyfile, 'w') as filer:
|
with open(dummyfile, 'w') as filer:
|
||||||
filer.write('text')
|
filer.write('text')
|
||||||
|
@ -285,6 +290,39 @@ def slugify(text):
|
||||||
return unicode(normalize('NFKD', text).encode('ascii', 'ignore'))
|
return unicode(normalize('NFKD', text).encode('ascii', 'ignore'))
|
||||||
|
|
||||||
|
|
||||||
|
def valid_filename(text):
|
||||||
|
"""
|
||||||
|
Return a valid filename after passing it in [unicode].
|
||||||
|
"""
|
||||||
|
# Get rid of all whitespace except a normal space
|
||||||
|
text = re.sub(r'(?! )\s', '', text)
|
||||||
|
# ASCII characters 0 to 31 (non-printable, just in case)
|
||||||
|
text = re.sub(u'[\x00-\x1f]', '', text)
|
||||||
|
if v.PLATFORM == 'Windows':
|
||||||
|
# Whitespace at the end of the filename is illegal
|
||||||
|
text = text.strip()
|
||||||
|
# Dot at the end of a filename is illegal
|
||||||
|
text = re.sub(r'\.+$', '', text)
|
||||||
|
# Illegal Windows characters
|
||||||
|
text = re.sub(r'[/\\:*?"<>|\^]', '', text)
|
||||||
|
elif v.PLATFORM == 'MacOSX':
|
||||||
|
# Colon is illegal
|
||||||
|
text = re.sub(r':', '', text)
|
||||||
|
# Files cannot begin with a dot
|
||||||
|
text = re.sub(r'^\.+', '', 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 4 chars for
|
||||||
|
# filename extension and 1 dot to separate the extension)
|
||||||
|
text = text[:min(len(text), 250)]
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def escape_html(string):
|
def escape_html(string):
|
||||||
"""
|
"""
|
||||||
Escapes the following:
|
Escapes the following:
|
||||||
|
@ -478,7 +516,7 @@ def reset():
|
||||||
addon = xbmcaddon.Addon()
|
addon = xbmcaddon.Addon()
|
||||||
addondir = try_decode(xbmc.translatePath(addon.getAddonInfo('profile')))
|
addondir = try_decode(xbmc.translatePath(addon.getAddonInfo('profile')))
|
||||||
LOG.info("Deleting: settings.xml")
|
LOG.info("Deleting: settings.xml")
|
||||||
remove("%ssettings.xml" % addondir)
|
os.remove("%ssettings.xml" % addondir)
|
||||||
reboot_kodi()
|
reboot_kodi()
|
||||||
|
|
||||||
|
|
||||||
|
@ -630,9 +668,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 = join(v.KODI_PROFILE, filename)
|
self.path = os.path.join(v.KODI_PROFILE, filename)
|
||||||
else:
|
else:
|
||||||
self.path = join(path, filename)
|
self.path = os.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
|
||||||
|
@ -929,13 +967,13 @@ def playlist_xsp(mediatype, tagname, viewid, viewtype="", delete=False):
|
||||||
# Create the playlist directory
|
# Create the playlist directory
|
||||||
if not exists(try_encode(path)):
|
if not exists(try_encode(path)):
|
||||||
LOG.info("Creating directory: %s", path)
|
LOG.info("Creating directory: %s", path)
|
||||||
makedirs(path)
|
os.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 exists(try_encode(xsppath)):
|
||||||
LOG.info('Path %s does exist', xsppath)
|
LOG.info('Path %s does exist', xsppath)
|
||||||
if delete:
|
if delete:
|
||||||
remove(xsppath)
|
os.remove(xsppath)
|
||||||
LOG.info("Successfully removed playlist: %s.", tagname)
|
LOG.info("Successfully removed playlist: %s.", tagname)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -966,29 +1004,32 @@ def delete_playlists():
|
||||||
Clean up the playlists
|
Clean up the playlists
|
||||||
"""
|
"""
|
||||||
path = try_decode(xbmc.translatePath("special://profile/playlists/video/"))
|
path = try_decode(xbmc.translatePath("special://profile/playlists/video/"))
|
||||||
for root, _, files in walk(path):
|
for root, _, files in os.walk(path):
|
||||||
for file in files:
|
for file in files:
|
||||||
if file.startswith('Plex'):
|
if file.startswith('Plex'):
|
||||||
remove(join(root, file))
|
os.remove(os.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 = try_decode(xbmc.translatePath("special://profile/library/video/"))
|
||||||
for root, dirs, _ in walk(path):
|
for root, dirs, _ in os.walk(path):
|
||||||
for directory in dirs:
|
for directory in dirs:
|
||||||
if directory.startswith('Plex-'):
|
if directory.startswith('Plex-'):
|
||||||
rmtree(join(root, directory))
|
rmtree(os.path.join(root, directory))
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def generate_file_md5(path):
|
def generate_file_md5(path):
|
||||||
"""
|
"""
|
||||||
Generates the md5 hash value for the file located at path [unicode]
|
Generates the md5 hash value for the file located at path [unicode].
|
||||||
|
The hash includes the path and is thus different for the same file for
|
||||||
|
different filenames.
|
||||||
Returns a unique string containing only hexadecimal digits
|
Returns a unique string containing only hexadecimal digits
|
||||||
"""
|
"""
|
||||||
m = hashlib.md5()
|
m = hashlib.md5()
|
||||||
|
m.update(path.encode('utf-8'))
|
||||||
with open(path, 'rb') as f:
|
with open(path, 'rb') as f:
|
||||||
while True:
|
while True:
|
||||||
piece = f.read(32768)
|
piece = f.read(32768)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from os.path import join
|
import os
|
||||||
|
|
||||||
import xbmc
|
import xbmc
|
||||||
from xbmcaddon import Addon
|
from xbmcaddon import Addon
|
||||||
|
@ -41,10 +41,6 @@ KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1)
|
||||||
KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
|
KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
|
||||||
KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion')
|
KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion')
|
||||||
KODI_PROFILE = try_decode(xbmc.translatePath("special://profile"))
|
KODI_PROFILE = try_decode(xbmc.translatePath("special://profile"))
|
||||||
PLAYLIST_PATH = join(KODI_PROFILE, 'playlist')
|
|
||||||
PLAYLIST_PATH_MIXED = join(PLAYLIST_PATH, 'mixed')
|
|
||||||
PLAYLIST_PATH_VIDEO = join(PLAYLIST_PATH, 'video')
|
|
||||||
PLAYLIST_PATH_MUSIC = join(PLAYLIST_PATH, 'music')
|
|
||||||
|
|
||||||
if xbmc.getCondVisibility('system.platform.osx'):
|
if xbmc.getCondVisibility('system.platform.osx'):
|
||||||
PLATFORM = "MacOSX"
|
PLATFORM = "MacOSX"
|
||||||
|
@ -127,6 +123,21 @@ 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
|
||||||
|
|
||||||
|
# Playlist stuff
|
||||||
|
PLAYLIST_PATH = os.path.join(KODI_PROFILE, 'playlist')
|
||||||
|
PLAYLIST_PATH_MIXED = os.path.join(PLAYLIST_PATH, 'mixed')
|
||||||
|
PLAYLIST_PATH_VIDEO = os.path.join(PLAYLIST_PATH, 'video')
|
||||||
|
PLAYLIST_PATH_MUSIC = os.path.join(PLAYLIST_PATH, 'music')
|
||||||
|
|
||||||
|
PLEX_TYPE_AUDIO_PLAYLIST = 'audio'
|
||||||
|
PLEX_TYPE_VIDEO_PLAYLIST = 'video'
|
||||||
|
KODI_TYPE_AUDIO_PLAYLIST = 'music'
|
||||||
|
KODI_TYPE_VIDEO_PLAYLIST = 'video'
|
||||||
|
KODI_PLAYLIST_TYPE_FROM_PLEX = {
|
||||||
|
PLEX_TYPE_AUDIO_PLAYLIST: KODI_TYPE_AUDIO_PLAYLIST,
|
||||||
|
PLEX_TYPE_VIDEO_PLAYLIST: KODI_TYPE_VIDEO_PLAYLIST
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# All the Plex types as communicated in the PMS xml replies
|
# All the Plex types as communicated in the PMS xml replies
|
||||||
PLEX_TYPE_VIDEO = 'video'
|
PLEX_TYPE_VIDEO = 'video'
|
||||||
|
|
Loading…
Reference in a new issue