2018-04-28 17:12:29 +10:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
from logging import getLogger
|
2018-07-06 17:01:46 +10:00
|
|
|
import Queue
|
|
|
|
import xbmc
|
2018-04-28 17:12:29 +10:00
|
|
|
|
2018-07-05 20:59:13 +10:00
|
|
|
from .watchdog import events
|
2018-06-22 03:24:37 +10:00
|
|
|
from .watchdog.observers import Observer
|
2018-07-06 17:01:46 +10:00
|
|
|
from .watchdog.utils.bricks import OrderedSetQueue
|
2018-06-22 03:24:37 +10:00
|
|
|
from . import playlist_func as PL
|
|
|
|
from .plex_api import API
|
|
|
|
from . import kodidb_functions as kodidb
|
|
|
|
from . import plexdb_functions as plexdb
|
|
|
|
from . import utils
|
2018-06-24 02:25:18 +10:00
|
|
|
from . import path_ops
|
2018-06-22 03:24:37 +10:00
|
|
|
from . import variables as v
|
|
|
|
from . import state
|
2018-04-28 17:12:29 +10:00
|
|
|
|
|
|
|
###############################################################################
|
|
|
|
|
2018-06-22 03:24:37 +10:00
|
|
|
LOG = getLogger('PLEX.playlists')
|
2018-05-03 01:45:31 +10:00
|
|
|
|
2018-07-06 17:01:46 +10:00
|
|
|
# Safety margin for playlist filesystem operations
|
2018-07-07 05:48:07 +10:00
|
|
|
FILESYSTEM_TIMEOUT = 1
|
2018-07-06 17:01:46 +10:00
|
|
|
# These filesystem events are considered similar
|
|
|
|
SIMILAR_EVENTS = (events.EVENT_TYPE_CREATED, events.EVENT_TYPE_MODIFIED)
|
|
|
|
|
2018-04-28 17:12:29 +10:00
|
|
|
# Which playlist formates are supported by PKC?
|
|
|
|
SUPPORTED_FILETYPES = (
|
|
|
|
'm3u',
|
2018-05-02 01:59:51 +10:00
|
|
|
# 'm3u8'
|
2018-04-28 17:12:29 +10:00
|
|
|
# 'pls',
|
|
|
|
# 'cue',
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2018-05-03 00:46:54 +10:00
|
|
|
def create_plex_playlist(playlist):
|
2018-04-28 17:12:29 +10:00
|
|
|
"""
|
2018-05-02 00:31:03 +10:00
|
|
|
Adds the playlist [Playlist_Object] to the PMS. If playlist.id is
|
2018-04-28 17:12:29 +10:00
|
|
|
not None the existing Plex playlist will be overwritten; otherwise a new
|
|
|
|
playlist will be generated and stored accordingly in the playlist object.
|
|
|
|
Will also add (or modify an existing) Plex playlist table entry.
|
2018-05-03 00:46:54 +10:00
|
|
|
Make sure that playlist.kodi_hash is set!
|
2018-04-28 17:12:29 +10:00
|
|
|
Returns None or raises PL.PlaylistError
|
|
|
|
"""
|
2018-05-03 22:02:00 +10:00
|
|
|
LOG.debug('Creating Plex playlist from Kodi file: %s', playlist)
|
2018-04-28 17:12:29 +10:00
|
|
|
plex_ids = _playlist_file_to_plex_ids(playlist)
|
2018-05-02 17:33:37 +10:00
|
|
|
if not plex_ids:
|
2018-05-03 22:02:00 +10:00
|
|
|
LOG.info('No Plex ids found for playlist %s', playlist)
|
2018-05-02 17:33:37 +10:00
|
|
|
raise PL.PlaylistError
|
2018-04-28 17:12:29 +10:00
|
|
|
for pos, plex_id in enumerate(plex_ids):
|
2018-05-02 23:44:54 +10:00
|
|
|
try:
|
|
|
|
if pos == 0 or not playlist.id:
|
|
|
|
PL.init_plex_playlist(playlist, plex_id)
|
|
|
|
else:
|
2018-05-02 23:50:26 +10:00
|
|
|
PL.add_item_to_plex_playlist(playlist, plex_id=plex_id)
|
2018-05-02 23:44:54 +10:00
|
|
|
except PL.PlaylistError:
|
|
|
|
continue
|
2018-05-03 00:46:54 +10:00
|
|
|
update_plex_table(playlist)
|
2018-05-03 22:02:00 +10:00
|
|
|
LOG.debug('Done creating Plex playlist %s', playlist)
|
2018-04-28 17:12:29 +10:00
|
|
|
|
|
|
|
|
|
|
|
def delete_plex_playlist(playlist):
|
|
|
|
"""
|
|
|
|
Removes the playlist [Playlist_Object] from the PMS. Will also delete the
|
|
|
|
entry in the Plex playlist table.
|
|
|
|
Returns None or raises PL.PlaylistError
|
|
|
|
"""
|
2018-07-03 04:23:16 +10:00
|
|
|
if (state.SYNC_SPECIFIC_KODI_PLAYLISTS and
|
|
|
|
not sync_kodi_playlist(playlist.kodi_path)):
|
|
|
|
# We might have already synced this playlist BEFORE user chose to only
|
|
|
|
# sync specific playlists. Let's NOT delete all those playlists.
|
|
|
|
# However, delete it from our database of synced playlists.
|
|
|
|
LOG.debug('Not deleting playlist since user chose not to sync: %s',
|
|
|
|
playlist)
|
|
|
|
else:
|
|
|
|
LOG.debug('Deleting playlist from PMS: %s', playlist)
|
|
|
|
PL.delete_playlist_from_pms(playlist)
|
2018-05-03 02:14:33 +10:00
|
|
|
update_plex_table(playlist, delete=True)
|
2018-04-28 17:12:29 +10:00
|
|
|
|
|
|
|
|
2018-05-02 01:09:58 +10:00
|
|
|
def create_kodi_playlist(plex_id=None, updated_at=None):
|
2018-04-28 17:12:29 +10:00
|
|
|
"""
|
|
|
|
Creates a new Kodi playlist file. Will also add (or modify an existing) Plex
|
|
|
|
playlist table entry.
|
|
|
|
Assumes that the Plex playlist is indeed new. A NEW Kodi playlist will be
|
2018-05-01 22:48:49 +10:00
|
|
|
created in any case (not replaced). Thus make sure that the "same" playlist
|
|
|
|
is deleted from both disk and the Plex database.
|
2018-04-28 17:12:29 +10:00
|
|
|
Returns the playlist or raises PL.PlaylistError
|
2018-07-03 04:23:16 +10:00
|
|
|
|
|
|
|
Be aware that user settings will be checked whether this Plex playlist
|
|
|
|
should actually indeed be synced
|
2018-04-28 17:12:29 +10:00
|
|
|
"""
|
2018-05-01 22:59:57 +10:00
|
|
|
xml = PL.get_PMS_playlist(PL.Playlist_Object(), playlist_id=plex_id)
|
2018-05-01 23:24:58 +10:00
|
|
|
if xml is None:
|
2018-05-01 22:48:49 +10:00
|
|
|
LOG.error('Could not get Plex playlist %s', plex_id)
|
2018-05-03 16:24:41 +10:00
|
|
|
raise PL.PlaylistError('Could not get Plex playlist %s' % plex_id)
|
2018-05-02 00:48:31 +10:00
|
|
|
api = API(xml)
|
2018-07-03 04:23:16 +10:00
|
|
|
if state.SYNC_SPECIFIC_PLEX_PLAYLISTS:
|
2018-07-04 15:46:30 +10:00
|
|
|
prefix = utils.settings('syncSpecificPlexPlaylistsPrefix').lower()
|
|
|
|
if api.title() and not api.title().lower().startswith(prefix):
|
2018-07-03 04:23:16 +10:00
|
|
|
LOG.debug('User chose to not sync playlist %s', api.title())
|
|
|
|
return
|
2018-05-01 22:48:49 +10:00
|
|
|
playlist = PL.Playlist_Object()
|
|
|
|
playlist.id = api.plex_id()
|
2018-05-02 00:44:40 +10:00
|
|
|
playlist.type = v.KODI_PLAYLIST_TYPE_FROM_PLEX[api.playlist_type()]
|
2018-05-03 16:01:26 +10:00
|
|
|
if not state.ENABLE_MUSIC and playlist.type == v.KODI_PLAYLIST_TYPE_AUDIO:
|
|
|
|
return
|
2018-05-01 22:48:49 +10:00
|
|
|
playlist.plex_name = api.title()
|
2018-05-02 01:09:58 +10:00
|
|
|
playlist.plex_updatedat = updated_at
|
2018-05-03 22:02:00 +10:00
|
|
|
LOG.debug('Creating new Kodi playlist from Plex playlist: %s', playlist)
|
2018-05-01 22:48:49 +10:00
|
|
|
name = utils.valid_filename(playlist.plex_name)
|
2018-06-24 02:25:18 +10:00
|
|
|
path = path_ops.path.join(v.PLAYLIST_PATH, playlist.type, '%s.m3u' % name)
|
|
|
|
while path_ops.exists(path) or playlist_object_from_db(path=path):
|
2018-05-01 22:48:49 +10:00
|
|
|
# In case the Plex playlist names are not unique
|
|
|
|
occurance = utils.REGEX_FILE_NUMBERING.search(path)
|
|
|
|
if not occurance:
|
2018-06-24 02:25:18 +10:00
|
|
|
path = path_ops.path.join(v.PLAYLIST_PATH,
|
|
|
|
playlist.type,
|
|
|
|
'%s_01.m3u' % name[:min(len(name), 248)])
|
2018-05-01 22:48:49 +10:00
|
|
|
else:
|
|
|
|
occurance = int(occurance.group(1)) + 1
|
2018-06-24 02:25:18 +10:00
|
|
|
path = path_ops.path.join(v.PLAYLIST_PATH,
|
|
|
|
playlist.type,
|
|
|
|
'%s_%02d.m3u' % (name[:min(len(name),
|
|
|
|
248)],
|
|
|
|
occurance))
|
2018-05-01 22:48:49 +10:00
|
|
|
LOG.debug('Kodi playlist path: %s', path)
|
|
|
|
playlist.kodi_path = path
|
2018-04-28 17:12:29 +10:00
|
|
|
# Derive filename close to Plex playlist name
|
|
|
|
_write_playlist_to_file(playlist, xml)
|
2018-05-03 01:55:27 +10:00
|
|
|
playlist.kodi_hash = utils.generate_file_md5(path)
|
|
|
|
update_plex_table(playlist)
|
2018-05-03 22:02:00 +10:00
|
|
|
LOG.debug('Created Kodi playlist based on Plex playlist: %s', playlist)
|
2018-04-28 17:12:29 +10:00
|
|
|
|
|
|
|
|
|
|
|
def delete_kodi_playlist(playlist):
|
|
|
|
"""
|
|
|
|
Removes the corresponding Kodi file for playlist [Playlist_Object] from
|
|
|
|
disk. Be sure that playlist.kodi_path is set. Will also delete the entry in
|
|
|
|
the Plex playlist table.
|
|
|
|
Returns None or raises PL.PlaylistError
|
|
|
|
"""
|
2018-07-03 04:23:16 +10:00
|
|
|
if not sync_kodi_playlist(playlist.kodi_path):
|
|
|
|
LOG.debug('Do not delete since we should not sync playlist %s',
|
|
|
|
playlist)
|
2018-04-28 17:12:29 +10:00
|
|
|
else:
|
2018-07-03 04:23:16 +10:00
|
|
|
try:
|
|
|
|
path_ops.remove(playlist.kodi_path)
|
|
|
|
except (OSError, IOError) as err:
|
|
|
|
LOG.error('Could not delete Kodi playlist file %s. Error:\n%s: %s',
|
|
|
|
playlist, err.errno, err.strerror)
|
|
|
|
raise PL.PlaylistError('Could not delete %s' % playlist.kodi_path)
|
|
|
|
update_plex_table(playlist, delete=True)
|
2018-04-28 17:12:29 +10:00
|
|
|
|
|
|
|
|
2018-05-03 01:55:27 +10:00
|
|
|
def update_plex_table(playlist, delete=False):
|
2018-04-28 17:12:29 +10:00
|
|
|
"""
|
|
|
|
Assumes that all sync operations are over. Takes playlist [Playlist_Object]
|
|
|
|
and creates/updates the corresponding Plex playlists table entry
|
|
|
|
|
|
|
|
Pass delete=True to delete the playlist entry
|
|
|
|
"""
|
|
|
|
if delete:
|
|
|
|
with plexdb.Get_Plex_DB() as plex_db:
|
|
|
|
plex_db.delete_playlist_entry(playlist)
|
|
|
|
return
|
|
|
|
with plexdb.Get_Plex_DB() as plex_db:
|
|
|
|
plex_db.insert_playlist_entry(playlist)
|
|
|
|
|
|
|
|
|
|
|
|
def _playlist_file_to_plex_ids(playlist):
|
|
|
|
"""
|
|
|
|
Takes the playlist file located at path [unicode] and parses it.
|
|
|
|
Returns a list of plex_ids (str) or raises PL.PlaylistError if a single
|
|
|
|
item cannot be parsed from Kodi to Plex.
|
|
|
|
"""
|
2018-05-03 00:57:27 +10:00
|
|
|
if playlist.kodi_extension == 'm3u':
|
2018-04-28 17:12:29 +10:00
|
|
|
plex_ids = m3u_to_plex_ids(playlist)
|
2018-07-05 21:03:18 +10:00
|
|
|
else:
|
|
|
|
LOG.error('Unknown playlist extension: %s', playlist.kodi_extension)
|
|
|
|
raise PL.PlaylistError
|
2018-04-28 17:12:29 +10:00
|
|
|
return plex_ids
|
|
|
|
|
|
|
|
|
|
|
|
def _m3u_iterator(text):
|
|
|
|
"""
|
|
|
|
Yields e.g. plugin://plugin.video.plexkodiconnect.movies/?plex_id=xxx
|
|
|
|
"""
|
|
|
|
lines = iter(text.split('\n'))
|
|
|
|
for line in lines:
|
|
|
|
if line.startswith('#EXTINF:'):
|
|
|
|
yield next(lines).strip()
|
|
|
|
|
|
|
|
|
|
|
|
def m3u_to_plex_ids(playlist):
|
|
|
|
"""
|
2018-05-02 01:59:51 +10:00
|
|
|
Adapter to process *.m3u playlist files. Encoding is not uniform!
|
2018-04-28 17:12:29 +10:00
|
|
|
"""
|
2018-05-02 00:35:26 +10:00
|
|
|
plex_ids = list()
|
2018-06-24 02:25:18 +10:00
|
|
|
with open(path_ops.encode_path(playlist.kodi_path), 'rb') as f:
|
2018-04-28 17:12:29 +10:00
|
|
|
text = f.read()
|
|
|
|
try:
|
2018-06-24 02:25:18 +10:00
|
|
|
text = text.decode(v.M3U_ENCODING)
|
2018-04-28 17:12:29 +10:00
|
|
|
except UnicodeDecodeError:
|
2018-05-03 02:22:23 +10:00
|
|
|
LOG.warning('Fallback to ISO-8859-1 decoding for %s', playlist)
|
2018-04-28 17:12:29 +10:00
|
|
|
text = text.decode('ISO-8859-1')
|
|
|
|
for entry in _m3u_iterator(text):
|
2018-05-01 22:48:49 +10:00
|
|
|
plex_id = utils.REGEX_PLEX_ID.search(entry)
|
2018-04-28 17:12:29 +10:00
|
|
|
if plex_id:
|
|
|
|
plex_id = plex_id.group(1)
|
|
|
|
plex_ids.append(plex_id)
|
|
|
|
else:
|
|
|
|
# Add-on paths not working, try direct
|
|
|
|
kodi_id, kodi_type = kodidb.kodiid_from_filename(
|
2018-05-03 02:50:31 +10:00
|
|
|
entry, db_type=playlist.type)
|
2018-04-28 17:12:29 +10:00
|
|
|
if not kodi_id:
|
|
|
|
continue
|
|
|
|
with plexdb.Get_Plex_DB() as plex_db:
|
|
|
|
plex_id = plex_db.getItem_byKodiId(kodi_id, kodi_type)
|
|
|
|
if plex_id:
|
2018-05-03 02:50:31 +10:00
|
|
|
plex_ids.append(plex_id[0])
|
2018-04-28 17:12:29 +10:00
|
|
|
return plex_ids
|
|
|
|
|
|
|
|
|
|
|
|
def _write_playlist_to_file(playlist, xml):
|
|
|
|
"""
|
2018-05-02 01:59:51 +10:00
|
|
|
Feed with playlist [Playlist_Object]. Will write the playlist to a m3u file
|
2018-04-28 17:12:29 +10:00
|
|
|
Returns None or raises PL.PlaylistError
|
|
|
|
"""
|
2018-06-24 02:25:18 +10:00
|
|
|
text = '#EXTCPlayListM3U::M3U\n'
|
2018-04-28 17:12:29 +10:00
|
|
|
for element in xml:
|
|
|
|
api = API(element)
|
2018-06-24 18:47:35 +10:00
|
|
|
append_season_episode = False
|
|
|
|
if api.plex_type() == v.PLEX_TYPE_EPISODE:
|
|
|
|
_, show, season_id, episode_id = api.episode_data()
|
|
|
|
try:
|
|
|
|
season_id = int(season_id)
|
|
|
|
episode_id = int(episode_id)
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
append_season_episode = True
|
|
|
|
if append_season_episode:
|
|
|
|
text += ('#EXTINF:%s,%s S%.2dE%.2d - %s\n%s\n'
|
|
|
|
% (api.runtime(), show, season_id, episode_id,
|
|
|
|
api.title(), api.path()))
|
|
|
|
else:
|
|
|
|
# Only append the TV show name
|
|
|
|
text += ('#EXTINF:%s,%s - %s\n%s\n'
|
|
|
|
% (api.runtime(), show, api.title(), api.path()))
|
|
|
|
else:
|
|
|
|
text += ('#EXTINF:%s,%s\n%s\n'
|
|
|
|
% (api.runtime(), api.title(), api.path()))
|
2018-04-28 17:12:29 +10:00
|
|
|
text += '\n'
|
2018-06-24 02:25:18 +10:00
|
|
|
text = text.encode(v.M3U_ENCODING, 'strict')
|
2018-05-01 22:48:49 +10:00
|
|
|
try:
|
2018-06-24 02:25:18 +10:00
|
|
|
with open(path_ops.encode_path(playlist.kodi_path), 'wb') as f:
|
2018-05-01 22:48:49 +10:00
|
|
|
f.write(text)
|
|
|
|
except (OSError, IOError) as err:
|
2018-05-03 22:02:00 +10:00
|
|
|
LOG.error('Could not write Kodi playlist file: %s', playlist)
|
2018-05-01 22:48:49 +10:00
|
|
|
LOG.error('Error message %s: %s', err.errno, err.strerror)
|
2018-05-03 22:02:00 +10:00
|
|
|
raise PL.PlaylistError('Cannot write Kodi playlist to path for %s'
|
|
|
|
% playlist)
|
2018-04-28 17:12:29 +10:00
|
|
|
|
|
|
|
|
|
|
|
def change_plex_playlist_name(playlist, new_name):
|
|
|
|
"""
|
|
|
|
TODO - Renames the existing playlist with new_name [unicode]
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
def plex_id_from_playlist_path(path):
|
|
|
|
"""
|
|
|
|
Given the Kodi playlist path [unicode], this will return the Plex id [str]
|
|
|
|
or None
|
|
|
|
"""
|
|
|
|
with plexdb.Get_Plex_DB() as plex_db:
|
|
|
|
plex_id = plex_db.plex_id_from_playlist_path(path)
|
|
|
|
if not plex_id:
|
|
|
|
LOG.error('Could not find existing entry for playlist path %s', path)
|
|
|
|
return plex_id
|
|
|
|
|
|
|
|
|
2018-05-01 22:48:49 +10:00
|
|
|
def playlist_object_from_db(path=None, kodi_hash=None, plex_id=None):
|
2018-04-28 17:12:29 +10:00
|
|
|
"""
|
2018-05-01 22:48:49 +10:00
|
|
|
Returns the playlist as a Playlist_Object for either the plex_id, path or
|
|
|
|
kodi_hash. kodi_hash will be more reliable as it includes path and file
|
|
|
|
content.
|
2018-04-28 17:12:29 +10:00
|
|
|
"""
|
|
|
|
playlist = PL.Playlist_Object()
|
2018-05-01 22:48:49 +10:00
|
|
|
with plexdb.Get_Plex_DB() as plex_db:
|
|
|
|
playlist = plex_db.retrieve_playlist(playlist, plex_id, path, kodi_hash)
|
2018-04-28 17:12:29 +10:00
|
|
|
return playlist
|
|
|
|
|
|
|
|
|
2018-05-01 22:48:49 +10:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2018-06-24 02:25:18 +10:00
|
|
|
def process_websocket(plex_id, updated_at, status):
|
2018-05-03 01:27:35 +10:00
|
|
|
"""
|
|
|
|
Hit by librarysync to process websocket messages concerning playlists
|
|
|
|
"""
|
|
|
|
create = False
|
2018-06-22 04:43:39 +10:00
|
|
|
with state.LOCK_PLAYLISTS:
|
2018-06-24 02:25:18 +10:00
|
|
|
playlist = playlist_object_from_db(plex_id=plex_id)
|
2018-06-22 04:43:39 +10:00
|
|
|
try:
|
2018-06-24 02:25:18 +10:00
|
|
|
if playlist and status == 9:
|
2018-06-22 04:43:39 +10:00
|
|
|
LOG.debug('Plex deletion of playlist detected: %s', playlist)
|
|
|
|
delete_kodi_playlist(playlist)
|
|
|
|
elif playlist and playlist.plex_updatedat == updated_at:
|
|
|
|
LOG.debug('Playlist with id %s already synced: %s',
|
|
|
|
plex_id, playlist)
|
|
|
|
elif playlist:
|
|
|
|
LOG.debug('Change of Plex playlist detected: %s', playlist)
|
|
|
|
delete_kodi_playlist(playlist)
|
|
|
|
create = True
|
2018-06-24 02:25:18 +10:00
|
|
|
elif not playlist and not status == 9:
|
2018-06-22 04:43:39 +10:00
|
|
|
LOG.debug('Creation of new Plex playlist detected: %s',
|
|
|
|
plex_id)
|
|
|
|
create = True
|
|
|
|
# To the actual work
|
|
|
|
if create:
|
|
|
|
create_kodi_playlist(plex_id=plex_id, updated_at=updated_at)
|
|
|
|
except PL.PlaylistError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2018-04-28 17:12:29 +10:00
|
|
|
def full_sync():
|
|
|
|
"""
|
|
|
|
Full sync of playlists between Kodi and Plex. Returns True is successful,
|
|
|
|
False otherwise
|
|
|
|
"""
|
2018-07-06 00:38:06 +10:00
|
|
|
if not state.SYNC_PLAYLISTS:
|
|
|
|
LOG.debug('Not syncing playlists')
|
|
|
|
return True
|
2018-05-01 22:48:49 +10:00
|
|
|
LOG.info('Starting playlist full sync')
|
2018-06-22 04:43:39 +10:00
|
|
|
with state.LOCK_PLAYLISTS:
|
|
|
|
return _full_sync()
|
|
|
|
|
|
|
|
|
|
|
|
def _full_sync():
|
|
|
|
"""
|
|
|
|
Need to lock because we're messing with playlists
|
|
|
|
"""
|
2018-05-01 22:48:49 +10:00
|
|
|
# Get all Plex playlists
|
2018-04-28 17:12:29 +10:00
|
|
|
xml = PL.get_all_playlists()
|
2018-05-01 23:24:58 +10:00
|
|
|
if xml is None:
|
2018-04-28 17:12:29 +10:00
|
|
|
return False
|
2018-05-01 22:48:49 +10:00
|
|
|
# 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
|
2018-05-02 00:44:40 +10:00
|
|
|
api.playlist_type() == v.PLEX_TYPE_AUDIO_PLAYLIST):
|
2018-05-01 22:48:49 +10:00
|
|
|
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())
|
2018-05-02 01:09:58 +10:00
|
|
|
create_kodi_playlist(api.plex_id(), api.updated_at())
|
2018-05-01 22:48:49 +10:00
|
|
|
continue
|
|
|
|
elif playlist.plex_updatedat != api.updated_at():
|
|
|
|
LOG.debug('Detected changed Plex playlist %s: %s',
|
|
|
|
api.plex_id(), api.title())
|
2018-06-24 02:25:18 +10:00
|
|
|
if path_ops.exists(playlist.kodi_path):
|
2018-05-02 00:52:12 +10:00
|
|
|
delete_kodi_playlist(playlist)
|
|
|
|
else:
|
|
|
|
update_plex_table(playlist, delete=True)
|
2018-05-02 01:09:58 +10:00
|
|
|
create_kodi_playlist(api.plex_id(), api.updated_at())
|
2018-05-01 22:48:49 +10:00
|
|
|
except PL.PlaylistError:
|
|
|
|
LOG.info('Skipping playlist %s: %s', api.plex_id(), api.title())
|
2018-05-02 00:12:25 +10:00
|
|
|
try:
|
|
|
|
old_plex_ids.remove(api.plex_id())
|
|
|
|
except ValueError:
|
|
|
|
pass
|
2018-05-01 22:48:49 +10:00
|
|
|
# Get rid of old Plex playlists that were deleted on the Plex side
|
|
|
|
for plex_id in old_plex_ids:
|
2018-06-20 20:31:18 +10:00
|
|
|
playlist = playlist_object_from_db(plex_id=plex_id)
|
2018-05-01 22:48:49 +10:00
|
|
|
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:
|
2018-06-24 02:25:18 +10:00
|
|
|
for root, _, files in path_ops.walk(master_path):
|
2018-05-01 22:48:49 +10:00
|
|
|
for file in files:
|
|
|
|
try:
|
|
|
|
extension = file.rsplit('.', 1)[1]
|
|
|
|
except IndexError:
|
|
|
|
continue
|
|
|
|
if extension not in SUPPORTED_FILETYPES:
|
|
|
|
continue
|
2018-06-24 02:25:18 +10:00
|
|
|
path = path_ops.path.join(root, file)
|
2018-07-03 04:23:16 +10:00
|
|
|
if not sync_kodi_playlist(path):
|
|
|
|
continue
|
2018-05-01 22:48:49 +10:00
|
|
|
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:
|
2018-05-03 00:46:54 +10:00
|
|
|
playlist = PL.Playlist_Object()
|
|
|
|
playlist.kodi_path = path
|
|
|
|
playlist.kodi_hash = kodi_hash
|
2018-05-01 22:48:49 +10:00
|
|
|
if playlist_2:
|
|
|
|
LOG.debug('Changed Kodi playlist %s detected: %s',
|
|
|
|
playlist_2.plex_name, path)
|
|
|
|
playlist.id = playlist_2.id
|
|
|
|
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
|
2018-05-03 00:46:54 +10:00
|
|
|
create_plex_playlist(playlist)
|
2018-05-01 22:48:49 +10:00
|
|
|
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
|
2018-05-02 01:13:00 +10:00
|
|
|
LOG.info('Playlist full sync done')
|
2018-05-01 22:48:49 +10:00
|
|
|
return True
|
2018-04-28 17:12:29 +10:00
|
|
|
|
|
|
|
|
2018-07-03 04:23:16 +10:00
|
|
|
def sync_kodi_playlist(path):
|
|
|
|
"""
|
|
|
|
Returns True if we should sync this Kodi playlist with path [unicode] to
|
|
|
|
Plex based on the playlist file name and the user settings, False otherwise
|
|
|
|
"""
|
|
|
|
if not state.SYNC_SPECIFIC_KODI_PLAYLISTS:
|
|
|
|
return True
|
|
|
|
playlist = PL.Playlist_Object()
|
|
|
|
try:
|
|
|
|
playlist.kodi_path = path
|
|
|
|
except PL.PlaylistError:
|
|
|
|
pass
|
|
|
|
else:
|
2018-07-04 15:46:30 +10:00
|
|
|
prefix = utils.settings('syncSpecificKodiPlaylistsPrefix').lower()
|
|
|
|
if playlist.kodi_filename.lower().startswith(prefix):
|
2018-07-03 04:23:16 +10:00
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2018-07-05 20:59:13 +10:00
|
|
|
class PlaylistEventhandler(events.FileSystemEventHandler):
|
2018-04-28 17:12:29 +10:00
|
|
|
"""
|
|
|
|
PKC eventhandler to monitor Kodi playlists safed to disk
|
|
|
|
"""
|
2018-05-03 01:45:31 +10:00
|
|
|
def dispatch(self, event):
|
2018-04-28 17:12:29 +10:00
|
|
|
"""
|
2018-05-03 01:45:31 +10:00
|
|
|
Dispatches events to the appropriate methods.
|
|
|
|
|
|
|
|
:param event:
|
|
|
|
The event object representing the file system event.
|
|
|
|
:type event:
|
|
|
|
:class:`FileSystemEvent`
|
2018-04-28 17:12:29 +10:00
|
|
|
"""
|
2018-07-06 00:38:06 +10:00
|
|
|
if not state.SYNC_PLAYLISTS:
|
|
|
|
# Sync is deactivated
|
|
|
|
return
|
2018-04-28 17:12:29 +10:00
|
|
|
if event.is_directory:
|
|
|
|
# todo: take care of folder renames
|
2018-05-03 01:45:31 +10:00
|
|
|
return
|
2018-04-28 17:12:29 +10:00
|
|
|
try:
|
|
|
|
_, extension = event.src_path.rsplit('.', 1)
|
|
|
|
except ValueError:
|
2018-05-03 01:45:31 +10:00
|
|
|
return
|
2018-04-28 17:12:29 +10:00
|
|
|
if extension.lower() not in SUPPORTED_FILETYPES:
|
2018-05-03 01:45:31 +10:00
|
|
|
return
|
2018-04-28 17:12:29 +10:00
|
|
|
if event.src_path.startswith(v.PLAYLIST_PATH_MIXED):
|
2018-05-03 01:45:31 +10:00
|
|
|
return
|
2018-05-03 16:01:26 +10:00
|
|
|
if (not state.ENABLE_MUSIC and
|
|
|
|
event.src_path.startswith(v.PLAYLIST_PATH_MUSIC)):
|
|
|
|
return
|
2018-07-05 21:10:42 +10:00
|
|
|
path = event.dest_path if event.event_type == events.EVENT_TYPE_MOVED \
|
2018-07-03 04:23:16 +10:00
|
|
|
else event.src_path
|
|
|
|
if not sync_kodi_playlist(path):
|
|
|
|
return
|
2018-05-03 01:45:31 +10:00
|
|
|
_method_map = {
|
2018-07-05 20:59:13 +10:00
|
|
|
events.EVENT_TYPE_MODIFIED: self.on_modified,
|
|
|
|
events.EVENT_TYPE_MOVED: self.on_moved,
|
|
|
|
events.EVENT_TYPE_CREATED: self.on_created,
|
|
|
|
events.EVENT_TYPE_DELETED: self.on_deleted,
|
2018-05-03 01:45:31 +10:00
|
|
|
}
|
|
|
|
event_type = event.event_type
|
2018-06-22 03:24:37 +10:00
|
|
|
with state.LOCK_PLAYLISTS:
|
2018-05-03 01:45:31 +10:00
|
|
|
_method_map[event_type](event)
|
2018-04-28 17:12:29 +10:00
|
|
|
|
|
|
|
def on_created(self, event):
|
|
|
|
LOG.debug('on_created: %s', event.src_path)
|
2018-05-03 01:55:19 +10:00
|
|
|
old_playlist = playlist_object_from_db(path=event.src_path)
|
|
|
|
if old_playlist:
|
|
|
|
LOG.debug('Playlist already in DB - skipping')
|
|
|
|
return
|
2018-04-28 17:12:29 +10:00
|
|
|
playlist = PL.Playlist_Object()
|
|
|
|
playlist.kodi_path = event.src_path
|
2018-05-03 00:46:54 +10:00
|
|
|
playlist.kodi_hash = utils.generate_file_md5(event.src_path)
|
|
|
|
try:
|
|
|
|
create_plex_playlist(playlist)
|
|
|
|
except PL.PlaylistError:
|
|
|
|
pass
|
2018-04-28 17:12:29 +10:00
|
|
|
|
|
|
|
def on_deleted(self, event):
|
|
|
|
LOG.debug('on_deleted: %s', event.src_path)
|
2018-05-03 00:46:54 +10:00
|
|
|
playlist = playlist_object_from_db(path=event.src_path)
|
|
|
|
if not playlist:
|
|
|
|
LOG.error('Playlist not found in DB for path %s', event.src_path)
|
|
|
|
else:
|
|
|
|
delete_plex_playlist(playlist)
|
2018-04-28 17:12:29 +10:00
|
|
|
|
|
|
|
def on_modified(self, event):
|
|
|
|
LOG.debug('on_modified: %s', event.src_path)
|
2018-05-03 00:46:54 +10:00
|
|
|
old_playlist = playlist_object_from_db(path=event.src_path)
|
|
|
|
new_playlist = PL.Playlist_Object()
|
2018-05-03 01:59:06 +10:00
|
|
|
if old_playlist:
|
|
|
|
# Retain the name! Might've vom from Plex
|
|
|
|
new_playlist.plex_name = old_playlist.plex_name
|
2018-05-03 00:46:54 +10:00
|
|
|
new_playlist.kodi_path = event.src_path
|
|
|
|
new_playlist.kodi_hash = utils.generate_file_md5(event.src_path)
|
|
|
|
try:
|
|
|
|
if not old_playlist:
|
|
|
|
LOG.debug('Old playlist not found, creating a new one')
|
|
|
|
try:
|
|
|
|
create_plex_playlist(new_playlist)
|
|
|
|
except PL.PlaylistError:
|
|
|
|
pass
|
|
|
|
elif old_playlist.kodi_hash == new_playlist.kodi_hash:
|
|
|
|
LOG.debug('Old and new playlist are identical - nothing to do')
|
|
|
|
else:
|
|
|
|
delete_plex_playlist(old_playlist)
|
|
|
|
create_plex_playlist(new_playlist)
|
|
|
|
except PL.PlaylistError:
|
|
|
|
pass
|
2018-04-28 17:12:29 +10:00
|
|
|
|
|
|
|
def on_moved(self, event):
|
|
|
|
LOG.debug('on_moved: %s to %s', event.src_path, event.dest_path)
|
2018-05-03 00:46:54 +10:00
|
|
|
old_playlist = playlist_object_from_db(path=event.src_path)
|
|
|
|
if not old_playlist:
|
2018-07-06 01:55:52 +10:00
|
|
|
LOG.error('Did not have source path in the DB %s', event.src_path)
|
2018-05-03 00:46:54 +10:00
|
|
|
else:
|
|
|
|
delete_plex_playlist(old_playlist)
|
|
|
|
new_playlist = PL.Playlist_Object()
|
|
|
|
new_playlist.kodi_path = event.dest_path
|
|
|
|
new_playlist.kodi_hash = utils.generate_file_md5(event.dest_path)
|
|
|
|
try:
|
|
|
|
create_plex_playlist(new_playlist)
|
|
|
|
except PL.PlaylistError:
|
|
|
|
pass
|
2018-04-28 17:12:29 +10:00
|
|
|
|
|
|
|
|
2018-07-07 05:44:08 +10:00
|
|
|
class PlaylistQueue(OrderedSetQueue):
|
|
|
|
"""
|
|
|
|
OrderedSetQueue that drops all directory events immediately
|
|
|
|
"""
|
|
|
|
def _put(self, item):
|
|
|
|
if item[0].is_directory:
|
|
|
|
self.unfinished_tasks -= 1
|
|
|
|
elif item not in self._set_of_items:
|
|
|
|
Queue.Queue._put(self, item)
|
|
|
|
self._set_of_items.add(item)
|
|
|
|
else:
|
|
|
|
# `put` increments `unfinished_tasks` even if we did not put
|
|
|
|
# anything into the queue here
|
|
|
|
self.unfinished_tasks -= 1
|
|
|
|
|
|
|
|
|
2018-07-06 17:01:46 +10:00
|
|
|
class PlaylistObserver(Observer):
|
|
|
|
"""
|
|
|
|
PKC implementation, overriding the dispatcher. PKC will wait for the
|
|
|
|
duration timeout (in seconds) before dispatching. A new event will reset
|
|
|
|
the timer.
|
|
|
|
Creating and modifying will be regarded as equal.
|
|
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super(PlaylistObserver, self).__init__(*args, **kwargs)
|
|
|
|
# Drop the same events that get into the queue even if there are other
|
|
|
|
# events in between these similar events
|
2018-07-07 05:44:08 +10:00
|
|
|
self._event_queue = PlaylistQueue()
|
2018-07-06 17:01:46 +10:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _pkc_similar_events(event1, event2):
|
|
|
|
if event1 == event2:
|
|
|
|
return True
|
|
|
|
elif (event1.src_path == event2.src_path and
|
|
|
|
event1.event_type in SIMILAR_EVENTS and
|
|
|
|
event2.event_type in SIMILAR_EVENTS):
|
|
|
|
# Ignore a consecutive firing of created and modified events
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
def _dispatch_iterator(self, event_queue, timeout):
|
|
|
|
"""
|
|
|
|
This iterator will block for timeout (seconds) until an event is
|
|
|
|
received or raise Queue.Empty.
|
|
|
|
"""
|
|
|
|
event, watch = event_queue.get(block=True, timeout=timeout)
|
|
|
|
event_queue.task_done()
|
|
|
|
start = utils.unix_timestamp()
|
|
|
|
while utils.unix_timestamp() - start < timeout:
|
|
|
|
if state.STOP_PKC:
|
|
|
|
raise Queue.Empty
|
|
|
|
try:
|
|
|
|
new_event, new_watch = event_queue.get(block=False)
|
|
|
|
except Queue.Empty:
|
|
|
|
xbmc.sleep(200)
|
|
|
|
else:
|
|
|
|
event_queue.task_done()
|
|
|
|
start = utils.unix_timestamp()
|
|
|
|
if self._pkc_similar_events(new_event, event):
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
# At least on Windows, a dir modified event will be
|
|
|
|
# triggered once the writing process is done. Fine though
|
|
|
|
yield event, watch
|
|
|
|
event, watch = new_event, new_watch
|
|
|
|
yield event, watch
|
|
|
|
|
|
|
|
def dispatch_events(self, event_queue, timeout):
|
|
|
|
for event, watch in self._dispatch_iterator(event_queue, timeout):
|
|
|
|
with self._lock:
|
|
|
|
# To allow unschedule/stop and safe removal of event handlers
|
|
|
|
# within event handlers itself, check if the handler is still
|
|
|
|
# registered after every dispatch.
|
|
|
|
for handler in list(self._handlers.get(watch, [])):
|
|
|
|
if handler in self._handlers.get(watch, []):
|
|
|
|
handler.dispatch(event)
|
|
|
|
|
|
|
|
|
2018-04-28 17:12:29 +10:00
|
|
|
def kodi_playlist_monitor():
|
|
|
|
"""
|
|
|
|
Monitors the Kodi playlist folder special://profile/playlist for the user.
|
|
|
|
Will thus catch all changes on the Kodi side of things.
|
|
|
|
|
|
|
|
Returns an watchdog Observer instance. Be sure to use
|
|
|
|
observer.stop() (and maybe observer.join()) to shut down properly
|
|
|
|
"""
|
|
|
|
event_handler = PlaylistEventhandler()
|
2018-07-06 17:01:46 +10:00
|
|
|
observer = PlaylistObserver(timeout=FILESYSTEM_TIMEOUT)
|
2018-04-28 17:12:29 +10:00
|
|
|
observer.schedule(event_handler, v.PLAYLIST_PATH, recursive=True)
|
|
|
|
observer.start()
|
|
|
|
return observer
|