331 lines
11 KiB
Python
331 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
from logging import getLogger
|
|
import os
|
|
import sys
|
|
import re
|
|
|
|
import watchdog
|
|
import playlist_func as PL
|
|
from PlexAPI import API
|
|
import kodidb_functions as kodidb
|
|
import plexdb_functions as plexdb
|
|
import utils
|
|
import variables as v
|
|
|
|
###############################################################################
|
|
|
|
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?
|
|
SUPPORTED_FILETYPES = (
|
|
'm3u',
|
|
'm3u8'
|
|
# 'pls',
|
|
# 'cue',
|
|
)
|
|
|
|
DEFAULT_ENCODING = sys.getdefaultencoding()
|
|
|
|
REGEX_PLEX_ID = re.compile(r'''plex_id=(\d+)''')
|
|
|
|
|
|
def create_plex_playlist(playlist):
|
|
"""
|
|
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
|
|
playlist will be generated and stored accordingly in the playlist object.
|
|
|
|
Will also add (or modify an existing) Plex playlist table entry.
|
|
|
|
Returns None or raises PL.PlaylistError
|
|
"""
|
|
LOG.info('Creating Plex playlist from Kodi file: %s', playlist.kodi_path)
|
|
plex_ids = _playlist_file_to_plex_ids(playlist)
|
|
for pos, plex_id in enumerate(plex_ids):
|
|
if pos == 0:
|
|
PL.init_Plex_playlist(playlist, plex_id=plex_id)
|
|
else:
|
|
PL.add_item_to_PMS_playlist(playlist, pos, plex_id=plex_id)
|
|
update_plex_table(playlist, update_kodi_hash=True)
|
|
LOG.info('Done creating Plex %s playlist %s',
|
|
playlist.type, playlist.plex_name)
|
|
|
|
|
|
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
|
|
"""
|
|
LOG.info('Deleting playlist %s from the PMS', playlist.plex_name)
|
|
try:
|
|
PL.delete_playlist_from_pms(playlist)
|
|
except PL.PlaylistError as err:
|
|
LOG.error('Could not delete Plex playlist: %s', err.strerror)
|
|
else:
|
|
update_plex_table(playlist, delete=True)
|
|
|
|
|
|
def create_kodi_playlist(plex_id):
|
|
"""
|
|
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
|
|
created in any case (not replaced)
|
|
Returns the playlist or raises PL.PlaylistError
|
|
"""
|
|
LOG.info('Creating new Kodi playlist from Plex playlist %s', plex_id)
|
|
playlist = PL.Playlist_Object()
|
|
playlist.id = plex_id
|
|
xml = PL.get_PMS_playlist(playlist)
|
|
if not xml:
|
|
LOG.error('Could not create Kodi playlist for %s', plex_id)
|
|
return
|
|
PL.get_playlist_details_from_xml(playlist, xml)
|
|
if xml.get('playlistType') == 'audio':
|
|
playlist.type = 'music'
|
|
elif xml.get('playlistType') == 'video':
|
|
playlist.type = 'video'
|
|
else:
|
|
raise RuntimeError('Plex playlist type unknown: %s'
|
|
% xml.get('playlistType'))
|
|
playlist.plex_name = xml.get('title')
|
|
name = utils.slugify(playlist.plex_name)
|
|
playlist.kodi_path = os.join(v.PLAYLIST_PATH,
|
|
playlist.type,
|
|
'%s.m3u8' % name)
|
|
# Derive filename close to Plex playlist name
|
|
_write_playlist_to_file(playlist, xml)
|
|
update_plex_table(playlist, update_kodi_hash=True)
|
|
LOG.info('Created Kodi playlist %s based on Plex playlist %s',
|
|
playlist.kodi_filename, playlist.plex_name)
|
|
|
|
|
|
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
|
|
"""
|
|
try:
|
|
os.remove(playlist.kodi_path)
|
|
except OSError as err:
|
|
LOG.error('Could not delete Kodi playlist file %s. Error:\n %s: %s',
|
|
playlist.kodi_path, err.errno, err.strerror)
|
|
else:
|
|
update_plex_table(playlist, delete=True)
|
|
|
|
|
|
def update_plex_table(playlist, delete=False, new_path=None,
|
|
update_kodi_hash=False):
|
|
"""
|
|
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
|
|
if update_kodi_hash:
|
|
playlist.kodi_hash = utils.generate_file_md5(playlist.kodi_path)
|
|
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.
|
|
"""
|
|
if playlist.kodi_extension in ('m3u', 'm3u8'):
|
|
plex_ids = m3u_to_plex_ids(playlist)
|
|
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):
|
|
"""
|
|
Adapter to process *.m3u playlist files. Encoding is not uniform except for
|
|
m3u8 files!
|
|
"""
|
|
plex_ids = set()
|
|
with open(playlist.kodi_path, 'rb') as f:
|
|
text = f.read()
|
|
if playlist.kodi_extension == 'm3u8':
|
|
encoding = 'utf-8'
|
|
elif v.PLATFORM == 'Windows':
|
|
encoding = 'mbcs'
|
|
else:
|
|
encoding = DEFAULT_ENCODING
|
|
try:
|
|
text = text.decode(encoding)
|
|
except UnicodeDecodeError:
|
|
LOG.warning('Fallback to ISO-8859-1 decoding for %s',
|
|
playlist.kodi_path)
|
|
text = text.decode('ISO-8859-1')
|
|
for entry in _m3u_iterator(text):
|
|
plex_id = REGEX_PLEX_ID.search(entry)
|
|
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(
|
|
playlist.kodi_path, db_type=playlist.type)
|
|
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:
|
|
plex_ids.append(plex_id)
|
|
return plex_ids
|
|
|
|
|
|
def _write_playlist_to_file(playlist, xml):
|
|
"""
|
|
Feed with playlist [Playlist_Object]. Will write the playlist to a m3u8 file
|
|
Returns None or raises PL.PlaylistError
|
|
"""
|
|
text = u'#EXTCPlayListM3U::M3U\n'
|
|
for element in xml:
|
|
api = API(element)
|
|
text += (u'#EXTINF:%s,%s\n%s\n'
|
|
% (api.runtime(), api.title(), api.path()))
|
|
text += '\n'
|
|
text = text.encode('utf-8')
|
|
with open(playlist.kodi_path, 'wb') as f:
|
|
f.write(text)
|
|
|
|
|
|
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
|
|
|
|
|
|
def playlist_object_from_db(path=None):
|
|
"""
|
|
Returns the playlist as a Playlist_Object for path [unicode] from the Plex
|
|
playlists table or None if not found.
|
|
"""
|
|
playlist = PL.Playlist_Object()
|
|
return playlist
|
|
|
|
|
|
def full_sync():
|
|
"""
|
|
Full sync of playlists between Kodi and Plex. Returns True is successful,
|
|
False otherwise
|
|
"""
|
|
xml = PL.get_all_playlists()
|
|
if not xml:
|
|
return False
|
|
for entry in xml:
|
|
|
|
|
|
class PlaylistEventhandler(watchdog.events.FileSystemEventHandler):
|
|
"""
|
|
PKC eventhandler to monitor Kodi playlists safed to disk
|
|
"""
|
|
@staticmethod
|
|
def _event_relevant(event):
|
|
"""
|
|
Returns True if the event is relevant for PKC, False otherwise (e.g.
|
|
when a smart playlist *.xsp is considered)
|
|
"""
|
|
LOG.debug('event.is_directory: %s, event.src_path: %s',
|
|
event.is_directory, event.src_path)
|
|
if event.is_directory:
|
|
# todo: take care of folder renames
|
|
return False
|
|
try:
|
|
_, extension = event.src_path.rsplit('.', 1)
|
|
except ValueError:
|
|
return False
|
|
if extension.lower() not in SUPPORTED_FILETYPES:
|
|
return False
|
|
if event.src_path.startswith(v.PLAYLIST_PATH_MIXED):
|
|
return False
|
|
return True
|
|
|
|
def on_created(self, event):
|
|
if not self._event_relevant(event):
|
|
return
|
|
LOG.debug('on_created: %s', event.src_path)
|
|
playlist = PL.Playlist_Object()
|
|
playlist.kodi_path = event.src_path
|
|
create_plex_playlist(playlist)
|
|
|
|
def on_deleted(self, event):
|
|
if not self._event_relevant(event):
|
|
return
|
|
LOG.debug('on_deleted: %s', event.src_path)
|
|
playlist = PL.Playlist_Object()
|
|
playlist.kodi_path = event.src_path
|
|
delete_plex_playlist(playlist)
|
|
|
|
def on_modified(self, event):
|
|
if not self._event_relevant(event):
|
|
return
|
|
LOG.debug('on_modified: %s', event.src_path)
|
|
playlist = PL.Playlist_Object()
|
|
playlist.kodi_path = event.src_path
|
|
delete_plex_playlist(playlist)
|
|
create_plex_playlist(playlist)
|
|
|
|
def on_moved(self, event):
|
|
if not self._event_relevant(event):
|
|
return
|
|
LOG.debug('on_moved: %s to %s', event.src_path, event.dest_path)
|
|
playlist = PL.Playlist_Object()
|
|
playlist.id = plex_id_from_playlist_path(event.src_path)
|
|
if not playlist.id:
|
|
return
|
|
playlist.kodi_path = event.dest_path
|
|
change_plex_playlist_name(playlist, playlist.kodi_filename)
|
|
update_plex_table(playlist)
|
|
|
|
|
|
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()
|
|
observer = watchdog.observers.Observer()
|
|
observer.schedule(event_handler, v.PLAYLIST_PATH, recursive=True)
|
|
observer.start()
|
|
return observer
|