commit
b5e13d0ab6
12 changed files with 119 additions and 63 deletions
|
@ -1,5 +1,5 @@
|
|||
[![stable version](https://img.shields.io/badge/stable_version-2.9.1-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip)
|
||||
[![beta version](https://img.shields.io/badge/beta_version-2.9.1-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip)
|
||||
[![stable version](https://img.shields.io/badge/stable_version-2.9.3-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip)
|
||||
[![beta version](https://img.shields.io/badge/beta_version-2.9.3-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip)
|
||||
|
||||
[![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation)
|
||||
[![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq)
|
||||
|
|
18
addon.xml
18
addon.xml
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.9.1" provider-name="croneter">
|
||||
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.9.3" provider-name="croneter">
|
||||
<requires>
|
||||
<import addon="xbmc.python" version="2.1.0"/>
|
||||
<import addon="script.module.requests" version="2.9.1" />
|
||||
|
@ -83,7 +83,21 @@
|
|||
<summary lang="lt_LT">Natūralioji „Plex“ integracija į „Kodi“</summary>
|
||||
<description lang="lt_LT">Prijunkite „Kodi“ prie „Plex Medija Serverio“. Šiame papildinyje daroma prielaida, kad valdote visus savo vaizdo įrašus naudodami „Plex“ (ir nė vieno su „Kodi“). Galite prarasti jau saugomus „Kodi“ vaizdo įrašų ir muzikos duomenų bazių duomenis (kadangi šis papildinys juos tiesiogiai pakeičia). Naudokite savo pačių rizika!</description>
|
||||
<disclaimer lang="lt_LT">Naudokite savo pačių rizika</disclaimer>
|
||||
<news>version 2.9.1:
|
||||
<news>version 2.9.3:
|
||||
- version 2.9.2 for everyone
|
||||
|
||||
version 2.9.2 (beta only):
|
||||
- Fix Plex Companion casting from iOS and Android
|
||||
- Faster sync of playlists
|
||||
- Sync playlists immediately after synching new/changed items and show an info dialog
|
||||
- Fix potential playlist sync issues if there is a dot in the playlist name
|
||||
- Correctly detect whether we already synched a Kodi playlist
|
||||
- Remove obsolete check if path is indeed in unicode
|
||||
- Add unicode representation to Playlist() class
|
||||
- Separate function to wipe all synched Plex playlists
|
||||
- Less logging when comparing PKC versions
|
||||
|
||||
version 2.9.1:
|
||||
- Fix On Deck and Recently Added Episodes for shows not appending showname and season and episode number
|
||||
|
||||
version 2.9.0:
|
||||
|
|
|
@ -1,3 +1,17 @@
|
|||
version 2.9.3:
|
||||
- version 2.9.2 for everyone
|
||||
|
||||
version 2.9.2 (beta only):
|
||||
- Fix Plex Companion casting from iOS and Android
|
||||
- Faster sync of playlists
|
||||
- Sync playlists immediately after synching new/changed items and show an info dialog
|
||||
- Fix potential playlist sync issues if there is a dot in the playlist name
|
||||
- Correctly detect whether we already synched a Kodi playlist
|
||||
- Remove obsolete check if path is indeed in unicode
|
||||
- Add unicode representation to Playlist() class
|
||||
- Separate function to wipe all synched Plex playlists
|
||||
- Less logging when comparing PKC versions
|
||||
|
||||
version 2.9.1:
|
||||
- Fix On Deck and Recently Added Episodes for shows not appending showname and season and episode number
|
||||
|
||||
|
|
|
@ -334,15 +334,27 @@ class FullSync(common.fullsync_mixin):
|
|||
plexdb.update_section_last_sync(section.section_id,
|
||||
self.current_sync)
|
||||
common.update_kodi_library(video=True, music=True)
|
||||
|
||||
# Sync Plex playlists to Kodi and vice-versa
|
||||
if common.PLAYLIST_SYNC_ENABLED:
|
||||
if self.show_dialog:
|
||||
if self.dialog:
|
||||
self.dialog.close()
|
||||
self.dialog = xbmcgui.DialogProgressBG()
|
||||
# "Synching playlists"
|
||||
self.dialog.create(utils.lang(39715))
|
||||
if not playlists.full_sync():
|
||||
return False
|
||||
|
||||
# SYNC PLAYSTATE of ALL items (otherwise we won't pick up on items that
|
||||
# were set to unwatched). Also mark all items on the PMS to be able
|
||||
# to delete the ones still in Kodi
|
||||
LOG.info('Start synching playstate and userdata for every item')
|
||||
# In order to not delete all your songs again
|
||||
if app.SYNC.enable_music:
|
||||
kinds.extend([
|
||||
(v.PLEX_TYPE_SONG, v.PLEX_TYPE_ARTIST, itemtypes.Song, True),
|
||||
])
|
||||
# SYNC PLAYSTATE of ALL items (otherwise we won't pick up on items that
|
||||
# were set to unwatched). Also mark all items on the PMS to be able
|
||||
# to delete the ones still in Kodi
|
||||
LOG.info('Start synching playstate and userdata for every item')
|
||||
# Make sure we're not showing an item's title in the sync dialog
|
||||
self.title = ''
|
||||
self.threader.shutdown()
|
||||
|
@ -428,13 +440,6 @@ class FullSync(common.fullsync_mixin):
|
|||
if self.isCanceled() or not self.full_library_sync():
|
||||
self.successful = False
|
||||
return
|
||||
if common.PLAYLIST_SYNC_ENABLED:
|
||||
if self.dialog:
|
||||
self.dialog.close()
|
||||
self.dialog = xbmcgui.DialogProgressBG()
|
||||
self.dialog.create(utils.lang(39715))
|
||||
if not playlists.full_sync():
|
||||
self.successful = False
|
||||
finally:
|
||||
common.update_kodi_library(video=True, music=True)
|
||||
if self.dialog:
|
||||
|
|
|
@ -57,4 +57,9 @@ def check_migration():
|
|||
sections.clear_window_vars()
|
||||
sections.delete_videonode_files()
|
||||
|
||||
if not utils.compare_version(last_migration, '2.9.3'):
|
||||
LOG.info('Migrating to version 2.9.2')
|
||||
# Re-sync all playlists to Kodi
|
||||
utils.wipe_synched_playlists()
|
||||
|
||||
utils.settings('last_migrated_PKC_version', value=v.ADDON_VERSION)
|
||||
|
|
|
@ -14,7 +14,8 @@
|
|||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from .common import Playlist, PlaylistError, PlaylistObserver
|
||||
from .common import Playlist, PlaylistError, PlaylistObserver, \
|
||||
kodi_playlist_hash
|
||||
from . import pms, db, kodi_pl, plex_pl
|
||||
|
||||
from ..watchdog import events
|
||||
|
@ -221,7 +222,7 @@ def _full_sync():
|
|||
pass
|
||||
if not sync_kodi_playlist(path):
|
||||
continue
|
||||
kodi_hash = utils.generate_file_md5(path)
|
||||
kodi_hash = kodi_playlist_hash(path)
|
||||
playlist = db.get_playlist(path=path)
|
||||
if playlist and playlist.kodi_hash == kodi_hash:
|
||||
continue
|
||||
|
@ -387,7 +388,7 @@ class PlaylistEventhandler(events.FileSystemEventHandler):
|
|||
def on_created(self, event):
|
||||
LOG.debug('on_created: %s', event.src_path)
|
||||
old_playlist = db.get_playlist(path=event.src_path)
|
||||
kodi_hash = utils.generate_file_md5(event.src_path)
|
||||
kodi_hash = kodi_playlist_hash(event.src_path)
|
||||
if old_playlist and old_playlist.kodi_hash == kodi_hash:
|
||||
LOG.debug('Playlist already in DB - skipping')
|
||||
return
|
||||
|
@ -406,7 +407,7 @@ class PlaylistEventhandler(events.FileSystemEventHandler):
|
|||
def on_modified(self, event):
|
||||
LOG.debug('on_modified: %s', event.src_path)
|
||||
old_playlist = db.get_playlist(path=event.src_path)
|
||||
kodi_hash = utils.generate_file_md5(event.src_path)
|
||||
kodi_hash = kodi_playlist_hash(event.src_path)
|
||||
if old_playlist and old_playlist.kodi_hash == kodi_hash:
|
||||
LOG.debug('Nothing modified, playlist already in DB - skipping')
|
||||
return
|
||||
|
@ -425,7 +426,7 @@ class PlaylistEventhandler(events.FileSystemEventHandler):
|
|||
|
||||
def on_moved(self, event):
|
||||
LOG.debug('on_moved: %s to %s', event.src_path, event.dest_path)
|
||||
kodi_hash = utils.generate_file_md5(event.dest_path)
|
||||
kodi_hash = kodi_playlist_hash(event.dest_path)
|
||||
# First check whether we don't already have destination playlist in
|
||||
# our DB. Just in case....
|
||||
old_playlist = db.get_playlist(path=event.dest_path)
|
||||
|
|
|
@ -4,6 +4,8 @@ from __future__ import absolute_import, division, unicode_literals
|
|||
from logging import getLogger
|
||||
import Queue
|
||||
import time
|
||||
import os
|
||||
import hashlib
|
||||
|
||||
from ..watchdog import events
|
||||
from ..watchdog.observers import Observer
|
||||
|
@ -63,7 +65,7 @@ class Playlist(object):
|
|||
self.kodi_type = None
|
||||
self.kodi_hash = None
|
||||
|
||||
def __repr__(self):
|
||||
def __unicode__(self):
|
||||
return ("{{"
|
||||
"'plex_id': {self.plex_id}, "
|
||||
"'plex_name': '{self.plex_name}', "
|
||||
|
@ -74,6 +76,9 @@ class Playlist(object):
|
|||
"'kodi_hash': '{self.kodi_hash}'"
|
||||
"}}").format(self=self)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__unicode__().encode('utf-8')
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
|
@ -101,11 +106,9 @@ class Playlist(object):
|
|||
|
||||
@kodi_path.setter
|
||||
def kodi_path(self, path):
|
||||
if not isinstance(path, unicode):
|
||||
raise RuntimeError('Path not in unicode: %s' % path)
|
||||
f = path_ops.path.basename(path)
|
||||
try:
|
||||
self.kodi_filename, self.kodi_extension = f.split('.', 1)
|
||||
self.kodi_filename, self.kodi_extension = f.rsplit('.', 1)
|
||||
except ValueError:
|
||||
LOG.error('Trying to set invalid path: %s', path)
|
||||
raise PlaylistError('Invalid path: %s' % path)
|
||||
|
@ -121,6 +124,22 @@ class Playlist(object):
|
|||
self._kodi_path = path
|
||||
|
||||
|
||||
def kodi_playlist_hash(path):
|
||||
"""
|
||||
Returns a md5 hash [unicode] using os.stat() st_size and st_mtime for the
|
||||
playlist located at path [unicode]
|
||||
(size of file in bytes and time of most recent content modification)
|
||||
|
||||
There are probably way more efficient ways out there to do this
|
||||
"""
|
||||
stat = os.stat(path_ops.encode_path(path))
|
||||
# stat.st_size is of type long; stat.st_mtime is of type float - hash both
|
||||
m = hashlib.md5()
|
||||
m.update(repr(stat.st_size))
|
||||
m.update(repr(stat.st_mtime))
|
||||
return m.hexdigest().decode('utf-8')
|
||||
|
||||
|
||||
class PlaylistQueue(OrderedSetQueue):
|
||||
"""
|
||||
OrderedSetQueue that drops all directory events immediately
|
||||
|
|
|
@ -7,7 +7,7 @@ from __future__ import absolute_import, division, unicode_literals
|
|||
from logging import getLogger
|
||||
import re
|
||||
|
||||
from .common import Playlist, PlaylistError
|
||||
from .common import Playlist, PlaylistError, kodi_playlist_hash
|
||||
from . import db, pms
|
||||
|
||||
from ..plex_api import API
|
||||
|
@ -71,7 +71,7 @@ def create(plex_id):
|
|||
except Exception:
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.remove(playlist.kodi_path)
|
||||
raise
|
||||
playlist.kodi_hash = utils.generate_file_md5(path)
|
||||
playlist.kodi_hash = kodi_playlist_hash(path)
|
||||
db.update_playlist(playlist)
|
||||
LOG.debug('Created Kodi playlist based on Plex playlist: %s', playlist)
|
||||
|
||||
|
|
|
@ -30,8 +30,8 @@ def create(playlist):
|
|||
if not plex_ids:
|
||||
LOG.warning('No Plex ids found for playlist %s', playlist)
|
||||
raise PlaylistError
|
||||
IGNORE_PLEX_PLAYLIST_CHANGE.append(playlist.plex_id)
|
||||
pms.add_items(playlist, plex_ids)
|
||||
IGNORE_PLEX_PLAYLIST_CHANGE.append(playlist.plex_id)
|
||||
db.update_playlist(playlist)
|
||||
LOG.debug('Done creating Plex playlist %s', playlist)
|
||||
|
||||
|
|
|
@ -167,7 +167,7 @@ class PlexCompanion(backgroundthread.KillableThread):
|
|||
repeat=query.get('repeat'),
|
||||
offset=data.get('offset'),
|
||||
transient_token=data.get('token'),
|
||||
key=key)
|
||||
start_plex_id=key)
|
||||
|
||||
@staticmethod
|
||||
def _process_streams(data):
|
||||
|
|
|
@ -311,12 +311,16 @@ def initialize():
|
|||
plexdb.cursor.execute(cmd)
|
||||
|
||||
|
||||
def wipe():
|
||||
def wipe(table=None):
|
||||
"""
|
||||
Completely resets the Plex database
|
||||
Completely resets the Plex database.
|
||||
If a table [unicode] name is provided, only that table will be dropped
|
||||
"""
|
||||
with PlexDBBase() as plexdb:
|
||||
plexdb.cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table'")
|
||||
tables = [i[0] for i in plexdb.cursor.fetchall()]
|
||||
if table:
|
||||
tables = [table]
|
||||
else:
|
||||
plexdb.cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table'")
|
||||
tables = [i[0] for i in plexdb.cursor.fetchall()]
|
||||
for table in tables:
|
||||
plexdb.cursor.execute('DROP table IF EXISTS %s' % table)
|
||||
|
|
|
@ -17,7 +17,6 @@ import xml.etree.ElementTree as etree
|
|||
from . import defused_etree
|
||||
from xml.etree.ElementTree import ParseError
|
||||
from functools import wraps
|
||||
import hashlib
|
||||
import re
|
||||
import gc
|
||||
try:
|
||||
|
@ -539,6 +538,29 @@ def create_kodi_db_indicees():
|
|||
conn.close()
|
||||
|
||||
|
||||
def wipe_synched_playlists():
|
||||
"""
|
||||
Deletes all synched playlist files on the Kodi side; resets the Plex table
|
||||
listing all synched Plex playlists
|
||||
"""
|
||||
from . import plex_db
|
||||
try:
|
||||
with plex_db.PlexDB() as plexdb:
|
||||
plexdb.cursor.execute('SELECT kodi_path FROM playlists')
|
||||
playlist_paths = [x[0] for x in plexdb.cursor]
|
||||
except OperationalError:
|
||||
# Plex DB completely empty yet
|
||||
playlist_paths = []
|
||||
for path in playlist_paths:
|
||||
try:
|
||||
path_ops.remove(path)
|
||||
LOG.info('Removed playlist %s', path)
|
||||
except (OSError, IOError):
|
||||
LOG.warn('Could not remove playlist %s', path)
|
||||
# Now wipe our database
|
||||
plex_db.wipe(table='playlists')
|
||||
|
||||
|
||||
def wipe_database(reboot=True):
|
||||
"""
|
||||
Deletes all Plex playlists as well as video nodes, then clears Kodi as well
|
||||
|
@ -550,8 +572,8 @@ def wipe_database(reboot=True):
|
|||
from . import kodi_db, plex_db
|
||||
# Clean up the playlists and video nodes
|
||||
delete_files()
|
||||
# First get the paths to all synced playlists
|
||||
playlist_paths = []
|
||||
# Wipe all synched playlists
|
||||
wipe_synched_playlists()
|
||||
try:
|
||||
with plex_db.PlexDB() as plexdb:
|
||||
if plexdb.songs_have_been_synced():
|
||||
|
@ -560,22 +582,12 @@ def wipe_database(reboot=True):
|
|||
else:
|
||||
LOG.info('No music has been synced in the past - not wiping')
|
||||
music = False
|
||||
plexdb.cursor.execute('SELECT kodi_path FROM playlists')
|
||||
for entry in plexdb.cursor:
|
||||
playlist_paths.append(entry[0])
|
||||
except OperationalError:
|
||||
# Plex DB completely empty yet. Wipe existing Kodi music only if we
|
||||
# expect to sync Plex music
|
||||
music = settings('enableMusic') == 'true'
|
||||
kodi_db.wipe_dbs(music)
|
||||
plex_db.wipe()
|
||||
# Delete all synced playlists
|
||||
for path in playlist_paths:
|
||||
try:
|
||||
path_ops.remove(path)
|
||||
LOG.debug('Removed playlist %s', path)
|
||||
except (OSError, IOError):
|
||||
LOG.warn('Could not remove playlist %s', path)
|
||||
|
||||
LOG.info("Resetting all cached artwork.")
|
||||
# Remove all cached artwork
|
||||
|
@ -639,7 +651,6 @@ def compare_version(current, minimum):
|
|||
|
||||
Input strings: e.g. "1.2.3"; always with Major, Minor and Patch!
|
||||
"""
|
||||
LOG.info("current DB: %s minimum DB: %s", current, minimum)
|
||||
try:
|
||||
curr_major, curr_minor, curr_patch = current.split(".")
|
||||
except ValueError:
|
||||
|
@ -931,23 +942,6 @@ class XmlKodiSetting(object):
|
|||
return element
|
||||
|
||||
|
||||
def generate_file_md5(path):
|
||||
"""
|
||||
Generates the md5 hash value for the file located at path [unicode].
|
||||
The hash does not include the path and filename and is thus identical for
|
||||
a file that was moved/changed name.
|
||||
Returns a unique unicode containing only hexadecimal digits
|
||||
"""
|
||||
m = hashlib.md5()
|
||||
with open(path_ops.encode_path(path), 'rb') as f:
|
||||
while True:
|
||||
piece = f.read(32768)
|
||||
if not piece:
|
||||
break
|
||||
m.update(piece)
|
||||
return m.hexdigest().decode('utf-8')
|
||||
|
||||
|
||||
def process_method_on_list(method_to_run, items):
|
||||
"""
|
||||
helper method that processes a method on each item with pooling if the
|
||||
|
|
Loading…
Reference in a new issue