Merge pull request #1 from croneter/master

resynch to master
This commit is contained in:
piotrsmolinski 2016-09-05 02:24:49 +02:00 committed by GitHub
commit 34af90bef8
38 changed files with 4814 additions and 3970 deletions

109
README.md
View file

@ -1,88 +1,111 @@
###IMPORTANT###
# PlexKodiConnect (PKC)
**Combine the best frontend media player Kodi with the best multimedia backend server Plex**
1. If you post logs, your **Plex tokens** might be included. Be sure to double and tripple check for tokens before posting any logs anywhere.
2. **Compatibility**: PKC is currently not compatible with Kodi's Video Extras plugin. **Deactivate Video Extras** if trailers/movies start randomly playing.
PKC combines the best of Kodi - ultra smooth navigation, beautiful and highly customizable user interfaces and playback of any file under the sun, and the Plex Media Server to manage all your media without lifting a finger.
Have a look at [some screenshots](https://github.com/croneter/PlexKodiConnect/wiki/Some-PKC-Screenshots) to see what's possible.
### Download and Installation
[ ![Download](https://api.bintray.com/packages/croneter/PlexKodiConnect/PlexKodiConnect/images/download.svg) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip)
The easiest way to install PKC is via our PlexKodiConnect Kodi repository (we cannot use the official Kodi repository as PKC messes with Kodi's databases). See the [installation guideline on how to do this](https://github.com/croneter/PlexKodiConnect/wiki/Installation).
**Possibly UNSTABLE BETA version:** [ ![Download](https://api.bintray.com/packages/croneter/PlexKodiConnect_BETA/PlexKodiConnect_BETA/images/download.svg) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip)
### Donations
I'm not in any way affiliated with Plex. Thank you very much for a small donation via ko-fi.com and PayPal if you appreciate PKC.
**Full disclaimer:** I will see your name and address on my PayPal account. Rest assured that I will not share this with anyone.
[ ![Download](https://az743702.vo.msecnd.net/cdn/kofi1.png?v=a|alt=Buy Me a Coffee)](https://ko-fi.com/A8182EB)
### IMPORTANT NOTES
1. If your are using a **low CPU device like a Raspberry Pi or a CuBox**, PKC might be instable or crash during initial sync. Lower the number of threads in the [PKC settings under Sync Options](https://github.com/croneter/PlexKodiConnect/wiki/PKC-settings#sync-options): `Limit artwork cache threads: 5`
Don't forget to reboot Kodi after that.
2. If you post logs, your **Plex tokens** might be included. Be sure to double and tripple check for tokens before posting any logs anywhere by searching for `token`
3. **Compatibility**: PKC is currently not compatible with Kodi's Video Extras plugin. **Deactivate Video Extras** if trailers/movies start randomly playing.
### [Checkout the Wiki](https://github.com/croneter/PlexKodiConnect/wiki)
[The Wiki will hopefully answer all your questions](https://github.com/croneter/PlexKodiConnect/wiki)
### Welcome to PlexKodiConnect
**Connect your Plex Media Server to a Kodi Front End**
PlexKodiConnect combines the best of Kodi - ultra smooth navigation, beautiful UIs and playback of any file under the sun, and Plex to manage all your media without lifting a finger.
### Checkout the PKC Wiki
The [Wiki can be found here](https://github.com/croneter/PlexKodiConnect/wiki) and will hopefully answer all your questions.
**What does it do?**
### What does PKC do?
With other addons for Kodi there are a couple of issues:
- 3rd party addons such as NextAired, remote apps etc. won't work
- Slow speed: when browsing the data has to be retrieved from the server. Especially on slower devices this can take too much time.
- Slow speed: when browsing the data has to be retrieved from the server. Especially on slower devices this can take too much time
- You can only use special Kodi skins
- All kinds of workarounds are needed to get the best experience on Kodi clients
This addon synchronizes your media on your Plex server to the native Kodi database. Because we use the native Kodi database with this new approach the above limitations are gone!
- You can browse your media full speed, e.g. images are cached
PKC synchronizes your media from your Plex server to the native Kodi database. Because PKC uses the native Kodi database, the above limitations are gone!
- You can browse your media full speed, images are cached
- All other Kodi addons will be able to "see" your media, thinking it's normal Kodi stuff
- Use any Kodi skin you want!
**Installation in Kodi**
### What is currently supported?
Check out the [Wiki for installation instructions](https://github.com/croneter/PlexKodiConnect/wiki)
**What is currently supported?**
Currently these features are working:
- Movies and Home Videos
- TV Shows
PKC currently provides the following features:
- All Plex library types
+ Movies and Home Videos
+ TV Shows
+ Music
+ Pictures and Photos
- Different PKC interface languages:
+ English
+ German
+ More coming up
- [Plex Watch Later / Plex It!](https://support.plex.tv/hc/en-us/sections/200211783-Plex-It-)
- Full sync at first run, then periodic delta syncs every 60min (customizable)
- Instant watched state/resume status sync: This is a 2-way synchronisation. Any watched state or resume status will be instantly (within seconds) reflected to or from Kodi and the server
- [Plex Companion](https://support.plex.tv/hc/en-us/sections/200276908-Plex-Companion): fling Plex media (or anything else) from other Plex devices to PlexKodiConnect
- Play directly from network paths (e.g. "\\\\server\\Plex\\movie.mkv" or "smb://server/Plex/movie.mkv") instead of slow HTTP (e.g. "192.168.1.1:32400"). You have to setup all your Plex libraries to point to such network paths. Do have a look at [the wiki here](https://github.com/croneter/PlexKodiConnect/wiki/Direct-Paths)
- [Plex Transcoding](https://support.plex.tv/hc/en-us/articles/200250377-Transcoding-Media)
- Automatically download more artwork from [Fanart.tv](https://fanart.tv/), just like the Kodi addon [Artwork Downloader](http://kodi.wiki/view/Add-on:Artwork_Downloader)
+ Banners
+ Disc art
+ Clear logos
+ Landscapes
+ Clear art
+ Extra fanart backgrounds
- Automatically group movies into [movie sets](http://kodi.wiki/view/movie_sets)
- Direct play from network paths (e.g. "\\\\server\\Plex\\movie.mkv") instead of streaming from slow HTTP (e.g. "192.168.1.1:32400"). You have to setup all your Plex libraries to point to such network paths. Do have a look at [the wiki here](https://github.com/croneter/PlexKodiConnect/wiki/Direct-Paths)
**Known Issues:**
### Known Larger Issues
Solutions are unlikely due to the nature of these issues
- *Plex Music when using Addon paths instead of Native Direct Paths:* Kodi tries to scan every(!) single Plex song on startup. This leads to errors in the Kodi log file and potentially even crashes. (Plex puts each song in a "dedicated folder", e.g. 'http://192.168.1.1:32400/library/parts/749450/'. Kodi unsuccessfully tries to scan these folders)
- *Plex Music when using Addon paths instead of Native Direct Paths:* Kodi tries to scan every(!) single Plex song on startup. This leads to errors in the Kodi log file and potentially even crashes. See the [Github issue](https://github.com/croneter/PlexKodiConnect/issues/14) for more details
- *Plex Music when using Addon paths instead of Native Direct Paths:* You must have a static IP address for your Plex media server if you plan to use Plex Music features
- If something on the PMS has changed, this change is synced to Kodi. Hence if you rescan your entire library, a long PlexKodiConnect re-sync is triggered
- External Plex subtitles (in separate files, e.g. mymovie.srt) can be used, but it is impossible to label them correctly/tell what language they are in. However, this is not the case if you use direct paths
- If using Addon Paths: In the TV show video nodes On Deck and Recently Added, Kodi will not display the Episode Information screen if you push "i". This is a Kodi issue. It does work if you use Direct Paths
- If something on the PMS has changed, this change is synced to Kodi. Hence if you rescan your entire library, a long PlexKodiConnect re-sync is triggered. You can [change your PMS settings to avoid that](https://github.com/croneter/PlexKodiConnect/wiki/Configure-PKC-on-the-First-Run#deactivate-frequent-updates)
- External Plex subtitles (in separate files, e.g. mymovie.srt) can be used, but it is impossible to label them correctly and tell what language they are in. However, this is not the case if you use direct paths
*Background Sync:*
The Plex Server does not tell anyone of the following changes. Hence PKC cannot detect these changes instantly but will notice them on full/delta syncs.
The Plex Server does not tell anyone of the following changes. Hence PKC cannot detect these changes instantly but will notice them only on full/delta syncs (standard settings is every 60 minutes)
- Toggle the viewstate of an item to (un)watched outside of Kodi
- Changing details of an item, e.g. replacing a poster
However, some changes to individual items are instantly detected, e.g. if you match a yet unrecognized movie.
**Known Bugs:**
- Resume a video does not work yet with Plex Watch Later
### Issues being worked on
Have a look at the [Github Issues Page](https://github.com/croneter/PlexKodiConnect/issues).
**What could be in the pipeline for future development?**
### What could be in the pipeline for future development?
- Playlists
- Pictures
- Music Videos
- Automatic updates
- Simultaneously connecting to several PMS
- Deleting PMS items from Kodi
- TV Shows Theme Music (ultra-low prio)
**Important note about MySQL database in kodi**
### Important note about MySQL database in Kodi
The addon is not (and will not be) compatible with the MySQL database replacement in Kodi. In fact, PlexKodiConnect takes over the point of having a MySQL database because it acts as a "man in the middle" for your entire media library.
**Important note about user collections/nodes**
Plex has the ability to create custom libraries for your Media, such as having a separate folder for your "Kids Movies" etc. In Kodi this isn't supported, you just have "movies" or "tvshows". But... Kodi let's you create your own playlists and tags to get this same experience. During the sync the foldernode from the Plex server is added to the movies that are imported. In Kodi you can browse to Movie library --> tags and you will have a filtered result that points at your custom node. If you have a skin that let's you create any kind of shortcut on the homescreen you can simply create a shortcut to that tag. Another possibility is to create a "smart playlist" with Kodi and set it to show the content of a certain tag.
### Credits
**Credits**
- PlexKodiConnect shamelessly uses pretty much all the code of "Emby for Kodi" by the awesome Emby team (see https://github.com/MediaBrowser/plugin.video.emby). Thanks for sharing guys!!
- Plex Companion ("PlexBMC Helper") and other stuff was adapted from @Hippojay 's great work (see https://github.com/hippojay).
- The foundation of the Plex API is all iBaa's work (https://github.com/iBaa/PlexConnect).
- The foundation of the Plex API is all iBaa's work (https://github.com/iBaa/PlexConnect).

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.plexkodiconnect"
name="PlexKodiConnect"
version="1.1.4"
version="1.3.0"
provider-name="croneter">
<requires>
<import addon="xbmc.python" version="2.1.0"/>

View file

@ -1,3 +1,139 @@
version 1.3.0
- Compatibility with latest Kodi Krypton
- Complete redesign of Plex Companion playlist handling
- Improvements to Plex Companion daemon
- Try reducing strain on PMS for metadata
- Don't let PMS crash: download one item at a time
- Don't open PKC settings if PMS not found
- sync: dont force show "full library sync finished" (thanks @milaq)
- fix 'raspberry pi' spelling and capitalisation (thanks @milaq)
- Revert: New setting to lower the number of PMS items to cache
version 1.2.14 (beta only)
- New setting to lower the number of PMS items to cache. Hopefully fixes Wetek crashes
version 1.2.13 (beta only)
- Compatibility with latest Kodi Krypton
- fix 'raspberry pi' spelling and capitalisation (thanks @milaq)
version 1.2.12 (beta only)
- Complete redesign of playlist handling
- Improvements to Plex Companion daemon
- Try reducing strain on PMS for metadata
- Don't let PMS crash: download one item at a time
- sync: dont force show "full library sync finished" (thanks @milaq)
version 1.2.11
- Fix PKC not releasing connections to the PMS. Should fix memory, connection and PMS issues
- Fix TypeError for playlists
version 1.2.10
- Hotfix: Fix ValueError for playing certain files
version 1.2.9
- Don't let the PMS force scan the media when requesting PMS item metadata
- Improve detection of the need to transcode
- Increase (and enforce) a higher connection timeout
- Enable stream/media selection for direct play (e.g. if you have several files for the same movie item on the PMS)
version 1.2.8
- Fix PKC playstate updates for widgets on Krypton
- Let user choose to always play trailer in highest quality
- Fixes to choice of media stream
- Plex Companion: fix skipping forward and backward
version 1.2.7
- Let the user pick between several streams or if you have, several different files for the same movie (can be deactivated in the settings)
- Use the playing item's filename and path to figure out the Plex id. Now Plex should really always be informed what you're currently playing
version 1.2.6
- Fix Watch Later TypeError
version 1.2.5
- Plex Photos! Choose "Refresh Plex playlists/nodes" to use the new feature.
- Compatibility with latest Kodi Krypton (which is still under heavy development and in an alpha state). If PKC stops working for you, update your Kodi Krypton to the latest version.
- Fixes to getExtraFanart. If not using the PKC repository, you will have to manually update plugin.video.plexkodiconnect.movies and plugin.video.plexkodiconnect.tvshows to profit from these changes.
- Use language codes ('spa'), not verbose 'español' for audio streams and subtitles. You will have to reset your Kodi DB manually to profit from this change.
- Fix fanart.tv fallback to English not working.
- Fix plex.tv Watch Later ignored resume points.
- Fix double PKC settings strings.
version 1.2.4
- Automatically download Plex collection artwork from FanArtTv! Many thanks to @im85288
- A dedicated PKC setting to download this set fanart (independent of the other FanArtTv download)
version 1.2.3
- Improvements to resume points. PKC should now correctly mark an item as completely watched.
- Get rid of obsolete setting markPlayed. Mark a video item as played after 90%, just like Plex.
version 1.2.2
- Fix filename change recognition for episodes and movies - finally! If you experienced this, you will have to manually reset the Kodi database in the PKC settings
- Fix PKC resume points set way too high
- Clarify that transcode settings are TARGET quality
version 1.2.1
- Fix crash when Kodi not playing as expected
- Improve player.py stability
- Background sync: don't try to process infinitely
- Only tell PMS we're connected to what we're playing. This should enable the Plex Media Server to fall to sleep because PKC is not constantly bugging it
- Ensure credentials are known when reconnecting
- Remove some emby references
- Ask on first run if we have a low powered device
version 1.2.0
- Re-wired connection manager completely
- Periodically check if PMS address has changed
- Smarter, faster way to tell that PMS went offline
- Fix DTS-HD audio is not correctly identified (you will need to manually reset your Kodi DB)
- Improvements to PMS connection checks
- Fix default transcoding quality / network speed (so that PKC won't transcode initially)
- Fix direct path replacing possibly several times
- Initialize Kodi DBs only once
- Correctly update views on server switch
version 1.1.11
- Episodes and movies should now correctly be marked as watched in Kodi
version 1.1.10
- A donation link is up. Your support is much appreciated :-)
- Movie sets are working (without set art as this is missing from Plex). Many thanks to mattsch!
- Fix playback report and marking an item played. Should fix issues with e.g. episodes not correctly being set to watched in Kodi in the On Deck view
- Fix UnicodeEncodeError for file paths
- New setting: show watched movies in recently added
- New setting: don't show already watched episodes
- New setting: Force transcode HEVC
- New setting: Force transcode 10bit
- New setting: do a sync after screensaver deactivated. Very useful for Kodi for Android as Android may put Kodi in a weird kind of sleep
- Merge with plugin.video.emby up to 417b8d3b2237f982d1eab462c130e8a4f445dd8b
version 1.1.9
- Fix new episodes not being detected
- Use direct file paths for on deck shows if enabled
- Added Python requests to the PKC repo (should fix install problems)
version 1.1.8
- Account for string.encode() not allowing args. This will hopefully fix any sync problems now, especially for Android TV
- Also show already watched recently added episodes
- Increase logging for background updates
version 1.1.7
- Fix UnicodeDecodeError with file paths on sync
- Remove Emby reference from logs
version 1.1.6
- Corrected the addon's folder name from `PlexKodiConnect-develop` to `plugin.video.plexkodiconnect` in the Kodi addon folder. If you still experience issues, check your addon folder and delete `PlexKodiConnect-develop`
- Fix TypeError during Plex user switch
- Fix TypeError when trying to transcode
version 1.1.5
(you will need to rescan your library)
- A Kodi repository for easy installation and updating is now available. Download the [ZIP file from Github](https://github.com/croneter/PlexKodiConnect). Instructions can be found [in the Wiki](https://github.com/croneter/PlexKodiConnect/wiki/Installation)
- Additional artwork download from FanartTV! Enable it in the PKC settings under Sync Options
- New setting: Add TV show name and SxxExx to an episode in Recently Added
- Fix UnicodeEncodeError during sync
- Plex Companion now always reports playstate. Should increase stability
- Merge Emby for Kodi commits up to 3dbdab79a9d213aab3cb6347af0b8fb905bb6e45
version 1.1.4
(you will need to rescan your library)
- Plex Watch Later available as a separate Video Node!

View file

@ -10,13 +10,26 @@ import xbmc
import xbmcaddon
import xbmcgui
addon_ = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
addon_path = addon_.getAddonInfo('path').decode('utf-8')
base_resource = xbmc.translatePath(os.path.join(addon_path, 'resources', 'lib')).decode('utf-8')
_addon = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
try:
addon_path = _addon.getAddonInfo('path').decode('utf-8')
except TypeError:
addon_path = _addon.getAddonInfo('path').decode()
try:
base_resource = xbmc.translatePath(os.path.join(
addon_path,
'resources',
'lib')).decode('utf-8')
except TypeError:
base_resource = xbmc.translatePath(os.path.join(
addon_path,
'resources',
'lib')).decode()
sys.path.append(base_resource)
import artwork
import utils
import artwork
import clientinfo
import downloadutils
import librarysync
@ -36,31 +49,31 @@ def logMsg(msg, lvl=1):
#Kodi contextmenu item to configure the emby settings
#for now used to set ratings but can later be used to sync individual items etc.
if __name__ == '__main__':
itemid = xbmc.getInfoLabel("ListItem.DBID").decode("utf-8")
itemtype = xbmc.getInfoLabel("ListItem.DBTYPE").decode("utf-8")
itemid = utils.tryDecode(xbmc.getInfoLabel("ListItem.DBID"))
itemtype = utils.tryDecode(xbmc.getInfoLabel("ListItem.DBTYPE"))
emby = embyserver.Read_EmbyServer()
embyid = ""
plexid = ""
if not itemtype and xbmc.getCondVisibility("Container.Content(albums)"): itemtype = "album"
if not itemtype and xbmc.getCondVisibility("Container.Content(artists)"): itemtype = "artist"
if not itemtype and xbmc.getCondVisibility("Container.Content(songs)"): itemtype = "song"
if not itemtype and xbmc.getCondVisibility("Container.Content(pictures)"): itemtype = "picture"
if (not itemid or itemid == "-1") and xbmc.getInfoLabel("ListItem.Property(embyid)"):
embyid = xbmc.getInfoLabel("ListItem.Property(embyid)")
if (not itemid or itemid == "-1") and xbmc.getInfoLabel("ListItem.Property(plexid)"):
plexid = xbmc.getInfoLabel("ListItem.Property(plexid)")
else:
with embydb.GetEmbyDB() as emby_db:
item = emby_db.getItem_byKodiId(itemid, itemtype)
if item:
embyid = item[0]
plexid = item[0]
logMsg("Contextmenu opened for embyid: %s - itemtype: %s" %(embyid,itemtype))
logMsg("Contextmenu opened for plexid: %s - itemtype: %s" %(plexid,itemtype))
if embyid:
item = PF.GetPlexMetadata(embyid)
if plexid:
item = PF.GetPlexMetadata(plexid)
if item is None or item == 401:
logMsg('Could not get item metadata for item %s' % embyid, -1)
logMsg('Could not get item metadata for item %s' % plexid, -1)
return
API = PlexAPI.API(item[0])
userdata = API.getUserData()
@ -98,15 +111,15 @@ if __name__ == '__main__':
ret = xbmcgui.Dialog().select(header, options)
if ret != -1:
if options[ret] == utils.language(30402):
emby.updateUserRating(embyid, deletelike=True)
emby.updateUserRating(plexid, deletelike=True)
if options[ret] == utils.language(30403):
emby.updateUserRating(embyid, like=True)
emby.updateUserRating(plexid, like=True)
if options[ret] == utils.language(30404):
emby.updateUserRating(embyid, like=False)
emby.updateUserRating(plexid, like=False)
if options[ret] == utils.language(30405):
emby.updateUserRating(embyid, favourite=True)
emby.updateUserRating(plexid, favourite=True)
if options[ret] == utils.language(30406):
emby.updateUserRating(embyid, favourite=False)
emby.updateUserRating(plexid, favourite=False)
if options[ret] == utils.language(30407):
kodiconn = utils.kodiSQL('music')
kodicursor = kodiconn.cursor()
@ -121,7 +134,7 @@ if __name__ == '__main__':
musicutils.updateRatingToFile(newvalue, API.getFilePath())
if utils.settings('enableExportSongRating') == "true":
like, favourite, deletelike = musicutils.getEmbyRatingFromKodiRating(newvalue)
emby.updateUserRating(embyid, like, favourite, deletelike)
emby.updateUserRating(plexid, like, favourite, deletelike)
query = ' '.join(( "UPDATE song","SET rating = ?", "WHERE idSong = ?" ))
kodicursor.execute(query, (newvalue,itemid,))
kodiconn.commit()
@ -139,15 +152,15 @@ if __name__ == '__main__':
line1=("Delete file from Emby Server? This will "
"also delete the file(s) from disk!"))
if not resp:
logMsg("User skipped deletion for: %s." % embyid, 1)
logMsg("User skipped deletion for: %s." % plexid, 1)
delete = False
if delete:
import downloadutils
doUtils = downloadutils.DownloadUtils()
url = "{server}/emby/Items/%s?format=json" % embyid
logMsg("Deleting request: %s" % embyid, 0)
doUtils.downloadUrl(url, type="DELETE")
url = "{server}/emby/Items/%s?format=json" % plexid
logMsg("Deleting request: %s" % plexid, 0)
doUtils.downloadUrl(url, action_type="DELETE")
'''if utils.settings('skipContextMenu') != "true":
if xbmcgui.Dialog().yesno(
@ -156,8 +169,7 @@ if __name__ == '__main__':
"also delete the file(s) from disk!")):
import downloadutils
doUtils = downloadutils.DownloadUtils()
url = "{server}/emby/Items/%s?format=json" % embyid
doUtils.downloadUrl(url, type="DELETE")'''
doUtils.downloadUrl("{server}/emby/Items/%s?format=json" % plexid, action_type="DELETE")'''
xbmc.sleep(500)
xbmc.executebuiltin("Container.Update")

View file

@ -10,11 +10,24 @@ import xbmc
import xbmcaddon
import xbmcgui
###############################################################################
addon_ = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
addon_path = addon_.getAddonInfo('path').decode('utf-8')
base_resource = xbmc.translatePath(os.path.join(addon_path, 'resources', 'lib')).decode('utf-8')
_addon = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
try:
addon_path = _addon.getAddonInfo('path').decode('utf-8')
except TypeError:
addon_path = _addon.getAddonInfo('path').decode()
try:
base_resource = xbmc.translatePath(os.path.join(
addon_path,
'resources',
'lib')).decode('utf-8')
except TypeError:
base_resource = xbmc.translatePath(os.path.join(
addon_path,
'resources',
'lib')).decode()
sys.path.append(base_resource)
###############################################################################
@ -35,7 +48,6 @@ class Main:
xbmc.log("PlexKodiConnect - Full sys.argv received: %s" % sys.argv)
base_url = sys.argv[0]
params = urlparse.parse_qs(sys.argv[2][1:])
xbmc.log("PlexKodiConnect - Parameter string: %s" % sys.argv[2])
try:
mode = params['mode'][0]
itemid = params.get('id', '')
@ -67,18 +79,20 @@ class Main:
'companion': entrypoint.plexCompanion,
'switchuser': entrypoint.switchPlexUser,
'deviceid': entrypoint.resetDeviceId,
'reConnect': entrypoint.reConnect,
'delete': entrypoint.deleteItem,
'browseplex': entrypoint.BrowsePlexContent,
'ondeck': entrypoint.getOnDeck,
'chooseServer': entrypoint.chooseServer,
'watchlater': entrypoint.watchlater
'watchlater': entrypoint.watchlater,
'enterPMS': entrypoint.enterPMS,
'togglePlexTV': entrypoint.togglePlexTV,
'playwatchlater': entrypoint.playWatchLater
}
if "/extrafanart" in sys.argv[0]:
embypath = sys.argv[2][1:]
embyid = params.get('id',[""])[0]
entrypoint.getExtraFanArt(embyid,embypath)
plexpath = sys.argv[2][1:]
plexid = params.get('id', [""])[0]
entrypoint.getExtraFanArt(plexid, plexpath)
# Called by e.g. 3rd party plugin video extras
if ("/Extras" in sys.argv[0] or "/VideoFiles" in sys.argv[0] or
@ -121,6 +135,8 @@ class Main:
modes[mode](itemid, folderid)
elif mode == "companion":
modes[mode](itemid, params=sys.argv[2])
elif mode == 'playwatchlater':
modes[mode](params.get('id')[0], params.get('viewOffset')[0])
else:
modes[mode]()
else:
@ -128,12 +144,14 @@ class Main:
if mode == "settings":
xbmc.executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)')
elif mode in ("manualsync", "repair"):
if utils.window('emby_online') != "true":
if utils.window('plex_online') != "true":
# Server is not online, do not run the sync
xbmcgui.Dialog().ok(heading="PlexKodiConnect",
line1=("Unable to run the sync, the add-on is not "
"connected to the Emby server."))
utils.logMsg("PLEX", "Not connected to the emby server.", 1)
xbmcgui.Dialog().ok(
"PlexKodiConnect",
"Unable to run the sync, the add-on is not connected "
"to a Plex server.")
utils.logMsg("PLEX",
"Not connected to a PMS.", -1)
return
else:
@ -159,8 +177,8 @@ if ( __name__ == "__main__" ):
import pstats
import random
from time import gmtime, strftime
addonid = addon_.getAddonInfo('id').decode( 'utf-8' )
datapath = os.path.join( xbmc.translatePath( "special://profile/" ).decode( 'utf-8' ), "addon_data", addonid )
addonid = utils.tryDecode(addon_.getAddonInfo('id'))
datapath = os.path.join(utils.tryDecode(xbmc.translatePath( "special://profile/" )), "addon_data", addonid )
filename = os.path.join( datapath, strftime( "%Y%m%d%H%M%S",gmtime() ) + "-" + str( random.randrange(0,100000) ) + ".log" )
cProfile.run( 'Main()', filename )

View file

@ -2,7 +2,7 @@
<strings>
<!-- Add-on settings -->
<string id="30000">Server Address (IP)</string><!-- Verified -->
<string id="30002">Prefered playback method</string><!-- Verified -->
<string id="30002">Preferred playback method</string><!-- Verified -->
<string id="30004">Log level</string><!-- Verified -->
<string id="30005">Username: </string>
<string id="30006">Password: </string>
@ -131,7 +131,7 @@
<string id="30157">Enable Enhanced Images (eg CoverArt)</string><!-- Verified -->
<string id="30158">Metadata</string>
<string id="30159">Artwork</string>
<string id="30160">Video Quality for Transcoding</string><!-- Verified -->
<string id="30160">Video Quality if Transcoding necessary</string><!-- Verified -->
<string id="30161">Enable Suggested Loader (Requires Restart)</string>
<string id="30162">Add Season Number</string>
@ -302,6 +302,10 @@
<string id="30536">Users must log in every time Kodi restarts</string>
<string id="30537">RESTART KODI IF YOU MAKE ANY CHANGES</string>
<string id="30538">Complete Re-Sync necessary</string>
<string id="30539">Download additional art from FanArtTV (slower!)</string>
<string id="30540">Download movie set/collection art from FanArtTV</string>
<string id="30541">Don't ask to pick a certain stream/quality</string>
<string id="30542">Always pick best quality for trailers</string>
<!-- service add-on -->
@ -328,7 +332,7 @@
<string id="33020">Gathering tv shows from:</string>
<string id="33021">Gathering:</string>
<string id="33022">Detected the database needs to be recreated for this version of Emby for Kodi. Proceed?</string>
<string id="33023">Emby for Kod may not work correctly until the database is reset.</string>
<string id="33023">Emby for Kodi may not work correctly until the database is reset.</string>
<string id="33024">Cancelling the database syncing process. The current Kodi version is unsupported.</string>
<string id="33025">completed in:</string>
<string id="33026">Comparing movies from:</string>
@ -338,7 +342,7 @@
<string id="33030">Comparing episodes from:</string>
<string id="33031">Comparing:</string>
<string id="33032">Failed to generate a new device Id. See your logs for more information.</string>
<string id="33033">A new device Id has been generated. Kodi will now restart.</string>
<string id="33033">Kodi will now restart to apply the changes.</string>
<!-- New to Plex -->
<string id="39000">- Number of trailers to play before a movie</string>
@ -366,7 +370,8 @@
<string id="39021">[COLOR yellow]Sync Emby Theme Media to Kodi[/COLOR]</string>
<string id="39022">local</string>
<string id="39023">Failed to authenticate. Did you login to plex.tv?</string>
<string id="39024">[COLOR yellow]Reset PMS and plex.tv connections to re-login[/COLOR]</string>
<string id="39025">Automatically log into plex.tv on startup</string>
<string id="39026">Enable constant background sync</string>
<string id="39027">Playback Mode</string>
@ -387,11 +392,10 @@
<string id="39042">Replace Plex MUSIC with:</string>
<string id="39043">Go a step further and completely replace all original Plex library paths (/volume1/media) with custom SMB paths (smb://NAS/MyStuff)?</string>
<string id="39044">Please enter your custom smb paths in the settings under "Sync Options" and then restart Kodi</string>
<string id="39045">Appearance Tweaks</string>
<string id="39046">TV Shows</string>
<string id="39045">Original Plex PHOTO path to replace:</string>
<string id="39046">Replace Plex PHOTO with:</string>
<string id="39047">On Deck: Append show title to episode</string>
<string id="39048">On Deck: Append season- and episode-number (e.g. S3E2)</string>
<string id="39048">On Deck: Append season- and episode-number SxxExx</string>
<string id="39049">Nothing works? Try a full reset!</string>
<string id="39050">[COLOR yellow]Choose Plex Server from a list[/COLOR]</string>
<string id="39051">Wait before sync new/changed PMS item [s]</string>
@ -402,6 +406,22 @@
<string id="39056">Used by Sync and when attempting to Direct Play</string>
<string id="39057">Customize Paths</string>
<string id="39058">Extend Plex TV Series "On Deck" view to all shows</string>
<string id="39059">Recently Added: Append show title to episode</string>
<string id="39060">Recently Added: Append season- and episode-number SxxExx</string>
<string id="39061">Would you like to download additional artwork from FanArtTV? Sync will be slower!</string>
<string id="39062">Sync when screensaver is deactivated</string>
<string id="39063">Force Transcode Hi10P</string>
<string id="39064">Recently Added: Also show already watched episodes</string>
<string id="39065">Force Transcode HEVC</string>
<string id="39066">Recently Added: Also show already watched movies (Refresh Plex playlist/nodes!)</string>
<string id="39067">Your current Plex Media Server:</string>
<string id="39068">[COLOR yellow]Manually enter Plex Media Server address[/COLOR]</string>
<string id="39069">Current address:</string>
<string id="39070">Current port:</string>
<string id="39071">Current plex.tv status:</string>
<string id="39072">Is your Kodi installed on a low-powered device like a Raspberry Pi? If yes, then we will reduce the strain on Kodi to prevent it from crashing.</string>
<string id="39073">Appearance Tweaks</string>
<string id="39074">TV Shows</string>
<!-- Plex Entrypoint.py -->
<string id="39200">Log-out Plex Home User </string>
@ -411,11 +431,20 @@
<string id="39204">Perform manual library sync</string>
<string id="39205">Unable to run the sync, the add-on is not connected to a Plex server.</string>
<string id="39206">Plex might lock your account if you fail to log in too many times. Proceed anyway?</string>
<string id="39207">Reseting PMS connections, please wait</string>
<string id="39208">Failed to reset PMS and plex.tv connects. Try to restart Kodi.</string>
<string id="39209">[COLOR yellow]Log-in to plex.tv[/COLOR]</string>
<string id="39207">Resetting PMS connections, please wait</string>
<string id="39208">Failed to reset PKC. Try to restart Kodi.</string>
<string id="39209">[COLOR yellow]Toggle plex.tv login (sign in or sign out)[/COLOR]</string>
<string id="39210">Not yet connected to Plex Server</string>
<string id="39211">Watch later</string>
<string id="39213">is offline</string>
<string id="39214">Even though we signed in to plex.tv, we could not authorize for PMS</string>
<string id="39215">Enter your Plex Media Server's IP or URL, Examples are:</string>
<string id="39217">Does your Plex Media Server support SSL connections? (https instead of http)?</string>
<string id="39218">Error contacting PMS</string>
<string id="39219">Abort (Yes) or save address anyway (No)?</string>
<string id="39220">connected</string>
<string id="39221">plex.tv toggle successful</string>
<!-- Plex Artwork.py -->

View file

@ -28,8 +28,10 @@
<string id="30536">Benutzer müssen sich bei jedem Neustart von Kodi neu anmelden</string>
<string id="30537">BEI ÄNDERUNGEN KODI NEU STARTEN</string>
<string id="30538">Komplette Neusynchronisierung nötig</string>
<string id="30539">Zusätzliche Bilder von FanArtTV herunterladen (langsamer!)</string>
<string id="30540">FanArtTV Film-Sets/Collections Bilder herunterladen</string>
<string id="30541">Nicht fragen, welcher Stream/Qualität gespielt wird</string>
<string id="30542">Trailer immer in der besten Qualität abspielen</string>
<string id="30014">Verbindung</string>
<string id="30015">Netzwerk</string>
@ -164,7 +166,7 @@
<string id="30157">Deaktiviere erweiterte Bilder (z.B. CoverArt)</string>
<string id="30158">Metadaten</string>
<string id="30159">Grafiken</string>
<string id="30160">Videoqualität für Transkodierung</string>
<string id="30160">Videoqualität falls Transkodierung nötig</string>
<string id="30161">'Empfohlen'-Loader aktivieren (Erfordert Neustart)</string>
<string id="30162">Staffelnummer hinzufügen</string>
@ -278,6 +280,8 @@
<string id="30311">Musikstücke</string>
<string id="30312">Kanäle</string>
<string id="33033">Kodi wird jetzt neu gestartet um die Änderungen anzuwenden.</string>
<!-- New to Plex -->
<string id="39000">- Anzahl abzuspielender Trailer vor einem Film</string>
<string id="39001">Audio Verstärkung (audio boost) wenn transkodiert wird</string>
@ -304,7 +308,8 @@
<string id="39021">[COLOR yellow]Plex Themes zu Kodi synchronisieren[/COLOR]</string>
<string id="39022">lokal</string>
<string id="39023">Plex Media Server Authentifizierung fehlgeschlagen. Haben Sie sich bei plex.tv eingeloggt?</string>
<string id="39024">[COLOR yellow]PMS und plex.tv Verbindungen zurücksetzen für erneuten Login[/COLOR]</string>
<string id="39025">Automatisch beim Starten bei plex.tv einloggen</string>
<string id="39026">Laufende Synchronisierung im Hintergrund aktivieren</string>
<string id="39027">Playback Modus</string>
@ -325,11 +330,10 @@
<string id="39042">Plex MUSIK Pfade ersetzen durch:</string>
<string id="39043">Sollen sogar sämtliche Plex Pfade wie /volume1/Hans/medien durch benutzerdefinierte smb Pfade wie smb://NAS/Filme ersetzt werden?</string>
<string id="39044">Bitte geben Sie Ihre benutzerdefinierten SMB Pfade nun in den Einstellungen unter Sync Optionen ein. Starten Sie dann Kodi neu.</string>
<string id="39045">Erscheinung</string>
<string id="39046">TV Serien</string>
<string id="39045">Ursprünglicher Plex Pfad für PHOTOS:</string>
<string id="39046">Plex PHOTO Pfade ersetzen durch:</string>
<string id="39047">"Aktuell": Serien- an Episoden-Titel anfügen</string>
<string id="39048">"Aktuell": Staffel und Episode anfügen (z.B. S3E2)</string>
<string id="39048">"Aktuell": Staffel und Episode anfügen, SxxExx</string>
<string id="39049">Nichts funktioniert? Setze mal alles zurück!</string>
<string id="39050">[COLOR yellow]Plex Server aus Liste auswählen[/COLOR]</string>
<string id="39051">Warten bevor neue/geänderte PMS Einträge gesynct werden [s]</string>
@ -340,8 +344,22 @@
<string id="39056">Verwendet für Synchronisierung sowie beim Versuch, Direct Play zu nutzen</string>
<string id="39057">Pfade ändern</string>
<string id="39058">Standard Plex Ansicht "Aktuell" auf alle TV Shows erweitern</string>
<string id="39059">"Zuletzt hinzugefügt": Serien- an Episoden-Titel anfügen</string>
<string id="39060">"Zuletzt hinzugefügt": Staffel und Episode anfügen, SxxExx</string>
<string id="39061">Zusätzliche Bilder von FanArtTV herunterladen? Die Synchronisierung wird länger dauern!</string>
<string id="39062">Sync wenn Bildschirmschoner deaktiviert wird</string>
<string id="39063">Hi10p Codec Transkodierung erzwingen</string>
<string id="39064">"Zuletzt hinzugefügt": gesehene Folgen anzeigen</string>
<string id="39065">HEVC Codec Transkodierung erzwingen</string>
<string id="39066">"Zuletzt hinzugefügt": gesehene Filme anzeigen (Plex Playlisten und Nodes zurücksetzen!)</string>
<string id="39067">Aktueller Plex Media Server:</string>
<string id="39068">[COLOR yellow]Plex Media Server Adresse manuell eingeben[/COLOR]</string>
<string id="39069">Aktuelle Adresse:</string>
<string id="39070">Aktueller Port:</string>
<string id="39071">Aktueller plex.tv Status:</string>
<string id="39072">Läuft Kodi auf einem Raspberry Pi oder ähnlichem Gerät mit äusserst wenig Rechenleistung? Falls ja, wird die Rechenlast reduziert, damit Kodi nicht abstürzt.</string>
<string id="39073">Tweaks Aussehen</string>
<string id="39074">TV Serien</string>
<!-- Plex Entrypoint.py -->
<string id="39200">Plex Home Benutzer abmelden: </string>
@ -352,10 +370,19 @@
<string id="39205">Plex Bibliothek kann nicht gescannt werden, da keine Verbindung mit einem Plex Server besteht.</string>
<string id="39206">Plex könnte möglicherweise Ihren Account sperren, wenn Sie zu oft versuchen, sich erfolglos anzumelden. Trotzdem fortfahren?</string>
<string id="39207">PMS Verbindungen werden zurückgesetzt</string>
<string id="39208">PMS und plex.tv Verbindungen konnten nicht zurückgesetzt werden. Bitte versuchen Sie, Kodi neu zu starten, um das Problem zu beheben.</string>
<string id="39209">[COLOR yellow]Bei plex.tv einloggen[/COLOR]</string>
<string id="39208">PKC konnte nicht zurückgesetzt werden. Bitte versuchen Sie, Kodi neu zu starten, um das Problem zu beheben.</string>
<string id="39209">[COLOR yellow]plex.tv Login wechseln (ein- resp. ausloggen)[/COLOR]</string>
<string id="39210">Noch nicht mit Plex Server verbunden</string>
<string id="39211">Später ansehen</string>
<string id="39213">ist offline</string>
<string id="39214">Obwohl mit plex.tv verbunden, konnte keine Verbindung hergestellt werden mit</string>
<string id="39215">Plex Media Server IP oder URL eingeben. Zum Beispiel:</string>
<string id="39217">Unterstützt der Plex Media Server sichere SSL Verbindungen (https anstelle von http)?</string>
<string id="39218">Error beim Verbinden mit PMS</string>
<string id="39219">Abbrechen (Ja) oder PMS Adresse trotzdem speichern (Nein)?</string>
<string id="39220">verbunden</string>
<string id="39221">plex.tv wechsel OK</string>
<!-- Plex Artwork.py -->
<string id="39250">Alle Plex Bilder in Kodi zwischenzuspeichern kann sehr lange dauern. Möchten Sie wirklich fortfahren?</string>

File diff suppressed because it is too large Load diff

View file

@ -2,18 +2,26 @@
import threading
import traceback
import socket
import Queue
import xbmc
import utils
from plexbmchelper import listener, plexgdm, subscribers, functions, \
httppersist, settings
from PlexFunctions import ParseContainerKey, GetPlayQueue, \
ConvertPlexToKodiTime
import playlist
import player
@utils.logging
@utils.ThreadMethodsAdditionalSuspend('emby_serverStatus')
@utils.ThreadMethodsAdditionalSuspend('plex_serverStatus')
@utils.ThreadMethods
class PlexCompanion(threading.Thread):
"""
Initialize with a Queue for callbacks
"""
def __init__(self):
self.logMsg("----===## Starting PlexCompanion ##===----", 1)
self.settings = settings.getSettings()
@ -22,55 +30,150 @@ class PlexCompanion(threading.Thread):
self.client = plexgdm.plexgdm()
self.client.clientDetails(self.settings)
self.logMsg("Registration string is: %s "
% self.client.getClientDetails(), 1)
% self.client.getClientDetails(), 2)
# Initialize playlist/queue stuff
self.playlist = playlist.Playlist('video')
# kodi player instance
self.player = player.Player()
threading.Thread.__init__(self)
def _getStartItem(self, string):
"""
Grabs the Plex id from e.g. '/library/metadata/12987'
and returns the tuple (typus, id) where typus is either 'queueId' or
'plexId' and id is the corresponding id as a string
"""
typus = 'plexId'
if string.startswith('/library/metadata'):
try:
string = string.split('/')[3]
except IndexError:
string = ''
else:
self.logMsg('Unknown string! %s' % string, -1)
return typus, string
def processTasks(self, task):
"""
Processes tasks picked up e.g. by Companion listener
task = {
'action': 'playlist'
'data': as received from Plex companion
}
"""
self.logMsg('Processing: %s' % task, 2)
data = task['data']
if task['action'] == 'playlist':
try:
_, queueId, query = ParseContainerKey(data['containerKey'])
except Exception as e:
self.logMsg('Exception while processing: %s' % e, -1)
import traceback
self.logMsg("Traceback:\n%s" % traceback.format_exc(), -1)
return
if self.playlist is not None:
if self.playlist.Typus() != data.get('type'):
self.logMsg('Switching to Kodi playlist of type %s'
% data.get('type'), 1)
self.playlist = None
if self.playlist is None:
if data.get('type') == 'music':
self.playlist = playlist.Playlist('music')
elif data.get('type') == 'video':
self.playlist = playlist.Playlist('video')
else:
self.playlist = playlist.Playlist()
if self.playlist is None:
self.logMsg('Could not initialize playlist', -1)
return
if queueId != self.playlist.QueueId():
self.logMsg('New playlist received, updating!', 1)
xml = GetPlayQueue(queueId)
if xml in (None, 401):
self.logMsg('Could not download Plex playlist.', -1)
return
# Clear existing playlist on the Kodi side
self.playlist.clear()
# Set new values
self.playlist.QueueId(queueId)
self.playlist.PlayQueueVersion(int(
xml.attrib.get('playQueueVersion')))
self.playlist.Guid(xml.attrib.get('guid'))
items = []
for item in xml:
items.append({
'playQueueItemID': item.get('playQueueItemID'),
'plexId': item.get('ratingKey'),
'kodiId': None})
self.playlist.playAll(
items,
startitem=self._getStartItem(data.get('key', '')),
offset=ConvertPlexToKodiTime(data.get('offset', 0)))
self.logMsg('Initiated playlist no %s with version %s'
% (self.playlist.QueueId(),
self.playlist.PlayQueueVersion()))
else:
self.logMsg('This has never happened before!', -1)
def run(self):
httpd = False
# Cache for quicker while loops
log = self.logMsg
client = self.client
threadStopped = self.threadStopped
threadSuspended = self.threadSuspended
start_count = 0
# Start up instances
requestMgr = httppersist.RequestMgr()
jsonClass = functions.jsonClass(requestMgr, self.settings)
subscriptionManager = subscribers.SubscriptionManager(
jsonClass, requestMgr)
jsonClass, requestMgr, self.player, self.playlist)
# Start up httpd
while True:
try:
httpd = listener.ThreadedHTTPServer(
client,
subscriptionManager,
jsonClass,
self.settings,
('', self.settings['myport']),
listener.MyHandler)
httpd.timeout = 0.95
break
except:
log("Unable to start PlexCompanion. Traceback:", -1)
log(traceback.print_exc(), -1)
queue = Queue.Queue(maxsize=100)
xbmc.sleep(3000)
if utils.settings('plexCompanion') == 'true':
self.logMsg('User activated Plex Companion', 0)
# Start up httpd
start_count = 0
while True:
try:
httpd = listener.ThreadedHTTPServer(
client,
subscriptionManager,
jsonClass,
self.settings,
queue,
('', self.settings['myport']),
listener.MyHandler)
httpd.timeout = 0.95
break
except:
log("Unable to start PlexCompanion. Traceback:", -1)
log(traceback.print_exc(), -1)
if start_count == 3:
log("Error: Unable to start web helper.", -1)
httpd = False
break
xbmc.sleep(3000)
start_count += 1
if start_count == 3:
log("Error: Unable to start web helper.", -1)
httpd = False
break
if not httpd:
return
start_count += 1
else:
self.logMsg('User deactivated Plex Companion', 0)
client.start_all()
message_count = 0
if httpd:
t = threading.Thread(target=httpd.handle_request)
while not threadStopped():
# If we are not authorized, sleep
# Otherwise, we trigger a download which leads to a
@ -80,34 +183,50 @@ class PlexCompanion(threading.Thread):
break
xbmc.sleep(1000)
try:
httpd.handle_request()
message_count += 1
if httpd:
if not t.isAlive():
# Use threads cause the method will stall
t = threading.Thread(target=httpd.handle_request)
t.start()
if message_count > 100:
if client.check_client_registration():
log("Client is still registered", 1)
else:
log("Client is no longer registered", 1)
log("Plex Companion still running on port %s"
% self.settings['myport'], 1)
message_count = 0
if message_count == 3000:
message_count = 0
if client.check_client_registration():
log("Client is still registered", 1)
else:
log("Client is no longer registered", 1)
log("Plex Companion still running on port %s"
% self.settings['myport'], 1)
# Get and set servers
subscriptionManager.serverlist = client.getServerList()
subscriptionManager.notify()
xbmc.sleep(50)
if message_count % 30 == 0:
subscriptionManager.serverlist = client.getServerList()
subscriptionManager.notify()
if not httpd:
message_count = 0
except:
log("Error in loop, continuing anyway", 0)
log("Error in loop, continuing anyway. Traceback:", 1)
log(traceback.format_exc(), 1)
xbmc.sleep(50)
# See if there's anything we need to process
try:
task = queue.get(block=False)
except Queue.Empty:
pass
else:
# Got instructions, process them
self.processTasks(task)
queue.task_done()
# Don't sleep
continue
xbmc.sleep(20)
client.stop_all()
try:
httpd.socket.shutdown(socket.SHUT_RDWR)
except:
pass
finally:
httpd.socket.close()
if httpd:
try:
httpd.socket.shutdown(socket.SHUT_RDWR)
except:
pass
finally:
httpd.socket.close()
log("----===## Plex Companion stopped ##===----", 0)

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from urllib import urlencode
from ast import literal_eval
from urlparse import urlparse, parse_qs
from urlparse import urlparse, parse_qsl
import re
from copy import deepcopy
@ -86,13 +86,13 @@ def GetPlexKeyNumber(plexKey):
def ParseContainerKey(containerKey):
"""
Parses e.g. /playQueues/3045?own=1&repeat=0&window=200 to:
'playQueues', '3045', {'window': ['200'], 'own': ['1'], 'repeat': ['0']}
'playQueues', '3045', {'window': '200', 'own': '1', 'repeat': '0'}
Output hence: library, key, query (query as a special dict)
Output hence: library, key, query (str, str, dict)
"""
result = urlparse(containerKey)
library, key = GetPlexKeyNumber(result.path)
query = parse_qs(result.query)
query = dict(parse_qsl(result.query))
return library, key, query
@ -139,7 +139,7 @@ def SelectStreams(url, args):
chosen.
"""
downloadutils.DownloadUtils().downloadUrl(
url + '?' + urlencode(args), type='PUT')
url + '?' + urlencode(args), action_type='PUT')
def GetPlayQueue(playQueueID):
@ -165,7 +165,7 @@ def GetPlexMetadata(key):
Can be called with either Plex key '/library/metadata/xxxx'metadata
OR with the digits 'xxxx' only.
Returns None if something went wrong
Returns None or 401 if something went wrong
"""
key = str(key)
if '/library/metadata/' in key:
@ -173,14 +173,15 @@ def GetPlexMetadata(key):
else:
url = "{server}/library/metadata/" + key
arguments = {
'checkFiles': 1, # No idea
'checkFiles': 0,
'includeExtras': 1, # Trailers and Extras => Extras
# 'includeRelated': 1, # Similar movies => Video -> Related
# 'includeRelatedCount': 5,
'includeReviews': 1,
'includeRelated': 0, # Similar movies => Video -> Related
# 'includeRelatedCount': 0,
# 'includeOnDeck': 1,
'includeChapters': 1,
'includePopularLeaves': 1,
'includeConcerts': 1
# 'includeChapters': 1,
# 'includePopularLeaves': 1,
# 'includeConcerts': 1
}
url = url + '?' + urlencode(arguments)
xml = downloadutils.DownloadUtils().downloadUrl(url)
@ -388,7 +389,7 @@ def GetPlexPlaylist(itemid, librarySectionUUID, mediatype='movie'):
'repeat': '0'
}
xml = downloadutils.DownloadUtils().downloadUrl(
url + '?' + urlencode(args), type="POST")
url + '?' + urlencode(args), action_type="POST")
try:
xml[0].tag
except (IndexError, TypeError, AttributeError):
@ -424,14 +425,14 @@ def PMSHttpsEnabled(url):
verifySSL=False)
try:
res.attrib
except:
except AttributeError:
# Might have SSL deactivated. Try with http
res = doUtils('http://%s/identity' % url,
authenticate=False,
verifySSL=False)
try:
res.attrib
except:
except AttributeError:
logMsg(title, "Could not contact PMS %s" % url, -1)
return None
else:
@ -448,16 +449,17 @@ def GetMachineIdentifier(url):
Returns None if something went wrong
"""
xml = downloadutils.DownloadUtils().downloadUrl(
url + '/identity', type="GET")
xml = downloadutils.DownloadUtils().downloadUrl('%s/identity' % url,
authenticate=False,
verifySSL=False,
timeout=4)
try:
xml.attrib
except:
machineIdentifier = xml.attrib['machineIdentifier']
except (AttributeError, KeyError):
logMsg(title, 'Could not get the PMS machineIdentifier for %s'
% url, -1)
return None
machineIdentifier = xml.attrib.get('machineIdentifier')
logMsg(title, 'Found machineIdentifier %s for %s'
logMsg(title, 'Found machineIdentifier %s for the PMS %s'
% (machineIdentifier, url), 1)
return machineIdentifier
@ -480,7 +482,6 @@ def GetPMSStatus(token):
answer = {}
xml = downloadutils.DownloadUtils().downloadUrl(
'{server}/status/sessions',
type="GET",
headerOptions={'X-Plex-Token': token})
try:
xml.attrib
@ -519,5 +520,5 @@ def scrobble(ratingKey, state):
url = "{server}/:/unscrobble?" + urlencode(args)
else:
return
downloadutils.DownloadUtils().downloadUrl(url, type="GET")
downloadutils.DownloadUtils().downloadUrl(url)
logMsg(title, "Toggled watched state for Plex item %s" % ratingKey, 1)

View file

@ -28,7 +28,7 @@ class Artwork():
xbmc_port = None
xbmc_username = None
xbmc_password = None
imageCacheThreads = []
imageCacheLimitThreads = 0
@ -36,9 +36,9 @@ class Artwork():
self.enableTextureCache = utils.settings('enableTextureCache') == "true"
self.imageCacheLimitThreads = int(utils.settings("imageCacheLimit"))
self.imageCacheLimitThreads = int(self.imageCacheLimitThreads * 5);
self.imageCacheLimitThreads = int(self.imageCacheLimitThreads * 5)
utils.logMsg("Using Image Cache Thread Count: " + str(self.imageCacheLimitThreads), 1)
if not self.xbmc_port and self.enableTextureCache:
self.setKodiWebServerDetails()
@ -48,15 +48,15 @@ class Artwork():
def double_urlencode(self, text):
text = self.single_urlencode(text)
text = self.single_urlencode(text)
return text
def single_urlencode(self, text):
text = urllib.urlencode({'blahblahblah':text.encode("utf-8")}) #urlencode needs a utf- string
text = urllib.urlencode({'blahblahblah': utils.tryEncode(text)}) #urlencode needs a utf- string
text = text[13:]
return text.decode("utf-8") #return the result again as unicode
return utils.tryDecode(text) #return the result again as unicode
def setKodiWebServerDetails(self):
# Get the Kodi webserver details - used to set the texture cache
@ -74,9 +74,9 @@ class Artwork():
result = json.loads(result)
try:
xbmc_webserver_enabled = result['result']['value']
except KeyError, TypeError:
except (KeyError, TypeError):
xbmc_webserver_enabled = False
if not xbmc_webserver_enabled:
# Enable the webserver, it is disabled
web_port = {
@ -159,7 +159,7 @@ class Artwork():
self.xbmc_password = result['result']['value']
except TypeError:
pass
def FullTextureCacheSync(self):
# This method will sync all Kodi artwork to textures13.db
# and cache them locally. This takes diskspace!
@ -167,30 +167,34 @@ class Artwork():
string = xbmcaddon.Addon().getLocalizedString
if not xbmcgui.Dialog().yesno(
"Image Texture Cache", string(39250).encode('utf-8')):
"Image Texture Cache", string(39250)):
return
self.logMsg("Doing Image Cache Sync", 1)
dialog = xbmcgui.DialogProgress()
dialog.create("Emby for Kodi", "Image Cache Sync")
dialog.create("PlexKodiConnect", "Image Cache Sync")
# ask to rest all existing or not
if xbmcgui.Dialog().yesno(
"Image Texture Cache", string(39251).encode('utf-8'), ""):
"Image Texture Cache", string(39251), ""):
self.logMsg("Resetting all cache data first", 1)
# Remove all existing textures first
path = xbmc.translatePath("special://thumbnails/").decode('utf-8')
path = utils.tryDecode(xbmc.translatePath("special://thumbnails/"))
if utils.IfExists(path):
allDirs, allFiles = xbmcvfs.listdir(path)
for dir in allDirs:
allDirs, allFiles = xbmcvfs.listdir(path+dir)
for file in allFiles:
if os.path.supports_unicode_filenames:
xbmcvfs.delete(os.path.join(path+dir.decode('utf-8'),file.decode('utf-8')))
xbmcvfs.delete(os.path.join(
path + utils.tryDecode(dir),
utils.tryDecode(file)))
else:
xbmcvfs.delete(os.path.join(path.encode('utf-8')+dir,file))
xbmcvfs.delete(os.path.join(
utils.tryEncode(path) + dir,
file))
# remove all existing data from texture DB
textureconnection = utils.kodiSQL('texture')
texturecursor = textureconnection.cursor()
@ -209,8 +213,8 @@ class Artwork():
cursor.execute("SELECT url FROM art WHERE media_type != 'actor'") # dont include actors
result = cursor.fetchall()
total = len(result)
count = 1
percentage = 0
count = 1
percentage = 0
self.logMsg("Image cache sync about to process " + str(total) + " images", 1)
for url in result:
if dialog.iscanceled():
@ -221,26 +225,26 @@ class Artwork():
self.CacheTexture(url[0])
count += 1
cursor.close()
# Cache all entries in music DB
connection = utils.kodiSQL('music')
cursor = connection.cursor()
cursor.execute("SELECT url FROM art")
result = cursor.fetchall()
total = len(result)
count = 1
count = 1
percentage = 0
self.logMsg("Image cache sync about to process " + str(total) + " images", 1)
for url in result:
if dialog.iscanceled():
break
break
percentage = int((float(count) / float(total))*100)
textMessage = str(count) + " of " + str(total)
dialog.update(percentage, "Updating Image Cache: " + textMessage)
self.CacheTexture(url[0])
count += 1
cursor.close()
dialog.update(100, "Waiting for all threads to exit: " + str(len(self.imageCacheThreads)))
self.logMsg("Waiting for all threads to exit", 1)
while len(self.imageCacheThreads) > 0:
@ -250,16 +254,16 @@ class Artwork():
dialog.update(100, "Waiting for all threads to exit: " + str(len(self.imageCacheThreads)))
self.logMsg("Waiting for all threads to exit: " + str(len(self.imageCacheThreads)), 1)
xbmc.sleep(500)
dialog.close()
def addWorkerImageCacheThread(self, urlToAdd):
while(True):
# removed finished
for thread in self.imageCacheThreads:
if thread.isFinished:
self.imageCacheThreads.remove(thread)
self.imageCacheThreads.remove(thread)
# add a new thread or wait and retry if we hit our limit
if(len(self.imageCacheThreads) < self.imageCacheLimitThreads):
@ -273,14 +277,14 @@ class Artwork():
else:
self.logMsg("Waiting for empty queue spot: " + str(len(self.imageCacheThreads)), 2)
xbmc.sleep(50)
def CacheTexture(self, url):
# Cache a single image url to the texture cache
if url and self.enableTextureCache:
if(self.imageCacheLimitThreads == 0 or self.imageCacheLimitThreads == None):
#Add image to texture cache by simply calling it at the http endpoint
url = self.double_urlencode(url)
try: # Extreme short timeouts so we will have a exception.
response = requests.head(
@ -291,7 +295,7 @@ class Artwork():
timeout=(0.01, 0.01))
# We don't need the result
except: pass
else:
self.addWorkerImageCacheThread(url)
@ -349,13 +353,13 @@ class Artwork():
mediaType=mediaType,
imageType="%s%s" % ("fanart", index),
cursor=cursor)
if backdropsNumber > 1:
try: # Will only fail on the first try, str to int.
index += 1
except TypeError:
index = 1
elif art == "Primary":
# Primary art is processed as thumb and poster for Kodi.
for artType in kodiart[art]:
@ -365,7 +369,7 @@ class Artwork():
mediaType=mediaType,
imageType=artType,
cursor=cursor)
elif kodiart.get(art):
# Process the rest artwork type that Kodi can use
self.addOrUpdateArt(
@ -391,9 +395,11 @@ class Artwork():
cursor.execute(query, (kodiId, mediaType, imageType,))
try: # Update the artwork
url = cursor.fetchone()[0]
except TypeError: # Add the artwork
cacheimage = True
self.logMsg("Adding Art Link for kodiId: %s (%s)" % (kodiId, imageUrl), 2)
query = (
'''
INSERT INTO art(media_id, media_type, type, url)
@ -402,17 +408,21 @@ class Artwork():
'''
)
cursor.execute(query, (kodiId, mediaType, imageType, imageUrl))
else: # Only cache artwork if it changed
if url != imageUrl:
cacheimage = True
# Only for the main backdrop, poster
if (utils.window('emby_initialScan') != "true" and
if (utils.window('plex_initialScan') != "true" and
imageType in ("fanart", "poster")):
# Delete current entry before updating with the new one
self.deleteCachedArtwork(url)
self.logMsg(
"Updating Art url for %s kodiId: %s (%s) -> (%s)"
% (imageType, kodiId, url, imageUrl), 1)
query = ' '.join((
"UPDATE art",
@ -422,9 +432,9 @@ class Artwork():
"AND type = ?"
))
cursor.execute(query, (imageUrl, kodiId, mediaType, imageType))
# Cache fanart and poster in Kodi texture cache
if cacheimage and imageType in ("fanart", "poster", "thumb"):
if cacheimage:
self.CacheTexture(imageUrl)
def deleteArtwork(self, kodiid, mediatype, cursor):
@ -453,24 +463,25 @@ class Artwork():
try:
cursor.execute("SELECT cachedurl FROM texture WHERE url = ?", (url,))
cachedurl = cursor.fetchone()[0]
except TypeError:
self.logMsg("Could not find cached url.", 1)
except OperationalError:
self.logMsg("Database is locked. Skip deletion process.", 1)
else: # Delete thumbnail as well as the entry
thumbnails = xbmc.translatePath("special://thumbnails/%s" % cachedurl).decode('utf-8')
thumbnails = utils.tryDecode(
xbmc.translatePath("special://thumbnails/%s" % cachedurl))
self.logMsg("Deleting cached thumbnail: %s" % thumbnails, 1)
xbmcvfs.delete(thumbnails)
try:
cursor.execute("DELETE FROM texture WHERE url = ?", (url,))
connection.commit()
except OperationalError:
self.logMsg("Issue deleting url from cache. Skipping.", 2)
finally:
cursor.close()
@ -487,7 +498,7 @@ class Artwork():
"%s/emby/Items/%s/Images/Primary?"
"MaxWidth=400&MaxHeight=400&Index=0&Tag=%s"
% (self.server, personId, tag))
person['imageurl'] = image
return people
@ -501,8 +512,6 @@ class Artwork():
def getAllArtwork(self, item, parentInfo=False):
server = self.server
itemid = item['Id']
artworks = item['ImageTags']
backdrops = item.get('BackdropImageTags',[])
@ -527,13 +536,13 @@ def getAllArtwork(self, item, parentInfo=False):
'Disc': "",
'Backdrop': []
}
# Process backdrops
for index, tag in enumerate(backdrops):
artwork = (
"%s/emby/Items/%s/Images/Backdrop/%s?"
"MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
% (server, itemid, index, maxWidth, maxHeight, tag, customquery))
% (self.server, itemid, index, maxWidth, maxHeight, tag, customquery))
allartworks['Backdrop'].append(artwork)
# Process the rest of the artwork
@ -544,15 +553,15 @@ def getAllArtwork(self, item, parentInfo=False):
artwork = (
"%s/emby/Items/%s/Images/%s/0?"
"MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
% (server, itemid, art, maxWidth, maxHeight, tag, customquery))
% (self.server, itemid, art, maxWidth, maxHeight, tag, customquery))
allartworks[art] = artwork
# Process parent items if the main item is missing artwork
if parentInfo:
# Process parent backdrops
if not allartworks['Backdrop']:
parentId = item.get('ParentBackdropItemId')
if parentId:
# If there is a parentId, go through the parent backdrop list
@ -562,7 +571,7 @@ def getAllArtwork(self, item, parentInfo=False):
artwork = (
"%s/emby/Items/%s/Images/Backdrop/%s?"
"MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
% (server, parentId, index, maxWidth, maxHeight, tag, customquery))
% (self.server, parentId, index, maxWidth, maxHeight, tag, customquery))
allartworks['Backdrop'].append(artwork)
# Process the rest of the artwork
@ -570,15 +579,15 @@ def getAllArtwork(self, item, parentInfo=False):
for parentart in parentartwork:
if not allartworks[parentart]:
parentId = item.get('Parent%sItemId' % parentart)
if parentId:
parentTag = item['Parent%sImageTag' % parentart]
artwork = (
"%s/emby/Items/%s/Images/%s/0?"
"MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
% (server, parentId, parentart,
% (self.server, parentId, parentart,
maxWidth, maxHeight, parentTag, customquery))
allartworks[parentart] = artwork
@ -587,12 +596,12 @@ def getAllArtwork(self, item, parentInfo=False):
parentId = item.get('AlbumId')
if parentId and item.get('AlbumPrimaryImageTag'):
parentTag = item['AlbumPrimaryImageTag']
artwork = (
"%s/emby/Items/%s/Images/Primary/0?"
"MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
% (server, parentId, maxWidth, maxHeight, parentTag, customquery))
% (self.server, parentId, maxWidth, maxHeight, parentTag, customquery))
allartworks['Primary'] = artwork
return allartworks
return allartworks

View file

@ -7,7 +7,7 @@ from uuid import uuid4
import xbmc
import xbmcaddon
from utils import logging, window, settings
from utils import logging, window, settings, tryDecode
###############################################################################
@ -67,8 +67,7 @@ class ClientInfo():
def getDeviceName(self):
if settings('deviceNameOpt') == "false":
# Use Kodi's deviceName
deviceName = xbmc.getInfoLabel(
'System.FriendlyName').decode('utf-8')
deviceName = tryDecode(xbmc.getInfoLabel('System.FriendlyName'))
else:
deviceName = settings('deviceName')
deviceName = deviceName.replace("\"", "_")

View file

@ -21,7 +21,7 @@ requests.packages.urllib3.disable_warnings(InsecurePlatformWarning)
class ConnectUtils():
# Borg - multiple instances, shared state
_shared_state = {}
clientInfo = clientinfo.ClientInfo()
@ -60,8 +60,6 @@ class ConnectUtils():
def startSession(self):
log = self.logMsg
self.deviceId = self.clientInfo.getDeviceId()
# User is identified from this point
@ -75,8 +73,8 @@ class ConnectUtils():
if self.sslclient is not None:
verify = self.sslclient
except:
log("Could not load SSL settings.", 1)
self.logMsg("Could not load SSL settings.", 1)
# Start session
self.c = requests.Session()
self.c.headers = header
@ -85,7 +83,7 @@ class ConnectUtils():
self.c.mount("http://", requests.adapters.HTTPAdapter(max_retries=1))
self.c.mount("https://", requests.adapters.HTTPAdapter(max_retries=1))
log("Requests session started on: %s" % self.server, 1)
self.logMsg("Requests session started on: %s" % self.server, 1)
def stopSession(self):
try:
@ -95,8 +93,7 @@ class ConnectUtils():
def getHeader(self, authenticate=True):
clientInfo = self.clientInfo
version = clientInfo.getVersion()
version = self.clientInfo.getVersion()
if not authenticate:
# If user is not authenticated
@ -105,9 +102,9 @@ class ConnectUtils():
'X-Application': "Kodi/%s" % version,
'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Accept': "application/json"
}
}
self.logMsg("Header: %s" % header, 1)
else:
token = self.token
# Attached to the requests session
@ -117,18 +114,17 @@ class ConnectUtils():
'Accept': "application/json",
'X-Application': "Kodi/%s" % version,
'X-Connect-UserToken': token
}
}
self.logMsg("Header: %s" % header, 1)
return header
def doUrl(self, url, data=None, postBody=None, rtype="GET",
parameters=None, authenticate=True, timeout=None):
log = self.logMsg
window = utils.window
log("=== ENTER connectUrl ===", 2)
self.logMsg("=== ENTER connectUrl ===", 2)
default_link = ""
if timeout is None:
timeout = self.timeout
@ -137,7 +133,7 @@ class ConnectUtils():
try:
# If connect user is authenticated
if authenticate:
try:
try:
c = self.c
# Replace for the real values
url = url.replace("{server}", self.server)
@ -167,7 +163,7 @@ class ConnectUtils():
verifyssl = self.sslclient
except AttributeError:
pass
# Prepare request
if rtype == "GET":
r = requests.get(url,
@ -195,7 +191,7 @@ class ConnectUtils():
verifyssl = self.sslclient
except AttributeError:
pass
# Prepare request
if rtype == "GET":
r = requests.get(url,
@ -213,28 +209,28 @@ class ConnectUtils():
verify=verifyssl)
##### THE RESPONSE #####
log(r.url, 1)
log(r, 1)
self.logMsg(r.url, 1)
self.logMsg(r, 1)
if r.status_code == 204:
# No body in the response
log("====== 204 Success ======", 1)
self.logMsg("====== 204 Success ======", 1)
elif r.status_code == requests.codes.ok:
try:
try:
# UNICODE - JSON object
r = r.json()
log("====== 200 Success ======", 1)
log("Response: %s" % r, 1)
self.logMsg("====== 200 Success ======", 1)
self.logMsg("Response: %s" % r, 1)
return r
except:
if r.headers.get('content-type') != "text/html":
log("Unable to convert the response for: %s" % url, 1)
self.logMsg("Unable to convert the response for: %s" % url, 1)
else:
r.raise_for_status()
##### EXCEPTIONS #####
except requests.exceptions.ConnectionError as e:
@ -242,8 +238,8 @@ class ConnectUtils():
pass
except requests.exceptions.ConnectTimeout as e:
log("Server timeout at: %s" % url, 0)
log(e, 1)
self.logMsg("Server timeout at: %s" % url, 0)
self.logMsg(e, 1)
except requests.exceptions.HTTPError as e:
@ -259,11 +255,11 @@ class ConnectUtils():
pass
except requests.exceptions.SSLError as e:
log("Invalid SSL certificate for: %s" % url, 0)
log(e, 1)
self.logMsg("Invalid SSL certificate for: %s" % url, 0)
self.logMsg(e, 1)
except requests.exceptions.RequestException as e:
log("Unknown error connecting to: %s" % url, 0)
log(e, 1)
self.logMsg("Unknown error connecting to: %s" % url, 0)
self.logMsg(e, 1)
return default_link

View file

@ -30,8 +30,6 @@ class DownloadUtils():
# Borg - multiple instances, shared state
_shared_state = {}
# Requests session
timeout = 30
# How many failed attempts before declaring PMS dead?
connectionAttempts = 2
# How many 401 returns before declaring unauthorized?
@ -39,6 +37,8 @@ class DownloadUtils():
def __init__(self):
self.__dict__ = self._shared_state
# Requests session
self.timeout = 30.0
def setUsername(self, username):
"""
@ -142,21 +142,22 @@ class DownloadUtils():
header.update(options)
return header
def __doDownload(self, s, type, **kwargs):
if type == "GET":
def __doDownload(self, s, action_type, **kwargs):
if action_type == "GET":
r = s.get(**kwargs)
elif type == "POST":
elif action_type == "POST":
r = s.post(**kwargs)
elif type == "DELETE":
elif action_type == "DELETE":
r = s.delete(**kwargs)
elif type == "OPTIONS":
elif action_type == "OPTIONS":
r = s.options(**kwargs)
elif type == "PUT":
elif action_type == "PUT":
r = s.put(**kwargs)
return r
def downloadUrl(self, url, type="GET", postBody=None, parameters=None,
authenticate=True, headerOptions=None, verifySSL=True):
def downloadUrl(self, url, action_type="GET", postBody=None,
parameters=None, authenticate=True, headerOptions=None,
verifySSL=True, timeout=None):
"""
Override SSL check with verifySSL=False
@ -164,14 +165,14 @@ class DownloadUtils():
Otherwise, 'empty' request will be made
Returns:
False If an error occured
None If an error occured
True If connection worked but no body was received
401, ... integer if PMS answered with HTTP error 401
(unauthorized) or other http error codes
xml xml etree root object, if applicable
JSON json() object, if applicable
"""
kwargs = {}
kwargs = {'timeout': self.timeout}
if authenticate is True:
# Get requests session
try:
@ -187,7 +188,6 @@ class DownloadUtils():
# plex.tv and to check for PMS servers
s = requests
headerOptions = self.getHeader(options=headerOptions)
kwargs['timeout'] = self.timeout
if settings('sslcert') != 'None':
kwargs['cert'] = settings('sslcert')
@ -202,10 +202,12 @@ class DownloadUtils():
kwargs['data'] = postBody
if parameters is not None:
kwargs['params'] = parameters
if timeout is not None:
kwargs['timeout'] = timeout
# ACTUAL DOWNLOAD HAPPENING HERE
try:
r = self.__doDownload(s, type, **kwargs)
r = self.__doDownload(s, action_type, **kwargs)
# THE EXCEPTIONS
except requests.exceptions.ConnectionError as e:
@ -252,6 +254,9 @@ class DownloadUtils():
if r.status_code == 204:
# No body in the response
# But read (empty) content to release connection back to pool
# (see requests: keep-alive documentation)
r.content
return True
elif r.status_code == 401:
@ -269,11 +274,11 @@ class DownloadUtils():
self.unauthorizedAttempts):
self.logMsg('We seem to be truly unauthorized for PMS'
' %s ' % url, -1)
if window('emby_serverStatus') not in ('401', 'Auth'):
if window('plex_serverStatus') not in ('401', 'Auth'):
# Tell userclient token has been revoked.
self.logMsg('Setting PMS server status to '
'unauthorized', 0)
window('emby_serverStatus', value="401")
window('plex_serverStatus', value="401")
xbmcgui.Dialog().notification(
self.addonName,
"Unauthorized for PMS",
@ -328,7 +333,7 @@ class DownloadUtils():
if int(window('countError')) >= self.connectionAttempts:
self.logMsg('Failed to connect to %s too many times. '
'Declare PMS dead' % url, -1)
window('emby_online', value="false")
window('plex_online', value="false")
except:
# 'countError' not yet set
pass

View file

@ -34,7 +34,6 @@ class Embydb_Functions():
def getViews(self):
embycursor = self.embycursor
views = []
query = ' '.join((
@ -42,8 +41,8 @@ class Embydb_Functions():
"SELECT view_id",
"FROM view"
))
embycursor.execute(query)
rows = embycursor.fetchall()
self.embycursor.execute(query)
rows = self.embycursor.fetchall()
for row in rows:
views.append(row[0])
return views
@ -68,7 +67,6 @@ class Embydb_Functions():
def getView_byId(self, viewid):
embycursor = self.embycursor
query = ' '.join((
@ -76,13 +74,13 @@ class Embydb_Functions():
"FROM view",
"WHERE view_id = ?"
))
embycursor.execute(query, (viewid,))
view = embycursor.fetchone()
self.embycursor.execute(query, (viewid,))
view = self.embycursor.fetchone()
return view
def getView_byType(self, mediatype):
embycursor = self.embycursor
views = []
query = ' '.join((
@ -91,8 +89,8 @@ class Embydb_Functions():
"FROM view",
"WHERE media_type = ?"
))
embycursor.execute(query, (mediatype,))
rows = embycursor.fetchall()
self.embycursor.execute(query, (mediatype,))
rows = self.embycursor.fetchall()
for row in rows:
views.append({
@ -105,23 +103,22 @@ class Embydb_Functions():
def getView_byName(self, tagname):
embycursor = self.embycursor
query = ' '.join((
"SELECT view_id",
"FROM view",
"WHERE view_name = ?"
))
embycursor.execute(query, (tagname,))
self.embycursor.execute(query, (tagname,))
try:
view = embycursor.fetchone()[0]
view = self.embycursor.fetchone()[0]
except TypeError:
view = None
return view
def addView(self, embyid, name, mediatype, tagid):
def addView(self, plexid, name, mediatype, tagid):
query = (
'''
@ -131,7 +128,7 @@ class Embydb_Functions():
VALUES (?, ?, ?, ?)
'''
)
self.embycursor.execute(query, (embyid, name, mediatype, tagid))
self.embycursor.execute(query, (plexid, name, mediatype, tagid))
def updateView(self, name, tagid, mediafolderid):
@ -188,9 +185,7 @@ class Embydb_Functions():
except:
return None
def getItem_byId(self, embyid):
embycursor = self.embycursor
def getItem_byId(self, plexid):
query = ' '.join((
@ -199,14 +194,12 @@ class Embydb_Functions():
"WHERE emby_id = ?"
))
try:
embycursor.execute(query, (embyid,))
item = embycursor.fetchone()
self.embycursor.execute(query, (plexid,))
item = self.embycursor.fetchone()
return item
except: return None
def getItem_byWildId(self, embyid):
embycursor = self.embycursor
def getItem_byWildId(self, plexid):
query = ' '.join((
@ -214,25 +207,19 @@ class Embydb_Functions():
"FROM emby",
"WHERE emby_id LIKE ?"
))
embycursor.execute(query, (embyid+"%",))
items = embycursor.fetchall()
return items
self.embycursor.execute(query, (plexid+"%",))
return self.embycursor.fetchall()
def getItem_byView(self, mediafolderid):
embycursor = self.embycursor
query = ' '.join((
"SELECT kodi_id",
"FROM emby",
"WHERE media_folder = ?"
))
embycursor.execute(query, (mediafolderid,))
items = embycursor.fetchall()
return items
self.embycursor.execute(query, (mediafolderid,))
return self.embycursor.fetchall()
def getPlexId(self, kodiid, mediatype):
"""
@ -253,8 +240,6 @@ class Embydb_Functions():
def getItem_byKodiId(self, kodiid, mediatype):
embycursor = self.embycursor
query = ' '.join((
"SELECT emby_id, parent_id",
@ -262,15 +247,11 @@ class Embydb_Functions():
"WHERE kodi_id = ?",
"AND media_type = ?"
))
embycursor.execute(query, (kodiid, mediatype,))
item = embycursor.fetchone()
return item
self.embycursor.execute(query, (kodiid, mediatype,))
return self.embycursor.fetchone()
def getItem_byParentId(self, parentid, mediatype):
embycursor = self.embycursor
query = ' '.join((
"SELECT emby_id, kodi_id, kodi_fileid",
@ -278,15 +259,11 @@ class Embydb_Functions():
"WHERE parent_id = ?",
"AND media_type = ?"
))
embycursor.execute(query, (parentid, mediatype,))
items = embycursor.fetchall()
return items
self.embycursor.execute(query, (parentid, mediatype,))
return self.embycursor.fetchall()
def getItemId_byParentId(self, parentid, mediatype):
embycursor = self.embycursor
query = ' '.join((
"SELECT emby_id, kodi_id",
@ -294,29 +271,21 @@ class Embydb_Functions():
"WHERE parent_id = ?",
"AND media_type = ?"
))
embycursor.execute(query, (parentid, mediatype,))
items = embycursor.fetchall()
return items
self.embycursor.execute(query, (parentid, mediatype,))
return self.embycursor.fetchall()
def getChecksum(self, mediatype):
embycursor = self.embycursor
query = ' '.join((
"SELECT emby_id, checksum",
"FROM emby",
"WHERE emby_type = ?"
))
embycursor.execute(query, (mediatype,))
items = embycursor.fetchall()
self.embycursor.execute(query, (mediatype,))
return self.embycursor.fetchall()
return items
def getMediaType_byId(self, embyid):
embycursor = self.embycursor
def getMediaType_byId(self, plexid):
query = ' '.join((
@ -324,9 +293,10 @@ class Embydb_Functions():
"FROM emby",
"WHERE emby_id = ?"
))
embycursor.execute(query, (embyid,))
self.embycursor.execute(query, (plexid,))
try:
itemtype = embycursor.fetchone()[0]
itemtype = self.embycursor.fetchone()[0]
except TypeError:
itemtype = None
@ -345,7 +315,7 @@ class Embydb_Functions():
return sorted_items
def addReference(self, embyid, kodiid, embytype, mediatype, fileid=None, pathid=None,
def addReference(self, plexid, kodiid, embytype, mediatype, fileid=None, pathid=None,
parentid=None, checksum=None, mediafolderid=None):
query = (
'''
@ -356,18 +326,18 @@ class Embydb_Functions():
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
'''
)
self.embycursor.execute(query, (embyid, kodiid, fileid, pathid, embytype, mediatype,
self.embycursor.execute(query, (plexid, kodiid, fileid, pathid, embytype, mediatype,
parentid, checksum, mediafolderid))
def updateReference(self, embyid, checksum):
def updateReference(self, plexid, checksum):
query = "UPDATE emby SET checksum = ? WHERE emby_id = ?"
self.embycursor.execute(query, (checksum, embyid))
self.embycursor.execute(query, (checksum, plexid))
def updateParentId(self, embyid, parent_kodiid):
def updateParentId(self, plexid, parent_kodiid):
query = "UPDATE emby SET parent_id = ? WHERE emby_id = ?"
self.embycursor.execute(query, (parent_kodiid, embyid))
self.embycursor.execute(query, (parent_kodiid, plexid))
def removeItems_byParentId(self, parent_kodiid, mediatype):
@ -389,13 +359,13 @@ class Embydb_Functions():
))
self.embycursor.execute(query, (kodiid, mediatype,))
def removeItem(self, embyid):
def removeItem(self, plexid):
query = "DELETE FROM emby WHERE emby_id = ?"
self.embycursor.execute(query, (embyid,))
self.embycursor.execute(query, (plexid,))
def removeWildItem(self, embyid):
def removeWildItem(self, plexid):
query = "DELETE FROM emby WHERE emby_id LIKE ?"
self.embycursor.execute(query, (embyid+"%",))
self.embycursor.execute(query, (plexid+"%",))

File diff suppressed because it is too large Load diff

View file

@ -12,6 +12,7 @@ import downloadutils
import userclient
import PlexAPI
from PlexFunctions import GetMachineIdentifier
###############################################################################
@ -20,129 +21,271 @@ import PlexAPI
class InitialSetup():
def __init__(self):
self.logMsg('Entering initialsetup class', 1)
self.clientInfo = clientinfo.ClientInfo()
self.addonId = self.clientInfo.getAddonId()
self.doUtils = downloadutils.DownloadUtils().downloadUrl
self.userClient = userclient.UserClient()
self.plx = PlexAPI.PlexAPI()
self.dialog = xbmcgui.Dialog()
def setup(self, forcePlexTV=False, chooseServer=False):
"""
Initial setup. Run once upon startup.
Check server, user, direct paths, music, direct stream if not direct
path.
"""
string = xbmcaddon.Addon().getLocalizedString
# SERVER INFO #####
self.logMsg("Initial setup called.", 0)
server = self.userClient.getServer()
serverid = utils.settings('plex_machineIdentifier')
self.string = xbmcaddon.Addon().getLocalizedString
self.server = self.userClient.getServer()
self.serverid = utils.settings('plex_machineIdentifier')
# Get Plex credentials from settings file, if they exist
plexdict = self.plx.GetPlexLoginFromSettings()
myplexlogin = plexdict['myplexlogin']
plexLogin = plexdict['plexLogin']
plexToken = plexdict['plexToken']
plexid = plexdict['plexid']
if plexToken:
self.logMsg('Found plex.tv token in settings', 0)
self.myplexlogin = plexdict['myplexlogin'] == 'true'
self.plexLogin = plexdict['plexLogin']
self.plexToken = plexdict['plexToken']
self.plexid = plexdict['plexid']
if self.plexToken:
self.logMsg('Found a plex.tv token in the settings', 1)
dialog = xbmcgui.Dialog()
def PlexTVSignIn(self):
"""
Signs (freshly) in to plex.tv (will be saved to file settings)
# Optionally sign into plex.tv. Will not be called on very first run
# as plexToken will be ''
if (plexToken and myplexlogin == 'true' and forcePlexTV is False
and chooseServer is False):
chk = self.plx.CheckConnection('plex.tv', plexToken)
try:
chk.attrib
except:
pass
else:
# Success - we downloaded an xml!
chk = 200
Returns True if successful, or False if not
"""
result = self.plx.PlexTvSignInWithPin()
if result:
self.plexLogin = result['username']
self.plexToken = result['token']
self.plexid = result['plexid']
return True
return False
def CheckPlexTVSignIn(self):
"""
Checks existing connection to plex.tv. If not, triggers sign in
Returns True if signed in, False otherwise
"""
answer = True
chk = self.plx.CheckConnection('plex.tv', token=self.plexToken)
if chk in (401, 403):
# HTTP Error: unauthorized. Token is no longer valid
if chk in (401, 403):
self.logMsg('plex.tv connection returned HTTP %s' % chk, 0)
# Delete token in the settings
utils.settings('plexToken', value='')
# Could not login, please try again
dialog.ok(self.addonName,
string(39009))
result = self.plx.PlexTvSignInWithPin()
if result:
plexLogin = result['username']
plexToken = result['token']
plexid = result['plexid']
elif chk is False or chk >= 400:
# Problems connecting to plex.tv. Network or internet issue?
self.logMsg('plex.tv connection returned HTTP %s'
% str(chk), 0)
dialog.ok(self.addonName,
string(39010))
self.logMsg('plex.tv connection returned HTTP %s' % str(chk), 1)
# Delete token in the settings
utils.settings('plexToken', value='')
utils.settings('plexLogin', value='')
# Could not login, please try again
self.dialog.ok(self.addonName,
self.string(39009))
answer = self.PlexTVSignIn()
elif chk is False or chk >= 400:
# Problems connecting to plex.tv. Network or internet issue?
self.logMsg('Problems connecting to plex.tv; connection returned '
'HTTP %s' % str(chk), 1)
self.dialog.ok(self.addonName,
self.string(39010))
answer = False
else:
self.logMsg('plex.tv connection with token successful', 1)
utils.settings('plex_status', value='Logged in to plex.tv')
# Refresh the info from Plex.tv
xml = self.doUtils('https://plex.tv/users/account',
authenticate=False,
headerOptions={'X-Plex-Token': self.plexToken})
try:
self.plexLogin = xml.attrib['title']
except (AttributeError, KeyError):
self.logMsg('Failed to update Plex info from plex.tv', -1)
else:
self.logMsg('plex.tv connection with token successful', 0)
# Refresh the info from Plex.tv
xml = self.doUtils('https://plex.tv/users/account',
authenticate=False,
headerOptions={'X-Plex-Token': plexToken})
try:
xml.attrib
except:
self.logMsg('Failed to update Plex info from plex.tv', -1)
utils.settings('plexLogin', value=self.plexLogin)
home = 'true' if xml.attrib.get('home') == '1' else 'false'
utils.settings('plexhome', value=home)
utils.settings('plexAvatar', value=xml.attrib.get('thumb'))
utils.settings(
'plexHomeSize', value=xml.attrib.get('homeSize', '1'))
self.logMsg('Updated Plex info from plex.tv', 1)
return answer
def CheckPMS(self):
"""
Check the PMS that was set in file settings.
Will return False if we need to reconnect, because:
PMS could not be reached (no matter the authorization)
machineIdentifier did not match
Will also set the PMS machineIdentifier in the file settings if it was
not set before
"""
answer = True
chk = self.plx.CheckConnection(self.server,
verifySSL=False)
if chk is False:
self.logMsg('Could not reach PMS %s' % self.server, -1)
answer = False
if answer is True and not self.serverid:
self.logMsg('No PMS machineIdentifier found for %s. Trying to '
'get the PMS unique ID' % self.server, 1)
self.serverid = GetMachineIdentifier(self.server)
if self.serverid is None:
self.logMsg('Could not retrieve machineIdentifier', -1)
answer = False
else:
utils.settings('plex_machineIdentifier', value=self.serverid)
elif answer is True:
tempServerid = GetMachineIdentifier(self.server)
if tempServerid != self.serverid:
self.logMsg('The current PMS %s was expected to have a '
'unique machineIdentifier of %s. But we got '
'%s. Pick a new server to be sure'
% (self.server, self.serverid, tempServerid), 1)
answer = False
return answer
def _getServerList(self):
"""
Returns a list of servers from GDM and possibly plex.tv
"""
self.plx.discoverPMS(xbmc.getIPAddress(),
plexToken=self.plexToken)
serverlist = self.plx.returnServerList(self.plx.g_PMS)
self.logMsg('PMS serverlist: %s' % serverlist, 2)
return serverlist
def _checkServerCon(self, server):
"""
Checks for server's connectivity. Returns CheckConnection result
"""
# Re-direct via plex if remote - will lead to the correct SSL
# certificate
if server['local'] == '1':
url = '%s://%s:%s' \
% (server['scheme'], server['ip'], server['port'])
# Deactive SSL verification if the server is local!
verifySSL = False
else:
url = server['baseURL']
verifySSL = None
chk = self.plx.CheckConnection(url,
token=server['accesstoken'],
verifySSL=verifySSL)
return chk
def PickPMS(self, showDialog=False):
"""
Searches for PMS in local Lan and optionally (if self.plexToken set)
also on plex.tv
showDialog=True: let the user pick one
showDialog=False: automatically pick PMS based on machineIdentifier
Returns the picked PMS' detail as a dict:
{
'name': friendlyName, the Plex server's name
'address': ip:port
'ip': ip, without http/https
'port': port
'scheme': 'http'/'https', nice for checking for secure connections
'local': '1'/'0', Is the server a local server?
'owned': '1'/'0', Is the server owned by the user?
'machineIdentifier': id, Plex server machine identifier
'accesstoken': token Access token to this server
'baseURL': baseURL scheme://ip:port
'ownername' Plex username of PMS owner
}
or None if unsuccessful
"""
server = None
# If no server is set, let user choose one
if not self.server or not self.serverid:
showDialog = True
if showDialog is True:
server = self._UserPickPMS()
else:
server = self._AutoPickPMS()
return server
def _AutoPickPMS(self):
"""
Will try to pick PMS based on machineIdentifier saved in file settings
but only once
Returns server or None if unsuccessful
"""
httpsUpdated = False
checkedPlexTV = False
server = None
while True:
if httpsUpdated is False:
serverlist = self._getServerList()
for item in serverlist:
if item.get('machineIdentifier') == self.serverid:
server = item
if server is None:
name = utils.settings('plex_servername')
self.logMsg('The PMS you have used before with a unique '
'machineIdentifier of %s and name %s is '
'offline' % (self.serverid, name), -1)
# "PMS xyz offline"
self.dialog.notification(self.addonName,
'%s %s'
% (name, self.string(39213)),
xbmcgui.NOTIFICATION_ERROR,
7000,
False)
return
chk = self._checkServerCon(server)
if chk == 504 and httpsUpdated is False:
# Not able to use HTTP, try HTTPs for now
server['scheme'] = 'https'
httpsUpdated = True
continue
if chk == 401:
self.logMsg('Not yet authorized for Plex server %s'
% server['name'], -1)
if self.CheckPlexTVSignIn() is True:
if checkedPlexTV is False:
# Try again
checkedPlexTV = True
httpsUpdated = False
continue
else:
self.logMsg('Not authorized even though we are signed '
' in to plex.tv correctly', -1)
self.dialog.ok(self.addonName, '%s %s'
% self.string(39214) + server['name'])
return
else:
plexLogin = xml.attrib.get('title')
utils.settings('plexLogin', value=plexLogin)
home = 'true' if xml.attrib.get('home') == '1' else 'false'
utils.settings('plexhome', value=home)
utils.settings('plexAvatar', value=xml.attrib.get('thumb'))
utils.settings(
'plexHomeSize', value=xml.attrib.get('homeSize', '1'))
self.logMsg('Updated Plex info from plex.tv', 0)
return
# Problems connecting
elif chk >= 400 or chk is False:
self.logMsg('Problems connecting to server %s. chk is %s'
% (server['name'], chk), -1)
return
self.logMsg('We found a server to automatically connect to: %s'
% server['name'], 1)
return server
# If a Plex server IP has already been set, return.
if server and forcePlexTV is False and chooseServer is False:
self.logMsg("Server is already set.", 0)
self.logMsg("url: %s, Plex machineIdentifier: %s"
% (server, serverid), 0)
return
def _UserPickPMS(self):
"""
Lets user pick his/her PMS from a list
# If not already retrieved myplex info, optionally let user sign in
# to plex.tv. This DOES get called on very first install run
if ((not plexToken and myplexlogin == 'true' and chooseServer is False)
or forcePlexTV):
result = self.plx.PlexTvSignInWithPin()
if result:
plexLogin = result['username']
plexToken = result['token']
plexid = result['plexid']
# Get g_PMS list of servers (saved to plx.g_PMS)
Returns server or None if unsuccessful
"""
httpsUpdated = False
while True:
if httpsUpdated is False:
# Populate g_PMS variable with the found Plex servers
self.plx.discoverPMS(xbmc.getIPAddress(),
plexToken=plexToken)
isconnected = False
self.logMsg('g_PMS: %s' % self.plx.g_PMS, 1)
serverlist = self.plx.returnServerList(self.plx.g_PMS)
self.logMsg('PMS serverlist: %s' % serverlist, 2)
# Let user pick server from a list
# Get a nicer list
dialoglist = []
serverlist = self._getServerList()
# Exit if no servers found
if len(serverlist) == 0:
dialog.ok(
self.addonName,
string(39011)
)
break
self.logMsg('No plex media servers found!', -1)
self.dialog.ok(self.addonName, self.string(39011))
return
# Get a nicer list
dialoglist = []
for server in serverlist:
if server['local'] == '1':
# server is in the same network as client. Add "local"
msg = string(39022)
# server is in the same network as client.
# Add"local"
msg = self.string(39022)
else:
# Add 'remote'
msg = string(39054)
msg = self.string(39054)
if server.get('ownername'):
# Display username if its not our PMS
dialoglist.append('%s (%s, %s)'
@ -151,33 +294,12 @@ class InitialSetup():
msg))
else:
dialoglist.append('%s (%s)'
% (server['name'],
msg))
resp = dialog.select(string(39012), dialoglist)
% (server['name'], msg))
# Let user pick server from a list
resp = self.dialog.select(self.string(39012), dialoglist)
server = serverlist[resp]
activeServer = server['machineIdentifier']
# Re-direct via plex if remote - will lead to the correct SSL
# certificate
if server['local'] == '1':
url = server['scheme'] + '://' + server['ip'] + ':' \
+ server['port']
else:
url = server['baseURL']
# Deactive SSL verification if the server is local!
# Watch out - settings is cached by Kodi - use dedicated var!
if server['local'] == '1':
utils.settings('sslverify', 'false')
self.logMsg("Setting SSL verify to false, because server is "
"local", 1)
verifySSL = False
else:
utils.settings('sslverify', 'true')
self.logMsg("Setting SSL verify to true, because server is "
"not local", 1)
verifySSL = None
chk = self.plx.CheckConnection(url,
server['accesstoken'],
verifySSL=verifySSL)
chk = self._checkServerCon(server)
if chk == 504 and httpsUpdated is False:
# Not able to use HTTP, try HTTPs for now
serverlist[resp]['scheme'] = 'https'
@ -185,68 +307,124 @@ class InitialSetup():
continue
httpsUpdated = False
if chk == 401:
# Not yet authorized for Plex server
self.logMsg('Not yet authorized for Plex server %s'
% server['name'], -1)
# Please sign in to plex.tv
dialog.ok(self.addonName,
string(39013) + server['name'],
string(39014))
result = self.plx.PlexTvSignInWithPin()
if result:
plexLogin = result['username']
plexToken = result['token']
plexid = result['plexid']
else:
self.dialog.ok(self.addonName,
self.string(39013) + server['name'],
self.string(39014))
if self.PlexTVSignIn() is False:
# Exit while loop if user cancels
break
return
# Problems connecting
elif chk >= 400 or chk is False:
# Problems connecting to server. Pick another server?
resp = dialog.yesno(self.addonName,
string(39015))
answ = self.dialog.yesno(self.addonName,
self.string(39015))
# Exit while loop if user chooses No
if not resp:
break
if not answ:
return
# Otherwise: connection worked!
else:
isconnected = True
break
if not isconnected:
# Enter Kodi settings instead
xbmc.executebuiltin('Addon.OpenSettings(%s)' % self.addonId)
return
# Write to Kodi settings file
utils.settings('plex_machineIdentifier', activeServer)
return server
def WritePMStoSettings(self, server):
"""
Saves server to file settings. server is a dict of the form:
{
'name': friendlyName, the Plex server's name
'address': ip:port
'ip': ip, without http/https
'port': port
'scheme': 'http'/'https', nice for checking for secure connections
'local': '1'/'0', Is the server a local server?
'owned': '1'/'0', Is the server owned by the user?
'machineIdentifier': id, Plex server machine identifier
'accesstoken': token Access token to this server
'baseURL': baseURL scheme://ip:port
'ownername' Plex username of PMS owner
}
"""
utils.settings('plex_machineIdentifier', server['machineIdentifier'])
utils.settings('plex_servername', server['name'])
utils.settings('plex_serverowned',
'true' if server['owned'] == '1'
else 'false')
# Careful to distinguish local from remote PMS
if server['local'] == '1':
scheme = server['scheme']
utils.settings('ipaddress', server['ip'])
utils.settings('port', server['port'])
self.logMsg("Setting SSL verify to false, because server is "
"local", 1)
utils.settings('sslverify', 'false')
else:
baseURL = server['baseURL'].split(':')
scheme = baseURL[0]
utils.settings('ipaddress', baseURL[1].replace('//', ''))
utils.settings('port', baseURL[2])
self.logMsg("Setting SSL verify to true, because server is not "
"local", 1)
utils.settings('sslverify', 'true')
if scheme == 'https':
utils.settings('https', 'true')
else:
utils.settings('https', 'false')
# And finally do some logging
self.logMsg("Writing to Kodi user settings file", 0)
self.logMsg("PMS machineIdentifier: %s, ip: %s, port: %s, https: %s "
% (activeServer, server['ip'], server['port'],
server['scheme']), 0)
% (server['machineIdentifier'], server['ip'],
server['port'], server['scheme']), 0)
if forcePlexTV is True or chooseServer is True:
def setup(self):
"""
Initial setup. Run once upon startup.
Check server, user, direct paths, music, direct stream if not direct
path.
"""
self.logMsg("Initial setup called.", 0)
dialog = self.dialog
string = self.string
# Optionally sign into plex.tv. Will not be called on very first run
# as plexToken will be ''
utils.settings('plex_status', value='Not logged in to plex.tv')
if self.plexToken and self.myplexlogin:
self.CheckPlexTVSignIn()
# If a Plex server IP has already been set
# return only if the right machine identifier is found
getNewIP = False
if self.server:
self.logMsg("PMS is already set: %s. Checking now..."
% self.server, 0)
getNewIP = not self.CheckPMS()
if getNewIP is False:
self.logMsg("Using PMS %s with machineIdentifier %s"
% (self.server, self.serverid), 0)
return
# If not already retrieved myplex info, optionally let user sign in
# to plex.tv. This DOES get called on very first install run
if not self.plexToken and self.myplexlogin:
self.PlexTVSignIn()
server = self.PickPMS()
if server is not None:
# Write our chosen server to Kodi settings file
self.WritePMStoSettings(server)
# User already answered the installation questions
if utils.settings('InstallQuestionsAnswered') == 'true':
return
goToSettings = False
# Additional settings where the user needs to choose
# Direct paths (\\NAS\mymovie.mkv) or addon (http)?
if dialog.yesno(heading=self.addonName,
line1=string(39027),
line2=string(39028),
if dialog.yesno(self.addonName,
string(39027),
string(39028),
nolabel="Addon (Default)",
yeslabel="Native (Direct Paths)"):
self.logMsg("User opted to use direct paths.", 1)
@ -284,15 +462,36 @@ class InitialSetup():
else:
utils.advancedSettingsXML()
# Download additional art from FanArtTV
if dialog.yesno(heading=self.addonName,
line1=string(39061)):
self.logMsg("User opted to use FanArtTV", 1)
utils.settings('FanartTV', value="true")
# Is your Kodi installed on a low-powered device like a Raspberry Pi?
# If yes, then we will reduce the strain on Kodi to prevent it from
# crashing.
if dialog.yesno(heading=self.addonName,
line1=string(39072)):
self.logMsg('User thinks that PKC runs on a raspi or similar', 1)
utils.settings('imageCacheLimit', value='1')
# Make sure that we only ask these questions upon first installation
utils.settings('InstallQuestionsAnswered', value='true')
if goToSettings is False:
# Open Settings page now? You will need to restart!
goToSettings = dialog.yesno(heading=self.addonName,
line1=string(39017))
if goToSettings:
utils.window('emby_serverStatus', value="Stop")
utils.window('plex_serverStatus', value="Stop")
xbmc.executebuiltin(
'Addon.OpenSettings(plugin.video.plexkodiconnect)')
else:
# "Kodi will now restart to apply the changes"
dialog.ok(
heading=self.addonName,
line1=string(33033))
xbmc.executebuiltin('RestartApp')
# We should always restart to ensure e.g. Kodi settings for Music
# are in use!

View file

@ -11,7 +11,6 @@ import xbmcgui
import xbmcvfs
import artwork
import downloadutils
import utils
import embydb_functions as embydb
import kodidb_functions as kodidb
@ -34,7 +33,6 @@ class Items(object):
"""
def __init__(self):
self.doUtils = downloadutils.DownloadUtils()
self.kodiversion = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
# self.directpath = utils.settings('useDirectPaths') == "1"
self.directpath = True if utils.window('useDirectPaths') == 'true' \
@ -264,14 +262,12 @@ class Items(object):
# If the playback was stopped, check whether we need to increment the
# playcount. PMS won't tell us the playcount via websockets
if item['state'] in ('stopped', 'ended'):
markPlayed = 0.90
complete = float(item['viewOffset']) / float(item['duration'])
complete = complete * 100
self.logMsg('Item %s stopped with completion rate %s percent.'
'Mark item played at %s percent.'
% (item['ratingKey'],
str(complete),
utils.settings('markPlayed')), 1)
if complete >= float(utils.settings('markPlayed')):
% (item['ratingKey'], str(complete), markPlayed), 1)
if complete >= markPlayed:
self.logMsg('Marking as completely watched in Kodi', 1)
try:
item['viewCount'] += 1
@ -309,31 +305,17 @@ class Movies(Items):
count = 0
for boxset in items:
title = boxset['Name']
if pdialog:
percentage = int((float(count) / float(total))*100)
pdialog.update(percentage, message=title)
pdialog.update(percentage, message=boxset['Name'])
count += 1
self.add_updateBoxset(boxset)
@utils.CatchExceptions(warnuser=True)
def add_update(self, item, viewtag=None, viewid=None):
try:
self.run_add_update(item, viewtag, viewid)
except Exception as e:
self.logMsg('itemtypes.py for movies has crashed for item %s. '
'Error:' % item.attrib.get('ratingKey', None), -1)
self.logMsg(e, -1)
import traceback
self.logMsg("Traceback:\n%s" % traceback.format_exc(), 0)
utils.window('plex_scancrashed', value='true')
# skip this item for now
return
def run_add_update(self, item, viewtag=None, viewid=None):
# Process single movie
kodicursor = self.kodicursor
emby_db = self.emby_db
kodi_db = self.kodi_db
artwork = self.artwork
API = PlexAPI.API(item)
@ -417,7 +399,7 @@ class Movies(Items):
doIndirect = not self.directpath
if self.directpath:
# Direct paths is set the Kodi way
playurl = API.getFilePath()
playurl = API.getFilePath(forceFirstMediaStream=True)
if playurl is None:
# Something went wrong, trying to use non-direct paths
doIndirect = True
@ -436,7 +418,7 @@ class Movies(Items):
# Set plugin path and media flags using real filename
path = "plugin://plugin.video.plexkodiconnect.movies/"
params = {
'filename': API.getKey().encode('utf-8'),
'filename': API.getKey(),
'id': itemid,
'dbid': movieid,
'mode': "play"
@ -444,11 +426,6 @@ class Movies(Items):
filename = "%s?%s" % (path, urllib.urlencode(params))
playurl = filename
# Even if the item is only updated, the file may have been moved or updated.
# In the worst case we get exactly the same values as we had before.
pathid = kodi_db.addPath(path)
fileid = kodi_db.addFile(filename, pathid)
# movie table:
# c22 - playurl
# c23 - pathid
@ -477,6 +454,10 @@ class Movies(Items):
##### OR ADD THE MOVIE #####
else:
self.logMsg("ADD movie itemid: %s - Title: %s" % (itemid, title), 1)
# Add path
pathid = self.kodi_db.addPath(path)
# Add the file
fileid = self.kodi_db.addFile(filename, pathid)
# Create the movie entry
query = (
@ -514,28 +495,30 @@ class Movies(Items):
kodicursor.execute(query, (pathid, filename, dateadded, fileid))
# Process countries
kodi_db.addCountries(movieid, countries, "movie")
self.kodi_db.addCountries(movieid, countries, "movie")
# Process cast
people = API.getPeopleList()
kodi_db.addPeople(movieid, people, "movie")
self.kodi_db.addPeople(movieid, people, "movie")
# Process genres
kodi_db.addGenres(movieid, genres, "movie")
self.kodi_db.addGenres(movieid, genres, "movie")
# Process artwork
allartworks = API.getAllArtwork()
artwork.addArtwork(allartworks, movieid, "movie", kodicursor)
# Process stream details
streams = API.getMediaStreams()
kodi_db.addStreams(fileid, streams, runtime)
self.kodi_db.addStreams(fileid, streams, runtime)
# Process studios
kodi_db.addStudios(movieid, studios, "movie")
self.kodi_db.addStudios(movieid, studios, "movie")
# Process tags: view, Plex collection tags
tags = [viewtag]
tags.extend(collections)
if userdata['Favorite']:
tags.append("Favorite movies")
kodi_db.addTags(movieid, tags, "movie")
self.kodi_db.addTags(movieid, tags, "movie")
# Add any sets from Plex collection tags
self.kodi_db.addSets(movieid, collections, kodicursor, API)
# Process playstates
kodi_db.addPlaystate(fileid, resume, runtime, playcount, dateplayed)
self.kodi_db.addPlaystate(fileid, resume, runtime, playcount, dateplayed)
def remove(self, itemid):
# Remove movieid, fileid, emby reference
@ -566,11 +549,11 @@ class Movies(Items):
# Delete kodi boxset
boxset_movies = emby_db.getItem_byParentId(kodiid, "movie")
for movie in boxset_movies:
embyid = movie[0]
plexid = movie[0]
movieid = movie[1]
self.kodi_db.removefromBoxset(movieid)
# Update emby reference
emby_db.updateParentId(embyid, None)
emby_db.updateParentId(plexid, None)
kodicursor.execute("DELETE FROM sets WHERE idSet = ?", (kodiid,))
@ -603,9 +586,8 @@ class MusicVideos(Items):
# Process single music video
kodicursor = self.kodicursor
emby_db = self.emby_db
kodi_db = self.kodi_db
artwork = self.artwork
API = api.API(item)
API = PlexAPI.API(item)
# If the item already exist in the local Kodi DB we'll perform a full item update
# If the item doesn't exist, we'll add it to the database
@ -638,7 +620,7 @@ class MusicVideos(Items):
if not viewtag or not viewid:
# Get view tag from emby
viewtag, viewid, mediatype = self.emby.getView_embyId(itemid)
viewtag, viewid, mediatype = self.emby.getView_plexid(itemid)
self.logMsg("View tag found: %s" % viewtag, 2)
# fileId information
@ -675,7 +657,7 @@ class MusicVideos(Items):
if self.directpath:
# Direct paths is set the Kodi way
if utils.window('emby_pathverified') != "true" and not xbmcvfs.exists(playurl):
if utils.window('plex_pathverified') != "true" and not xbmcvfs.exists(playurl):
# Validate the path is correct with user intervention
resp = xbmcgui.Dialog().yesno(
heading="Can't validate path",
@ -686,17 +668,17 @@ class MusicVideos(Items):
"to format your path correctly. Stop syncing?"
% playurl))
if resp:
utils.window('emby_shouldStop', value="true")
utils.window('plex_shouldStop', value="true")
return False
path = playurl.replace(filename, "")
utils.window('emby_pathverified', value="true")
utils.window('plex_pathverified', value="true")
else:
# Set plugin path and media flags using real filename
path = "plugin://plugin.video.plexkodiconnect.musicvideos/"
params = {
'filename': filename.encode('utf-8'),
'filename': utils.tryEncode(filename),
'id': itemid,
'dbid': mvideoid,
'mode': "play"
@ -794,32 +776,31 @@ class MusicVideos(Items):
artist['Type'] = "Artist"
people.extend(artists)
people = artwork.getPeopleArtwork(people)
kodi_db.addPeople(mvideoid, people, "musicvideo")
self.kodi_db.addPeople(mvideoid, people, "musicvideo")
# Process genres
kodi_db.addGenres(mvideoid, genres, "musicvideo")
self.kodi_db.addGenres(mvideoid, genres, "musicvideo")
# Process artwork
artwork.addArtwork(artwork.getAllArtwork(item), mvideoid, "musicvideo", kodicursor)
# Process stream details
streams = API.getMediaStreams()
kodi_db.addStreams(fileid, streams, runtime)
self.kodi_db.addStreams(fileid, streams, runtime)
# Process studios
kodi_db.addStudios(mvideoid, studios, "musicvideo")
self.kodi_db.addStudios(mvideoid, studios, "musicvideo")
# Process tags: view, emby tags
tags = [viewtag]
tags.extend(item['Tags'])
if userdata['Favorite']:
tags.append("Favorite musicvideos")
kodi_db.addTags(mvideoid, tags, "musicvideo")
self.kodi_db.addTags(mvideoid, tags, "musicvideo")
# Process playstates
resume = API.adjustResume(userdata['Resume'])
total = round(float(runtime), 6)
kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed)
self.kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed)
def updateUserdata(self, item):
# This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
# Poster with progress bar
emby_db = self.emby_db
kodi_db = self.kodi_db
API = api.API(item)
# Get emby information
@ -841,9 +822,9 @@ class MusicVideos(Items):
# Process favorite tags
if userdata['Favorite']:
kodi_db.addTag(mvideoid, "Favorite musicvideos", "musicvideo")
self.kodi_db.addTag(mvideoid, "Favorite musicvideos", "musicvideo")
else:
kodi_db.removeTag(mvideoid, "Favorite musicvideos", "musicvideo")
self.kodi_db.removeTag(mvideoid, "Favorite musicvideos", "musicvideo")
# Process playstates
playcount = userdata['PlayCount']
@ -851,7 +832,7 @@ class MusicVideos(Items):
resume = API.adjustResume(userdata['Resume'])
total = round(float(runtime), 6)
kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed)
self.kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed)
emby_db.updateReference(itemid, checksum)
def remove(self, itemid):
@ -878,8 +859,7 @@ class MusicVideos(Items):
"AND media_type = 'musicvideo'"
))
kodicursor.execute(query, (mvideoid,))
rows = kodicursor.fetchall()
for row in rows:
for row in kodicursor.fetchall():
url = row[0]
imagetype = row[1]
@ -942,24 +922,11 @@ class TVShows(Items):
if not pdialog and self.contentmsg:
self.contentPop(title, self.newvideo_time)
@utils.CatchExceptions(warnuser=True)
def add_update(self, item, viewtag=None, viewid=None):
try:
self.run_add_update(item, viewtag, viewid)
except Exception as e:
self.logMsg('itemtypes.py for tv show has crashed for item %s. '
'Error:' % item.attrib.get('ratingKey', None), -1)
self.logMsg(e, -1)
import traceback
self.logMsg("Traceback:\n%s" % traceback.format_exc(), 0)
utils.window('plex_scancrashed', value='true')
# skip this item for now
return
def run_add_update(self, item, viewtag=None, viewid=None):
# Process single tvshow
kodicursor = self.kodicursor
emby_db = self.emby_db
kodi_db = self.kodi_db
artwork = self.artwork
API = PlexAPI.API(item)
@ -997,7 +964,7 @@ class TVShows(Items):
if viewtag is None or viewid is None:
# Get view tag from emby
viewtag, viewid, mediatype = embyserver.getView_embyId(itemid)
viewtag, viewid, mediatype = embyserver.getView_plexid(itemid)
self.logMsg("View tag found: %s" % viewtag, 2)
# fileId information
@ -1047,7 +1014,7 @@ class TVShows(Items):
path = "%s%s/" % (toplevelpath, itemid)
# Add top path
toppathid = kodi_db.addPath(toplevelpath)
toppathid = self.kodi_db.addPath(toplevelpath)
# UPDATE THE TVSHOW #####
if update_item:
self.logMsg("UPDATE tvshow itemid: %s - Title: %s" % (itemid, title), 1)
@ -1079,7 +1046,7 @@ class TVShows(Items):
kodicursor.execute(query, (toplevelpath, "tvshows", "metadata.local", 1, toppathid))
# Add path
pathid = kodi_db.addPath(path)
pathid = self.kodi_db.addPath(path)
# Create the tvshow entry
query = (
@ -1112,18 +1079,18 @@ class TVShows(Items):
# Process cast
people = API.getPeopleList()
kodi_db.addPeople(showid, people, "tvshow")
self.kodi_db.addPeople(showid, people, "tvshow")
# Process genres
kodi_db.addGenres(showid, genres, "tvshow")
self.kodi_db.addGenres(showid, genres, "tvshow")
# Process artwork
allartworks = API.getAllArtwork()
artwork.addArtwork(allartworks, showid, "tvshow", kodicursor)
# Process studios
kodi_db.addStudios(showid, studios, "tvshow")
self.kodi_db.addStudios(showid, studios, "tvshow")
# Process tags: view, PMS collection tags
tags = [viewtag]
tags.extend(collections)
kodi_db.addTags(showid, tags, "tvshow")
self.kodi_db.addTags(showid, tags, "tvshow")
if force_episodes:
# We needed to recreate the show entry. Re-add episodes now.
@ -1133,20 +1100,8 @@ class TVShows(Items):
self.kodiconn.commit()
self.embyconn.commit()
@utils.CatchExceptions(warnuser=True)
def add_updateSeason(self, item, viewtag=None, viewid=None):
try:
self.run_add_updateSeason(item, viewtag, viewid)
except Exception as e:
self.logMsg('itemtypes.py for tv seasons has crashed for item %s. '
'Error:' % item.attrib.get('ratingKey', None), -1)
self.logMsg(e, -1)
import traceback
self.logMsg("Traceback:\n%s" % traceback.format_exc(), 0)
utils.window('plex_scancrashed', value='true')
# skip this item for now
return
def run_add_updateSeason(self, item, viewtag=None, viewid=None):
API = PlexAPI.API(item)
itemid = API.getRatingKey()
if not itemid:
@ -1154,7 +1109,6 @@ class TVShows(Items):
return
kodicursor = self.kodicursor
emby_db = self.emby_db
kodi_db = self.kodi_db
artwork = self.artwork
seasonnum = API.getIndex()
# Get parent tv show Plex id
@ -1169,7 +1123,7 @@ class TVShows(Items):
% (itemid), -1)
return
seasonid = kodi_db.addSeason(showid, seasonnum)
seasonid = self.kodi_db.addSeason(showid, seasonnum)
checksum = API.getChecksum()
# Check whether Season already exists
update_item = True
@ -1192,28 +1146,14 @@ class TVShows(Items):
self.kodiconn.commit()
self.embyconn.commit()
@utils.CatchExceptions(warnuser=True)
def add_updateEpisode(self, item, viewtag=None, viewid=None):
try:
self.run_add_updateEpisode(item, viewtag, viewid)
except Exception as e:
self.logMsg('itemtypes.py for tv episode has crashed for item %s. '
'Error:' % item.attrib.get('ratingKey', None), -1)
self.logMsg(e, -1)
import traceback
self.logMsg("Traceback:\n%s" % traceback.format_exc(), 0)
utils.window('plex_scancrashed', value='true')
# skip this item for now
return
def run_add_updateEpisode(self, item, viewtag=None, viewid=None):
"""
viewtag and viewid are irrelevant!
"""
# Process single episode
kodiversion = self.kodiversion
kodicursor = self.kodicursor
emby_db = self.emby_db
kodi_db = self.kodi_db
artwork = self.artwork
API = PlexAPI.API(item)
@ -1310,11 +1250,11 @@ class TVShows(Items):
# self.logMsg("Skipping: %s. Unable to add series: %s." % (itemid, seriesId), -1)
self.logMsg("Parent tvshow now found, skip item", 2)
return False
seasonid = kodi_db.addSeason(showid, season)
seasonid = self.kodi_db.addSeason(showid, season)
# GET THE FILE AND PATH #####
doIndirect = not self.directpath
playurl = API.getFilePath()
playurl = API.getFilePath(forceFirstMediaStream=True)
if self.directpath:
# Direct paths is set the Kodi way
if playurl is None:
@ -1331,7 +1271,7 @@ class TVShows(Items):
# Network share
filename = playurl.rsplit("/", 1)[1]
path = playurl.replace(filename, "")
parentPathId = kodi_db.getParentPathId(path)
parentPathId = self.kodi_db.getParentPathId(path)
if doIndirect:
# Set plugin path and media flags using real filename
if playurl is not None:
@ -1340,24 +1280,20 @@ class TVShows(Items):
else:
filename = playurl.rsplit('/', 1)[1]
else:
filename = 'file_not_found'
filename = 'file_not_found.mkv'
path = "plugin://plugin.video.plexkodiconnect.tvshows/%s/" % seriesId
params = {
'filename': filename.encode('utf-8'),
'filename': utils.tryEncode(filename),
'id': itemid,
'dbid': episodeid,
'mode': "play"
}
filename = "%s?%s" % (path, urllib.urlencode(params))
filename = "%s?%s" % (path,
utils.tryDecode(urllib.urlencode(params)))
playurl = filename
parentPathId = kodi_db.addPath(
parentPathId = self.kodi_db.addPath(
'plugin://plugin.video.plexkodiconnect.tvshows/')
# Even if the item is only updated, the file may have been moved or updated.
# In the worst case we get exactly the same values as we had before.
pathid = kodi_db.addPath(path)
fileid = kodi_db.addFile(filename, pathid)
# episodes table:
# c18 - playurl
# c19 - pathid
@ -1368,7 +1304,7 @@ class TVShows(Items):
self.logMsg("UPDATE episode itemid: %s" % (itemid), 1)
# Update the movie entry
if kodiversion in (16, 17):
if self.kodiversion in (16, 17):
# Kodi Jarvis, Krypton
query = ' '.join((
@ -1400,10 +1336,13 @@ class TVShows(Items):
##### OR ADD THE EPISODE #####
else:
self.logMsg("ADD episode itemid: %s" % (itemid), 1)
self.logMsg("ADD episode itemid: %s - Title: %s" % (itemid, title), 1)
# Add path
pathid = self.kodi_db.addPath(path)
# Add the file
fileid = self.kodi_db.addFile(filename, pathid)
# Create the episode entry
if kodiversion in (16, 17):
if self.kodiversion in (16, 17):
# Kodi Jarvis, Krypton
query = (
'''
@ -1454,7 +1393,7 @@ class TVShows(Items):
kodicursor.execute(query, (pathid, filename, dateadded, fileid))
# Process cast
people = API.getPeopleList()
kodi_db.addPeople(episodeid, people, "episode")
self.kodi_db.addPeople(episodeid, people, "episode")
# Process artwork
# Wide "screenshot" of particular episode
poster = item.attrib.get('thumb')
@ -1473,13 +1412,13 @@ class TVShows(Items):
# Process stream details
streams = API.getMediaStreams()
kodi_db.addStreams(fileid, streams, runtime)
self.kodi_db.addStreams(fileid, streams, runtime)
# Process playstates
kodi_db.addPlaystate(fileid, resume, runtime, playcount, dateplayed)
self.kodi_db.addPlaystate(fileid, resume, runtime, playcount, dateplayed)
if not self.directpath and resume:
# Create additional entry for widgets. This is only required for plugin/episode.
temppathid = kodi_db.getPath("plugin://plugin.video.plexkodiconnect.tvshows/")
tempfileid = kodi_db.addFile(filename, temppathid)
temppathid = self.kodi_db.getPath("plugin://plugin.video.plexkodiconnect.tvshows/")
tempfileid = self.kodi_db.addFile(filename, temppathid)
query = ' '.join((
"UPDATE files",
@ -1487,7 +1426,7 @@ class TVShows(Items):
"WHERE idFile = ?"
))
kodicursor.execute(query, (temppathid, filename, dateadded, tempfileid))
kodi_db.addPlaystate(tempfileid, resume, runtime, playcount, dateplayed)
self.kodi_db.addPlaystate(tempfileid, resume, runtime, playcount, dateplayed)
self.kodiconn.commit()
self.embyconn.commit()
@ -1600,27 +1539,23 @@ class TVShows(Items):
def removeShow(self, kodiid):
kodicursor = self.kodicursor
artwork = self.artwork
artwork.deleteArtwork(kodiid, "tvshow", kodicursor)
self.artwork.deleteArtwork(kodiid, "tvshow", kodicursor)
kodicursor.execute("DELETE FROM tvshow WHERE idShow = ?", (kodiid,))
self.logMsg("Removed tvshow: %s." % kodiid, 2)
def removeSeason(self, kodiid):
kodicursor = self.kodicursor
artwork = self.artwork
artwork.deleteArtwork(kodiid, "season", kodicursor)
self.artwork.deleteArtwork(kodiid, "season", kodicursor)
kodicursor.execute("DELETE FROM seasons WHERE idSeason = ?", (kodiid,))
self.logMsg("Removed season: %s." % kodiid, 2)
def removeEpisode(self, kodiid, fileid):
kodicursor = self.kodicursor
artwork = self.artwork
artwork.deleteArtwork(kodiid, "episode", kodicursor)
self.artwork.deleteArtwork(kodiid, "episode", kodicursor)
kodicursor.execute("DELETE FROM episode WHERE idEpisode = ?", (kodiid,))
kodicursor.execute("DELETE FROM files WHERE idFile = ?", (fileid,))
self.logMsg("Removed episode: %s." % kodiid, 2)
@ -1656,10 +1591,9 @@ class Music(Items):
count = 0
for artist in items:
title = artist['Name']
if pdialog:
percentage = int((float(count) / float(total))*100)
pdialog.update(percentage, message=title)
pdialog.update(percentage, message=artist['Name'])
count += 1
self.add_updateArtist(artist)
# Add albums
@ -1672,10 +1606,9 @@ class Music(Items):
count = 0
for album in items:
title = album['Name']
if pdialog:
percentage = int((float(count) / float(total))*100)
pdialog.update(percentage, message=title)
pdialog.update(percentage, message=album['Name'])
count += 1
self.add_updateAlbum(album)
# Add songs
@ -1688,34 +1621,19 @@ class Music(Items):
count = 0
for song in items:
title = song['Name']
if pdialog:
percentage = int((float(count) / float(total))*100)
pdialog.update(percentage, message=title)
pdialog.update(percentage, message=song['Name'])
count += 1
self.add_updateSong(song)
if not pdialog and self.contentmsg:
self.contentPop(title, self.newmusic_time)
self.contentPop(song['Name'], self.newmusic_time)
def add_updateArtist(self, item, viewtag=None, viewid=None, artisttype="MusicArtist"):
try:
self.run_add_updateArtist(item, viewtag, viewid, artisttype)
except Exception as e:
self.logMsg('itemtypes.py for music artist has crashed for '
'item %s. Error:'
% item.attrib.get('ratingKey', None), -1)
self.logMsg(e, -1)
import traceback
self.logMsg("Traceback:\n%s" % traceback.format_exc(), 0)
utils.window('plex_scancrashed', value='true')
# skip this item for now
return
def run_add_updateArtist(self, item, viewtag=None, viewid=None,
@utils.CatchExceptions(warnuser=True)
def add_updateArtist(self, item, viewtag=None, viewid=None,
artisttype="MusicArtist"):
kodicursor = self.kodicursor
emby_db = self.emby_db
kodi_db = self.kodi_db
artwork = self.artwork
API = PlexAPI.API(item)
@ -1764,7 +1682,7 @@ class Music(Items):
# multiple times.
# Kodi doesn't allow that. In case that happens we just merge the
# artist entries.
artistid = kodi_db.addArtist(name, musicBrainzId)
artistid = self.kodi_db.addArtist(name, musicBrainzId)
# Create the reference in emby table
emby_db.addReference(
itemid, artistid, artisttype, "artist", checksum=checksum)
@ -1796,25 +1714,10 @@ class Music(Items):
self.embyconn.commit()
self.kodiconn.commit()
@utils.CatchExceptions(warnuser=True)
def add_updateAlbum(self, item, viewtag=None, viewid=None):
try:
self.run_add_updateAlbum(item, viewtag, viewid)
except Exception as e:
self.logMsg('itemtypes.py for music album has crashed for '
'item %s. Error:'
% item.attrib.get('ratingKey', None), -1)
self.logMsg(e, -1)
import traceback
self.logMsg("Traceback:\n%s" % traceback.format_exc(), 0)
utils.window('plex_scancrashed', value='true')
# skip this item for now
return
def run_add_updateAlbum(self, item, viewtag=None, viewid=None):
kodiversion = self.kodiversion
kodicursor = self.kodicursor
emby_db = self.emby_db
kodi_db = self.kodi_db
artwork = self.artwork
API = PlexAPI.API(item)
@ -1875,13 +1778,13 @@ class Music(Items):
# multiple times.
# Kodi doesn't allow that. In case that happens we just merge the
# artist entries.
albumid = kodi_db.addAlbum(name, musicBrainzId)
albumid = self.kodi_db.addAlbum(name, musicBrainzId)
# Create the reference in emby table
emby_db.addReference(
itemid, albumid, "MusicAlbum", "album", checksum=checksum)
# Process the album info
if kodiversion == 17:
if self.kodiversion == 17:
# Kodi Krypton
query = ' '.join((
@ -1894,7 +1797,7 @@ class Music(Items):
kodicursor.execute(query, (artistname, year, genre, bio, thumb,
rating, lastScraped, "album", studio,
albumid))
elif kodiversion == 16:
elif self.kodiversion == 16:
# Kodi Jarvis
query = ' '.join((
@ -1907,7 +1810,7 @@ class Music(Items):
kodicursor.execute(query, (artistname, year, genre, bio, thumb,
rating, lastScraped, "album", studio,
albumid))
elif kodiversion == 15:
elif self.kodiversion == 15:
# Kodi Isengard
query = ' '.join((
@ -1998,33 +1901,18 @@ class Music(Items):
# Update emby reference with parentid
emby_db.updateParentId(artistId, albumid)
# Add genres
kodi_db.addMusicGenres(albumid, genres, "album")
self.kodi_db.addMusicGenres(albumid, genres, "album")
# Update artwork
artwork.addArtwork(artworks, albumid, "album", kodicursor)
self.embyconn.commit()
self.kodiconn.commit()
@utils.CatchExceptions(warnuser=True)
def add_updateSong(self, item, viewtag=None, viewid=None):
try:
self.run_add_updateSong(item, viewtag, viewid)
except Exception as e:
self.logMsg('itemtypes.py for music song has crashed for '
'item %s. Error:'
% item.attrib.get('ratingKey', None), -1)
self.logMsg(e, -1)
import traceback
self.logMsg("Traceback:\n%s" % traceback.format_exc(), 0)
utils.window('plex_scancrashed', value='true')
# skip this item for now
return
def run_add_updateSong(self, item, viewtag=None, viewid=None):
# Process single song
kodiversion = self.kodiversion
kodicursor = self.kodicursor
emby = self.emby
emby_db = self.emby_db
kodi_db = self.kodi_db
artwork = self.artwork
API = PlexAPI.API(item)
@ -2085,7 +1973,7 @@ class Music(Items):
doIndirect = not self.directpath
if self.directpath:
# Direct paths is set the Kodi way
playurl = API.getFilePath()
playurl = API.getFilePath(forceFirstMediaStream=True)
if playurl is None:
# Something went wrong, trying to use non-direct paths
doIndirect = True
@ -2136,7 +2024,7 @@ class Music(Items):
self.logMsg("ADD song itemid: %s - Title: %s" % (itemid, title), 1)
# Add path
pathid = kodi_db.addPath(path, strHash="123")
pathid = self.kodi_db.addPath(path, strHash="123")
try:
# Get the album
@ -2148,7 +2036,7 @@ class Music(Items):
album_name = item.get('parentTitle')
if album_name:
self.logMsg("Creating virtual music album for song: %s." % itemid, 1)
albumid = kodi_db.addAlbum(album_name, API.getProvider('MusicBrainzAlbum'))
albumid = self.kodi_db.addAlbum(album_name, API.getProvider('MusicBrainzAlbum'))
emby_db.addReference("%salbum%s" % (itemid, albumid), albumid, "MusicAlbum_", "album")
else:
# No album Id associated to the song.
@ -2173,7 +2061,7 @@ class Music(Items):
self.logMsg("Failed to add album. Creating singles.", 1)
kodicursor.execute("select coalesce(max(idAlbum),0) from album")
albumid = kodicursor.fetchone()[0] + 1
if kodiversion == 16:
if self.kodiversion == 16:
# Kodi Jarvis
query = (
'''
@ -2183,7 +2071,7 @@ class Music(Items):
'''
)
kodicursor.execute(query, (albumid, genre, year, "single"))
elif kodiversion == 15:
elif self.kodiversion == 15:
# Kodi Isengard
query = (
'''
@ -2262,13 +2150,31 @@ class Music(Items):
artist_edb = emby_db.getItem_byId(artist_eid)
artistid = artist_edb[0]
finally:
query = (
'''
INSERT OR REPLACE INTO song_artist(idArtist, idSong, iOrder, strArtist)
VALUES (?, ?, ?, ?)
'''
)
kodicursor.execute(query, (artistid, songid, index, artist_name))
if self.kodiversion >= 17:
# Kodi Krypton
query = (
'''
INSERT OR REPLACE INTO song_artist(idArtist, idSong, idRole, iOrder, strArtist)
VALUES (?, ?, ?, ?, ?)
'''
)
kodicursor.execute(query,(artistid, songid, 1, index, artist_name))
# May want to look into only doing this once?
query = (
'''
INSERT OR REPLACE INTO role(idRole, strRole)
VALUES (?, ?)
'''
)
kodicursor.execute(query, (1, 'Composer'))
else:
query = (
'''
INSERT OR REPLACE INTO song_artist(idArtist, idSong, iOrder, strArtist)
VALUES (?, ?, ?, ?)
'''
)
kodicursor.execute(query, (artistid, songid, index, artist_name))
# Verify if album artist exists
album_artists = []
@ -2316,11 +2222,11 @@ class Music(Items):
result = kodicursor.fetchone()
if result and result[0] != album_artists:
# Field is empty
if kodiversion in (16, 17):
if self.kodiversion in (16, 17):
# Kodi Jarvis, Krypton
query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?"
kodicursor.execute(query, (album_artists, albumid))
elif kodiversion == 15:
elif self.kodiversion == 15:
# Kodi Isengard
query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?"
kodicursor.execute(query, (album_artists, albumid))
@ -2330,7 +2236,7 @@ class Music(Items):
kodicursor.execute(query, (album_artists, albumid))
# Add genres
kodi_db.addMusicGenres(songid, genres, "song")
self.kodi_db.addMusicGenres(songid, genres, "song")
# Update artwork
allart = API.getAllArtwork(parentInfo=True)
@ -2372,10 +2278,9 @@ class Music(Items):
self.removeSong(kodiid)
# This should only address single song scenario, where server doesn't actually
# create an album for the song.
customitems = emby_db.getItem_byWildId(itemid)
emby_db.removeWildItem(itemid)
for item in customitems:
for item in emby_db.getItem_byWildId(itemid):
item_kid = item[0]
item_mediatype = item[1]
@ -2431,23 +2336,16 @@ class Music(Items):
def removeSong(self, kodiid):
kodicursor = self.kodicursor
artwork = self.artwork
artwork.deleteArtwork(kodiid, "song", kodicursor)
kodicursor.execute("DELETE FROM song WHERE idSong = ?", (kodiid,))
self.artwork.deleteArtwork(kodiid, "song", self.kodicursor)
self.kodicursor.execute("DELETE FROM song WHERE idSong = ?", (kodiid,))
def removeAlbum(self, kodiid):
kodicursor = self.kodicursor
artwork = self.artwork
artwork.deleteArtwork(kodiid, "album", kodicursor)
kodicursor.execute("DELETE FROM album WHERE idAlbum = ?", (kodiid,))
self.artwork.deleteArtwork(kodiid, "album", self.kodicursor)
self.kodicursor.execute("DELETE FROM album WHERE idAlbum = ?", (kodiid,))
def removeArtist(self, kodiid):
kodicursor = self.kodicursor
artwork = self.artwork
artwork.deleteArtwork(kodiid, "artist", kodicursor)
kodicursor.execute("DELETE FROM artist WHERE idArtist = ?", (kodiid,))
self.artwork.deleteArtwork(kodiid, "artist", self.kodicursor)
self.kodicursor.execute("DELETE FROM artist WHERE idArtist = ?", (kodiid,))

File diff suppressed because it is too large Load diff

View file

@ -24,28 +24,28 @@ class KodiMonitor(xbmc.Monitor):
self.doUtils = downloadutils.DownloadUtils().downloadUrl
self.xbmcplayer = xbmc.Player()
xbmc.Monitor.__init__(self)
self.logMsg("Kodi monitor started.", 1)
def onScanStarted(self, library):
self.logMsg("Kodi library scan %s running." % library, 2)
if library == "video":
utils.window('emby_kodiScan', value="true")
utils.window('plex_kodiScan', value="true")
def onScanFinished(self, library):
self.logMsg("Kodi library scan %s finished." % library, 2)
if library == "video":
utils.window('emby_kodiScan', clear=True)
utils.window('plex_kodiScan', clear=True)
def onSettingsChanged(self):
# Monitor emby settings
# Review reset setting at a later time, need to be adjusted to account for initial setup
# changes.
'''currentPath = utils.settings('useDirectPaths')
if utils.window('emby_pluginpath') != currentPath:
if utils.window('plex_pluginpath') != currentPath:
# Plugin path value changed. Offer to reset
self.logMsg("Changed to playback mode detected", 1)
utils.window('emby_pluginpath', value=currentPath)
utils.window('plex_pluginpath', value=currentPath)
resp = xbmcgui.Dialog().yesno(
heading="Playback mode change detected",
line1=(
@ -56,17 +56,17 @@ class KodiMonitor(xbmc.Monitor):
utils.reset()'''
currentLog = utils.settings('logLevel')
if utils.window('emby_logLevel') != currentLog:
if utils.window('plex_logLevel') != currentLog:
# The log level changed, set new prop
self.logMsg("New log level: %s" % currentLog, 1)
utils.window('emby_logLevel', value=currentLog)
utils.window('plex_logLevel', value=currentLog)
@utils.CatchExceptions(warnuser=False)
def onNotification(self, sender, method, data):
if method not in ("Playlist.OnAdd"):
self.logMsg("Method: %s Data: %s" % (method, data), 1)
if data:
data = json.loads(data, 'utf-8')
self.logMsg("Method: %s Data: %s" % (method, data), 1)
if method == "Player.OnPlay":
self.PlayBackStart(data)
@ -82,13 +82,13 @@ class KodiMonitor(xbmc.Monitor):
item = data.get('item')
try:
kodiid = item['id']
type = item['type']
item_type = item['type']
except (KeyError, TypeError):
self.logMsg("Item is invalid for playstate update.", 1)
else:
# Send notification to the server.
with embydb.GetEmbyDB() as emby_db:
emby_dbitem = emby_db.getItem_byKodiId(kodiid, type)
emby_dbitem = emby_db.getItem_byKodiId(kodiid, item_type)
try:
itemid = emby_dbitem[0]
except TypeError:
@ -137,14 +137,19 @@ class KodiMonitor(xbmc.Monitor):
url = "{server}/emby/Items/%s?format=json" % itemid
self.logMsg("Deleting request: %s" % itemid)
doUtils.downloadUrl(url, type="DELETE")
doUtils.downloadUrl(url, action_type="DELETE")
finally:
embycursor.close()'''
elif method == "System.OnWake":
# Allow network to wake up
xbmc.sleep(10000)
utils.window('emby_onWake', value="true")
utils.window('plex_onWake', value="true")
elif method == "GUI.OnScreensaverDeactivated":
if utils.settings('dbSyncScreensaver') == "true":
xbmc.sleep(5000)
utils.window('plex_runLibScan', value="full")
elif method == "Playlist.OnClear":
pass
@ -155,7 +160,6 @@ class KodiMonitor(xbmc.Monitor):
"""
log = self.logMsg
window = utils.window
# Get currently playing file - can take a while. Will be utf-8!
try:
currentFile = self.xbmcplayer.getPlayingFile()
@ -173,53 +177,77 @@ class KodiMonitor(xbmc.Monitor):
return
else:
count += 1
log("Currently playing file is: %s" % currentFile.decode('utf-8'), 1)
# Just to be on the safe side
currentFile = utils.tryDecode(currentFile)
log("Currently playing file is: %s" % currentFile, 1)
# Try to get a Kodi ID
item = data.get('item')
# Get the type of media we're playing
try:
type = item['type']
except:
typus = data['item']['type']
except (TypeError, KeyError):
log("Item is invalid for PMS playstate update.", 0)
return
try:
kodiid = item['id']
except (KeyError, TypeError):
itemType = window("emby_%s.type" % currentFile)
log("No kodi id passed. Playing itemtype is: %s" % itemType, 1)
if itemType in ('movie', 'episode'):
# Window was setup by PKC and is NOT a trailer ('clip')
with kodidb.GetKodiDB('video') as kodi_db:
kodiid = kodi_db.getIdFromTitle(data.get('item'))
if kodiid is None:
log("Skip playstate update. No unique Kodi title found"
" for %s" % data.get('item'), 0)
return
else:
log("Item is invalid for PMS playstate update.", 0)
return
log("Playing itemtype is (or appears to be): %s" % typus, 1)
# Get Plex' item id
with embydb.GetEmbyDB() as emby_db:
emby_dbitem = emby_db.getItem_byKodiId(kodiid, type)
try:
plexid = emby_dbitem[0]
except TypeError:
log("No Plex id returned for kodiid %s" % kodiid, 0)
return
log("Found Plex id %s for Kodi id %s" % (plexid, kodiid), 1)
# Try to get a Kodi ID
# If PKC was used - native paths, not direct paths
plexid = utils.window('emby_%s.itemid'
% utils.tryEncode(currentFile))
# Get rid of the '' if the window property was not set
plexid = None if not plexid else plexid
kodiid = None
if plexid is None:
log('Did not get Plex id from window properties', 1)
try:
kodiid = data['item']['id']
except (TypeError, KeyError):
log('Did not get a Kodi id from Kodi, darn', 1)
# For direct paths, if we're not streaming something
# When using Widgets, Kodi doesn't tell us shit so we need this hack
if (kodiid is None and plexid is None and typus != 'song'
and not currentFile.startswith('http')):
try:
filename = currentFile.rsplit('/', 1)[1]
path = currentFile.rsplit('/', 1)[0] + '/'
except IndexError:
filename = currentFile.rsplit('\\', 1)[1]
path = currentFile.rsplit('\\', 1)[0] + '\\'
log('Trying to figure out playing item from filename: %s and '
'path: %s' % (filename, path), 1)
with kodidb.GetKodiDB('video') as kodi_db:
try:
kodiid, typus = kodi_db.getIdFromFilename(filename, path)
except TypeError:
log('Aborting playback report', 1)
return
if plexid is None:
# Get Plex' item id
with embydb.GetEmbyDB() as emby_db:
emby_dbitem = emby_db.getItem_byKodiId(kodiid, typus)
try:
plexid = emby_dbitem[0]
except TypeError:
log("No Plex id returned for kodiid %s" % kodiid, 1)
log('Aborting playback report', 1)
return
log("Found Plex id %s for Kodi id %s for type %s"
% (plexid, kodiid, typus), 1)
# Set some stuff if Kodi initiated playback
if ((utils.settings('useDirectPaths') == "1" and not type == "song") or
(type == "song" and utils.settings('enableMusic') == "true")):
if self.StartDirectPath(plexid, type, currentFile) is False:
if ((utils.settings('useDirectPaths') == "1" and not typus == "song")
or
(typus == "song" and utils.settings('enableMusic') == "true")):
if self.StartDirectPath(plexid,
typus,
utils.tryEncode(currentFile)) is False:
log('Could not initiate monitoring; aborting', -1)
return
# Save currentFile for cleanup later and to be able to access refs
window('plex_lastPlayedFiled', value=currentFile.decode('utf-8'))
window('plex_lastPlayedFiled', value=currentFile)
window('Plex_currently_playing_itemid', value=plexid)
window("emby_%s.itemid" % currentFile, value=plexid)
window("emby_%s.itemid" % utils.tryEncode(currentFile), value=plexid)
log('Finish playback startup', 1)
def StartDirectPath(self, plexid, type, currentFile):

View file

@ -255,7 +255,7 @@ class ThreadedShowSyncInfo(Thread):
@utils.logging
@utils.ThreadMethodsAdditionalSuspend('suspend_LibraryThread')
@utils.ThreadMethodsAdditionalStop('emby_shouldStop')
@utils.ThreadMethodsAdditionalStop('plex_shouldStop')
@utils.ThreadMethods
class LibrarySync(Thread):
"""
@ -287,7 +287,6 @@ class LibrarySync(Thread):
self.vnodes = videonodes.VideoNodes()
self.dialog = xbmcgui.Dialog()
self.syncThreadNumber = int(utils.settings('syncThreadNumber'))
self.installSyncDone = True if \
utils.settings('SyncInstallRunDone') == 'true' else False
self.showDbSync = True if \
@ -298,8 +297,8 @@ class LibrarySync(Thread):
'enableBackgroundSync') == "true" else False
self.limitindex = int(utils.settings('limitindex'))
if utils.settings('emby_pathverified') == 'true':
utils.window('emby_pathverified', value='true')
if utils.settings('plex_pathverified') == 'true':
utils.window('plex_pathverified', value='true')
# Just in case a time sync goes wrong
self.timeoffset = int(utils.settings('kodiplextimeoffset'))
@ -477,9 +476,6 @@ class LibrarySync(Thread):
# Add sources
utils.sourcesXML()
# Ensure that DBs exist if called for very first time
self.initializeDBs()
# Set views. Abort if unsuccessful
if not self.maintainViews():
xbmc.executebuiltin('InhibitIdleShutdown(false)')
@ -507,7 +503,7 @@ class LibrarySync(Thread):
if self.enableMusic:
xbmc.executebuiltin('UpdateLibrary(music)')
utils.window('emby_initialScan', clear=True)
utils.window('plex_initialScan', clear=True)
xbmc.executebuiltin('InhibitIdleShutdown(false)')
utils.setScreensaver(value=screensaver)
if utils.window('plex_scancrashed') == 'true':
@ -516,7 +512,7 @@ class LibrarySync(Thread):
utils.window('plex_scancrashed', clear=True)
elif utils.window('plex_scancrashed') == '401':
utils.window('plex_scancrashed', clear=True)
if utils.window('emby_serverStatus') not in ('401', 'Auth'):
if utils.window('plex_serverStatus') not in ('401', 'Auth'):
# Plex server had too much and returned ERROR
self.dialog.ok(self.addonName, self.__language__(39409))
@ -536,7 +532,7 @@ class LibrarySync(Thread):
folder = folderItem.attrib
mediatype = folder['type']
# Only process supported formats
if mediatype not in ('movie', 'show', 'artist'):
if mediatype not in ('movie', 'show', 'artist', 'photo'):
return totalnodes
# Prevent duplicate for nodes of the same type
@ -683,18 +679,20 @@ class LibrarySync(Thread):
self.nodes = {
'movie': [],
'show': [],
'artist': []
'artist': [],
'photo': []
}
self.playlists = {
'movie': [],
'show': [],
'artist': []
'artist': [],
'photo': []
}
self.sorted_views = []
for view in sections:
itemType = view.attrib['type']
if itemType in ('movie', 'show'): # and NOT artist for now
if itemType in ('movie', 'show', 'photo'): # NOT artist for now
self.sorted_views.append(view.attrib['title'])
self.logMsg('Sorted views: %s' % self.sorted_views, 1)
@ -733,15 +731,15 @@ class LibrarySync(Thread):
pass
# Save total
utils.window('Emby.nodes.total', str(totalnodes))
utils.window('Plex.nodes.total', str(totalnodes))
# Reopen DB connection to ensure that changes were commited before
with embydb.GetEmbyDB() as emby_db:
# update views for all:
self.views = emby_db.getAllViewInfo()
self.logMsg("Removing views: %s" % self.old_views, 1)
for view in self.old_views:
emby_db.removeView(view)
# update views for all:
self.views = emby_db.getAllViewInfo()
self.logMsg("Finished processing views. Views saved: %s"
% self.views, 1)
@ -847,16 +845,15 @@ class LibrarySync(Thread):
# Populate queue: GetMetadata
for updateItem in self.updatelist:
getMetadataQueue.put(updateItem)
# Spawn GetMetadata threads for downloading
# Spawn GetMetadata thread for downloading
threads = []
for i in range(min(self.syncThreadNumber, itemNumber)):
thread = ThreadedGetMetadata(getMetadataQueue,
processMetadataQueue,
getMetadataLock,
processMetadataLock)
thread.setDaemon(True)
thread.start()
threads.append(thread)
thread = ThreadedGetMetadata(getMetadataQueue,
processMetadataQueue,
getMetadataLock,
processMetadataLock)
thread.setDaemon(True)
thread.start()
threads.append(thread)
self.logMsg("%s download threads spawned" % len(threads), 1)
# Spawn one more thread to process Metadata, once downloaded
thread = ThreadedProcessMetadata(processMetadataQueue,
@ -1347,11 +1344,18 @@ class LibrarySync(Thread):
# We haven't waited long enough for the PMS to finish
# processing the item. Do it later
continue
if item['state'] == 5:
if self.process_newitems(item) is True:
deleteListe.append(i)
elif item['state'] == 9:
if self.process_deleteditems(item) is True:
if item['state'] == 9:
successful = self.process_deleteditems(item)
else:
successful = self.process_newitems(item)
if successful is True:
deleteListe.append(i)
else:
# Safety net if we can't process an item
item['attempt'] += 1
if item['attempt'] > 3:
self.logMsg('Repeatetly could not process item %s, abort'
% item, -1)
deleteListe.append(i)
# Get rid of the items we just processed
@ -1423,15 +1427,23 @@ class LibrarySync(Thread):
"processing queue" for later
"""
for item in data:
state = item.get('state')
typus = item.get('type')
if state == 9 or (state == 5 and typus in (1, 4, 10)):
self.itemsToProcess.append({
'state': state,
'type': typus,
'ratingKey': item.get('itemID'),
'timestamp': utils.getUnixTimestamp()
})
state = item.get('state')
if state == 9 or typus in (1, 4, 10):
itemId = item.get('itemID')
# Have we already added this element?
for existingItem in self.itemsToProcess:
if existingItem['ratingKey'] == itemId:
break
else:
# Haven't added this element to the queue yet
self.itemsToProcess.append({
'state': state,
'type': typus,
'ratingKey': itemId,
'timestamp': utils.getUnixTimestamp(),
'attempt': 0
})
def process_playing(self, data):
"""
@ -1508,14 +1520,19 @@ class LibrarySync(Thread):
userdata = API.getUserData()
currSess['duration'] = userdata['Runtime']
currSess['viewCount'] = userdata['PlayCount']
# Sometimes, Plex tells us resume points in milliseconds and
# not in seconds - thank you very much!
if item.get('viewOffset') > currSess['duration']:
resume = item.get('viewOffset') / 1000
else:
resume = item.get('viewOffset')
# Append to list that we need to process
items.append({
'ratingKey': ratingKey,
'kodi_id': kodiInfo[0],
'file_id': kodiInfo[1],
'kodi_type': kodiInfo[4],
'viewOffset': PF.ConvertPlexToKodiTime(
item.get('viewOffset')),
'viewOffset': resume,
'state': state,
'duration': currSess['duration'],
'viewCount': currSess['viewCount'],
@ -1536,7 +1553,7 @@ class LibrarySync(Thread):
try:
self.run_internal()
except Exception as e:
utils.window('emby_dbScan', clear=True)
utils.window('plex_dbScan', clear=True)
self.logMsg('LibrarySync thread crashed', -1)
self.logMsg('Error message: %s' % e, -1)
import traceback
@ -1575,6 +1592,9 @@ class LibrarySync(Thread):
log("---===### Starting LibrarySync ###===---", 0)
# Ensure that DBs exist if called for very first time
self.initializeDBs()
if self.enableMusic:
utils.advancedSettingsXML()
@ -1589,10 +1609,10 @@ class LibrarySync(Thread):
return
xbmc.sleep(1000)
if (window('emby_dbCheck') != "true" and installSyncDone):
if (window('plex_dbCheck') != "true" and installSyncDone):
# Verify the validity of the database
currentVersion = settings('dbCreatedWithVersion')
minVersion = window('emby_minDBVersion')
minVersion = window('plex_minDBVersion')
if not self.compareDBVersion(currentVersion, minVersion):
log("Db version out of date: %s minimum version required: "
@ -1609,7 +1629,7 @@ class LibrarySync(Thread):
utils.reset()
break
window('emby_dbCheck', value="true")
window('plex_dbCheck', value="true")
if not startupComplete:
# Also runs when first installed
@ -1619,15 +1639,15 @@ class LibrarySync(Thread):
# Database does not exists
log("The current Kodi version is incompatible "
"to know which Kodi versions are supported.", -1)
log('Current Kodi version: %s' % xbmc.getInfoLabel(
'System.BuildVersion').decode('utf-8'))
log('Current Kodi version: %s' % utils.tryDecode(
xbmc.getInfoLabel('System.BuildVersion')))
# "Current Kodi version is unsupported, cancel lib sync"
self.dialog.ok(heading=self.addonName,
line1=string(39403))
break
# Run start up sync
window('emby_dbScan', value="true")
window('plex_dbScan', value="true")
log("Db version: %s" % settings('dbCreatedWithVersion'), 0)
lastTimeSync = utils.getUnixTimestamp()
self.syncPMStime()
@ -1635,7 +1655,7 @@ class LibrarySync(Thread):
lastSync = utils.getUnixTimestamp()
librarySync = fullSync()
# Initialize time offset Kodi - PMS
window('emby_dbScan', clear=True)
window('plex_dbScan', clear=True)
if librarySync:
log("Initial start-up full sync successful", 0)
startupComplete = True
@ -1655,23 +1675,23 @@ class LibrarySync(Thread):
break
# Currently no db scan, so we can start a new scan
elif window('emby_dbScan') != "true":
elif window('plex_dbScan') != "true":
# Full scan was requested from somewhere else, e.g. userclient
if window('plex_runLibScan') in ("full", "repair"):
log('Full library scan requested, starting', 0)
window('emby_dbScan', value="true")
window('plex_dbScan', value="true")
if window('plex_runLibScan') == "full":
fullSync()
elif window('plex_runLibScan') == "repair":
fullSync(repair=True)
window('plex_runLibScan', clear=True)
window('emby_dbScan', clear=True)
window('plex_dbScan', clear=True)
# Full library sync finished
self.showKodiNote(string(39407), forced=True)
self.showKodiNote(string(39407), forced=False)
# Reset views was requested from somewhere else
elif window('plex_runLibScan') == "views":
log('Refresh playlist and nodes requested, starting', 0)
window('emby_dbScan', value="true")
window('plex_dbScan', value="true")
window('plex_runLibScan', clear=True)
# First remove playlists
@ -1691,36 +1711,36 @@ class LibrarySync(Thread):
self.showKodiNote(string(39406),
forced=True,
icon="error")
window('emby_dbScan', clear=True)
window('plex_dbScan', clear=True)
else:
now = utils.getUnixTimestamp()
if (now - lastSync > fullSyncInterval and
not xbmcplayer.isPlaying()):
lastSync = now
log('Doing scheduled full library scan', 1)
window('emby_dbScan', value="true")
window('plex_dbScan', value="true")
if fullSync() is False and not threadStopped():
log('Could not finish scheduled full sync', -1)
self.showKodiNote(string(39410),
forced=True,
icon='error')
window('emby_dbScan', clear=True)
window('plex_dbScan', clear=True)
# Full library sync finished
self.showKodiNote(string(39407), forced=False)
elif now - lastTimeSync > oneDay:
lastTimeSync = now
log('Starting daily time sync', 0)
window('emby_dbScan', value="true")
window('plex_dbScan', value="true")
self.syncPMStime()
window('emby_dbScan', clear=True)
window('plex_dbScan', clear=True)
elif enableBackgroundSync:
# Check back whether we should process something
# Only do this once every 10 seconds
if now - lastProcessing > 10:
lastProcessing = now
window('emby_dbScan', value="true")
window('plex_dbScan', value="true")
processItems()
window('emby_dbScan', clear=True)
window('plex_dbScan', clear=True)
# See if there is a PMS message we need to handle
try:
message = queue.get(block=False)
@ -1729,10 +1749,10 @@ class LibrarySync(Thread):
continue
# Got a message from PMS; process it
else:
window('emby_dbScan', value="true")
window('plex_dbScan', value="true")
processMessage(message)
queue.task_done()
window('emby_dbScan', clear=True)
window('plex_dbScan', clear=True)
# NO sleep!
continue
else:

View file

@ -32,7 +32,7 @@ def getRealFileName(filename, isTemp=False):
if os.path.supports_unicode_filenames:
checkfile = filename
else:
checkfile = filename.encode("utf-8")
checkfile = utils.tryEncode(filename)
# determine if our python module is able to access the file directly...
if os.path.exists(checkfile):
@ -46,7 +46,7 @@ def getRealFileName(filename, isTemp=False):
else: filepart = filename.split("\\")[-1]
tempfile = "special://temp/"+filepart
xbmcvfs.copy(filename, tempfile)
filename = xbmc.translatePath(tempfile).decode("utf-8")
filename = utils.tryDecode(xbmc.translatePath(tempfile))
return (isTemp,filename)
@ -65,14 +65,14 @@ def getEmbyRatingFromKodiRating(rating):
if (rating >= 5): favourite = True
return(like, favourite, deletelike)
def getAdditionalSongTags(embyid, emby_rating, API, kodicursor, emby_db, enableimportsongrating, enableexportsongrating, enableupdatesongrating):
def getAdditionalSongTags(plexid, plex_rating, API, kodicursor, emby_db, enableimportsongrating, enableexportsongrating, enableupdatesongrating):
emby = embyserver.Read_EmbyServer()
previous_values = None
filename = API.getFilePath()
rating = 0
emby_rating = int(round(emby_rating, 0))
plex_rating = int(round(plex_rating, 0))
#get file rating and comment tag from file itself.
if enableimportsongrating:
@ -83,7 +83,7 @@ def getAdditionalSongTags(embyid, emby_rating, API, kodicursor, emby_db, enablei
hasEmbeddedCover = False
emby_dbitem = emby_db.getItem_byId(embyid)
emby_dbitem = emby_db.getItem_byId(plexid)
try:
kodiid = emby_dbitem[0]
except TypeError:
@ -100,44 +100,44 @@ def getAdditionalSongTags(embyid, emby_rating, API, kodicursor, emby_db, enablei
if file_rating is None and currentvalue:
return (currentvalue, comment, False)
elif file_rating is None and not currentvalue:
return (emby_rating, comment, False)
return (plex_rating, comment, False)
logMsg("getAdditionalSongTags --> embyid: %s - emby_rating: %s - file_rating: %s - current rating in kodidb: %s" %(embyid, emby_rating, file_rating, currentvalue))
logMsg("getAdditionalSongTags --> plexid: %s - plex_rating: %s - file_rating: %s - current rating in kodidb: %s" %(plexid, plex_rating, file_rating, currentvalue))
updateFileRating = False
updateEmbyRating = False
if currentvalue != None:
# we need to translate the emby values...
if emby_rating == 1 and currentvalue == 2:
emby_rating = 2
if emby_rating == 3 and currentvalue == 4:
emby_rating = 4
if plex_rating == 1 and currentvalue == 2:
plex_rating = 2
if plex_rating == 3 and currentvalue == 4:
plex_rating = 4
#if updating rating into file is disabled, we ignore the rating in the file...
if not enableupdatesongrating:
file_rating = currentvalue
#if convert emby likes/favourites convert to song rating is disabled, we ignore the emby rating...
if not enableexportsongrating:
emby_rating = currentvalue
plex_rating = currentvalue
if (emby_rating == file_rating) and (file_rating != currentvalue):
if (plex_rating == file_rating) and (file_rating != currentvalue):
#the rating has been updated from kodi itself, update change to both emby ands file
rating = currentvalue
updateFileRating = True
updateEmbyRating = True
elif (emby_rating != currentvalue) and (file_rating == currentvalue):
elif (plex_rating != currentvalue) and (file_rating == currentvalue):
#emby rating changed - update the file
rating = emby_rating
rating = plex_rating
updateFileRating = True
elif (file_rating != currentvalue) and (emby_rating == currentvalue):
elif (file_rating != currentvalue) and (plex_rating == currentvalue):
#file rating was updated, sync change to emby
rating = file_rating
updateEmbyRating = True
elif (emby_rating != currentvalue) and (file_rating != currentvalue):
elif (plex_rating != currentvalue) and (file_rating != currentvalue):
#both ratings have changed (corner case) - the highest rating wins...
if emby_rating > file_rating:
rating = emby_rating
if plex_rating > file_rating:
rating = plex_rating
updateFileRating = True
else:
rating = file_rating
@ -152,16 +152,16 @@ def getAdditionalSongTags(embyid, emby_rating, API, kodicursor, emby_db, enablei
rating = file_rating
#determine if we should also send the rating to emby server
if enableexportsongrating:
if emby_rating == 1 and file_rating == 2:
emby_rating = 2
if emby_rating == 3 and file_rating == 4:
emby_rating = 4
if emby_rating != file_rating:
if plex_rating == 1 and file_rating == 2:
plex_rating = 2
if plex_rating == 3 and file_rating == 4:
plex_rating = 4
if plex_rating != file_rating:
updateEmbyRating = True
elif enableexportsongrating:
#set the initial rating to emby value
rating = emby_rating
rating = plex_rating
if updateFileRating and enableupdatesongrating:
updateRatingToFile(rating, filename)
@ -169,8 +169,8 @@ def getAdditionalSongTags(embyid, emby_rating, API, kodicursor, emby_db, enablei
if updateEmbyRating and enableexportsongrating:
# sync details to emby server. Translation needed between ID3 rating and emby likes/favourites:
like, favourite, deletelike = getEmbyRatingFromKodiRating(rating)
utils.window("ignore-update-%s" %embyid, "true") #set temp windows prop to ignore the update from webclient update
emby.updateUserRating(embyid, like, favourite, deletelike)
utils.window("ignore-update-%s" %plexid, "true") #set temp windows prop to ignore the update from webclient update
emby.updateUserRating(plexid, like, favourite, deletelike)
return (rating, comment, hasEmbeddedCover)
@ -193,6 +193,7 @@ def getSongTags(file):
if pic.type == 3 and pic.data:
#the file has an embedded cover
hasEmbeddedCover = True
break
if audio.get("rating"):
rating = float(audio.get("rating")[0])
#flac rating is 0-100 and needs to be converted to 0-5 range
@ -241,7 +242,7 @@ def updateRatingToFile(rating, file):
else: filepart = file.split("\\")[-1]
tempfile = "special://temp/"+filepart
xbmcvfs.copy(file, tempfile)
tempfile = xbmc.translatePath(tempfile).decode("utf-8")
tempfile = utils.tryDecode(xbmc.translatePath(tempfile))
logMsg( "setting song rating: %s for filename: %s - using tempfile: %s" %(rating,file,tempfile))

View file

@ -40,11 +40,13 @@ class PlaybackUtils():
self.artwork = artwork.Artwork()
self.emby = embyserver.Read_EmbyServer()
self.pl = playlist.Playlist()
if self.API.getType() == 'track':
self.pl = playlist.Playlist(typus='music')
else:
self.pl = playlist.Playlist(typus='video')
def play(self, itemid, dbid=None):
log = self.logMsg
window = utils.window
settings = utils.settings
@ -56,7 +58,7 @@ class PlaybackUtils():
listitem = xbmcgui.ListItem()
playutils = putils.PlayUtils(item[0])
log("Play called.", 1)
self.logMsg("Play called.", 1)
playurl = playutils.getPlayUrl()
if not playurl:
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem)
@ -72,18 +74,18 @@ class PlaybackUtils():
xml = downloadutils.DownloadUtils().downloadUrl(
'{server}%s' % item[0][0][0].attrib.get('key'))
if xml in (None, 401):
log('Could not download %s'
self.logMsg('Could not download %s'
% item[0][0][0].attrib.get('key'), -1)
return xbmcplugin.setResolvedUrl(
int(sys.argv[1]), False, listitem)
playurl = xml[0].attrib.get('key').encode('utf-8')
playurl = utils.tryEncode(xml[0].attrib.get('key'))
window('emby_%s.playmethod' % playurl, value='DirectStream')
playmethod = window('emby_%s.playmethod' % playurl)
if playmethod == "Transcode":
window('emby_%s.playmethod' % playurl, clear=True)
playurl = playutils.audioSubsPref(
listitem, playurl.decode('utf-8')).encode('utf-8')
playurl = utils.tryEncode(playutils.audioSubsPref(
listitem, utils.tryDecode(playurl)))
window('emby_%s.playmethod' % playurl, "Transcode")
listitem.setPath(playurl)
self.setProperties(playurl, listitem)
@ -92,18 +94,18 @@ class PlaybackUtils():
############### ORGANIZE CURRENT PLAYLIST ################
homeScreen = xbmc.getCondVisibility('Window.IsActive(home)')
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
playlist = self.pl.playlist
startPos = max(playlist.getposition(), 0) # Can return -1
sizePlaylist = playlist.size()
self.currentPosition = startPos
propertiesPlayback = window('emby_playbackProps') == "true"
propertiesPlayback = window('plex_playbackProps') == "true"
introsPlaylist = False
dummyPlaylist = False
log("Playlist start position: %s" % startPos, 1)
log("Playlist plugin position: %s" % self.currentPosition, 1)
log("Playlist size: %s" % sizePlaylist, 1)
self.logMsg("Playlist start position: %s" % startPos, 2)
self.logMsg("Playlist plugin position: %s" % self.currentPosition, 2)
self.logMsg("Playlist size: %s" % sizePlaylist, 2)
############### RESUME POINT ################
@ -113,12 +115,12 @@ class PlaybackUtils():
# Otherwise we get a loop.
if not propertiesPlayback:
window('emby_playbackProps', value="true")
log("Setting up properties in playlist.", 1)
window('plex_playbackProps', value="true")
self.logMsg("Setting up properties in playlist.", 1)
if (not homeScreen and not seektime and
window('emby_customPlaylist') != "true"):
log("Adding dummy file to playlist.", 2)
window('plex_customplaylist') != "true"):
self.logMsg("Adding dummy file to playlist.", 2)
dummyPlaylist = True
playlist.add(playurl, listitem, index=startPos)
# Remove the original item from playlist
@ -145,7 +147,7 @@ class PlaybackUtils():
if homeScreen and not seektime and not sizePlaylist:
# Extend our current playlist with the actual item to play
# only if there's no playlist first
log("Adding main item to playlist.", 1)
self.logMsg("Adding main item to playlist.", 1)
self.pl.addtoPlaylist(
dbid,
PF.GetKodiTypeFromPlex(API.getType()))
@ -166,7 +168,7 @@ class PlaybackUtils():
additionalListItem = xbmcgui.ListItem()
additionalPlayurl = playutils.getPlayUrl(
partNumber=counter)
log("Adding additional part: %s" % counter, 1)
self.logMsg("Adding additional part: %s" % counter, 1)
self.setProperties(additionalPlayurl, additionalListItem)
self.setArtwork(additionalListItem)
@ -181,14 +183,14 @@ class PlaybackUtils():
if dummyPlaylist:
# Added a dummy file to the playlist,
# because the first item is going to fail automatically.
log("Processed as a playlist. First item is skipped.", 1)
self.logMsg("Processed as a playlist. First item is skipped.", 1)
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem)
# We just skipped adding properties. Reset flag for next time.
elif propertiesPlayback:
log("Resetting properties playback flag.", 2)
window('emby_playbackProps', clear=True)
self.logMsg("Resetting properties playback flag.", 2)
window('plex_playbackProps', clear=True)
#self.pl.verifyPlaylist()
########## SETUP MAIN ITEM ##########
@ -196,8 +198,8 @@ class PlaybackUtils():
# For transcoding only, ask for audio/subs pref
if window('emby_%s.playmethod' % playurl) == "Transcode":
window('emby_%s.playmethod' % playurl, clear=True)
playurl = playutils.audioSubsPref(
listitem, playurl.decode('utf-8')).encode('utf-8')
playurl = utils.tryEncode(playutils.audioSubsPref(
listitem, utils.tryDecode(playurl)))
window('emby_%s.playmethod' % playurl, value="Transcode")
listitem.setPath(playurl)
@ -205,19 +207,19 @@ class PlaybackUtils():
############### PLAYBACK ################
if homeScreen and seektime and window('emby_customPlaylist') != "true":
log("Play as a widget item.", 1)
if homeScreen and seektime and window('plex_customplaylist') != "true":
self.logMsg("Play as a widget item.", 1)
API.CreateListItemFromPlexItem(listitem)
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
elif ((introsPlaylist and window('emby_customPlaylist') == "true") or
elif ((introsPlaylist and window('plex_customplaylist') == "true") or
(homeScreen and not sizePlaylist)):
# Playlist was created just now, play it.
log("Play playlist.", 1)
self.logMsg("Play playlist.", 1)
xbmc.Player().play(playlist, startpos=startPos)
else:
log("Play as a regular item.", 1)
self.logMsg("Play as a regular item.", 1)
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
def AddTrailers(self, xml):
@ -268,17 +270,13 @@ class PlaybackUtils():
# Set all properties necessary for plugin path playback
itemid = self.API.getRatingKey()
itemtype = self.API.getType()
resume, runtime = self.API.getRuntime()
userdata = self.API.getUserData()
embyitem = "emby_%s" % playurl
window('%s.runtime' % embyitem, value=str(runtime))
window('%s.runtime' % embyitem, value=str(userdata['Runtime']))
window('%s.type' % embyitem, value=itemtype)
window('%s.itemid' % embyitem, value=itemid)
# We need to keep track of playQueueItemIDs for Plex Companion
window('plex_%s.playQueueItemID'
% playurl, self.API.GetPlayQueueItemID())
window('plex_%s.guid' % playurl, self.API.getGuid())
window('%s.playcount' % embyitem, value=str(userdata['PlayCount']))
if itemtype == "episode":
window('%s.refreshid' % embyitem,

View file

@ -10,8 +10,8 @@ import xbmcgui
import utils
import clientinfo
import downloadutils
from urllib import urlencode
import embydb_functions as embydb
import kodidb_functions as kodidb
###############################################################################
@ -32,17 +32,10 @@ class Player(xbmc.Player):
self.clientInfo = clientinfo.ClientInfo()
self.doUtils = downloadutils.DownloadUtils().downloadUrl
self.xbmcplayer = xbmc.Player()
self.logMsg("Starting playback monitor.", 2)
xbmc.Player.__init__(self)
# Should we start notification or is this done by Plex Companion?
self.doNotify = False if (utils.settings('plexCompanion') == 'true') \
else True
if self.doNotify:
self.logMsg("PMS notifications not done by Plex Companion", 2)
else:
self.logMsg("PMS notifications done by Plex Companion", 2)
self.logMsg("Started playback monitor.", 2)
def GetPlayStats(self):
return self.playStats
@ -51,15 +44,13 @@ class Player(xbmc.Player):
"""
Window values need to have been set in Kodimonitor.py
"""
log = self.logMsg
window = utils.window
# Will be called when xbmc starts playing a file
xbmcplayer = self.xbmcplayer
self.stopAll()
# Get current file (in utf-8!)
try:
currentFile = xbmcplayer.getPlayingFile()
currentFile = self.getPlayingFile()
xbmc.sleep(300)
except:
currentFile = ""
@ -67,53 +58,65 @@ class Player(xbmc.Player):
while not currentFile:
xbmc.sleep(100)
try:
currentFile = xbmcplayer.getPlayingFile()
currentFile = self.getPlayingFile()
except:
pass
if count == 20:
log("Cancelling playback report...", 1)
self.logMsg("Cancelling playback report...", 1)
break
else:
count += 1
if not currentFile:
log('Error getting a currently playing file; abort reporting', -1)
self.logMsg('Error getting a currently playing file; abort '
'reporting', -1)
return
# Save currentFile for cleanup later and for references
self.currentFile = currentFile
window('plex_lastPlayedFiled', value=currentFile.decode('utf-8'))
window('plex_lastPlayedFiled', value=utils.tryDecode(currentFile))
# We may need to wait for info to be set in kodi monitor
itemId = window("emby_%s.itemid" % currentFile)
count = 0
while not itemId:
xbmc.sleep(200)
itemId = window("emby_%s.itemid" % currentFile)
# try 20 times or about 10 seconds
if count == 20:
log("Could not find itemId, cancelling playback report...", -1)
if count == 5:
self.logMsg("Could not find itemId, cancelling playback "
"report!", -1)
return
count += 1
log("ONPLAYBACK_STARTED: %s itemid: %s"
% (currentFile.decode('utf-8'), itemId), 0)
self.logMsg("ONPLAYBACK_STARTED: %s itemid: %s"
% (utils.tryDecode(currentFile), itemId), 0)
embyitem = "emby_%s" % currentFile
runtime = window("%s.runtime" % embyitem)
refresh_id = window("%s.refreshid" % embyitem)
playMethod = window("%s.playmethod" % embyitem)
itemType = window("%s.type" % embyitem)
try:
playcount = int(window("%s.playcount" % embyitem))
except ValueError:
playcount = 0
window('emby_skipWatched%s' % itemId, value="true")
log("Playing itemtype is: %s" % itemType, 1)
self.logMsg("Playing itemtype is: %s" % itemType, 1)
customseek = window('emby_customPlaylist.seektime')
if (window('emby_customPlaylist') == "true" and customseek):
customseek = window('plex_customplaylist.seektime')
if customseek:
# Start at, when using custom playlist (play to Kodi from webclient)
log("Seeking to: %s" % customseek, 1)
xbmcplayer.seekTime(int(customseek))
window('emby_customPlaylist.seektime', clear=True)
self.logMsg("Seeking to: %s" % customseek, 1)
try:
self.seekTime(int(customseek))
except:
self.logMsg('Could not seek!', -1)
window('plex_customplaylist.seektime', clear=True)
seekTime = xbmcplayer.getTime()
try:
seekTime = self.getTime()
except RuntimeError:
self.logMsg('Could not get current seektime from xbmc player', -1)
seekTime = 0
# Get playback volume
volume_query = {
@ -193,7 +196,8 @@ class Player(xbmc.Player):
if mapping: # Set in playbackutils.py
log("Mapping for external subtitles index: %s" % mapping, 2)
self.logMsg("Mapping for external subtitles index: %s"
% mapping, 2)
externalIndex = json.loads(mapping)
if externalIndex.get(str(indexSubs)):
@ -218,18 +222,24 @@ class Player(xbmc.Player):
try:
runtime = int(runtime)
except ValueError:
runtime = xbmcplayer.getTotalTime()
log("Runtime is missing, Kodi runtime: %s" % runtime, 1)
try:
runtime = self.getTotalTime()
self.logMsg("Runtime is missing, Kodi runtime: %s"
% runtime, 1)
except:
self.logMsg('Could not get kodi runtime, setting to zero', -1)
runtime = 0
playQueueVersion = window('playQueueVersion')
playQueueID = window('playQueueID')
playQueueItemID = window('plex_%s.playQueueItemID' % currentFile)
with embydb.GetEmbyDB() as emby_db:
emby_dbitem = emby_db.getItem_byId(itemId)
try:
fileid = emby_dbitem[1]
except TypeError:
self.logMsg("Could not find fileid in plex db.", 1)
fileid = None
# Save data map for updates and position calls
data = {
'playQueueVersion': playQueueVersion,
'playQueueID': playQueueID,
'playQueueItemID': playQueueItemID,
'runtime': runtime * 1000,
'runtime': runtime,
'item_id': itemId,
'refresh_id': refresh_id,
'currentfile': currentFile,
@ -237,11 +247,14 @@ class Player(xbmc.Player):
'SubtitleStreamIndex': postdata['SubtitleStreamIndex'],
'playmethod': playMethod,
'Type': itemType,
'currentPosition': int(seekTime)
'currentPosition': int(seekTime),
'fileid': fileid,
'itemType': itemType,
'playcount': playcount
}
self.played_info[currentFile] = data
log("ADDING_FILE: %s" % self.played_info, 1)
self.logMsg("ADDING_FILE: %s" % data, 1)
# log some playback stats
'''if(itemType != None):
@ -258,199 +271,48 @@ class Player(xbmc.Player):
else:
self.playStats[playMethod] = 1'''
def reportPlayback(self):
# Don't use if Plex Companion is enabled
if not self.doNotify:
return
log = self.logMsg
log("reportPlayback Called", 2)
# Get current file
currentFile = self.currentFile
data = self.played_info.get(currentFile)
# only report playback if emby has initiated the playback (item_id has value)
if data:
# Get playback inforation
itemId = data['item_id']
audioindex = data['AudioStreamIndex']
subtitleindex = data['SubtitleStreamIndex']
playTime = data['currentPosition']
playMethod = data['playmethod']
paused = data.get('paused', False)
duration = data.get('runtime', '')
# Get playback volume
volume_query = {
"jsonrpc": "2.0",
"id": 1,
"method": "Application.GetProperties",
"params": {
"properties": ["volume", "muted"]
}
}
result = xbmc.executeJSONRPC(json.dumps(volume_query))
result = json.loads(result)
result = result.get('result')
volume = result.get('volume')
muted = result.get('muted')
# Postdata for the websocketclient report
# postdata = {
# 'QueueableMediaTypes': "Video",
# 'CanSeek': True,
# 'ItemId': itemId,
# 'MediaSourceId': itemId,
# 'PlayMethod': playMethod,
# 'PositionTicks': int(playTime * 10000000),
# 'IsPaused': paused,
# 'VolumeLevel': volume,
# 'IsMuted': muted
# }
if paused == 'stopped':
state = 'stopped'
elif paused is True:
state = 'paused'
else:
state = 'playing'
postdata = {
'ratingKey': itemId,
'state': state, # 'stopped', 'paused', 'buffering', 'playing'
'time': int(playTime) * 1000,
'duration': int(duration) * 1000
}
# For PMS playQueues/playlists only
if data.get('playQueueID'):
postdata['containerKey'] = '/playQueues/' + data.get('playQueueID')
postdata['playQueueVersion'] = data.get('playQueueVersion')
postdata['playQueueItemID'] = data.get('playQueueItemID')
if playMethod == "Transcode":
# Track can't be changed, keep reporting the same index
postdata['AudioStreamIndex'] = audioindex
postdata['AudioStreamIndex'] = subtitleindex
else:
# Get current audio and subtitles track
tracks_query = {
"jsonrpc": "2.0",
"id": 1,
"method": "Player.GetProperties",
"params": {
"playerid": 1,
"properties": ["currentsubtitle","currentaudiostream","subtitleenabled"]
}
}
result = xbmc.executeJSONRPC(json.dumps(tracks_query))
result = json.loads(result)
result = result.get('result')
try: # Audio tracks
indexAudio = result['currentaudiostream']['index']
except (KeyError, TypeError):
indexAudio = 0
try: # Subtitles tracks
indexSubs = result['currentsubtitle']['index']
except (KeyError, TypeError):
indexSubs = 0
try: # If subtitles are enabled
subsEnabled = result['subtitleenabled']
except (KeyError, TypeError):
subsEnabled = ""
# Postdata for the audio
data['AudioStreamIndex'], postdata['AudioStreamIndex'] = [indexAudio + 1] * 2
# Postdata for the subtitles
if subsEnabled and len(xbmc.Player().getAvailableSubtitleStreams()) > 0:
# Number of audiotracks to help get Emby Index
audioTracks = len(xbmc.Player().getAvailableAudioStreams())
mapping = utils.window("emby_%s.indexMapping" % currentFile)
if mapping: # Set in PlaybackUtils.py
log("Mapping for external subtitles index: %s" % mapping, 2)
externalIndex = json.loads(mapping)
if externalIndex.get(str(indexSubs)):
# If the current subtitle is in the mapping
subindex = [externalIndex[str(indexSubs)]] * 2
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex
else:
# Internal subtitle currently selected
subindex = [indexSubs - len(externalIndex) + audioTracks + 1] * 2
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex
else: # Direct paths enabled scenario or no external subtitles set
subindex = [indexSubs + audioTracks + 1] * 2
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = subindex
else:
data['SubtitleStreamIndex'], postdata['SubtitleStreamIndex'] = [""] * 2
# Report progress via websocketclient
# postdata = json.dumps(postdata)
# self.ws.sendProgressUpdate(postdata)
self.doUtils(
"{server}/:/timeline?" + urlencode(postdata), type="GET")
def onPlayBackPaused(self):
currentFile = self.currentFile
self.logMsg("PLAYBACK_PAUSED: %s" % currentFile.decode('utf-8'), 2)
self.logMsg("PLAYBACK_PAUSED: %s" % utils.tryDecode(currentFile), 2)
if self.played_info.get(currentFile):
self.played_info[currentFile]['paused'] = True
self.reportPlayback()
def onPlayBackResumed(self):
currentFile = self.currentFile
self.logMsg("PLAYBACK_RESUMED: %s" % currentFile.decode('utf-8'), 2)
self.logMsg("PLAYBACK_RESUMED: %s" % utils.tryDecode(currentFile), 2)
if self.played_info.get(currentFile):
self.played_info[currentFile]['paused'] = False
self.reportPlayback()
def onPlayBackSeek(self, time, seekOffset):
# Make position when seeking a bit more accurate
currentFile = self.currentFile
self.logMsg("PLAYBACK_SEEK: %s" % currentFile.decode('utf-8'), 2)
self.logMsg("PLAYBACK_SEEK: %s" % utils.tryDecode(currentFile), 2)
if self.played_info.get(currentFile):
position = self.xbmcplayer.getTime()
try:
position = self.getTime()
except RuntimeError:
# When Kodi is not playing
return
self.played_info[currentFile]['currentPosition'] = position
self.reportPlayback()
def onPlayBackStopped(self):
# Will be called when user stops xbmc playing a file
log = self.logMsg
window = utils.window
log("ONPLAYBACK_STOPPED", 1)
self.logMsg("ONPLAYBACK_STOPPED", 1)
self.stopAll()
window('Plex_currently_playing_itemid', clear=True)
window('emby_customPlaylist', clear=True)
window('emby_customPlaylist.seektime', clear=True)
window('emby_customPlaylist.seektime', clear=True)
log("Clear playlist properties.", 1)
window('plex_customplaylist', clear=True)
window('plex_customplaylist.seektime', clear=True)
window('plex_customplaylist.seektime', clear=True)
self.logMsg("Clear playlist properties.", 1)
def onPlayBackEnded(self):
# Will be called when xbmc stops playing a file, because the file ended
@ -459,31 +321,28 @@ class Player(xbmc.Player):
def stopAll(self):
log = self.logMsg
lang = utils.language
settings = utils.settings
doUtils = self.doUtils
if not self.played_info:
return
log("Played_information: %s" % self.played_info, 1)
self.logMsg("Played_information: %s" % self.played_info, 1)
# Process each items
for item in self.played_info:
data = self.played_info.get(item)
if data:
log("Item path: %s" % item, 2)
log("Item data: %s" % data, 2)
self.logMsg("Item path: %s" % item, 2)
self.logMsg("Item data: %s" % data, 2)
runtime = data['runtime']
currentPosition = data['currentPosition']
itemid = data['item_id']
refresh_id = data['refresh_id']
currentFile = data['currentfile']
type = data['Type']
media_type = data['Type']
playMethod = data['playmethod']
# Prevent manually mark as watched in Kodi monitor
@ -491,21 +350,31 @@ class Player(xbmc.Player):
if currentPosition and runtime:
try:
percentComplete = currentPosition / int(runtime)
percentComplete = float(currentPosition) / float(runtime)
except ZeroDivisionError:
# Runtime is 0.
percentComplete = 0
markPlayedAt = float(settings('markPlayed')) / 100
log("Percent complete: %s Mark played at: %s"
% (percentComplete, markPlayedAt), 1)
markPlayed = 0.90
self.logMsg("Percent complete: %s Mark played at: %s"
% (percentComplete, markPlayed), 1)
if percentComplete >= markPlayed:
# Tell Kodi that we've finished watching (Plex knows)
if (data['fileid'] is not None and
data['itemType'] in ('movie', 'episode')):
with kodidb.GetKodiDB('video') as kodi_db:
kodi_db.addPlaystate(
data['fileid'],
None,
None,
data['playcount'] + 1,
utils.DateToKodi(utils.getUnixTimestamp()))
# Send the delete action to the server.
offerDelete = False
if type == "Episode" and settings('deleteTV') == "true":
if media_type == "Episode" and settings('deleteTV') == "true":
offerDelete = True
elif type == "Movie" and settings('deleteMovies') == "true":
elif media_type == "Movie" and settings('deleteMovies') == "true":
offerDelete = True
if settings('offerDelete') != "true":
@ -514,19 +383,18 @@ class Player(xbmc.Player):
# Plex: never delete
offerDelete = False
if percentComplete >= markPlayedAt and offerDelete:
if percentComplete >= markPlayed and offerDelete:
resp = xbmcgui.Dialog().yesno(
lang(30091),
lang(33015),
autoclose=120000)
if not resp:
log("User skipped deletion.", 1)
self.logMsg("User skipped deletion.", 1)
continue
url = "{server}/emby/Items/%s?format=json" % itemid
log("Deleting request: %s" % itemid, 1)
doUtils(url, type="DELETE")
self.stopPlayback(data)
self.logMsg("Deleting request: %s" % itemid, 1)
self.doUtils(url, action_type="DELETE")
# Clean the WINDOW properties
for filename in self.played_info:
@ -536,35 +404,18 @@ class Player(xbmc.Player):
'emby_%s.refreshid' % filename,
'emby_%s.playmethod' % filename,
'emby_%s.type' % filename,
'plex_%s.playQueueItemID' % filename,
'plex_%s.playlistPosition' % filename,
'plex_%s.guid' % filename
'emby_%s.runtime' % filename,
'emby_%s.playcount' % filename,
'plex_%s.playlistPosition' % filename
)
for item in cleanup:
utils.window(item, clear=True)
# Stop transcoding
if playMethod == "Transcode":
log("Transcoding for %s terminating" % itemid, 1)
doUtils(
self.logMsg("Transcoding for %s terminating" % itemid, 1)
self.doUtils(
"{server}/video/:/transcode/universal/stop",
parameters={'session': self.clientInfo.getDeviceId()})
self.played_info.clear()
def stopPlayback(self, data):
self.logMsg("stopPlayback called", 1)
itemId = data['item_id']
playTime = data['currentPosition']
duration = data.get('runtime', '')
url = "{server}/:/timeline?"
args = {
'ratingKey': itemId,
'state': 'stopped', # 'stopped', 'paused', 'buffering', 'playing'
'time': int(playTime) * 1000,
'duration': int(duration)
}
url = url + urlencode(args)
self.doUtils(url, type="GET")

View file

@ -4,144 +4,287 @@
import json
from urllib import urlencode
from threading import Lock
from functools import wraps
import xbmc
import xbmcgui
import embydb_functions as embydb
import read_embyserver as embyserver
import utils
import playbackutils
import PlexFunctions
import PlexAPI
###############################################################################
class lockMethod:
"""
Decorator for class methods to lock hem completely. Same lock is used for
every single decorator and instance used!
Here only used for Playlist()
"""
lock = Lock()
@classmethod
def decorate(cls, func):
@wraps(func)
def wrapper(*args, **kwargs):
with cls.lock:
result = func(*args, **kwargs)
return result
return wrapper
@utils.logging
class Playlist():
"""
Initiate with Playlist(typus='video' or 'music')
"""
# Borg - multiple instances, shared state
_shared_state = {}
typus = None
queueId = None
playQueueVersion = None
guid = None
playlistId = None
player = xbmc.Player()
# "interal" PKC playlist
items = []
@lockMethod.decorate
def __init__(self, typus=None):
# Borg
self.__dict__ = self._shared_state
def __init__(self):
self.userid = utils.window('currUserId')
self.server = utils.window('pms_server')
# Construct the Kodi playlist instance
if self.typus == typus:
return
if typus == 'video':
self.playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
self.typus = 'video'
self.logMsg('Initiated video playlist', 1)
elif typus == 'music':
self.playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
self.typus = 'music'
self.logMsg('Initiated music playlist', 1)
else:
self.playlist = None
self.typus = None
self.logMsg('Empty playlist initiated', 1)
if self.playlist is not None:
self.playlistId = self.playlist.getPlayListId()
self.emby = embyserver.Read_EmbyServer()
@lockMethod.decorate
def getQueueIdFromPosition(self, playlistPosition):
return self.items[playlistPosition]['playQueueItemID']
def playAll(self, itemids, startat):
log = self.logMsg
window = utils.window
@lockMethod.decorate
def Typus(self, value=None):
if value:
self.typus = value
else:
return self.typus
embyconn = utils.kodiSQL('emby')
embycursor = embyconn.cursor()
emby_db = embydb.Embydb_Functions(embycursor)
@lockMethod.decorate
def PlayQueueVersion(self, value=None):
if value:
self.playQueueVersion = value
else:
return self.playQueueVersion
player = xbmc.Player()
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
playlist.clear()
@lockMethod.decorate
def QueueId(self, value=None):
if value:
self.queueId = value
else:
return self.queueId
log("---*** PLAY ALL ***---", 1)
log("Items: %s and start at: %s" % (itemids, startat), 1)
@lockMethod.decorate
def Guid(self, value=None):
if value:
self.guid = value
else:
return self.guid
started = False
window('emby_customplaylist', value="true")
if startat != 0:
# Seek to the starting position
window('emby_customplaylist.seektime', str(startat))
@lockMethod.decorate
def clear(self):
"""
Empties current Kodi playlist and associated variables
"""
self.logMsg('Clearing playlist', 1)
self.playlist.clear()
self.items = []
self.queueId = None
self.playQueueVersion = None
self.guid = None
def _initiatePlaylist(self):
self.logMsg('Initiating playlist', 1)
playlist = None
with embydb.GetEmbyDB() as emby_db:
for itemid in itemids:
for item in self.items:
itemid = item['plexId']
embydb_item = emby_db.getItem_byId(itemid)
try:
dbid = embydb_item[0]
mediatype = embydb_item[4]
except TypeError:
# Item is not found in our database, add item manually
log("Item was not found in the database, manually adding item.", 1)
self.logMsg('Couldnt find item %s in Kodi db'
% itemid, 1)
item = PlexFunctions.GetPlexMetadata(itemid)
if item is None or item == 401:
log('Could not download itemid %s' % itemid, -1)
if item in (None, 401):
self.logMsg('Couldnt find item %s on PMS, trying next'
% itemid, 1)
continue
if PlexAPI.API(item[0]).getType() == 'track':
playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
self.logMsg('Music playlist initiated', 1)
self.typus = 'music'
else:
self.addtoPlaylist_xbmc(playlist, item)
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
self.logMsg('Video playlist initiated', 1)
self.typus = 'video'
else:
if mediatype == 'song':
playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
self.logMsg('Music playlist initiated', 1)
self.typus = 'music'
else:
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
self.logMsg('Video playlist initiated', 1)
self.typus = 'video'
break
self.playlist = playlist
if self.playlist is not None:
self.playlistId = self.playlist.getPlayListId()
def _processItems(self, startitem, startPlayer=False):
startpos = None
with embydb.GetEmbyDB() as emby_db:
for pos, item in enumerate(self.items):
kodiId = None
plexId = item['plexId']
embydb_item = emby_db.getItem_byId(plexId)
try:
kodiId = embydb_item[0]
mediatype = embydb_item[4]
except TypeError:
self.logMsg('Couldnt find item %s in Kodi db' % plexId, 1)
xml = PlexFunctions.GetPlexMetadata(plexId)
if xml in (None, 401):
self.logMsg('Could not download plexId %s'
% plexId, -1)
else:
self.logMsg('Downloaded xml metadata, adding now', 1)
self._addtoPlaylist_xbmc(xml[0])
else:
# Add to playlist
self.addtoPlaylist(dbid, mediatype)
self.logMsg("Adding %s PlexId %s, KodiId %s to playlist."
% (mediatype, plexId, kodiId), 1)
self._addtoPlaylist(kodiId, mediatype)
# Add the kodiId
if kodiId is not None:
item['kodiId'] = str(kodiId)
if (startpos is None and startitem[1] == item[startitem[0]]):
startpos = pos
log("Adding %s to playlist." % itemid, 1)
if not started:
started = True
player.play(playlist)
self.verifyPlaylist()
def modifyPlaylist(self, itemids):
log = self.logMsg
embyconn = utils.kodiSQL('emby')
embycursor = embyconn.cursor()
emby_db = embydb.Embydb_Functions(embycursor)
log("---*** ADD TO PLAYLIST ***---", 1)
log("Items: %s" % itemids, 1)
# player = xbmc.Player()
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
for itemid in itemids:
embydb_item = emby_db.getItem_byId(itemid)
try:
dbid = embydb_item[0]
mediatype = embydb_item[4]
except TypeError:
# Item is not found in our database, add item manually
item = self.emby.getItem(itemid)
self.addtoPlaylist_xbmc(playlist, item)
if startPlayer is True and len(self.playlist) > 0:
if startpos is not None:
self.player.play(self.playlist, startpos=startpos)
else:
# Add to playlist
self.addtoPlaylist(dbid, mediatype)
self.logMsg('Never received a starting item for playlist, '
'starting with the first entry', 1)
self.player.play(self.playlist)
log("Adding %s to playlist." % itemid, 1)
@lockMethod.decorate
def playAll(self, items, startitem, offset):
"""
items: list of dicts of the form
{
'playQueueItemID': Plex playQueueItemID, e.g. '29175'
'plexId': Plex ratingKey, e.g. '125'
'kodiId': Kodi's db id of the same item
}
self.verifyPlaylist()
embycursor.close()
return playlist
startitem: tuple (typus, id), where typus is either
'playQueueItemID' or 'plexId' and id is the corresponding
id as a string
offset: First item's time offset to play in Kodi time (an int)
"""
self.logMsg("---*** PLAY ALL ***---", 1)
self.logMsg('Startitem: %s, offset: %s, items: %s'
% (startitem, offset, items), 1)
self.items = items
if self.playlist is None:
self._initiatePlaylist()
if self.playlist is None:
self.logMsg('Could not create playlist, abort', -1)
return
utils.window('plex_customplaylist', value="true")
if offset != 0:
# Seek to the starting position
utils.window('plex_customplaylist.seektime', str(offset))
self._processItems(startitem, startPlayer=True)
# Log playlist
self._verifyPlaylist()
self.logMsg('Internal playlist: %s' % self.items, 2)
@lockMethod.decorate
def modifyPlaylist(self, itemids):
self.logMsg("---*** ADD TO PLAYLIST ***---", 1)
self.logMsg("Items: %s" % itemids, 1)
self._initiatePlaylist(itemids)
self._processItems(itemids, startPlayer=True)
self._verifyPlaylist()
@lockMethod.decorate
def addtoPlaylist(self, dbid=None, mediatype=None, url=None):
"""
mediatype: Kodi type: 'movie', 'episode', 'musicvideo', 'artist',
'album', 'song', 'genre'
"""
self._addtoPlaylist(dbid=None, mediatype=None, url=None)
def _addtoPlaylist(self, dbid=None, mediatype=None, url=None):
pl = {
'jsonrpc': "2.0",
'id': 1,
'method': "Playlist.Add",
'params': {
'playlistid': 1
'playlistid': self.playlistId
}
}
if dbid is not None:
pl['params']['item'] = {'%sid' % mediatype: int(dbid)}
pl['params']['item'] = {'%sid' % utils.tryEncode(mediatype):
int(dbid)}
else:
pl['params']['item'] = {'file': url}
self.logMsg(xbmc.executeJSONRPC(json.dumps(pl)), 2)
result = xbmc.executeJSONRPC(json.dumps(pl))
self.logMsg(result, 2)
def addtoPlaylist_xbmc(self, playlist, item):
path = "plugin://plugin.video.plexkodiconnect.movies/"
def _addtoPlaylist_xbmc(self, item):
API = PlexAPI.API(item)
params = {
'mode': "play",
'dbid': 999999999
'dbid': 999999999,
'id': API.getRatingKey(),
'filename': API.getKey()
}
API = PlexAPI.API(item[0])
params['id'] = API.getRatingKey()
params['filename'] = API.getKey()
playurl = path + '?' + urlencode(params)
playurl = "plugin://plugin.video.plexkodiconnect.movies/?%s" \
% urlencode(params)
listitem = xbmcgui.ListItem()
listitem = API.CreateListItemFromPlexItem()
playbackutils.PlaybackUtils(item).setArtwork(listitem)
playlist.add(playurl, listitem)
self.playlist.add(playurl, listitem)
@lockMethod.decorate
def insertintoPlaylist(self, position, dbid=None, mediatype=None, url=None):
pl = {
@ -151,20 +294,23 @@ class Playlist():
'method': "Playlist.Insert",
'params': {
'playlistid': 1,
'playlistid': self.playlistId,
'position': position
}
}
if dbid is not None:
pl['params']['item'] = {'%sid' % mediatype: int(dbid)}
pl['params']['item'] = {'%sid' % utils.tryEncode(mediatype):
int(dbid)}
else:
pl['params']['item'] = {'file': url}
result = xbmc.executeJSONRPC(json.dumps(pl))
self.logMsg(result, 2)
self.logMsg(xbmc.executeJSONRPC(json.dumps(pl)), 2)
@lockMethod.decorate
def verifyPlaylist(self):
self._verifyPlaylist()
def _verifyPlaylist(self):
pl = {
'jsonrpc': "2.0",
@ -172,13 +318,13 @@ class Playlist():
'method': "Playlist.GetItems",
'params': {
'playlistid': 1,
'playlistid': self.playlistId,
'properties': ['title', 'file']
}
}
result = xbmc.executeJSONRPC(json.dumps(pl))
self.logMsg(result, 2)
self.logMsg(xbmc.executeJSONRPC(json.dumps(pl)), 2)
@lockMethod.decorate
def removefromPlaylist(self, position):
pl = {
@ -188,9 +334,8 @@ class Playlist():
'method': "Playlist.Remove",
'params': {
'playlistid': 1,
'playlistid': self.playlistId,
'position': position
}
}
result = xbmc.executeJSONRPC(json.dumps(pl))
self.logMsg(result, 2)
self.logMsg(xbmc.executeJSONRPC(json.dumps(pl)), 2)

View file

@ -37,100 +37,82 @@ class PlayUtils():
playurl is utf-8 encoded!
"""
log = self.logMsg
window = utils.window
self.API.setPartNumber(partNumber)
playurl = self.isDirectPlay()
if playurl:
log("File is direct playing.", 1)
playurl = playurl.encode('utf-8')
if playurl is not None:
self.logMsg("File is direct playing.", 1)
playurl = utils.tryEncode(playurl)
# Set playmethod property
window('emby_%s.playmethod' % playurl, "DirectPlay")
utils.window('emby_%s.playmethod' % playurl, "DirectPlay")
elif self.isDirectStream():
self.logMsg("File is direct streaming.", 1)
playurl = self.API.getTranscodeVideoPath('DirectStream').encode('utf-8')
playurl = utils.tryEncode(
self.API.getTranscodeVideoPath('DirectStream'))
# Set playmethod property
utils.window('emby_%s.playmethod' % playurl, "DirectStream")
elif self.isTranscoding():
log("File is transcoding.", 1)
quality = {
'maxVideoBitrate': self.getBitrate(),
'videoResolution': self.getResolution(),
'videoQuality': '100'
}
playurl = self.API.getTranscodeVideoPath(
else:
self.logMsg("File is transcoding.", 1)
playurl = utils.tryEncode(self.API.getTranscodeVideoPath(
'Transcode',
quality=quality).encode('utf-8')
quality={
'maxVideoBitrate': self.getBitrate(),
'videoResolution': self.getResolution(),
'videoQuality': '100'
}))
# Set playmethod property
window('emby_%s.playmethod' % playurl, value="Transcode")
utils.window('emby_%s.playmethod' % playurl, value="Transcode")
log("The playurl is: %s" % playurl, 1)
self.logMsg("The playurl is: %s" % playurl, 1)
return playurl
def httpPlay(self):
# Audio, Video, Photo
item = self.item
server = self.server
itemid = item['Id']
mediatype = item['MediaType']
itemid = self.item['Id']
mediatype = self.item['MediaType']
if mediatype == "Audio":
playurl = "%s/emby/Audio/%s/stream" % (server, itemid)
playurl = "%s/emby/Audio/%s/stream" % (self.server, itemid)
else:
playurl = "%s/emby/Videos/%s/stream?static=true" % (server, itemid)
playurl = "%s/emby/Videos/%s/stream?static=true" % (self.server, itemid)
return playurl
def isDirectPlay(self):
"""
Returns the path/playurl if successful, False otherwise
Returns the path/playurl if we can direct play, None otherwise
"""
# True for e.g. plex.tv watch later
if self.API.shouldStream() is True:
self.logMsg("Plex item optimized for direct streaming", 1)
return False
return
# set to either 'Direct Stream=1' or 'Transcode=2'
# and NOT to 'Direct Play=0'
if utils.settings('playType') != "0":
# User forcing to play via HTTP
self.logMsg("User chose to not direct play", 1)
return False
if self.h265enabled():
return False
path = self.API.validatePlayurl(self.API.getFilePath(),
return
if self.mustTranscode():
return
return self.API.validatePlayurl(self.API.getFilePath(),
self.API.getType(),
forceCheck=True)
if path is None:
self.logMsg('Kodi cannot access file %s - no direct play'
% path, 1)
return False
else:
self.logMsg('Kodi can access file %s - direct playing' % path, 1)
return path
def directPlay(self):
item = self.item
try:
playurl = item['MediaSources'][0]['Path']
playurl = self.item['MediaSources'][0]['Path']
except (IndexError, KeyError):
playurl = item['Path']
playurl = self.item['Path']
if item.get('VideoType'):
if self.item.get('VideoType'):
# Specific format modification
type = item['VideoType']
if type == "Dvd":
if self.item['VideoType'] == "Dvd":
playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl
elif type == "BluRay":
elif self.item['VideoType'] == "BluRay":
playurl = "%s/BDMV/index.bdmv" % playurl
# Assign network protocol
@ -146,59 +128,71 @@ class PlayUtils():
def fileExists(self):
log = self.logMsg
if 'Path' not in self.item:
# File has no path defined in server
return False
# Convert path to direct play
path = self.directPlay()
log("Verifying path: %s" % path, 1)
self.logMsg("Verifying path: %s" % path, 1)
if xbmcvfs.exists(path):
log("Path exists.", 1)
self.logMsg("Path exists.", 1)
return True
elif ":" not in path:
log("Can't verify path, assumed linux. Still try to direct play.", 1)
self.logMsg("Can't verify path, assumed linux. Still try to direct play.", 1)
return True
else:
log("Failed to find file.", 1)
self.logMsg("Failed to find file.", 1)
return False
def h265enabled(self):
def mustTranscode(self):
"""
Returns True if we need to transcode
Returns True if we need to transcode because
- codec is in h265
- 10bit video codec
- HEVC codec
if the corresponding file settings are set to 'true'
"""
videoCodec = self.API.getVideoCodec()
self.logMsg("videoCodec: %s" % videoCodec, 2)
codec = videoCodec['videocodec']
resolution = videoCodec['resolution']
h265 = self.getH265()
try:
if not ('h265' in codec or 'hevc' in codec) or (h265 is None):
return False
# E.g. trailers without a codec of None
except TypeError:
return False
if resolution >= h265:
self.logMsg("Option to transcode h265 enabled. Resolution media: "
"%s, transcoding limit resolution: %s"
% (resolution, h265), 1)
if (utils.settings('transcodeHi10P') == 'true' and
videoCodec['bitDepth'] == '10'):
self.logMsg('Option to transcode 10bit video content enabled.', 1)
return True
codec = videoCodec['videocodec']
if (utils.settings('transcodeHEVC') == 'true' and codec == 'hevc'):
self.logMsg('Option to transcode HEVC video codec enabled.', 1)
return True
if codec is None:
# e.g. trailers. Avoids TypeError with "'h265' in codec"
self.logMsg('No codec from PMS, not transcoding.', 1)
return False
try:
resolution = int(videoCodec['resolution'])
except (TypeError, ValueError):
self.logMsg('No video resolution from PMS, not transcoding.', 1)
return False
if 'h265' in codec:
if resolution >= self.getH265():
self.logMsg("Option to transcode h265 enabled. Resolution of "
"the media: %s, transcoding limit resolution: %s"
% (str(resolution), str(self.getH265())), 1)
return True
return False
def isDirectStream(self):
# Never transcode Music
if self.API.getType() == 'track':
return True
# set to 'Transcode=2'
if utils.settings('playType') == "2":
# User forcing to play via HTTP
self.logMsg("User chose to transcode", 1)
return False
if self.h265enabled():
if self.mustTranscode():
return False
# Verify the bitrate
if not self.isNetworkSufficient():
@ -207,25 +201,6 @@ class PlayUtils():
return False
return True
def directStream(self):
server = self.server
itemid = self.API.getRatingKey()
type = self.API.getType()
# if 'Path' in item and item['Path'].endswith('.strm'):
# # Allow strm loading when direct streaming
# playurl = self.directPlay()
if type == "Audio":
playurl = "%s/emby/Audio/%s/stream.mp3" % (server, itemid)
else:
playurl = "%s/emby/Videos/%s/stream?static=true" % (server, itemid)
playurl = "{server}/player/playback/playMedia?key=%2Flibrary%2Fmetadata%2F%s&offset=0&X-Plex-Client-Identifier={clientId}&machineIdentifier={SERVER ID}&address={SERVER IP}&port={SERVER PORT}&protocol=http&path=http%3A%2F%2F{SERVER IP}%3A{SERVER PORT}%2Flibrary%2Fmetadata%2F{MEDIA ID}" % (itemid)
playurl = self.API.replaceURLtags()
return playurl
def isNetworkSufficient(self):
"""
Returns True if the network is sufficient (set in file settings)
@ -243,38 +218,6 @@ class PlayUtils():
return False
return True
def isTranscoding(self):
# I hope Plex transcodes everything
return True
item = self.item
canTranscode = item['MediaSources'][0]['SupportsTranscoding']
# Make sure the server supports it
if not canTranscode:
return False
return True
def transcoding(self):
item = self.item
if 'Path' in item and item['Path'].endswith('.strm'):
# Allow strm loading when transcoding
playurl = self.directPlay()
else:
itemid = item['Id']
deviceId = self.clientInfo.getDeviceId()
playurl = (
"%s/emby/Videos/%s/master.m3u8?MediaSourceId=%s"
% (self.server, itemid, itemid)
)
playurl = (
"%s&VideoCodec=h264&AudioCodec=ac3&MaxAudioChannels=6&deviceId=%s&VideoBitrate=%s"
% (playurl, deviceId, self.getBitrate()*1000))
return playurl
def getBitrate(self):
# get the addon video quality
videoQuality = utils.settings('transcoderVideoQualities')
@ -295,14 +238,19 @@ class PlayUtils():
return bitrate.get(videoQuality, 2147483)
def getH265(self):
chosen = utils.settings('transcodeH265')
"""
Returns the user settings for transcoding h265: boundary resolutions
of 480, 720 or 1080 as an int
OR 9999999 (int) if user chose not to transcode
"""
H265 = {
'0': None,
'0': 9999999,
'1': 480,
'2': 720,
'3': 1080
}
return H265.get(chosen, None)
return H265[utils.settings('transcodeH265')]
def getResolution(self):
chosen = utils.settings('transcoderVideoQualities')
@ -365,7 +313,7 @@ class PlayUtils():
#audioStreamsChannelsList[audioNum] = stream.attrib['channels']
audioStreamsList.append(index)
audioStreams.append(track.encode('utf-8'))
audioStreams.append(utils.tryEncode(track))
audioNum += 1
# Subtitles
@ -389,7 +337,7 @@ class PlayUtils():
downloadableStreams.append(index)
subtitleStreamsList.append(index)
subtitleStreams.append(track.encode('utf-8'))
subtitleStreams.append(utils.tryEncode(track))
subNum += 1
if audioNum > 1:
@ -421,7 +369,7 @@ class PlayUtils():
% (self.server, selectSubsIndex)
url = self.API.addPlexHeadersToUrl(url)
self.logMsg("Downloadable sub: %s: %s" % (selectSubsIndex, url), 1)
listitem.setSubtitles([url.encode('utf-8')])
listitem.setSubtitles([utils.tryEncode(url)])
else:
self.logMsg('Need to burn in subtitle %s' % selectSubsIndex, 1)
playurlprefs["subtitleStreamID"] = selectSubsIndex

View file

@ -5,6 +5,7 @@ import string
import xbmc
from utils import logging
import embydb_functions as embydb
def xbmc_photo():
@ -58,7 +59,9 @@ def getOKMsg():
def timeToMillis(time):
return (time['hours']*3600 + time['minutes']*60 + time['seconds'])*1000 + time['milliseconds']
return (time['hours']*3600 +
time['minutes']*60 +
time['seconds'])*1000 + time['milliseconds']
def millisToTime(t):
@ -69,7 +72,10 @@ def millisToTime(t):
seconds = seconds % 60
minutes = minutes % 60
millis = millis % 1000
return {'hours':hours,'minutes':minutes,'seconds':seconds,'milliseconds':millis}
return {'hours': hours,
'minutes': minutes,
'seconds': seconds,
'milliseconds': millis}
def textFromXml(element):
@ -86,44 +92,70 @@ class jsonClass():
def jsonrpc(self, action, arguments={}):
""" put some JSON together for the JSON-RPC APIv6 """
if action.lower() == "sendkey":
request=json.dumps({ "jsonrpc" : "2.0" , "method" : "Input.SendText", "params" : { "text" : arguments[0], "done" : False }} )
request = json.dumps({
"jsonrpc": "2.0",
"method": "Input.SendText",
"params": {
"text": arguments[0],
"done": False
}
})
elif action.lower() == "ping":
request=json.dumps({ "jsonrpc" : "2.0",
"id" : 1 ,
"method" : "JSONRPC.Ping" })
elif action.lower() == "playmedia":
xbmc.Player().play("plugin://plugin.video.plexkodiconnect/"
"?mode=companion&arguments=%s"
% arguments)
return True
request = json.dumps({
"jsonrpc": "2.0",
"id": 1,
"method": "JSONRPC.Ping"
})
elif arguments:
request=json.dumps({ "id" : 1,
"jsonrpc" : "2.0",
"method" : action,
"params" : arguments})
request = json.dumps({
"id": 1,
"jsonrpc": "2.0",
"method": action,
"params": arguments})
else:
request=json.dumps({ "id" : 1,
"jsonrpc" : "2.0",
"method" : action})
request = json.dumps({
"id": 1,
"jsonrpc": "2.0",
"method": action
})
result = self.parseJSONRPC(xbmc.executeJSONRPC(request))
if not result and self.settings['webserver_enabled']:
# xbmc.executeJSONRPC appears to fail on the login screen, but going
# through the network stack works, so let's try the request again
# xbmc.executeJSONRPC appears to fail on the login screen, but
# going through the network stack works, so let's try the request
# again
result = self.parseJSONRPC(self.requestMgr.post(
"127.0.0.1",
self.settings['port'],
"/jsonrpc",
request,
{ 'Content-Type' : 'application/json',
'Authorization' : 'Basic ' + string.strip(base64.encodestring(self.settings['user'] + ':' + self.settings['passwd'])) }))
{'Content-Type': 'application/json',
'Authorization': 'Basic %s' % string.strip(
base64.encodestring('%s:%s'
% (self.settings['user'],
self.settings['passwd'])))
}))
return result
def skipTo(self, plexId, typus):
self.logMsg('players: %s' % self.getPlayers())
# playlistId = self.getPlaylistId(tryDecode(xbmc_type(typus)))
# playerId = self.
with embydb.GetEmbyDB() as emby_db:
embydb_item = emby_db.getItem_byId(plexId)
try:
dbid = embydb_item[0]
mediatype = embydb_item[4]
except TypeError:
self.logMsg('Couldnt find item %s in Kodi db' % plexId, 1)
return
self.logMsg('plexid: %s, kodi id: %s, type: %s'
% (plexId, dbid, mediatype))
def getPlexHeaders(self):
h = {
"Content-type": "application/x-www-form-urlencoded",
"Content-type": "text/xml",
"Access-Control-Allow-Origin": "*",
"X-Plex-Version": self.settings['version'],
"X-Plex-Client-Identifier": self.settings['uuid'],
@ -143,7 +175,7 @@ class jsonClass():
self.logMsg("Empty response from XBMC", 1)
return {}
else:
parsed=json.loads(jsonraw)
parsed = json.loads(jsonraw)
if parsed.get('error', False):
self.logMsg("XBMC returned an error: %s" % parsed.get('error'), -1)
return parsed.get('result', {})
@ -156,6 +188,27 @@ class jsonClass():
ret[player['type']] = player
return ret
def getPlaylistId(self, typus):
"""
typus: one of the Kodi types, e.g. audio or video
Returns None if nothing was found
"""
for playlist in self.getPlaylists():
if playlist.get('type') == typus:
return playlist.get('playlistid')
def getPlaylists(self):
"""
Returns a list, e.g.
[
{u'playlistid': 0, u'type': u'audio'},
{u'playlistid': 1, u'type': u'video'},
{u'playlistid': 2, u'type': u'picture'}
]
"""
return self.jsonrpc('Playlist.GetPlaylists')
def getPlayerIds(self):
ret = []
for player in self.getPlayers().values():
@ -178,7 +231,10 @@ class jsonClass():
return players.get(xbmc_photo(), {}).get('playerid', None)
def getVolume(self):
answ = self.jsonrpc('Application.GetProperties', { "properties": [ "volume", 'muted' ] })
answ = self.jsonrpc('Application.GetProperties',
{
"properties": ["volume", 'muted']
})
vol = str(answ.get('volume', 100))
mute = ("0", "1")[answ.get('muted', False)]
return (vol, mute)

View file

@ -1,5 +1,4 @@
import re
import traceback
from SocketServer import ThreadingMixIn
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from urlparse import urlparse, parse_qs
@ -13,6 +12,7 @@ from utils import logging
@logging
class MyHandler(BaseHTTPRequestHandler):
protocol_version = 'HTTP/1.1'
regex = re.compile(r'''/playQueues/(\d+)$''')
def __init__(self, *args, **kwargs):
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
@ -38,18 +38,20 @@ class MyHandler(BaseHTTPRequestHandler):
def do_OPTIONS(self):
self.send_response(200)
self.send_header('Content-Length', '0')
self.send_header('X-Plex-Client-Identifier', self.server.settings['uuid'])
self.send_header('X-Plex-Client-Identifier',
self.server.settings['uuid'])
self.send_header('Content-Type', 'text/plain')
self.send_header('Connection', 'close')
self.send_header('Access-Control-Max-Age', '1209600')
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods',
'POST, GET, OPTIONS, DELETE, PUT, HEAD')
self.send_header('Access-Control-Allow-Headers',
'x-plex-version, x-plex-platform-version, '
'x-plex-username, x-plex-client-identifier, '
'x-plex-target-client-identifier, x-plex-device-name, '
'x-plex-platform, x-plex-product, accept, x-plex-device')
'POST, GET, OPTIONS, DELETE, PUT, HEAD')
self.send_header(
'Access-Control-Allow-Headers',
'x-plex-version, x-plex-platform-version, x-plex-username, '
'x-plex-client-identifier, x-plex-target-client-identifier, '
'x-plex-device-name, x-plex-platform, x-plex-product, accept, '
'x-plex-device')
self.end_headers()
self.wfile.close()
@ -71,9 +73,10 @@ class MyHandler(BaseHTTPRequestHandler):
def answer_request(self, sendData):
self.serverlist = self.server.client.getServerList()
self.subMgr = self.server.subscriptionManager
self.js = self.server.jsonClass
self.settings = self.server.settings
subMgr = self.server.subscriptionManager
js = self.server.jsonClass
settings = self.server.settings
queue = self.server.queue
try:
request_path = self.path[1:]
@ -83,60 +86,82 @@ class MyHandler(BaseHTTPRequestHandler):
params = {}
for key in paramarrays:
params[key] = paramarrays[key][0]
self.logMsg("remote request_path: %s" % request_path, 2)
self.logMsg("params received from remote: %s" % params, 2)
self.subMgr.updateCommandID(self.headers.get('X-Plex-Client-Identifier', self.client_address[0]), params.get('commandID', False))
if request_path=="version":
self.response("PleXBMC Helper Remote Redirector: Running\r\nVersion: %s" % self.settings['version'])
elif request_path=="verify":
result=self.js.jsonrpc("ping")
self.response("XBMC JSON connection test:\r\n"+result)
subMgr.updateCommandID(self.headers.get(
'X-Plex-Client-Identifier',
self.client_address[0]),
params.get('commandID', False))
if request_path == "version":
self.response(
"PlexKodiConnect Plex Companion: Running\r\nVersion: %s"
% settings['version'])
elif request_path == "verify":
self.response("XBMC JSON connection test:\r\n" +
js.jsonrpc("ping"))
elif "resources" == request_path:
resp = getXMLHeader()
resp += "<MediaContainer>"
resp += "<Player"
resp += ' title="%s"' % self.settings['client_name']
resp += ' protocol="plex"'
resp += ' protocolVersion="1"'
resp += ' protocolCapabilities="navigation,playback,timeline"'
resp += ' machineIdentifier="%s"' % self.settings['uuid']
resp += ' product="PlexKodiConnect"'
resp += ' platform="%s"' % self.settings['platform']
resp += ' platformVersion="%s"' % self.settings['plexbmc_version']
resp += ' deviceClass="pc"'
resp += "/>"
resp += "</MediaContainer>"
resp = ('%s'
'<MediaContainer>'
'<Player'
' title="%s"'
' protocol="plex"'
' protocolVersion="1"'
' protocolCapabilities="navigation,playback,timeline"'
' machineIdentifier="%s"'
' product="PlexKodiConnect"'
' platform="%s"'
' platformVersion="%s"'
' deviceClass="pc"'
'/>'
'</MediaContainer>'
% (getXMLHeader(),
settings['client_name'],
settings['uuid'],
settings['platform'],
settings['plexbmc_version']))
self.logMsg("crafted resources response: %s" % resp, 2)
self.response(resp, self.js.getPlexHeaders())
self.response(resp, js.getPlexHeaders())
elif "/subscribe" in request_path:
self.response(getOKMsg(), self.js.getPlexHeaders())
self.response(getOKMsg(), js.getPlexHeaders())
protocol = params.get('protocol', False)
host = self.client_address[0]
port = params.get('port', False)
uuid = self.headers.get('X-Plex-Client-Identifier', "")
commandID = params.get('commandID', 0)
self.subMgr.addSubscriber(protocol, host, port, uuid, commandID)
subMgr.addSubscriber(protocol,
host,
port,
uuid,
commandID)
elif "/poll" in request_path:
if params.get('wait', False) == '1':
sleep(950)
commandID = params.get('commandID', 0)
self.response(re.sub(r"INSERTCOMMANDID", str(commandID), self.subMgr.msg(self.js.getPlayers())), {
'X-Plex-Client-Identifier': self.settings['uuid'],
'Access-Control-Expose-Headers': 'X-Plex-Client-Identifier',
'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/xml'
})
self.response(
re.sub(r"INSERTCOMMANDID",
str(commandID),
subMgr.msg(js.getPlayers())),
{
'X-Plex-Client-Identifier': settings['uuid'],
'Access-Control-Expose-Headers':
'X-Plex-Client-Identifier',
'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/xml'
})
elif "/unsubscribe" in request_path:
self.response(getOKMsg(), self.js.getPlexHeaders())
uuid = self.headers.get('X-Plex-Client-Identifier', False) or self.client_address[0]
self.subMgr.removeSubscriber(uuid)
self.response(getOKMsg(), js.getPlexHeaders())
uuid = self.headers.get('X-Plex-Client-Identifier', False) \
or self.client_address[0]
subMgr.removeSubscriber(uuid)
elif request_path == "player/playback/setParameters":
self.response(getOKMsg(), self.js.getPlexHeaders())
self.response(getOKMsg(), js.getPlexHeaders())
if 'volume' in params:
volume = int(params['volume'])
self.logMsg("adjusting the volume to %s%%" % volume, 2)
self.js.jsonrpc("Application.SetVolume", {"volume": volume})
js.jsonrpc("Application.SetVolume",
{"volume": volume})
elif "/playMedia" in request_path:
self.response(getOKMsg(), self.js.getPlexHeaders())
self.response(getOKMsg(), js.getPlexHeaders())
offset = params.get('viewOffset', params.get('offset', "0"))
protocol = params.get('protocol', "http")
address = params.get('address', self.client_address[0])
@ -146,78 +171,102 @@ class MyHandler(BaseHTTPRequestHandler):
containerKey = urlparse(params.get('containerKey')).path
except:
containerKey = ''
regex = re.compile(r'''/playQueues/(\d+)$''')
try:
playQueueID = regex.findall(containerKey)[0]
playQueueID = self.regex.findall(containerKey)[0]
except IndexError:
playQueueID = ''
self.js.jsonrpc("playmedia", params)
self.subMgr.lastkey = params['key']
self.subMgr.containerKey = containerKey
self.subMgr.playQueueID = playQueueID
self.subMgr.server = server.get('server', 'localhost')
self.subMgr.port = port
self.subMgr.protocol = protocol
self.subMgr.notify()
# We need to tell service.py
queue.put({
'action': 'playlist',
'data': params
})
subMgr.lastkey = params['key']
subMgr.containerKey = containerKey
subMgr.playQueueID = playQueueID
subMgr.server = server.get('server', 'localhost')
subMgr.port = port
subMgr.protocol = protocol
subMgr.notify()
elif request_path == "player/playback/play":
self.response(getOKMsg(), self.js.getPlexHeaders())
for playerid in self.js.getPlayerIds():
self.js.jsonrpc("Player.PlayPause", {"playerid" : playerid, "play": True})
self.response(getOKMsg(), js.getPlexHeaders())
for playerid in js.getPlayerIds():
js.jsonrpc("Player.PlayPause",
{"playerid": playerid, "play": True})
subMgr.notify()
elif request_path == "player/playback/pause":
self.response(getOKMsg(), self.js.getPlexHeaders())
for playerid in self.js.getPlayerIds():
self.js.jsonrpc("Player.PlayPause", {"playerid" : playerid, "play": False})
self.response(getOKMsg(), js.getPlexHeaders())
for playerid in js.getPlayerIds():
js.jsonrpc("Player.PlayPause",
{"playerid": playerid, "play": False})
subMgr.notify()
elif request_path == "player/playback/stop":
self.response(getOKMsg(), self.js.getPlexHeaders())
for playerid in self.js.getPlayerIds():
self.js.jsonrpc("Player.Stop", {"playerid" : playerid})
self.response(getOKMsg(), js.getPlexHeaders())
for playerid in js.getPlayerIds():
js.jsonrpc("Player.Stop", {"playerid": playerid})
subMgr.notify()
elif request_path == "player/playback/seekTo":
self.response(getOKMsg(), self.js.getPlexHeaders())
for playerid in self.js.getPlayerIds():
self.js.jsonrpc("Player.Seek", {"playerid":playerid, "value":millisToTime(params.get('offset', 0))})
self.subMgr.notify()
self.response(getOKMsg(), js.getPlexHeaders())
for playerid in js.getPlayerIds():
js.jsonrpc("Player.Seek",
{"playerid": playerid,
"value": millisToTime(
params.get('offset', 0))})
subMgr.notify()
elif request_path == "player/playback/stepForward":
self.response(getOKMsg(), self.js.getPlexHeaders())
for playerid in self.js.getPlayerIds():
self.js.jsonrpc("Player.Seek", {"playerid":playerid, "value":"smallforward"})
self.subMgr.notify()
self.response(getOKMsg(), js.getPlexHeaders())
for playerid in js.getPlayerIds():
js.jsonrpc("Player.Seek",
{"playerid": playerid,
"value": "smallforward"})
subMgr.notify()
elif request_path == "player/playback/stepBack":
self.response(getOKMsg(), self.js.getPlexHeaders())
for playerid in self.js.getPlayerIds():
self.js.jsonrpc("Player.Seek", {"playerid":playerid, "value":"smallbackward"})
self.subMgr.notify()
self.response(getOKMsg(), js.getPlexHeaders())
for playerid in js.getPlayerIds():
js.jsonrpc("Player.Seek",
{"playerid": playerid,
"value": "smallbackward"})
subMgr.notify()
elif request_path == "player/playback/skipNext":
self.response(getOKMsg(), self.js.getPlexHeaders())
for playerid in self.js.getPlayerIds():
self.js.jsonrpc("Player.Seek", {"playerid":playerid, "value":"bigforward"})
self.subMgr.notify()
self.response(getOKMsg(), js.getPlexHeaders())
for playerid in js.getPlayerIds():
js.jsonrpc("Player.GoTo",
{"playerid": playerid,
"to": "next"})
subMgr.notify()
elif request_path == "player/playback/skipPrevious":
self.response(getOKMsg(), self.js.getPlexHeaders())
for playerid in self.js.getPlayerIds():
self.js.jsonrpc("Player.Seek", {"playerid":playerid, "value":"bigbackward"})
self.subMgr.notify()
self.response(getOKMsg(), js.getPlexHeaders())
for playerid in js.getPlayerIds():
js.jsonrpc("Player.GoTo",
{"playerid": playerid,
"to": "previous"})
subMgr.notify()
elif request_path == "player/playback/skipTo":
js.skipTo(params.get('key').rsplit('/', 1)[1],
params.get('type'))
subMgr.notify()
elif request_path == "player/navigation/moveUp":
self.response(getOKMsg(), self.js.getPlexHeaders())
self.js.jsonrpc("Input.Up")
self.response(getOKMsg(), js.getPlexHeaders())
js.jsonrpc("Input.Up")
elif request_path == "player/navigation/moveDown":
self.response(getOKMsg(), self.js.getPlexHeaders())
self.js.jsonrpc("Input.Down")
self.response(getOKMsg(), js.getPlexHeaders())
js.jsonrpc("Input.Down")
elif request_path == "player/navigation/moveLeft":
self.response(getOKMsg(), self.js.getPlexHeaders())
self.js.jsonrpc("Input.Left")
self.response(getOKMsg(), js.getPlexHeaders())
js.jsonrpc("Input.Left")
elif request_path == "player/navigation/moveRight":
self.response(getOKMsg(), self.js.getPlexHeaders())
self.js.jsonrpc("Input.Right")
self.response(getOKMsg(), js.getPlexHeaders())
js.jsonrpc("Input.Right")
elif request_path == "player/navigation/select":
self.response(getOKMsg(), self.js.getPlexHeaders())
self.js.jsonrpc("Input.Select")
self.response(getOKMsg(), js.getPlexHeaders())
js.jsonrpc("Input.Select")
elif request_path == "player/navigation/home":
self.response(getOKMsg(), self.js.getPlexHeaders())
self.js.jsonrpc("Input.Home")
self.response(getOKMsg(), js.getPlexHeaders())
js.jsonrpc("Input.Home")
elif request_path == "player/navigation/back":
self.response(getOKMsg(), self.js.getPlexHeaders())
self.js.jsonrpc("Input.Back")
self.response(getOKMsg(), js.getPlexHeaders())
js.jsonrpc("Input.Back")
else:
self.logMsg('Unknown request path: %s' % request_path, -1)
# elif 'player/mirror/details' in request_path:
# # Detailed e.g. Movie information page was opened
# # CURRENTLY NOT POSSIBLE DUE TO KODI RESTRICTIONS
@ -240,6 +289,7 @@ class MyHandler(BaseHTTPRequestHandler):
except:
self.logMsg('Error encountered. Traceback:', -1)
import traceback
self.logMsg(traceback.print_exc(), -1)
@ -247,7 +297,7 @@ class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
daemon_threads = True
def __init__(self, client, subscriptionManager, jsonClass, settings,
*args, **kwargs):
queue, *args, **kwargs):
"""
client: Class handle to plexgdm.plexgdm. We can thus ask for an up-to-
date serverlist without instantiating anything
@ -258,4 +308,5 @@ class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
self.subscriptionManager = subscriptionManager
self.jsonClass = jsonClass
self.settings = settings
self.queue = queue
HTTPServer.__init__(self, *args, **kwargs)

View file

@ -22,18 +22,15 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
MA 02110-1301, USA.
"""
__author__ = 'DHJ (hippojay) <plex@h-jay.com>'
import socket
import struct
import threading
import time
from xbmc import sleep
import downloadutils
from PlexFunctions import PMSHttpsEnabled
from utils import window, logging
from utils import window, logging, settings
@logging
@ -60,7 +57,7 @@ class plexgdm:
self.client_registered = False
self.download = downloadutils.DownloadUtils().downloadUrl
def clientDetails(self, settings):
def clientDetails(self, options):
self.client_data = (
"Content-Type: plex/media-player\r\n"
"Resource-Identifier: %s\r\n"
@ -74,13 +71,13 @@ class plexgdm:
"mirror,playqueues\r\n"
"Device-Class: HTPC"
) % (
settings['uuid'],
settings['client_name'],
settings['myport'],
settings['addonName'],
settings['version']
options['uuid'],
options['client_name'],
options['myport'],
options['addonName'],
options['version']
)
self.client_id = settings['uuid']
self.client_id = options['uuid']
def getClientDetails(self):
if not self.client_data:
@ -215,119 +212,28 @@ class plexgdm:
return self.server_list
def discover(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Set a timeout so the socket does not block indefinitely
sock.settimeout(0.6)
# Set the time-to-live for messages to 1 for local network
ttl = struct.pack('b', 1)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
returnData = []
try:
# Send data to the multicast group
self.logMsg("Sending discovery messages: %s"
% self.discover_message, 2)
sock.sendto(self.discover_message, self.discover_group)
# Look for responses from all recipients
while True:
try:
data, server = sock.recvfrom(1024)
self.logMsg("Received data from %s, %s" % server, 2)
returnData.append({'from': server,
'data': data})
except socket.timeout:
break
except:
# if we can't send our discovery query, just abort and try again
# on the next loop
return
finally:
sock.close()
self.discovery_complete = True
discovered_servers = []
if returnData:
for response in returnData:
update = {'server': response.get('from')[0]}
# Check if we had a positive HTTP reponse
if "200 OK" in response.get('data'):
for each in response.get('data').split('\r\n'):
update['discovery'] = "auto"
update['owned'] = '1'
update['master'] = 1
update['role'] = 'master'
update['class'] = None
if "Content-Type:" in each:
update['content-type'] = each.split(':')[1].strip()
elif "Resource-Identifier:" in each:
update['uuid'] = each.split(':')[1].strip()
elif "Name:" in each:
update['serverName'] = each.split(':')[1].strip()
elif "Port:" in each:
update['port'] = each.split(':')[1].strip()
elif "Updated-At:" in each:
update['updated'] = each.split(':')[1].strip()
elif "Version:" in each:
update['version'] = each.split(':')[1].strip()
elif "Server-Class:" in each:
update['class'] = each.split(':')[1].strip()
# Quickly test if we need https
https = PMSHttpsEnabled(
'%s:%s' % (update['server'], update['port']))
if https is None:
# Error contacting server
continue
elif https:
update['protocol'] = 'https'
else:
update['protocol'] = 'http'
discovered_servers.append(update)
# Append REMOTE PMS that we haven't found yet; if necessary
currServer = window('pms_server')
if currServer:
currServerProt, currServerIP, currServerPort = \
currServer.split(':')
currServerIP = currServerIP.replace('/', '')
for server in discovered_servers:
if server['server'] == currServerIP:
break
else:
# Currently active server was not discovered via GDM; ADD
update = {
'port': currServerPort,
'protocol': currServerProt,
'class': None,
'content-type': 'plex/media-server',
'discovery': 'auto',
'master': 1,
'owned': '1',
'role': 'master',
'server': currServerIP,
'serverName': window('plex_servername'),
'updated': int(time.time()),
'uuid': window('plex_machineIdentifier'),
'version': 'irrelevant'
}
discovered_servers.append(update)
self.server_list = discovered_servers
if not self.server_list:
self.logMsg("No servers have been discovered", 0)
else:
self.logMsg("Number of servers Discovered: %s"
% len(self.server_list), 2)
for items in self.server_list:
self.logMsg("Server Discovered: %s" % items, 2)
if not currServer:
return
currServerProt, currServerIP, currServerPort = \
currServer.split(':')
currServerIP = currServerIP.replace('/', '')
# Currently active server was not discovered via GDM; ADD
self.server_list = [{
'port': currServerPort,
'protocol': currServerProt,
'class': None,
'content-type': 'plex/media-server',
'discovery': 'auto',
'master': 1,
'owned': '1',
'role': 'master',
'server': currServerIP,
'serverName': window('plex_servername'),
'updated': int(time.time()),
'uuid': window('plex_machineIdentifier'),
'version': 'irrelevant'
}]
def setInterval(self, interval):
self.discovery_interval = interval
@ -388,4 +294,5 @@ class plexgdm:
def start_all(self, daemon=False):
self.start_discovery(daemon)
self.start_registration(daemon)
if settings('plexCompanion') == 'true':
self.start_registration(daemon)

View file

@ -1,8 +1,6 @@
import re
import threading
from xbmc import Player
import downloadutils
from utils import window, logging
import PlexFunctions as pf
@ -11,13 +9,19 @@ from functions import *
@logging
class SubscriptionManager:
def __init__(self, jsonClass, RequestMgr):
def __init__(self, jsonClass, RequestMgr, player, playlist):
self.serverlist = []
self.subscribers = {}
self.info = {}
self.lastkey = ""
self.containerKey = ""
self.lastratingkey = ""
self.ratingkey = ""
self.lastplayers = {}
self.lastinfo = {
'video': {},
'audio': {},
'picture': {}
}
self.volume = 0
self.mute = '0'
self.server = ""
@ -25,7 +29,8 @@ class SubscriptionManager:
self.port = ""
self.playerprops = {}
self.doUtils = downloadutils.DownloadUtils().downloadUrl
self.xbmcplayer = Player()
self.xbmcplayer = player
self.playlist = playlist
self.js = jsonClass
self.RequestMgr = RequestMgr
@ -62,7 +67,7 @@ class SubscriptionManager:
msg += self.getTimelineXML(self.js.getVideoPlayerId(players), plex_video())
msg += "\r\n</MediaContainer>"
return msg
def getTimelineXML(self, playerid, ptype):
if playerid is not None:
info = self.getPlayerProperties(playerid)
@ -95,21 +100,22 @@ class SubscriptionManager:
xbmc.sleep(100)
count += 1
if keyid:
self.lastkey = "/library/metadata/%s"%keyid
self.lastratingkey = keyid
ret += ' location="%s"' % (self.mainlocation)
ret += ' key="%s"' % (self.lastkey)
ret += ' ratingKey="%s"' % (self.lastratingkey)
self.lastkey = "/library/metadata/%s" % keyid
self.ratingkey = keyid
ret += ' location="%s"' % self.mainlocation
ret += ' key="%s"' % self.lastkey
ret += ' ratingKey="%s"' % self.ratingkey
serv = self.getServerByHost(self.server)
if info.get('playQueueID'):
self.containerKey = "/playQueues/%s" % info.get('playQueueID')
ret += ' playQueueID="%s"' % info.get('playQueueID')
ret += ' playQueueVersion="%s"' % info.get('playQueueVersion')
ret += ' playQueueItemID="%s"' % (info.get('playQueueItemID'))
ret += ' playQueueItemID="%s"' % info.get('playQueueItemID')
ret += ' containerKey="%s"' % self.containerKey
ret += ' guid="%s"' % info['guid']
elif keyid:
self.containerKey = self.lastkey
ret += ' containerKey="%s"' % (self.containerKey)
ret += ' containerKey="%s"' % self.containerKey
ret += ' duration="%s"' % info['duration']
ret += ' seekRange="0-%s"' % info['duration']
@ -118,7 +124,6 @@ class SubscriptionManager:
ret += ' protocol="%s"' % serv.get('protocol', "http")
ret += ' address="%s"' % serv.get('server', self.server)
ret += ' port="%s"' % serv.get('port', self.port)
ret += ' guid="%s"' % info['guid']
ret += ' volume="%s"' % info['volume']
ret += ' shuffle="%s"' % info['shuffle']
ret += ' mute="%s"' % self.mute
@ -132,13 +137,14 @@ class SubscriptionManager:
def updateCommandID(self, uuid, commandID):
if commandID and self.subscribers.get(uuid, False):
self.subscribers[uuid].commandID = int(commandID)
def notify(self, event = False):
self.subscribers[uuid].commandID = int(commandID)
def notify(self, event=False):
self.cleanup()
# Don't tell anyone if we don't know a Plex ID and are still playing
# (e.g. no stop called). Used for e.g. PVR/TV without PKC usage
if not window('Plex_currently_playing_itemid'):
if (not window('Plex_currently_playing_itemid')
and not self.lastplayers):
return True
players = self.js.getPlayers()
# fetch the message, subscribers or not, since the server
@ -147,35 +153,46 @@ class SubscriptionManager:
if self.subscribers:
with threading.RLock():
for sub in self.subscribers.values():
sub.send_update(msg, len(players)==0)
sub.send_update(msg, len(players) == 0)
self.notifyServer(players)
self.lastplayers = players
return True
def notifyServer(self, players):
if not players:
return True
for p in players.values():
info = self.playerprops[p.get('playerid')]
params = {'state': 'stopped'}
params['containerKey'] = (self.containerKey or "/library/metadata/900000")
if info.get('playQueueID'):
params['containerKey'] = '/playQueues/' + info['playQueueID']
params['playQueueVersion'] = info['playQueueVersion']
params['playQueueItemID'] = info['playQueueItemID']
params['key'] = (self.lastkey or "/library/metadata/900000")
params['ratingKey'] = (self.lastratingkey or "900000")
params['state'] = info['state']
params['time'] = info['time']
params['duration'] = info['duration']
serv = self.getServerByHost(self.server)
url = serv.get('protocol', 'http') + '://' \
+ serv.get('server', 'localhost') + ':' \
+ serv.get('port', '32400') + "/:/timeline"
self.doUtils(url, type="GET", parameters=params)
# requests.getwithparams(serv.get('server', 'localhost'), serv.get('port', 32400), "/:/timeline", params, getPlexHeaders(), serv.get('protocol', 'http'))
self.logMsg("sent server notification with state = %s"
% params['state'], 2)
def notifyServer(self, players):
for typus, p in players.iteritems():
info = self.playerprops[p.get('playerid')]
self._sendNotification(info)
self.lastinfo[typus] = info
# Cross the one of the list
try:
del self.lastplayers[typus]
except KeyError:
pass
# Process the players we have left (to signal a stop)
for typus, p in self.lastplayers.iteritems():
self.lastinfo[typus]['state'] = 'stopped'
self._sendNotification(self.lastinfo[typus])
def _sendNotification(self, info):
params = {
'containerKey': self.containerKey or "/library/metadata/900000",
'key': self.lastkey or "/library/metadata/900000",
'ratingKey': self.ratingkey or "900000",
'state': info['state'],
'time': info['time'],
'duration': info['duration']
}
if info.get('playQueueID'):
params['containerKey'] = '/playQueues/%s' % info['playQueueID']
params['playQueueVersion'] = info['playQueueVersion']
params['playQueueItemID'] = info['playQueueItemID']
serv = self.getServerByHost(self.server)
url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'),
serv.get('server', 'localhost'),
serv.get('port', '32400'))
self.doUtils(url, parameters=params)
self.logMsg("Sent server notification with parameters: %s to %s"
% (params, url), 2)
def controllable(self):
return "volume,shuffle,repeat,audioStream,videoStream,subtitleStream,skipPrevious,skipNext,seekTo,stepBack,stepForward,stop,playPause"
@ -205,30 +222,50 @@ class SubscriptionManager:
if sub.age > 30:
sub.cleanup()
del self.subscribers[sub.uuid]
def getPlayerProperties(self, playerid):
info = {}
try:
# get info from the player
props = self.js.jsonrpc("Player.GetProperties", {"playerid": playerid, "properties": ["time", "totaltime", "speed", "shuffled", "repeat"]})
self.logMsg(self.js.jsonrpc("Player.GetItem", {"playerid": playerid, "properties": ["file", "showlink", "episode", "season"]}), 2)
info['time'] = timeToMillis(props['time'])
info['duration'] = timeToMillis(props['totaltime'])
info['state'] = ("paused", "playing")[int(props['speed'])]
info['shuffle'] = ("0","1")[props.get('shuffled', False)]
info['repeat'] = pf.getPlexRepeat(props.get('repeat'))
# New PMS playQueue attributes
cf = self.xbmcplayer.getPlayingFile()
info['playQueueID'] = window('playQueueID')
info['playQueueVersion'] = window('playQueueVersion')
info['playQueueItemID'] = window('plex_%s.playQueueItemID' % cf)
info['guid'] = window('plex_%s.guid' % cf)
props = self.js.jsonrpc(
"Player.GetProperties",
{"playerid": playerid,
"properties": ["time",
"totaltime",
"speed",
"shuffled",
"repeat"]})
info = {
'time': timeToMillis(props['time']),
'duration': timeToMillis(props['totaltime']),
'state': ("paused", "playing")[int(props['speed'])],
'shuffle': ("0", "1")[props.get('shuffled', False)],
'repeat': pf.getPlexRepeat(props.get('repeat')),
}
if self.playlist is not None:
if self.playlist.QueueId() is not None:
info['playQueueID'] = self.playlist.QueueId()
info['playQueueVersion'] = self.playlist.PlayQueueVersion()
info['guid'] = self.playlist.Guid()
# Get the playlist position
pos = self.js.jsonrpc(
"Player.GetProperties",
{"playerid": playerid,
"properties": ["position"]})
info['playQueueItemID'] = \
self.playlist.getQueueIdFromPosition(pos['position'])
except:
info['time'] = 0
info['duration'] = 0
info['state'] = "stopped"
info['shuffle'] = False
import traceback
self.logMsg("Traceback:\n%s"
% traceback.format_exc(), -1)
info = {
'time': 0,
'duration': 0,
'state': 'stopped',
'shuffle': False,
'repeat': 0
}
# get the volume from the application
info['volume'] = self.volume
info['mute'] = self.mute
@ -283,6 +320,6 @@ class Subscriber:
"""
response = self.doUtils(url,
postBody=msg,
type="POST")
action_type="POST")
if response in [False, None, 401]:
self.subMgr.removeSubscriber(self.uuid)

View file

@ -31,8 +31,7 @@ class Read_EmbyServer():
# This will return the full item
item = {}
url = "{server}/emby/Users/{UserId}/Items/%s?format=json" % itemid
result = self.doUtils(url)
result = self.doUtils("{server}/emby/Users/{UserId}/Items/%s?format=json" % itemid)
if result:
item = result
@ -45,13 +44,12 @@ class Read_EmbyServer():
itemlists = self.split_list(itemlist, 50)
for itemlist in itemlists:
# Will return basic information
url = "{server}/emby/Users/{UserId}/Items?&format=json"
params = {
'Ids': ",".join(itemlist),
'Fields': "Etag"
}
result = self.doUtils(url, parameters=params)
result = self.doUtils("{server}/emby/Users/{UserId}/Items?&format=json", parameters=params)
if result:
items.extend(result['Items'])
@ -64,7 +62,6 @@ class Read_EmbyServer():
itemlists = self.split_list(itemlist, 50)
for itemlist in itemlists:
url = "{server}/emby/Users/{UserId}/Items?format=json"
params = {
"Ids": ",".join(itemlist),
@ -75,25 +72,22 @@ class Read_EmbyServer():
"Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
"CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
"Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers,"
"MediaSources"
"MediaSources,VoteCount"
)
}
result = self.doUtils(url, parameters=params)
result = self.doUtils("{server}/emby/Users/{UserId}/Items?format=json", parameters=params)
if result:
items.extend(result['Items'])
return items
def getView_embyId(self, itemid):
# Returns ancestors using embyId
def getView_plexid(self, itemid):
# Returns ancestors using plexid
viewId = None
url = "{server}/emby/Items/%s/Ancestors?UserId={UserId}&format=json" % itemid
result = self.doUtils(url)
for view in result:
for view in self.doUtils("{server}/emby/Items/%s/Ancestors?UserId={UserId}&format=json" % itemid):
viewtype = view['Type']
if viewtype == "CollectionFolder":
if view['Type'] == "CollectionFolder":
# Found view
viewId = view['Id']
@ -120,8 +114,6 @@ class Read_EmbyServer():
return [viewName, viewId, mediatype]
def getFilteredSection(self, parentid, itemtype=None, sortby="SortName", recursive=True, limit=None, sortorder="Ascending", filter=""):
doUtils = self.doUtils
url = "{server}/emby/Users/{UserId}/Items?format=json"
params = {
'ParentId': parentid,
@ -140,11 +132,9 @@ class Read_EmbyServer():
"CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
"Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers")
}
return doUtils(url, parameters=params)
return self.doUtils("{server}/emby/Users/{UserId}/Items?format=json", parameters=params)
def getTvChannels(self):
doUtils = self.doUtils
url = "{server}/emby/LiveTv/Channels/?userid={UserId}&format=json"
params = {
'EnableImages': True,
@ -154,11 +144,9 @@ class Read_EmbyServer():
"CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
"Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers")
}
return doUtils(url, parameters=params)
return self.doUtils("{server}/emby/LiveTv/Channels/?userid={UserId}&format=json", parameters=params)
def getTvRecordings(self, groupid):
doUtils = self.doUtils
url = "{server}/emby/LiveTv/Recordings/?userid={UserId}&format=json"
if groupid == "root": groupid = ""
params = {
@ -170,13 +158,10 @@ class Read_EmbyServer():
"CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
"Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers")
}
return doUtils(url, parameters=params)
return self.doUtils("{server}/emby/LiveTv/Recordings/?userid={UserId}&format=json", parameters=params)
def getSection(self, parentid, itemtype=None, sortby="SortName", basic=False, dialog=None):
log = self.logMsg
doUtils = self.doUtils
items = {
'Items': [],
@ -195,13 +180,13 @@ class Read_EmbyServer():
'Recursive': True,
'Limit': 1
}
result = doUtils(url, parameters=params)
result = self.doUtils(url, parameters=params)
try:
total = result['TotalRecordCount']
items['TotalRecordCount'] = total
except TypeError: # Failed to retrieve
log("%s:%s Failed to retrieve the server response." % (url, params), 2)
self.logMsg("%s:%s Failed to retrieve the server response." % (url, params), 2)
else:
index = 0
@ -234,36 +219,36 @@ class Read_EmbyServer():
"Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
"CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
"Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers,"
"MediaSources"
"MediaSources,VoteCount"
)
result = doUtils(url, parameters=params)
result = self.doUtils(url, parameters=params)
try:
items['Items'].extend(result['Items'])
except TypeError:
# Something happened to the connection
if not throttled:
throttled = True
log("Throttle activated.", 1)
self.logMsg("Throttle activated.", 1)
if jump == highestjump:
# We already tried with the highestjump, but it failed. Reset value.
log("Reset highest value.", 1)
self.logMsg("Reset highest value.", 1)
highestjump = 0
# Lower the number by half
if highestjump:
throttled = False
jump = highestjump
log("Throttle deactivated.", 1)
self.logMsg("Throttle deactivated.", 1)
else:
jump = int(jump/4)
log("Set jump limit to recover: %s" % jump, 2)
self.logMsg("Set jump limit to recover: %s" % jump, 2)
retry = 0
while utils.window('emby_online') != "true":
while utils.window('plex_online') != "true":
# Wait server to come back online
if retry == 5:
log("Unable to reconnect to server. Abort process.", 1)
self.logMsg("Unable to reconnect to server. Abort process.", 1)
return items
retry += 1
@ -291,12 +276,11 @@ class Read_EmbyServer():
increment = 10
jump += increment
log("Increase jump limit to: %s" % jump, 1)
self.logMsg("Increase jump limit to: %s" % jump, 1)
return items
def getViews(self, mediatype="", root=False, sortedlist=False):
# Build a list of user views
doUtils = self.doUtils
views = []
mediatype = mediatype.lower()
@ -305,7 +289,7 @@ class Read_EmbyServer():
else: # Views ungrouped
url = "{server}/emby/Users/{UserId}/Items?Sortby=SortName&format=json"
result = doUtils(url)
result = self.doUtils(url)
try:
items = result['Items']
except TypeError:
@ -313,11 +297,8 @@ class Read_EmbyServer():
else:
for item in items:
name = item['Name']
itemId = item['Id']
viewtype = item['Type']
if viewtype == "Channel":
item['Name'] = item['Name']
if item['Type'] == "Channel":
# Filter view types
continue
@ -328,20 +309,20 @@ class Read_EmbyServer():
# Assumed missing is mixed then.
'''if itemtype is None:
url = "{server}/emby/Library/MediaFolders?format=json"
result = doUtils(url)
result = self.doUtils(url)
for folder in result['Items']:
if itemId == folder['Id']:
if item['Id'] == folder['Id']:
itemtype = folder.get('CollectionType', "mixed")'''
if name not in ('Collections', 'Trailers'):
if item['Name'] not in ('Collections', 'Trailers'):
if sortedlist:
views.append({
'name': name,
'name': item['Name'],
'type': itemtype,
'id': itemId
'id': item['Id']
})
elif (itemtype == mediatype or
@ -349,9 +330,9 @@ class Read_EmbyServer():
views.append({
'name': name,
'name': item['Name'],
'type': itemtype,
'id': itemId
'id': item['Id']
})
return views
@ -359,8 +340,6 @@ class Read_EmbyServer():
def verifyView(self, parentid, itemid):
belongs = False
url = "{server}/emby/Users/{UserId}/Items?format=json"
params = {
'ParentId': parentid,
@ -370,7 +349,7 @@ class Read_EmbyServer():
'Recursive': True,
'Ids': itemid
}
result = self.doUtils(url, parameters=params)
result = self.doUtils("{server}/emby/Users/{UserId}/Items?format=json", parameters=params)
try:
total = result['TotalRecordCount']
except TypeError:
@ -383,40 +362,23 @@ class Read_EmbyServer():
return belongs
def getMovies(self, parentId, basic=False, dialog=None):
items = self.getSection(parentId, "Movie", basic=basic, dialog=dialog)
return items
return self.getSection(parentId, "Movie", basic=basic, dialog=dialog)
def getBoxset(self, dialog=None):
items = self.getSection(None, "BoxSet", dialog=dialog)
return items
return self.getSection(None, "BoxSet", dialog=dialog)
def getMovies_byBoxset(self, boxsetid):
items = self.getSection(boxsetid, "Movie")
return items
return self.getSection(boxsetid, "Movie")
def getMusicVideos(self, parentId, basic=False, dialog=None):
items = self.getSection(parentId, "MusicVideo", basic=basic, dialog=dialog)
return items
return self.getSection(parentId, "MusicVideo", basic=basic, dialog=dialog)
def getHomeVideos(self, parentId):
items = self.getSection(parentId, "Video")
return items
return self.getSection(parentId, "Video")
def getShows(self, parentId, basic=False, dialog=None):
items = self.getSection(parentId, "Series", basic=basic, dialog=dialog)
return items
return self.getSection(parentId, "Series", basic=basic, dialog=dialog)
def getSeasons(self, showId):
@ -426,13 +388,12 @@ class Read_EmbyServer():
'TotalRecordCount': 0
}
url = "{server}/emby/Shows/%s/Seasons?UserId={UserId}&format=json" % showId
params = {
'IsVirtualUnaired': False,
'Fields': "Etag"
}
result = self.doUtils(url, parameters=params)
result = self.doUtils("{server}/emby/Shows/%s/Seasons?UserId={UserId}&format=json" % showId, parameters=params)
if result:
items = result
@ -440,25 +401,19 @@ class Read_EmbyServer():
def getEpisodes(self, parentId, basic=False, dialog=None):
items = self.getSection(parentId, "Episode", basic=basic, dialog=dialog)
return items
return self.getSection(parentId, "Episode", basic=basic, dialog=dialog)
def getEpisodesbyShow(self, showId):
items = self.getSection(showId, "Episode")
return items
return self.getSection(showId, "Episode")
def getEpisodesbySeason(self, seasonId):
items = self.getSection(seasonId, "Episode")
return self.getSection(seasonId, "Episode")
return items
def getArtists(self, dialog=None):
doUtils = self.doUtils
items = {
'Items': [],
@ -472,7 +427,7 @@ class Read_EmbyServer():
'Recursive': True,
'Limit': 1
}
result = doUtils(url, parameters=params)
result = self.doUtils(url, parameters=params)
try:
total = result['TotalRecordCount']
items['TotalRecordCount'] = total
@ -502,7 +457,7 @@ class Read_EmbyServer():
"AirTime,DateCreated,MediaStreams,People,ProviderIds,Overview"
)
}
result = doUtils(url, parameters=params)
result = self.doUtils(url, parameters=params)
items['Items'].extend(result['Items'])
index += jump
@ -512,28 +467,17 @@ class Read_EmbyServer():
return items
def getAlbums(self, basic=False, dialog=None):
items = self.getSection(None, "MusicAlbum", sortby="DateCreated", basic=basic, dialog=dialog)
return items
return self.getSection(None, "MusicAlbum", sortby="DateCreated", basic=basic, dialog=dialog)
def getAlbumsbyArtist(self, artistId):
items = self.getSection(artistId, "MusicAlbum", sortby="DateCreated")
return items
return self.getSection(artistId, "MusicAlbum", sortby="DateCreated")
def getSongs(self, basic=False, dialog=None):
items = self.getSection(None, "Audio", basic=basic, dialog=dialog)
return items
return self.getSection(None, "Audio", basic=basic, dialog=dialog)
def getSongsbyAlbum(self, albumId):
return self.getSection(albumId, "Audio")
items = self.getSection(albumId, "Audio")
return items
def getAdditionalParts(self, itemId):
@ -543,8 +487,7 @@ class Read_EmbyServer():
'TotalRecordCount': 0
}
url = "{server}/emby/Videos/%s/AdditionalParts?UserId={UserId}&format=json" % itemId
result = self.doUtils(url)
result = self.doUtils("{server}/emby/Videos/%s/AdditionalParts?UserId={UserId}&format=json" % itemId)
if result:
items = result
@ -566,25 +509,21 @@ class Read_EmbyServer():
def updateUserRating(self, itemid, like=None, favourite=None, deletelike=False):
# Updates the user rating to Emby
doUtils = self.doUtils
if favourite:
url = "{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid
doUtils(url, type="POST")
self.doUtils("{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid, action_type="POST")
elif favourite == False:
url = "{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid
doUtils(url, type="DELETE")
self.doUtils("{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid, action_type="DELETE")
if not deletelike and like:
url = "{server}/emby/Users/{UserId}/Items/%s/Rating?Likes=true&format=json" % itemid
doUtils(url, type="POST")
elif not deletelike and like == False:
url = "{server}/emby/Users/{UserId}/Items/%s/Rating?Likes=false&format=json" % itemid
doUtil(url, type="POST")
self.doUtils("{server}/emby/Users/{UserId}/Items/%s/Rating?Likes=true&format=json" % itemid, action_type="POST")
elif not deletelike and like is False:
self.doUtils("{server}/emby/Users/{UserId}/Items/%s/Rating?Likes=false&format=json" % itemid, action_type="POST")
elif deletelike:
url = "{server}/emby/Users/{UserId}/Items/%s/Rating?format=json" % itemid
doUtils(url, type="DELETE")
self.doUtils("{server}/emby/Users/{UserId}/Items/%s/Rating?format=json" % itemid, action_type="DELETE")
else:
self.logMsg("Error processing user rating.", 1)
self.logMsg("Update user rating to emby for itemid: %s "
"| like: %s | favourite: %s | deletelike: %s"
% (itemid, like, favourite, deletelike), 1)
% (itemid, like, favourite, deletelike), 1)

View file

@ -114,8 +114,6 @@ class UserClient(threading.Thread):
# url = "{server}/emby/System/Configuration?format=json"
# result = doUtils.downloadUrl(url)
# utils.settings('markPlayed', value=str(result['MaxResumePct']))
def hasAccess(self):
# Plex: always return True for now
return True
@ -126,19 +124,19 @@ class UserClient(threading.Thread):
url = "{server}/emby/Users?format=json"
result = self.doUtils.downloadUrl(url)
if result == False:
if result is False:
# Access is restricted, set in downloadutils.py via exception
log("Access is restricted.", 1)
self.HasAccess = False
elif window('emby_online') != "true":
elif window('plex_online') != "true":
# Server connection failed
pass
elif window('emby_serverStatus') == "restricted":
elif window('plex_serverStatus') == "restricted":
log("Access is granted.", 1)
self.HasAccess = True
window('emby_serverStatus', clear=True)
window('plex_serverStatus', clear=True)
xbmcgui.Dialog().notification(self.addonName,
utils.language(33007))
@ -239,7 +237,7 @@ class UserClient(threading.Thread):
# Give attempts at entering password / selecting user
if self.retry >= 2:
log("Too many retries to login.", -1)
window('emby_serverStatus', value="Stop")
window('plex_serverStatus', value="Stop")
dialog.ok(lang(33001),
lang(39023))
xbmc.executebuiltin(
@ -247,8 +245,8 @@ class UserClient(threading.Thread):
return False
# Get /profile/addon_data
addondir = xbmc.translatePath(
self.addon.getAddonInfo('profile')).decode('utf-8')
addondir = utils.tryDecode(xbmc.translatePath(
self.addon.getAddonInfo('profile')))
hasSettings = xbmcvfs.exists("%ssettings.xml" % addondir)
# If there's no settings.xml
@ -358,7 +356,7 @@ class UserClient(threading.Thread):
break
xbmc.sleep(1000)
status = window('emby_serverStatus')
status = window('plex_serverStatus')
if status == "Stop":
xbmc.sleep(500)
@ -371,7 +369,7 @@ class UserClient(threading.Thread):
elif status == "401":
# Unauthorized access, revoke token
window('emby_serverStatus', value="Auth")
window('plex_serverStatus', value="Auth")
self.resetClient()
xbmc.sleep(2000)
@ -389,7 +387,7 @@ class UserClient(threading.Thread):
log("Current accessToken: xxxx", 1)
self.retry = 0
window('suspend_LibraryThread', clear=True)
window('emby_serverStatus', clear=True)
window('plex_serverStatus', clear=True)
if not self.auth and (self.currUser is None):
# Loop if no server found

View file

@ -16,6 +16,7 @@ from functools import wraps
from calendar import timegm
import os
import xbmc
import xbmcaddon
import xbmcgui
@ -98,7 +99,7 @@ def IfExists(path):
Returns True if path exists, else false
"""
dummyfile = os.path.join(path, 'dummyfile.txt').encode('utf-8')
dummyfile = tryEncode(os.path.join(path, 'dummyfile.txt'))
try:
etree.ElementTree(etree.Element('test')).write(dummyfile)
except:
@ -111,6 +112,46 @@ def IfExists(path):
return answer
def forEveryMethod(decorator):
"""
Wrapper for classes to add the decorator "decorator" to all methods of the
class
"""
def decorate(cls):
for attr in cls.__dict__: # there's propably a better way to do this
if callable(getattr(cls, attr)):
setattr(cls, attr, decorator(getattr(cls, attr)))
return cls
return decorate
def CatchExceptions(warnuser=False):
"""
Decorator for methods to catch exceptions and log them. Useful for e.g.
librarysync threads using itemtypes.py, because otherwise we would not
get informed of crashes
warnuser=True: sets the window flag 'plex_scancrashed' to true
which will trigger a Kodi infobox to inform user
"""
def decorate(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
logMsg(addonName, '%s has crashed' % func.__name__, -1)
logMsg(addonName, e, -1)
import traceback
logMsg(addonName, "Traceback:\n%s"
% traceback.format_exc(), -1)
if warnuser:
window('plex_scancrashed', value='true')
return
return wrapper
return decorate
def LogTime(func):
"""
Decorator for functions and methods to log the time it took to run the code
@ -254,7 +295,7 @@ def getUnixTimestamp(secondsIntoTheFuture=None):
def logMsg(title, msg, level=1):
# Get the logLevel set in UserClient
try:
logLevel = int(window('emby_logLevel'))
logLevel = int(window('plex_logLevel'))
except ValueError:
logLevel = 0
kodiLevel = {
@ -272,7 +313,7 @@ def logMsg(title, msg, level=1):
except UnicodeEncodeError:
try:
xbmc.log("%s -> %s : %s" % (
title, func.co_name, msg.encode('utf-8')),
title, func.co_name, tryEncode(msg)),
level=kodiLevel[level])
except:
xbmc.log("%s -> %s : %s" % (
@ -283,7 +324,7 @@ def logMsg(title, msg, level=1):
xbmc.log("%s -> %s" % (title, msg), level=kodiLevel[level])
except UnicodeEncodeError:
try:
xbmc.log("%s -> %s" % (title, msg.encode('utf-8')),
xbmc.log("%s -> %s" % (title, tryEncode(msg)),
level=kodiLevel[level])
except:
xbmc.log("%s -> %s " % (title, 'COULDNT LOG'),
@ -299,18 +340,14 @@ def window(property, value=None, clear=False, windowid=10000):
Property needs to be string; value may be string or unicode
"""
WINDOW = xbmcgui.Window(windowid)
#setproperty accepts both string and unicode but utf-8 strings are adviced by kodi devs because some unicode can give issues
'''if isinstance(property, unicode):
property = property.encode("utf-8")
if isinstance(value, unicode):
value = value.encode("utf-8")'''
if clear:
WINDOW.clearProperty(property)
elif value is not None:
WINDOW.setProperty(property, value.encode('utf-8'))
WINDOW.setProperty(property, tryEncode(value))
else:
return WINDOW.getProperty(property).decode('utf-8')
return tryDecode(WINDOW.getProperty(property))
def settings(setting, value=None):
"""
@ -323,10 +360,10 @@ def settings(setting, value=None):
if value is not None:
# Takes string or unicode by default!
addon.setSetting(setting, value.encode('utf-8'))
addon.setSetting(setting, tryEncode(value))
else:
# Should return unicode by default, but just in case
return addon.getSetting(setting).decode('utf-8')
return tryDecode(addon.getSetting(setting))
def language(stringid):
# Central string retrieval
@ -334,40 +371,39 @@ def language(stringid):
string = addon.getLocalizedString(stringid) #returns unicode object
return string
def kodiSQL(type="video"):
if type == "emby":
dbPath = xbmc.translatePath("special://database/emby.db").decode('utf-8')
elif type == "music":
def kodiSQL(media_type="video"):
if media_type == "emby":
dbPath = tryDecode(xbmc.translatePath("special://database/emby.db"))
elif media_type == "music":
dbPath = getKodiMusicDBPath()
elif type == "texture":
dbPath = xbmc.translatePath("special://database/Textures13.db").decode('utf-8')
elif media_type == "texture":
dbPath = tryDecode(xbmc.translatePath(
"special://database/Textures13.db"))
else:
dbPath = getKodiVideoDBPath()
connection = sqlite3.connect(dbPath)
return connection
def getKodiVideoDBPath():
kodibuild = xbmc.getInfoLabel('System.BuildVersion')[:2]
dbVersion = {
"13": 78, # Gotham
"14": 90, # Helix
"15": 93, # Isengard
"16": 99, # Jarvis
"17":104 # Krypton
"17": 107 # Krypton
}
dbPath = xbmc.translatePath(
"special://database/MyVideos%s.db"
% dbVersion.get(kodibuild, "")).decode('utf-8')
dbPath = tryDecode(xbmc.translatePath(
"special://database/MyVideos%s.db"
% dbVersion.get(xbmc.getInfoLabel('System.BuildVersion')[:2], "")))
return dbPath
def getKodiMusicDBPath():
kodibuild = xbmc.getInfoLabel('System.BuildVersion')[:2]
dbVersion = {
"13": 46, # Gotham
@ -377,9 +413,9 @@ def getKodiMusicDBPath():
"17": 60 # Krypton
}
dbPath = xbmc.translatePath(
"special://database/MyMusic%s.db"
% dbVersion.get(kodibuild, "")).decode('utf-8')
dbPath = tryDecode(xbmc.translatePath(
"special://database/MyMusic%s.db"
% dbVersion.get(xbmc.getInfoLabel('System.BuildVersion')[:2], "")))
return dbPath
def getScreensaver():
@ -394,11 +430,7 @@ def getScreensaver():
'setting': "screensaver.mode"
}
}
result = xbmc.executeJSONRPC(json.dumps(query))
result = json.loads(result)
screensaver = result['result']['value']
return screensaver
return json.loads(xbmc.executeJSONRPC(json.dumps(query)))['result']['value']
def setScreensaver(value):
# Toggle the screensaver
@ -413,21 +445,19 @@ def setScreensaver(value):
'value': value
}
}
result = xbmc.executeJSONRPC(json.dumps(query))
logMsg("PLEX", "Toggling screensaver: %s %s" % (value, result), 1)
logMsg("PLEX", "Toggling screensaver: %s %s" % (value, xbmc.executeJSONRPC(json.dumps(query))), 1)
def reset():
dialog = xbmcgui.Dialog()
resp = dialog.yesno("Warning", "Are you sure you want to reset your local Kodi database?")
if resp == 0:
if dialog.yesno("Warning", "Are you sure you want to reset your local Kodi database?") == 0:
return
# first stop any db sync
window('emby_shouldStop', value="true")
window('plex_shouldStop', value="true")
count = 10
while window('emby_dbScan') == "true":
while window('plex_dbScan') == "true":
logMsg("PLEX", "Sync is running, will retry: %s..." % count)
count -= 1
if count == 0:
@ -442,7 +472,7 @@ def reset():
deleteNodes()
# Wipe the kodi databases
logMsg("EMBY", "Resetting the Kodi video database.", 0)
logMsg("Plex", "Resetting the Kodi video database.", 0)
connection = kodiSQL('video')
cursor = connection.cursor()
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
@ -455,7 +485,7 @@ def reset():
cursor.close()
if settings('enableMusic') == "true":
logMsg("EMBY", "Resetting the Kodi music database.")
logMsg("Plex", "Resetting the Kodi music database.")
connection = kodiSQL('music')
cursor = connection.cursor()
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
@ -467,8 +497,8 @@ def reset():
connection.commit()
cursor.close()
# Wipe the emby database
logMsg("EMBY", "Resetting the Emby database.", 0)
# Wipe the Plex database
logMsg("Plex", "Resetting the Emby database.", 0)
connection = kodiSQL('emby')
cursor = connection.cursor()
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
@ -483,21 +513,25 @@ def reset():
cursor.close()
# Offer to wipe cached thumbnails
resp = dialog.yesno("Warning", "Removed all cached artwork?")
resp = dialog.yesno("Warning", "Remove all cached artwork?")
if resp:
logMsg("EMBY", "Resetting all cached artwork.", 0)
# Remove all existing textures first
path = xbmc.translatePath("special://thumbnails/").decode('utf-8')
path = tryDecode(xbmc.translatePath("special://thumbnails/"))
if xbmcvfs.exists(path):
allDirs, allFiles = xbmcvfs.listdir(path)
for dir in allDirs:
allDirs, allFiles = xbmcvfs.listdir(path+dir)
for file in allFiles:
if os.path.supports_unicode_filenames:
xbmcvfs.delete(os.path.join(path+dir.decode('utf-8'),file.decode('utf-8')))
xbmcvfs.delete(os.path.join(
path + tryDecode(dir),
tryDecode(file)))
else:
xbmcvfs.delete(os.path.join(path.encode('utf-8')+dir,file))
xbmcvfs.delete(os.path.join(
tryEncode(path) + dir,
file))
# remove all existing data from texture DB
connection = kodiSQL('texture')
cursor = connection.cursor()
@ -509,8 +543,8 @@ def reset():
cursor.execute("DELETE FROM " + tableName)
connection.commit()
cursor.close()
# reset the install run flag
# reset the install run flag
settings('SyncInstallRunDone', value="false")
# Remove emby info
@ -518,9 +552,9 @@ def reset():
if resp:
# Delete the settings
addon = xbmcaddon.Addon()
addondir = xbmc.translatePath(addon.getAddonInfo('profile')).decode('utf-8')
addondir = tryDecode(xbmc.translatePath(addon.getAddonInfo('profile')))
dataPath = "%ssettings.xml" % addondir
xbmcvfs.delete(dataPath.encode('utf-8'))
xbmcvfs.delete(tryEncode(dataPath))
logMsg("PLEX", "Deleting: settings.xml", 1)
dialog.ok(
@ -532,7 +566,7 @@ def profiling(sortby="cumulative"):
# Will print results to Kodi log
def decorator(func):
def wrapper(*args, **kwargs):
pr = cProfile.Profile()
pr.enable()
@ -575,8 +609,8 @@ def normalize_nodes(text):
# Remove dots from the last character as windows can not have directories
# with dots at the end
text = text.rstrip('.')
text = unicodedata.normalize('NFKD', unicode(text, 'utf-8')).encode('ascii', 'ignore')
text = tryEncode(unicodedata.normalize('NFKD', unicode(text, 'utf-8')))
return text
def normalize_string(text):
@ -594,7 +628,7 @@ def normalize_string(text):
# Remove dots from the last character as windows can not have directories
# with dots at the end
text = text.rstrip('.')
text = unicodedata.normalize('NFKD', unicode(text, 'utf-8')).encode('ascii', 'ignore')
text = tryEncode(unicodedata.normalize('NFKD', unicode(text, 'utf-8')))
return text
@ -619,7 +653,7 @@ def guisettingsXML():
"""
Returns special://userdata/guisettings.xml as an etree xml root element
"""
path = xbmc.translatePath("special://profile/").decode('utf-8')
path = tryDecode(xbmc.translatePath("special://profile/"))
xmlpath = "%sguisettings.xml" % path
try:
@ -678,7 +712,7 @@ def advancedSettingsXML():
usetags : set to "false"
findremotethumbs : set to "false"
"""
path = xbmc.translatePath("special://profile/").decode('utf-8')
path = tryDecode(xbmc.translatePath("special://profile/"))
xmlpath = "%sadvancedsettings.xml" % path
try:
@ -708,7 +742,7 @@ def advancedSettingsXML():
def sourcesXML():
# To make Master lock compatible
path = xbmc.translatePath("special://profile/").decode('utf-8')
path = tryDecode(xbmc.translatePath("special://profile/"))
xmlpath = "%ssources.xml" % path
try:
@ -717,7 +751,7 @@ def sourcesXML():
root = etree.Element('sources')
else:
root = xmlparse.getroot()
video = root.find('video')
if video is None:
@ -729,7 +763,7 @@ def sourcesXML():
for source in root.findall('.//path'):
if source.text == "smb://":
count -= 1
if count == 0:
# sources already set
break
@ -749,7 +783,7 @@ def sourcesXML():
def passwordsXML():
# To add network credentials
path = xbmc.translatePath("special://userdata/").decode('utf-8')
path = tryDecode(xbmc.translatePath("special://userdata/"))
xmlpath = "%spasswords.xml" % path
logMsg('passwordsXML', 'Path to passwords.xml: %s' % xmlpath, 1)
@ -775,9 +809,7 @@ def passwordsXML():
elif option == 1:
# User selected remove
iterator = root.getiterator('passwords')
for paths in iterator:
for paths in root.getiterator('passwords'):
for path in paths:
if path.find('.//from').text == "smb://%s/" % credentials:
paths.remove(path)
@ -787,8 +819,8 @@ def passwordsXML():
etree.ElementTree(root).write(xmlpath)
break
else:
logMsg("EMBY", "Failed to find saved server: %s in passwords.xml" % credentials, 1)
logMsg("Plex", "Failed to find saved server: %s in passwords.xml" % credentials, 1)
settings('networkCreds', value="")
xbmcgui.Dialog().notification(
heading='PlexKodiConnect',
@ -842,7 +874,7 @@ def passwordsXML():
# Force Kodi to see the credentials without restarting
xbmcvfs.exists(topath)
# Add credentials
# Add credentials
settings('networkCreds', value="%s" % server)
logMsg("PLEX", "Added server: %s to passwords.xml" % server, 1)
# Prettify and write to file
@ -850,7 +882,7 @@ def passwordsXML():
indent(root)
except: pass
etree.ElementTree(root).write(xmlpath)
# dialog.notification(
# heading="PlexKodiConnect",
# message="Added to passwords.xml",
@ -862,7 +894,7 @@ def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False):
"""
Feed with tagname as unicode
"""
path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8')
path = tryDecode(xbmc.translatePath("special://profile/playlists/video/"))
if viewtype == "mixed":
plname = "%s - %s" % (tagname, mediatype)
xsppath = "%sPlex %s - %s.xsp" % (path, viewid, mediatype)
@ -871,17 +903,17 @@ def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False):
xsppath = "%sPlex %s.xsp" % (path, viewid)
# Create the playlist directory
if not xbmcvfs.exists(path.encode('utf-8')):
if not xbmcvfs.exists(tryEncode(path)):
logMsg("PLEX", "Creating directory: %s" % path, 1)
xbmcvfs.mkdirs(path.encode('utf-8'))
xbmcvfs.mkdirs(tryEncode(path))
# Only add the playlist if it doesn't already exists
if xbmcvfs.exists(xsppath.encode('utf-8')):
if xbmcvfs.exists(tryEncode(xsppath)):
logMsg('Path %s does exist' % xsppath, 1)
if delete:
xbmcvfs.delete(xsppath.encode('utf-8'))
xbmcvfs.delete(tryEncode(xsppath))
logMsg("PLEX", "Successfully removed playlist: %s." % tagname, 1)
return
# Using write process since there's no guarantee the xml declaration works with etree
@ -892,12 +924,12 @@ def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False):
}
logMsg("Plex", "Writing playlist file to: %s" % xsppath, 1)
try:
f = xbmcvfs.File(xsppath.encode('utf-8'), 'wb')
f = xbmcvfs.File(tryEncode(xsppath), 'wb')
except:
logMsg("Plex", "Failed to create playlist: %s" % xsppath, -1)
return
else:
f.write((
f.write(tryEncode(
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n'
'<smartplaylist type="%s">\n\t'
'<name>Plex %s</name>\n\t'
@ -906,47 +938,67 @@ def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False):
'<value>%s</value>\n\t'
'</rule>\n'
'</smartplaylist>\n'
% (itemtypes.get(mediatype, mediatype), plname, tagname))
.encode('utf-8'))
% (itemtypes.get(mediatype, mediatype), plname, tagname)))
f.close()
logMsg("Plex", "Successfully added playlist: %s" % tagname)
def deletePlaylists():
# Clean up the playlists
path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8')
dirs, files = xbmcvfs.listdir(path.encode('utf-8'))
path = tryDecode(xbmc.translatePath("special://profile/playlists/video/"))
dirs, files = xbmcvfs.listdir(tryEncode(path))
for file in files:
if file.decode('utf-8').startswith('Plex'):
xbmcvfs.delete(("%s%s" % (path, file.decode('utf-8'))).encode('utf-8'))
if tryDecode(file).startswith('Plex'):
xbmcvfs.delete(tryEncode("%s%s" % (path, tryDecode(file))))
def deleteNodes():
# Clean up video nodes
import shutil
path = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
dirs, files = xbmcvfs.listdir(path.encode('utf-8'))
path = tryDecode(xbmc.translatePath("special://profile/library/video/"))
dirs, files = xbmcvfs.listdir(tryEncode(path))
for dir in dirs:
if dir.decode('utf-8').startswith('Plex'):
if tryDecode(dir).startswith('Plex'):
try:
shutil.rmtree("%s%s" % (path, dir.decode('utf-8')))
shutil.rmtree("%s%s" % (path, tryDecode(dir)))
except:
logMsg("PLEX", "Failed to delete directory: %s" % dir.decode('utf-8'))
logMsg("PLEX", "Failed to delete directory: %s"
% tryDecode(dir))
for file in files:
if file.decode('utf-8').startswith('plex'):
if tryDecode(file).startswith('plex'):
try:
xbmcvfs.delete(("%s%s" % (path, file.decode('utf-8'))).encode('utf-8'))
xbmcvfs.delete(tryEncode("%s%s" % (path, tryDecode(file))))
except:
logMsg("PLEX", "Failed to file: %s" % file.decode('utf-8'))
logMsg("PLEX", "Failed to file: %s" % tryDecode(file))
def try_encode(text, encoding="utf-8"):
try:
return text.encode(encoding,"ignore")
except:
return text
def try_decode(text, encoding="utf-8"):
def tryEncode(uniString, encoding='utf-8'):
"""
Will try to encode uniString (in unicode) to encoding. This possibly
fails with e.g. Android TV's Python, which does not accept arguments for
string.encode()
"""
try:
return text.decode(encoding,"ignore")
except:
return text
uniString = uniString.encode(encoding, "ignore")
except TypeError:
uniString = uniString.encode()
except UnicodeDecodeError:
# already encoded
pass
return uniString
def tryDecode(string, encoding='utf-8'):
"""
Will try to decode string (encoded) using encoding. This possibly
fails with e.g. Android TV's Python, which does not accept arguments for
string.encode()
"""
try:
string = string.decode(encoding, "ignore")
except TypeError:
string = string.decode()
except UnicodeEncodeError:
# Already in unicode - e.g. sometimes file paths
pass
return string

View file

@ -54,16 +54,16 @@ class VideoNodes(object):
mediatype = mediatypes[mediatype]
window = utils.window
kodiversion = self.kodiversion
if viewtype == "mixed":
dirname = "%s-%s" % (viewid, mediatype)
else:
dirname = viewid
path = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
nodepath = xbmc.translatePath(
"special://profile/library/video/Plex-%s/" % dirname).decode('utf-8')
path = utils.tryDecode(xbmc.translatePath(
"special://profile/library/video/"))
nodepath = utils.tryDecode(xbmc.translatePath(
"special://profile/library/video/Plex-%s/" % dirname))
# Verify the video directory
# KODI BUG
@ -71,20 +71,22 @@ class VideoNodes(object):
# so try creating a file
if utils.IfExists(path) is False:
shutil.copytree(
src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'),
dst=xbmc.translatePath("special://profile/library/video").decode('utf-8'))
src=utils.tryDecode(xbmc.translatePath(
"special://xbmc/system/library/video")),
dst=utils.tryDecode(xbmc.translatePath(
"special://profile/library/video")))
# Create the node directory
if mediatype != "photo":
if mediatype != "photos":
if utils.IfExists(nodepath) is False:
# folder does not exist yet
self.logMsg('Creating folder %s' % nodepath, 1)
xbmcvfs.mkdirs(nodepath.encode('utf-8'))
xbmcvfs.mkdirs(utils.tryEncode(nodepath))
if delete:
dirs, files = xbmcvfs.listdir(nodepath.encode('utf-8'))
dirs, files = xbmcvfs.listdir(utils.tryEncode(nodepath))
for file in files:
xbmcvfs.delete(
(nodepath + file.decode('utf-8')).encode('utf-8'))
xbmcvfs.delete(utils.tryEncode(
(nodepath + utils.tryDecode(file))))
self.logMsg("Sucessfully removed videonode: %s."
% tagname, 1)
@ -96,16 +98,16 @@ class VideoNodes(object):
path = "library://video/Plex-%s/" % dirname
for i in range(1, indexnumber):
# Verify to make sure we don't create duplicates
if window('Emby.nodes.%s.index' % i) == path:
if window('Plex.nodes.%s.index' % i) == path:
return
if mediatype == "photo":
if mediatype == "photos":
path = "plugin://plugin.video.plexkodiconnect/?id=%s&mode=getsubfolders" % indexnumber
window('Emby.nodes.%s.index' % indexnumber, value=path)
window('Plex.nodes.%s.index' % indexnumber, value=path)
# Root
if not mediatype == "photo":
if not mediatype == "photos":
if viewtype == "mixed":
specialtag = "%s-%s" % (tagname, mediatype)
root = self.commonRoot(order=0, label=specialtag, tagname=tagname, roottype=0)
@ -215,14 +217,14 @@ class VideoNodes(object):
label = stringid
# Set window properties
if (mediatype == "homevideos" or mediatype == "photo") and nodetype == "all":
if (mediatype == "homevideos" or mediatype == "photos") and nodetype == "all":
# Custom query
path = ("plugin://plugin.video.plexkodiconnect/?id=%s&mode=browsecontent&type=%s"
% (tagname, mediatype))
elif (mediatype == "homevideos" or mediatype == "photo"):
path = ("plugin://plugin.video.plexkodiconnect/?id=%s&mode=browseplex&type=%s"
% (viewid, mediatype))
elif (mediatype == "homevideos" or mediatype == "photos"):
# Custom query
path = ("plugin://plugin.video.plexkodiconnect/?id=%s&mode=browsecontent&type=%s&folderid=%s"
% (tagname, mediatype, nodetype))
path = ("plugin://plugin.video.plexkodiconnect/?id=%s&mode=browseplex&type=%s&folderid=%s"
% (viewid, mediatype, nodetype))
elif nodetype == "nextepisodes":
# Custom query
path = "plugin://plugin.video.plexkodiconnect/?id=%s&mode=nextup&limit=%s" % (tagname, limit)
@ -231,7 +233,7 @@ class VideoNodes(object):
# Custom query
path = ("plugin://plugin.video.plexkodiconnect/?id=%s&mode=recentepisodes&type=%s&tagname=%s&limit=%s"
% (viewid, mediatype, tagname, limit))
elif kodiversion == 14 and nodetype == "inprogressepisodes":
elif self.kodiversion == 14 and nodetype == "inprogressepisodes":
# Custom query
path = "plugin://plugin.video.plexkodiconnect/?id=%s&mode=inprogressepisodes&limit=%s" % (tagname, limit)
elif nodetype == 'ondeck':
@ -245,10 +247,14 @@ class VideoNodes(object):
else:
path = "library://video/Plex-%s/%s_%s.xml" % (dirname, viewid, nodetype)
if mediatype == "photo":
if mediatype == "photos":
windowpath = "ActivateWindow(Pictures,%s,return)" % path
else:
windowpath = "ActivateWindow(Video,%s,return)" % path
if self.kodiversion >= 17:
# Krypton
windowpath = "ActivateWindow(Videos,%s,return)" % path
else:
windowpath = "ActivateWindow(Video,%s,return)" % path
if nodetype == "all":
@ -257,24 +263,24 @@ class VideoNodes(object):
else:
templabel = label
embynode = "Emby.nodes.%s" % indexnumber
embynode = "Plex.nodes.%s" % indexnumber
window('%s.title' % embynode, value=templabel)
window('%s.path' % embynode, value=windowpath)
window('%s.content' % embynode, value=path)
window('%s.type' % embynode, value=mediatype)
else:
embynode = "Emby.nodes.%s.%s" % (indexnumber, nodetype)
embynode = "Plex.nodes.%s.%s" % (indexnumber, nodetype)
window('%s.title' % embynode, value=label)
window('%s.path' % embynode, value=windowpath)
window('%s.content' % embynode, value=path)
if mediatype == "photo":
if mediatype == "photos":
# For photos, we do not create a node in videos but we do want the window props
# to be created.
# To do: add our photos nodes to kodi picture sources somehow
continue
if xbmcvfs.exists(nodeXML.encode('utf-8')):
if xbmcvfs.exists(utils.tryEncode(nodeXML)):
# Don't recreate xml if already exists
continue
@ -298,8 +304,12 @@ class VideoNodes(object):
elif nodetype == "recent":
etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded"
etree.SubElement(root, 'limit').text = limit
rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"})
etree.SubElement(rule, 'value').text = "0"
if utils.settings('MovieShowWatched') == 'false':
rule = etree.SubElement(root,
'rule',
{'field': "playcount",
'operator': "is"})
etree.SubElement(rule, 'value').text = "0"
elif nodetype == "inprogress":
etree.SubElement(root, 'rule', {'field': "inprogress", 'operator': "true"})
@ -358,19 +368,26 @@ class VideoNodes(object):
window = utils.window
tagname = tagname.encode('utf-8')
tagname = utils.tryEncode(tagname)
cleantagname = utils.normalize_nodes(tagname)
nodepath = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
nodepath = utils.tryDecode(xbmc.translatePath(
"special://profile/library/video/"))
nodeXML = "%splex_%s.xml" % (nodepath, cleantagname)
path = "library://video/plex_%s.xml" % cleantagname
windowpath = "ActivateWindow(Video,%s,return)" % path
if self.kodiversion >= 17:
# Krypton
windowpath = "ActivateWindow(Videos,%s,return)" % path
else:
windowpath = "ActivateWindow(Video,%s,return)" % path
# Create the video node directory
if not xbmcvfs.exists(nodepath):
# We need to copy over the default items
shutil.copytree(
src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'),
dst=xbmc.translatePath("special://profile/library/video").decode('utf-8'))
src=utils.tryDecode(xbmc.translatePath(
"special://xbmc/system/library/video")),
dst=utils.tryDecode(xbmc.translatePath(
"special://profile/library/video")))
xbmcvfs.exists(path)
labels = {
@ -380,7 +397,7 @@ class VideoNodes(object):
'channels': 30173
}
label = utils.language(labels[tagname])
embynode = "Emby.nodes.%s" % indexnumber
embynode = "Plex.nodes.%s" % indexnumber
window('%s.title' % embynode, value=label)
window('%s.path' % embynode, value=windowpath)
window('%s.content' % embynode, value=path)
@ -409,7 +426,7 @@ class VideoNodes(object):
window = utils.window
self.logMsg("Clearing nodes properties.", 1)
embyprops = window('Emby.nodes.total')
plexprops = window('Plex.nodes.total')
propnames = [
"index","path","title","content",
@ -424,8 +441,8 @@ class VideoNodes(object):
"inprogressepisodes.content","inprogressepisodes.path"
]
if embyprops:
totalnodes = int(embyprops)
if plexprops:
totalnodes = int(plexprops)
for i in range(totalnodes):
for prop in propnames:
window('Emby.nodes.%s.%s' % (str(i), prop), clear=True)
window('Plex.nodes.%s.%s' % (str(i), prop), clear=True)

File diff suppressed because it is too large Load diff

View file

@ -44,6 +44,7 @@ class WebSocket(threading.Thread):
if typus is None:
self.logMsg('No message type, dropping message: %s' % message, -1)
return False
self.logMsg('Received message from PMS server: %s' % message, 2)
# Drop everything we're not interested in
if typus not in ('playing', 'timeline'):
return True
@ -87,17 +88,16 @@ class WebSocket(threading.Thread):
uri = "%s/:/websockets/notifications" % server
if token:
uri += '?X-Plex-Token=%s' % token
return uri
def run(self):
log = self.logMsg
# Currently not working due to missing SSL environment
sslopt = {}
if utils.settings('sslverify') == "false":
sslopt["cert_reqs"] = ssl.CERT_NONE
return uri, sslopt
def run(self):
log = self.logMsg
log("----===## Starting WebSocketClient ##===----", 0)
counter = 0
threadStopped = self.threadStopped
threadSuspended = self.threadSuspended
while not threadStopped():
@ -122,7 +122,7 @@ class WebSocket(threading.Thread):
pass
except websocket.WebSocketConnectionClosedException:
log("Connection closed, (re)connecting", 0)
uri = self.getUri()
uri, sslopt = self.getUri()
try:
# Low timeout - let's us shut this thread down!
self.ws = websocket.create_connection(
@ -131,8 +131,15 @@ class WebSocket(threading.Thread):
sslopt=sslopt,
enable_multithread=True)
except IOError:
# Server is probably offline
log("Error connecting", 0)
self.ws = None
counter += 1
if counter > 10:
log("Repeatedly could not connect to PMS, declaring "
"the connection dead", -1)
utils.window('plex_online', value='false')
counter = 0
xbmc.sleep(1000)
except websocket.WebSocketTimeoutException:
log("timeout while connecting, trying again", 0)
@ -142,6 +149,8 @@ class WebSocket(threading.Thread):
log("Unknown exception encountered in connecting: %s" % e)
self.ws = None
xbmc.sleep(1000)
else:
counter = 0
except Exception as e:
log("Unknown exception encountered: %s" % e)
try:

View file

@ -1,37 +1,30 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<settings>
<category label="30014"><!-- Connection -->
<!-- Primary address -->
<setting label="39050" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=chooseServer)" option="close" /><!-- Choose Plex Server from a list -->
<setting id="ipaddress" label="30000" type="text" default="" />
<setting id="port" label="30030" type="number" default="32400" />
<setting label="39068" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=enterPMS)" option="close" /><!-- Manually enter Plex Media Server IP -->
<setting id="plex_servername" label="39067" type="text" default="" enable="false" /><!-- Your current PMS server: -->
<setting id="ipaddress" label="39069" type="text" default="" enable="false" /><!-- Current address: -->
<setting id="port" label="39070" type="text" default="" enable="false" /><!-- Current port: -->
<setting id="plex_serverowned" label="30031" type="bool" default="true" /><!-- I own this PMS -->
<setting id="https" label="30243" type="bool" default="false" />
<setting id="sslverify" subsetting="true" label="30500" type="bool" default="false" visible="eq(-1,true)" />
<setting id="sslcert" subsetting="true" label="30501" type="file" default="None" visible="eq(-2,true)" />
<!-- Secondary address -->
<setting id="altip" label="30502" type="bool" default="false" visible="false"/>
<setting id="secondipaddress" subsetting="true" label="30503" type="text" default="" visible="eq(-1,true)" />
<setting id="secondport" subsetting="true" label="30030" type="number" default="8096" visible="eq(-2,true)" />
<setting id="secondhttps" subsetting="true" label="30243" type="bool" default="false" visible="eq(-3,true)" />
<setting id="secondsslverify" subsetting="true" label="30500" type="bool" default="false" visible="eq(-1,true)" />
<setting id="secondsslcert" subsetting="true" label="30501" type="file" default="None" visible="eq(-2,true)" />
<!-- User settings -->
<setting type="sep" />
<setting type="sep" text=""/>
<setting id="enforceUserLogin" label="30536" type="bool" default="false" />
<setting label="30505" type="action" visible="eq(1,) + !eq(-15,)" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=resetauth)" option="close" />
<setting label="30517" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=passwords)" option="close" /><!-- Network credentials -->
<setting label="30505" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=resetauth)" option="close" /><!-- reset connection attempts -->
<setting id="accessToken" type="text" visible="false" default="" />
<setting id="pathsub" type="bool" visible="false" default="false" />
<setting label="39024" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=reConnect)" option="close" />
</category>
<category label="plex.tv"><!-- plex.tv -->
<!-- Primary address -->
<setting id="plex_status" label="39071" type="text" default="Not logged in to plex.tv" enable="false" /><!-- Current plex.tv status: -->
<setting type="sep" text=""/>
<setting id="myplexlogin" label="39025" type="bool" default="true" /> <!-- Log into plex.tv on startup -->
<setting label="39209" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=reConnect)" option="close" />
<setting label="39209" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=togglePlexTV)" option="close" />
<setting id="plexLogin" label="plex.tv username" type="text" default="" visible="false" />
<setting id="plexhome" label="Plex home in use" type="bool" default="" visible="false" />
@ -47,18 +40,19 @@
<setting id="limitindex" type="number" label="30515" default="200" option="int" /><!-- Maximum items to request from the server at once -->
<setting id="enableTextureCache" label="30512" type="bool" default="true" /> <!-- Force Artwork Caching -->
<setting id="imageCacheLimit" type="enum" label="30513" values="Disabled|5|10|15|20|25" default="5" visible="eq(-1,true)" subsetting="true" /> <!-- Limit artwork cache threads -->
<setting id="syncThreadNumber" type="slider" label="39003" default="5" option="int" range="1,1,20"/>
<setting id="serverSync" type="bool" label="30514" default="true" visible="false"/><!-- Enable fast startup (requires server plugin) -->
<setting type="lsep" label="39052" /><!-- Background Sync -->
<setting id="enableBackgroundSync" type="bool" label="39026" default="true" visible="true"/>
<setting id="saftyMargin" type="slider" label="39051" default="30" option="int" range="10,1,300" visible="eq(-1,true)"/>
<setting id="saftyMargin" type="slider" label="39051" default="60" option="int" range="10,1,300" visible="eq(-1,true)"/>
<setting id="fullSyncInterval" type="number" label="39053" default="60" option="int" />
<setting id="dbSyncScreensaver" type="bool" label="39062" default="false" /><!--Sync when screensaver is deactivated-->
<setting type="lsep" label="30538" /><!-- Complete Re-Sync necessary -->
<setting id="FanartTV" label="30539" type="bool" default="false" /><!-- Download additional art from FanArtTV -->
<setting id="setFanartTV" label="30540" type="bool" default="true" /><!-- Download movie set/collection art from FanArtTV -->
<setting id="enableMusic" type="bool" label="30509" default="true" />
<setting id="useDirectPaths" type="enum" label="30511" values="Addon(Default)|Native(Direct paths)" default="0" visible="true"/> <!-- Playback mode -->
<setting id="streamMusic" type="bool" label="30510" default="false" visible="false" subsetting="true"/> <!-- Direct stream Music library -->
<setting type="lsep" label="30523" visible="false"/> <!-- Music metadata options -->
@ -66,7 +60,9 @@
<setting id="enableExportSongRating" type="bool" label="30525" default="false" visible="false" />
<setting id="kodiplextimeoffset" type="number" label="Time difference in seconds (Koditime - Plextime)" default="0" visible="false" option="int" />
<setting id="enableUpdateSongRating" type="bool" label="30526" default="false" visible="false" />
<setting id="emby_pathverified" type="bool" default="false" visible="false" /> <!-- If 'false': one single warning message pops up if PKC cannot verify direct paths -->
<setting id="plex_pathverified" type="bool" default="false" visible="false" /> <!-- If 'false': one single warning message pops up if PKC cannot verify direct paths -->
<setting id="themoviedbAPIKey" type="text" default="ae06df54334aa653354e9a010f4b81cb" visible="false"/>
<setting id="FanArtTVAPIKey" type="text" default="639191cb0774661597f28a47e7e2bad5" visible="false"/>
</category>
<category label="39057"><!-- Customize Paths -->
@ -79,10 +75,14 @@
<setting id="remapSMBtvNew" type="text" label="39040" default="smb://" visible="eq(-4,true)"/> <!-- Replace Plex TV SHOWS with: -->
<setting id="remapSMBmusicOrg" type="text" label="39041" default="" visible="eq(-5,true)"/> <!-- Original Plex MUSIC path to replace: -->
<setting id="remapSMBmusicNew" type="text" label="39042" default="smb://" visible="eq(-6,true)"/> <!-- Replace Plex MUSIC with: -->
<setting id="remapSMBphotoOrg" type="text" label="39045" default="" visible="eq(-7,true)"/> <!-- Original Plex MUSIC path to replace: -->
<setting id="remapSMBphotoNew" type="text" label="39046" default="smb://" visible="eq(-8,true)"/> <!-- Replace Plex MUSIC with: -->
</category>
<category label="30516"><!-- Playback -->
<setting type="sep" />
<setting id="bestQuality" type="bool" label="30541" default="false" />
<setting id="bestTrailer" type="bool" label="30542" default="true" />
<setting id="enableCinema" type="bool" label="30518" default="true" />
<setting id="askCinema" type="bool" label="30519" default="false" visible="eq(-1,true)" subsetting="true" />
<setting id="trailerNumber" type="slider" label="39000" default="3" visible="eq(-2,true)" range="1,1,15" option="int" />
@ -93,11 +93,12 @@
<setting id="resumeJumpBack" type="slider" label="30521" default="10" range="0,1,120" option="int" />
<setting type="sep" />
<setting id="playType" type="enum" label="30002" values="Direct Play (default)|Direct Stream|Force Transcode" default="0" />
<setting id="transcoderVideoQualities" type="enum" label="30160" values="420x420, 320Kbps|576x320, 720Kbps|720x480, 1.5Mbps|1024x768, 2Mbps|1280x720, 3Mbps|1280x720, 4Mbps|1920x1080, 8Mbps|1920x1080, 10Mbps|1920x1080, 12Mbps|1920x1080, 20Mbps|1920x1080, 40Mbps" default="11" />
<setting id="transcoderVideoQualities" type="enum" label="30160" values="420x420, 320Kbps|576x320, 720Kbps|720x480, 1.5Mbps|1024x768, 2Mbps|1280x720, 3Mbps|1280x720, 4Mbps|1920x1080, 8Mbps|1920x1080, 10Mbps|1920x1080, 12Mbps|1920x1080, 20Mbps|1920x1080, 40Mbps" default="10" />
<setting id="transcodeH265" type="enum" label="30522" default="0" values="Disabled (default)|480p (and higher)|720p (and higher)|1080p" />
<setting id="transcodeHi10P" type="bool" label="39063" default="false"/>
<setting id="transcodeHEVC" type="bool" label="39065" default="false"/>
<setting id="audioBoost" type="slider" label="39001" default="0" range="0,10,100" option="int"/>
<setting id="subtitleSize" label="39002" type="slider" option="int" range="0,30,300" default="100" />
<setting id="markPlayed" type="number" visible="false" default="95" />
<setting id="failedCount" type="number" visible="false" default="0" />
<setting id="networkCreds" type="text" visible="false" default="" />
</category>
@ -125,12 +126,17 @@
</category>
<category label="39045"><!-- Appearance Tweaks -->
<category label="39073"><!-- Appearance Tweaks -->
<setting id="connectMsg" type="bool" label="30249" default="true" />
<setting type="lsep" label="39046" />
<setting type="lsep" label="39074" /><!-- TV Shows -->
<setting id="OnDeckTVextended" type="bool" label="39058" default="true" /><!-- Extend Plex TV Series "On Deck" view to all shows -->
<setting id="OnDeckTvAppendShow" type="bool" label="39047" default="false" /><!--On Deck view: Append show title to episode-->
<setting id="OnDeckTvAppendSeason" type="bool" label="39048" default="false" /><!--On Deck view: Append season number to episode-->
<setting id="TVShowWatched" type="bool" label="39064" default="true" /><!--Recently Added: Also show already watched episodes-->
<setting id="RecentTvAppendShow" type="bool" label="39059" default="false" /><!--Recently added: Append show title to episode-->
<setting id="RecentTvAppendSeason" type="bool" label="39060" default="false" /><!--Recently Added: Append season- and episode-number SxxExx-->
<setting type="lsep" label="30302" /><!-- Movies -->
<setting id="MovieShowWatched" type="bool" label="39066" default="true" /><!--Recently Added: Also show already watched episodes-->
</category>
<category label="30022"><!-- Advanced -->

View file

@ -4,7 +4,6 @@
import os
import sys
from datetime import datetime
import Queue
import xbmc
@ -14,19 +13,31 @@ import xbmcgui
###############################################################################
_addon = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
addon_path = _addon.getAddonInfo('path').decode('utf-8')
base_resource = xbmc.translatePath(os.path.join(addon_path, 'resources', 'lib')).decode('utf-8')
try:
addon_path = _addon.getAddonInfo('path').decode('utf-8')
except TypeError:
addon_path = _addon.getAddonInfo('path').decode()
try:
base_resource = xbmc.translatePath(os.path.join(
addon_path,
'resources',
'lib')).decode('utf-8')
except TypeError:
base_resource = xbmc.translatePath(os.path.join(
addon_path,
'resources',
'lib')).decode()
sys.path.append(base_resource)
###############################################################################
import utils
import userclient
import clientinfo
import initialsetup
import kodimonitor
import librarysync
import player
import utils
import videonodes
import websocket_client as wsc
import downloadutils
@ -59,11 +70,9 @@ class Service():
logLevel = self.getLogLevel()
self.monitor = xbmc.Monitor()
window('emby_logLevel', value=str(logLevel))
window('emby_kodiProfile', value=xbmc.translatePath("special://profile"))
window('emby_pluginpath', value=utils.settings('useDirectPaths'))
self.runPlexCompanion = utils.settings('plexCompanion')
window('plex_logLevel', value=str(logLevel))
window('plex_kodiProfile', value=xbmc.translatePath("special://profile"))
window('plex_pluginpath', value=utils.settings('useDirectPaths'))
# Initial logging
log("======== START %s ========" % self.addonName, 0)
@ -76,16 +85,17 @@ class Service():
# Reset window props for profile switch
properties = [
"emby_online", "emby_serverStatus", "emby_onWake",
"emby_syncRunning", "emby_dbCheck", "emby_kodiScan",
"emby_shouldStop", "currUserId", "emby_dbScan", "emby_sessionId",
"emby_initialScan", "emby_customplaylist", "emby_playbackProps",
"plex_online", "plex_serverStatus", "plex_onWake",
"plex_dbCheck", "plex_kodiScan",
"plex_shouldStop", "currUserId", "plex_dbScan",
"plex_initialScan", "plex_customplaylist", "plex_playbackProps",
"plex_runLibScan", "plex_username", "pms_token", "plex_token",
"pms_server", "plex_machineIdentifier", "plex_servername",
"plex_authenticated", "PlexUserImage", "useDirectPaths",
"replaceSMB", "remapSMB", "remapSMBmovieOrg", "remapSMBtvOrg",
"remapSMBmusicOrg", "remapSMBmovieNew", "remapSMBtvNew",
"remapSMBmusicNew", "suspend_LibraryThread", "plex_terminateNow",
"remapSMBmusicNew", "remapSMBphotoOrg", "remapSMBphotoNew",
"suspend_LibraryThread", "plex_terminateNow",
"kodiplextimeoffset", "countError", "countUnauthorized"
]
for prop in properties:
@ -93,9 +103,9 @@ class Service():
# Clear video nodes properties
videonodes.VideoNodes().clearProperties()
# Set the minimum database version
window('emby_minDBVersion', value="1.1.4")
window('plex_minDBVersion', value="1.1.5")
def getLogLevel(self):
try:
@ -127,61 +137,28 @@ class Service():
user = userclient.UserClient()
ws = wsc.WebSocket(queue)
library = librarysync.LibrarySync(queue)
kplayer = player.Player()
xplayer = xbmc.Player()
plx = PlexAPI.PlexAPI()
# Sync and progress report
lastProgressUpdate = datetime.today()
counter = 0
while not monitor.abortRequested():
if window('emby_kodiProfile') != kodiProfile:
if window('plex_kodiProfile') != kodiProfile:
# Profile change happened, terminate this thread and others
log("Kodi profile was: %s and changed to: %s. Terminating old Emby thread."
% (kodiProfile, utils.window('emby_kodiProfile')), 1)
log("Kodi profile was: %s and changed to: %s. Terminating old "
"PlexKodiConnect thread."
% (kodiProfile, utils.window('plex_kodiProfile')), 1)
break
# Before proceeding, need to make sure:
# 1. Server is online
# 2. User is set
# 3. User has access to the server
if window('emby_online') == "true":
# Emby server is online
if window('plex_online') == "true":
# Plex server is online
# Verify if user is set and has access to the server
if (user.currUser is not None) and user.HasAccess:
# If an item is playing
if xplayer.isPlaying():
try:
# Update and report progress
playtime = xplayer.getTime()
totalTime = xplayer.getTotalTime()
currentFile = kplayer.currentFile
# Update positionticks
if kplayer.played_info.get(currentFile) is not None:
kplayer.played_info[currentFile]['currentPosition'] = playtime
td = datetime.today() - lastProgressUpdate
secDiff = td.seconds
# Report progress to Emby server
if (secDiff > 3):
kplayer.reportPlayback()
lastProgressUpdate = datetime.today()
elif window('emby_command') == "true":
# Received a remote control command that
# requires updating immediately
window('emby_command', clear=True)
kplayer.reportPlayback()
lastProgressUpdate = datetime.today()
except Exception as e:
log("Exception in Playback Monitor Service: %s" % e, 1)
pass
else:
if not self.kodimonitor_running:
# Start up events
self.warn_auth = True
if connectMsg and self.welcome_msg:
@ -194,8 +171,7 @@ class Service():
time=2000,
sound=False)
# Start monitoring kodi events
if not self.kodimonitor_running:
self.kodimonitor_running = kodimonitor.KodiMonitor()
self.kodimonitor_running = kodimonitor.KodiMonitor()
# Start the Websocket Client
if not self.websocket_running:
@ -206,8 +182,7 @@ class Service():
self.library_running = True
library.start()
# Start the Plex Companion thread
if not self.plexCompanion_running and \
self.runPlexCompanion == "true":
if not self.plexCompanion_running:
self.plexCompanion_running = True
plexCompanion = PlexCompanion.PlexCompanion()
plexCompanion.start()
@ -224,7 +199,7 @@ class Service():
# Verify access with an API call
user.hasAccess()
if window('emby_online') != "true":
if window('plex_online') != "true":
# Server went offline
break
@ -233,7 +208,7 @@ class Service():
break
xbmc.sleep(50)
else:
# Wait until Emby server is online
# Wait until Plex server is online
# or Kodi is shut down.
while not monitor.abortRequested():
server = user.getServer()
@ -244,11 +219,10 @@ class Service():
# Server is offline or cannot be reached
# Alert the user and suppress future warning
if self.server_online:
log("Server is offline.", 1)
window('emby_online', value="false")
log("Server is offline.", -1)
window('plex_online', value="false")
# Suspend threads
window('suspend_LibraryThread', value='true')
xbmcgui.Dialog().notification(
heading=lang(33001),
message="%s %s"
@ -257,8 +231,17 @@ class Service():
"plexkodiconnect/icon.png",
sound=False)
self.server_online = False
counter += 1
# Periodically check if the IP changed, e.g. per minute
if counter > 30:
counter = 0
setup = initialsetup.InitialSetup()
tmp = setup.PickPMS()
if tmp is not None:
setup.WritePMStoSettings(tmp)
else:
# Server is online
counter = 0
if not self.server_online:
# Server was offline when Kodi started.
# Wait for server to be fully established.
@ -271,11 +254,11 @@ class Service():
message=lang(33003),
icon="special://home/addons/plugin.video."
"plexkodiconnect/icon.png",
time=2000,
time=5000,
sound=False)
self.server_online = True
log("Server %s is online and ready." % server, 1)
window('emby_online', value="true")
window('plex_online', value="true")
if window('plex_authenticated') == 'true':
# Server got offline when we were authenticated.
# Hence resume threads
@ -288,12 +271,11 @@ class Service():
break
if monitor.waitForAbort(1):
if monitor.waitForAbort(2):
# Abort was requested while waiting.
break
xbmc.sleep(50)
if monitor.waitForAbort(1):
if monitor.waitForAbort(0.05):
# Abort was requested while waiting. We should exit
break
@ -335,6 +317,6 @@ delay = int(utils.settings('startupDelay'))
xbmc.log("Delaying Plex startup by: %s sec..." % delay)
if delay and xbmc.Monitor().waitForAbort(delay):
# Start the service
xbmc.log("Abort requested while waiting. Emby for kodi not started.")
xbmc.log("Abort requested while waiting. PKC not started.")
else:
Service().ServiceEntryPoint()