Merge branch 'translations' into l10n_translations
This commit is contained in:
commit
318c01deb9
23 changed files with 1024 additions and 884 deletions
131
README.md
131
README.md
|
@ -1,7 +1,11 @@
|
||||||
##Status
|
[![stable version](https://img.shields.io/badge/stable_version-1.7.5-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip)
|
||||||
|
[![beta version](https://img.shields.io/badge/beta_version-1.7.5-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip)
|
||||||
|
|
||||||
[![GitHub issues](https://img.shields.io/github/issues/croneter/PlexKodiConnect.svg?maxAge=60&style=flat-square)](https://github.com/croneter/PlexKodiConnect/issues)
|
[![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation)
|
||||||
[![GitHub pull requests](https://img.shields.io/github/issues-pr/croneter/PlexKodiConnect.svg?maxAge=60&style=flat-square)](https://github.com/croneter/PlexKodiConnect/pulls)
|
[![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq)
|
||||||
|
[![Forum](https://img.shields.io/badge/forum-plex-orange.svg?maxAge=60&style=flat)](https://forums.plex.tv/discussion/210023/plexkodiconnect-let-kodi-talk-to-your-plex)
|
||||||
|
|
||||||
|
[![GitHub issues](https://img.shields.io/github/issues/croneter/PlexKodiConnect.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/issues) [![GitHub pull requests](https://img.shields.io/github/issues-pr/croneter/PlexKodiConnect.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/pulls) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/a66870f19ced4fb98f94d9fd56e34e87)](https://www.codacy.com/app/croneter/PlexKodiConnect?utm_source=github.com&utm_medium=referral&utm_content=croneter/PlexKodiConnect&utm_campaign=Badge_Grade)
|
||||||
|
|
||||||
|
|
||||||
# PlexKodiConnect (PKC)
|
# PlexKodiConnect (PKC)
|
||||||
|
@ -11,93 +15,81 @@ PKC combines the best of Kodi - ultra smooth navigation, beautiful and highly cu
|
||||||
|
|
||||||
Have a look at [some screenshots](https://github.com/croneter/PlexKodiConnect/wiki/Some-PKC-Screenshots) to see what's possible.
|
Have a look at [some screenshots](https://github.com/croneter/PlexKodiConnect/wiki/Some-PKC-Screenshots) to see what's possible.
|
||||||
|
|
||||||
### Call for Translations
|
### Please Help Translating
|
||||||
|
|
||||||
Please help translate PlexKodiConnect into your language: [visit crowdin.com](https://crowdin.com/project/plexkodiconnect/invite)
|
Please help translate PlexKodiConnect into your language: [crowdin.com](https://crowdin.com/project/plexkodiconnect/invite)
|
||||||
|
|
||||||
|
|
||||||
### Content
|
### Content
|
||||||
* [**Warning**](#warning)
|
* [**Warning**](#warning)
|
||||||
* [**What does PKC do and how is it different from the official 'Plex for Kodi'**](#what-does-pkc-do-and-how-is-it-different-from-the-official-plex-for-kod)
|
* [**What does PKC do?**](#what-does-pkc-do)
|
||||||
|
* [**PKC Features**](#pkc-features)
|
||||||
* [**Download and Installation**](#download-and-installation)
|
* [**Download and Installation**](#download-and-installation)
|
||||||
* [**Important notes**](#important-notes)
|
* [**Important notes**](#important-notes)
|
||||||
* [**Donations**](#donations)
|
* [**Donations**](#donations)
|
||||||
* [**What is currently supported?**](#what-is-currently-supported)
|
* [**Request a New Feature**](#request-a-new-feature)
|
||||||
* [**Known Larger Issues**](#known-larger-issues)
|
* [**Known Larger Issues**](#known-larger-issues)
|
||||||
* [**Issues being worked on**](#issues-being-worked-on)
|
* [**Issues being worked on**](#issues-being-worked-on)
|
||||||
* [**Requests for new features**](#requests-for-new-features)
|
|
||||||
* [**Checkout the PKC Wiki**](#checkout-the-pkc-wiki)
|
|
||||||
* [**Credits**](#credits)
|
* [**Credits**](#credits)
|
||||||
|
|
||||||
### Warning
|
### Warning
|
||||||
Use at your own risk! This plugin assumes that you manage all your videos with Plex (and none with Kodi). You might lose data already stored in the Kodi video and music databases as this plugin directly changes them. Don't worry if you want Plex to manage all your media (like you should ;-)).
|
Use at your own risk! This plugin assumes that you manage all your videos with Plex (and none with Kodi). You might lose data already stored in the Kodi video and music databases as this plugin directly changes them. Don't worry if you want Plex to manage all your media (like you should ;-)).
|
||||||
|
|
||||||
### What does PKC do and how is it different from the official ['Plex for Kodi'](https://www.plex.tv/apps/computer/kodi/)?
|
|
||||||
|
|
||||||
With other Plex addons for Kodi such as the official [Plex for Kodi](https://www.plex.tv/apps/computer/kodi/) or [PlexBMC](https://forums.plex.tv/discussion/106593/plexbmc-xbmc-add-on-to-connect-to-plex-media-server) there are a couple of issues:
|
|
||||||
- Other Kodi addons such as NextAired, remote apps and others won't work
|
|
||||||
- You can only use special Kodi skins
|
|
||||||
- Slow speed: when browsing data has to be retrieved from the server. Especially on slower devices this can take too much time and you will notice artwork being loaded slowly while you browse the library
|
|
||||||
- All kinds of workarounds are needed to get the best experience on Kodi clients
|
|
||||||
|
|
||||||
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!
|
|
||||||
- Use any Kodi skin you want!
|
|
||||||
- You can browse your media at full speed, images are cached
|
|
||||||
- All other Kodi addons will be able to "see" your media, thinking it's normal Kodi stuff
|
|
||||||
|
|
||||||
Some people argue that PKC is 'hacky' because of the way it directly accesses the Kodi database. See [here for a more thorough discussion](https://github.com/croneter/PlexKodiConnect/wiki/Is-PKC-'hacky'%3F).
|
Some people argue that PKC is 'hacky' because of the way it directly accesses the Kodi database. See [here for a more thorough discussion](https://github.com/croneter/PlexKodiConnect/wiki/Is-PKC-'hacky'%3F).
|
||||||
|
|
||||||
### Download and Installation
|
### What does PKC do?
|
||||||
[ ![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)
|
PKC synchronizes your media from your Plex server to the native Kodi database. Hence:
|
||||||
|
- Use virtually any other Kodi add-on
|
||||||
|
- Use any Kodi skin, completely customize Kodi's look
|
||||||
|
- Browse your media at full speed (cached artwork)
|
||||||
|
- Automatically get additional artwork (more than Plex offers)
|
||||||
|
- Enjoy Plex features using the Kodi interface
|
||||||
|
|
||||||
Install PKC via the 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).
|
### PKC Features
|
||||||
|
|
||||||
**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)
|
- [Amazon Alexa voice recognition](https://www.plex.tv/apps/streaming-devices/amazon-alexa)
|
||||||
|
- [Plex Watch Later / Plex It!](https://support.plex.tv/hc/en-us/sections/200211783-Plex-It-)
|
||||||
### Important Notes
|
- [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 Transcoding](https://support.plex.tv/hc/en-us/articles/200250377-Transcoding-Media)
|
||||||
1. If you 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`
|
- 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)
|
||||||
Don't forget to reboot Kodi after that.
|
- Automatically group movies into [movie sets](http://kodi.wiki/view/movie_sets)
|
||||||
2. **Compatibility**:
|
- [Direct play](https://github.com/croneter/PlexKodiConnect/wiki/Direct-Play) from network paths (e.g. "\\\\server\\Plex\\movie.mkv"), something unique to PKC
|
||||||
* PKC is currently not compatible with Kodi's Video Extras plugin. **Deactivate Video Extras** if trailers/movies start randomly playing.
|
- Delete PMS items from the Kodi context menu
|
||||||
* PKC is not (and will never be) compatible with the **MySQL database replacement** in Kodi. In fact, PKC replaces the MySQL functionality because it acts as a "man in the middle" for your entire media library.
|
- PKC is available in the following languages:
|
||||||
* If **another plugin is not working** like it's supposed to, try to use [PKC direct paths](https://github.com/croneter/PlexKodiConnect/wiki/Direct-Paths)
|
|
||||||
3. If you post logs, your **Plex tokens** might be included. Be sure to double and triple check for tokens before posting any logs anywhere by searching for `token`
|
|
||||||
|
|
||||||
### 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)
|
|
||||||
|
|
||||||
### What is currently supported?
|
|
||||||
|
|
||||||
PKC currently provides the following features:
|
|
||||||
- All Plex library types
|
|
||||||
+ Movies and Home Videos
|
|
||||||
+ TV Shows
|
|
||||||
+ Music
|
|
||||||
+ Pictures and Photos
|
|
||||||
- Different PKC interface languages:
|
|
||||||
+ English
|
+ English
|
||||||
+ German
|
+ German
|
||||||
+ Czech, thanks @Pavuucek
|
+ Czech, thanks @Pavuucek
|
||||||
+ Spanish, thanks @bartolomesoriano
|
+ Spanish, thanks @bartolomesoriano
|
||||||
+ Danish, thanks @FIGHT
|
+ Danish, thanks @FIGHT
|
||||||
+ More coming up: [you can help!](https://crowdin.com/project/plexkodiconnect/invite)
|
+ Italian, thanks @nikkux, @chicco83
|
||||||
- [Plex Watch Later / Plex It!](https://support.plex.tv/hc/en-us/sections/200211783-Plex-It-)
|
+ Dutch, thanks @mvanbaak
|
||||||
- [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
|
+ [Please help translating](https://crowdin.com/project/plexkodiconnect/invite)
|
||||||
- [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)
|
### Download and Installation
|
||||||
+ Banners
|
|
||||||
+ Disc art
|
Install PKC via the PlexKodiConnect Kodi repository below (we cannot use the official Kodi repository as PKC messes with Kodi's databases). See the [github wiki installation manual](https://github.com/croneter/PlexKodiConnect/wiki/Installation) for a detailed guide. Please use the stable version except if you really know what you're doing. Kodi will update PKC automatically.
|
||||||
+ Clear logos
|
|
||||||
+ Landscapes
|
| Stable version | Beta version |
|
||||||
+ Clear art
|
|----------------|--------------|
|
||||||
+ Extra fanart backgrounds
|
| [![stable version](https://img.shields.io/badge/stable_version-latest-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) | [![beta version](https://img.shields.io/badge/beta_version-latest-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) |
|
||||||
- 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)
|
### Important Notes
|
||||||
- Delete PMS items from the Kodi context menu
|
|
||||||
|
1. If you 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). Don't forget to reboot Kodi after that.
|
||||||
|
2. **Compatibility**:
|
||||||
|
* PKC is currently not compatible with Kodi's Video Extras plugin. **Deactivate Video Extras** if trailers/movies start randomly playing.
|
||||||
|
* PKC is not (and will never be) compatible with the **MySQL database replacement** in Kodi. In fact, PKC replaces the MySQL functionality because it acts as a "man in the middle" for your entire media library.
|
||||||
|
* If **another plugin is not working** like it's supposed to, try to use [PKC direct paths](https://github.com/croneter/PlexKodiConnect/wiki/Direct-Paths)
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
[![Donations](https://az743702.vo.msecnd.net/cdn/kofi1.png?v=a)](https://ko-fi.com/A8182EB)
|
||||||
|
|
||||||
|
### Request a New Feature
|
||||||
|
|
||||||
|
[![Feature Requests](http://feathub.com/croneter/PlexKodiConnect?format=svg)](http://feathub.com/croneter/PlexKodiConnect)
|
||||||
|
|
||||||
### Known Larger Issues
|
### Known Larger Issues
|
||||||
|
|
||||||
|
@ -120,13 +112,6 @@ However, some changes to individual items are instantly detected, e.g. if you ma
|
||||||
Have a look at the [Github Issues Page](https://github.com/croneter/PlexKodiConnect/issues). Before you open your own issue, please read [How to report a bug](https://github.com/croneter/PlexKodiConnect/wiki/How-to-Report-A-Bug).
|
Have a look at the [Github Issues Page](https://github.com/croneter/PlexKodiConnect/issues). Before you open your own issue, please read [How to report a bug](https://github.com/croneter/PlexKodiConnect/wiki/How-to-Report-A-Bug).
|
||||||
|
|
||||||
|
|
||||||
### Requests for new features
|
|
||||||
|
|
||||||
[![Feature Requests](http://feathub.com/croneter/PlexKodiConnect?format=svg)](http://feathub.com/croneter/PlexKodiConnect)
|
|
||||||
|
|
||||||
### Checkout the PKC Wiki
|
|
||||||
The [Wiki can be found here](https://github.com/croneter/PlexKodiConnect/wiki) and will hopefully answer all your questions. You can even edit the wiki yourself!
|
|
||||||
|
|
||||||
### 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!!
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="1.6.5" provider-name="croneter">
|
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="1.7.5" provider-name="croneter">
|
||||||
<requires>
|
<requires>
|
||||||
<import addon="xbmc.python" version="2.1.0"/>
|
<import addon="xbmc.python" version="2.1.0"/>
|
||||||
<import addon="script.module.requests" version="2.3.0" />
|
<import addon="script.module.requests" version="2.3.0" />
|
||||||
|
|
|
@ -1,3 +1,37 @@
|
||||||
|
version 1.7.5
|
||||||
|
- Dutch translation, thanks @mvanbaak
|
||||||
|
|
||||||
|
version 1.7.4 (beta only)
|
||||||
|
- Show menu item only for appropriate Kodi library: Be careful to start video content through Videos -> Video Addons -> ... and pictures through Pictures -> Picture Addons -> ...
|
||||||
|
- Fix playback error popup when using Alexa
|
||||||
|
- New Italian translations, thanks @nikkux, @chicco83
|
||||||
|
- Update translations
|
||||||
|
- Rewire Kodi ListItem stuff
|
||||||
|
- Fix TypeError for setting ListItem streams
|
||||||
|
- Fix Kodi setContent for images
|
||||||
|
- Fix AttributeError due to missing Kodi sort methods
|
||||||
|
|
||||||
|
version 1.7.3 (beta only)
|
||||||
|
- Fix KeyError for channels if no media streams
|
||||||
|
- Move plex node navigation, playback to main thread
|
||||||
|
- Fix TypeError for malformed browsing xml
|
||||||
|
- Fix IndexError if we can't get a valid xml from PMS
|
||||||
|
- Pass 'None' instead of empty string in url args
|
||||||
|
|
||||||
|
version 1.7.2
|
||||||
|
- Fix for some channels not starting playback
|
||||||
|
|
||||||
|
version 1.7.1
|
||||||
|
- Fix Alexa not doing anything
|
||||||
|
|
||||||
|
version 1.7.0
|
||||||
|
- Amazon Alexa support! Be sure to check the Plex Alexa forum first if you encounter issues; there are still many bugs completely unrelated to PKC
|
||||||
|
- Plex Channels!
|
||||||
|
- Browse video nodes by folder/path
|
||||||
|
- Fix IndexError for playqueues
|
||||||
|
- Update translations
|
||||||
|
- Code optimization
|
||||||
|
|
||||||
version 1.6.5 (beta only)
|
version 1.6.5 (beta only)
|
||||||
- Plex Channels!
|
- Plex Channels!
|
||||||
- Browse video nodes by folder/path
|
- Browse video nodes by folder/path
|
||||||
|
|
11
default.py
11
default.py
|
@ -63,6 +63,9 @@ class Main():
|
||||||
if mode == 'play':
|
if mode == 'play':
|
||||||
self.play()
|
self.play()
|
||||||
|
|
||||||
|
elif mode == 'plex_node':
|
||||||
|
self.play()
|
||||||
|
|
||||||
elif mode == 'ondeck':
|
elif mode == 'ondeck':
|
||||||
entrypoint.getOnDeck(itemid,
|
entrypoint.getOnDeck(itemid,
|
||||||
params.get('type'),
|
params.get('type'),
|
||||||
|
@ -83,9 +86,6 @@ class Main():
|
||||||
entrypoint.getInProgressEpisodes(params['tagname'],
|
entrypoint.getInProgressEpisodes(params['tagname'],
|
||||||
int(params['limit']))
|
int(params['limit']))
|
||||||
|
|
||||||
elif mode == 'Plex_Node':
|
|
||||||
entrypoint.Plex_Node(itemid, params.get('viewOffset'))
|
|
||||||
|
|
||||||
elif mode == 'browseplex':
|
elif mode == 'browseplex':
|
||||||
entrypoint.browse_plex(key=params.get('key'),
|
entrypoint.browse_plex(key=params.get('key'),
|
||||||
plex_section_id=params.get('id'))
|
plex_section_id=params.get('id'))
|
||||||
|
@ -165,9 +165,12 @@ class Main():
|
||||||
entrypoint.getVideoFiles(plexId, params)
|
entrypoint.getVideoFiles(plexId, params)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
entrypoint.doMainListing()
|
entrypoint.doMainListing(content_type=params.get('content_type'))
|
||||||
|
|
||||||
def play(self):
|
def play(self):
|
||||||
|
"""
|
||||||
|
Start up playback_starter in main Python thread
|
||||||
|
"""
|
||||||
# Put the request into the 'queue'
|
# Put the request into the 'queue'
|
||||||
while window('plex_play_new_item'):
|
while window('plex_play_new_item'):
|
||||||
sleep(50)
|
sleep(50)
|
||||||
|
|
|
@ -236,9 +236,9 @@
|
||||||
|
|
||||||
<string id="30251">Senest tilføjet hjemmevideoer</string><!-- Verified -->
|
<string id="30251">Senest tilføjet hjemmevideoer</string><!-- Verified -->
|
||||||
<string id="30252">Senest tilføjet billeder</string><!-- Verified -->
|
<string id="30252">Senest tilføjet billeder</string><!-- Verified -->
|
||||||
<string id="30253">Favorite Home Videos</string><!-- Verified -->
|
<string id="30253">Favorit hjemmevideoer</string><!-- Verified -->
|
||||||
<string id="30254">Favorite Photos</string><!-- Verified -->
|
<string id="30254">Favorit fotos</string><!-- Verified -->
|
||||||
<string id="30255">Favorite Albums</string>
|
<string id="30255">Favorit albums</string>
|
||||||
|
|
||||||
<string id="30256">Senest tilføjet musik videoer</string><!-- Verified -->
|
<string id="30256">Senest tilføjet musik videoer</string><!-- Verified -->
|
||||||
<string id="30257">Igangværende musikvideoer</string><!-- Verified -->
|
<string id="30257">Igangværende musikvideoer</string><!-- Verified -->
|
||||||
|
|
|
@ -236,9 +236,9 @@
|
||||||
|
|
||||||
<string id="30251">Video aggiunti di recente</string><!-- Verified -->
|
<string id="30251">Video aggiunti di recente</string><!-- Verified -->
|
||||||
<string id="30252">Foto aggiunte di recente</string><!-- Verified -->
|
<string id="30252">Foto aggiunte di recente</string><!-- Verified -->
|
||||||
<string id="30253">Favorite Home Videos</string><!-- Verified -->
|
<string id="30253">Video preferiti</string><!-- Verified -->
|
||||||
<string id="30254">Favorite Photos</string><!-- Verified -->
|
<string id="30254">Foto preferite</string><!-- Verified -->
|
||||||
<string id="30255">Favorite Albums</string>
|
<string id="30255">Album preferiti</string>
|
||||||
|
|
||||||
<string id="30256">Video Musicali aggiuni di recente</string><!-- Verified -->
|
<string id="30256">Video Musicali aggiuni di recente</string><!-- Verified -->
|
||||||
<string id="30257">Video Musicali in corso</string><!-- Verified -->
|
<string id="30257">Video Musicali in corso</string><!-- Verified -->
|
||||||
|
|
|
@ -14,18 +14,23 @@ def convert_PKC_to_listitem(PKC_listitem):
|
||||||
"""
|
"""
|
||||||
Insert a PKC_listitem and you will receive a valid XBMC listitem
|
Insert a PKC_listitem and you will receive a valid XBMC listitem
|
||||||
"""
|
"""
|
||||||
listitem = ListItem()
|
data = PKC_listitem.data
|
||||||
for func, args in PKC_listitem.data.items():
|
log.debug('data is: %s' % data)
|
||||||
if isinstance(args, list):
|
listitem = ListItem(label=data.get('label'),
|
||||||
for arg in args:
|
label2=data.get('label2'),
|
||||||
getattr(listitem, func)(*arg)
|
path=data.get('path'))
|
||||||
elif isinstance(args, dict):
|
if data['info']:
|
||||||
for arg in args.items():
|
listitem.setInfo(**data['info'])
|
||||||
getattr(listitem, func)(*arg)
|
for stream in data['stream_info']:
|
||||||
elif args is None:
|
# Kodi documentation up to date? CAREFUL as type= seems to be cType=
|
||||||
continue
|
# and values= seems to be dictionary=
|
||||||
else:
|
listitem.addStreamInfo(**stream)
|
||||||
getattr(listitem, func)(args)
|
if data['art']:
|
||||||
|
listitem.setArt(data['art'])
|
||||||
|
for key, value in data['property'].iteritems():
|
||||||
|
listitem.setProperty(key, value)
|
||||||
|
if data['subtitles']:
|
||||||
|
listitem.setSubtitles(data['subtitles'])
|
||||||
return listitem
|
return listitem
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,14 +43,14 @@ class PKC_ListItem(object):
|
||||||
"""
|
"""
|
||||||
def __init__(self, label=None, label2=None, path=None):
|
def __init__(self, label=None, label2=None, path=None):
|
||||||
self.data = {
|
self.data = {
|
||||||
'addStreamInfo': [], # (type, values: dict { label: value })
|
'stream_info': [], # (type, values: dict { label: value })
|
||||||
'setArt': [], # dict: { label: value }
|
'art': {}, # dict
|
||||||
'setInfo': {}, # type: infoLabel (dict { label: value })
|
'info': {}, # type: infoLabel (dict { label: value })
|
||||||
'setLabel': label, # string
|
'label': label, # string
|
||||||
'setLabel2': label2, # string
|
'label2': label2, # string
|
||||||
'setPath': path, # string
|
'path': path, # string
|
||||||
'setProperty': {}, # (key, value)
|
'property': {}, # (key, value)
|
||||||
'setSubtitles': [], # string
|
'subtitles': [], # strings
|
||||||
}
|
}
|
||||||
|
|
||||||
def addContextMenuItems(self, items, replaceItems):
|
def addContextMenuItems(self, items, replaceItems):
|
||||||
|
@ -87,19 +92,19 @@ class PKC_ListItem(object):
|
||||||
- Subtitle Values:
|
- Subtitle Values:
|
||||||
- language : string (en)
|
- language : string (en)
|
||||||
"""
|
"""
|
||||||
self.data['addStreamInfo'].append((type, values))
|
self.data['stream_info'].append({'cType': type, 'dictionary': values})
|
||||||
|
|
||||||
def getLabel(self):
|
def getLabel(self):
|
||||||
"""
|
"""
|
||||||
Returns the listitem label
|
Returns the listitem label
|
||||||
"""
|
"""
|
||||||
return self.data['setLabel']
|
return self.data.get('label')
|
||||||
|
|
||||||
def getLabel2(self):
|
def getLabel2(self):
|
||||||
"""
|
"""
|
||||||
Returns the listitem label.
|
Returns the listitem label.
|
||||||
"""
|
"""
|
||||||
return self.data['setLabel2']
|
return self.data.get('label2')
|
||||||
|
|
||||||
def getMusicInfoTag(self):
|
def getMusicInfoTag(self):
|
||||||
"""
|
"""
|
||||||
|
@ -118,7 +123,7 @@ class PKC_ListItem(object):
|
||||||
|
|
||||||
Once you use a keyword, all following arguments require the keyword.
|
Once you use a keyword, all following arguments require the keyword.
|
||||||
"""
|
"""
|
||||||
return self.data['setProperty'].get(key)
|
return self.data['property'].get(key)
|
||||||
|
|
||||||
def getVideoInfoTag(self):
|
def getVideoInfoTag(self):
|
||||||
"""
|
"""
|
||||||
|
@ -172,7 +177,7 @@ class PKC_ListItem(object):
|
||||||
- landscape : string - image filename
|
- landscape : string - image filename
|
||||||
- icon : string - image filename
|
- icon : string - image filename
|
||||||
"""
|
"""
|
||||||
self.data['setArt'].append(values)
|
self.data['art'].update(values)
|
||||||
|
|
||||||
def setContentLookup(self, enable):
|
def setContentLookup(self, enable):
|
||||||
"""
|
"""
|
||||||
|
@ -270,21 +275,21 @@ class PKC_ListItem(object):
|
||||||
- exif : string (See CPictureInfoTag::TranslateString in
|
- exif : string (See CPictureInfoTag::TranslateString in
|
||||||
PictureInfoTag.cpp for valid strings)
|
PictureInfoTag.cpp for valid strings)
|
||||||
"""
|
"""
|
||||||
self.data['setInfo'][type] = infoLabels
|
self.data['info'] = {'type': type, 'infoLabels': infoLabels}
|
||||||
|
|
||||||
def setLabel(self, label):
|
def setLabel(self, label):
|
||||||
"""
|
"""
|
||||||
Sets the listitem's label.
|
Sets the listitem's label.
|
||||||
label : string or unicode - text string.
|
label : string or unicode - text string.
|
||||||
"""
|
"""
|
||||||
self.data['setLabel'] = label
|
self.data['label'] = label
|
||||||
|
|
||||||
def setLabel2(self, label):
|
def setLabel2(self, label):
|
||||||
"""
|
"""
|
||||||
Sets the listitem's label2.
|
Sets the listitem's label2.
|
||||||
label : string or unicode - text string.
|
label : string or unicode - text string.
|
||||||
"""
|
"""
|
||||||
self.data['setLabel2'] = label
|
self.data['label2'] = label
|
||||||
|
|
||||||
def setMimeType(self, mimetype):
|
def setMimeType(self, mimetype):
|
||||||
"""
|
"""
|
||||||
|
@ -303,7 +308,7 @@ class PKC_ListItem(object):
|
||||||
|
|
||||||
*Note, You can use the above as keywords for arguments.
|
*Note, You can use the above as keywords for arguments.
|
||||||
"""
|
"""
|
||||||
self.data['setPath'] = path
|
self.data['path'] = path
|
||||||
|
|
||||||
def setProperty(self, key, value):
|
def setProperty(self, key, value):
|
||||||
"""
|
"""
|
||||||
|
@ -321,7 +326,7 @@ class PKC_ListItem(object):
|
||||||
start playback of an item. Others may be used in the skin to add extra
|
start playback of an item. Others may be used in the skin to add extra
|
||||||
information, such as 'WatchedCount' for tvshow items
|
information, such as 'WatchedCount' for tvshow items
|
||||||
"""
|
"""
|
||||||
self.data['setProperty'][key] = value
|
self.data['property'][key] = value
|
||||||
|
|
||||||
def setSubtitles(self, subtitles):
|
def setSubtitles(self, subtitles):
|
||||||
"""
|
"""
|
||||||
|
@ -331,4 +336,4 @@ class PKC_ListItem(object):
|
||||||
- listitem.setSubtitles(['special://temp/example.srt',
|
- listitem.setSubtitles(['special://temp/example.srt',
|
||||||
'http://example.com/example.srt' ])
|
'http://example.com/example.srt' ])
|
||||||
"""
|
"""
|
||||||
self.data['setSubtitles'].extend(([subtitles],))
|
self.data['subtitles'].extend(subtitles)
|
||||||
|
|
|
@ -39,6 +39,7 @@ import xml.etree.ElementTree as etree
|
||||||
from re import compile as re_compile, sub
|
from re import compile as re_compile, sub
|
||||||
from json import dumps
|
from json import dumps
|
||||||
from urllib import urlencode, quote_plus, unquote
|
from urllib import urlencode, quote_plus, unquote
|
||||||
|
from os import path as os_path
|
||||||
|
|
||||||
import xbmcgui
|
import xbmcgui
|
||||||
from xbmc import sleep, executebuiltin
|
from xbmc import sleep, executebuiltin
|
||||||
|
@ -1208,6 +1209,30 @@ class API():
|
||||||
ans = unquote(ans).decode('latin1')
|
ans = unquote(ans).decode('latin1')
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
def get_picture_path(self):
|
||||||
|
"""
|
||||||
|
Returns the item's picture path (transcode, if necessary) as string.
|
||||||
|
Will always use addon paths, never direct paths
|
||||||
|
"""
|
||||||
|
extension = self.item[0][0].attrib['key'][self.item[0][0].attrib['key'].rfind('.'):].lower()
|
||||||
|
if (window('plex_force_transcode_pix') == 'true' or
|
||||||
|
extension not in v.KODI_SUPPORTED_IMAGES):
|
||||||
|
# Let Plex transcode
|
||||||
|
# max width/height supported by plex image transcoder is 1920x1080
|
||||||
|
path = self.server + PlexAPI().getTranscodeImagePath(
|
||||||
|
self.item[0][0].attrib.get('key'),
|
||||||
|
window('pms_token'),
|
||||||
|
"%s%s" % (self.server, self.item[0][0].attrib.get('key')),
|
||||||
|
1920,
|
||||||
|
1080)
|
||||||
|
else:
|
||||||
|
path = self.addPlexCredentialsToUrl(
|
||||||
|
'%s%s' % (window('pms_server'),
|
||||||
|
self.item[0][0].attrib['key']))
|
||||||
|
# Attach Plex id to url to let it be picked up by our playqueue agent
|
||||||
|
# later
|
||||||
|
return tryEncode('%s&plex_id=%s' % (path, self.getRatingKey()))
|
||||||
|
|
||||||
def getTVShowPath(self):
|
def getTVShowPath(self):
|
||||||
"""
|
"""
|
||||||
Returns the direct path to the TV show, e.g. '\\NAS\tv\series'
|
Returns the direct path to the TV show, e.g. '\\NAS\tv\series'
|
||||||
|
@ -2162,13 +2187,38 @@ class API():
|
||||||
# Several streams/files available.
|
# Several streams/files available.
|
||||||
dialoglist = []
|
dialoglist = []
|
||||||
for entry in self.item.findall('./Media'):
|
for entry in self.item.findall('./Media'):
|
||||||
dialoglist.append(
|
# Get additional info (filename / languages)
|
||||||
"%sp %s - %s (%s)"
|
filename = None
|
||||||
% (entry.attrib.get('videoResolution', 'unknown'),
|
if 'file' in entry[0].attrib:
|
||||||
entry.attrib.get('videoCodec', 'unknown'),
|
filename = os_path.basename(entry[0].attrib['file'])
|
||||||
entry.attrib.get('audioProfile', 'unknown'),
|
# Languages of audio streams
|
||||||
entry.attrib.get('audioCodec', 'unknown'))
|
languages = []
|
||||||
)
|
for stream in entry[0]:
|
||||||
|
if (stream.attrib['streamType'] == '1' and
|
||||||
|
'language' in stream.attrib):
|
||||||
|
languages.append(stream.attrib['language'])
|
||||||
|
languages = ', '.join(languages)
|
||||||
|
if filename:
|
||||||
|
option = tryEncode(filename)
|
||||||
|
if languages:
|
||||||
|
if option:
|
||||||
|
option = '%s (%s): ' % (option, tryEncode(languages))
|
||||||
|
else:
|
||||||
|
option = '%s: ' % tryEncode(languages)
|
||||||
|
if 'videoResolution' in entry.attrib:
|
||||||
|
option = '%s%sp ' % (option,
|
||||||
|
entry.attrib.get('videoResolution'))
|
||||||
|
if 'videoCodec' in entry.attrib:
|
||||||
|
option = '%s%s' % (option,
|
||||||
|
entry.attrib.get('videoCodec'))
|
||||||
|
option = option.strip() + ' - '
|
||||||
|
if 'audioProfile' in entry.attrib:
|
||||||
|
option = '%s%s ' % (option,
|
||||||
|
entry.attrib.get('audioProfile'))
|
||||||
|
if 'audioCodec' in entry.attrib:
|
||||||
|
option = '%s%s ' % (option,
|
||||||
|
entry.attrib.get('audioCodec'))
|
||||||
|
dialoglist.append(option)
|
||||||
media = xbmcgui.Dialog().select('Select stream', dialoglist)
|
media = xbmcgui.Dialog().select('Select stream', dialoglist)
|
||||||
else:
|
else:
|
||||||
media = 0
|
media = 0
|
||||||
|
@ -2275,18 +2325,6 @@ class API():
|
||||||
log.info('Found external subs: %s' % externalsubs)
|
log.info('Found external subs: %s' % externalsubs)
|
||||||
return externalsubs
|
return externalsubs
|
||||||
|
|
||||||
def CreateListItemFromPlexItem(self,
|
|
||||||
listItem=None,
|
|
||||||
appendShowTitle=False,
|
|
||||||
appendSxxExx=False):
|
|
||||||
if self.getType() == 'photo':
|
|
||||||
listItem = self._createPhotoListItem(listItem)
|
|
||||||
else:
|
|
||||||
listItem = self._createVideoListItem(listItem,
|
|
||||||
appendShowTitle,
|
|
||||||
appendSxxExx)
|
|
||||||
return listItem
|
|
||||||
|
|
||||||
def GetKodiPremierDate(self):
|
def GetKodiPremierDate(self):
|
||||||
"""
|
"""
|
||||||
Takes Plex' originallyAvailableAt of the form "yyyy-mm-dd" and returns
|
Takes Plex' originallyAvailableAt of the form "yyyy-mm-dd" and returns
|
||||||
|
@ -2301,7 +2339,24 @@ class API():
|
||||||
date = None
|
date = None
|
||||||
return date
|
return date
|
||||||
|
|
||||||
def _createPhotoListItem(self, listItem=None):
|
def CreateListItemFromPlexItem(self,
|
||||||
|
listItem=None,
|
||||||
|
appendShowTitle=False,
|
||||||
|
appendSxxExx=False):
|
||||||
|
if self.getType() == v.PLEX_TYPE_PHOTO:
|
||||||
|
listItem = self.__createPhotoListItem(listItem)
|
||||||
|
# Only set the bare minimum of artwork
|
||||||
|
listItem.setArt({'icon': 'DefaultPicture.png',
|
||||||
|
'fanart': self.__getOneArtwork('thumb')})
|
||||||
|
else:
|
||||||
|
listItem = self.__createVideoListItem(listItem,
|
||||||
|
appendShowTitle,
|
||||||
|
appendSxxExx)
|
||||||
|
self.add_video_streams(listItem)
|
||||||
|
self.set_listitem_artwork(listItem)
|
||||||
|
return listItem
|
||||||
|
|
||||||
|
def __createPhotoListItem(self, listItem=None):
|
||||||
"""
|
"""
|
||||||
Use for photo items only
|
Use for photo items only
|
||||||
"""
|
"""
|
||||||
|
@ -2310,63 +2365,21 @@ class API():
|
||||||
listItem = xbmcgui.ListItem(title)
|
listItem = xbmcgui.ListItem(title)
|
||||||
else:
|
else:
|
||||||
listItem.setLabel(title)
|
listItem.setLabel(title)
|
||||||
listItem.setProperty('IsPlayable', 'true')
|
|
||||||
extension = self.item[0][0].attrib['key'][self.item[0][0].attrib['key'].rfind('.'):].lower()
|
|
||||||
if (window('plex_force_transcode_pix') == 'true' or
|
|
||||||
extension not in v.KODI_SUPPORTED_IMAGES):
|
|
||||||
# Let Plex transcode
|
|
||||||
# max width/height supported by plex image transcoder is 1920x1080
|
|
||||||
path = self.server + PlexAPI().getTranscodeImagePath(
|
|
||||||
self.item[0][0].attrib.get('key'),
|
|
||||||
window('pms_token'),
|
|
||||||
"%s%s" % (self.server, self.item[0][0].attrib.get('key')),
|
|
||||||
1920,
|
|
||||||
1080)
|
|
||||||
else:
|
|
||||||
# Don't transcode
|
|
||||||
if window('useDirectPaths') == 'true':
|
|
||||||
# Addon Mode. Just give the path of the file to Kodi
|
|
||||||
path = self.addPlexCredentialsToUrl(
|
|
||||||
'%s%s' % (window('pms_server'),
|
|
||||||
self.item[0][0].attrib['key']))
|
|
||||||
else:
|
|
||||||
# Native direct paths
|
|
||||||
path = self.validatePlayurl(
|
|
||||||
self.getFilePath(forceFirstMediaStream=True),
|
|
||||||
'photo')
|
|
||||||
|
|
||||||
path = tryEncode(path)
|
|
||||||
metadata = {
|
metadata = {
|
||||||
'date': self.GetKodiPremierDate(),
|
'date': self.GetKodiPremierDate(),
|
||||||
'picturepath': path,
|
|
||||||
'size': long(self.item[0][0].attrib.get('size', 0)),
|
'size': long(self.item[0][0].attrib.get('size', 0)),
|
||||||
'exif:width': self.item[0].attrib.get('width', ''),
|
'exif:width': self.item[0].attrib.get('width', ''),
|
||||||
'exif:height': self.item[0].attrib.get('height', ''),
|
'exif:height': self.item[0].attrib.get('height', ''),
|
||||||
'title': title
|
|
||||||
}
|
}
|
||||||
listItem.setInfo('pictures', infoLabels=metadata)
|
listItem.setInfo(type='image', infoLabels=metadata)
|
||||||
try:
|
|
||||||
if int(metadata['exif:width']) > int(metadata['exif:height']):
|
|
||||||
# add image as fanart for use with skinhelper auto thumb/
|
|
||||||
# backgrund creation
|
|
||||||
listItem.setArt({'fanart': path})
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
# Stuff that we CANNOT set with listItem.setInfo
|
|
||||||
listItem.setProperty('path', path)
|
|
||||||
listItem.setProperty('plot', self.getPlot())
|
listItem.setProperty('plot', self.getPlot())
|
||||||
listItem.setProperty('plexid', self.getRatingKey())
|
listItem.setProperty('plexid', self.getRatingKey())
|
||||||
# We do NOT set these props
|
|
||||||
# listItem.setProperty('isPlayable', 'true')
|
|
||||||
# listItem.setProperty('isFolder', 'true')
|
|
||||||
# Further stuff
|
|
||||||
listItem.setArt({'icon': 'DefaultPicture.png'})
|
|
||||||
return listItem
|
return listItem
|
||||||
|
|
||||||
def _createVideoListItem(self,
|
def __createVideoListItem(self,
|
||||||
listItem=None,
|
listItem=None,
|
||||||
appendShowTitle=False,
|
appendShowTitle=False,
|
||||||
appendSxxExx=False):
|
appendSxxExx=False):
|
||||||
"""
|
"""
|
||||||
Use for video items only
|
Use for video items only
|
||||||
Call on a child level of PMS xml response (e.g. in a for loop)
|
Call on a child level of PMS xml response (e.g. in a for loop)
|
||||||
|
@ -2383,8 +2396,10 @@ class API():
|
||||||
|
|
||||||
if listItem is None:
|
if listItem is None:
|
||||||
listItem = xbmcgui.ListItem(title)
|
listItem = xbmcgui.ListItem(title)
|
||||||
|
else:
|
||||||
|
listItem.setLabel(title)
|
||||||
|
# Necessary; Kodi won't start video otherwise!
|
||||||
listItem.setProperty('IsPlayable', 'true')
|
listItem.setProperty('IsPlayable', 'true')
|
||||||
|
|
||||||
# Video items, e.g. movies and episodes or clips
|
# Video items, e.g. movies and episodes or clips
|
||||||
people = self.getPeople()
|
people = self.getPeople()
|
||||||
userdata = self.getUserData()
|
userdata = self.getUserData()
|
||||||
|
@ -2410,8 +2425,7 @@ class API():
|
||||||
listItem.setProperty('resumetime', str(userdata['Resume']))
|
listItem.setProperty('resumetime', str(userdata['Resume']))
|
||||||
listItem.setProperty('totaltime', str(userdata['Runtime']))
|
listItem.setProperty('totaltime', str(userdata['Runtime']))
|
||||||
|
|
||||||
if typus == "episode":
|
if typus == v.PLEX_TYPE_EPISODE:
|
||||||
# Only for tv shows
|
|
||||||
key, show, season, episode = self.getEpisodeDetails()
|
key, show, season, episode = self.getEpisodeDetails()
|
||||||
season = -1 if season is None else int(season)
|
season = -1 if season is None else int(season)
|
||||||
episode = -1 if episode is None else int(episode)
|
episode = -1 if episode is None else int(episode)
|
||||||
|
@ -2426,7 +2440,9 @@ class API():
|
||||||
listItem.setArt({'icon': 'DefaultTVShows.png'})
|
listItem.setArt({'icon': 'DefaultTVShows.png'})
|
||||||
if appendShowTitle is True:
|
if appendShowTitle is True:
|
||||||
title = "%s - %s " % (show, title)
|
title = "%s - %s " % (show, title)
|
||||||
elif typus == "movie":
|
if appendShowTitle or appendSxxExx:
|
||||||
|
listItem.setLabel(title)
|
||||||
|
elif typus == v.PLEX_TYPE_MOVIE:
|
||||||
listItem.setArt({'icon': 'DefaultMovies.png'})
|
listItem.setArt({'icon': 'DefaultMovies.png'})
|
||||||
else:
|
else:
|
||||||
# E.g. clips, trailers, ...
|
# E.g. clips, trailers, ...
|
||||||
|
@ -2442,11 +2458,10 @@ class API():
|
||||||
pass
|
pass
|
||||||
# Expensive operation
|
# Expensive operation
|
||||||
metadata['title'] = title
|
metadata['title'] = title
|
||||||
listItem.setLabel(title)
|
|
||||||
listItem.setInfo('video', infoLabels=metadata)
|
listItem.setInfo('video', infoLabels=metadata)
|
||||||
return listItem
|
return listItem
|
||||||
|
|
||||||
def AddStreamInfo(self, listItem):
|
def add_video_streams(self, listItem):
|
||||||
"""
|
"""
|
||||||
Add media stream information to xbmcgui.ListItem
|
Add media stream information to xbmcgui.ListItem
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -3,20 +3,18 @@ import logging
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
import Queue
|
import Queue
|
||||||
from socket import SHUT_RDWR
|
from socket import SHUT_RDWR
|
||||||
|
from urllib import urlencode
|
||||||
|
|
||||||
from xbmc import sleep
|
from xbmc import sleep, executebuiltin
|
||||||
|
|
||||||
from utils import settings, ThreadMethodsAdditionalSuspend, ThreadMethods, \
|
from utils import settings, ThreadMethodsAdditionalSuspend, ThreadMethods
|
||||||
window
|
|
||||||
from plexbmchelper import listener, plexgdm, subscribers, functions, \
|
from plexbmchelper import listener, plexgdm, subscribers, functions, \
|
||||||
httppersist, plexsettings
|
httppersist, plexsettings
|
||||||
from PlexFunctions import ParseContainerKey, GetPlexMetadata
|
from PlexFunctions import ParseContainerKey, GetPlexMetadata
|
||||||
from PlexAPI import API
|
from PlexAPI import API
|
||||||
import player
|
import player
|
||||||
from entrypoint import Plex_Node
|
|
||||||
import variables as v
|
import variables as v
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
log = logging.getLogger("PLEX."+__name__)
|
||||||
|
@ -93,23 +91,27 @@ class PlexCompanion(Thread):
|
||||||
self.mgr.playqueue.init_playqueue_from_plex_children(
|
self.mgr.playqueue.init_playqueue_from_plex_children(
|
||||||
api.getRatingKey())
|
api.getRatingKey())
|
||||||
else:
|
else:
|
||||||
thread = Thread(target=Plex_Node,
|
params = {
|
||||||
args=('{server}%s' % data.get('key'),
|
'mode': 'plex_node',
|
||||||
data.get('offset'),
|
'key': '{server}%s' % data.get('key'),
|
||||||
True,
|
'view_offset': data.get('offset'),
|
||||||
False),)
|
'play_directly': 'true',
|
||||||
thread.setDaemon(True)
|
'node': 'false'
|
||||||
thread.start()
|
}
|
||||||
|
executebuiltin('RunPlugin(plugin://%s?%s)'
|
||||||
|
% (v.ADDON_ID, urlencode(params)))
|
||||||
|
|
||||||
elif (task['action'] == 'playlist' and
|
elif (task['action'] == 'playlist' and
|
||||||
data.get('address') == 'node.plexapp.com'):
|
data.get('address') == 'node.plexapp.com'):
|
||||||
# E.g. watch later initiated by Companion
|
# E.g. watch later initiated by Companion
|
||||||
thread = Thread(target=Plex_Node,
|
params = {
|
||||||
args=('{server}%s' % data.get('key'),
|
'mode': 'plex_node',
|
||||||
data.get('offset'),
|
'key': '{server}%s' % data.get('key'),
|
||||||
True),)
|
'view_offset': data.get('offset'),
|
||||||
thread.setDaemon(True)
|
'play_directly': 'true'
|
||||||
thread.start()
|
}
|
||||||
|
executebuiltin('RunPlugin(plugin://%s?%s)'
|
||||||
|
% (v.ADDON_ID, urlencode(params)))
|
||||||
|
|
||||||
elif task['action'] == 'playlist':
|
elif task['action'] == 'playlist':
|
||||||
# Get the playqueue ID
|
# Get the playqueue ID
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import logging
|
from logging import getLogger
|
||||||
from urllib import urlencode
|
from urllib import urlencode
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
from urlparse import urlparse, parse_qsl
|
from urlparse import urlparse, parse_qsl
|
||||||
|
@ -12,7 +12,9 @@ from variables import PLEX_TO_KODI_TIMEFACTOR
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
log = getLogger("PLEX."+__name__)
|
||||||
|
|
||||||
|
CONTAINERSIZE = int(settings('limitindex'))
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -141,7 +143,7 @@ def GetPlexMetadata(key):
|
||||||
return xml
|
return xml
|
||||||
|
|
||||||
|
|
||||||
def GetAllPlexChildren(key, containerSize=None):
|
def GetAllPlexChildren(key):
|
||||||
"""
|
"""
|
||||||
Returns a list (raw xml API dump) of all Plex children for the key.
|
Returns a list (raw xml API dump) of all Plex children for the key.
|
||||||
(e.g. /library/metadata/194853/children pointing to a season)
|
(e.g. /library/metadata/194853/children pointing to a season)
|
||||||
|
@ -149,11 +151,10 @@ def GetAllPlexChildren(key, containerSize=None):
|
||||||
Input:
|
Input:
|
||||||
key Key to a Plex item, e.g. 12345
|
key Key to a Plex item, e.g. 12345
|
||||||
"""
|
"""
|
||||||
url = "{server}/library/metadata/%s/children?" % key
|
return DownloadChunks("{server}/library/metadata/%s/children?" % key)
|
||||||
return DownloadChunks(url, containerSize)
|
|
||||||
|
|
||||||
|
|
||||||
def GetPlexSectionResults(viewId, args=None, containerSize=None):
|
def GetPlexSectionResults(viewId, args=None):
|
||||||
"""
|
"""
|
||||||
Returns a list (XML API dump) of all Plex items in the Plex
|
Returns a list (XML API dump) of all Plex items in the Plex
|
||||||
section with key = viewId.
|
section with key = viewId.
|
||||||
|
@ -166,38 +167,23 @@ def GetPlexSectionResults(viewId, args=None, containerSize=None):
|
||||||
url = "{server}/library/sections/%s/all?" % viewId
|
url = "{server}/library/sections/%s/all?" % viewId
|
||||||
if args:
|
if args:
|
||||||
url += urlencode(args) + '&'
|
url += urlencode(args) + '&'
|
||||||
return DownloadChunks(url, containerSize)
|
return DownloadChunks(url)
|
||||||
|
|
||||||
|
|
||||||
def DownloadChunks(url, containerSize):
|
def DownloadChunks(url):
|
||||||
"""
|
"""
|
||||||
Downloads PMS url in chunks of containerSize (int).
|
Downloads PMS url in chunks of CONTAINERSIZE.
|
||||||
If containerSize is None: ONE xml is fetched directly
|
|
||||||
|
|
||||||
url MUST end with '?' (if no other url encoded args are present) or '&'
|
url MUST end with '?' (if no other url encoded args are present) or '&'
|
||||||
|
|
||||||
Returns a stitched-together xml or None.
|
Returns a stitched-together xml or None.
|
||||||
"""
|
"""
|
||||||
if containerSize is None:
|
|
||||||
# Get rid of '?' or '&' at the end of url
|
|
||||||
xml = downloadutils.DownloadUtils().downloadUrl(url[:-1])
|
|
||||||
if xml == 401:
|
|
||||||
return 401
|
|
||||||
try:
|
|
||||||
xml.attrib
|
|
||||||
except AttributeError:
|
|
||||||
# Nope, not an XML, abort
|
|
||||||
log.error("Error getting url %s" % url[:-1])
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return xml
|
|
||||||
|
|
||||||
xml = None
|
xml = None
|
||||||
pos = 0
|
pos = 0
|
||||||
errorCounter = 0
|
errorCounter = 0
|
||||||
while errorCounter < 10:
|
while errorCounter < 10:
|
||||||
args = {
|
args = {
|
||||||
'X-Plex-Container-Size': containerSize,
|
'X-Plex-Container-Size': CONTAINERSIZE,
|
||||||
'X-Plex-Container-Start': pos
|
'X-Plex-Container-Start': pos
|
||||||
}
|
}
|
||||||
xmlpart = downloadutils.DownloadUtils().downloadUrl(
|
xmlpart = downloadutils.DownloadUtils().downloadUrl(
|
||||||
|
@ -208,33 +194,32 @@ def DownloadChunks(url, containerSize):
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
log.error('Error while downloading chunks: %s'
|
log.error('Error while downloading chunks: %s'
|
||||||
% (url + urlencode(args)))
|
% (url + urlencode(args)))
|
||||||
pos += containerSize
|
pos += CONTAINERSIZE
|
||||||
errorCounter += 1
|
errorCounter += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Very first run: starting xml (to retain data in xml's root!)
|
# Very first run: starting xml (to retain data in xml's root!)
|
||||||
if xml is None:
|
if xml is None:
|
||||||
xml = deepcopy(xmlpart)
|
xml = deepcopy(xmlpart)
|
||||||
if len(xmlpart) < containerSize:
|
if len(xmlpart) < CONTAINERSIZE:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
pos += containerSize
|
pos += CONTAINERSIZE
|
||||||
continue
|
continue
|
||||||
# Build answer xml - containing the entire library
|
# Build answer xml - containing the entire library
|
||||||
for child in xmlpart:
|
for child in xmlpart:
|
||||||
xml.append(child)
|
xml.append(child)
|
||||||
# Done as soon as we don't receive a full complement of items
|
# Done as soon as we don't receive a full complement of items
|
||||||
if len(xmlpart) < containerSize:
|
if len(xmlpart) < CONTAINERSIZE:
|
||||||
break
|
break
|
||||||
pos += containerSize
|
pos += CONTAINERSIZE
|
||||||
if errorCounter == 10:
|
if errorCounter == 10:
|
||||||
log.error('Fatal error while downloading chunks for %s' % url)
|
log.error('Fatal error while downloading chunks for %s' % url)
|
||||||
return None
|
return None
|
||||||
return xml
|
return xml
|
||||||
|
|
||||||
|
|
||||||
def GetAllPlexLeaves(viewId, lastViewedAt=None, updatedAt=None,
|
def GetAllPlexLeaves(viewId, lastViewedAt=None, updatedAt=None):
|
||||||
containerSize=None):
|
|
||||||
"""
|
"""
|
||||||
Returns a list (raw XML API dump) of all Plex subitems for the key.
|
Returns a list (raw XML API dump) of all Plex subitems for the key.
|
||||||
(e.g. /library/sections/2/allLeaves pointing to all TV shows)
|
(e.g. /library/sections/2/allLeaves pointing to all TV shows)
|
||||||
|
@ -245,7 +230,6 @@ def GetAllPlexLeaves(viewId, lastViewedAt=None, updatedAt=None,
|
||||||
since that point of time until now.
|
since that point of time until now.
|
||||||
updatedAt Unix timestamp; only retrieves PMS items updated
|
updatedAt Unix timestamp; only retrieves PMS items updated
|
||||||
by the PMS since that point of time until now.
|
by the PMS since that point of time until now.
|
||||||
containerSize Number of items simultaneously fetched from PMS
|
|
||||||
|
|
||||||
If lastViewedAt and updatedAt=None, ALL PMS items are returned.
|
If lastViewedAt and updatedAt=None, ALL PMS items are returned.
|
||||||
|
|
||||||
|
@ -265,14 +249,13 @@ def GetAllPlexLeaves(viewId, lastViewedAt=None, updatedAt=None,
|
||||||
url += '?' + '&'.join(args) + '&'
|
url += '?' + '&'.join(args) + '&'
|
||||||
else:
|
else:
|
||||||
url += '?'
|
url += '?'
|
||||||
return DownloadChunks(url, containerSize)
|
return DownloadChunks(url)
|
||||||
|
|
||||||
|
|
||||||
def GetPlexOnDeck(viewId, containerSize=None):
|
def GetPlexOnDeck(viewId):
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
url = "{server}/library/sections/%s/onDeck?" % viewId
|
return DownloadChunks("{server}/library/sections/%s/onDeck?" % viewId)
|
||||||
return DownloadChunks(url, containerSize)
|
|
||||||
|
|
||||||
|
|
||||||
def GetPlexCollections(mediatype):
|
def GetPlexCollections(mediatype):
|
||||||
|
|
|
@ -6,21 +6,16 @@ from sys import argv
|
||||||
from urllib import urlencode
|
from urllib import urlencode
|
||||||
|
|
||||||
import xbmcplugin
|
import xbmcplugin
|
||||||
from xbmc import sleep, Player, executebuiltin, getCondVisibility, \
|
from xbmc import sleep, executebuiltin, translatePath
|
||||||
translatePath
|
|
||||||
from xbmcgui import ListItem
|
from xbmcgui import ListItem
|
||||||
|
|
||||||
from utils import window, settings, language as lang, dialog, tryDecode,\
|
from utils import window, settings, language as lang, dialog, tryDecode,\
|
||||||
tryEncode, CatchExceptions, JSONRPC
|
tryEncode, CatchExceptions, JSONRPC
|
||||||
import downloadutils
|
import downloadutils
|
||||||
import playbackutils as pbutils
|
|
||||||
import plexdb_functions as plexdb
|
|
||||||
|
|
||||||
from PlexFunctions import GetPlexMetadata, GetPlexSectionResults, \
|
from PlexFunctions import GetPlexMetadata, GetPlexSectionResults, \
|
||||||
GetMachineIdentifier
|
GetMachineIdentifier
|
||||||
from PlexAPI import API
|
from PlexAPI import API
|
||||||
from PKC_listitem import convert_PKC_to_listitem
|
|
||||||
from playqueue import Playqueue
|
|
||||||
import variables as v
|
import variables as v
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
@ -97,62 +92,6 @@ def togglePlexTV():
|
||||||
sound=False)
|
sound=False)
|
||||||
|
|
||||||
|
|
||||||
def Plex_Node(url, viewOffset, playdirectly=False, node=True):
|
|
||||||
"""
|
|
||||||
Called only for a SINGLE element for Plex.tv watch later
|
|
||||||
|
|
||||||
Always to return with a "setResolvedUrl"
|
|
||||||
"""
|
|
||||||
log.info('Plex_Node called with url: %s, viewOffset: %s'
|
|
||||||
% (url, viewOffset))
|
|
||||||
# Plex redirect, e.g. watch later. Need to get actual URLs
|
|
||||||
if url.startswith('http'):
|
|
||||||
xml = downloadutils.DownloadUtils().downloadUrl(url)
|
|
||||||
else:
|
|
||||||
xml = downloadutils.DownloadUtils().downloadUrl('{server}%s' % url)
|
|
||||||
try:
|
|
||||||
xml[0].attrib
|
|
||||||
except:
|
|
||||||
log.error('Could not download PMS metadata')
|
|
||||||
return
|
|
||||||
if viewOffset != '0':
|
|
||||||
try:
|
|
||||||
viewOffset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(viewOffset))
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
window('plex_customplaylist.seektime', value=str(viewOffset))
|
|
||||||
log.info('Set resume point to %s' % str(viewOffset))
|
|
||||||
api = API(xml[0])
|
|
||||||
typus = v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]
|
|
||||||
if node is True:
|
|
||||||
plex_id = None
|
|
||||||
kodi_id = 'plexnode'
|
|
||||||
else:
|
|
||||||
plex_id = api.getRatingKey()
|
|
||||||
kodi_id = None
|
|
||||||
with plexdb.Get_Plex_DB() as plex_db:
|
|
||||||
plexdb_item = plex_db.getItem_byId(plex_id)
|
|
||||||
try:
|
|
||||||
kodi_id = plexdb_item[0]
|
|
||||||
except TypeError:
|
|
||||||
log.info('Couldnt find item %s in Kodi db'
|
|
||||||
% api.getRatingKey())
|
|
||||||
playqueue = Playqueue().get_playqueue_from_type(typus)
|
|
||||||
result = pbutils.PlaybackUtils(xml, playqueue).play(
|
|
||||||
plex_id,
|
|
||||||
kodi_id=kodi_id,
|
|
||||||
plex_lib_UUID=xml.attrib.get('librarySectionUUID'))
|
|
||||||
if result.listitem:
|
|
||||||
listitem = convert_PKC_to_listitem(result.listitem)
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
if playdirectly:
|
|
||||||
Player().play(listitem.getfilename(), listitem)
|
|
||||||
else:
|
|
||||||
xbmcplugin.setResolvedUrl(HANDLE, True, listitem)
|
|
||||||
|
|
||||||
|
|
||||||
##### DO RESET AUTH #####
|
##### DO RESET AUTH #####
|
||||||
def resetAuth():
|
def resetAuth():
|
||||||
# User tried login and failed too many times
|
# User tried login and failed too many times
|
||||||
|
@ -172,7 +111,8 @@ def addDirectoryItem(label, path, folder=True):
|
||||||
xbmcplugin.addDirectoryItem(handle=HANDLE, url=path, listitem=li, isFolder=folder)
|
xbmcplugin.addDirectoryItem(handle=HANDLE, url=path, listitem=li, isFolder=folder)
|
||||||
|
|
||||||
|
|
||||||
def doMainListing():
|
def doMainListing(content_type=None):
|
||||||
|
log.debug('Do main listing with content_type: %s' % content_type)
|
||||||
xbmcplugin.setContent(HANDLE, 'files')
|
xbmcplugin.setContent(HANDLE, 'files')
|
||||||
# Get emby nodes from the window props
|
# Get emby nodes from the window props
|
||||||
plexprops = window('Plex.nodes.total')
|
plexprops = window('Plex.nodes.total')
|
||||||
|
@ -182,37 +122,38 @@ def doMainListing():
|
||||||
path = window('Plex.nodes.%s.index' % i)
|
path = window('Plex.nodes.%s.index' % i)
|
||||||
if not path:
|
if not path:
|
||||||
path = window('Plex.nodes.%s.content' % i)
|
path = window('Plex.nodes.%s.content' % i)
|
||||||
|
if not path:
|
||||||
|
continue
|
||||||
label = window('Plex.nodes.%s.title' % i)
|
label = window('Plex.nodes.%s.title' % i)
|
||||||
node_type = window('Plex.nodes.%s.type' % i)
|
node_type = window('Plex.nodes.%s.type' % i)
|
||||||
#because we do not use seperate entrypoints for each content type, we need to figure out which items to show in each listing.
|
# because we do not use seperate entrypoints for each content type,
|
||||||
#for now we just only show picture nodes in the picture library video nodes in the video library and all nodes in any other window
|
# we need to figure out which items to show in each listing. for
|
||||||
if path and getCondVisibility("Window.IsActive(Pictures)") and node_type == "photos":
|
# now we just only show picture nodes in the picture library video
|
||||||
|
# nodes in the video library and all nodes in any other window
|
||||||
|
if node_type == 'photos' and content_type == 'image':
|
||||||
addDirectoryItem(label, path)
|
addDirectoryItem(label, path)
|
||||||
elif path and getCondVisibility("Window.IsActive(VideoLibrary)") and node_type != "photos":
|
elif (node_type != 'photos' and
|
||||||
addDirectoryItem(label, path)
|
content_type not in ('image', 'audio')):
|
||||||
elif path and not getCondVisibility("Window.IsActive(VideoLibrary) | Window.IsActive(Pictures) | Window.IsActive(MusicLibrary)"):
|
|
||||||
addDirectoryItem(label, path)
|
addDirectoryItem(label, path)
|
||||||
|
|
||||||
# Plex Watch later
|
# Plex Watch later
|
||||||
addDirectoryItem(lang(39211),
|
if content_type not in ('image', 'audio'):
|
||||||
"plugin://plugin.video.plexkodiconnect/?mode=watchlater")
|
addDirectoryItem(lang(39211),
|
||||||
|
"plugin://%s?mode=watchlater" % v.ADDON_ID)
|
||||||
# Plex Channels
|
# Plex Channels
|
||||||
addDirectoryItem(lang(30173),
|
addDirectoryItem(lang(30173),
|
||||||
"plugin://plugin.video.plexkodiconnect/?mode=channels")
|
"plugin://%s?mode=channels" % v.ADDON_ID)
|
||||||
# Plex user switch
|
# Plex user switch
|
||||||
addDirectoryItem(lang(39200) + window('plex_username'),
|
addDirectoryItem(lang(39200) + window('plex_username'),
|
||||||
"plugin://plugin.video.plexkodiconnect/"
|
"plugin://%s?mode=switchuser" % v.ADDON_ID)
|
||||||
"?mode=switchuser")
|
|
||||||
|
|
||||||
#experimental live tv nodes
|
# some extra entries for settings and stuff
|
||||||
# addDirectoryItem("Live Tv Channels (experimental)", "plugin://plugin.video.plexkodiconnect/?mode=browsecontent&type=tvchannels&folderid=root")
|
addDirectoryItem(lang(39201),
|
||||||
# addDirectoryItem("Live Tv Recordings (experimental)", "plugin://plugin.video.plexkodiconnect/?mode=browsecontent&type=recordings&folderid=root")
|
"plugin://%s?mode=settings" % v.ADDON_ID)
|
||||||
|
addDirectoryItem(lang(39203),
|
||||||
# some extra entries for settings and stuff. TODO --> localize the labels
|
"plugin://%s?mode=refreshplaylist" % v.ADDON_ID)
|
||||||
addDirectoryItem(lang(39201), "plugin://plugin.video.plexkodiconnect/?mode=settings")
|
addDirectoryItem(lang(39204),
|
||||||
# addDirectoryItem("Add user to session", "plugin://plugin.video.plexkodiconnect/?mode=adduser")
|
"plugin://%s?mode=manualsync" % v.ADDON_ID)
|
||||||
addDirectoryItem(lang(39203), "plugin://plugin.video.plexkodiconnect/?mode=refreshplaylist")
|
|
||||||
addDirectoryItem(lang(39204), "plugin://plugin.video.plexkodiconnect/?mode=manualsync")
|
|
||||||
xbmcplugin.endOfDirectory(HANDLE)
|
xbmcplugin.endOfDirectory(HANDLE)
|
||||||
|
|
||||||
|
|
||||||
|
@ -680,8 +621,6 @@ def getOnDeck(viewid, mediatype, tagname, limit):
|
||||||
listitem = api.CreateListItemFromPlexItem(
|
listitem = api.CreateListItemFromPlexItem(
|
||||||
appendShowTitle=appendShowTitle,
|
appendShowTitle=appendShowTitle,
|
||||||
appendSxxExx=appendSxxExx)
|
appendSxxExx=appendSxxExx)
|
||||||
api.AddStreamInfo(listitem)
|
|
||||||
api.set_listitem_artwork(listitem)
|
|
||||||
if directpaths:
|
if directpaths:
|
||||||
url = api.getFilePath()
|
url = api.getFilePath()
|
||||||
else:
|
else:
|
||||||
|
@ -852,12 +791,10 @@ def browse_plex(key=None, plex_section_id=None):
|
||||||
if key:
|
if key:
|
||||||
xml = downloadutils.DownloadUtils().downloadUrl('{server}%s' % key)
|
xml = downloadutils.DownloadUtils().downloadUrl('{server}%s' % key)
|
||||||
else:
|
else:
|
||||||
xml = GetPlexSectionResults(
|
xml = GetPlexSectionResults(plex_section_id)
|
||||||
plex_section_id,
|
|
||||||
containerSize=int(settings('limitindex')))
|
|
||||||
try:
|
try:
|
||||||
xml[0].attrib
|
xml[0].attrib
|
||||||
except (ValueError, AttributeError, IndexError):
|
except (ValueError, AttributeError, IndexError, TypeError):
|
||||||
log.error('Could not browse to %s' % key)
|
log.error('Could not browse to %s' % key)
|
||||||
return xbmcplugin.endOfDirectory(HANDLE, False)
|
return xbmcplugin.endOfDirectory(HANDLE, False)
|
||||||
|
|
||||||
|
@ -871,10 +808,10 @@ def browse_plex(key=None, plex_section_id=None):
|
||||||
albums = False
|
albums = False
|
||||||
musicvideos = False
|
musicvideos = False
|
||||||
for item in xml:
|
for item in xml:
|
||||||
typus = item.attrib.get('type')
|
|
||||||
if item.tag == 'Directory':
|
if item.tag == 'Directory':
|
||||||
__build_folder(item, plex_section_id=plex_section_id)
|
__build_folder(item, plex_section_id=plex_section_id)
|
||||||
else:
|
else:
|
||||||
|
typus = item.attrib.get('type')
|
||||||
__build_item(item)
|
__build_item(item)
|
||||||
if typus == v.PLEX_TYPE_PHOTO:
|
if typus == v.PLEX_TYPE_PHOTO:
|
||||||
photos = True
|
photos = True
|
||||||
|
@ -903,7 +840,7 @@ def browse_plex(key=None, plex_section_id=None):
|
||||||
xbmcplugin.setContent(HANDLE, 'movies')
|
xbmcplugin.setContent(HANDLE, 'movies')
|
||||||
sort_methods = v.SORT_METHODS_CLIPS
|
sort_methods = v.SORT_METHODS_CLIPS
|
||||||
elif photos is True:
|
elif photos is True:
|
||||||
xbmcplugin.setContent(HANDLE, 'files')
|
xbmcplugin.setContent(HANDLE, 'images')
|
||||||
sort_methods = v.SORT_METHODS_PHOTOS
|
sort_methods = v.SORT_METHODS_PHOTOS
|
||||||
elif tvshows is True:
|
elif tvshows is True:
|
||||||
xbmcplugin.setContent(HANDLE, 'tvshows')
|
xbmcplugin.setContent(HANDLE, 'tvshows')
|
||||||
|
@ -961,23 +898,24 @@ def __build_folder(xml_element, plex_section_id=None):
|
||||||
def __build_item(xml_element):
|
def __build_item(xml_element):
|
||||||
api = API(xml_element)
|
api = API(xml_element)
|
||||||
listitem = api.CreateListItemFromPlexItem()
|
listitem = api.CreateListItemFromPlexItem()
|
||||||
api.AddStreamInfo(listitem)
|
if (api.getKey().startswith('/system/services') or
|
||||||
api.set_listitem_artwork(listitem)
|
api.getKey().startswith('http')):
|
||||||
if api.getType() == v.PLEX_TYPE_CLIP:
|
|
||||||
params = {
|
params = {
|
||||||
'mode': "Plex_Node",
|
'mode': 'plex_node',
|
||||||
'id': xml_element.attrib.get('key'),
|
'key': xml_element.attrib.get('key'),
|
||||||
'viewOffset': xml_element.attrib.get('viewOffset', '0'),
|
'view_offset': xml_element.attrib.get('viewOffset', '0'),
|
||||||
'plex_type': xml_element.attrib.get('type')
|
|
||||||
}
|
}
|
||||||
|
url = "plugin://%s?%s" % (v.ADDON_ID, urlencode(params))
|
||||||
|
elif api.getType() == v.PLEX_TYPE_PHOTO:
|
||||||
|
url = api.get_picture_path()
|
||||||
else:
|
else:
|
||||||
params = {
|
params = {
|
||||||
|
'mode': 'play',
|
||||||
'filename': api.getKey(),
|
'filename': api.getKey(),
|
||||||
'id': api.getRatingKey(),
|
'id': api.getRatingKey(),
|
||||||
'dbid': listitem.getProperty('dbid') or '',
|
'dbid': listitem.getProperty('dbid')
|
||||||
'mode': "play"
|
|
||||||
}
|
}
|
||||||
url = "plugin://%s?%s" % (v.ADDON_ID, urlencode(params))
|
url = "plugin://%s?%s" % (v.ADDON_ID, urlencode(params))
|
||||||
xbmcplugin.addDirectoryItem(handle=HANDLE,
|
xbmcplugin.addDirectoryItem(handle=HANDLE,
|
||||||
url=url,
|
url=url,
|
||||||
listitem=listitem)
|
listitem=listitem)
|
||||||
|
|
|
@ -9,8 +9,7 @@ from datetime import datetime
|
||||||
from xbmc import sleep
|
from xbmc import sleep
|
||||||
|
|
||||||
import artwork
|
import artwork
|
||||||
from utils import tryEncode, tryDecode, settings, window, kodiSQL, \
|
from utils import tryEncode, tryDecode, window, kodiSQL, CatchExceptions
|
||||||
CatchExceptions
|
|
||||||
import plexdb_functions as plexdb
|
import plexdb_functions as plexdb
|
||||||
import kodidb_functions as kodidb
|
import kodidb_functions as kodidb
|
||||||
|
|
||||||
|
@ -1259,14 +1258,6 @@ class TVShows(Items):
|
||||||
|
|
||||||
class Music(Items):
|
class Music(Items):
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
Items.__init__(self)
|
|
||||||
|
|
||||||
self.directstream = settings('streamMusic') == "true"
|
|
||||||
self.enableimportsongrating = settings('enableImportSongRating') == "true"
|
|
||||||
self.enableexportsongrating = settings('enableExportSongRating') == "true"
|
|
||||||
self.enableupdatesongrating = settings('enableUpdateSongRating') == "true"
|
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
"""
|
"""
|
||||||
OVERWRITE this method, because we need to open another DB.
|
OVERWRITE this method, because we need to open another DB.
|
||||||
|
@ -1305,7 +1296,7 @@ class Music(Items):
|
||||||
name, sortname = API.getTitle()
|
name, sortname = API.getTitle()
|
||||||
# musicBrainzId = API.getProvider('MusicBrainzArtist')
|
# musicBrainzId = API.getProvider('MusicBrainzArtist')
|
||||||
musicBrainzId = None
|
musicBrainzId = None
|
||||||
genres = API.joinList(API.getGenres())
|
genres = ' / '.join(API.getGenres())
|
||||||
bio = API.getPlot()
|
bio = API.getPlot()
|
||||||
|
|
||||||
# Associate artwork
|
# Associate artwork
|
||||||
|
@ -1344,31 +1335,32 @@ class Music(Items):
|
||||||
|
|
||||||
# Process the artist
|
# Process the artist
|
||||||
if v.KODIVERSION >= 16:
|
if v.KODIVERSION >= 16:
|
||||||
query = ' '.join((
|
query = '''
|
||||||
|
UPDATE artist
|
||||||
"UPDATE artist",
|
SET strGenres = ?, strBiography = ?, strImage = ?,
|
||||||
"SET strGenres = ?, strBiography = ?, strImage = ?, strFanart = ?,",
|
strFanart = ?, lastScraped = ?
|
||||||
"lastScraped = ?",
|
WHERE idArtist = ?
|
||||||
"WHERE idArtist = ?"
|
'''
|
||||||
))
|
|
||||||
kodicursor.execute(query, (genres, bio, thumb, fanart,
|
kodicursor.execute(query, (genres, bio, thumb, fanart,
|
||||||
lastScraped, artistid))
|
lastScraped, artistid))
|
||||||
else:
|
else:
|
||||||
query = ' '.join((
|
query = '''
|
||||||
|
UPDATE artist
|
||||||
"UPDATE artist",
|
SET strGenres = ?, strBiography = ?, strImage = ?,
|
||||||
"SET strGenres = ?, strBiography = ?, strImage = ?, strFanart = ?,",
|
strFanart = ?, lastScraped = ?, dateAdded = ?
|
||||||
"lastScraped = ?, dateAdded = ?",
|
WHERE idArtist = ?
|
||||||
"WHERE idArtist = ?"
|
'''
|
||||||
))
|
|
||||||
kodicursor.execute(query, (genres, bio, thumb, fanart, lastScraped,
|
kodicursor.execute(query, (genres, bio, thumb, fanart, lastScraped,
|
||||||
dateadded, artistid))
|
dateadded, artistid))
|
||||||
|
|
||||||
# Update artwork
|
# Update artwork
|
||||||
artwork.addArtwork(artworks, artistid, "artist", kodicursor)
|
artwork.addArtwork(artworks, artistid, v.KODI_TYPE_ARTIST, kodicursor)
|
||||||
|
|
||||||
@CatchExceptions(warnuser=True)
|
@CatchExceptions(warnuser=True)
|
||||||
def add_updateAlbum(self, item, viewtag=None, viewid=None):
|
def add_updateAlbum(self, item, viewtag=None, viewid=None, children=None):
|
||||||
|
"""
|
||||||
|
children: list of child xml's, so in this case songs
|
||||||
|
"""
|
||||||
kodicursor = self.kodicursor
|
kodicursor = self.kodicursor
|
||||||
plex_db = self.plex_db
|
plex_db = self.plex_db
|
||||||
artwork = self.artwork
|
artwork = self.artwork
|
||||||
|
@ -1396,21 +1388,21 @@ class Music(Items):
|
||||||
# musicBrainzId = API.getProvider('MusicBrainzAlbum')
|
# musicBrainzId = API.getProvider('MusicBrainzAlbum')
|
||||||
musicBrainzId = None
|
musicBrainzId = None
|
||||||
year = API.getYear()
|
year = API.getYear()
|
||||||
genres = API.getGenres()
|
self.genres = API.getGenres()
|
||||||
genre = API.joinList(genres)
|
self.genre = ' / '.join(self.genres)
|
||||||
bio = API.getPlot()
|
bio = API.getPlot()
|
||||||
rating = userdata['UserRating']
|
rating = userdata['UserRating']
|
||||||
studio = API.getMusicStudio()
|
studio = API.getMusicStudio()
|
||||||
# artists = item['AlbumArtists']
|
|
||||||
# if not artists:
|
|
||||||
# artists = item['ArtistItems']
|
|
||||||
# artistname = []
|
|
||||||
# for artist in artists:
|
|
||||||
# artistname.append(artist['Name'])
|
|
||||||
artistname = item.attrib.get('parentTitle')
|
artistname = item.attrib.get('parentTitle')
|
||||||
if not artistname:
|
if not artistname:
|
||||||
artistname = item.attrib.get('originalTitle')
|
artistname = item.attrib.get('originalTitle')
|
||||||
|
# See if we have a compilation - Plex does NOT feature a compilation
|
||||||
|
# flag for albums
|
||||||
|
self.compilation = 0
|
||||||
|
for child in children:
|
||||||
|
if child.attrib.get('originalTitle') is not None:
|
||||||
|
self.compilation = 1
|
||||||
|
break
|
||||||
# Associate artwork
|
# Associate artwork
|
||||||
artworks = API.getAllArtwork(parentInfo=True)
|
artworks = API.getAllArtwork(parentInfo=True)
|
||||||
thumb = artworks['Primary']
|
thumb = artworks['Primary']
|
||||||
|
@ -1442,56 +1434,54 @@ class Music(Items):
|
||||||
# Process the album info
|
# Process the album info
|
||||||
if v.KODIVERSION >= 17:
|
if v.KODIVERSION >= 17:
|
||||||
# Kodi Krypton
|
# Kodi Krypton
|
||||||
query = ' '.join((
|
query = '''
|
||||||
|
UPDATE album
|
||||||
"UPDATE album",
|
SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?,
|
||||||
"SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,",
|
strImage = ?, iUserrating = ?, lastScraped = ?,
|
||||||
"iUserrating = ?, lastScraped = ?, strReleaseType = ?, "
|
strReleaseType = ?, strLabel = ?, bCompilation = ?
|
||||||
"strLabel = ? ",
|
WHERE idAlbum = ?
|
||||||
"WHERE idAlbum = ?"
|
'''
|
||||||
))
|
kodicursor.execute(query, (artistname, year, self.genre, bio,
|
||||||
kodicursor.execute(query, (artistname, year, genre, bio, thumb,
|
thumb, rating, lastScraped,
|
||||||
rating, lastScraped, "album", studio,
|
v.KODI_TYPE_ALBUM, studio,
|
||||||
albumid))
|
self.compilation, albumid))
|
||||||
elif v.KODIVERSION == 16:
|
elif v.KODIVERSION == 16:
|
||||||
# Kodi Jarvis
|
# Kodi Jarvis
|
||||||
query = ' '.join((
|
query = '''
|
||||||
|
UPDATE album
|
||||||
"UPDATE album",
|
SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?,
|
||||||
"SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,",
|
strImage = ?, iRating = ?, lastScraped = ?,
|
||||||
"iRating = ?, lastScraped = ?, strReleaseType = ?, "
|
strReleaseType = ?, strLabel = ?, bCompilation = ?
|
||||||
"strLabel = ? ",
|
WHERE idAlbum = ?
|
||||||
"WHERE idAlbum = ?"
|
'''
|
||||||
))
|
kodicursor.execute(query, (artistname, year, self.genre, bio,
|
||||||
kodicursor.execute(query, (artistname, year, genre, bio, thumb,
|
thumb, rating, lastScraped,
|
||||||
rating, lastScraped, "album", studio,
|
v.KODI_TYPE_ALBUM, studio,
|
||||||
albumid))
|
self.compilation, albumid))
|
||||||
elif v.KODIVERSION == 15:
|
elif v.KODIVERSION == 15:
|
||||||
# Kodi Isengard
|
# Kodi Isengard
|
||||||
query = ' '.join((
|
query = '''
|
||||||
|
UPDATE album
|
||||||
"UPDATE album",
|
SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?,
|
||||||
"SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,",
|
strImage = ?, iRating = ?, lastScraped = ?, dateAdded = ?,
|
||||||
"iRating = ?, lastScraped = ?, dateAdded = ?, "
|
strReleaseType = ?, strLabel = ?
|
||||||
"strReleaseType = ?, strLabel = ? ",
|
WHERE idAlbum = ?
|
||||||
"WHERE idAlbum = ?"
|
'''
|
||||||
))
|
kodicursor.execute(query, (artistname, year, self.genre, bio,
|
||||||
kodicursor.execute(query, (artistname, year, genre, bio, thumb,
|
thumb, rating, lastScraped, dateadded,
|
||||||
rating, lastScraped, dateadded,
|
v.KODI_TYPE_ALBUM, studio, albumid))
|
||||||
"album", studio, albumid))
|
|
||||||
else:
|
else:
|
||||||
# Kodi Helix
|
# Kodi Helix
|
||||||
query = ' '.join((
|
query = '''
|
||||||
|
UPDATE album
|
||||||
"UPDATE album",
|
SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?,
|
||||||
"SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,",
|
strImage = ?, iRating = ?, lastScraped = ?, dateAdded = ?,
|
||||||
"iRating = ?, lastScraped = ?, dateAdded = ?, "
|
strLabel = ?
|
||||||
"strLabel = ? ",
|
WHERE idAlbum = ?
|
||||||
"WHERE idAlbum = ?"
|
'''
|
||||||
))
|
kodicursor.execute(query, (artistname, year, self.genre, bio,
|
||||||
kodicursor.execute(query, (artistname, year, genre, bio, thumb,
|
thumb, rating, lastScraped, dateadded,
|
||||||
rating, lastScraped, dateadded, studio,
|
studio, albumid))
|
||||||
albumid))
|
|
||||||
|
|
||||||
# Associate the parentid for plex reference
|
# Associate the parentid for plex reference
|
||||||
parentId = item.attrib.get('parentRatingKey')
|
parentId = item.attrib.get('parentRatingKey')
|
||||||
|
@ -1505,7 +1495,7 @@ class Music(Items):
|
||||||
artist = GetPlexMetadata(parentId)
|
artist = GetPlexMetadata(parentId)
|
||||||
# Item may not be an artist, verification necessary.
|
# Item may not be an artist, verification necessary.
|
||||||
if artist is not None and artist != 401:
|
if artist is not None and artist != 401:
|
||||||
if artist[0].attrib.get('type') == "artist":
|
if artist[0].attrib.get('type') == v.PLEX_TYPE_ARTIST:
|
||||||
# Update with the parentId, for remove reference
|
# Update with the parentId, for remove reference
|
||||||
plex_db.addReference(parentId,
|
plex_db.addReference(parentId,
|
||||||
v.PLEX_TYPE_ARTIST,
|
v.PLEX_TYPE_ARTIST,
|
||||||
|
@ -1539,29 +1529,26 @@ class Music(Items):
|
||||||
% (artistname, artistid))
|
% (artistname, artistid))
|
||||||
|
|
||||||
# Add artist to album
|
# Add artist to album
|
||||||
query = (
|
query = '''
|
||||||
'''
|
|
||||||
INSERT OR REPLACE INTO album_artist(idArtist, idAlbum, strArtist)
|
INSERT OR REPLACE INTO album_artist(idArtist, idAlbum, strArtist)
|
||||||
|
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
'''
|
'''
|
||||||
)
|
|
||||||
kodicursor.execute(query, (artistid, albumid, artistname))
|
kodicursor.execute(query, (artistid, albumid, artistname))
|
||||||
# Update discography
|
# Update discography
|
||||||
query = (
|
query = '''
|
||||||
'''
|
|
||||||
INSERT OR REPLACE INTO discography(idArtist, strAlbum, strYear)
|
INSERT OR REPLACE INTO discography(idArtist, strAlbum, strYear)
|
||||||
|
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
'''
|
'''
|
||||||
)
|
|
||||||
kodicursor.execute(query, (artistid, name, year))
|
kodicursor.execute(query, (artistid, name, year))
|
||||||
# Update plex reference with parentid
|
# Update plex reference with parentid
|
||||||
plex_db.updateParentId(artistId, albumid)
|
plex_db.updateParentId(artistId, albumid)
|
||||||
# Add genres
|
# Add genres
|
||||||
self.kodi_db.addMusicGenres(albumid, genres, "album")
|
self.kodi_db.addMusicGenres(albumid, self.genres, v.KODI_TYPE_ALBUM)
|
||||||
# Update artwork
|
# Update artwork
|
||||||
artwork.addArtwork(artworks, albumid, "album", kodicursor)
|
artwork.addArtwork(artworks, albumid, v.KODI_TYPE_ALBUM, kodicursor)
|
||||||
|
# Add all children - all tracks
|
||||||
|
for child in children:
|
||||||
|
self.add_updateSong(child, viewtag, viewid)
|
||||||
|
|
||||||
@CatchExceptions(warnuser=True)
|
@CatchExceptions(warnuser=True)
|
||||||
def add_updateSong(self, item, viewtag=None, viewid=None):
|
def add_updateSong(self, item, viewtag=None, viewid=None):
|
||||||
|
@ -1601,9 +1588,22 @@ class Music(Items):
|
||||||
title, sorttitle = API.getTitle()
|
title, sorttitle = API.getTitle()
|
||||||
# musicBrainzId = API.getProvider('MusicBrainzTrackId')
|
# musicBrainzId = API.getProvider('MusicBrainzTrackId')
|
||||||
musicBrainzId = None
|
musicBrainzId = None
|
||||||
genres = API.getGenres()
|
try:
|
||||||
genre = API.joinList(genres)
|
genres = self.genres
|
||||||
artists = item.attrib.get('grandparentTitle')
|
genre = self.genre
|
||||||
|
except AttributeError:
|
||||||
|
# No parent album - hence no genre information from Plex
|
||||||
|
genres = None
|
||||||
|
genre = None
|
||||||
|
try:
|
||||||
|
if self.compilation == 0:
|
||||||
|
artists = item.attrib.get('grandparentTitle')
|
||||||
|
else:
|
||||||
|
artists = item.attrib.get('originalTitle')
|
||||||
|
except AttributeError:
|
||||||
|
# compilation not set
|
||||||
|
artists = item.attrib.get('originalTitle',
|
||||||
|
item.attrib.get('grandparentTitle'))
|
||||||
tracknumber = int(item.attrib.get('index', 0))
|
tracknumber = int(item.attrib.get('index', 0))
|
||||||
disc = int(item.attrib.get('parentIndex', 1))
|
disc = int(item.attrib.get('parentIndex', 1))
|
||||||
if disc == 1:
|
if disc == 1:
|
||||||
|
@ -1613,9 +1613,13 @@ class Music(Items):
|
||||||
year = API.getYear()
|
year = API.getYear()
|
||||||
resume, duration = API.getRuntime()
|
resume, duration = API.getRuntime()
|
||||||
rating = userdata['UserRating']
|
rating = userdata['UserRating']
|
||||||
|
|
||||||
hasEmbeddedCover = False
|
|
||||||
comment = None
|
comment = None
|
||||||
|
# Moods
|
||||||
|
moods = []
|
||||||
|
for entry in item:
|
||||||
|
if entry.tag == 'Mood':
|
||||||
|
moods.append(entry.attrib['tag'])
|
||||||
|
mood = ' / '.join(moods)
|
||||||
|
|
||||||
# GET THE FILE AND PATH #####
|
# GET THE FILE AND PATH #####
|
||||||
doIndirect = not self.directpath
|
doIndirect = not self.directpath
|
||||||
|
@ -1653,16 +1657,18 @@ class Music(Items):
|
||||||
kodicursor.execute(query, (path, '123', pathid))
|
kodicursor.execute(query, (path, '123', pathid))
|
||||||
|
|
||||||
# Update the song entry
|
# Update the song entry
|
||||||
query = ' '.join((
|
query = '''
|
||||||
"UPDATE song",
|
UPDATE song
|
||||||
"SET idAlbum = ?, strArtists = ?, strGenres = ?, strTitle = ?, iTrack = ?,",
|
SET idAlbum = ?, strArtists = ?, strGenres = ?, strTitle = ?,
|
||||||
"iDuration = ?, iYear = ?, strFilename = ?, iTimesPlayed = ?, lastplayed = ?,",
|
iTrack = ?, iDuration = ?, iYear = ?, strFilename = ?,
|
||||||
"rating = ?, comment = ?",
|
iTimesPlayed = ?, lastplayed = ?, rating = ?, comment = ?,
|
||||||
"WHERE idSong = ?"
|
mood = ?
|
||||||
))
|
WHERE idSong = ?
|
||||||
|
'''
|
||||||
kodicursor.execute(query, (albumid, artists, genre, title, track,
|
kodicursor.execute(query, (albumid, artists, genre, title, track,
|
||||||
duration, year, filename, playcount,
|
duration, year, filename, playcount,
|
||||||
dateplayed, rating, comment, songid))
|
dateplayed, rating, comment, mood,
|
||||||
|
songid))
|
||||||
|
|
||||||
# Update the checksum in plex table
|
# Update the checksum in plex table
|
||||||
plex_db.updateReference(itemid, checksum)
|
plex_db.updateReference(itemid, checksum)
|
||||||
|
@ -1685,7 +1691,9 @@ class Music(Items):
|
||||||
if album_name:
|
if album_name:
|
||||||
log.info("Creating virtual music album for song: %s."
|
log.info("Creating virtual music album for song: %s."
|
||||||
% itemid)
|
% itemid)
|
||||||
albumid = self.kodi_db.addAlbum(album_name, API.getProvider('MusicBrainzAlbum'))
|
albumid = self.kodi_db.addAlbum(
|
||||||
|
album_name,
|
||||||
|
API.getProvider('MusicBrainzAlbum'))
|
||||||
plex_db.addReference("%salbum%s" % (itemid, albumid),
|
plex_db.addReference("%salbum%s" % (itemid, albumid),
|
||||||
v.PLEX_TYPE_ALBUM,
|
v.PLEX_TYPE_ALBUM,
|
||||||
albumid,
|
albumid,
|
||||||
|
@ -1713,54 +1721,51 @@ class Music(Items):
|
||||||
except TypeError:
|
except TypeError:
|
||||||
# No album found, create a single's album
|
# No album found, create a single's album
|
||||||
log.info("Failed to add album. Creating singles.")
|
log.info("Failed to add album. Creating singles.")
|
||||||
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 v.KODIVERSION >= 16:
|
if v.KODIVERSION >= 16:
|
||||||
# Kodi Jarvis
|
# Kodi Jarvis
|
||||||
query = (
|
query = '''
|
||||||
'''
|
INSERT INTO album(
|
||||||
INSERT INTO album(idAlbum, strGenres, iYear, strReleaseType)
|
idAlbum, strGenres, iYear, strReleaseType)
|
||||||
|
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
'''
|
'''
|
||||||
)
|
kodicursor.execute(query,
|
||||||
kodicursor.execute(query, (albumid, genre, year, "single"))
|
(albumid, genre, year, "single"))
|
||||||
elif v.KODIVERSION == 15:
|
elif v.KODIVERSION == 15:
|
||||||
# Kodi Isengard
|
# Kodi Isengard
|
||||||
query = (
|
query = '''
|
||||||
'''
|
INSERT INTO album(
|
||||||
INSERT INTO album(idAlbum, strGenres, iYear, dateAdded, strReleaseType)
|
idAlbum, strGenres, iYear, dateAdded,
|
||||||
|
strReleaseType)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
'''
|
'''
|
||||||
)
|
kodicursor.execute(query, (albumid, genre, year,
|
||||||
kodicursor.execute(query, (albumid, genre, year, dateadded, "single"))
|
dateadded, "single"))
|
||||||
else:
|
else:
|
||||||
# Kodi Helix
|
# Kodi Helix
|
||||||
query = (
|
query = '''
|
||||||
'''
|
INSERT INTO album(
|
||||||
INSERT INTO album(idAlbum, strGenres, iYear, dateAdded)
|
idAlbum, strGenres, iYear, dateAdded)
|
||||||
|
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
'''
|
'''
|
||||||
)
|
kodicursor.execute(query, (albumid, genre, year,
|
||||||
kodicursor.execute(query, (albumid, genre, year, dateadded))
|
dateadded))
|
||||||
|
|
||||||
# Create the song entry
|
# Create the song entry
|
||||||
query = (
|
query = '''
|
||||||
'''
|
|
||||||
INSERT INTO song(
|
INSERT INTO song(
|
||||||
idSong, idAlbum, idPath, strArtists, strGenres, strTitle, iTrack,
|
idSong, idAlbum, idPath, strArtists, strGenres, strTitle,
|
||||||
iDuration, iYear, strFileName, strMusicBrainzTrackID, iTimesPlayed, lastplayed,
|
iTrack, iDuration, iYear, strFileName,
|
||||||
rating, iStartOffset, iEndOffset)
|
strMusicBrainzTrackID, iTimesPlayed, lastplayed,
|
||||||
|
rating, iStartOffset, iEndOffset, mood)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
'''
|
'''
|
||||||
)
|
|
||||||
kodicursor.execute(
|
kodicursor.execute(
|
||||||
query, (songid, albumid, pathid, artists, genre, title, track,
|
query, (songid, albumid, pathid, artists, genre, title, track,
|
||||||
duration, year, filename, musicBrainzId, playcount,
|
duration, year, filename, musicBrainzId, playcount,
|
||||||
dateplayed, rating, 0, 0))
|
dateplayed, rating, 0, 0, mood))
|
||||||
|
|
||||||
# Create the reference in plex table
|
# Create the reference in plex table
|
||||||
plex_db.addReference(itemid,
|
plex_db.addReference(itemid,
|
||||||
|
@ -1773,14 +1778,11 @@ class Music(Items):
|
||||||
view_id=viewid)
|
view_id=viewid)
|
||||||
|
|
||||||
# Link song to album
|
# Link song to album
|
||||||
query = (
|
query = '''
|
||||||
'''
|
|
||||||
INSERT OR REPLACE INTO albuminfosong(
|
INSERT OR REPLACE INTO albuminfosong(
|
||||||
idAlbumInfoSong, idAlbumInfo, iTrack, strTitle, iDuration)
|
idAlbumInfoSong, idAlbumInfo, iTrack, strTitle, iDuration)
|
||||||
|
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
'''
|
'''
|
||||||
)
|
|
||||||
kodicursor.execute(query, (songid, albumid, track, title, duration))
|
kodicursor.execute(query, (songid, albumid, track, title, duration))
|
||||||
|
|
||||||
# Link song to artists
|
# Link song to artists
|
||||||
|
@ -1808,29 +1810,27 @@ class Music(Items):
|
||||||
finally:
|
finally:
|
||||||
if v.KODIVERSION >= 17:
|
if v.KODIVERSION >= 17:
|
||||||
# Kodi Krypton
|
# Kodi Krypton
|
||||||
query = (
|
query = '''
|
||||||
'''
|
INSERT OR REPLACE INTO song_artist(
|
||||||
INSERT OR REPLACE INTO song_artist(idArtist, idSong, idRole, iOrder, strArtist)
|
idArtist, idSong, idRole, iOrder, strArtist)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
'''
|
'''
|
||||||
)
|
kodicursor.execute(query, (artistid, songid, 1, index,
|
||||||
kodicursor.execute(query,(artistid, songid, 1, index, artist_name))
|
artist_name))
|
||||||
# May want to look into only doing this once?
|
# May want to look into only doing this once?
|
||||||
query = (
|
query = '''
|
||||||
'''
|
|
||||||
INSERT OR REPLACE INTO role(idRole, strRole)
|
INSERT OR REPLACE INTO role(idRole, strRole)
|
||||||
VALUES (?, ?)
|
VALUES (?, ?)
|
||||||
'''
|
'''
|
||||||
)
|
|
||||||
kodicursor.execute(query, (1, 'Composer'))
|
kodicursor.execute(query, (1, 'Composer'))
|
||||||
else:
|
else:
|
||||||
query = (
|
query = '''
|
||||||
'''
|
INSERT OR REPLACE INTO song_artist(
|
||||||
INSERT OR REPLACE INTO song_artist(idArtist, idSong, iOrder, strArtist)
|
idArtist, idSong, iOrder, strArtist)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
'''
|
'''
|
||||||
)
|
kodicursor.execute(query, (artistid, songid, index,
|
||||||
kodicursor.execute(query, (artistid, songid, index, artist_name))
|
artist_name))
|
||||||
|
|
||||||
# Verify if album artist exists
|
# Verify if album artist exists
|
||||||
album_artists = []
|
album_artists = []
|
||||||
|
@ -1852,31 +1852,28 @@ class Music(Items):
|
||||||
artist_edb = plex_db.getItem_byId(artist_eid)
|
artist_edb = plex_db.getItem_byId(artist_eid)
|
||||||
artistid = artist_edb[0]
|
artistid = artist_edb[0]
|
||||||
finally:
|
finally:
|
||||||
query = (
|
query = '''
|
||||||
'''
|
INSERT OR REPLACE INTO album_artist(
|
||||||
INSERT OR REPLACE INTO album_artist(idArtist, idAlbum, strArtist)
|
idArtist, idAlbum, strArtist)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
'''
|
'''
|
||||||
)
|
|
||||||
kodicursor.execute(query, (artistid, albumid, artist_name))
|
kodicursor.execute(query, (artistid, albumid, artist_name))
|
||||||
# Update discography
|
# Update discography
|
||||||
if item.get('Album'):
|
if item.get('Album'):
|
||||||
query = (
|
query = '''
|
||||||
'''
|
INSERT OR REPLACE INTO discography(
|
||||||
INSERT OR REPLACE INTO discography(idArtist, strAlbum, strYear)
|
idArtist, strAlbum, strYear)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
'''
|
'''
|
||||||
)
|
|
||||||
kodicursor.execute(query, (artistid, item['Album'], 0))
|
kodicursor.execute(query, (artistid, item['Album'], 0))
|
||||||
# else:
|
# else:
|
||||||
if False:
|
if False:
|
||||||
album_artists = " / ".join(album_artists)
|
album_artists = " / ".join(album_artists)
|
||||||
query = ' '.join((
|
query = '''
|
||||||
|
SELECT strArtists
|
||||||
"SELECT strArtists",
|
FROM album
|
||||||
"FROM album",
|
WHERE idAlbum = ?
|
||||||
"WHERE idAlbum = ?"
|
'''
|
||||||
))
|
|
||||||
kodicursor.execute(query, (albumid,))
|
kodicursor.execute(query, (albumid,))
|
||||||
result = kodicursor.fetchone()
|
result = kodicursor.fetchone()
|
||||||
if result and result[0] != album_artists:
|
if result and result[0] != album_artists:
|
||||||
|
@ -1895,18 +1892,16 @@ class Music(Items):
|
||||||
kodicursor.execute(query, (album_artists, albumid))
|
kodicursor.execute(query, (album_artists, albumid))
|
||||||
|
|
||||||
# Add genres
|
# Add genres
|
||||||
self.kodi_db.addMusicGenres(songid, genres, "song")
|
if genres:
|
||||||
|
self.kodi_db.addMusicGenres(songid, genres, v.KODI_TYPE_SONG)
|
||||||
|
|
||||||
# Update artwork
|
# Update artwork
|
||||||
allart = API.getAllArtwork(parentInfo=True)
|
allart = API.getAllArtwork(parentInfo=True)
|
||||||
if hasEmbeddedCover:
|
artwork.addArtwork(allart, songid, v.KODI_TYPE_SONG, kodicursor)
|
||||||
allart["Primary"] = "image://music@" + artwork.single_urlencode( playurl )
|
|
||||||
artwork.addArtwork(allart, songid, "song", kodicursor)
|
|
||||||
|
|
||||||
# if item.get('AlbumId') is None:
|
|
||||||
if item.get('parentKey') is None:
|
if item.get('parentKey') is None:
|
||||||
# Update album artwork
|
# Update album artwork
|
||||||
artwork.addArtwork(allart, albumid, "album", kodicursor)
|
artwork.addArtwork(allart, albumid, v.KODI_TYPE_ALBUM, kodicursor)
|
||||||
|
|
||||||
def remove(self, itemid):
|
def remove(self, itemid):
|
||||||
# Remove kodiid, fileid, pathid, plex reference
|
# Remove kodiid, fileid, pathid, plex reference
|
||||||
|
|
1
resources/lib/library_sync/__init__.py
Normal file
1
resources/lib/library_sync/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Dummy file to make this directory a package.
|
88
resources/lib/library_sync/fanart.py
Normal file
88
resources/lib/library_sync/fanart.py
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from logging import getLogger
|
||||||
|
from threading import Thread
|
||||||
|
from Queue import Empty
|
||||||
|
|
||||||
|
from xbmc import sleep
|
||||||
|
|
||||||
|
from utils import ThreadMethodsAdditionalStop, ThreadMethods, window, \
|
||||||
|
ThreadMethodsAdditionalSuspend
|
||||||
|
import plexdb_functions as plexdb
|
||||||
|
import itemtypes
|
||||||
|
import variables as v
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
log = getLogger("PLEX."+__name__)
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
@ThreadMethodsAdditionalSuspend('suspend_LibraryThread')
|
||||||
|
@ThreadMethodsAdditionalStop('plex_shouldStop')
|
||||||
|
@ThreadMethods
|
||||||
|
class Process_Fanart_Thread(Thread):
|
||||||
|
"""
|
||||||
|
Threaded download of additional fanart in the background
|
||||||
|
|
||||||
|
Input:
|
||||||
|
queue Queue.Queue() object that you will need to fill with
|
||||||
|
dicts of the following form:
|
||||||
|
{
|
||||||
|
'plex_id': the Plex id as a string
|
||||||
|
'plex_type': the Plex media type, e.g. 'movie'
|
||||||
|
'refresh': True/False if True, will overwrite any 3rd party
|
||||||
|
fanart. If False, will only get missing
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
def __init__(self, queue):
|
||||||
|
self.queue = queue
|
||||||
|
Thread.__init__(self)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""
|
||||||
|
Catch all exceptions and log them
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.__run()
|
||||||
|
except Exception as e:
|
||||||
|
log.error('Exception %s' % e)
|
||||||
|
import traceback
|
||||||
|
log.error("Traceback:\n%s" % traceback.format_exc())
|
||||||
|
|
||||||
|
def __run(self):
|
||||||
|
"""
|
||||||
|
Do the work
|
||||||
|
"""
|
||||||
|
log.debug("---===### Starting FanartSync ###===---")
|
||||||
|
threadStopped = self.threadStopped
|
||||||
|
threadSuspended = self.threadSuspended
|
||||||
|
queue = self.queue
|
||||||
|
while not threadStopped():
|
||||||
|
# In the event the server goes offline
|
||||||
|
while threadSuspended() or window('plex_dbScan'):
|
||||||
|
# Set in service.py
|
||||||
|
if threadStopped():
|
||||||
|
# Abort was requested while waiting. We should exit
|
||||||
|
log.info("---===### Stopped FanartSync ###===---")
|
||||||
|
return
|
||||||
|
sleep(1000)
|
||||||
|
# grabs Plex item from queue
|
||||||
|
try:
|
||||||
|
item = queue.get(block=False)
|
||||||
|
except Empty:
|
||||||
|
sleep(200)
|
||||||
|
continue
|
||||||
|
|
||||||
|
log.debug('Get additional fanart for Plex id %s' % item['plex_id'])
|
||||||
|
with getattr(itemtypes,
|
||||||
|
v.ITEMTYPE_FROM_PLEXTYPE[item['plex_type']])() as cls:
|
||||||
|
result = cls.getfanart(item['plex_id'],
|
||||||
|
refresh=item['refresh'])
|
||||||
|
if result is True:
|
||||||
|
log.debug('Done getting fanart for Plex id %s'
|
||||||
|
% item['plex_id'])
|
||||||
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
|
plex_db.set_fanart_synched(item['plex_id'])
|
||||||
|
queue.task_done()
|
||||||
|
log.debug("---===### Stopped FanartSync ###===---")
|
140
resources/lib/library_sync/get_metadata.py
Normal file
140
resources/lib/library_sync/get_metadata.py
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from logging import getLogger
|
||||||
|
from threading import Thread
|
||||||
|
from Queue import Empty
|
||||||
|
|
||||||
|
from xbmc import sleep
|
||||||
|
|
||||||
|
from utils import ThreadMethodsAdditionalStop, ThreadMethods, window
|
||||||
|
from PlexFunctions import GetPlexMetadata, GetAllPlexChildren
|
||||||
|
import sync_info
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
log = getLogger("PLEX."+__name__)
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
@ThreadMethodsAdditionalStop('suspend_LibraryThread')
|
||||||
|
@ThreadMethods
|
||||||
|
class Threaded_Get_Metadata(Thread):
|
||||||
|
"""
|
||||||
|
Threaded download of Plex XML metadata for a certain library item.
|
||||||
|
Fills the out_queue with the downloaded etree XML objects
|
||||||
|
|
||||||
|
Input:
|
||||||
|
queue Queue.Queue() object that you'll need to fill up
|
||||||
|
with Plex itemIds
|
||||||
|
out_queue Queue() object where this thread will store
|
||||||
|
the downloaded metadata XMLs as etree objects
|
||||||
|
"""
|
||||||
|
def __init__(self, queue, out_queue):
|
||||||
|
self.queue = queue
|
||||||
|
self.out_queue = out_queue
|
||||||
|
Thread.__init__(self)
|
||||||
|
|
||||||
|
def terminate_now(self):
|
||||||
|
"""
|
||||||
|
Needed to terminate this thread, because there might be items left in
|
||||||
|
the queue which could cause other threads to hang
|
||||||
|
"""
|
||||||
|
while not self.queue.empty():
|
||||||
|
# Still try because remaining item might have been taken
|
||||||
|
try:
|
||||||
|
self.queue.get(block=False)
|
||||||
|
except Empty:
|
||||||
|
sleep(10)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
self.queue.task_done()
|
||||||
|
if self.threadStopped():
|
||||||
|
# Shutdown from outside requested; purge out_queue as well
|
||||||
|
while not self.out_queue.empty():
|
||||||
|
# Still try because remaining item might have been taken
|
||||||
|
try:
|
||||||
|
self.out_queue.get(block=False)
|
||||||
|
except Empty:
|
||||||
|
sleep(10)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
self.out_queue.task_done()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""
|
||||||
|
Catch all exceptions and log them
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.__run()
|
||||||
|
except Exception as e:
|
||||||
|
log.error('Exception %s' % e)
|
||||||
|
import traceback
|
||||||
|
log.error("Traceback:\n%s" % traceback.format_exc())
|
||||||
|
|
||||||
|
def __run(self):
|
||||||
|
"""
|
||||||
|
Do the work
|
||||||
|
"""
|
||||||
|
log.debug('Starting get metadata thread')
|
||||||
|
# cache local variables because it's faster
|
||||||
|
queue = self.queue
|
||||||
|
out_queue = self.out_queue
|
||||||
|
threadStopped = self.threadStopped
|
||||||
|
while threadStopped() is False:
|
||||||
|
# grabs Plex item from queue
|
||||||
|
try:
|
||||||
|
item = queue.get(block=False)
|
||||||
|
# Empty queue
|
||||||
|
except Empty:
|
||||||
|
sleep(20)
|
||||||
|
continue
|
||||||
|
# Download Metadata
|
||||||
|
xml = GetPlexMetadata(item['itemId'])
|
||||||
|
if xml is None:
|
||||||
|
# Did not receive a valid XML - skip that item for now
|
||||||
|
log.error("Could not get metadata for %s. Skipping that item "
|
||||||
|
"for now" % item['itemId'])
|
||||||
|
# Increase BOTH counters - since metadata won't be processed
|
||||||
|
with sync_info.LOCK:
|
||||||
|
sync_info.GET_METADATA_COUNT += 1
|
||||||
|
sync_info.PROCESS_METADATA_COUNT += 1
|
||||||
|
queue.task_done()
|
||||||
|
continue
|
||||||
|
elif xml == 401:
|
||||||
|
log.error('HTTP 401 returned by PMS. Too much strain? '
|
||||||
|
'Cancelling sync for now')
|
||||||
|
window('plex_scancrashed', value='401')
|
||||||
|
# Kill remaining items in queue (for main thread to cont.)
|
||||||
|
queue.task_done()
|
||||||
|
break
|
||||||
|
|
||||||
|
item['XML'] = xml
|
||||||
|
if item.get('get_children') is True:
|
||||||
|
children_xml = GetAllPlexChildren(item['itemId'])
|
||||||
|
try:
|
||||||
|
children_xml[0].attrib
|
||||||
|
except (TypeError, IndexError, AttributeError):
|
||||||
|
log.error('Could not get children for Plex id %s'
|
||||||
|
% item['itemId'])
|
||||||
|
else:
|
||||||
|
item['children'] = []
|
||||||
|
for child in children_xml:
|
||||||
|
child_xml = GetPlexMetadata(child.attrib['ratingKey'])
|
||||||
|
try:
|
||||||
|
child_xml[0].attrib
|
||||||
|
except (TypeError, IndexError, AttributeError):
|
||||||
|
log.error('Could not get child for Plex id %s'
|
||||||
|
% child.attrib['ratingKey'])
|
||||||
|
else:
|
||||||
|
item['children'].append(child_xml[0])
|
||||||
|
|
||||||
|
# place item into out queue
|
||||||
|
out_queue.put(item)
|
||||||
|
# Keep track of where we are at
|
||||||
|
with sync_info.LOCK:
|
||||||
|
sync_info.GET_METADATA_COUNT += 1
|
||||||
|
# signals to queue job is done
|
||||||
|
queue.task_done()
|
||||||
|
# Empty queue in case PKC was shut down (main thread hangs otherwise)
|
||||||
|
self.terminate_now()
|
||||||
|
log.debug('Get metadata thread terminated')
|
104
resources/lib/library_sync/process_metadata.py
Normal file
104
resources/lib/library_sync/process_metadata.py
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from logging import getLogger
|
||||||
|
from threading import Thread
|
||||||
|
from Queue import Empty
|
||||||
|
|
||||||
|
from xbmc import sleep
|
||||||
|
|
||||||
|
from utils import ThreadMethodsAdditionalStop, ThreadMethods
|
||||||
|
import itemtypes
|
||||||
|
import sync_info
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
log = getLogger("PLEX."+__name__)
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
@ThreadMethodsAdditionalStop('suspend_LibraryThread')
|
||||||
|
@ThreadMethods
|
||||||
|
class Threaded_Process_Metadata(Thread):
|
||||||
|
"""
|
||||||
|
Not yet implemented for more than 1 thread - if ever. Only to be called by
|
||||||
|
ONE thread!
|
||||||
|
Processes the XML metadata in the queue
|
||||||
|
|
||||||
|
Input:
|
||||||
|
queue: Queue.Queue() object that you'll need to fill up with
|
||||||
|
the downloaded XML eTree objects
|
||||||
|
item_type: as used to call functions in itemtypes.py e.g. 'Movies' =>
|
||||||
|
itemtypes.Movies()
|
||||||
|
"""
|
||||||
|
def __init__(self, queue, item_type):
|
||||||
|
self.queue = queue
|
||||||
|
self.item_type = item_type
|
||||||
|
Thread.__init__(self)
|
||||||
|
|
||||||
|
def terminate_now(self):
|
||||||
|
"""
|
||||||
|
Needed to terminate this thread, because there might be items left in
|
||||||
|
the queue which could cause other threads to hang
|
||||||
|
"""
|
||||||
|
while not self.queue.empty():
|
||||||
|
# Still try because remaining item might have been taken
|
||||||
|
try:
|
||||||
|
self.queue.get(block=False)
|
||||||
|
except Empty:
|
||||||
|
sleep(10)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
self.queue.task_done()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""
|
||||||
|
Catch all exceptions and log them
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.__run()
|
||||||
|
except Exception as e:
|
||||||
|
log.error('Exception %s' % e)
|
||||||
|
import traceback
|
||||||
|
log.error("Traceback:\n%s" % traceback.format_exc())
|
||||||
|
|
||||||
|
def __run(self):
|
||||||
|
"""
|
||||||
|
Do the work
|
||||||
|
"""
|
||||||
|
log.debug('Processing thread started')
|
||||||
|
# Constructs the method name, e.g. itemtypes.Movies
|
||||||
|
item_fct = getattr(itemtypes, self.item_type)
|
||||||
|
# cache local variables because it's faster
|
||||||
|
queue = self.queue
|
||||||
|
threadStopped = self.threadStopped
|
||||||
|
with item_fct() as item_class:
|
||||||
|
while threadStopped() is False:
|
||||||
|
# grabs item from queue
|
||||||
|
try:
|
||||||
|
item = queue.get(block=False)
|
||||||
|
except Empty:
|
||||||
|
sleep(20)
|
||||||
|
continue
|
||||||
|
# Do the work
|
||||||
|
item_method = getattr(item_class, item['method'])
|
||||||
|
if item.get('children') is not None:
|
||||||
|
item_method(item['XML'][0],
|
||||||
|
viewtag=item['viewName'],
|
||||||
|
viewid=item['viewId'],
|
||||||
|
children=item['children'])
|
||||||
|
else:
|
||||||
|
item_method(item['XML'][0],
|
||||||
|
viewtag=item['viewName'],
|
||||||
|
viewid=item['viewId'])
|
||||||
|
# Keep track of where we are at
|
||||||
|
try:
|
||||||
|
log.debug('found child: %s'
|
||||||
|
% item['children'].attrib)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
with sync_info.LOCK:
|
||||||
|
sync_info.PROCESS_METADATA_COUNT += 1
|
||||||
|
sync_info.PROCESSING_VIEW_NAME = item['title']
|
||||||
|
queue.task_done()
|
||||||
|
self.terminate_now()
|
||||||
|
log.debug('Processing thread terminated')
|
81
resources/lib/library_sync/sync_info.py
Normal file
81
resources/lib/library_sync/sync_info.py
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from logging import getLogger
|
||||||
|
from threading import Thread, Lock
|
||||||
|
|
||||||
|
from xbmc import sleep
|
||||||
|
|
||||||
|
from utils import ThreadMethodsAdditionalStop, ThreadMethods, language as lang
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
log = getLogger("PLEX."+__name__)
|
||||||
|
|
||||||
|
GET_METADATA_COUNT = 0
|
||||||
|
PROCESS_METADATA_COUNT = 0
|
||||||
|
PROCESSING_VIEW_NAME = ''
|
||||||
|
LOCK = Lock()
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
@ThreadMethodsAdditionalStop('suspend_LibraryThread')
|
||||||
|
@ThreadMethods
|
||||||
|
class Threaded_Show_Sync_Info(Thread):
|
||||||
|
"""
|
||||||
|
Threaded class to show the Kodi statusbar of the metadata download.
|
||||||
|
|
||||||
|
Input:
|
||||||
|
dialog xbmcgui.DialogProgressBG() object to show progress
|
||||||
|
total: Total number of items to get
|
||||||
|
"""
|
||||||
|
def __init__(self, dialog, total, item_type):
|
||||||
|
self.total = total
|
||||||
|
self.dialog = dialog
|
||||||
|
self.item_type = item_type
|
||||||
|
Thread.__init__(self)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""
|
||||||
|
Catch all exceptions and log them
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.__run()
|
||||||
|
except Exception as e:
|
||||||
|
log.error('Exception %s' % e)
|
||||||
|
import traceback
|
||||||
|
log.error("Traceback:\n%s" % traceback.format_exc())
|
||||||
|
|
||||||
|
def __run(self):
|
||||||
|
"""
|
||||||
|
Do the work
|
||||||
|
"""
|
||||||
|
log.debug('Show sync info thread started')
|
||||||
|
# cache local variables because it's faster
|
||||||
|
total = self.total
|
||||||
|
dialog = self.dialog
|
||||||
|
threadStopped = self.threadStopped
|
||||||
|
dialog.create("%s: Sync %s: %s items"
|
||||||
|
% (lang(29999), self.item_type, str(total)),
|
||||||
|
"Starting")
|
||||||
|
|
||||||
|
total = 2 * total
|
||||||
|
totalProgress = 0
|
||||||
|
while threadStopped() is False:
|
||||||
|
with LOCK:
|
||||||
|
get_progress = GET_METADATA_COUNT
|
||||||
|
process_progress = PROCESS_METADATA_COUNT
|
||||||
|
viewName = PROCESSING_VIEW_NAME
|
||||||
|
totalProgress = get_progress + process_progress
|
||||||
|
try:
|
||||||
|
percentage = int(float(totalProgress) / float(total)*100.0)
|
||||||
|
except ZeroDivisionError:
|
||||||
|
percentage = 0
|
||||||
|
dialog.update(percentage,
|
||||||
|
message="%s downloaded. %s processed: %s"
|
||||||
|
% (get_progress,
|
||||||
|
process_progress,
|
||||||
|
viewName))
|
||||||
|
# Sleep for x milliseconds
|
||||||
|
sleep(200)
|
||||||
|
dialog.close()
|
||||||
|
log.debug('Show sync info thread terminated')
|
|
@ -3,7 +3,7 @@
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from threading import Thread, Lock
|
from threading import Thread
|
||||||
import Queue
|
import Queue
|
||||||
from random import shuffle
|
from random import shuffle
|
||||||
|
|
||||||
|
@ -28,6 +28,10 @@ import variables as v
|
||||||
from PlexFunctions import GetPlexMetadata, GetAllPlexLeaves, scrobble, \
|
from PlexFunctions import GetPlexMetadata, GetAllPlexLeaves, scrobble, \
|
||||||
GetPlexSectionResults, GetAllPlexChildren, GetPMSStatus
|
GetPlexSectionResults, GetAllPlexChildren, GetPMSStatus
|
||||||
import PlexAPI
|
import PlexAPI
|
||||||
|
from library_sync.get_metadata import Threaded_Get_Metadata
|
||||||
|
from library_sync.process_metadata import Threaded_Process_Metadata
|
||||||
|
import library_sync.sync_info as sync_info
|
||||||
|
from library_sync.fanart import Process_Fanart_Thread
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -36,282 +40,6 @@ log = logging.getLogger("PLEX."+__name__)
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
@ThreadMethodsAdditionalStop('suspend_LibraryThread')
|
|
||||||
@ThreadMethods
|
|
||||||
class ThreadedGetMetadata(Thread):
|
|
||||||
"""
|
|
||||||
Threaded download of Plex XML metadata for a certain library item.
|
|
||||||
Fills the out_queue with the downloaded etree XML objects
|
|
||||||
|
|
||||||
Input:
|
|
||||||
queue Queue.Queue() object that you'll need to fill up
|
|
||||||
with Plex itemIds
|
|
||||||
out_queue Queue() object where this thread will store
|
|
||||||
the downloaded metadata XMLs as etree objects
|
|
||||||
lock Lock(), used for counting where we are
|
|
||||||
"""
|
|
||||||
def __init__(self, queue, out_queue, lock, processlock):
|
|
||||||
self.queue = queue
|
|
||||||
self.out_queue = out_queue
|
|
||||||
self.lock = lock
|
|
||||||
self.processlock = processlock
|
|
||||||
Thread.__init__(self)
|
|
||||||
|
|
||||||
def terminateNow(self):
|
|
||||||
while not self.queue.empty():
|
|
||||||
# Still try because remaining item might have been taken
|
|
||||||
try:
|
|
||||||
self.queue.get(block=False)
|
|
||||||
except Queue.Empty:
|
|
||||||
xbmc.sleep(10)
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
self.queue.task_done()
|
|
||||||
if self.threadStopped():
|
|
||||||
# Shutdown from outside requested; purge out_queue as well
|
|
||||||
while not self.out_queue.empty():
|
|
||||||
# Still try because remaining item might have been taken
|
|
||||||
try:
|
|
||||||
self.out_queue.get(block=False)
|
|
||||||
except Queue.Empty:
|
|
||||||
xbmc.sleep(10)
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
self.out_queue.task_done()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
# cache local variables because it's faster
|
|
||||||
queue = self.queue
|
|
||||||
out_queue = self.out_queue
|
|
||||||
lock = self.lock
|
|
||||||
processlock = self.processlock
|
|
||||||
threadStopped = self.threadStopped
|
|
||||||
global getMetadataCount
|
|
||||||
global processMetadataCount
|
|
||||||
while threadStopped() is False:
|
|
||||||
# grabs Plex item from queue
|
|
||||||
try:
|
|
||||||
updateItem = queue.get(block=False)
|
|
||||||
# Empty queue
|
|
||||||
except Queue.Empty:
|
|
||||||
xbmc.sleep(10)
|
|
||||||
continue
|
|
||||||
# Download Metadata
|
|
||||||
plexXML = GetPlexMetadata(updateItem['itemId'])
|
|
||||||
if plexXML is None:
|
|
||||||
# Did not receive a valid XML - skip that item for now
|
|
||||||
log.warn("Could not get metadata for %s. Skipping that item "
|
|
||||||
"for now" % updateItem['itemId'])
|
|
||||||
# Increase BOTH counters - since metadata won't be processed
|
|
||||||
with lock:
|
|
||||||
getMetadataCount += 1
|
|
||||||
with processlock:
|
|
||||||
processMetadataCount += 1
|
|
||||||
queue.task_done()
|
|
||||||
continue
|
|
||||||
elif plexXML == 401:
|
|
||||||
log.warn('HTTP 401 returned by PMS. Too much strain? '
|
|
||||||
'Cancelling sync for now')
|
|
||||||
window('plex_scancrashed', value='401')
|
|
||||||
# Kill remaining items in queue (for main thread to cont.)
|
|
||||||
queue.task_done()
|
|
||||||
break
|
|
||||||
|
|
||||||
updateItem['XML'] = plexXML
|
|
||||||
# place item into out queue
|
|
||||||
out_queue.put(updateItem)
|
|
||||||
# Keep track of where we are at
|
|
||||||
with lock:
|
|
||||||
getMetadataCount += 1
|
|
||||||
# signals to queue job is done
|
|
||||||
queue.task_done()
|
|
||||||
# Empty queue in case PKC was shut down (main thread hangs otherwise)
|
|
||||||
self.terminateNow()
|
|
||||||
log.debug('Download thread terminated')
|
|
||||||
|
|
||||||
|
|
||||||
@ThreadMethodsAdditionalStop('suspend_LibraryThread')
|
|
||||||
@ThreadMethods
|
|
||||||
class ThreadedProcessMetadata(Thread):
|
|
||||||
"""
|
|
||||||
Not yet implemented - if ever. Only to be called by ONE thread!
|
|
||||||
Processes the XML metadata in the queue
|
|
||||||
|
|
||||||
Input:
|
|
||||||
queue: Queue.Queue() object that you'll need to fill up with
|
|
||||||
the downloaded XML eTree objects
|
|
||||||
itemType: as used to call functions in itemtypes.py
|
|
||||||
e.g. 'Movies' => itemtypes.Movies()
|
|
||||||
lock: Lock(), used for counting where we are
|
|
||||||
"""
|
|
||||||
def __init__(self, queue, itemType, lock):
|
|
||||||
self.queue = queue
|
|
||||||
self.lock = lock
|
|
||||||
self.itemType = itemType
|
|
||||||
Thread.__init__(self)
|
|
||||||
|
|
||||||
def terminateNow(self):
|
|
||||||
while not self.queue.empty():
|
|
||||||
# Still try because remaining item might have been taken
|
|
||||||
try:
|
|
||||||
self.queue.get(block=False)
|
|
||||||
except Queue.Empty:
|
|
||||||
xbmc.sleep(10)
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
self.queue.task_done()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
# Constructs the method name, e.g. itemtypes.Movies
|
|
||||||
itemFkt = getattr(itemtypes, self.itemType)
|
|
||||||
# cache local variables because it's faster
|
|
||||||
queue = self.queue
|
|
||||||
lock = self.lock
|
|
||||||
threadStopped = self.threadStopped
|
|
||||||
global processMetadataCount
|
|
||||||
global processingViewName
|
|
||||||
with itemFkt() as item:
|
|
||||||
while threadStopped() is False:
|
|
||||||
# grabs item from queue
|
|
||||||
try:
|
|
||||||
updateItem = queue.get(block=False)
|
|
||||||
except Queue.Empty:
|
|
||||||
xbmc.sleep(10)
|
|
||||||
continue
|
|
||||||
# Do the work
|
|
||||||
plexitem = updateItem['XML']
|
|
||||||
method = updateItem['method']
|
|
||||||
viewName = updateItem['viewName']
|
|
||||||
viewId = updateItem['viewId']
|
|
||||||
title = updateItem['title']
|
|
||||||
itemSubFkt = getattr(item, method)
|
|
||||||
# Get the one child entry in the xml and process
|
|
||||||
for child in plexitem:
|
|
||||||
itemSubFkt(child,
|
|
||||||
viewtag=viewName,
|
|
||||||
viewid=viewId)
|
|
||||||
# Keep track of where we are at
|
|
||||||
with lock:
|
|
||||||
processMetadataCount += 1
|
|
||||||
processingViewName = title
|
|
||||||
# signals to queue job is done
|
|
||||||
queue.task_done()
|
|
||||||
# Empty queue in case PKC was shut down (main thread hangs otherwise)
|
|
||||||
self.terminateNow()
|
|
||||||
log.debug('Processing thread terminated')
|
|
||||||
|
|
||||||
|
|
||||||
@ThreadMethodsAdditionalStop('suspend_LibraryThread')
|
|
||||||
@ThreadMethods
|
|
||||||
class ThreadedShowSyncInfo(Thread):
|
|
||||||
"""
|
|
||||||
Threaded class to show the Kodi statusbar of the metadata download.
|
|
||||||
|
|
||||||
Input:
|
|
||||||
dialog xbmcgui.DialogProgressBG() object to show progress
|
|
||||||
locks = [downloadLock, processLock] Locks() to the other threads
|
|
||||||
total: Total number of items to get
|
|
||||||
"""
|
|
||||||
def __init__(self, dialog, locks, total, itemType):
|
|
||||||
self.locks = locks
|
|
||||||
self.total = total
|
|
||||||
self.dialog = dialog
|
|
||||||
self.itemType = itemType
|
|
||||||
Thread.__init__(self)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
# cache local variables because it's faster
|
|
||||||
total = self.total
|
|
||||||
dialog = self.dialog
|
|
||||||
threadStopped = self.threadStopped
|
|
||||||
downloadLock = self.locks[0]
|
|
||||||
processLock = self.locks[1]
|
|
||||||
dialog.create("%s: Sync %s: %s items"
|
|
||||||
% (lang(29999), self.itemType, str(total)),
|
|
||||||
"Starting")
|
|
||||||
global getMetadataCount
|
|
||||||
global processMetadataCount
|
|
||||||
global processingViewName
|
|
||||||
total = 2 * total
|
|
||||||
totalProgress = 0
|
|
||||||
while threadStopped() is False:
|
|
||||||
with downloadLock:
|
|
||||||
getMetadataProgress = getMetadataCount
|
|
||||||
with processLock:
|
|
||||||
processMetadataProgress = processMetadataCount
|
|
||||||
viewName = processingViewName
|
|
||||||
totalProgress = getMetadataProgress + processMetadataProgress
|
|
||||||
try:
|
|
||||||
percentage = int(float(totalProgress) / float(total)*100.0)
|
|
||||||
except ZeroDivisionError:
|
|
||||||
percentage = 0
|
|
||||||
dialog.update(percentage,
|
|
||||||
message="%s downloaded. %s processed: %s"
|
|
||||||
% (getMetadataProgress,
|
|
||||||
processMetadataProgress,
|
|
||||||
viewName))
|
|
||||||
# Sleep for x milliseconds
|
|
||||||
xbmc.sleep(200)
|
|
||||||
dialog.close()
|
|
||||||
log.debug('Dialog Infobox thread terminated')
|
|
||||||
|
|
||||||
|
|
||||||
@ThreadMethodsAdditionalSuspend('suspend_LibraryThread')
|
|
||||||
@ThreadMethodsAdditionalStop('plex_shouldStop')
|
|
||||||
@ThreadMethods
|
|
||||||
class ProcessFanartThread(Thread):
|
|
||||||
"""
|
|
||||||
Threaded download of additional fanart in the background
|
|
||||||
|
|
||||||
Input:
|
|
||||||
queue Queue.Queue() object that you will need to fill with
|
|
||||||
dicts of the following form:
|
|
||||||
{
|
|
||||||
'plex_id': the Plex id as a string
|
|
||||||
'plex_type': the Plex media type, e.g. 'movie'
|
|
||||||
'refresh': True/False if True, will overwrite any 3rd party
|
|
||||||
fanart. If False, will only get missing
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
def __init__(self, queue):
|
|
||||||
self.queue = queue
|
|
||||||
Thread.__init__(self)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
threadStopped = self.threadStopped
|
|
||||||
threadSuspended = self.threadSuspended
|
|
||||||
queue = self.queue
|
|
||||||
log.info("---===### Starting FanartSync ###===---")
|
|
||||||
while not threadStopped():
|
|
||||||
# In the event the server goes offline
|
|
||||||
while threadSuspended() or window('plex_dbScan'):
|
|
||||||
# Set in service.py
|
|
||||||
if threadStopped():
|
|
||||||
# Abort was requested while waiting. We should exit
|
|
||||||
log.info("---===### Stopped FanartSync ###===---")
|
|
||||||
return
|
|
||||||
xbmc.sleep(1000)
|
|
||||||
# grabs Plex item from queue
|
|
||||||
try:
|
|
||||||
item = queue.get(block=False)
|
|
||||||
except Queue.Empty:
|
|
||||||
xbmc.sleep(200)
|
|
||||||
continue
|
|
||||||
|
|
||||||
log.debug('Get additional fanart for Plex id %s' % item['plex_id'])
|
|
||||||
with getattr(itemtypes,
|
|
||||||
v.ITEMTYPE_FROM_PLEXTYPE[item['plex_type']])() as cls:
|
|
||||||
result = cls.getfanart(item['plex_id'],
|
|
||||||
refresh=item['refresh'])
|
|
||||||
if result is True:
|
|
||||||
log.debug('Done getting fanart for Plex id %s'
|
|
||||||
% item['plex_id'])
|
|
||||||
with plexdb.Get_Plex_DB() as plex_db:
|
|
||||||
plex_db.set_fanart_synched(item['plex_id'])
|
|
||||||
queue.task_done()
|
|
||||||
log.info("---===### Stopped FanartSync ###===---")
|
|
||||||
|
|
||||||
|
|
||||||
@ThreadMethodsAdditionalSuspend('suspend_LibraryThread')
|
@ThreadMethodsAdditionalSuspend('suspend_LibraryThread')
|
||||||
@ThreadMethodsAdditionalStop('plex_shouldStop')
|
@ThreadMethodsAdditionalStop('plex_shouldStop')
|
||||||
@ThreadMethods
|
@ThreadMethods
|
||||||
|
@ -330,7 +58,7 @@ class LibrarySync(Thread):
|
||||||
self.sessionKeys = []
|
self.sessionKeys = []
|
||||||
self.fanartqueue = Queue.Queue()
|
self.fanartqueue = Queue.Queue()
|
||||||
if settings('FanartTV') == 'true':
|
if settings('FanartTV') == 'true':
|
||||||
self.fanartthread = ProcessFanartThread(self.fanartqueue)
|
self.fanartthread = Process_Fanart_Thread(self.fanartqueue)
|
||||||
# How long should we wait at least to process new/changed PMS items?
|
# How long should we wait at least to process new/changed PMS items?
|
||||||
self.saftyMargin = int(settings('backgroundsync_saftyMargin'))
|
self.saftyMargin = int(settings('backgroundsync_saftyMargin'))
|
||||||
|
|
||||||
|
@ -346,7 +74,6 @@ class LibrarySync(Thread):
|
||||||
self.enableMusic = settings('enableMusic') == "true"
|
self.enableMusic = settings('enableMusic') == "true"
|
||||||
self.enableBackgroundSync = settings(
|
self.enableBackgroundSync = settings(
|
||||||
'enableBackgroundSync') == "true"
|
'enableBackgroundSync') == "true"
|
||||||
self.limitindex = int(settings('limitindex'))
|
|
||||||
|
|
||||||
# Init for replacing paths
|
# Init for replacing paths
|
||||||
window('remapSMB', value=settings('remapSMB'))
|
window('remapSMB', value=settings('remapSMB'))
|
||||||
|
@ -422,8 +149,7 @@ class LibrarySync(Thread):
|
||||||
if not view.attrib['type'] == mediatype:
|
if not view.attrib['type'] == mediatype:
|
||||||
continue
|
continue
|
||||||
libraryId = view.attrib['key']
|
libraryId = view.attrib['key']
|
||||||
items = GetAllPlexLeaves(libraryId,
|
items = GetAllPlexLeaves(libraryId)
|
||||||
containerSize=self.limitindex)
|
|
||||||
if items in (None, 401):
|
if items in (None, 401):
|
||||||
log.error("Could not download section %s"
|
log.error("Could not download section %s"
|
||||||
% view.attrib['key'])
|
% view.attrib['key'])
|
||||||
|
@ -468,9 +194,7 @@ class LibrarySync(Thread):
|
||||||
# Let the PMS process this first!
|
# Let the PMS process this first!
|
||||||
xbmc.sleep(1000)
|
xbmc.sleep(1000)
|
||||||
# Get PMS items to find the item we just changed
|
# Get PMS items to find the item we just changed
|
||||||
items = GetAllPlexLeaves(libraryId,
|
items = GetAllPlexLeaves(libraryId, lastViewedAt=timestamp)
|
||||||
lastViewedAt=timestamp,
|
|
||||||
containerSize=self.limitindex)
|
|
||||||
# Toggle watched state back
|
# Toggle watched state back
|
||||||
scrobble(plexId, 'unwatched')
|
scrobble(plexId, 'unwatched')
|
||||||
if items in (None, 401):
|
if items in (None, 401):
|
||||||
|
@ -704,8 +428,8 @@ class LibrarySync(Thread):
|
||||||
viewid=folderid,
|
viewid=folderid,
|
||||||
delete=True)
|
delete=True)
|
||||||
# Added new playlist
|
# Added new playlist
|
||||||
if (foldername not in playlists and
|
if (foldername not in playlists and mediatype in
|
||||||
mediatype in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)):
|
(v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)):
|
||||||
playlistXSP(mediatype,
|
playlistXSP(mediatype,
|
||||||
foldername,
|
foldername,
|
||||||
folderid,
|
folderid,
|
||||||
|
@ -730,8 +454,8 @@ class LibrarySync(Thread):
|
||||||
else:
|
else:
|
||||||
# Validate the playlist exists or recreate it
|
# Validate the playlist exists or recreate it
|
||||||
if mediatype != v.PLEX_TYPE_ARTIST:
|
if mediatype != v.PLEX_TYPE_ARTIST:
|
||||||
if (foldername not in playlists and
|
if (foldername not in playlists and mediatype in
|
||||||
mediatype in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)):
|
(v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)):
|
||||||
playlistXSP(mediatype,
|
playlistXSP(mediatype,
|
||||||
foldername,
|
foldername,
|
||||||
folderid,
|
folderid,
|
||||||
|
@ -781,7 +505,8 @@ class LibrarySync(Thread):
|
||||||
|
|
||||||
for view in sections:
|
for view in sections:
|
||||||
itemType = view.attrib['type']
|
itemType = view.attrib['type']
|
||||||
if itemType in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW, v.PLEX_TYPE_PHOTO): # NOT artist for now
|
if (itemType in
|
||||||
|
(v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW, v.PLEX_TYPE_PHOTO)):
|
||||||
self.sorted_views.append(view.attrib['title'])
|
self.sorted_views.append(view.attrib['title'])
|
||||||
log.debug('Sorted views: %s' % self.sorted_views)
|
log.debug('Sorted views: %s' % self.sorted_views)
|
||||||
|
|
||||||
|
@ -863,7 +588,8 @@ class LibrarySync(Thread):
|
||||||
with itemtypes.Music() as music:
|
with itemtypes.Music() as music:
|
||||||
music.remove(item['plex_id'])
|
music.remove(item['plex_id'])
|
||||||
|
|
||||||
def GetUpdatelist(self, xml, itemType, method, viewName, viewId):
|
def GetUpdatelist(self, xml, itemType, method, viewName, viewId,
|
||||||
|
get_children=False):
|
||||||
"""
|
"""
|
||||||
THIS METHOD NEEDS TO BE FAST! => e.g. no API calls
|
THIS METHOD NEEDS TO BE FAST! => e.g. no API calls
|
||||||
|
|
||||||
|
@ -876,6 +602,8 @@ class LibrarySync(Thread):
|
||||||
see itemtypes.py
|
see itemtypes.py
|
||||||
viewName: Name of the Plex view (e.g. 'My TV shows')
|
viewName: Name of the Plex view (e.g. 'My TV shows')
|
||||||
viewId: Id/Key of Plex library (e.g. '1')
|
viewId: Id/Key of Plex library (e.g. '1')
|
||||||
|
get_children: will get Plex children of the item if True,
|
||||||
|
e.g. for music albums
|
||||||
|
|
||||||
Output: self.updatelist, self.allPlexElementsId
|
Output: self.updatelist, self.allPlexElementsId
|
||||||
self.updatelist APPENDED(!!) list itemids (Plex Keys as
|
self.updatelist APPENDED(!!) list itemids (Plex Keys as
|
||||||
|
@ -910,7 +638,8 @@ class LibrarySync(Thread):
|
||||||
'viewName': viewName,
|
'viewName': viewName,
|
||||||
'viewId': viewId,
|
'viewId': viewId,
|
||||||
'title': item.attrib.get('title', 'Missing Title'),
|
'title': item.attrib.get('title', 'Missing Title'),
|
||||||
'mediaType': item.attrib.get('type')
|
'mediaType': item.attrib.get('type'),
|
||||||
|
'get_children': get_children
|
||||||
})
|
})
|
||||||
self.just_processed[itemId] = now
|
self.just_processed[itemId] = now
|
||||||
return
|
return
|
||||||
|
@ -936,7 +665,8 @@ class LibrarySync(Thread):
|
||||||
'viewName': viewName,
|
'viewName': viewName,
|
||||||
'viewId': viewId,
|
'viewId': viewId,
|
||||||
'title': item.attrib.get('title', 'Missing Title'),
|
'title': item.attrib.get('title', 'Missing Title'),
|
||||||
'mediaType': item.attrib.get('type')
|
'mediaType': item.attrib.get('type'),
|
||||||
|
'get_children': get_children
|
||||||
})
|
})
|
||||||
self.just_processed[itemId] = now
|
self.just_processed[itemId] = now
|
||||||
else:
|
else:
|
||||||
|
@ -955,7 +685,8 @@ class LibrarySync(Thread):
|
||||||
'viewName': viewName,
|
'viewName': viewName,
|
||||||
'viewId': viewId,
|
'viewId': viewId,
|
||||||
'title': item.attrib.get('title', 'Missing Title'),
|
'title': item.attrib.get('title', 'Missing Title'),
|
||||||
'mediaType': item.attrib.get('type')
|
'mediaType': item.attrib.get('type'),
|
||||||
|
'get_children': get_children
|
||||||
})
|
})
|
||||||
self.just_processed[itemId] = now
|
self.just_processed[itemId] = now
|
||||||
|
|
||||||
|
@ -980,49 +711,38 @@ class LibrarySync(Thread):
|
||||||
log.info("Starting sync threads")
|
log.info("Starting sync threads")
|
||||||
getMetadataQueue = Queue.Queue()
|
getMetadataQueue = Queue.Queue()
|
||||||
processMetadataQueue = Queue.Queue(maxsize=100)
|
processMetadataQueue = Queue.Queue(maxsize=100)
|
||||||
getMetadataLock = Lock()
|
|
||||||
processMetadataLock = Lock()
|
|
||||||
# To keep track
|
# To keep track
|
||||||
global getMetadataCount
|
sync_info.GET_METADATA_COUNT = 0
|
||||||
getMetadataCount = 0
|
sync_info.PROCESS_METADATA_COUNT = 0
|
||||||
global processMetadataCount
|
sync_info.PROCESSING_VIEW_NAME = ''
|
||||||
processMetadataCount = 0
|
|
||||||
global processingViewName
|
|
||||||
processingViewName = ''
|
|
||||||
# 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 threads for downloading
|
||||||
threads = []
|
threads = []
|
||||||
for i in range(min(self.syncThreadNumber, itemNumber)):
|
for i in range(min(self.syncThreadNumber, itemNumber)):
|
||||||
thread = ThreadedGetMetadata(getMetadataQueue,
|
thread = Threaded_Get_Metadata(getMetadataQueue,
|
||||||
processMetadataQueue,
|
processMetadataQueue)
|
||||||
getMetadataLock,
|
|
||||||
processMetadataLock)
|
|
||||||
thread.setDaemon(True)
|
thread.setDaemon(True)
|
||||||
thread.start()
|
thread.start()
|
||||||
threads.append(thread)
|
threads.append(thread)
|
||||||
log.info("%s download threads spawned" % len(threads))
|
log.info("%s download threads spawned" % len(threads))
|
||||||
# Spawn one more thread to process Metadata, once downloaded
|
# Spawn one more thread to process Metadata, once downloaded
|
||||||
thread = ThreadedProcessMetadata(processMetadataQueue,
|
thread = Threaded_Process_Metadata(processMetadataQueue,
|
||||||
itemType,
|
itemType)
|
||||||
processMetadataLock)
|
|
||||||
thread.setDaemon(True)
|
thread.setDaemon(True)
|
||||||
thread.start()
|
thread.start()
|
||||||
threads.append(thread)
|
threads.append(thread)
|
||||||
log.info("Processing thread spawned")
|
|
||||||
# Start one thread to show sync progress ONLY for new PMS items
|
# Start one thread to show sync progress ONLY for new PMS items
|
||||||
if self.new_items_only is True and window('dbSyncIndicator') == 'true':
|
if self.new_items_only is True and window('dbSyncIndicator') == 'true':
|
||||||
dialog = xbmcgui.DialogProgressBG()
|
dialog = xbmcgui.DialogProgressBG()
|
||||||
thread = ThreadedShowSyncInfo(
|
thread = sync_info.Threaded_Show_Sync_Info(
|
||||||
dialog,
|
dialog,
|
||||||
[getMetadataLock, processMetadataLock],
|
|
||||||
itemNumber,
|
itemNumber,
|
||||||
itemType)
|
itemType)
|
||||||
thread.setDaemon(True)
|
thread.setDaemon(True)
|
||||||
thread.start()
|
thread.start()
|
||||||
threads.append(thread)
|
threads.append(thread)
|
||||||
log.info("Kodi Infobox thread spawned")
|
|
||||||
|
|
||||||
# Wait until finished
|
# Wait until finished
|
||||||
getMetadataQueue.join()
|
getMetadataQueue.join()
|
||||||
|
@ -1083,8 +803,7 @@ class LibrarySync(Thread):
|
||||||
# Get items per view
|
# Get items per view
|
||||||
viewId = view['id']
|
viewId = view['id']
|
||||||
viewName = view['name']
|
viewName = view['name']
|
||||||
all_plexmovies = GetPlexSectionResults(
|
all_plexmovies = GetPlexSectionResults(viewId, args=None)
|
||||||
viewId, args=None, containerSize=self.limitindex)
|
|
||||||
if all_plexmovies is None:
|
if all_plexmovies is None:
|
||||||
log.info("Couldnt get section items, aborting for view.")
|
log.info("Couldnt get section items, aborting for view.")
|
||||||
continue
|
continue
|
||||||
|
@ -1127,8 +846,7 @@ class LibrarySync(Thread):
|
||||||
return
|
return
|
||||||
xml = GetAllPlexLeaves(viewId,
|
xml = GetAllPlexLeaves(viewId,
|
||||||
lastViewedAt=lastViewedAt,
|
lastViewedAt=lastViewedAt,
|
||||||
updatedAt=updatedAt,
|
updatedAt=updatedAt)
|
||||||
containerSize=self.limitindex)
|
|
||||||
# Return if there are no items in PMS reply - it's faster
|
# Return if there are no items in PMS reply - it's faster
|
||||||
try:
|
try:
|
||||||
xml[0].attrib
|
xml[0].attrib
|
||||||
|
@ -1178,8 +896,7 @@ class LibrarySync(Thread):
|
||||||
# Get items per view
|
# Get items per view
|
||||||
viewId = view['id']
|
viewId = view['id']
|
||||||
viewName = view['name']
|
viewName = view['name']
|
||||||
allPlexTvShows = GetPlexSectionResults(
|
allPlexTvShows = GetPlexSectionResults(viewId)
|
||||||
viewId, containerSize=self.limitindex)
|
|
||||||
if allPlexTvShows is None:
|
if allPlexTvShows is None:
|
||||||
log.error("Error downloading show xml for view %s" % viewId)
|
log.error("Error downloading show xml for view %s" % viewId)
|
||||||
continue
|
continue
|
||||||
|
@ -1206,8 +923,7 @@ class LibrarySync(Thread):
|
||||||
if self.threadStopped():
|
if self.threadStopped():
|
||||||
return False
|
return False
|
||||||
# Grab all seasons to tvshow from PMS
|
# Grab all seasons to tvshow from PMS
|
||||||
seasons = GetAllPlexChildren(
|
seasons = GetAllPlexChildren(tvShowId)
|
||||||
tvShowId, containerSize=self.limitindex)
|
|
||||||
if seasons is None:
|
if seasons is None:
|
||||||
log.error("Error download season xml for show %s" % tvShowId)
|
log.error("Error download season xml for show %s" % tvShowId)
|
||||||
continue
|
continue
|
||||||
|
@ -1232,8 +948,7 @@ class LibrarySync(Thread):
|
||||||
if self.threadStopped():
|
if self.threadStopped():
|
||||||
return False
|
return False
|
||||||
# Grab all episodes to tvshow from PMS
|
# Grab all episodes to tvshow from PMS
|
||||||
episodes = GetAllPlexLeaves(
|
episodes = GetAllPlexLeaves(view['id'])
|
||||||
view['id'], containerSize=self.limitindex)
|
|
||||||
if episodes is None:
|
if episodes is None:
|
||||||
log.error("Error downloading episod xml for view %s"
|
log.error("Error downloading episod xml for view %s"
|
||||||
% view.get('name'))
|
% view.get('name'))
|
||||||
|
@ -1297,12 +1012,17 @@ class LibrarySync(Thread):
|
||||||
}
|
}
|
||||||
|
|
||||||
# Process artist, then album and tracks last to minimize overhead
|
# Process artist, then album and tracks last to minimize overhead
|
||||||
|
# Each album needs to be processed directly with its songs
|
||||||
|
# Remaining songs without album will be processed last
|
||||||
for kind in (v.PLEX_TYPE_ARTIST,
|
for kind in (v.PLEX_TYPE_ARTIST,
|
||||||
v.PLEX_TYPE_ALBUM,
|
v.PLEX_TYPE_ALBUM,
|
||||||
v.PLEX_TYPE_SONG):
|
v.PLEX_TYPE_SONG):
|
||||||
if self.threadStopped():
|
if self.threadStopped():
|
||||||
return False
|
return False
|
||||||
log.debug("Start processing music %s" % kind)
|
log.debug("Start processing music %s" % kind)
|
||||||
|
self.allKodiElementsId = {}
|
||||||
|
self.allPlexElementsId = {}
|
||||||
|
self.updatelist = []
|
||||||
if self.ProcessMusic(views,
|
if self.ProcessMusic(views,
|
||||||
kind,
|
kind,
|
||||||
urlArgs[kind],
|
urlArgs[kind],
|
||||||
|
@ -1326,10 +1046,8 @@ class LibrarySync(Thread):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def ProcessMusic(self, views, kind, urlArgs, method):
|
def ProcessMusic(self, views, kind, urlArgs, method):
|
||||||
self.allKodiElementsId = {}
|
# For albums, we need to look at the album's songs simultaneously
|
||||||
self.allPlexElementsId = {}
|
get_children = True if kind == v.PLEX_TYPE_ALBUM else False
|
||||||
self.updatelist = []
|
|
||||||
|
|
||||||
# Get a list of items already existing in Kodi db
|
# Get a list of items already existing in Kodi db
|
||||||
if self.compare:
|
if self.compare:
|
||||||
with plexdb.Get_Plex_DB() as plex_db:
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
|
@ -1340,17 +1058,13 @@ class LibrarySync(Thread):
|
||||||
# Yet empty/nothing yet synched
|
# Yet empty/nothing yet synched
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
for view in views:
|
for view in views:
|
||||||
if self.threadStopped():
|
if self.threadStopped():
|
||||||
return False
|
return False
|
||||||
# Get items per view
|
# Get items per view
|
||||||
viewId = view['id']
|
itemsXML = GetPlexSectionResults(view['id'], args=urlArgs)
|
||||||
viewName = view['name']
|
|
||||||
itemsXML = GetPlexSectionResults(
|
|
||||||
viewId, args=urlArgs, containerSize=self.limitindex)
|
|
||||||
if itemsXML is None:
|
if itemsXML is None:
|
||||||
log.error("Error downloading xml for view %s" % viewId)
|
log.error("Error downloading xml for view %s" % view['id'])
|
||||||
continue
|
continue
|
||||||
elif itemsXML == 401:
|
elif itemsXML == 401:
|
||||||
return False
|
return False
|
||||||
|
@ -1358,9 +1072,9 @@ class LibrarySync(Thread):
|
||||||
self.GetUpdatelist(itemsXML,
|
self.GetUpdatelist(itemsXML,
|
||||||
'Music',
|
'Music',
|
||||||
method,
|
method,
|
||||||
viewName,
|
view['name'],
|
||||||
viewId)
|
view['id'],
|
||||||
|
get_children=get_children)
|
||||||
if self.compare:
|
if self.compare:
|
||||||
# Manual sync, process deletes
|
# Manual sync, process deletes
|
||||||
with itemtypes.Music() as Music:
|
with itemtypes.Music() as Music:
|
||||||
|
|
|
@ -4,6 +4,8 @@ import logging
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from urlparse import parse_qsl
|
from urlparse import parse_qsl
|
||||||
|
|
||||||
|
from xbmc import Player
|
||||||
|
|
||||||
from PKC_listitem import PKC_ListItem
|
from PKC_listitem import PKC_ListItem
|
||||||
from pickler import pickle_me, Playback_Successful
|
from pickler import pickle_me, Playback_Successful
|
||||||
from playbackutils import PlaybackUtils
|
from playbackutils import PlaybackUtils
|
||||||
|
@ -12,6 +14,9 @@ from PlexFunctions import GetPlexMetadata
|
||||||
from PlexAPI import API
|
from PlexAPI import API
|
||||||
from playqueue import lock
|
from playqueue import lock
|
||||||
import variables as v
|
import variables as v
|
||||||
|
from downloadutils import DownloadUtils
|
||||||
|
from PKC_listitem import convert_PKC_to_listitem
|
||||||
|
import plexdb_functions as plexdb
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
log = logging.getLogger("PLEX."+__name__)
|
||||||
|
@ -41,7 +46,7 @@ class Playback_Starter(Thread):
|
||||||
xml = GetPlexMetadata(plex_id)
|
xml = GetPlexMetadata(plex_id)
|
||||||
try:
|
try:
|
||||||
xml[0].attrib
|
xml[0].attrib
|
||||||
except (TypeError, AttributeError):
|
except (IndexError, TypeError, AttributeError):
|
||||||
log.error('Could not get a PMS xml for plex id %s' % plex_id)
|
log.error('Could not get a PMS xml for plex id %s' % plex_id)
|
||||||
return
|
return
|
||||||
api = API(xml[0])
|
api = API(xml[0])
|
||||||
|
@ -50,8 +55,6 @@ class Playback_Starter(Thread):
|
||||||
result = Playback_Successful()
|
result = Playback_Successful()
|
||||||
listitem = PKC_ListItem()
|
listitem = PKC_ListItem()
|
||||||
listitem = api.CreateListItemFromPlexItem(listitem)
|
listitem = api.CreateListItemFromPlexItem(listitem)
|
||||||
api.AddStreamInfo(listitem)
|
|
||||||
api.set_listitem_artwork(listitem)
|
|
||||||
result.listitem = listitem
|
result.listitem = listitem
|
||||||
else:
|
else:
|
||||||
# Video and Music
|
# Video and Music
|
||||||
|
@ -66,9 +69,65 @@ class Playback_Starter(Thread):
|
||||||
% self.playqueue.playqueues)
|
% self.playqueue.playqueues)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def process_plex_node(self, url, viewOffset, directplay=False,
|
||||||
|
node=True):
|
||||||
|
"""
|
||||||
|
Called for Plex directories or redirect for playback (e.g. trailers,
|
||||||
|
clips, watchlater)
|
||||||
|
"""
|
||||||
|
log.info('process_plex_node called with url: %s, viewOffset: %s'
|
||||||
|
% (url, viewOffset))
|
||||||
|
# Plex redirect, e.g. watch later. Need to get actual URLs
|
||||||
|
if url.startswith('http') or url.startswith('{server}'):
|
||||||
|
xml = DownloadUtils().downloadUrl(url)
|
||||||
|
else:
|
||||||
|
xml = DownloadUtils().downloadUrl('{server}%s' % url)
|
||||||
|
try:
|
||||||
|
xml[0].attrib
|
||||||
|
except:
|
||||||
|
log.error('Could not download PMS metadata')
|
||||||
|
return
|
||||||
|
if viewOffset != '0':
|
||||||
|
try:
|
||||||
|
viewOffset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(viewOffset))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
window('plex_customplaylist.seektime', value=str(viewOffset))
|
||||||
|
log.info('Set resume point to %s' % str(viewOffset))
|
||||||
|
api = API(xml[0])
|
||||||
|
typus = v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]
|
||||||
|
if node is True:
|
||||||
|
plex_id = None
|
||||||
|
kodi_id = 'plexnode'
|
||||||
|
else:
|
||||||
|
plex_id = api.getRatingKey()
|
||||||
|
kodi_id = None
|
||||||
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
|
plexdb_item = plex_db.getItem_byId(plex_id)
|
||||||
|
try:
|
||||||
|
kodi_id = plexdb_item[0]
|
||||||
|
except TypeError:
|
||||||
|
log.info('Couldnt find item %s in Kodi db'
|
||||||
|
% api.getRatingKey())
|
||||||
|
playqueue = self.playqueue.get_playqueue_from_type(typus)
|
||||||
|
with lock:
|
||||||
|
result = PlaybackUtils(xml, playqueue).play(
|
||||||
|
plex_id,
|
||||||
|
kodi_id=kodi_id,
|
||||||
|
plex_lib_UUID=xml.attrib.get('librarySectionUUID'))
|
||||||
|
if directplay:
|
||||||
|
if result.listitem:
|
||||||
|
listitem = convert_PKC_to_listitem(result.listitem)
|
||||||
|
Player().play(listitem.getfilename(), listitem)
|
||||||
|
return Playback_Successful()
|
||||||
|
else:
|
||||||
|
return result
|
||||||
|
|
||||||
def triage(self, item):
|
def triage(self, item):
|
||||||
mode, params = item.split('?', 1)
|
_, params = item.split('?', 1)
|
||||||
params = dict(parse_qsl(params))
|
params = dict(parse_qsl(params))
|
||||||
|
mode = params.get('mode')
|
||||||
log.debug('Received mode: %s, params: %s' % (mode, params))
|
log.debug('Received mode: %s, params: %s' % (mode, params))
|
||||||
try:
|
try:
|
||||||
if mode == 'play':
|
if mode == 'play':
|
||||||
|
@ -76,6 +135,12 @@ class Playback_Starter(Thread):
|
||||||
params.get('dbid'))
|
params.get('dbid'))
|
||||||
elif mode == 'companion':
|
elif mode == 'companion':
|
||||||
result = self.process_companion()
|
result = self.process_companion()
|
||||||
|
elif mode == 'plex_node':
|
||||||
|
result = self.process_plex_node(
|
||||||
|
params.get('key'),
|
||||||
|
params.get('view_offset'),
|
||||||
|
directplay=True if params.get('play_directly') else False,
|
||||||
|
node=False if params.get('node') == 'false' else True)
|
||||||
except:
|
except:
|
||||||
log.error('Error encountered for mode %s, params %s'
|
log.error('Error encountered for mode %s, params %s'
|
||||||
% (mode, params))
|
% (mode, params))
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import logging
|
import logging
|
||||||
from urllib import quote
|
from urllib import quote
|
||||||
|
from urlparse import parse_qsl, urlsplit
|
||||||
|
|
||||||
import plexdb_functions as plexdb
|
import plexdb_functions as plexdb
|
||||||
from downloadutils import DownloadUtils as DU
|
from downloadutils import DownloadUtils as DU
|
||||||
from utils import JSONRPC, tryEncode, tryDecode
|
from utils import JSONRPC, tryEncode
|
||||||
from PlexAPI import API
|
from PlexAPI import API
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
@ -111,6 +112,9 @@ def playlist_item_from_kodi(kodi_item):
|
||||||
except TypeError:
|
except TypeError:
|
||||||
pass
|
pass
|
||||||
item.file = kodi_item.get('file')
|
item.file = kodi_item.get('file')
|
||||||
|
if item.file is not None and item.plex_id is None:
|
||||||
|
item.plex_id = dict(
|
||||||
|
parse_qsl(urlsplit(item.file).query)).get('plex_id')
|
||||||
item.kodi_type = kodi_item.get('type')
|
item.kodi_type = kodi_item.get('type')
|
||||||
if item.plex_id is None:
|
if item.plex_id is None:
|
||||||
item.uri = 'library://whatever/item/%s' % quote(item.file, safe='')
|
item.uri = 'library://whatever/item/%s' % quote(item.file, safe='')
|
||||||
|
@ -164,17 +168,6 @@ def playlist_item_from_xml(playlist, xml_video_element):
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
def _log_xml(xml):
|
|
||||||
try:
|
|
||||||
xml.attrib
|
|
||||||
except AttributeError:
|
|
||||||
log.error('Did not receive an XML. Answer was: %s' % xml)
|
|
||||||
else:
|
|
||||||
from xml.etree.ElementTree import dump
|
|
||||||
log.error('XML received from the PMS:')
|
|
||||||
dump(xml)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_playListVersion_from_xml(playlist, xml):
|
def _get_playListVersion_from_xml(playlist, xml):
|
||||||
"""
|
"""
|
||||||
Takes a PMS xml as input to overwrite the playlist version (e.g. Plex
|
Takes a PMS xml as input to overwrite the playlist version (e.g. Plex
|
||||||
|
@ -185,7 +178,6 @@ def _get_playListVersion_from_xml(playlist, xml):
|
||||||
except (TypeError, AttributeError, KeyError):
|
except (TypeError, AttributeError, KeyError):
|
||||||
log.error('Could not get new playlist Version for playlist %s'
|
log.error('Could not get new playlist Version for playlist %s'
|
||||||
% playlist)
|
% playlist)
|
||||||
_log_xml(xml)
|
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -208,7 +200,6 @@ def get_playlist_details_from_xml(playlist, xml):
|
||||||
% playlist)
|
% playlist)
|
||||||
import traceback
|
import traceback
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
_log_xml(xml)
|
|
||||||
raise KeyError
|
raise KeyError
|
||||||
log.debug('Updated playlist from xml: %s' % playlist)
|
log.debug('Updated playlist from xml: %s' % playlist)
|
||||||
|
|
||||||
|
@ -341,7 +332,6 @@ def add_item_to_PMS_playlist(playlist, pos, plex_id=None, kodi_item=None):
|
||||||
except (TypeError, AttributeError, KeyError):
|
except (TypeError, AttributeError, KeyError):
|
||||||
log.error('Could not add item %s to playlist %s'
|
log.error('Could not add item %s to playlist %s'
|
||||||
% (kodi_item, playlist))
|
% (kodi_item, playlist))
|
||||||
_log_xml(xml)
|
|
||||||
return
|
return
|
||||||
# Get the guid for this item
|
# Get the guid for this item
|
||||||
for plex_item in xml:
|
for plex_item in xml:
|
||||||
|
|
|
@ -329,7 +329,6 @@ SORT_METHODS_SONGS = (
|
||||||
'SORT_METHOD_TRACKNUM',
|
'SORT_METHOD_TRACKNUM',
|
||||||
'SORT_METHOD_DURATION',
|
'SORT_METHOD_DURATION',
|
||||||
'SORT_METHOD_ARTIST',
|
'SORT_METHOD_ARTIST',
|
||||||
'SORT_METHOD_ARTIST_AND_YEAR',
|
|
||||||
'SORT_METHOD_ALBUM',
|
'SORT_METHOD_ALBUM',
|
||||||
'SORT_METHOD_SONG_RATING',
|
'SORT_METHOD_SONG_RATING',
|
||||||
'SORT_METHOD_SONG_USER_RATING'
|
'SORT_METHOD_SONG_USER_RATING'
|
||||||
|
@ -341,7 +340,6 @@ SORT_METHODS_ARTISTS = (
|
||||||
'SORT_METHOD_TRACKNUM',
|
'SORT_METHOD_TRACKNUM',
|
||||||
'SORT_METHOD_DURATION',
|
'SORT_METHOD_DURATION',
|
||||||
'SORT_METHOD_ARTIST',
|
'SORT_METHOD_ARTIST',
|
||||||
'SORT_METHOD_ARTIST_AND_YEAR',
|
|
||||||
'SORT_METHOD_ALBUM',
|
'SORT_METHOD_ALBUM',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -351,6 +349,5 @@ SORT_METHODS_ALBUMS = (
|
||||||
'SORT_METHOD_TRACKNUM',
|
'SORT_METHOD_TRACKNUM',
|
||||||
'SORT_METHOD_DURATION',
|
'SORT_METHOD_DURATION',
|
||||||
'SORT_METHOD_ARTIST',
|
'SORT_METHOD_ARTIST',
|
||||||
'SORT_METHOD_ARTIST_AND_YEAR',
|
|
||||||
'SORT_METHOD_ALBUM',
|
'SORT_METHOD_ALBUM',
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue