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. 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.
2. **Compatibility**: PKC is currently not compatible with Kodi's Video Extras plugin. **Deactivate Video Extras** if trailers/movies start randomly playing.
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) ### Checkout the PKC Wiki
[The Wiki will hopefully answer all your questions](https://github.com/croneter/PlexKodiConnect/wiki) The [Wiki can be found here](https://github.com/croneter/PlexKodiConnect/wiki) and will hopefully answer all your questions.
### 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.
**What does it do?** ### What does PKC do?
With other addons for Kodi there are a couple of issues: With other addons for Kodi there are a couple of issues:
- 3rd party addons such as NextAired, remote apps etc. won't work - 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 - You can only use special Kodi skins
- All kinds of workarounds are needed to get the best experience on Kodi clients - 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! 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, e.g. images are cached - 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 - All other Kodi addons will be able to "see" your media, thinking it's normal Kodi stuff
- Use any Kodi skin you want! - 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) PKC currently provides the following features:
- All Plex library types
+ Movies and Home Videos
**What is currently supported?** + TV Shows
+ Music
Currently these features are working: + Pictures and Photos
- Movies and Home Videos - Different PKC interface languages:
- TV Shows + English
+ German
+ More coming up
- [Plex Watch Later / Plex It!](https://support.plex.tv/hc/en-us/sections/200211783-Plex-It-) - [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 - [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) - [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 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 - *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 - 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/tell what language they are in. However, this is not the case if you use direct paths - 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
- 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
*Background Sync:* *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 - Toggle the viewstate of an item to (un)watched outside of Kodi
- Changing details of an item, e.g. replacing a poster - 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. However, some changes to individual items are instantly detected, e.g. if you match a yet unrecognized movie.
**Known Bugs:** ### Issues being worked on
- Resume a video does not work yet with Plex Watch Later
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 - Playlists
- Pictures
- Music Videos - Music Videos
- Automatic updates - Deleting PMS items from Kodi
- Simultaneously connecting to several PMS
- TV Shows Theme Music (ultra-low prio) - 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. 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!! - 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). - 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"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.plexkodiconnect" <addon id="plugin.video.plexkodiconnect"
name="PlexKodiConnect" name="PlexKodiConnect"
version="1.1.4" version="1.3.0"
provider-name="croneter"> provider-name="croneter">
<requires> <requires>
<import addon="xbmc.python" version="2.1.0"/> <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 version 1.1.4
(you will need to rescan your library) (you will need to rescan your library)
- Plex Watch Later available as a separate Video Node! - Plex Watch Later available as a separate Video Node!

View file

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

View file

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

View file

@ -2,7 +2,7 @@
<strings> <strings>
<!-- Add-on settings --> <!-- Add-on settings -->
<string id="30000">Server Address (IP)</string><!-- Verified --> <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="30004">Log level</string><!-- Verified -->
<string id="30005">Username: </string> <string id="30005">Username: </string>
<string id="30006">Password: </string> <string id="30006">Password: </string>
@ -131,7 +131,7 @@
<string id="30157">Enable Enhanced Images (eg CoverArt)</string><!-- Verified --> <string id="30157">Enable Enhanced Images (eg CoverArt)</string><!-- Verified -->
<string id="30158">Metadata</string> <string id="30158">Metadata</string>
<string id="30159">Artwork</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="30161">Enable Suggested Loader (Requires Restart)</string>
<string id="30162">Add Season Number</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="30536">Users must log in every time Kodi restarts</string>
<string id="30537">RESTART KODI IF YOU MAKE ANY CHANGES</string> <string id="30537">RESTART KODI IF YOU MAKE ANY CHANGES</string>
<string id="30538">Complete Re-Sync necessary</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 --> <!-- service add-on -->
@ -328,7 +332,7 @@
<string id="33020">Gathering tv shows from:</string> <string id="33020">Gathering tv shows from:</string>
<string id="33021">Gathering:</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="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="33024">Cancelling the database syncing process. The current Kodi version is unsupported.</string>
<string id="33025">completed in:</string> <string id="33025">completed in:</string>
<string id="33026">Comparing movies from:</string> <string id="33026">Comparing movies from:</string>
@ -338,7 +342,7 @@
<string id="33030">Comparing episodes from:</string> <string id="33030">Comparing episodes from:</string>
<string id="33031">Comparing:</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="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 --> <!-- New to Plex -->
<string id="39000">- Number of trailers to play before a movie</string> <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="39021">[COLOR yellow]Sync Emby Theme Media to Kodi[/COLOR]</string>
<string id="39022">local</string> <string id="39022">local</string>
<string id="39023">Failed to authenticate. Did you login to plex.tv?</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="39025">Automatically log into plex.tv on startup</string>
<string id="39026">Enable constant background sync</string> <string id="39026">Enable constant background sync</string>
<string id="39027">Playback Mode</string> <string id="39027">Playback Mode</string>
@ -387,11 +392,10 @@
<string id="39042">Replace Plex MUSIC with:</string> <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="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="39044">Please enter your custom smb paths in the settings under "Sync Options" and then restart Kodi</string>
<string id="39045">Original Plex PHOTO path to replace:</string>
<string id="39045">Appearance Tweaks</string> <string id="39046">Replace Plex PHOTO with:</string>
<string id="39046">TV Shows</string>
<string id="39047">On Deck: Append show title to episode</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="39049">Nothing works? Try a full reset!</string>
<string id="39050">[COLOR yellow]Choose Plex Server from a list[/COLOR]</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> <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="39056">Used by Sync and when attempting to Direct Play</string>
<string id="39057">Customize Paths</string> <string id="39057">Customize Paths</string>
<string id="39058">Extend Plex TV Series "On Deck" view to all shows</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 --> <!-- Plex Entrypoint.py -->
<string id="39200">Log-out Plex Home User </string> <string id="39200">Log-out Plex Home User </string>
@ -411,11 +431,20 @@
<string id="39204">Perform manual library sync</string> <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="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="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="39207">Resetting PMS connections, please wait</string>
<string id="39208">Failed to reset PMS and plex.tv connects. Try to restart Kodi.</string> <string id="39208">Failed to reset PKC. Try to restart Kodi.</string>
<string id="39209">[COLOR yellow]Log-in to plex.tv[/COLOR]</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="39210">Not yet connected to Plex Server</string>
<string id="39211">Watch later</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 --> <!-- 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="30536">Benutzer müssen sich bei jedem Neustart von Kodi neu anmelden</string>
<string id="30537">BEI ÄNDERUNGEN KODI NEU STARTEN</string> <string id="30537">BEI ÄNDERUNGEN KODI NEU STARTEN</string>
<string id="30538">Komplette Neusynchronisierung nötig</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="30014">Verbindung</string>
<string id="30015">Netzwerk</string> <string id="30015">Netzwerk</string>
@ -164,7 +166,7 @@
<string id="30157">Deaktiviere erweiterte Bilder (z.B. CoverArt)</string> <string id="30157">Deaktiviere erweiterte Bilder (z.B. CoverArt)</string>
<string id="30158">Metadaten</string> <string id="30158">Metadaten</string>
<string id="30159">Grafiken</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="30161">'Empfohlen'-Loader aktivieren (Erfordert Neustart)</string>
<string id="30162">Staffelnummer hinzufügen</string> <string id="30162">Staffelnummer hinzufügen</string>
@ -278,6 +280,8 @@
<string id="30311">Musikstücke</string> <string id="30311">Musikstücke</string>
<string id="30312">Kanäle</string> <string id="30312">Kanäle</string>
<string id="33033">Kodi wird jetzt neu gestartet um die Änderungen anzuwenden.</string>
<!-- New to Plex --> <!-- New to Plex -->
<string id="39000">- Anzahl abzuspielender Trailer vor einem Film</string> <string id="39000">- Anzahl abzuspielender Trailer vor einem Film</string>
<string id="39001">Audio Verstärkung (audio boost) wenn transkodiert wird</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="39021">[COLOR yellow]Plex Themes zu Kodi synchronisieren[/COLOR]</string>
<string id="39022">lokal</string> <string id="39022">lokal</string>
<string id="39023">Plex Media Server Authentifizierung fehlgeschlagen. Haben Sie sich bei plex.tv eingeloggt?</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="39025">Automatisch beim Starten bei plex.tv einloggen</string>
<string id="39026">Laufende Synchronisierung im Hintergrund aktivieren</string> <string id="39026">Laufende Synchronisierung im Hintergrund aktivieren</string>
<string id="39027">Playback Modus</string> <string id="39027">Playback Modus</string>
@ -325,11 +330,10 @@
<string id="39042">Plex MUSIK Pfade ersetzen durch:</string> <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="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="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">Ursprünglicher Plex Pfad für PHOTOS:</string>
<string id="39045">Erscheinung</string> <string id="39046">Plex PHOTO Pfade ersetzen durch:</string>
<string id="39046">TV Serien</string>
<string id="39047">"Aktuell": Serien- an Episoden-Titel anfügen</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="39049">Nichts funktioniert? Setze mal alles zurück!</string>
<string id="39050">[COLOR yellow]Plex Server aus Liste auswählen[/COLOR]</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> <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="39056">Verwendet für Synchronisierung sowie beim Versuch, Direct Play zu nutzen</string>
<string id="39057">Pfade ändern</string> <string id="39057">Pfade ändern</string>
<string id="39058">Standard Plex Ansicht "Aktuell" auf alle TV Shows erweitern</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 --> <!-- Plex Entrypoint.py -->
<string id="39200">Plex Home Benutzer abmelden: </string> <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="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="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="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="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]Bei plex.tv einloggen[/COLOR]</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="39210">Noch nicht mit Plex Server verbunden</string>
<string id="39211">Später ansehen</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 --> <!-- Plex Artwork.py -->
<string id="39250">Alle Plex Bilder in Kodi zwischenzuspeichern kann sehr lange dauern. Möchten Sie wirklich fortfahren?</string> <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 threading
import traceback import traceback
import socket import socket
import Queue
import xbmc import xbmc
import utils import utils
from plexbmchelper import listener, plexgdm, subscribers, functions, \ from plexbmchelper import listener, plexgdm, subscribers, functions, \
httppersist, settings httppersist, settings
from PlexFunctions import ParseContainerKey, GetPlayQueue, \
ConvertPlexToKodiTime
import playlist
import player
@utils.logging @utils.logging
@utils.ThreadMethodsAdditionalSuspend('emby_serverStatus') @utils.ThreadMethodsAdditionalSuspend('plex_serverStatus')
@utils.ThreadMethods @utils.ThreadMethods
class PlexCompanion(threading.Thread): class PlexCompanion(threading.Thread):
"""
Initialize with a Queue for callbacks
"""
def __init__(self): def __init__(self):
self.logMsg("----===## Starting PlexCompanion ##===----", 1) self.logMsg("----===## Starting PlexCompanion ##===----", 1)
self.settings = settings.getSettings() self.settings = settings.getSettings()
@ -22,55 +30,150 @@ class PlexCompanion(threading.Thread):
self.client = plexgdm.plexgdm() self.client = plexgdm.plexgdm()
self.client.clientDetails(self.settings) self.client.clientDetails(self.settings)
self.logMsg("Registration string is: %s " 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) 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): def run(self):
httpd = False
# Cache for quicker while loops # Cache for quicker while loops
log = self.logMsg log = self.logMsg
client = self.client client = self.client
threadStopped = self.threadStopped threadStopped = self.threadStopped
threadSuspended = self.threadSuspended threadSuspended = self.threadSuspended
start_count = 0
# Start up instances # Start up instances
requestMgr = httppersist.RequestMgr() requestMgr = httppersist.RequestMgr()
jsonClass = functions.jsonClass(requestMgr, self.settings) jsonClass = functions.jsonClass(requestMgr, self.settings)
subscriptionManager = subscribers.SubscriptionManager( subscriptionManager = subscribers.SubscriptionManager(
jsonClass, requestMgr) jsonClass, requestMgr, self.player, self.playlist)
# Start up httpd queue = Queue.Queue(maxsize=100)
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)
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: xbmc.sleep(3000)
log("Error: Unable to start web helper.", -1)
httpd = False
break
start_count += 1 if start_count == 3:
log("Error: Unable to start web helper.", -1)
httpd = False
break
if not httpd: start_count += 1
return else:
self.logMsg('User deactivated Plex Companion', 0)
client.start_all() client.start_all()
message_count = 0 message_count = 0
if httpd:
t = threading.Thread(target=httpd.handle_request)
while not threadStopped(): while not threadStopped():
# If we are not authorized, sleep # If we are not authorized, sleep
# Otherwise, we trigger a download which leads to a # Otherwise, we trigger a download which leads to a
@ -80,34 +183,50 @@ class PlexCompanion(threading.Thread):
break break
xbmc.sleep(1000) xbmc.sleep(1000)
try: try:
httpd.handle_request()
message_count += 1 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 message_count == 3000:
if client.check_client_registration(): message_count = 0
log("Client is still registered", 1) if client.check_client_registration():
else: log("Client is still registered", 1)
log("Client is no longer registered", 1) else:
log("Plex Companion still running on port %s" log("Client is no longer registered", 1)
% self.settings['myport'], 1) log("Plex Companion still running on port %s"
message_count = 0 % self.settings['myport'], 1)
# Get and set servers # Get and set servers
subscriptionManager.serverlist = client.getServerList() if message_count % 30 == 0:
subscriptionManager.serverlist = client.getServerList()
subscriptionManager.notify() subscriptionManager.notify()
xbmc.sleep(50) if not httpd:
message_count = 0
except: except:
log("Error in loop, continuing anyway", 0) log("Error in loop, continuing anyway. Traceback:", 1)
log(traceback.format_exc(), 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() client.stop_all()
try: if httpd:
httpd.socket.shutdown(socket.SHUT_RDWR) try:
except: httpd.socket.shutdown(socket.SHUT_RDWR)
pass except:
finally: pass
httpd.socket.close() finally:
httpd.socket.close()
log("----===## Plex Companion stopped ##===----", 0) log("----===## Plex Companion stopped ##===----", 0)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -34,7 +34,6 @@ class Embydb_Functions():
def getViews(self): def getViews(self):
embycursor = self.embycursor
views = [] views = []
query = ' '.join(( query = ' '.join((
@ -42,8 +41,8 @@ class Embydb_Functions():
"SELECT view_id", "SELECT view_id",
"FROM view" "FROM view"
)) ))
embycursor.execute(query) self.embycursor.execute(query)
rows = embycursor.fetchall() rows = self.embycursor.fetchall()
for row in rows: for row in rows:
views.append(row[0]) views.append(row[0])
return views return views
@ -68,7 +67,6 @@ class Embydb_Functions():
def getView_byId(self, viewid): def getView_byId(self, viewid):
embycursor = self.embycursor
query = ' '.join(( query = ' '.join((
@ -76,13 +74,13 @@ class Embydb_Functions():
"FROM view", "FROM view",
"WHERE view_id = ?" "WHERE view_id = ?"
)) ))
embycursor.execute(query, (viewid,)) self.embycursor.execute(query, (viewid,))
view = embycursor.fetchone() view = self.embycursor.fetchone()
return view return view
def getView_byType(self, mediatype): def getView_byType(self, mediatype):
embycursor = self.embycursor
views = [] views = []
query = ' '.join(( query = ' '.join((
@ -91,8 +89,8 @@ class Embydb_Functions():
"FROM view", "FROM view",
"WHERE media_type = ?" "WHERE media_type = ?"
)) ))
embycursor.execute(query, (mediatype,)) self.embycursor.execute(query, (mediatype,))
rows = embycursor.fetchall() rows = self.embycursor.fetchall()
for row in rows: for row in rows:
views.append({ views.append({
@ -105,23 +103,22 @@ class Embydb_Functions():
def getView_byName(self, tagname): def getView_byName(self, tagname):
embycursor = self.embycursor
query = ' '.join(( query = ' '.join((
"SELECT view_id", "SELECT view_id",
"FROM view", "FROM view",
"WHERE view_name = ?" "WHERE view_name = ?"
)) ))
embycursor.execute(query, (tagname,)) self.embycursor.execute(query, (tagname,))
try: try:
view = embycursor.fetchone()[0] view = self.embycursor.fetchone()[0]
except TypeError: except TypeError:
view = None view = None
return view return view
def addView(self, embyid, name, mediatype, tagid): def addView(self, plexid, name, mediatype, tagid):
query = ( query = (
''' '''
@ -131,7 +128,7 @@ class Embydb_Functions():
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?)
''' '''
) )
self.embycursor.execute(query, (embyid, name, mediatype, tagid)) self.embycursor.execute(query, (plexid, name, mediatype, tagid))
def updateView(self, name, tagid, mediafolderid): def updateView(self, name, tagid, mediafolderid):
@ -188,9 +185,7 @@ class Embydb_Functions():
except: except:
return None return None
def getItem_byId(self, embyid): def getItem_byId(self, plexid):
embycursor = self.embycursor
query = ' '.join(( query = ' '.join((
@ -199,14 +194,12 @@ class Embydb_Functions():
"WHERE emby_id = ?" "WHERE emby_id = ?"
)) ))
try: try:
embycursor.execute(query, (embyid,)) self.embycursor.execute(query, (plexid,))
item = embycursor.fetchone() item = self.embycursor.fetchone()
return item return item
except: return None except: return None
def getItem_byWildId(self, embyid): def getItem_byWildId(self, plexid):
embycursor = self.embycursor
query = ' '.join(( query = ' '.join((
@ -214,25 +207,19 @@ class Embydb_Functions():
"FROM emby", "FROM emby",
"WHERE emby_id LIKE ?" "WHERE emby_id LIKE ?"
)) ))
embycursor.execute(query, (embyid+"%",)) self.embycursor.execute(query, (plexid+"%",))
items = embycursor.fetchall() return self.embycursor.fetchall()
return items
def getItem_byView(self, mediafolderid): def getItem_byView(self, mediafolderid):
embycursor = self.embycursor
query = ' '.join(( query = ' '.join((
"SELECT kodi_id", "SELECT kodi_id",
"FROM emby", "FROM emby",
"WHERE media_folder = ?" "WHERE media_folder = ?"
)) ))
embycursor.execute(query, (mediafolderid,)) self.embycursor.execute(query, (mediafolderid,))
items = embycursor.fetchall() return self.embycursor.fetchall()
return items
def getPlexId(self, kodiid, mediatype): def getPlexId(self, kodiid, mediatype):
""" """
@ -253,8 +240,6 @@ class Embydb_Functions():
def getItem_byKodiId(self, kodiid, mediatype): def getItem_byKodiId(self, kodiid, mediatype):
embycursor = self.embycursor
query = ' '.join(( query = ' '.join((
"SELECT emby_id, parent_id", "SELECT emby_id, parent_id",
@ -262,15 +247,11 @@ class Embydb_Functions():
"WHERE kodi_id = ?", "WHERE kodi_id = ?",
"AND media_type = ?" "AND media_type = ?"
)) ))
embycursor.execute(query, (kodiid, mediatype,)) self.embycursor.execute(query, (kodiid, mediatype,))
item = embycursor.fetchone() return self.embycursor.fetchone()
return item
def getItem_byParentId(self, parentid, mediatype): def getItem_byParentId(self, parentid, mediatype):
embycursor = self.embycursor
query = ' '.join(( query = ' '.join((
"SELECT emby_id, kodi_id, kodi_fileid", "SELECT emby_id, kodi_id, kodi_fileid",
@ -278,15 +259,11 @@ class Embydb_Functions():
"WHERE parent_id = ?", "WHERE parent_id = ?",
"AND media_type = ?" "AND media_type = ?"
)) ))
embycursor.execute(query, (parentid, mediatype,)) self.embycursor.execute(query, (parentid, mediatype,))
items = embycursor.fetchall() return self.embycursor.fetchall()
return items
def getItemId_byParentId(self, parentid, mediatype): def getItemId_byParentId(self, parentid, mediatype):
embycursor = self.embycursor
query = ' '.join(( query = ' '.join((
"SELECT emby_id, kodi_id", "SELECT emby_id, kodi_id",
@ -294,29 +271,21 @@ class Embydb_Functions():
"WHERE parent_id = ?", "WHERE parent_id = ?",
"AND media_type = ?" "AND media_type = ?"
)) ))
embycursor.execute(query, (parentid, mediatype,)) self.embycursor.execute(query, (parentid, mediatype,))
items = embycursor.fetchall() return self.embycursor.fetchall()
return items
def getChecksum(self, mediatype): def getChecksum(self, mediatype):
embycursor = self.embycursor
query = ' '.join(( query = ' '.join((
"SELECT emby_id, checksum", "SELECT emby_id, checksum",
"FROM emby", "FROM emby",
"WHERE emby_type = ?" "WHERE emby_type = ?"
)) ))
embycursor.execute(query, (mediatype,)) self.embycursor.execute(query, (mediatype,))
items = embycursor.fetchall() return self.embycursor.fetchall()
return items def getMediaType_byId(self, plexid):
def getMediaType_byId(self, embyid):
embycursor = self.embycursor
query = ' '.join(( query = ' '.join((
@ -324,9 +293,10 @@ class Embydb_Functions():
"FROM emby", "FROM emby",
"WHERE emby_id = ?" "WHERE emby_id = ?"
)) ))
embycursor.execute(query, (embyid,)) self.embycursor.execute(query, (plexid,))
try: try:
itemtype = embycursor.fetchone()[0] itemtype = self.embycursor.fetchone()[0]
except TypeError: except TypeError:
itemtype = None itemtype = None
@ -345,7 +315,7 @@ class Embydb_Functions():
return sorted_items 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): parentid=None, checksum=None, mediafolderid=None):
query = ( query = (
''' '''
@ -356,18 +326,18 @@ class Embydb_Functions():
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
''' '''
) )
self.embycursor.execute(query, (embyid, kodiid, fileid, pathid, embytype, mediatype, self.embycursor.execute(query, (plexid, kodiid, fileid, pathid, embytype, mediatype,
parentid, checksum, mediafolderid)) parentid, checksum, mediafolderid))
def updateReference(self, embyid, checksum): def updateReference(self, plexid, checksum):
query = "UPDATE emby SET checksum = ? WHERE emby_id = ?" 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 = ?" 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): def removeItems_byParentId(self, parent_kodiid, mediatype):
@ -389,13 +359,13 @@ class Embydb_Functions():
)) ))
self.embycursor.execute(query, (kodiid, mediatype,)) self.embycursor.execute(query, (kodiid, mediatype,))
def removeItem(self, embyid): def removeItem(self, plexid):
query = "DELETE FROM emby WHERE emby_id = ?" 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 ?" 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 userclient
import PlexAPI import PlexAPI
from PlexFunctions import GetMachineIdentifier
############################################################################### ###############################################################################
@ -20,129 +21,271 @@ import PlexAPI
class InitialSetup(): class InitialSetup():
def __init__(self): def __init__(self):
self.logMsg('Entering initialsetup class', 1)
self.clientInfo = clientinfo.ClientInfo() self.clientInfo = clientinfo.ClientInfo()
self.addonId = self.clientInfo.getAddonId() self.addonId = self.clientInfo.getAddonId()
self.doUtils = downloadutils.DownloadUtils().downloadUrl self.doUtils = downloadutils.DownloadUtils().downloadUrl
self.userClient = userclient.UserClient() self.userClient = userclient.UserClient()
self.plx = PlexAPI.PlexAPI() self.plx = PlexAPI.PlexAPI()
self.dialog = xbmcgui.Dialog()
def setup(self, forcePlexTV=False, chooseServer=False): self.string = xbmcaddon.Addon().getLocalizedString
"""
Initial setup. Run once upon startup. self.server = self.userClient.getServer()
Check server, user, direct paths, music, direct stream if not direct self.serverid = utils.settings('plex_machineIdentifier')
path.
"""
string = xbmcaddon.Addon().getLocalizedString
# SERVER INFO #####
self.logMsg("Initial setup called.", 0)
server = self.userClient.getServer()
serverid = utils.settings('plex_machineIdentifier')
# Get Plex credentials from settings file, if they exist # Get Plex credentials from settings file, if they exist
plexdict = self.plx.GetPlexLoginFromSettings() plexdict = self.plx.GetPlexLoginFromSettings()
myplexlogin = plexdict['myplexlogin'] self.myplexlogin = plexdict['myplexlogin'] == 'true'
plexLogin = plexdict['plexLogin'] self.plexLogin = plexdict['plexLogin']
plexToken = plexdict['plexToken'] self.plexToken = plexdict['plexToken']
plexid = plexdict['plexid'] self.plexid = plexdict['plexid']
if plexToken: if self.plexToken:
self.logMsg('Found plex.tv token in settings', 0) 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 Returns True if successful, or False if not
# as plexToken will be '' """
if (plexToken and myplexlogin == 'true' and forcePlexTV is False result = self.plx.PlexTvSignInWithPin()
and chooseServer is False): if result:
chk = self.plx.CheckConnection('plex.tv', plexToken) self.plexLogin = result['username']
try: self.plexToken = result['token']
chk.attrib self.plexid = result['plexid']
except: return True
pass return False
else:
# Success - we downloaded an xml! def CheckPlexTVSignIn(self):
chk = 200 """
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 # HTTP Error: unauthorized. Token is no longer valid
if chk in (401, 403): self.logMsg('plex.tv connection returned HTTP %s' % str(chk), 1)
self.logMsg('plex.tv connection returned HTTP %s' % chk, 0) # Delete token in the settings
# Delete token in the settings utils.settings('plexToken', value='')
utils.settings('plexToken', value='') utils.settings('plexLogin', value='')
# Could not login, please try again # Could not login, please try again
dialog.ok(self.addonName, self.dialog.ok(self.addonName,
string(39009)) self.string(39009))
result = self.plx.PlexTvSignInWithPin() answer = self.PlexTVSignIn()
if result: elif chk is False or chk >= 400:
plexLogin = result['username'] # Problems connecting to plex.tv. Network or internet issue?
plexToken = result['token'] self.logMsg('Problems connecting to plex.tv; connection returned '
plexid = result['plexid'] 'HTTP %s' % str(chk), 1)
elif chk is False or chk >= 400: self.dialog.ok(self.addonName,
# Problems connecting to plex.tv. Network or internet issue? self.string(39010))
self.logMsg('plex.tv connection returned HTTP %s' answer = False
% str(chk), 0) else:
dialog.ok(self.addonName, self.logMsg('plex.tv connection with token successful', 1)
string(39010)) 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: else:
self.logMsg('plex.tv connection with token successful', 0) utils.settings('plexLogin', value=self.plexLogin)
# Refresh the info from Plex.tv home = 'true' if xml.attrib.get('home') == '1' else 'false'
xml = self.doUtils('https://plex.tv/users/account', utils.settings('plexhome', value=home)
authenticate=False, utils.settings('plexAvatar', value=xml.attrib.get('thumb'))
headerOptions={'X-Plex-Token': plexToken}) utils.settings(
try: 'plexHomeSize', value=xml.attrib.get('homeSize', '1'))
xml.attrib self.logMsg('Updated Plex info from plex.tv', 1)
except: return answer
self.logMsg('Failed to update Plex info from plex.tv', -1)
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: else:
plexLogin = xml.attrib.get('title') return
utils.settings('plexLogin', value=plexLogin) # Problems connecting
home = 'true' if xml.attrib.get('home') == '1' else 'false' elif chk >= 400 or chk is False:
utils.settings('plexhome', value=home) self.logMsg('Problems connecting to server %s. chk is %s'
utils.settings('plexAvatar', value=xml.attrib.get('thumb')) % (server['name'], chk), -1)
utils.settings( return
'plexHomeSize', value=xml.attrib.get('homeSize', '1')) self.logMsg('We found a server to automatically connect to: %s'
self.logMsg('Updated Plex info from plex.tv', 0) % server['name'], 1)
return server
# If a Plex server IP has already been set, return. def _UserPickPMS(self):
if server and forcePlexTV is False and chooseServer is False: """
self.logMsg("Server is already set.", 0) Lets user pick his/her PMS from a list
self.logMsg("url: %s, Plex machineIdentifier: %s"
% (server, serverid), 0)
return
# If not already retrieved myplex info, optionally let user sign in Returns server or None if unsuccessful
# 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)
httpsUpdated = False httpsUpdated = False
while True: while True:
if httpsUpdated is False: if httpsUpdated is False:
# Populate g_PMS variable with the found Plex servers serverlist = self._getServerList()
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 = []
# Exit if no servers found # Exit if no servers found
if len(serverlist) == 0: if len(serverlist) == 0:
dialog.ok( self.logMsg('No plex media servers found!', -1)
self.addonName, self.dialog.ok(self.addonName, self.string(39011))
string(39011) return
) # Get a nicer list
break dialoglist = []
for server in serverlist: for server in serverlist:
if server['local'] == '1': if server['local'] == '1':
# server is in the same network as client. Add "local" # server is in the same network as client.
msg = string(39022) # Add"local"
msg = self.string(39022)
else: else:
# Add 'remote' # Add 'remote'
msg = string(39054) msg = self.string(39054)
if server.get('ownername'): if server.get('ownername'):
# Display username if its not our PMS # Display username if its not our PMS
dialoglist.append('%s (%s, %s)' dialoglist.append('%s (%s, %s)'
@ -151,33 +294,12 @@ class InitialSetup():
msg)) msg))
else: else:
dialoglist.append('%s (%s)' dialoglist.append('%s (%s)'
% (server['name'], % (server['name'], msg))
msg)) # Let user pick server from a list
resp = dialog.select(string(39012), dialoglist) resp = self.dialog.select(self.string(39012), dialoglist)
server = serverlist[resp] server = serverlist[resp]
activeServer = server['machineIdentifier'] chk = self._checkServerCon(server)
# 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)
if chk == 504 and httpsUpdated is False: if chk == 504 and httpsUpdated is False:
# Not able to use HTTP, try HTTPs for now # Not able to use HTTP, try HTTPs for now
serverlist[resp]['scheme'] = 'https' serverlist[resp]['scheme'] = 'https'
@ -185,68 +307,124 @@ class InitialSetup():
continue continue
httpsUpdated = False httpsUpdated = False
if chk == 401: 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 # Please sign in to plex.tv
dialog.ok(self.addonName, self.dialog.ok(self.addonName,
string(39013) + server['name'], self.string(39013) + server['name'],
string(39014)) self.string(39014))
result = self.plx.PlexTvSignInWithPin() if self.PlexTVSignIn() is False:
if result:
plexLogin = result['username']
plexToken = result['token']
plexid = result['plexid']
else:
# Exit while loop if user cancels # Exit while loop if user cancels
break return
# Problems connecting # Problems connecting
elif chk >= 400 or chk is False: elif chk >= 400 or chk is False:
# Problems connecting to server. Pick another server? # Problems connecting to server. Pick another server?
resp = dialog.yesno(self.addonName, answ = self.dialog.yesno(self.addonName,
string(39015)) self.string(39015))
# Exit while loop if user chooses No # Exit while loop if user chooses No
if not resp: if not answ:
break return
# Otherwise: connection worked! # Otherwise: connection worked!
else: else:
isconnected = True return server
break
if not isconnected: def WritePMStoSettings(self, server):
# Enter Kodi settings instead """
xbmc.executebuiltin('Addon.OpenSettings(%s)' % self.addonId) Saves server to file settings. server is a dict of the form:
return {
# Write to Kodi settings file 'name': friendlyName, the Plex server's name
utils.settings('plex_machineIdentifier', activeServer) '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_servername', server['name'])
utils.settings('plex_serverowned', utils.settings('plex_serverowned',
'true' if server['owned'] == '1' 'true' if server['owned'] == '1'
else 'false') else 'false')
# Careful to distinguish local from remote PMS
if server['local'] == '1': if server['local'] == '1':
scheme = server['scheme'] scheme = server['scheme']
utils.settings('ipaddress', server['ip']) utils.settings('ipaddress', server['ip'])
utils.settings('port', server['port']) utils.settings('port', server['port'])
self.logMsg("Setting SSL verify to false, because server is "
"local", 1)
utils.settings('sslverify', 'false')
else: else:
baseURL = server['baseURL'].split(':') baseURL = server['baseURL'].split(':')
scheme = baseURL[0] scheme = baseURL[0]
utils.settings('ipaddress', baseURL[1].replace('//', '')) utils.settings('ipaddress', baseURL[1].replace('//', ''))
utils.settings('port', baseURL[2]) 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': if scheme == 'https':
utils.settings('https', 'true') utils.settings('https', 'true')
else: else:
utils.settings('https', 'false') utils.settings('https', 'false')
# And finally do some logging
self.logMsg("Writing to Kodi user settings file", 0) self.logMsg("Writing to Kodi user settings file", 0)
self.logMsg("PMS machineIdentifier: %s, ip: %s, port: %s, https: %s " self.logMsg("PMS machineIdentifier: %s, ip: %s, port: %s, https: %s "
% (activeServer, server['ip'], server['port'], % (server['machineIdentifier'], server['ip'],
server['scheme']), 0) 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 return
goToSettings = False # Additional settings where the user needs to choose
# Direct paths (\\NAS\mymovie.mkv) or addon (http)? # Direct paths (\\NAS\mymovie.mkv) or addon (http)?
if dialog.yesno(heading=self.addonName, if dialog.yesno(self.addonName,
line1=string(39027), string(39027),
line2=string(39028), string(39028),
nolabel="Addon (Default)", nolabel="Addon (Default)",
yeslabel="Native (Direct Paths)"): yeslabel="Native (Direct Paths)"):
self.logMsg("User opted to use direct paths.", 1) self.logMsg("User opted to use direct paths.", 1)
@ -284,15 +462,36 @@ class InitialSetup():
else: else:
utils.advancedSettingsXML() 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: if goToSettings is False:
# Open Settings page now? You will need to restart! # Open Settings page now? You will need to restart!
goToSettings = dialog.yesno(heading=self.addonName, goToSettings = dialog.yesno(heading=self.addonName,
line1=string(39017)) line1=string(39017))
if goToSettings: if goToSettings:
utils.window('emby_serverStatus', value="Stop") utils.window('plex_serverStatus', value="Stop")
xbmc.executebuiltin( xbmc.executebuiltin(
'Addon.OpenSettings(plugin.video.plexkodiconnect)') 'Addon.OpenSettings(plugin.video.plexkodiconnect)')
else: else:
# "Kodi will now restart to apply the changes"
dialog.ok(
heading=self.addonName,
line1=string(33033))
xbmc.executebuiltin('RestartApp') xbmc.executebuiltin('RestartApp')
# We should always restart to ensure e.g. Kodi settings for Music # We should always restart to ensure e.g. Kodi settings for Music
# are in use! # are in use!

View file

@ -11,7 +11,6 @@ import xbmcgui
import xbmcvfs import xbmcvfs
import artwork import artwork
import downloadutils
import utils import utils
import embydb_functions as embydb import embydb_functions as embydb
import kodidb_functions as kodidb import kodidb_functions as kodidb
@ -34,7 +33,6 @@ class Items(object):
""" """
def __init__(self): def __init__(self):
self.doUtils = downloadutils.DownloadUtils()
self.kodiversion = int(xbmc.getInfoLabel("System.BuildVersion")[:2]) self.kodiversion = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
# self.directpath = utils.settings('useDirectPaths') == "1" # self.directpath = utils.settings('useDirectPaths') == "1"
self.directpath = True if utils.window('useDirectPaths') == 'true' \ 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 # If the playback was stopped, check whether we need to increment the
# playcount. PMS won't tell us the playcount via websockets # playcount. PMS won't tell us the playcount via websockets
if item['state'] in ('stopped', 'ended'): if item['state'] in ('stopped', 'ended'):
markPlayed = 0.90
complete = float(item['viewOffset']) / float(item['duration']) complete = float(item['viewOffset']) / float(item['duration'])
complete = complete * 100
self.logMsg('Item %s stopped with completion rate %s percent.' self.logMsg('Item %s stopped with completion rate %s percent.'
'Mark item played at %s percent.' 'Mark item played at %s percent.'
% (item['ratingKey'], % (item['ratingKey'], str(complete), markPlayed), 1)
str(complete), if complete >= markPlayed:
utils.settings('markPlayed')), 1)
if complete >= float(utils.settings('markPlayed')):
self.logMsg('Marking as completely watched in Kodi', 1) self.logMsg('Marking as completely watched in Kodi', 1)
try: try:
item['viewCount'] += 1 item['viewCount'] += 1
@ -309,31 +305,17 @@ class Movies(Items):
count = 0 count = 0
for boxset in items: for boxset in items:
title = boxset['Name']
if pdialog: if pdialog:
percentage = int((float(count) / float(total))*100) percentage = int((float(count) / float(total))*100)
pdialog.update(percentage, message=title) pdialog.update(percentage, message=boxset['Name'])
count += 1 count += 1
self.add_updateBoxset(boxset) self.add_updateBoxset(boxset)
@utils.CatchExceptions(warnuser=True)
def add_update(self, item, viewtag=None, viewid=None): 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 # Process single movie
kodicursor = self.kodicursor kodicursor = self.kodicursor
emby_db = self.emby_db emby_db = self.emby_db
kodi_db = self.kodi_db
artwork = self.artwork artwork = self.artwork
API = PlexAPI.API(item) API = PlexAPI.API(item)
@ -417,7 +399,7 @@ class Movies(Items):
doIndirect = not self.directpath doIndirect = not self.directpath
if self.directpath: if self.directpath:
# Direct paths is set the Kodi way # Direct paths is set the Kodi way
playurl = API.getFilePath() playurl = API.getFilePath(forceFirstMediaStream=True)
if playurl is None: if playurl is None:
# Something went wrong, trying to use non-direct paths # Something went wrong, trying to use non-direct paths
doIndirect = True doIndirect = True
@ -436,7 +418,7 @@ class Movies(Items):
# Set plugin path and media flags using real filename # Set plugin path and media flags using real filename
path = "plugin://plugin.video.plexkodiconnect.movies/" path = "plugin://plugin.video.plexkodiconnect.movies/"
params = { params = {
'filename': API.getKey().encode('utf-8'), 'filename': API.getKey(),
'id': itemid, 'id': itemid,
'dbid': movieid, 'dbid': movieid,
'mode': "play" 'mode': "play"
@ -444,11 +426,6 @@ class Movies(Items):
filename = "%s?%s" % (path, urllib.urlencode(params)) filename = "%s?%s" % (path, urllib.urlencode(params))
playurl = filename 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: # movie table:
# c22 - playurl # c22 - playurl
# c23 - pathid # c23 - pathid
@ -477,6 +454,10 @@ class Movies(Items):
##### OR ADD THE MOVIE ##### ##### OR ADD THE MOVIE #####
else: else:
self.logMsg("ADD movie itemid: %s - Title: %s" % (itemid, title), 1) 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 # Create the movie entry
query = ( query = (
@ -514,28 +495,30 @@ class Movies(Items):
kodicursor.execute(query, (pathid, filename, dateadded, fileid)) kodicursor.execute(query, (pathid, filename, dateadded, fileid))
# Process countries # Process countries
kodi_db.addCountries(movieid, countries, "movie") self.kodi_db.addCountries(movieid, countries, "movie")
# Process cast # Process cast
people = API.getPeopleList() people = API.getPeopleList()
kodi_db.addPeople(movieid, people, "movie") self.kodi_db.addPeople(movieid, people, "movie")
# Process genres # Process genres
kodi_db.addGenres(movieid, genres, "movie") self.kodi_db.addGenres(movieid, genres, "movie")
# Process artwork # Process artwork
allartworks = API.getAllArtwork() allartworks = API.getAllArtwork()
artwork.addArtwork(allartworks, movieid, "movie", kodicursor) artwork.addArtwork(allartworks, movieid, "movie", kodicursor)
# Process stream details # Process stream details
streams = API.getMediaStreams() streams = API.getMediaStreams()
kodi_db.addStreams(fileid, streams, runtime) self.kodi_db.addStreams(fileid, streams, runtime)
# Process studios # Process studios
kodi_db.addStudios(movieid, studios, "movie") self.kodi_db.addStudios(movieid, studios, "movie")
# Process tags: view, Plex collection tags # Process tags: view, Plex collection tags
tags = [viewtag] tags = [viewtag]
tags.extend(collections) tags.extend(collections)
if userdata['Favorite']: if userdata['Favorite']:
tags.append("Favorite movies") 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 # Process playstates
kodi_db.addPlaystate(fileid, resume, runtime, playcount, dateplayed) self.kodi_db.addPlaystate(fileid, resume, runtime, playcount, dateplayed)
def remove(self, itemid): def remove(self, itemid):
# Remove movieid, fileid, emby reference # Remove movieid, fileid, emby reference
@ -566,11 +549,11 @@ class Movies(Items):
# Delete kodi boxset # Delete kodi boxset
boxset_movies = emby_db.getItem_byParentId(kodiid, "movie") boxset_movies = emby_db.getItem_byParentId(kodiid, "movie")
for movie in boxset_movies: for movie in boxset_movies:
embyid = movie[0] plexid = movie[0]
movieid = movie[1] movieid = movie[1]
self.kodi_db.removefromBoxset(movieid) self.kodi_db.removefromBoxset(movieid)
# Update emby reference # Update emby reference
emby_db.updateParentId(embyid, None) emby_db.updateParentId(plexid, None)
kodicursor.execute("DELETE FROM sets WHERE idSet = ?", (kodiid,)) kodicursor.execute("DELETE FROM sets WHERE idSet = ?", (kodiid,))
@ -603,9 +586,8 @@ class MusicVideos(Items):
# Process single music video # Process single music video
kodicursor = self.kodicursor kodicursor = self.kodicursor
emby_db = self.emby_db emby_db = self.emby_db
kodi_db = self.kodi_db
artwork = self.artwork 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 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 # 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: if not viewtag or not viewid:
# Get view tag from emby # 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) self.logMsg("View tag found: %s" % viewtag, 2)
# fileId information # fileId information
@ -675,7 +657,7 @@ class MusicVideos(Items):
if self.directpath: if self.directpath:
# Direct paths is set the Kodi way # 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 # Validate the path is correct with user intervention
resp = xbmcgui.Dialog().yesno( resp = xbmcgui.Dialog().yesno(
heading="Can't validate path", heading="Can't validate path",
@ -686,17 +668,17 @@ class MusicVideos(Items):
"to format your path correctly. Stop syncing?" "to format your path correctly. Stop syncing?"
% playurl)) % playurl))
if resp: if resp:
utils.window('emby_shouldStop', value="true") utils.window('plex_shouldStop', value="true")
return False return False
path = playurl.replace(filename, "") path = playurl.replace(filename, "")
utils.window('emby_pathverified', value="true") utils.window('plex_pathverified', value="true")
else: else:
# Set plugin path and media flags using real filename # Set plugin path and media flags using real filename
path = "plugin://plugin.video.plexkodiconnect.musicvideos/" path = "plugin://plugin.video.plexkodiconnect.musicvideos/"
params = { params = {
'filename': filename.encode('utf-8'), 'filename': utils.tryEncode(filename),
'id': itemid, 'id': itemid,
'dbid': mvideoid, 'dbid': mvideoid,
'mode': "play" 'mode': "play"
@ -794,32 +776,31 @@ class MusicVideos(Items):
artist['Type'] = "Artist" artist['Type'] = "Artist"
people.extend(artists) people.extend(artists)
people = artwork.getPeopleArtwork(people) people = artwork.getPeopleArtwork(people)
kodi_db.addPeople(mvideoid, people, "musicvideo") self.kodi_db.addPeople(mvideoid, people, "musicvideo")
# Process genres # Process genres
kodi_db.addGenres(mvideoid, genres, "musicvideo") self.kodi_db.addGenres(mvideoid, genres, "musicvideo")
# Process artwork # Process artwork
artwork.addArtwork(artwork.getAllArtwork(item), mvideoid, "musicvideo", kodicursor) artwork.addArtwork(artwork.getAllArtwork(item), mvideoid, "musicvideo", kodicursor)
# Process stream details # Process stream details
streams = API.getMediaStreams() streams = API.getMediaStreams()
kodi_db.addStreams(fileid, streams, runtime) self.kodi_db.addStreams(fileid, streams, runtime)
# Process studios # Process studios
kodi_db.addStudios(mvideoid, studios, "musicvideo") self.kodi_db.addStudios(mvideoid, studios, "musicvideo")
# Process tags: view, emby tags # Process tags: view, emby tags
tags = [viewtag] tags = [viewtag]
tags.extend(item['Tags']) tags.extend(item['Tags'])
if userdata['Favorite']: if userdata['Favorite']:
tags.append("Favorite musicvideos") tags.append("Favorite musicvideos")
kodi_db.addTags(mvideoid, tags, "musicvideo") self.kodi_db.addTags(mvideoid, tags, "musicvideo")
# Process playstates # Process playstates
resume = API.adjustResume(userdata['Resume']) resume = API.adjustResume(userdata['Resume'])
total = round(float(runtime), 6) 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): def updateUserdata(self, item):
# This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks # This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
# Poster with progress bar # Poster with progress bar
emby_db = self.emby_db emby_db = self.emby_db
kodi_db = self.kodi_db
API = api.API(item) API = api.API(item)
# Get emby information # Get emby information
@ -841,9 +822,9 @@ class MusicVideos(Items):
# Process favorite tags # Process favorite tags
if userdata['Favorite']: if userdata['Favorite']:
kodi_db.addTag(mvideoid, "Favorite musicvideos", "musicvideo") self.kodi_db.addTag(mvideoid, "Favorite musicvideos", "musicvideo")
else: else:
kodi_db.removeTag(mvideoid, "Favorite musicvideos", "musicvideo") self.kodi_db.removeTag(mvideoid, "Favorite musicvideos", "musicvideo")
# Process playstates # Process playstates
playcount = userdata['PlayCount'] playcount = userdata['PlayCount']
@ -851,7 +832,7 @@ class MusicVideos(Items):
resume = API.adjustResume(userdata['Resume']) resume = API.adjustResume(userdata['Resume'])
total = round(float(runtime), 6) 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) emby_db.updateReference(itemid, checksum)
def remove(self, itemid): def remove(self, itemid):
@ -878,8 +859,7 @@ class MusicVideos(Items):
"AND media_type = 'musicvideo'" "AND media_type = 'musicvideo'"
)) ))
kodicursor.execute(query, (mvideoid,)) kodicursor.execute(query, (mvideoid,))
rows = kodicursor.fetchall() for row in kodicursor.fetchall():
for row in rows:
url = row[0] url = row[0]
imagetype = row[1] imagetype = row[1]
@ -942,24 +922,11 @@ class TVShows(Items):
if not pdialog and self.contentmsg: if not pdialog and self.contentmsg:
self.contentPop(title, self.newvideo_time) self.contentPop(title, self.newvideo_time)
@utils.CatchExceptions(warnuser=True)
def add_update(self, item, viewtag=None, viewid=None): 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 # Process single tvshow
kodicursor = self.kodicursor kodicursor = self.kodicursor
emby_db = self.emby_db emby_db = self.emby_db
kodi_db = self.kodi_db
artwork = self.artwork artwork = self.artwork
API = PlexAPI.API(item) API = PlexAPI.API(item)
@ -997,7 +964,7 @@ class TVShows(Items):
if viewtag is None or viewid is None: if viewtag is None or viewid is None:
# Get view tag from emby # 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) self.logMsg("View tag found: %s" % viewtag, 2)
# fileId information # fileId information
@ -1047,7 +1014,7 @@ class TVShows(Items):
path = "%s%s/" % (toplevelpath, itemid) path = "%s%s/" % (toplevelpath, itemid)
# Add top path # Add top path
toppathid = kodi_db.addPath(toplevelpath) toppathid = self.kodi_db.addPath(toplevelpath)
# UPDATE THE TVSHOW ##### # UPDATE THE TVSHOW #####
if update_item: if update_item:
self.logMsg("UPDATE tvshow itemid: %s - Title: %s" % (itemid, title), 1) 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)) kodicursor.execute(query, (toplevelpath, "tvshows", "metadata.local", 1, toppathid))
# Add path # Add path
pathid = kodi_db.addPath(path) pathid = self.kodi_db.addPath(path)
# Create the tvshow entry # Create the tvshow entry
query = ( query = (
@ -1112,18 +1079,18 @@ class TVShows(Items):
# Process cast # Process cast
people = API.getPeopleList() people = API.getPeopleList()
kodi_db.addPeople(showid, people, "tvshow") self.kodi_db.addPeople(showid, people, "tvshow")
# Process genres # Process genres
kodi_db.addGenres(showid, genres, "tvshow") self.kodi_db.addGenres(showid, genres, "tvshow")
# Process artwork # Process artwork
allartworks = API.getAllArtwork() allartworks = API.getAllArtwork()
artwork.addArtwork(allartworks, showid, "tvshow", kodicursor) artwork.addArtwork(allartworks, showid, "tvshow", kodicursor)
# Process studios # Process studios
kodi_db.addStudios(showid, studios, "tvshow") self.kodi_db.addStudios(showid, studios, "tvshow")
# Process tags: view, PMS collection tags # Process tags: view, PMS collection tags
tags = [viewtag] tags = [viewtag]
tags.extend(collections) tags.extend(collections)
kodi_db.addTags(showid, tags, "tvshow") self.kodi_db.addTags(showid, tags, "tvshow")
if force_episodes: if force_episodes:
# We needed to recreate the show entry. Re-add episodes now. # We needed to recreate the show entry. Re-add episodes now.
@ -1133,20 +1100,8 @@ class TVShows(Items):
self.kodiconn.commit() self.kodiconn.commit()
self.embyconn.commit() self.embyconn.commit()
@utils.CatchExceptions(warnuser=True)
def add_updateSeason(self, item, viewtag=None, viewid=None): 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) API = PlexAPI.API(item)
itemid = API.getRatingKey() itemid = API.getRatingKey()
if not itemid: if not itemid:
@ -1154,7 +1109,6 @@ class TVShows(Items):
return return
kodicursor = self.kodicursor kodicursor = self.kodicursor
emby_db = self.emby_db emby_db = self.emby_db
kodi_db = self.kodi_db
artwork = self.artwork artwork = self.artwork
seasonnum = API.getIndex() seasonnum = API.getIndex()
# Get parent tv show Plex id # Get parent tv show Plex id
@ -1169,7 +1123,7 @@ class TVShows(Items):
% (itemid), -1) % (itemid), -1)
return return
seasonid = kodi_db.addSeason(showid, seasonnum) seasonid = self.kodi_db.addSeason(showid, seasonnum)
checksum = API.getChecksum() checksum = API.getChecksum()
# Check whether Season already exists # Check whether Season already exists
update_item = True update_item = True
@ -1192,28 +1146,14 @@ class TVShows(Items):
self.kodiconn.commit() self.kodiconn.commit()
self.embyconn.commit() self.embyconn.commit()
@utils.CatchExceptions(warnuser=True)
def add_updateEpisode(self, item, viewtag=None, viewid=None): 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! viewtag and viewid are irrelevant!
""" """
# Process single episode # Process single episode
kodiversion = self.kodiversion
kodicursor = self.kodicursor kodicursor = self.kodicursor
emby_db = self.emby_db emby_db = self.emby_db
kodi_db = self.kodi_db
artwork = self.artwork artwork = self.artwork
API = PlexAPI.API(item) 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("Skipping: %s. Unable to add series: %s." % (itemid, seriesId), -1)
self.logMsg("Parent tvshow now found, skip item", 2) self.logMsg("Parent tvshow now found, skip item", 2)
return False return False
seasonid = kodi_db.addSeason(showid, season) seasonid = self.kodi_db.addSeason(showid, season)
# GET THE FILE AND PATH ##### # GET THE FILE AND PATH #####
doIndirect = not self.directpath doIndirect = not self.directpath
playurl = API.getFilePath() playurl = API.getFilePath(forceFirstMediaStream=True)
if self.directpath: if self.directpath:
# Direct paths is set the Kodi way # Direct paths is set the Kodi way
if playurl is None: if playurl is None:
@ -1331,7 +1271,7 @@ class TVShows(Items):
# Network share # Network share
filename = playurl.rsplit("/", 1)[1] filename = playurl.rsplit("/", 1)[1]
path = playurl.replace(filename, "") path = playurl.replace(filename, "")
parentPathId = kodi_db.getParentPathId(path) parentPathId = self.kodi_db.getParentPathId(path)
if doIndirect: if doIndirect:
# Set plugin path and media flags using real filename # Set plugin path and media flags using real filename
if playurl is not None: if playurl is not None:
@ -1340,24 +1280,20 @@ class TVShows(Items):
else: else:
filename = playurl.rsplit('/', 1)[1] filename = playurl.rsplit('/', 1)[1]
else: else:
filename = 'file_not_found' filename = 'file_not_found.mkv'
path = "plugin://plugin.video.plexkodiconnect.tvshows/%s/" % seriesId path = "plugin://plugin.video.plexkodiconnect.tvshows/%s/" % seriesId
params = { params = {
'filename': filename.encode('utf-8'), 'filename': utils.tryEncode(filename),
'id': itemid, 'id': itemid,
'dbid': episodeid, 'dbid': episodeid,
'mode': "play" 'mode': "play"
} }
filename = "%s?%s" % (path, urllib.urlencode(params)) filename = "%s?%s" % (path,
utils.tryDecode(urllib.urlencode(params)))
playurl = filename playurl = filename
parentPathId = kodi_db.addPath( parentPathId = self.kodi_db.addPath(
'plugin://plugin.video.plexkodiconnect.tvshows/') '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: # episodes table:
# c18 - playurl # c18 - playurl
# c19 - pathid # c19 - pathid
@ -1368,7 +1304,7 @@ class TVShows(Items):
self.logMsg("UPDATE episode itemid: %s" % (itemid), 1) self.logMsg("UPDATE episode itemid: %s" % (itemid), 1)
# Update the movie entry # Update the movie entry
if kodiversion in (16, 17): if self.kodiversion in (16, 17):
# Kodi Jarvis, Krypton # Kodi Jarvis, Krypton
query = ' '.join(( query = ' '.join((
@ -1400,10 +1336,13 @@ class TVShows(Items):
##### OR ADD THE EPISODE ##### ##### OR ADD THE EPISODE #####
else: 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 # Create the episode entry
if kodiversion in (16, 17): if self.kodiversion in (16, 17):
# Kodi Jarvis, Krypton # Kodi Jarvis, Krypton
query = ( query = (
''' '''
@ -1454,7 +1393,7 @@ class TVShows(Items):
kodicursor.execute(query, (pathid, filename, dateadded, fileid)) kodicursor.execute(query, (pathid, filename, dateadded, fileid))
# Process cast # Process cast
people = API.getPeopleList() people = API.getPeopleList()
kodi_db.addPeople(episodeid, people, "episode") self.kodi_db.addPeople(episodeid, people, "episode")
# Process artwork # Process artwork
# Wide "screenshot" of particular episode # Wide "screenshot" of particular episode
poster = item.attrib.get('thumb') poster = item.attrib.get('thumb')
@ -1473,13 +1412,13 @@ class TVShows(Items):
# Process stream details # Process stream details
streams = API.getMediaStreams() streams = API.getMediaStreams()
kodi_db.addStreams(fileid, streams, runtime) self.kodi_db.addStreams(fileid, streams, runtime)
# Process playstates # 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: if not self.directpath and resume:
# Create additional entry for widgets. This is only required for plugin/episode. # Create additional entry for widgets. This is only required for plugin/episode.
temppathid = kodi_db.getPath("plugin://plugin.video.plexkodiconnect.tvshows/") temppathid = self.kodi_db.getPath("plugin://plugin.video.plexkodiconnect.tvshows/")
tempfileid = kodi_db.addFile(filename, temppathid) tempfileid = self.kodi_db.addFile(filename, temppathid)
query = ' '.join(( query = ' '.join((
"UPDATE files", "UPDATE files",
@ -1487,7 +1426,7 @@ class TVShows(Items):
"WHERE idFile = ?" "WHERE idFile = ?"
)) ))
kodicursor.execute(query, (temppathid, filename, dateadded, tempfileid)) 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.kodiconn.commit()
self.embyconn.commit() self.embyconn.commit()
@ -1600,27 +1539,23 @@ class TVShows(Items):
def removeShow(self, kodiid): def removeShow(self, kodiid):
kodicursor = self.kodicursor kodicursor = self.kodicursor
artwork = self.artwork self.artwork.deleteArtwork(kodiid, "tvshow", kodicursor)
artwork.deleteArtwork(kodiid, "tvshow", kodicursor)
kodicursor.execute("DELETE FROM tvshow WHERE idShow = ?", (kodiid,)) kodicursor.execute("DELETE FROM tvshow WHERE idShow = ?", (kodiid,))
self.logMsg("Removed tvshow: %s." % kodiid, 2) self.logMsg("Removed tvshow: %s." % kodiid, 2)
def removeSeason(self, kodiid): def removeSeason(self, kodiid):
kodicursor = self.kodicursor 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,)) kodicursor.execute("DELETE FROM seasons WHERE idSeason = ?", (kodiid,))
self.logMsg("Removed season: %s." % kodiid, 2) self.logMsg("Removed season: %s." % kodiid, 2)
def removeEpisode(self, kodiid, fileid): def removeEpisode(self, kodiid, fileid):
kodicursor = self.kodicursor 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 episode WHERE idEpisode = ?", (kodiid,))
kodicursor.execute("DELETE FROM files WHERE idFile = ?", (fileid,)) kodicursor.execute("DELETE FROM files WHERE idFile = ?", (fileid,))
self.logMsg("Removed episode: %s." % kodiid, 2) self.logMsg("Removed episode: %s." % kodiid, 2)
@ -1656,10 +1591,9 @@ class Music(Items):
count = 0 count = 0
for artist in items: for artist in items:
title = artist['Name']
if pdialog: if pdialog:
percentage = int((float(count) / float(total))*100) percentage = int((float(count) / float(total))*100)
pdialog.update(percentage, message=title) pdialog.update(percentage, message=artist['Name'])
count += 1 count += 1
self.add_updateArtist(artist) self.add_updateArtist(artist)
# Add albums # Add albums
@ -1672,10 +1606,9 @@ class Music(Items):
count = 0 count = 0
for album in items: for album in items:
title = album['Name']
if pdialog: if pdialog:
percentage = int((float(count) / float(total))*100) percentage = int((float(count) / float(total))*100)
pdialog.update(percentage, message=title) pdialog.update(percentage, message=album['Name'])
count += 1 count += 1
self.add_updateAlbum(album) self.add_updateAlbum(album)
# Add songs # Add songs
@ -1688,34 +1621,19 @@ class Music(Items):
count = 0 count = 0
for song in items: for song in items:
title = song['Name']
if pdialog: if pdialog:
percentage = int((float(count) / float(total))*100) percentage = int((float(count) / float(total))*100)
pdialog.update(percentage, message=title) pdialog.update(percentage, message=song['Name'])
count += 1 count += 1
self.add_updateSong(song) self.add_updateSong(song)
if not pdialog and self.contentmsg: 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"): @utils.CatchExceptions(warnuser=True)
try: def add_updateArtist(self, item, viewtag=None, viewid=None,
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,
artisttype="MusicArtist"): artisttype="MusicArtist"):
kodicursor = self.kodicursor kodicursor = self.kodicursor
emby_db = self.emby_db emby_db = self.emby_db
kodi_db = self.kodi_db
artwork = self.artwork artwork = self.artwork
API = PlexAPI.API(item) API = PlexAPI.API(item)
@ -1764,7 +1682,7 @@ class Music(Items):
# multiple times. # multiple times.
# Kodi doesn't allow that. In case that happens we just merge the # Kodi doesn't allow that. In case that happens we just merge the
# artist entries. # artist entries.
artistid = kodi_db.addArtist(name, musicBrainzId) artistid = self.kodi_db.addArtist(name, musicBrainzId)
# Create the reference in emby table # Create the reference in emby table
emby_db.addReference( emby_db.addReference(
itemid, artistid, artisttype, "artist", checksum=checksum) itemid, artistid, artisttype, "artist", checksum=checksum)
@ -1796,25 +1714,10 @@ class Music(Items):
self.embyconn.commit() self.embyconn.commit()
self.kodiconn.commit() self.kodiconn.commit()
@utils.CatchExceptions(warnuser=True)
def add_updateAlbum(self, item, viewtag=None, viewid=None): 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 kodicursor = self.kodicursor
emby_db = self.emby_db emby_db = self.emby_db
kodi_db = self.kodi_db
artwork = self.artwork artwork = self.artwork
API = PlexAPI.API(item) API = PlexAPI.API(item)
@ -1875,13 +1778,13 @@ class Music(Items):
# multiple times. # multiple times.
# Kodi doesn't allow that. In case that happens we just merge the # Kodi doesn't allow that. In case that happens we just merge the
# artist entries. # artist entries.
albumid = kodi_db.addAlbum(name, musicBrainzId) albumid = self.kodi_db.addAlbum(name, musicBrainzId)
# Create the reference in emby table # Create the reference in emby table
emby_db.addReference( emby_db.addReference(
itemid, albumid, "MusicAlbum", "album", checksum=checksum) itemid, albumid, "MusicAlbum", "album", checksum=checksum)
# Process the album info # Process the album info
if kodiversion == 17: if self.kodiversion == 17:
# Kodi Krypton # Kodi Krypton
query = ' '.join(( query = ' '.join((
@ -1894,7 +1797,7 @@ class Music(Items):
kodicursor.execute(query, (artistname, year, genre, bio, thumb, kodicursor.execute(query, (artistname, year, genre, bio, thumb,
rating, lastScraped, "album", studio, rating, lastScraped, "album", studio,
albumid)) albumid))
elif kodiversion == 16: elif self.kodiversion == 16:
# Kodi Jarvis # Kodi Jarvis
query = ' '.join(( query = ' '.join((
@ -1907,7 +1810,7 @@ class Music(Items):
kodicursor.execute(query, (artistname, year, genre, bio, thumb, kodicursor.execute(query, (artistname, year, genre, bio, thumb,
rating, lastScraped, "album", studio, rating, lastScraped, "album", studio,
albumid)) albumid))
elif kodiversion == 15: elif self.kodiversion == 15:
# Kodi Isengard # Kodi Isengard
query = ' '.join(( query = ' '.join((
@ -1998,33 +1901,18 @@ class Music(Items):
# Update emby reference with parentid # Update emby reference with parentid
emby_db.updateParentId(artistId, albumid) emby_db.updateParentId(artistId, albumid)
# Add genres # Add genres
kodi_db.addMusicGenres(albumid, genres, "album") self.kodi_db.addMusicGenres(albumid, genres, "album")
# Update artwork # Update artwork
artwork.addArtwork(artworks, albumid, "album", kodicursor) artwork.addArtwork(artworks, albumid, "album", kodicursor)
self.embyconn.commit() self.embyconn.commit()
self.kodiconn.commit() self.kodiconn.commit()
@utils.CatchExceptions(warnuser=True)
def add_updateSong(self, item, viewtag=None, viewid=None): 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 # Process single song
kodiversion = self.kodiversion
kodicursor = self.kodicursor kodicursor = self.kodicursor
emby = self.emby emby = self.emby
emby_db = self.emby_db emby_db = self.emby_db
kodi_db = self.kodi_db
artwork = self.artwork artwork = self.artwork
API = PlexAPI.API(item) API = PlexAPI.API(item)
@ -2085,7 +1973,7 @@ class Music(Items):
doIndirect = not self.directpath doIndirect = not self.directpath
if self.directpath: if self.directpath:
# Direct paths is set the Kodi way # Direct paths is set the Kodi way
playurl = API.getFilePath() playurl = API.getFilePath(forceFirstMediaStream=True)
if playurl is None: if playurl is None:
# Something went wrong, trying to use non-direct paths # Something went wrong, trying to use non-direct paths
doIndirect = True doIndirect = True
@ -2136,7 +2024,7 @@ class Music(Items):
self.logMsg("ADD song itemid: %s - Title: %s" % (itemid, title), 1) self.logMsg("ADD song itemid: %s - Title: %s" % (itemid, title), 1)
# Add path # Add path
pathid = kodi_db.addPath(path, strHash="123") pathid = self.kodi_db.addPath(path, strHash="123")
try: try:
# Get the album # Get the album
@ -2148,7 +2036,7 @@ class Music(Items):
album_name = item.get('parentTitle') album_name = item.get('parentTitle')
if album_name: if album_name:
self.logMsg("Creating virtual music album for song: %s." % itemid, 1) 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") emby_db.addReference("%salbum%s" % (itemid, albumid), albumid, "MusicAlbum_", "album")
else: else:
# No album Id associated to the song. # No album Id associated to the song.
@ -2173,7 +2061,7 @@ class Music(Items):
self.logMsg("Failed to add album. Creating singles.", 1) self.logMsg("Failed to add album. Creating singles.", 1)
kodicursor.execute("select coalesce(max(idAlbum),0) from album") kodicursor.execute("select coalesce(max(idAlbum),0) from album")
albumid = kodicursor.fetchone()[0] + 1 albumid = kodicursor.fetchone()[0] + 1
if kodiversion == 16: if self.kodiversion == 16:
# Kodi Jarvis # Kodi Jarvis
query = ( query = (
''' '''
@ -2183,7 +2071,7 @@ class Music(Items):
''' '''
) )
kodicursor.execute(query, (albumid, genre, year, "single")) kodicursor.execute(query, (albumid, genre, year, "single"))
elif kodiversion == 15: elif self.kodiversion == 15:
# Kodi Isengard # Kodi Isengard
query = ( query = (
''' '''
@ -2262,13 +2150,31 @@ class Music(Items):
artist_edb = emby_db.getItem_byId(artist_eid) artist_edb = emby_db.getItem_byId(artist_eid)
artistid = artist_edb[0] artistid = artist_edb[0]
finally: finally:
query = ( if self.kodiversion >= 17:
''' # Kodi Krypton
INSERT OR REPLACE INTO song_artist(idArtist, idSong, iOrder, strArtist) query = (
VALUES (?, ?, ?, ?) '''
''' INSERT OR REPLACE INTO song_artist(idArtist, idSong, idRole, iOrder, strArtist)
) VALUES (?, ?, ?, ?, ?)
kodicursor.execute(query, (artistid, songid, index, artist_name)) '''
)
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 # Verify if album artist exists
album_artists = [] album_artists = []
@ -2316,11 +2222,11 @@ class Music(Items):
result = kodicursor.fetchone() result = kodicursor.fetchone()
if result and result[0] != album_artists: if result and result[0] != album_artists:
# Field is empty # Field is empty
if kodiversion in (16, 17): if self.kodiversion in (16, 17):
# Kodi Jarvis, Krypton # Kodi Jarvis, Krypton
query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?" query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?"
kodicursor.execute(query, (album_artists, albumid)) kodicursor.execute(query, (album_artists, albumid))
elif kodiversion == 15: elif self.kodiversion == 15:
# Kodi Isengard # Kodi Isengard
query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?" query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?"
kodicursor.execute(query, (album_artists, albumid)) kodicursor.execute(query, (album_artists, albumid))
@ -2330,7 +2236,7 @@ class Music(Items):
kodicursor.execute(query, (album_artists, albumid)) kodicursor.execute(query, (album_artists, albumid))
# Add genres # Add genres
kodi_db.addMusicGenres(songid, genres, "song") self.kodi_db.addMusicGenres(songid, genres, "song")
# Update artwork # Update artwork
allart = API.getAllArtwork(parentInfo=True) allart = API.getAllArtwork(parentInfo=True)
@ -2372,10 +2278,9 @@ class Music(Items):
self.removeSong(kodiid) self.removeSong(kodiid)
# This should only address single song scenario, where server doesn't actually # This should only address single song scenario, where server doesn't actually
# create an album for the song. # create an album for the song.
customitems = emby_db.getItem_byWildId(itemid)
emby_db.removeWildItem(itemid) emby_db.removeWildItem(itemid)
for item in customitems: for item in emby_db.getItem_byWildId(itemid):
item_kid = item[0] item_kid = item[0]
item_mediatype = item[1] item_mediatype = item[1]
@ -2431,23 +2336,16 @@ class Music(Items):
def removeSong(self, kodiid): def removeSong(self, kodiid):
kodicursor = self.kodicursor kodicursor = self.kodicursor
artwork = self.artwork
artwork.deleteArtwork(kodiid, "song", kodicursor) self.artwork.deleteArtwork(kodiid, "song", self.kodicursor)
kodicursor.execute("DELETE FROM song WHERE idSong = ?", (kodiid,)) self.kodicursor.execute("DELETE FROM song WHERE idSong = ?", (kodiid,))
def removeAlbum(self, kodiid): def removeAlbum(self, kodiid):
kodicursor = self.kodicursor self.artwork.deleteArtwork(kodiid, "album", self.kodicursor)
artwork = self.artwork self.kodicursor.execute("DELETE FROM album WHERE idAlbum = ?", (kodiid,))
artwork.deleteArtwork(kodiid, "album", kodicursor)
kodicursor.execute("DELETE FROM album WHERE idAlbum = ?", (kodiid,))
def removeArtist(self, kodiid): def removeArtist(self, kodiid):
kodicursor = self.kodicursor self.artwork.deleteArtwork(kodiid, "artist", self.kodicursor)
artwork = self.artwork self.kodicursor.execute("DELETE FROM artist WHERE idArtist = ?", (kodiid,))
artwork.deleteArtwork(kodiid, "artist", kodicursor)
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.doUtils = downloadutils.DownloadUtils().downloadUrl
self.xbmcplayer = xbmc.Player() self.xbmcplayer = xbmc.Player()
xbmc.Monitor.__init__(self)
self.logMsg("Kodi monitor started.", 1) self.logMsg("Kodi monitor started.", 1)
def onScanStarted(self, library): def onScanStarted(self, library):
self.logMsg("Kodi library scan %s running." % library, 2) self.logMsg("Kodi library scan %s running." % library, 2)
if library == "video": if library == "video":
utils.window('emby_kodiScan', value="true") utils.window('plex_kodiScan', value="true")
def onScanFinished(self, library): def onScanFinished(self, library):
self.logMsg("Kodi library scan %s finished." % library, 2) self.logMsg("Kodi library scan %s finished." % library, 2)
if library == "video": if library == "video":
utils.window('emby_kodiScan', clear=True) utils.window('plex_kodiScan', clear=True)
def onSettingsChanged(self): def onSettingsChanged(self):
# Monitor emby settings # Monitor emby settings
# Review reset setting at a later time, need to be adjusted to account for initial setup # Review reset setting at a later time, need to be adjusted to account for initial setup
# changes. # changes.
'''currentPath = utils.settings('useDirectPaths') '''currentPath = utils.settings('useDirectPaths')
if utils.window('emby_pluginpath') != currentPath: if utils.window('plex_pluginpath') != currentPath:
# Plugin path value changed. Offer to reset # Plugin path value changed. Offer to reset
self.logMsg("Changed to playback mode detected", 1) self.logMsg("Changed to playback mode detected", 1)
utils.window('emby_pluginpath', value=currentPath) utils.window('plex_pluginpath', value=currentPath)
resp = xbmcgui.Dialog().yesno( resp = xbmcgui.Dialog().yesno(
heading="Playback mode change detected", heading="Playback mode change detected",
line1=( line1=(
@ -56,17 +56,17 @@ class KodiMonitor(xbmc.Monitor):
utils.reset()''' utils.reset()'''
currentLog = utils.settings('logLevel') currentLog = utils.settings('logLevel')
if utils.window('emby_logLevel') != currentLog: if utils.window('plex_logLevel') != currentLog:
# The log level changed, set new prop # The log level changed, set new prop
self.logMsg("New log level: %s" % currentLog, 1) 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): def onNotification(self, sender, method, data):
if method not in ("Playlist.OnAdd"):
self.logMsg("Method: %s Data: %s" % (method, data), 1)
if data: if data:
data = json.loads(data, 'utf-8') data = json.loads(data, 'utf-8')
self.logMsg("Method: %s Data: %s" % (method, data), 1)
if method == "Player.OnPlay": if method == "Player.OnPlay":
self.PlayBackStart(data) self.PlayBackStart(data)
@ -82,13 +82,13 @@ class KodiMonitor(xbmc.Monitor):
item = data.get('item') item = data.get('item')
try: try:
kodiid = item['id'] kodiid = item['id']
type = item['type'] item_type = item['type']
except (KeyError, TypeError): except (KeyError, TypeError):
self.logMsg("Item is invalid for playstate update.", 1) self.logMsg("Item is invalid for playstate update.", 1)
else: else:
# Send notification to the server. # Send notification to the server.
with embydb.GetEmbyDB() as emby_db: with embydb.GetEmbyDB() as emby_db:
emby_dbitem = emby_db.getItem_byKodiId(kodiid, type) emby_dbitem = emby_db.getItem_byKodiId(kodiid, item_type)
try: try:
itemid = emby_dbitem[0] itemid = emby_dbitem[0]
except TypeError: except TypeError:
@ -137,14 +137,19 @@ class KodiMonitor(xbmc.Monitor):
url = "{server}/emby/Items/%s?format=json" % itemid url = "{server}/emby/Items/%s?format=json" % itemid
self.logMsg("Deleting request: %s" % itemid) self.logMsg("Deleting request: %s" % itemid)
doUtils.downloadUrl(url, type="DELETE") doUtils.downloadUrl(url, action_type="DELETE")
finally: finally:
embycursor.close()''' embycursor.close()'''
elif method == "System.OnWake": elif method == "System.OnWake":
# Allow network to wake up # Allow network to wake up
xbmc.sleep(10000) 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": elif method == "Playlist.OnClear":
pass pass
@ -155,7 +160,6 @@ class KodiMonitor(xbmc.Monitor):
""" """
log = self.logMsg log = self.logMsg
window = utils.window window = utils.window
# Get currently playing file - can take a while. Will be utf-8! # Get currently playing file - can take a while. Will be utf-8!
try: try:
currentFile = self.xbmcplayer.getPlayingFile() currentFile = self.xbmcplayer.getPlayingFile()
@ -173,53 +177,77 @@ class KodiMonitor(xbmc.Monitor):
return return
else: else:
count += 1 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 # Get the type of media we're playing
item = data.get('item')
try: try:
type = item['type'] typus = data['item']['type']
except: except (TypeError, KeyError):
log("Item is invalid for PMS playstate update.", 0) log("Item is invalid for PMS playstate update.", 0)
return return
try: log("Playing itemtype is (or appears to be): %s" % typus, 1)
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
# Get Plex' item id # Try to get a Kodi ID
with embydb.GetEmbyDB() as emby_db: # If PKC was used - native paths, not direct paths
emby_dbitem = emby_db.getItem_byKodiId(kodiid, type) plexid = utils.window('emby_%s.itemid'
try: % utils.tryEncode(currentFile))
plexid = emby_dbitem[0] # Get rid of the '' if the window property was not set
except TypeError: plexid = None if not plexid else plexid
log("No Plex id returned for kodiid %s" % kodiid, 0) kodiid = None
return if plexid is None:
log("Found Plex id %s for Kodi id %s" % (plexid, kodiid), 1) 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 # Set some stuff if Kodi initiated playback
if ((utils.settings('useDirectPaths') == "1" and not type == "song") or if ((utils.settings('useDirectPaths') == "1" and not typus == "song")
(type == "song" and utils.settings('enableMusic') == "true")): or
if self.StartDirectPath(plexid, type, currentFile) is False: (typus == "song" and utils.settings('enableMusic') == "true")):
if self.StartDirectPath(plexid,
typus,
utils.tryEncode(currentFile)) is False:
log('Could not initiate monitoring; aborting', -1) log('Could not initiate monitoring; aborting', -1)
return return
# Save currentFile for cleanup later and to be able to access refs # 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('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) log('Finish playback startup', 1)
def StartDirectPath(self, plexid, type, currentFile): def StartDirectPath(self, plexid, type, currentFile):

View file

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

View file

@ -32,7 +32,7 @@ def getRealFileName(filename, isTemp=False):
if os.path.supports_unicode_filenames: if os.path.supports_unicode_filenames:
checkfile = filename checkfile = filename
else: else:
checkfile = filename.encode("utf-8") checkfile = utils.tryEncode(filename)
# determine if our python module is able to access the file directly... # determine if our python module is able to access the file directly...
if os.path.exists(checkfile): if os.path.exists(checkfile):
@ -46,7 +46,7 @@ def getRealFileName(filename, isTemp=False):
else: filepart = filename.split("\\")[-1] else: filepart = filename.split("\\")[-1]
tempfile = "special://temp/"+filepart tempfile = "special://temp/"+filepart
xbmcvfs.copy(filename, tempfile) xbmcvfs.copy(filename, tempfile)
filename = xbmc.translatePath(tempfile).decode("utf-8") filename = utils.tryDecode(xbmc.translatePath(tempfile))
return (isTemp,filename) return (isTemp,filename)
@ -65,14 +65,14 @@ def getEmbyRatingFromKodiRating(rating):
if (rating >= 5): favourite = True if (rating >= 5): favourite = True
return(like, favourite, deletelike) 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() emby = embyserver.Read_EmbyServer()
previous_values = None previous_values = None
filename = API.getFilePath() filename = API.getFilePath()
rating = 0 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. #get file rating and comment tag from file itself.
if enableimportsongrating: if enableimportsongrating:
@ -83,7 +83,7 @@ def getAdditionalSongTags(embyid, emby_rating, API, kodicursor, emby_db, enablei
hasEmbeddedCover = False hasEmbeddedCover = False
emby_dbitem = emby_db.getItem_byId(embyid) emby_dbitem = emby_db.getItem_byId(plexid)
try: try:
kodiid = emby_dbitem[0] kodiid = emby_dbitem[0]
except TypeError: except TypeError:
@ -100,44 +100,44 @@ def getAdditionalSongTags(embyid, emby_rating, API, kodicursor, emby_db, enablei
if file_rating is None and currentvalue: if file_rating is None and currentvalue:
return (currentvalue, comment, False) return (currentvalue, comment, False)
elif file_rating is None and not currentvalue: 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 updateFileRating = False
updateEmbyRating = False updateEmbyRating = False
if currentvalue != None: if currentvalue != None:
# we need to translate the emby values... # we need to translate the emby values...
if emby_rating == 1 and currentvalue == 2: if plex_rating == 1 and currentvalue == 2:
emby_rating = 2 plex_rating = 2
if emby_rating == 3 and currentvalue == 4: if plex_rating == 3 and currentvalue == 4:
emby_rating = 4 plex_rating = 4
#if updating rating into file is disabled, we ignore the rating in the file... #if updating rating into file is disabled, we ignore the rating in the file...
if not enableupdatesongrating: if not enableupdatesongrating:
file_rating = currentvalue file_rating = currentvalue
#if convert emby likes/favourites convert to song rating is disabled, we ignore the emby rating... #if convert emby likes/favourites convert to song rating is disabled, we ignore the emby rating...
if not enableexportsongrating: 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 #the rating has been updated from kodi itself, update change to both emby ands file
rating = currentvalue rating = currentvalue
updateFileRating = True updateFileRating = True
updateEmbyRating = 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 #emby rating changed - update the file
rating = emby_rating rating = plex_rating
updateFileRating = True 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 #file rating was updated, sync change to emby
rating = file_rating rating = file_rating
updateEmbyRating = True 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... #both ratings have changed (corner case) - the highest rating wins...
if emby_rating > file_rating: if plex_rating > file_rating:
rating = emby_rating rating = plex_rating
updateFileRating = True updateFileRating = True
else: else:
rating = file_rating rating = file_rating
@ -152,16 +152,16 @@ def getAdditionalSongTags(embyid, emby_rating, API, kodicursor, emby_db, enablei
rating = file_rating rating = file_rating
#determine if we should also send the rating to emby server #determine if we should also send the rating to emby server
if enableexportsongrating: if enableexportsongrating:
if emby_rating == 1 and file_rating == 2: if plex_rating == 1 and file_rating == 2:
emby_rating = 2 plex_rating = 2
if emby_rating == 3 and file_rating == 4: if plex_rating == 3 and file_rating == 4:
emby_rating = 4 plex_rating = 4
if emby_rating != file_rating: if plex_rating != file_rating:
updateEmbyRating = True updateEmbyRating = True
elif enableexportsongrating: elif enableexportsongrating:
#set the initial rating to emby value #set the initial rating to emby value
rating = emby_rating rating = plex_rating
if updateFileRating and enableupdatesongrating: if updateFileRating and enableupdatesongrating:
updateRatingToFile(rating, filename) updateRatingToFile(rating, filename)
@ -169,8 +169,8 @@ def getAdditionalSongTags(embyid, emby_rating, API, kodicursor, emby_db, enablei
if updateEmbyRating and enableexportsongrating: if updateEmbyRating and enableexportsongrating:
# sync details to emby server. Translation needed between ID3 rating and emby likes/favourites: # sync details to emby server. Translation needed between ID3 rating and emby likes/favourites:
like, favourite, deletelike = getEmbyRatingFromKodiRating(rating) like, favourite, deletelike = getEmbyRatingFromKodiRating(rating)
utils.window("ignore-update-%s" %embyid, "true") #set temp windows prop to ignore the update from webclient update utils.window("ignore-update-%s" %plexid, "true") #set temp windows prop to ignore the update from webclient update
emby.updateUserRating(embyid, like, favourite, deletelike) emby.updateUserRating(plexid, like, favourite, deletelike)
return (rating, comment, hasEmbeddedCover) return (rating, comment, hasEmbeddedCover)
@ -193,6 +193,7 @@ def getSongTags(file):
if pic.type == 3 and pic.data: if pic.type == 3 and pic.data:
#the file has an embedded cover #the file has an embedded cover
hasEmbeddedCover = True hasEmbeddedCover = True
break
if audio.get("rating"): if audio.get("rating"):
rating = float(audio.get("rating")[0]) rating = float(audio.get("rating")[0])
#flac rating is 0-100 and needs to be converted to 0-5 range #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] else: filepart = file.split("\\")[-1]
tempfile = "special://temp/"+filepart tempfile = "special://temp/"+filepart
xbmcvfs.copy(file, tempfile) 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)) 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.artwork = artwork.Artwork()
self.emby = embyserver.Read_EmbyServer() 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): def play(self, itemid, dbid=None):
log = self.logMsg
window = utils.window window = utils.window
settings = utils.settings settings = utils.settings
@ -56,7 +58,7 @@ class PlaybackUtils():
listitem = xbmcgui.ListItem() listitem = xbmcgui.ListItem()
playutils = putils.PlayUtils(item[0]) playutils = putils.PlayUtils(item[0])
log("Play called.", 1) self.logMsg("Play called.", 1)
playurl = playutils.getPlayUrl() playurl = playutils.getPlayUrl()
if not playurl: if not playurl:
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem) return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem)
@ -72,18 +74,18 @@ class PlaybackUtils():
xml = downloadutils.DownloadUtils().downloadUrl( xml = downloadutils.DownloadUtils().downloadUrl(
'{server}%s' % item[0][0][0].attrib.get('key')) '{server}%s' % item[0][0][0].attrib.get('key'))
if xml in (None, 401): if xml in (None, 401):
log('Could not download %s' self.logMsg('Could not download %s'
% item[0][0][0].attrib.get('key'), -1) % item[0][0][0].attrib.get('key'), -1)
return xbmcplugin.setResolvedUrl( return xbmcplugin.setResolvedUrl(
int(sys.argv[1]), False, listitem) 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') window('emby_%s.playmethod' % playurl, value='DirectStream')
playmethod = window('emby_%s.playmethod' % playurl) playmethod = window('emby_%s.playmethod' % playurl)
if playmethod == "Transcode": if playmethod == "Transcode":
window('emby_%s.playmethod' % playurl, clear=True) window('emby_%s.playmethod' % playurl, clear=True)
playurl = playutils.audioSubsPref( playurl = utils.tryEncode(playutils.audioSubsPref(
listitem, playurl.decode('utf-8')).encode('utf-8') listitem, utils.tryDecode(playurl)))
window('emby_%s.playmethod' % playurl, "Transcode") window('emby_%s.playmethod' % playurl, "Transcode")
listitem.setPath(playurl) listitem.setPath(playurl)
self.setProperties(playurl, listitem) self.setProperties(playurl, listitem)
@ -92,18 +94,18 @@ class PlaybackUtils():
############### ORGANIZE CURRENT PLAYLIST ################ ############### ORGANIZE CURRENT PLAYLIST ################
homeScreen = xbmc.getCondVisibility('Window.IsActive(home)') homeScreen = xbmc.getCondVisibility('Window.IsActive(home)')
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) playlist = self.pl.playlist
startPos = max(playlist.getposition(), 0) # Can return -1 startPos = max(playlist.getposition(), 0) # Can return -1
sizePlaylist = playlist.size() sizePlaylist = playlist.size()
self.currentPosition = startPos self.currentPosition = startPos
propertiesPlayback = window('emby_playbackProps') == "true" propertiesPlayback = window('plex_playbackProps') == "true"
introsPlaylist = False introsPlaylist = False
dummyPlaylist = False dummyPlaylist = False
log("Playlist start position: %s" % startPos, 1) self.logMsg("Playlist start position: %s" % startPos, 2)
log("Playlist plugin position: %s" % self.currentPosition, 1) self.logMsg("Playlist plugin position: %s" % self.currentPosition, 2)
log("Playlist size: %s" % sizePlaylist, 1) self.logMsg("Playlist size: %s" % sizePlaylist, 2)
############### RESUME POINT ################ ############### RESUME POINT ################
@ -113,12 +115,12 @@ class PlaybackUtils():
# Otherwise we get a loop. # Otherwise we get a loop.
if not propertiesPlayback: if not propertiesPlayback:
window('emby_playbackProps', value="true") window('plex_playbackProps', value="true")
log("Setting up properties in playlist.", 1) self.logMsg("Setting up properties in playlist.", 1)
if (not homeScreen and not seektime and if (not homeScreen and not seektime and
window('emby_customPlaylist') != "true"): window('plex_customplaylist') != "true"):
log("Adding dummy file to playlist.", 2) self.logMsg("Adding dummy file to playlist.", 2)
dummyPlaylist = True dummyPlaylist = True
playlist.add(playurl, listitem, index=startPos) playlist.add(playurl, listitem, index=startPos)
# Remove the original item from playlist # Remove the original item from playlist
@ -145,7 +147,7 @@ class PlaybackUtils():
if homeScreen and not seektime and not sizePlaylist: if homeScreen and not seektime and not sizePlaylist:
# Extend our current playlist with the actual item to play # Extend our current playlist with the actual item to play
# only if there's no playlist first # 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( self.pl.addtoPlaylist(
dbid, dbid,
PF.GetKodiTypeFromPlex(API.getType())) PF.GetKodiTypeFromPlex(API.getType()))
@ -166,7 +168,7 @@ class PlaybackUtils():
additionalListItem = xbmcgui.ListItem() additionalListItem = xbmcgui.ListItem()
additionalPlayurl = playutils.getPlayUrl( additionalPlayurl = playutils.getPlayUrl(
partNumber=counter) partNumber=counter)
log("Adding additional part: %s" % counter, 1) self.logMsg("Adding additional part: %s" % counter, 1)
self.setProperties(additionalPlayurl, additionalListItem) self.setProperties(additionalPlayurl, additionalListItem)
self.setArtwork(additionalListItem) self.setArtwork(additionalListItem)
@ -181,14 +183,14 @@ class PlaybackUtils():
if dummyPlaylist: if dummyPlaylist:
# Added a dummy file to the playlist, # Added a dummy file to the playlist,
# because the first item is going to fail automatically. # 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) return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, listitem)
# We just skipped adding properties. Reset flag for next time. # We just skipped adding properties. Reset flag for next time.
elif propertiesPlayback: elif propertiesPlayback:
log("Resetting properties playback flag.", 2) self.logMsg("Resetting properties playback flag.", 2)
window('emby_playbackProps', clear=True) window('plex_playbackProps', clear=True)
#self.pl.verifyPlaylist() #self.pl.verifyPlaylist()
########## SETUP MAIN ITEM ########## ########## SETUP MAIN ITEM ##########
@ -196,8 +198,8 @@ class PlaybackUtils():
# For transcoding only, ask for audio/subs pref # For transcoding only, ask for audio/subs pref
if window('emby_%s.playmethod' % playurl) == "Transcode": if window('emby_%s.playmethod' % playurl) == "Transcode":
window('emby_%s.playmethod' % playurl, clear=True) window('emby_%s.playmethod' % playurl, clear=True)
playurl = playutils.audioSubsPref( playurl = utils.tryEncode(playutils.audioSubsPref(
listitem, playurl.decode('utf-8')).encode('utf-8') listitem, utils.tryDecode(playurl)))
window('emby_%s.playmethod' % playurl, value="Transcode") window('emby_%s.playmethod' % playurl, value="Transcode")
listitem.setPath(playurl) listitem.setPath(playurl)
@ -205,19 +207,19 @@ class PlaybackUtils():
############### PLAYBACK ################ ############### PLAYBACK ################
if homeScreen and seektime and window('emby_customPlaylist') != "true": if homeScreen and seektime and window('plex_customplaylist') != "true":
log("Play as a widget item.", 1) self.logMsg("Play as a widget item.", 1)
API.CreateListItemFromPlexItem(listitem) API.CreateListItemFromPlexItem(listitem)
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, 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)): (homeScreen and not sizePlaylist)):
# Playlist was created just now, play it. # Playlist was created just now, play it.
log("Play playlist.", 1) self.logMsg("Play playlist.", 1)
xbmc.Player().play(playlist, startpos=startPos) xbmc.Player().play(playlist, startpos=startPos)
else: else:
log("Play as a regular item.", 1) self.logMsg("Play as a regular item.", 1)
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
def AddTrailers(self, xml): def AddTrailers(self, xml):
@ -268,17 +270,13 @@ class PlaybackUtils():
# Set all properties necessary for plugin path playback # Set all properties necessary for plugin path playback
itemid = self.API.getRatingKey() itemid = self.API.getRatingKey()
itemtype = self.API.getType() itemtype = self.API.getType()
resume, runtime = self.API.getRuntime() userdata = self.API.getUserData()
embyitem = "emby_%s" % playurl 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.type' % embyitem, value=itemtype)
window('%s.itemid' % embyitem, value=itemid) window('%s.itemid' % embyitem, value=itemid)
window('%s.playcount' % embyitem, value=str(userdata['PlayCount']))
# 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())
if itemtype == "episode": if itemtype == "episode":
window('%s.refreshid' % embyitem, window('%s.refreshid' % embyitem,

View file

@ -10,8 +10,8 @@ import xbmcgui
import utils import utils
import clientinfo import clientinfo
import downloadutils import downloadutils
import embydb_functions as embydb
from urllib import urlencode import kodidb_functions as kodidb
############################################################################### ###############################################################################
@ -32,17 +32,10 @@ class Player(xbmc.Player):
self.clientInfo = clientinfo.ClientInfo() self.clientInfo = clientinfo.ClientInfo()
self.doUtils = downloadutils.DownloadUtils().downloadUrl 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.logMsg("Started playback monitor.", 2)
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)
def GetPlayStats(self): def GetPlayStats(self):
return self.playStats return self.playStats
@ -51,15 +44,13 @@ class Player(xbmc.Player):
""" """
Window values need to have been set in Kodimonitor.py Window values need to have been set in Kodimonitor.py
""" """
log = self.logMsg
window = utils.window window = utils.window
# Will be called when xbmc starts playing a file # Will be called when xbmc starts playing a file
xbmcplayer = self.xbmcplayer
self.stopAll() self.stopAll()
# Get current file (in utf-8!) # Get current file (in utf-8!)
try: try:
currentFile = xbmcplayer.getPlayingFile() currentFile = self.getPlayingFile()
xbmc.sleep(300) xbmc.sleep(300)
except: except:
currentFile = "" currentFile = ""
@ -67,53 +58,65 @@ class Player(xbmc.Player):
while not currentFile: while not currentFile:
xbmc.sleep(100) xbmc.sleep(100)
try: try:
currentFile = xbmcplayer.getPlayingFile() currentFile = self.getPlayingFile()
except: except:
pass pass
if count == 20: if count == 20:
log("Cancelling playback report...", 1) self.logMsg("Cancelling playback report...", 1)
break break
else: else:
count += 1 count += 1
if not currentFile: 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 return
# Save currentFile for cleanup later and for references # Save currentFile for cleanup later and for references
self.currentFile = currentFile 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 # We may need to wait for info to be set in kodi monitor
itemId = window("emby_%s.itemid" % currentFile) itemId = window("emby_%s.itemid" % currentFile)
count = 0 count = 0
while not itemId: while not itemId:
xbmc.sleep(200) xbmc.sleep(200)
itemId = window("emby_%s.itemid" % currentFile) itemId = window("emby_%s.itemid" % currentFile)
# try 20 times or about 10 seconds if count == 5:
if count == 20: self.logMsg("Could not find itemId, cancelling playback "
log("Could not find itemId, cancelling playback report...", -1) "report!", -1)
return return
count += 1 count += 1
log("ONPLAYBACK_STARTED: %s itemid: %s" self.logMsg("ONPLAYBACK_STARTED: %s itemid: %s"
% (currentFile.decode('utf-8'), itemId), 0) % (utils.tryDecode(currentFile), itemId), 0)
embyitem = "emby_%s" % currentFile embyitem = "emby_%s" % currentFile
runtime = window("%s.runtime" % embyitem) runtime = window("%s.runtime" % embyitem)
refresh_id = window("%s.refreshid" % embyitem) refresh_id = window("%s.refreshid" % embyitem)
playMethod = window("%s.playmethod" % embyitem) playMethod = window("%s.playmethod" % embyitem)
itemType = window("%s.type" % embyitem) itemType = window("%s.type" % embyitem)
try:
playcount = int(window("%s.playcount" % embyitem))
except ValueError:
playcount = 0
window('emby_skipWatched%s' % itemId, value="true") 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') customseek = window('plex_customplaylist.seektime')
if (window('emby_customPlaylist') == "true" and customseek): if customseek:
# Start at, when using custom playlist (play to Kodi from webclient) # Start at, when using custom playlist (play to Kodi from webclient)
log("Seeking to: %s" % customseek, 1) self.logMsg("Seeking to: %s" % customseek, 1)
xbmcplayer.seekTime(int(customseek)) try:
window('emby_customPlaylist.seektime', clear=True) 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 # Get playback volume
volume_query = { volume_query = {
@ -193,7 +196,8 @@ class Player(xbmc.Player):
if mapping: # Set in playbackutils.py 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) externalIndex = json.loads(mapping)
if externalIndex.get(str(indexSubs)): if externalIndex.get(str(indexSubs)):
@ -218,18 +222,24 @@ class Player(xbmc.Player):
try: try:
runtime = int(runtime) runtime = int(runtime)
except ValueError: except ValueError:
runtime = xbmcplayer.getTotalTime() try:
log("Runtime is missing, Kodi runtime: %s" % runtime, 1) 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') with embydb.GetEmbyDB() as emby_db:
playQueueID = window('playQueueID') emby_dbitem = emby_db.getItem_byId(itemId)
playQueueItemID = window('plex_%s.playQueueItemID' % currentFile) 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 # Save data map for updates and position calls
data = { data = {
'playQueueVersion': playQueueVersion, 'runtime': runtime,
'playQueueID': playQueueID,
'playQueueItemID': playQueueItemID,
'runtime': runtime * 1000,
'item_id': itemId, 'item_id': itemId,
'refresh_id': refresh_id, 'refresh_id': refresh_id,
'currentfile': currentFile, 'currentfile': currentFile,
@ -237,11 +247,14 @@ class Player(xbmc.Player):
'SubtitleStreamIndex': postdata['SubtitleStreamIndex'], 'SubtitleStreamIndex': postdata['SubtitleStreamIndex'],
'playmethod': playMethod, 'playmethod': playMethod,
'Type': itemType, 'Type': itemType,
'currentPosition': int(seekTime) 'currentPosition': int(seekTime),
'fileid': fileid,
'itemType': itemType,
'playcount': playcount
} }
self.played_info[currentFile] = data self.played_info[currentFile] = data
log("ADDING_FILE: %s" % self.played_info, 1) self.logMsg("ADDING_FILE: %s" % data, 1)
# log some playback stats # log some playback stats
'''if(itemType != None): '''if(itemType != None):
@ -258,199 +271,48 @@ class Player(xbmc.Player):
else: else:
self.playStats[playMethod] = 1''' 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): def onPlayBackPaused(self):
currentFile = self.currentFile 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): if self.played_info.get(currentFile):
self.played_info[currentFile]['paused'] = True self.played_info[currentFile]['paused'] = True
self.reportPlayback()
def onPlayBackResumed(self): def onPlayBackResumed(self):
currentFile = self.currentFile 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): if self.played_info.get(currentFile):
self.played_info[currentFile]['paused'] = False self.played_info[currentFile]['paused'] = False
self.reportPlayback()
def onPlayBackSeek(self, time, seekOffset): def onPlayBackSeek(self, time, seekOffset):
# Make position when seeking a bit more accurate # Make position when seeking a bit more accurate
currentFile = self.currentFile 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): 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.played_info[currentFile]['currentPosition'] = position
self.reportPlayback()
def onPlayBackStopped(self): def onPlayBackStopped(self):
# Will be called when user stops xbmc playing a file # Will be called when user stops xbmc playing a file
log = self.logMsg
window = utils.window window = utils.window
log("ONPLAYBACK_STOPPED", 1) self.logMsg("ONPLAYBACK_STOPPED", 1)
self.stopAll() self.stopAll()
window('Plex_currently_playing_itemid', clear=True) window('Plex_currently_playing_itemid', clear=True)
window('emby_customPlaylist', clear=True) window('plex_customplaylist', clear=True)
window('emby_customPlaylist.seektime', clear=True) window('plex_customplaylist.seektime', clear=True)
window('emby_customPlaylist.seektime', clear=True) window('plex_customplaylist.seektime', clear=True)
log("Clear playlist properties.", 1) self.logMsg("Clear playlist properties.", 1)
def onPlayBackEnded(self): def onPlayBackEnded(self):
# Will be called when xbmc stops playing a file, because the file ended # Will be called when xbmc stops playing a file, because the file ended
@ -459,31 +321,28 @@ class Player(xbmc.Player):
def stopAll(self): def stopAll(self):
log = self.logMsg
lang = utils.language lang = utils.language
settings = utils.settings settings = utils.settings
doUtils = self.doUtils
if not self.played_info: if not self.played_info:
return return
log("Played_information: %s" % self.played_info, 1) self.logMsg("Played_information: %s" % self.played_info, 1)
# Process each items # Process each items
for item in self.played_info: for item in self.played_info:
data = self.played_info.get(item) data = self.played_info.get(item)
if data: if data:
log("Item path: %s" % item, 2) self.logMsg("Item path: %s" % item, 2)
log("Item data: %s" % data, 2) self.logMsg("Item data: %s" % data, 2)
runtime = data['runtime'] runtime = data['runtime']
currentPosition = data['currentPosition'] currentPosition = data['currentPosition']
itemid = data['item_id'] itemid = data['item_id']
refresh_id = data['refresh_id'] refresh_id = data['refresh_id']
currentFile = data['currentfile'] currentFile = data['currentfile']
type = data['Type'] media_type = data['Type']
playMethod = data['playmethod'] playMethod = data['playmethod']
# Prevent manually mark as watched in Kodi monitor # Prevent manually mark as watched in Kodi monitor
@ -491,21 +350,31 @@ class Player(xbmc.Player):
if currentPosition and runtime: if currentPosition and runtime:
try: try:
percentComplete = currentPosition / int(runtime) percentComplete = float(currentPosition) / float(runtime)
except ZeroDivisionError: except ZeroDivisionError:
# Runtime is 0. # Runtime is 0.
percentComplete = 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. # Send the delete action to the server.
offerDelete = False offerDelete = False
if type == "Episode" and settings('deleteTV') == "true": if media_type == "Episode" and settings('deleteTV') == "true":
offerDelete = True offerDelete = True
elif type == "Movie" and settings('deleteMovies') == "true": elif media_type == "Movie" and settings('deleteMovies') == "true":
offerDelete = True offerDelete = True
if settings('offerDelete') != "true": if settings('offerDelete') != "true":
@ -514,19 +383,18 @@ class Player(xbmc.Player):
# Plex: never delete # Plex: never delete
offerDelete = False offerDelete = False
if percentComplete >= markPlayedAt and offerDelete: if percentComplete >= markPlayed and offerDelete:
resp = xbmcgui.Dialog().yesno( resp = xbmcgui.Dialog().yesno(
lang(30091), lang(30091),
lang(33015), lang(33015),
autoclose=120000) autoclose=120000)
if not resp: if not resp:
log("User skipped deletion.", 1) self.logMsg("User skipped deletion.", 1)
continue continue
url = "{server}/emby/Items/%s?format=json" % itemid url = "{server}/emby/Items/%s?format=json" % itemid
log("Deleting request: %s" % itemid, 1) self.logMsg("Deleting request: %s" % itemid, 1)
doUtils(url, type="DELETE") self.doUtils(url, action_type="DELETE")
self.stopPlayback(data)
# Clean the WINDOW properties # Clean the WINDOW properties
for filename in self.played_info: for filename in self.played_info:
@ -536,35 +404,18 @@ class Player(xbmc.Player):
'emby_%s.refreshid' % filename, 'emby_%s.refreshid' % filename,
'emby_%s.playmethod' % filename, 'emby_%s.playmethod' % filename,
'emby_%s.type' % filename, 'emby_%s.type' % filename,
'plex_%s.playQueueItemID' % filename, 'emby_%s.runtime' % filename,
'plex_%s.playlistPosition' % filename, 'emby_%s.playcount' % filename,
'plex_%s.guid' % filename 'plex_%s.playlistPosition' % filename
) )
for item in cleanup: for item in cleanup:
utils.window(item, clear=True) utils.window(item, clear=True)
# Stop transcoding # Stop transcoding
if playMethod == "Transcode": if playMethod == "Transcode":
log("Transcoding for %s terminating" % itemid, 1) self.logMsg("Transcoding for %s terminating" % itemid, 1)
doUtils( self.doUtils(
"{server}/video/:/transcode/universal/stop", "{server}/video/:/transcode/universal/stop",
parameters={'session': self.clientInfo.getDeviceId()}) parameters={'session': self.clientInfo.getDeviceId()})
self.played_info.clear() 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 import json
from urllib import urlencode from urllib import urlencode
from threading import Lock
from functools import wraps
import xbmc import xbmc
import xbmcgui
import embydb_functions as embydb import embydb_functions as embydb
import read_embyserver as embyserver
import utils import utils
import playbackutils
import PlexFunctions import PlexFunctions
import PlexAPI 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 @utils.logging
class Playlist(): 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.userid = utils.window('currUserId')
self.server = utils.window('pms_server') 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): @lockMethod.decorate
log = self.logMsg def Typus(self, value=None):
window = utils.window if value:
self.typus = value
else:
return self.typus
embyconn = utils.kodiSQL('emby') @lockMethod.decorate
embycursor = embyconn.cursor() def PlayQueueVersion(self, value=None):
emby_db = embydb.Embydb_Functions(embycursor) if value:
self.playQueueVersion = value
else:
return self.playQueueVersion
player = xbmc.Player() @lockMethod.decorate
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) def QueueId(self, value=None):
playlist.clear() if value:
self.queueId = value
else:
return self.queueId
log("---*** PLAY ALL ***---", 1) @lockMethod.decorate
log("Items: %s and start at: %s" % (itemids, startat), 1) def Guid(self, value=None):
if value:
self.guid = value
else:
return self.guid
started = False @lockMethod.decorate
window('emby_customplaylist', value="true") def clear(self):
"""
if startat != 0: Empties current Kodi playlist and associated variables
# Seek to the starting position """
window('emby_customplaylist.seektime', str(startat)) 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: 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) embydb_item = emby_db.getItem_byId(itemid)
try: try:
dbid = embydb_item[0]
mediatype = embydb_item[4] mediatype = embydb_item[4]
except TypeError: except TypeError:
# Item is not found in our database, add item manually self.logMsg('Couldnt find item %s in Kodi db'
log("Item was not found in the database, manually adding item.", 1) % itemid, 1)
item = PlexFunctions.GetPlexMetadata(itemid) item = PlexFunctions.GetPlexMetadata(itemid)
if item is None or item == 401: if item in (None, 401):
log('Could not download itemid %s' % itemid, -1) 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: 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: else:
# Add to playlist # 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 startPlayer is True and len(self.playlist) > 0:
if startpos is not None:
if not started: self.player.play(self.playlist, startpos=startpos)
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)
else: else:
# Add to playlist self.logMsg('Never received a starting item for playlist, '
self.addtoPlaylist(dbid, mediatype) '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() startitem: tuple (typus, id), where typus is either
embycursor.close() 'playQueueItemID' or 'plexId' and id is the corresponding
return playlist 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): 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 = { pl = {
'jsonrpc': "2.0", 'jsonrpc': "2.0",
'id': 1, 'id': 1,
'method': "Playlist.Add", 'method': "Playlist.Add",
'params': { 'params': {
'playlistid': self.playlistId
'playlistid': 1
} }
} }
if dbid is not None: if dbid is not None:
pl['params']['item'] = {'%sid' % mediatype: int(dbid)} pl['params']['item'] = {'%sid' % utils.tryEncode(mediatype):
int(dbid)}
else: else:
pl['params']['item'] = {'file': url} pl['params']['item'] = {'file': url}
self.logMsg(xbmc.executeJSONRPC(json.dumps(pl)), 2)
result = xbmc.executeJSONRPC(json.dumps(pl)) def _addtoPlaylist_xbmc(self, item):
self.logMsg(result, 2) API = PlexAPI.API(item)
def addtoPlaylist_xbmc(self, playlist, item):
path = "plugin://plugin.video.plexkodiconnect.movies/"
params = { params = {
'mode': "play", 'mode': "play",
'dbid': 999999999 'dbid': 999999999,
'id': API.getRatingKey(),
'filename': API.getKey()
} }
API = PlexAPI.API(item[0]) playurl = "plugin://plugin.video.plexkodiconnect.movies/?%s" \
params['id'] = API.getRatingKey() % urlencode(params)
params['filename'] = API.getKey()
playurl = path + '?' + 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): def insertintoPlaylist(self, position, dbid=None, mediatype=None, url=None):
pl = { pl = {
@ -151,20 +294,23 @@ class Playlist():
'method': "Playlist.Insert", 'method': "Playlist.Insert",
'params': { 'params': {
'playlistid': 1, 'playlistid': self.playlistId,
'position': position 'position': position
} }
} }
if dbid is not None: if dbid is not None:
pl['params']['item'] = {'%sid' % mediatype: int(dbid)} pl['params']['item'] = {'%sid' % utils.tryEncode(mediatype):
int(dbid)}
else: else:
pl['params']['item'] = {'file': url} pl['params']['item'] = {'file': url}
result = xbmc.executeJSONRPC(json.dumps(pl)) self.logMsg(xbmc.executeJSONRPC(json.dumps(pl)), 2)
self.logMsg(result, 2)
@lockMethod.decorate
def verifyPlaylist(self): def verifyPlaylist(self):
self._verifyPlaylist()
def _verifyPlaylist(self):
pl = { pl = {
'jsonrpc': "2.0", 'jsonrpc': "2.0",
@ -172,13 +318,13 @@ class Playlist():
'method': "Playlist.GetItems", 'method': "Playlist.GetItems",
'params': { 'params': {
'playlistid': 1, 'playlistid': self.playlistId,
'properties': ['title', 'file'] 'properties': ['title', 'file']
} }
} }
result = xbmc.executeJSONRPC(json.dumps(pl)) self.logMsg(xbmc.executeJSONRPC(json.dumps(pl)), 2)
self.logMsg(result, 2)
@lockMethod.decorate
def removefromPlaylist(self, position): def removefromPlaylist(self, position):
pl = { pl = {
@ -188,9 +334,8 @@ class Playlist():
'method': "Playlist.Remove", 'method': "Playlist.Remove",
'params': { 'params': {
'playlistid': 1, 'playlistid': self.playlistId,
'position': position 'position': position
} }
} }
result = xbmc.executeJSONRPC(json.dumps(pl)) self.logMsg(xbmc.executeJSONRPC(json.dumps(pl)), 2)
self.logMsg(result, 2)

View file

@ -37,100 +37,82 @@ class PlayUtils():
playurl is utf-8 encoded! playurl is utf-8 encoded!
""" """
log = self.logMsg
window = utils.window
self.API.setPartNumber(partNumber) self.API.setPartNumber(partNumber)
playurl = self.isDirectPlay() playurl = self.isDirectPlay()
if playurl: if playurl is not None:
log("File is direct playing.", 1) self.logMsg("File is direct playing.", 1)
playurl = playurl.encode('utf-8') playurl = utils.tryEncode(playurl)
# Set playmethod property # Set playmethod property
window('emby_%s.playmethod' % playurl, "DirectPlay") utils.window('emby_%s.playmethod' % playurl, "DirectPlay")
elif self.isDirectStream(): elif self.isDirectStream():
self.logMsg("File is direct streaming.", 1) 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 # Set playmethod property
utils.window('emby_%s.playmethod' % playurl, "DirectStream") utils.window('emby_%s.playmethod' % playurl, "DirectStream")
elif self.isTranscoding(): else:
log("File is transcoding.", 1) self.logMsg("File is transcoding.", 1)
quality = { playurl = utils.tryEncode(self.API.getTranscodeVideoPath(
'maxVideoBitrate': self.getBitrate(),
'videoResolution': self.getResolution(),
'videoQuality': '100'
}
playurl = self.API.getTranscodeVideoPath(
'Transcode', 'Transcode',
quality=quality).encode('utf-8') quality={
'maxVideoBitrate': self.getBitrate(),
'videoResolution': self.getResolution(),
'videoQuality': '100'
}))
# Set playmethod property # 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 return playurl
def httpPlay(self): def httpPlay(self):
# Audio, Video, Photo # Audio, Video, Photo
item = self.item
server = self.server
itemid = item['Id'] itemid = self.item['Id']
mediatype = item['MediaType'] mediatype = self.item['MediaType']
if mediatype == "Audio": if mediatype == "Audio":
playurl = "%s/emby/Audio/%s/stream" % (server, itemid) playurl = "%s/emby/Audio/%s/stream" % (self.server, itemid)
else: else:
playurl = "%s/emby/Videos/%s/stream?static=true" % (server, itemid) playurl = "%s/emby/Videos/%s/stream?static=true" % (self.server, itemid)
return playurl return playurl
def isDirectPlay(self): 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 # True for e.g. plex.tv watch later
if self.API.shouldStream() is True: if self.API.shouldStream() is True:
self.logMsg("Plex item optimized for direct streaming", 1) self.logMsg("Plex item optimized for direct streaming", 1)
return False return
# set to either 'Direct Stream=1' or 'Transcode=2' # set to either 'Direct Stream=1' or 'Transcode=2'
# and NOT to 'Direct Play=0'
if utils.settings('playType') != "0": if utils.settings('playType') != "0":
# User forcing to play via HTTP # User forcing to play via HTTP
self.logMsg("User chose to not direct play", 1) self.logMsg("User chose to not direct play", 1)
return False return
if self.mustTranscode():
if self.h265enabled(): return
return False return self.API.validatePlayurl(self.API.getFilePath(),
path = self.API.validatePlayurl(self.API.getFilePath(),
self.API.getType(), self.API.getType(),
forceCheck=True) 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): def directPlay(self):
item = self.item
try: try:
playurl = item['MediaSources'][0]['Path'] playurl = self.item['MediaSources'][0]['Path']
except (IndexError, KeyError): except (IndexError, KeyError):
playurl = item['Path'] playurl = self.item['Path']
if item.get('VideoType'): if self.item.get('VideoType'):
# Specific format modification # Specific format modification
type = item['VideoType'] if self.item['VideoType'] == "Dvd":
if type == "Dvd":
playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl
elif type == "BluRay": elif self.item['VideoType'] == "BluRay":
playurl = "%s/BDMV/index.bdmv" % playurl playurl = "%s/BDMV/index.bdmv" % playurl
# Assign network protocol # Assign network protocol
@ -146,59 +128,71 @@ class PlayUtils():
def fileExists(self): def fileExists(self):
log = self.logMsg
if 'Path' not in self.item: if 'Path' not in self.item:
# File has no path defined in server # File has no path defined in server
return False return False
# Convert path to direct play # Convert path to direct play
path = self.directPlay() path = self.directPlay()
log("Verifying path: %s" % path, 1) self.logMsg("Verifying path: %s" % path, 1)
if xbmcvfs.exists(path): if xbmcvfs.exists(path):
log("Path exists.", 1) self.logMsg("Path exists.", 1)
return True return True
elif ":" not in path: 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 return True
else: else:
log("Failed to find file.", 1) self.logMsg("Failed to find file.", 1)
return False 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() videoCodec = self.API.getVideoCodec()
self.logMsg("videoCodec: %s" % videoCodec, 2) self.logMsg("videoCodec: %s" % videoCodec, 2)
codec = videoCodec['videocodec'] if (utils.settings('transcodeHi10P') == 'true' and
resolution = videoCodec['resolution'] videoCodec['bitDepth'] == '10'):
h265 = self.getH265() self.logMsg('Option to transcode 10bit video content enabled.', 1)
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)
return True 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 return False
def isDirectStream(self): def isDirectStream(self):
# Never transcode Music
if self.API.getType() == 'track':
return True
# set to 'Transcode=2' # set to 'Transcode=2'
if utils.settings('playType') == "2": if utils.settings('playType') == "2":
# User forcing to play via HTTP # User forcing to play via HTTP
self.logMsg("User chose to transcode", 1) self.logMsg("User chose to transcode", 1)
return False return False
if self.h265enabled(): if self.mustTranscode():
return False return False
# Verify the bitrate # Verify the bitrate
if not self.isNetworkSufficient(): if not self.isNetworkSufficient():
@ -207,25 +201,6 @@ class PlayUtils():
return False return False
return True 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): def isNetworkSufficient(self):
""" """
Returns True if the network is sufficient (set in file settings) Returns True if the network is sufficient (set in file settings)
@ -243,38 +218,6 @@ class PlayUtils():
return False return False
return True 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): def getBitrate(self):
# get the addon video quality # get the addon video quality
videoQuality = utils.settings('transcoderVideoQualities') videoQuality = utils.settings('transcoderVideoQualities')
@ -295,14 +238,19 @@ class PlayUtils():
return bitrate.get(videoQuality, 2147483) return bitrate.get(videoQuality, 2147483)
def getH265(self): 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 = { H265 = {
'0': None, '0': 9999999,
'1': 480, '1': 480,
'2': 720, '2': 720,
'3': 1080 '3': 1080
} }
return H265.get(chosen, None) return H265[utils.settings('transcodeH265')]
def getResolution(self): def getResolution(self):
chosen = utils.settings('transcoderVideoQualities') chosen = utils.settings('transcoderVideoQualities')
@ -365,7 +313,7 @@ class PlayUtils():
#audioStreamsChannelsList[audioNum] = stream.attrib['channels'] #audioStreamsChannelsList[audioNum] = stream.attrib['channels']
audioStreamsList.append(index) audioStreamsList.append(index)
audioStreams.append(track.encode('utf-8')) audioStreams.append(utils.tryEncode(track))
audioNum += 1 audioNum += 1
# Subtitles # Subtitles
@ -389,7 +337,7 @@ class PlayUtils():
downloadableStreams.append(index) downloadableStreams.append(index)
subtitleStreamsList.append(index) subtitleStreamsList.append(index)
subtitleStreams.append(track.encode('utf-8')) subtitleStreams.append(utils.tryEncode(track))
subNum += 1 subNum += 1
if audioNum > 1: if audioNum > 1:
@ -421,7 +369,7 @@ class PlayUtils():
% (self.server, selectSubsIndex) % (self.server, selectSubsIndex)
url = self.API.addPlexHeadersToUrl(url) url = self.API.addPlexHeadersToUrl(url)
self.logMsg("Downloadable sub: %s: %s" % (selectSubsIndex, url), 1) self.logMsg("Downloadable sub: %s: %s" % (selectSubsIndex, url), 1)
listitem.setSubtitles([url.encode('utf-8')]) listitem.setSubtitles([utils.tryEncode(url)])
else: else:
self.logMsg('Need to burn in subtitle %s' % selectSubsIndex, 1) self.logMsg('Need to burn in subtitle %s' % selectSubsIndex, 1)
playurlprefs["subtitleStreamID"] = selectSubsIndex playurlprefs["subtitleStreamID"] = selectSubsIndex

View file

@ -5,6 +5,7 @@ import string
import xbmc import xbmc
from utils import logging from utils import logging
import embydb_functions as embydb
def xbmc_photo(): def xbmc_photo():
@ -58,7 +59,9 @@ def getOKMsg():
def timeToMillis(time): 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): def millisToTime(t):
@ -69,7 +72,10 @@ def millisToTime(t):
seconds = seconds % 60 seconds = seconds % 60
minutes = minutes % 60 minutes = minutes % 60
millis = millis % 1000 millis = millis % 1000
return {'hours':hours,'minutes':minutes,'seconds':seconds,'milliseconds':millis} return {'hours': hours,
'minutes': minutes,
'seconds': seconds,
'milliseconds': millis}
def textFromXml(element): def textFromXml(element):
@ -86,44 +92,70 @@ class jsonClass():
def jsonrpc(self, action, arguments={}): def jsonrpc(self, action, arguments={}):
""" put some JSON together for the JSON-RPC APIv6 """ """ put some JSON together for the JSON-RPC APIv6 """
if action.lower() == "sendkey": 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": elif action.lower() == "ping":
request=json.dumps({ "jsonrpc" : "2.0", request = json.dumps({
"id" : 1 , "jsonrpc": "2.0",
"method" : "JSONRPC.Ping" }) "id": 1,
elif action.lower() == "playmedia": "method": "JSONRPC.Ping"
xbmc.Player().play("plugin://plugin.video.plexkodiconnect/" })
"?mode=companion&arguments=%s"
% arguments)
return True
elif arguments: elif arguments:
request=json.dumps({ "id" : 1, request = json.dumps({
"jsonrpc" : "2.0", "id": 1,
"method" : action, "jsonrpc": "2.0",
"params" : arguments}) "method": action,
"params": arguments})
else: else:
request=json.dumps({ "id" : 1, request = json.dumps({
"jsonrpc" : "2.0", "id": 1,
"method" : action}) "jsonrpc": "2.0",
"method": action
})
result = self.parseJSONRPC(xbmc.executeJSONRPC(request)) result = self.parseJSONRPC(xbmc.executeJSONRPC(request))
if not result and self.settings['webserver_enabled']: if not result and self.settings['webserver_enabled']:
# xbmc.executeJSONRPC appears to fail on the login screen, but going # xbmc.executeJSONRPC appears to fail on the login screen, but
# through the network stack works, so let's try the request again # going through the network stack works, so let's try the request
# again
result = self.parseJSONRPC(self.requestMgr.post( result = self.parseJSONRPC(self.requestMgr.post(
"127.0.0.1", "127.0.0.1",
self.settings['port'], self.settings['port'],
"/jsonrpc", "/jsonrpc",
request, request,
{ 'Content-Type' : 'application/json', {'Content-Type': 'application/json',
'Authorization' : 'Basic ' + string.strip(base64.encodestring(self.settings['user'] + ':' + self.settings['passwd'])) })) 'Authorization': 'Basic %s' % string.strip(
base64.encodestring('%s:%s'
% (self.settings['user'],
self.settings['passwd'])))
}))
return result 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): def getPlexHeaders(self):
h = { h = {
"Content-type": "application/x-www-form-urlencoded", "Content-type": "text/xml",
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
"X-Plex-Version": self.settings['version'], "X-Plex-Version": self.settings['version'],
"X-Plex-Client-Identifier": self.settings['uuid'], "X-Plex-Client-Identifier": self.settings['uuid'],
@ -143,7 +175,7 @@ class jsonClass():
self.logMsg("Empty response from XBMC", 1) self.logMsg("Empty response from XBMC", 1)
return {} return {}
else: else:
parsed=json.loads(jsonraw) parsed = json.loads(jsonraw)
if parsed.get('error', False): if parsed.get('error', False):
self.logMsg("XBMC returned an error: %s" % parsed.get('error'), -1) self.logMsg("XBMC returned an error: %s" % parsed.get('error'), -1)
return parsed.get('result', {}) return parsed.get('result', {})
@ -156,6 +188,27 @@ class jsonClass():
ret[player['type']] = player ret[player['type']] = player
return ret 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): def getPlayerIds(self):
ret = [] ret = []
for player in self.getPlayers().values(): for player in self.getPlayers().values():
@ -178,7 +231,10 @@ class jsonClass():
return players.get(xbmc_photo(), {}).get('playerid', None) return players.get(xbmc_photo(), {}).get('playerid', None)
def getVolume(self): 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)) vol = str(answ.get('volume', 100))
mute = ("0", "1")[answ.get('muted', False)] mute = ("0", "1")[answ.get('muted', False)]
return (vol, mute) return (vol, mute)

View file

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

View file

@ -22,18 +22,15 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
MA 02110-1301, USA. MA 02110-1301, USA.
""" """
__author__ = 'DHJ (hippojay) <plex@h-jay.com>'
import socket import socket
import struct
import threading import threading
import time import time
from xbmc import sleep from xbmc import sleep
import downloadutils import downloadutils
from PlexFunctions import PMSHttpsEnabled from utils import window, logging, settings
from utils import window, logging
@logging @logging
@ -60,7 +57,7 @@ class plexgdm:
self.client_registered = False self.client_registered = False
self.download = downloadutils.DownloadUtils().downloadUrl self.download = downloadutils.DownloadUtils().downloadUrl
def clientDetails(self, settings): def clientDetails(self, options):
self.client_data = ( self.client_data = (
"Content-Type: plex/media-player\r\n" "Content-Type: plex/media-player\r\n"
"Resource-Identifier: %s\r\n" "Resource-Identifier: %s\r\n"
@ -74,13 +71,13 @@ class plexgdm:
"mirror,playqueues\r\n" "mirror,playqueues\r\n"
"Device-Class: HTPC" "Device-Class: HTPC"
) % ( ) % (
settings['uuid'], options['uuid'],
settings['client_name'], options['client_name'],
settings['myport'], options['myport'],
settings['addonName'], options['addonName'],
settings['version'] options['version']
) )
self.client_id = settings['uuid'] self.client_id = options['uuid']
def getClientDetails(self): def getClientDetails(self):
if not self.client_data: if not self.client_data:
@ -215,119 +212,28 @@ class plexgdm:
return self.server_list return self.server_list
def discover(self): 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') currServer = window('pms_server')
if currServer: if not currServer:
currServerProt, currServerIP, currServerPort = \ return
currServer.split(':') currServerProt, currServerIP, currServerPort = \
currServerIP = currServerIP.replace('/', '') currServer.split(':')
for server in discovered_servers: currServerIP = currServerIP.replace('/', '')
if server['server'] == currServerIP: # Currently active server was not discovered via GDM; ADD
break self.server_list = [{
else: 'port': currServerPort,
# Currently active server was not discovered via GDM; ADD 'protocol': currServerProt,
update = { 'class': None,
'port': currServerPort, 'content-type': 'plex/media-server',
'protocol': currServerProt, 'discovery': 'auto',
'class': None, 'master': 1,
'content-type': 'plex/media-server', 'owned': '1',
'discovery': 'auto', 'role': 'master',
'master': 1, 'server': currServerIP,
'owned': '1', 'serverName': window('plex_servername'),
'role': 'master', 'updated': int(time.time()),
'server': currServerIP, 'uuid': window('plex_machineIdentifier'),
'serverName': window('plex_servername'), 'version': 'irrelevant'
'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)
def setInterval(self, interval): def setInterval(self, interval):
self.discovery_interval = interval self.discovery_interval = interval
@ -388,4 +294,5 @@ class plexgdm:
def start_all(self, daemon=False): def start_all(self, daemon=False):
self.start_discovery(daemon) 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 re
import threading import threading
from xbmc import Player
import downloadutils import downloadutils
from utils import window, logging from utils import window, logging
import PlexFunctions as pf import PlexFunctions as pf
@ -11,13 +9,19 @@ from functions import *
@logging @logging
class SubscriptionManager: class SubscriptionManager:
def __init__(self, jsonClass, RequestMgr): def __init__(self, jsonClass, RequestMgr, player, playlist):
self.serverlist = [] self.serverlist = []
self.subscribers = {} self.subscribers = {}
self.info = {} self.info = {}
self.lastkey = "" self.lastkey = ""
self.containerKey = "" self.containerKey = ""
self.lastratingkey = "" self.ratingkey = ""
self.lastplayers = {}
self.lastinfo = {
'video': {},
'audio': {},
'picture': {}
}
self.volume = 0 self.volume = 0
self.mute = '0' self.mute = '0'
self.server = "" self.server = ""
@ -25,7 +29,8 @@ class SubscriptionManager:
self.port = "" self.port = ""
self.playerprops = {} self.playerprops = {}
self.doUtils = downloadutils.DownloadUtils().downloadUrl self.doUtils = downloadutils.DownloadUtils().downloadUrl
self.xbmcplayer = Player() self.xbmcplayer = player
self.playlist = playlist
self.js = jsonClass self.js = jsonClass
self.RequestMgr = RequestMgr self.RequestMgr = RequestMgr
@ -62,7 +67,7 @@ class SubscriptionManager:
msg += self.getTimelineXML(self.js.getVideoPlayerId(players), plex_video()) msg += self.getTimelineXML(self.js.getVideoPlayerId(players), plex_video())
msg += "\r\n</MediaContainer>" msg += "\r\n</MediaContainer>"
return msg return msg
def getTimelineXML(self, playerid, ptype): def getTimelineXML(self, playerid, ptype):
if playerid is not None: if playerid is not None:
info = self.getPlayerProperties(playerid) info = self.getPlayerProperties(playerid)
@ -95,21 +100,22 @@ class SubscriptionManager:
xbmc.sleep(100) xbmc.sleep(100)
count += 1 count += 1
if keyid: if keyid:
self.lastkey = "/library/metadata/%s"%keyid self.lastkey = "/library/metadata/%s" % keyid
self.lastratingkey = keyid self.ratingkey = keyid
ret += ' location="%s"' % (self.mainlocation) ret += ' location="%s"' % self.mainlocation
ret += ' key="%s"' % (self.lastkey) ret += ' key="%s"' % self.lastkey
ret += ' ratingKey="%s"' % (self.lastratingkey) ret += ' ratingKey="%s"' % self.ratingkey
serv = self.getServerByHost(self.server) serv = self.getServerByHost(self.server)
if info.get('playQueueID'): if info.get('playQueueID'):
self.containerKey = "/playQueues/%s" % info.get('playQueueID') self.containerKey = "/playQueues/%s" % info.get('playQueueID')
ret += ' playQueueID="%s"' % info.get('playQueueID') ret += ' playQueueID="%s"' % info.get('playQueueID')
ret += ' playQueueVersion="%s"' % info.get('playQueueVersion') ret += ' playQueueVersion="%s"' % info.get('playQueueVersion')
ret += ' playQueueItemID="%s"' % (info.get('playQueueItemID')) ret += ' playQueueItemID="%s"' % info.get('playQueueItemID')
ret += ' containerKey="%s"' % self.containerKey ret += ' containerKey="%s"' % self.containerKey
ret += ' guid="%s"' % info['guid']
elif keyid: elif keyid:
self.containerKey = self.lastkey self.containerKey = self.lastkey
ret += ' containerKey="%s"' % (self.containerKey) ret += ' containerKey="%s"' % self.containerKey
ret += ' duration="%s"' % info['duration'] ret += ' duration="%s"' % info['duration']
ret += ' seekRange="0-%s"' % info['duration'] ret += ' seekRange="0-%s"' % info['duration']
@ -118,7 +124,6 @@ class SubscriptionManager:
ret += ' protocol="%s"' % serv.get('protocol', "http") ret += ' protocol="%s"' % serv.get('protocol', "http")
ret += ' address="%s"' % serv.get('server', self.server) ret += ' address="%s"' % serv.get('server', self.server)
ret += ' port="%s"' % serv.get('port', self.port) ret += ' port="%s"' % serv.get('port', self.port)
ret += ' guid="%s"' % info['guid']
ret += ' volume="%s"' % info['volume'] ret += ' volume="%s"' % info['volume']
ret += ' shuffle="%s"' % info['shuffle'] ret += ' shuffle="%s"' % info['shuffle']
ret += ' mute="%s"' % self.mute ret += ' mute="%s"' % self.mute
@ -132,13 +137,14 @@ class SubscriptionManager:
def updateCommandID(self, uuid, commandID): def updateCommandID(self, uuid, commandID):
if commandID and self.subscribers.get(uuid, False): if commandID and self.subscribers.get(uuid, False):
self.subscribers[uuid].commandID = int(commandID) self.subscribers[uuid].commandID = int(commandID)
def notify(self, event = False): def notify(self, event=False):
self.cleanup() self.cleanup()
# Don't tell anyone if we don't know a Plex ID and are still playing # 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 # (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 return True
players = self.js.getPlayers() players = self.js.getPlayers()
# fetch the message, subscribers or not, since the server # fetch the message, subscribers or not, since the server
@ -147,35 +153,46 @@ class SubscriptionManager:
if self.subscribers: if self.subscribers:
with threading.RLock(): with threading.RLock():
for sub in self.subscribers.values(): for sub in self.subscribers.values():
sub.send_update(msg, len(players)==0) sub.send_update(msg, len(players) == 0)
self.notifyServer(players) self.notifyServer(players)
self.lastplayers = players
return True 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) def notifyServer(self, players):
url = serv.get('protocol', 'http') + '://' \ for typus, p in players.iteritems():
+ serv.get('server', 'localhost') + ':' \ info = self.playerprops[p.get('playerid')]
+ serv.get('port', '32400') + "/:/timeline" self._sendNotification(info)
self.doUtils(url, type="GET", parameters=params) self.lastinfo[typus] = info
# requests.getwithparams(serv.get('server', 'localhost'), serv.get('port', 32400), "/:/timeline", params, getPlexHeaders(), serv.get('protocol', 'http')) # Cross the one of the list
self.logMsg("sent server notification with state = %s" try:
% params['state'], 2) 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): def controllable(self):
return "volume,shuffle,repeat,audioStream,videoStream,subtitleStream,skipPrevious,skipNext,seekTo,stepBack,stepForward,stop,playPause" return "volume,shuffle,repeat,audioStream,videoStream,subtitleStream,skipPrevious,skipNext,seekTo,stepBack,stepForward,stop,playPause"
@ -205,30 +222,50 @@ class SubscriptionManager:
if sub.age > 30: if sub.age > 30:
sub.cleanup() sub.cleanup()
del self.subscribers[sub.uuid] del self.subscribers[sub.uuid]
def getPlayerProperties(self, playerid): def getPlayerProperties(self, playerid):
info = {}
try: try:
# get info from the player # get info from the player
props = self.js.jsonrpc("Player.GetProperties", {"playerid": playerid, "properties": ["time", "totaltime", "speed", "shuffled", "repeat"]}) props = self.js.jsonrpc(
self.logMsg(self.js.jsonrpc("Player.GetItem", {"playerid": playerid, "properties": ["file", "showlink", "episode", "season"]}), 2) "Player.GetProperties",
info['time'] = timeToMillis(props['time']) {"playerid": playerid,
info['duration'] = timeToMillis(props['totaltime']) "properties": ["time",
info['state'] = ("paused", "playing")[int(props['speed'])] "totaltime",
info['shuffle'] = ("0","1")[props.get('shuffled', False)] "speed",
info['repeat'] = pf.getPlexRepeat(props.get('repeat')) "shuffled",
# New PMS playQueue attributes "repeat"]})
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)
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: except:
info['time'] = 0 import traceback
info['duration'] = 0 self.logMsg("Traceback:\n%s"
info['state'] = "stopped" % traceback.format_exc(), -1)
info['shuffle'] = False info = {
'time': 0,
'duration': 0,
'state': 'stopped',
'shuffle': False,
'repeat': 0
}
# get the volume from the application # get the volume from the application
info['volume'] = self.volume info['volume'] = self.volume
info['mute'] = self.mute info['mute'] = self.mute
@ -283,6 +320,6 @@ class Subscriber:
""" """
response = self.doUtils(url, response = self.doUtils(url,
postBody=msg, postBody=msg,
type="POST") action_type="POST")
if response in [False, None, 401]: if response in [False, None, 401]:
self.subMgr.removeSubscriber(self.uuid) self.subMgr.removeSubscriber(self.uuid)

View file

@ -31,8 +31,7 @@ class Read_EmbyServer():
# This will return the full item # This will return the full item
item = {} item = {}
url = "{server}/emby/Users/{UserId}/Items/%s?format=json" % itemid result = self.doUtils("{server}/emby/Users/{UserId}/Items/%s?format=json" % itemid)
result = self.doUtils(url)
if result: if result:
item = result item = result
@ -45,13 +44,12 @@ class Read_EmbyServer():
itemlists = self.split_list(itemlist, 50) itemlists = self.split_list(itemlist, 50)
for itemlist in itemlists: for itemlist in itemlists:
# Will return basic information # Will return basic information
url = "{server}/emby/Users/{UserId}/Items?&format=json"
params = { params = {
'Ids': ",".join(itemlist), 'Ids': ",".join(itemlist),
'Fields': "Etag" 'Fields': "Etag"
} }
result = self.doUtils(url, parameters=params) result = self.doUtils("{server}/emby/Users/{UserId}/Items?&format=json", parameters=params)
if result: if result:
items.extend(result['Items']) items.extend(result['Items'])
@ -64,7 +62,6 @@ class Read_EmbyServer():
itemlists = self.split_list(itemlist, 50) itemlists = self.split_list(itemlist, 50)
for itemlist in itemlists: for itemlist in itemlists:
url = "{server}/emby/Users/{UserId}/Items?format=json"
params = { params = {
"Ids": ",".join(itemlist), "Ids": ",".join(itemlist),
@ -75,25 +72,22 @@ class Read_EmbyServer():
"Metascore,AirTime,DateCreated,MediaStreams,People,Overview," "Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
"CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
"Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers," "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: if result:
items.extend(result['Items']) items.extend(result['Items'])
return items return items
def getView_embyId(self, itemid): def getView_plexid(self, itemid):
# Returns ancestors using embyId # Returns ancestors using plexid
viewId = None 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 view['Type'] == "CollectionFolder":
if viewtype == "CollectionFolder":
# Found view # Found view
viewId = view['Id'] viewId = view['Id']
@ -120,8 +114,6 @@ class Read_EmbyServer():
return [viewName, viewId, mediatype] return [viewName, viewId, mediatype]
def getFilteredSection(self, parentid, itemtype=None, sortby="SortName", recursive=True, limit=None, sortorder="Ascending", filter=""): 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 = { params = {
'ParentId': parentid, 'ParentId': parentid,
@ -140,11 +132,9 @@ class Read_EmbyServer():
"CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
"Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers") "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): def getTvChannels(self):
doUtils = self.doUtils
url = "{server}/emby/LiveTv/Channels/?userid={UserId}&format=json"
params = { params = {
'EnableImages': True, 'EnableImages': True,
@ -154,11 +144,9 @@ class Read_EmbyServer():
"CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
"Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers") "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): def getTvRecordings(self, groupid):
doUtils = self.doUtils
url = "{server}/emby/LiveTv/Recordings/?userid={UserId}&format=json"
if groupid == "root": groupid = "" if groupid == "root": groupid = ""
params = { params = {
@ -170,13 +158,10 @@ class Read_EmbyServer():
"CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
"Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers") "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): def getSection(self, parentid, itemtype=None, sortby="SortName", basic=False, dialog=None):
log = self.logMsg
doUtils = self.doUtils
items = { items = {
'Items': [], 'Items': [],
@ -195,13 +180,13 @@ class Read_EmbyServer():
'Recursive': True, 'Recursive': True,
'Limit': 1 'Limit': 1
} }
result = doUtils(url, parameters=params) result = self.doUtils(url, parameters=params)
try: try:
total = result['TotalRecordCount'] total = result['TotalRecordCount']
items['TotalRecordCount'] = total items['TotalRecordCount'] = total
except TypeError: # Failed to retrieve 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: else:
index = 0 index = 0
@ -234,36 +219,36 @@ class Read_EmbyServer():
"Metascore,AirTime,DateCreated,MediaStreams,People,Overview," "Metascore,AirTime,DateCreated,MediaStreams,People,Overview,"
"CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations,"
"Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers," "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers,"
"MediaSources" "MediaSources,VoteCount"
) )
result = doUtils(url, parameters=params) result = self.doUtils(url, parameters=params)
try: try:
items['Items'].extend(result['Items']) items['Items'].extend(result['Items'])
except TypeError: except TypeError:
# Something happened to the connection # Something happened to the connection
if not throttled: if not throttled:
throttled = True throttled = True
log("Throttle activated.", 1) self.logMsg("Throttle activated.", 1)
if jump == highestjump: if jump == highestjump:
# We already tried with the highestjump, but it failed. Reset value. # We already tried with the highestjump, but it failed. Reset value.
log("Reset highest value.", 1) self.logMsg("Reset highest value.", 1)
highestjump = 0 highestjump = 0
# Lower the number by half # Lower the number by half
if highestjump: if highestjump:
throttled = False throttled = False
jump = highestjump jump = highestjump
log("Throttle deactivated.", 1) self.logMsg("Throttle deactivated.", 1)
else: else:
jump = int(jump/4) 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 retry = 0
while utils.window('emby_online') != "true": while utils.window('plex_online') != "true":
# Wait server to come back online # Wait server to come back online
if retry == 5: if retry == 5:
log("Unable to reconnect to server. Abort process.", 1) self.logMsg("Unable to reconnect to server. Abort process.", 1)
return items return items
retry += 1 retry += 1
@ -291,12 +276,11 @@ class Read_EmbyServer():
increment = 10 increment = 10
jump += increment jump += increment
log("Increase jump limit to: %s" % jump, 1) self.logMsg("Increase jump limit to: %s" % jump, 1)
return items return items
def getViews(self, mediatype="", root=False, sortedlist=False): def getViews(self, mediatype="", root=False, sortedlist=False):
# Build a list of user views # Build a list of user views
doUtils = self.doUtils
views = [] views = []
mediatype = mediatype.lower() mediatype = mediatype.lower()
@ -305,7 +289,7 @@ class Read_EmbyServer():
else: # Views ungrouped else: # Views ungrouped
url = "{server}/emby/Users/{UserId}/Items?Sortby=SortName&format=json" url = "{server}/emby/Users/{UserId}/Items?Sortby=SortName&format=json"
result = doUtils(url) result = self.doUtils(url)
try: try:
items = result['Items'] items = result['Items']
except TypeError: except TypeError:
@ -313,11 +297,8 @@ class Read_EmbyServer():
else: else:
for item in items: for item in items:
name = item['Name'] item['Name'] = item['Name']
itemId = item['Id'] if item['Type'] == "Channel":
viewtype = item['Type']
if viewtype == "Channel":
# Filter view types # Filter view types
continue continue
@ -328,20 +309,20 @@ class Read_EmbyServer():
# Assumed missing is mixed then. # Assumed missing is mixed then.
'''if itemtype is None: '''if itemtype is None:
url = "{server}/emby/Library/MediaFolders?format=json" url = "{server}/emby/Library/MediaFolders?format=json"
result = doUtils(url) result = self.doUtils(url)
for folder in result['Items']: for folder in result['Items']:
if itemId == folder['Id']: if item['Id'] == folder['Id']:
itemtype = folder.get('CollectionType', "mixed")''' itemtype = folder.get('CollectionType', "mixed")'''
if name not in ('Collections', 'Trailers'): if item['Name'] not in ('Collections', 'Trailers'):
if sortedlist: if sortedlist:
views.append({ views.append({
'name': name, 'name': item['Name'],
'type': itemtype, 'type': itemtype,
'id': itemId 'id': item['Id']
}) })
elif (itemtype == mediatype or elif (itemtype == mediatype or
@ -349,9 +330,9 @@ class Read_EmbyServer():
views.append({ views.append({
'name': name, 'name': item['Name'],
'type': itemtype, 'type': itemtype,
'id': itemId 'id': item['Id']
}) })
return views return views
@ -359,8 +340,6 @@ class Read_EmbyServer():
def verifyView(self, parentid, itemid): def verifyView(self, parentid, itemid):
belongs = False belongs = False
url = "{server}/emby/Users/{UserId}/Items?format=json"
params = { params = {
'ParentId': parentid, 'ParentId': parentid,
@ -370,7 +349,7 @@ class Read_EmbyServer():
'Recursive': True, 'Recursive': True,
'Ids': itemid 'Ids': itemid
} }
result = self.doUtils(url, parameters=params) result = self.doUtils("{server}/emby/Users/{UserId}/Items?format=json", parameters=params)
try: try:
total = result['TotalRecordCount'] total = result['TotalRecordCount']
except TypeError: except TypeError:
@ -383,40 +362,23 @@ class Read_EmbyServer():
return belongs return belongs
def getMovies(self, parentId, basic=False, dialog=None): def getMovies(self, parentId, basic=False, dialog=None):
return self.getSection(parentId, "Movie", basic=basic, dialog=dialog)
items = self.getSection(parentId, "Movie", basic=basic, dialog=dialog)
return items
def getBoxset(self, dialog=None): def getBoxset(self, dialog=None):
return self.getSection(None, "BoxSet", dialog=dialog)
items = self.getSection(None, "BoxSet", dialog=dialog)
return items
def getMovies_byBoxset(self, boxsetid): def getMovies_byBoxset(self, boxsetid):
return self.getSection(boxsetid, "Movie")
items = self.getSection(boxsetid, "Movie")
return items
def getMusicVideos(self, parentId, basic=False, dialog=None): def getMusicVideos(self, parentId, basic=False, dialog=None):
return self.getSection(parentId, "MusicVideo", basic=basic, dialog=dialog)
items = self.getSection(parentId, "MusicVideo", basic=basic, dialog=dialog)
return items
def getHomeVideos(self, parentId): def getHomeVideos(self, parentId):
items = self.getSection(parentId, "Video") return self.getSection(parentId, "Video")
return items
def getShows(self, parentId, basic=False, dialog=None): def getShows(self, parentId, basic=False, dialog=None):
return self.getSection(parentId, "Series", basic=basic, dialog=dialog)
items = self.getSection(parentId, "Series", basic=basic, dialog=dialog)
return items
def getSeasons(self, showId): def getSeasons(self, showId):
@ -426,13 +388,12 @@ class Read_EmbyServer():
'TotalRecordCount': 0 'TotalRecordCount': 0
} }
url = "{server}/emby/Shows/%s/Seasons?UserId={UserId}&format=json" % showId
params = { params = {
'IsVirtualUnaired': False, 'IsVirtualUnaired': False,
'Fields': "Etag" '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: if result:
items = result items = result
@ -440,25 +401,19 @@ class Read_EmbyServer():
def getEpisodes(self, parentId, basic=False, dialog=None): def getEpisodes(self, parentId, basic=False, dialog=None):
items = self.getSection(parentId, "Episode", basic=basic, dialog=dialog) return self.getSection(parentId, "Episode", basic=basic, dialog=dialog)
return items
def getEpisodesbyShow(self, showId): def getEpisodesbyShow(self, showId):
items = self.getSection(showId, "Episode") return self.getSection(showId, "Episode")
return items
def getEpisodesbySeason(self, seasonId): def getEpisodesbySeason(self, seasonId):
items = self.getSection(seasonId, "Episode") return self.getSection(seasonId, "Episode")
return items
def getArtists(self, dialog=None): def getArtists(self, dialog=None):
doUtils = self.doUtils
items = { items = {
'Items': [], 'Items': [],
@ -472,7 +427,7 @@ class Read_EmbyServer():
'Recursive': True, 'Recursive': True,
'Limit': 1 'Limit': 1
} }
result = doUtils(url, parameters=params) result = self.doUtils(url, parameters=params)
try: try:
total = result['TotalRecordCount'] total = result['TotalRecordCount']
items['TotalRecordCount'] = total items['TotalRecordCount'] = total
@ -502,7 +457,7 @@ class Read_EmbyServer():
"AirTime,DateCreated,MediaStreams,People,ProviderIds,Overview" "AirTime,DateCreated,MediaStreams,People,ProviderIds,Overview"
) )
} }
result = doUtils(url, parameters=params) result = self.doUtils(url, parameters=params)
items['Items'].extend(result['Items']) items['Items'].extend(result['Items'])
index += jump index += jump
@ -512,28 +467,17 @@ class Read_EmbyServer():
return items return items
def getAlbums(self, basic=False, dialog=None): def getAlbums(self, basic=False, dialog=None):
return self.getSection(None, "MusicAlbum", sortby="DateCreated", basic=basic, dialog=dialog)
items = self.getSection(None, "MusicAlbum", sortby="DateCreated", basic=basic, dialog=dialog)
return items
def getAlbumsbyArtist(self, artistId): def getAlbumsbyArtist(self, artistId):
return self.getSection(artistId, "MusicAlbum", sortby="DateCreated")
items = self.getSection(artistId, "MusicAlbum", sortby="DateCreated")
return items
def getSongs(self, basic=False, dialog=None): def getSongs(self, basic=False, dialog=None):
return self.getSection(None, "Audio", basic=basic, dialog=dialog)
items = self.getSection(None, "Audio", basic=basic, dialog=dialog)
return items
def getSongsbyAlbum(self, albumId): def getSongsbyAlbum(self, albumId):
return self.getSection(albumId, "Audio")
items = self.getSection(albumId, "Audio")
return items
def getAdditionalParts(self, itemId): def getAdditionalParts(self, itemId):
@ -543,8 +487,7 @@ class Read_EmbyServer():
'TotalRecordCount': 0 'TotalRecordCount': 0
} }
url = "{server}/emby/Videos/%s/AdditionalParts?UserId={UserId}&format=json" % itemId result = self.doUtils("{server}/emby/Videos/%s/AdditionalParts?UserId={UserId}&format=json" % itemId)
result = self.doUtils(url)
if result: if result:
items = result items = result
@ -566,25 +509,21 @@ class Read_EmbyServer():
def updateUserRating(self, itemid, like=None, favourite=None, deletelike=False): def updateUserRating(self, itemid, like=None, favourite=None, deletelike=False):
# Updates the user rating to Emby # Updates the user rating to Emby
doUtils = self.doUtils
if favourite: if favourite:
url = "{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid self.doUtils("{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid, action_type="POST")
doUtils(url, type="POST")
elif favourite == False: elif favourite == False:
url = "{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid self.doUtils("{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid, action_type="DELETE")
doUtils(url, type="DELETE")
if not deletelike and like: if not deletelike and like:
url = "{server}/emby/Users/{UserId}/Items/%s/Rating?Likes=true&format=json" % itemid self.doUtils("{server}/emby/Users/{UserId}/Items/%s/Rating?Likes=true&format=json" % itemid, action_type="POST")
doUtils(url, type="POST") elif not deletelike and like is False:
elif not deletelike and like == False: self.doUtils("{server}/emby/Users/{UserId}/Items/%s/Rating?Likes=false&format=json" % itemid, action_type="POST")
url = "{server}/emby/Users/{UserId}/Items/%s/Rating?Likes=false&format=json" % itemid
doUtil(url, type="POST")
elif deletelike: elif deletelike:
url = "{server}/emby/Users/{UserId}/Items/%s/Rating?format=json" % itemid self.doUtils("{server}/emby/Users/{UserId}/Items/%s/Rating?format=json" % itemid, action_type="DELETE")
doUtils(url, type="DELETE") else:
self.logMsg("Error processing user rating.", 1)
self.logMsg("Update user rating to emby for itemid: %s " self.logMsg("Update user rating to emby for itemid: %s "
"| like: %s | favourite: %s | deletelike: %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" # url = "{server}/emby/System/Configuration?format=json"
# result = doUtils.downloadUrl(url) # result = doUtils.downloadUrl(url)
# utils.settings('markPlayed', value=str(result['MaxResumePct']))
def hasAccess(self): def hasAccess(self):
# Plex: always return True for now # Plex: always return True for now
return True return True
@ -126,19 +124,19 @@ class UserClient(threading.Thread):
url = "{server}/emby/Users?format=json" url = "{server}/emby/Users?format=json"
result = self.doUtils.downloadUrl(url) result = self.doUtils.downloadUrl(url)
if result == False: if result is False:
# Access is restricted, set in downloadutils.py via exception # Access is restricted, set in downloadutils.py via exception
log("Access is restricted.", 1) log("Access is restricted.", 1)
self.HasAccess = False self.HasAccess = False
elif window('emby_online') != "true": elif window('plex_online') != "true":
# Server connection failed # Server connection failed
pass pass
elif window('emby_serverStatus') == "restricted": elif window('plex_serverStatus') == "restricted":
log("Access is granted.", 1) log("Access is granted.", 1)
self.HasAccess = True self.HasAccess = True
window('emby_serverStatus', clear=True) window('plex_serverStatus', clear=True)
xbmcgui.Dialog().notification(self.addonName, xbmcgui.Dialog().notification(self.addonName,
utils.language(33007)) utils.language(33007))
@ -239,7 +237,7 @@ class UserClient(threading.Thread):
# Give attempts at entering password / selecting user # Give attempts at entering password / selecting user
if self.retry >= 2: if self.retry >= 2:
log("Too many retries to login.", -1) log("Too many retries to login.", -1)
window('emby_serverStatus', value="Stop") window('plex_serverStatus', value="Stop")
dialog.ok(lang(33001), dialog.ok(lang(33001),
lang(39023)) lang(39023))
xbmc.executebuiltin( xbmc.executebuiltin(
@ -247,8 +245,8 @@ class UserClient(threading.Thread):
return False return False
# Get /profile/addon_data # Get /profile/addon_data
addondir = xbmc.translatePath( addondir = utils.tryDecode(xbmc.translatePath(
self.addon.getAddonInfo('profile')).decode('utf-8') self.addon.getAddonInfo('profile')))
hasSettings = xbmcvfs.exists("%ssettings.xml" % addondir) hasSettings = xbmcvfs.exists("%ssettings.xml" % addondir)
# If there's no settings.xml # If there's no settings.xml
@ -358,7 +356,7 @@ class UserClient(threading.Thread):
break break
xbmc.sleep(1000) xbmc.sleep(1000)
status = window('emby_serverStatus') status = window('plex_serverStatus')
if status == "Stop": if status == "Stop":
xbmc.sleep(500) xbmc.sleep(500)
@ -371,7 +369,7 @@ class UserClient(threading.Thread):
elif status == "401": elif status == "401":
# Unauthorized access, revoke token # Unauthorized access, revoke token
window('emby_serverStatus', value="Auth") window('plex_serverStatus', value="Auth")
self.resetClient() self.resetClient()
xbmc.sleep(2000) xbmc.sleep(2000)
@ -389,7 +387,7 @@ class UserClient(threading.Thread):
log("Current accessToken: xxxx", 1) log("Current accessToken: xxxx", 1)
self.retry = 0 self.retry = 0
window('suspend_LibraryThread', clear=True) window('suspend_LibraryThread', clear=True)
window('emby_serverStatus', clear=True) window('plex_serverStatus', clear=True)
if not self.auth and (self.currUser is None): if not self.auth and (self.currUser is None):
# Loop if no server found # Loop if no server found

View file

@ -16,6 +16,7 @@ from functools import wraps
from calendar import timegm from calendar import timegm
import os import os
import xbmc import xbmc
import xbmcaddon import xbmcaddon
import xbmcgui import xbmcgui
@ -98,7 +99,7 @@ def IfExists(path):
Returns True if path exists, else false 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: try:
etree.ElementTree(etree.Element('test')).write(dummyfile) etree.ElementTree(etree.Element('test')).write(dummyfile)
except: except:
@ -111,6 +112,46 @@ def IfExists(path):
return answer 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): def LogTime(func):
""" """
Decorator for functions and methods to log the time it took to run the code 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): def logMsg(title, msg, level=1):
# Get the logLevel set in UserClient # Get the logLevel set in UserClient
try: try:
logLevel = int(window('emby_logLevel')) logLevel = int(window('plex_logLevel'))
except ValueError: except ValueError:
logLevel = 0 logLevel = 0
kodiLevel = { kodiLevel = {
@ -272,7 +313,7 @@ def logMsg(title, msg, level=1):
except UnicodeEncodeError: except UnicodeEncodeError:
try: try:
xbmc.log("%s -> %s : %s" % ( xbmc.log("%s -> %s : %s" % (
title, func.co_name, msg.encode('utf-8')), title, func.co_name, tryEncode(msg)),
level=kodiLevel[level]) level=kodiLevel[level])
except: except:
xbmc.log("%s -> %s : %s" % ( xbmc.log("%s -> %s : %s" % (
@ -283,7 +324,7 @@ def logMsg(title, msg, level=1):
xbmc.log("%s -> %s" % (title, msg), level=kodiLevel[level]) xbmc.log("%s -> %s" % (title, msg), level=kodiLevel[level])
except UnicodeEncodeError: except UnicodeEncodeError:
try: try:
xbmc.log("%s -> %s" % (title, msg.encode('utf-8')), xbmc.log("%s -> %s" % (title, tryEncode(msg)),
level=kodiLevel[level]) level=kodiLevel[level])
except: except:
xbmc.log("%s -> %s " % (title, 'COULDNT LOG'), 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 Property needs to be string; value may be string or unicode
""" """
WINDOW = xbmcgui.Window(windowid) 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 #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: if clear:
WINDOW.clearProperty(property) WINDOW.clearProperty(property)
elif value is not None: elif value is not None:
WINDOW.setProperty(property, value.encode('utf-8')) WINDOW.setProperty(property, tryEncode(value))
else: else:
return WINDOW.getProperty(property).decode('utf-8') return tryDecode(WINDOW.getProperty(property))
def settings(setting, value=None): def settings(setting, value=None):
""" """
@ -323,10 +360,10 @@ def settings(setting, value=None):
if value is not None: if value is not None:
# Takes string or unicode by default! # Takes string or unicode by default!
addon.setSetting(setting, value.encode('utf-8')) addon.setSetting(setting, tryEncode(value))
else: else:
# Should return unicode by default, but just in case # Should return unicode by default, but just in case
return addon.getSetting(setting).decode('utf-8') return tryDecode(addon.getSetting(setting))
def language(stringid): def language(stringid):
# Central string retrieval # Central string retrieval
@ -334,40 +371,39 @@ def language(stringid):
string = addon.getLocalizedString(stringid) #returns unicode object string = addon.getLocalizedString(stringid) #returns unicode object
return string return string
def kodiSQL(type="video"): def kodiSQL(media_type="video"):
if type == "emby": if media_type == "emby":
dbPath = xbmc.translatePath("special://database/emby.db").decode('utf-8') dbPath = tryDecode(xbmc.translatePath("special://database/emby.db"))
elif type == "music": elif media_type == "music":
dbPath = getKodiMusicDBPath() dbPath = getKodiMusicDBPath()
elif type == "texture": elif media_type == "texture":
dbPath = xbmc.translatePath("special://database/Textures13.db").decode('utf-8') dbPath = tryDecode(xbmc.translatePath(
"special://database/Textures13.db"))
else: else:
dbPath = getKodiVideoDBPath() dbPath = getKodiVideoDBPath()
connection = sqlite3.connect(dbPath) connection = sqlite3.connect(dbPath)
return connection return connection
def getKodiVideoDBPath(): def getKodiVideoDBPath():
kodibuild = xbmc.getInfoLabel('System.BuildVersion')[:2]
dbVersion = { dbVersion = {
"13": 78, # Gotham "13": 78, # Gotham
"14": 90, # Helix "14": 90, # Helix
"15": 93, # Isengard "15": 93, # Isengard
"16": 99, # Jarvis "16": 99, # Jarvis
"17":104 # Krypton "17": 107 # Krypton
} }
dbPath = xbmc.translatePath( dbPath = tryDecode(xbmc.translatePath(
"special://database/MyVideos%s.db" "special://database/MyVideos%s.db"
% dbVersion.get(kodibuild, "")).decode('utf-8') % dbVersion.get(xbmc.getInfoLabel('System.BuildVersion')[:2], "")))
return dbPath return dbPath
def getKodiMusicDBPath(): def getKodiMusicDBPath():
kodibuild = xbmc.getInfoLabel('System.BuildVersion')[:2]
dbVersion = { dbVersion = {
"13": 46, # Gotham "13": 46, # Gotham
@ -377,9 +413,9 @@ def getKodiMusicDBPath():
"17": 60 # Krypton "17": 60 # Krypton
} }
dbPath = xbmc.translatePath( dbPath = tryDecode(xbmc.translatePath(
"special://database/MyMusic%s.db" "special://database/MyMusic%s.db"
% dbVersion.get(kodibuild, "")).decode('utf-8') % dbVersion.get(xbmc.getInfoLabel('System.BuildVersion')[:2], "")))
return dbPath return dbPath
def getScreensaver(): def getScreensaver():
@ -394,11 +430,7 @@ def getScreensaver():
'setting': "screensaver.mode" 'setting': "screensaver.mode"
} }
} }
result = xbmc.executeJSONRPC(json.dumps(query)) return json.loads(xbmc.executeJSONRPC(json.dumps(query)))['result']['value']
result = json.loads(result)
screensaver = result['result']['value']
return screensaver
def setScreensaver(value): def setScreensaver(value):
# Toggle the screensaver # Toggle the screensaver
@ -413,21 +445,19 @@ def setScreensaver(value):
'value': value 'value': value
} }
} }
result = xbmc.executeJSONRPC(json.dumps(query)) logMsg("PLEX", "Toggling screensaver: %s %s" % (value, xbmc.executeJSONRPC(json.dumps(query))), 1)
logMsg("PLEX", "Toggling screensaver: %s %s" % (value, result), 1)
def reset(): def reset():
dialog = xbmcgui.Dialog() dialog = xbmcgui.Dialog()
resp = dialog.yesno("Warning", "Are you sure you want to reset your local Kodi database?") if dialog.yesno("Warning", "Are you sure you want to reset your local Kodi database?") == 0:
if resp == 0:
return return
# first stop any db sync # first stop any db sync
window('emby_shouldStop', value="true") window('plex_shouldStop', value="true")
count = 10 count = 10
while window('emby_dbScan') == "true": while window('plex_dbScan') == "true":
logMsg("PLEX", "Sync is running, will retry: %s..." % count) logMsg("PLEX", "Sync is running, will retry: %s..." % count)
count -= 1 count -= 1
if count == 0: if count == 0:
@ -442,7 +472,7 @@ def reset():
deleteNodes() deleteNodes()
# Wipe the kodi databases # Wipe the kodi databases
logMsg("EMBY", "Resetting the Kodi video database.", 0) logMsg("Plex", "Resetting the Kodi video database.", 0)
connection = kodiSQL('video') connection = kodiSQL('video')
cursor = connection.cursor() cursor = connection.cursor()
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
@ -455,7 +485,7 @@ def reset():
cursor.close() cursor.close()
if settings('enableMusic') == "true": if settings('enableMusic') == "true":
logMsg("EMBY", "Resetting the Kodi music database.") logMsg("Plex", "Resetting the Kodi music database.")
connection = kodiSQL('music') connection = kodiSQL('music')
cursor = connection.cursor() cursor = connection.cursor()
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
@ -467,8 +497,8 @@ def reset():
connection.commit() connection.commit()
cursor.close() cursor.close()
# Wipe the emby database # Wipe the Plex database
logMsg("EMBY", "Resetting the Emby database.", 0) logMsg("Plex", "Resetting the Emby database.", 0)
connection = kodiSQL('emby') connection = kodiSQL('emby')
cursor = connection.cursor() cursor = connection.cursor()
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
@ -483,21 +513,25 @@ def reset():
cursor.close() cursor.close()
# Offer to wipe cached thumbnails # Offer to wipe cached thumbnails
resp = dialog.yesno("Warning", "Removed all cached artwork?") resp = dialog.yesno("Warning", "Remove all cached artwork?")
if resp: if resp:
logMsg("EMBY", "Resetting all cached artwork.", 0) logMsg("EMBY", "Resetting all cached artwork.", 0)
# Remove all existing textures first # Remove all existing textures first
path = xbmc.translatePath("special://thumbnails/").decode('utf-8') path = tryDecode(xbmc.translatePath("special://thumbnails/"))
if xbmcvfs.exists(path): if xbmcvfs.exists(path):
allDirs, allFiles = xbmcvfs.listdir(path) allDirs, allFiles = xbmcvfs.listdir(path)
for dir in allDirs: for dir in allDirs:
allDirs, allFiles = xbmcvfs.listdir(path+dir) allDirs, allFiles = xbmcvfs.listdir(path+dir)
for file in allFiles: for file in allFiles:
if os.path.supports_unicode_filenames: 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: 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 # remove all existing data from texture DB
connection = kodiSQL('texture') connection = kodiSQL('texture')
cursor = connection.cursor() cursor = connection.cursor()
@ -509,8 +543,8 @@ def reset():
cursor.execute("DELETE FROM " + tableName) cursor.execute("DELETE FROM " + tableName)
connection.commit() connection.commit()
cursor.close() cursor.close()
# reset the install run flag # reset the install run flag
settings('SyncInstallRunDone', value="false") settings('SyncInstallRunDone', value="false")
# Remove emby info # Remove emby info
@ -518,9 +552,9 @@ def reset():
if resp: if resp:
# Delete the settings # Delete the settings
addon = xbmcaddon.Addon() addon = xbmcaddon.Addon()
addondir = xbmc.translatePath(addon.getAddonInfo('profile')).decode('utf-8') addondir = tryDecode(xbmc.translatePath(addon.getAddonInfo('profile')))
dataPath = "%ssettings.xml" % addondir dataPath = "%ssettings.xml" % addondir
xbmcvfs.delete(dataPath.encode('utf-8')) xbmcvfs.delete(tryEncode(dataPath))
logMsg("PLEX", "Deleting: settings.xml", 1) logMsg("PLEX", "Deleting: settings.xml", 1)
dialog.ok( dialog.ok(
@ -532,7 +566,7 @@ def profiling(sortby="cumulative"):
# Will print results to Kodi log # Will print results to Kodi log
def decorator(func): def decorator(func):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
pr = cProfile.Profile() pr = cProfile.Profile()
pr.enable() pr.enable()
@ -575,8 +609,8 @@ def normalize_nodes(text):
# Remove dots from the last character as windows can not have directories # Remove dots from the last character as windows can not have directories
# with dots at the end # with dots at the end
text = text.rstrip('.') 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 return text
def normalize_string(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 # Remove dots from the last character as windows can not have directories
# with dots at the end # with dots at the end
text = text.rstrip('.') 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 return text
@ -619,7 +653,7 @@ def guisettingsXML():
""" """
Returns special://userdata/guisettings.xml as an etree xml root element 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 xmlpath = "%sguisettings.xml" % path
try: try:
@ -678,7 +712,7 @@ def advancedSettingsXML():
usetags : set to "false" usetags : set to "false"
findremotethumbs : 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 xmlpath = "%sadvancedsettings.xml" % path
try: try:
@ -708,7 +742,7 @@ def advancedSettingsXML():
def sourcesXML(): def sourcesXML():
# To make Master lock compatible # To make Master lock compatible
path = xbmc.translatePath("special://profile/").decode('utf-8') path = tryDecode(xbmc.translatePath("special://profile/"))
xmlpath = "%ssources.xml" % path xmlpath = "%ssources.xml" % path
try: try:
@ -717,7 +751,7 @@ def sourcesXML():
root = etree.Element('sources') root = etree.Element('sources')
else: else:
root = xmlparse.getroot() root = xmlparse.getroot()
video = root.find('video') video = root.find('video')
if video is None: if video is None:
@ -729,7 +763,7 @@ def sourcesXML():
for source in root.findall('.//path'): for source in root.findall('.//path'):
if source.text == "smb://": if source.text == "smb://":
count -= 1 count -= 1
if count == 0: if count == 0:
# sources already set # sources already set
break break
@ -749,7 +783,7 @@ def sourcesXML():
def passwordsXML(): def passwordsXML():
# To add network credentials # To add network credentials
path = xbmc.translatePath("special://userdata/").decode('utf-8') path = tryDecode(xbmc.translatePath("special://userdata/"))
xmlpath = "%spasswords.xml" % path xmlpath = "%spasswords.xml" % path
logMsg('passwordsXML', 'Path to passwords.xml: %s' % xmlpath, 1) logMsg('passwordsXML', 'Path to passwords.xml: %s' % xmlpath, 1)
@ -775,9 +809,7 @@ def passwordsXML():
elif option == 1: elif option == 1:
# User selected remove # User selected remove
iterator = root.getiterator('passwords') for paths in root.getiterator('passwords'):
for paths in iterator:
for path in paths: for path in paths:
if path.find('.//from').text == "smb://%s/" % credentials: if path.find('.//from').text == "smb://%s/" % credentials:
paths.remove(path) paths.remove(path)
@ -787,8 +819,8 @@ def passwordsXML():
etree.ElementTree(root).write(xmlpath) etree.ElementTree(root).write(xmlpath)
break break
else: 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="") settings('networkCreds', value="")
xbmcgui.Dialog().notification( xbmcgui.Dialog().notification(
heading='PlexKodiConnect', heading='PlexKodiConnect',
@ -842,7 +874,7 @@ def passwordsXML():
# Force Kodi to see the credentials without restarting # Force Kodi to see the credentials without restarting
xbmcvfs.exists(topath) xbmcvfs.exists(topath)
# Add credentials # Add credentials
settings('networkCreds', value="%s" % server) settings('networkCreds', value="%s" % server)
logMsg("PLEX", "Added server: %s to passwords.xml" % server, 1) logMsg("PLEX", "Added server: %s to passwords.xml" % server, 1)
# Prettify and write to file # Prettify and write to file
@ -850,7 +882,7 @@ def passwordsXML():
indent(root) indent(root)
except: pass except: pass
etree.ElementTree(root).write(xmlpath) etree.ElementTree(root).write(xmlpath)
# dialog.notification( # dialog.notification(
# heading="PlexKodiConnect", # heading="PlexKodiConnect",
# message="Added to passwords.xml", # message="Added to passwords.xml",
@ -862,7 +894,7 @@ def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False):
""" """
Feed with tagname as unicode 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": if viewtype == "mixed":
plname = "%s - %s" % (tagname, mediatype) plname = "%s - %s" % (tagname, mediatype)
xsppath = "%sPlex %s - %s.xsp" % (path, viewid, 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) xsppath = "%sPlex %s.xsp" % (path, viewid)
# Create the playlist directory # 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) 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 # 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) logMsg('Path %s does exist' % xsppath, 1)
if delete: if delete:
xbmcvfs.delete(xsppath.encode('utf-8')) xbmcvfs.delete(tryEncode(xsppath))
logMsg("PLEX", "Successfully removed playlist: %s." % tagname, 1) logMsg("PLEX", "Successfully removed playlist: %s." % tagname, 1)
return return
# Using write process since there's no guarantee the xml declaration works with etree # 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) logMsg("Plex", "Writing playlist file to: %s" % xsppath, 1)
try: try:
f = xbmcvfs.File(xsppath.encode('utf-8'), 'wb') f = xbmcvfs.File(tryEncode(xsppath), 'wb')
except: except:
logMsg("Plex", "Failed to create playlist: %s" % xsppath, -1) logMsg("Plex", "Failed to create playlist: %s" % xsppath, -1)
return return
else: else:
f.write(( f.write(tryEncode(
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n' '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n'
'<smartplaylist type="%s">\n\t' '<smartplaylist type="%s">\n\t'
'<name>Plex %s</name>\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' '<value>%s</value>\n\t'
'</rule>\n' '</rule>\n'
'</smartplaylist>\n' '</smartplaylist>\n'
% (itemtypes.get(mediatype, mediatype), plname, tagname)) % (itemtypes.get(mediatype, mediatype), plname, tagname)))
.encode('utf-8'))
f.close() f.close()
logMsg("Plex", "Successfully added playlist: %s" % tagname) logMsg("Plex", "Successfully added playlist: %s" % tagname)
def deletePlaylists(): def deletePlaylists():
# Clean up the playlists # Clean up the playlists
path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8') path = tryDecode(xbmc.translatePath("special://profile/playlists/video/"))
dirs, files = xbmcvfs.listdir(path.encode('utf-8')) dirs, files = xbmcvfs.listdir(tryEncode(path))
for file in files: for file in files:
if file.decode('utf-8').startswith('Plex'): if tryDecode(file).startswith('Plex'):
xbmcvfs.delete(("%s%s" % (path, file.decode('utf-8'))).encode('utf-8')) xbmcvfs.delete(tryEncode("%s%s" % (path, tryDecode(file))))
def deleteNodes(): def deleteNodes():
# Clean up video nodes # Clean up video nodes
import shutil import shutil
path = xbmc.translatePath("special://profile/library/video/").decode('utf-8') path = tryDecode(xbmc.translatePath("special://profile/library/video/"))
dirs, files = xbmcvfs.listdir(path.encode('utf-8')) dirs, files = xbmcvfs.listdir(tryEncode(path))
for dir in dirs: for dir in dirs:
if dir.decode('utf-8').startswith('Plex'): if tryDecode(dir).startswith('Plex'):
try: try:
shutil.rmtree("%s%s" % (path, dir.decode('utf-8'))) shutil.rmtree("%s%s" % (path, tryDecode(dir)))
except: 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: for file in files:
if file.decode('utf-8').startswith('plex'): if tryDecode(file).startswith('plex'):
try: try:
xbmcvfs.delete(("%s%s" % (path, file.decode('utf-8'))).encode('utf-8')) xbmcvfs.delete(tryEncode("%s%s" % (path, tryDecode(file))))
except: 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: try:
return text.decode(encoding,"ignore") uniString = uniString.encode(encoding, "ignore")
except: except TypeError:
return text 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] mediatype = mediatypes[mediatype]
window = utils.window window = utils.window
kodiversion = self.kodiversion
if viewtype == "mixed": if viewtype == "mixed":
dirname = "%s-%s" % (viewid, mediatype) dirname = "%s-%s" % (viewid, mediatype)
else: else:
dirname = viewid dirname = viewid
path = xbmc.translatePath("special://profile/library/video/").decode('utf-8') path = utils.tryDecode(xbmc.translatePath(
nodepath = xbmc.translatePath( "special://profile/library/video/"))
"special://profile/library/video/Plex-%s/" % dirname).decode('utf-8') nodepath = utils.tryDecode(xbmc.translatePath(
"special://profile/library/video/Plex-%s/" % dirname))
# Verify the video directory # Verify the video directory
# KODI BUG # KODI BUG
@ -71,20 +71,22 @@ class VideoNodes(object):
# so try creating a file # so try creating a file
if utils.IfExists(path) is False: if utils.IfExists(path) is False:
shutil.copytree( shutil.copytree(
src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'), src=utils.tryDecode(xbmc.translatePath(
dst=xbmc.translatePath("special://profile/library/video").decode('utf-8')) "special://xbmc/system/library/video")),
dst=utils.tryDecode(xbmc.translatePath(
"special://profile/library/video")))
# Create the node directory # Create the node directory
if mediatype != "photo": if mediatype != "photos":
if utils.IfExists(nodepath) is False: if utils.IfExists(nodepath) is False:
# folder does not exist yet # folder does not exist yet
self.logMsg('Creating folder %s' % nodepath, 1) self.logMsg('Creating folder %s' % nodepath, 1)
xbmcvfs.mkdirs(nodepath.encode('utf-8')) xbmcvfs.mkdirs(utils.tryEncode(nodepath))
if delete: if delete:
dirs, files = xbmcvfs.listdir(nodepath.encode('utf-8')) dirs, files = xbmcvfs.listdir(utils.tryEncode(nodepath))
for file in files: for file in files:
xbmcvfs.delete( xbmcvfs.delete(utils.tryEncode(
(nodepath + file.decode('utf-8')).encode('utf-8')) (nodepath + utils.tryDecode(file))))
self.logMsg("Sucessfully removed videonode: %s." self.logMsg("Sucessfully removed videonode: %s."
% tagname, 1) % tagname, 1)
@ -96,16 +98,16 @@ class VideoNodes(object):
path = "library://video/Plex-%s/" % dirname path = "library://video/Plex-%s/" % dirname
for i in range(1, indexnumber): for i in range(1, indexnumber):
# Verify to make sure we don't create duplicates # 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 return
if mediatype == "photo": if mediatype == "photos":
path = "plugin://plugin.video.plexkodiconnect/?id=%s&mode=getsubfolders" % indexnumber 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 # Root
if not mediatype == "photo": if not mediatype == "photos":
if viewtype == "mixed": if viewtype == "mixed":
specialtag = "%s-%s" % (tagname, mediatype) specialtag = "%s-%s" % (tagname, mediatype)
root = self.commonRoot(order=0, label=specialtag, tagname=tagname, roottype=0) root = self.commonRoot(order=0, label=specialtag, tagname=tagname, roottype=0)
@ -215,14 +217,14 @@ class VideoNodes(object):
label = stringid label = stringid
# Set window properties # Set window properties
if (mediatype == "homevideos" or mediatype == "photo") and nodetype == "all": if (mediatype == "homevideos" or mediatype == "photos") and nodetype == "all":
# Custom query # Custom query
path = ("plugin://plugin.video.plexkodiconnect/?id=%s&mode=browsecontent&type=%s" path = ("plugin://plugin.video.plexkodiconnect/?id=%s&mode=browseplex&type=%s"
% (tagname, mediatype)) % (viewid, mediatype))
elif (mediatype == "homevideos" or mediatype == "photo"): elif (mediatype == "homevideos" or mediatype == "photos"):
# Custom query # Custom query
path = ("plugin://plugin.video.plexkodiconnect/?id=%s&mode=browsecontent&type=%s&folderid=%s" path = ("plugin://plugin.video.plexkodiconnect/?id=%s&mode=browseplex&type=%s&folderid=%s"
% (tagname, mediatype, nodetype)) % (viewid, mediatype, nodetype))
elif nodetype == "nextepisodes": elif nodetype == "nextepisodes":
# Custom query # Custom query
path = "plugin://plugin.video.plexkodiconnect/?id=%s&mode=nextup&limit=%s" % (tagname, limit) path = "plugin://plugin.video.plexkodiconnect/?id=%s&mode=nextup&limit=%s" % (tagname, limit)
@ -231,7 +233,7 @@ class VideoNodes(object):
# Custom query # Custom query
path = ("plugin://plugin.video.plexkodiconnect/?id=%s&mode=recentepisodes&type=%s&tagname=%s&limit=%s" path = ("plugin://plugin.video.plexkodiconnect/?id=%s&mode=recentepisodes&type=%s&tagname=%s&limit=%s"
% (viewid, mediatype, tagname, limit)) % (viewid, mediatype, tagname, limit))
elif kodiversion == 14 and nodetype == "inprogressepisodes": elif self.kodiversion == 14 and nodetype == "inprogressepisodes":
# Custom query # Custom query
path = "plugin://plugin.video.plexkodiconnect/?id=%s&mode=inprogressepisodes&limit=%s" % (tagname, limit) path = "plugin://plugin.video.plexkodiconnect/?id=%s&mode=inprogressepisodes&limit=%s" % (tagname, limit)
elif nodetype == 'ondeck': elif nodetype == 'ondeck':
@ -245,10 +247,14 @@ class VideoNodes(object):
else: else:
path = "library://video/Plex-%s/%s_%s.xml" % (dirname, viewid, nodetype) path = "library://video/Plex-%s/%s_%s.xml" % (dirname, viewid, nodetype)
if mediatype == "photo": if mediatype == "photos":
windowpath = "ActivateWindow(Pictures,%s,return)" % path windowpath = "ActivateWindow(Pictures,%s,return)" % path
else: 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": if nodetype == "all":
@ -257,24 +263,24 @@ class VideoNodes(object):
else: else:
templabel = label templabel = label
embynode = "Emby.nodes.%s" % indexnumber embynode = "Plex.nodes.%s" % indexnumber
window('%s.title' % embynode, value=templabel) window('%s.title' % embynode, value=templabel)
window('%s.path' % embynode, value=windowpath) window('%s.path' % embynode, value=windowpath)
window('%s.content' % embynode, value=path) window('%s.content' % embynode, value=path)
window('%s.type' % embynode, value=mediatype) window('%s.type' % embynode, value=mediatype)
else: else:
embynode = "Emby.nodes.%s.%s" % (indexnumber, nodetype) embynode = "Plex.nodes.%s.%s" % (indexnumber, nodetype)
window('%s.title' % embynode, value=label) window('%s.title' % embynode, value=label)
window('%s.path' % embynode, value=windowpath) window('%s.path' % embynode, value=windowpath)
window('%s.content' % embynode, value=path) 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 # For photos, we do not create a node in videos but we do want the window props
# to be created. # to be created.
# To do: add our photos nodes to kodi picture sources somehow # To do: add our photos nodes to kodi picture sources somehow
continue continue
if xbmcvfs.exists(nodeXML.encode('utf-8')): if xbmcvfs.exists(utils.tryEncode(nodeXML)):
# Don't recreate xml if already exists # Don't recreate xml if already exists
continue continue
@ -298,8 +304,12 @@ class VideoNodes(object):
elif nodetype == "recent": elif nodetype == "recent":
etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded" etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded"
etree.SubElement(root, 'limit').text = limit etree.SubElement(root, 'limit').text = limit
rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) if utils.settings('MovieShowWatched') == 'false':
etree.SubElement(rule, 'value').text = "0" rule = etree.SubElement(root,
'rule',
{'field': "playcount",
'operator': "is"})
etree.SubElement(rule, 'value').text = "0"
elif nodetype == "inprogress": elif nodetype == "inprogress":
etree.SubElement(root, 'rule', {'field': "inprogress", 'operator': "true"}) etree.SubElement(root, 'rule', {'field': "inprogress", 'operator': "true"})
@ -358,19 +368,26 @@ class VideoNodes(object):
window = utils.window window = utils.window
tagname = tagname.encode('utf-8') tagname = utils.tryEncode(tagname)
cleantagname = utils.normalize_nodes(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) nodeXML = "%splex_%s.xml" % (nodepath, cleantagname)
path = "library://video/plex_%s.xml" % 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 # Create the video node directory
if not xbmcvfs.exists(nodepath): if not xbmcvfs.exists(nodepath):
# We need to copy over the default items # We need to copy over the default items
shutil.copytree( shutil.copytree(
src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'), src=utils.tryDecode(xbmc.translatePath(
dst=xbmc.translatePath("special://profile/library/video").decode('utf-8')) "special://xbmc/system/library/video")),
dst=utils.tryDecode(xbmc.translatePath(
"special://profile/library/video")))
xbmcvfs.exists(path) xbmcvfs.exists(path)
labels = { labels = {
@ -380,7 +397,7 @@ class VideoNodes(object):
'channels': 30173 'channels': 30173
} }
label = utils.language(labels[tagname]) label = utils.language(labels[tagname])
embynode = "Emby.nodes.%s" % indexnumber embynode = "Plex.nodes.%s" % indexnumber
window('%s.title' % embynode, value=label) window('%s.title' % embynode, value=label)
window('%s.path' % embynode, value=windowpath) window('%s.path' % embynode, value=windowpath)
window('%s.content' % embynode, value=path) window('%s.content' % embynode, value=path)
@ -409,7 +426,7 @@ class VideoNodes(object):
window = utils.window window = utils.window
self.logMsg("Clearing nodes properties.", 1) self.logMsg("Clearing nodes properties.", 1)
embyprops = window('Emby.nodes.total') plexprops = window('Plex.nodes.total')
propnames = [ propnames = [
"index","path","title","content", "index","path","title","content",
@ -424,8 +441,8 @@ class VideoNodes(object):
"inprogressepisodes.content","inprogressepisodes.path" "inprogressepisodes.content","inprogressepisodes.path"
] ]
if embyprops: if plexprops:
totalnodes = int(embyprops) totalnodes = int(plexprops)
for i in range(totalnodes): for i in range(totalnodes):
for prop in propnames: 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: if typus is None:
self.logMsg('No message type, dropping message: %s' % message, -1) self.logMsg('No message type, dropping message: %s' % message, -1)
return False return False
self.logMsg('Received message from PMS server: %s' % message, 2)
# Drop everything we're not interested in # Drop everything we're not interested in
if typus not in ('playing', 'timeline'): if typus not in ('playing', 'timeline'):
return True return True
@ -87,17 +88,16 @@ class WebSocket(threading.Thread):
uri = "%s/:/websockets/notifications" % server uri = "%s/:/websockets/notifications" % server
if token: if token:
uri += '?X-Plex-Token=%s' % token uri += '?X-Plex-Token=%s' % token
return uri
def run(self):
log = self.logMsg
# Currently not working due to missing SSL environment
sslopt = {} sslopt = {}
if utils.settings('sslverify') == "false": if utils.settings('sslverify') == "false":
sslopt["cert_reqs"] = ssl.CERT_NONE sslopt["cert_reqs"] = ssl.CERT_NONE
return uri, sslopt
def run(self):
log = self.logMsg
log("----===## Starting WebSocketClient ##===----", 0) log("----===## Starting WebSocketClient ##===----", 0)
counter = 0
threadStopped = self.threadStopped threadStopped = self.threadStopped
threadSuspended = self.threadSuspended threadSuspended = self.threadSuspended
while not threadStopped(): while not threadStopped():
@ -122,7 +122,7 @@ class WebSocket(threading.Thread):
pass pass
except websocket.WebSocketConnectionClosedException: except websocket.WebSocketConnectionClosedException:
log("Connection closed, (re)connecting", 0) log("Connection closed, (re)connecting", 0)
uri = self.getUri() uri, sslopt = self.getUri()
try: try:
# Low timeout - let's us shut this thread down! # Low timeout - let's us shut this thread down!
self.ws = websocket.create_connection( self.ws = websocket.create_connection(
@ -131,8 +131,15 @@ class WebSocket(threading.Thread):
sslopt=sslopt, sslopt=sslopt,
enable_multithread=True) enable_multithread=True)
except IOError: except IOError:
# Server is probably offline
log("Error connecting", 0) log("Error connecting", 0)
self.ws = None 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) xbmc.sleep(1000)
except websocket.WebSocketTimeoutException: except websocket.WebSocketTimeoutException:
log("timeout while connecting, trying again", 0) log("timeout while connecting, trying again", 0)
@ -142,6 +149,8 @@ class WebSocket(threading.Thread):
log("Unknown exception encountered in connecting: %s" % e) log("Unknown exception encountered in connecting: %s" % e)
self.ws = None self.ws = None
xbmc.sleep(1000) xbmc.sleep(1000)
else:
counter = 0
except Exception as e: except Exception as e:
log("Unknown exception encountered: %s" % e) log("Unknown exception encountered: %s" % e)
try: try:

View file

@ -1,37 +1,30 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?> <?xml version="1.0" encoding="utf-8" standalone="yes"?>
<settings> <settings>
<category label="30014"><!-- Connection --> <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 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 label="39068" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=enterPMS)" option="close" /><!-- Manually enter Plex Media Server IP -->
<setting id="port" label="30030" type="number" default="32400" /> <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="plex_serverowned" label="30031" type="bool" default="true" /><!-- I own this PMS -->
<setting id="https" label="30243" type="bool" default="false" /> <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="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)" /> <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 type="sep" text=""/>
<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 id="enforceUserLogin" label="30536" type="bool" default="false" /> <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="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="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>
<category label="plex.tv"><!-- plex.tv --> <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 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="plexLogin" label="plex.tv username" type="text" default="" visible="false" />
<setting id="plexhome" label="Plex home in use" type="bool" 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="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="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="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 id="serverSync" type="bool" label="30514" default="true" visible="false"/><!-- Enable fast startup (requires server plugin) -->
<setting type="lsep" label="39052" /><!-- Background Sync --> <setting type="lsep" label="39052" /><!-- Background Sync -->
<setting id="enableBackgroundSync" type="bool" label="39026" default="true" visible="true"/> <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="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 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="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="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 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 --> <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="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="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="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>
<category label="39057"><!-- Customize Paths --> <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="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="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="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>
<category label="30516"><!-- Playback --> <category label="30516"><!-- Playback -->
<setting type="sep" /> <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="enableCinema" type="bool" label="30518" default="true" />
<setting id="askCinema" type="bool" label="30519" default="false" visible="eq(-1,true)" subsetting="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" /> <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 id="resumeJumpBack" type="slider" label="30521" default="10" range="0,1,120" option="int" />
<setting type="sep" /> <setting type="sep" />
<setting id="playType" type="enum" label="30002" values="Direct Play (default)|Direct Stream|Force Transcode" default="0" /> <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="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="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="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="failedCount" type="number" visible="false" default="0" />
<setting id="networkCreds" type="text" visible="false" default="" /> <setting id="networkCreds" type="text" visible="false" default="" />
</category> </category>
@ -125,12 +126,17 @@
</category> </category>
<category label="39045"><!-- Appearance Tweaks --> <category label="39073"><!-- Appearance Tweaks -->
<setting id="connectMsg" type="bool" label="30249" default="true" /> <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="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="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="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>
<category label="30022"><!-- Advanced --> <category label="30022"><!-- Advanced -->

View file

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