Compare commits

..

74 Commits

Author SHA1 Message Date
croneter 6cba4a1d01 Fix playback startup for Plex Companion 2019-05-26 17:56:16 +02:00
croneter 48d288ac53 Remove some logging 2019-05-26 17:43:22 +02:00
croneter 27c4c6ac38 Replace window var plex.playlist.start with app.PLAYSTATE var 2019-05-26 17:41:17 +02:00
croneter e204ef9849 Replace window var plex.playlist.ready with app.PLAYSTATE var 2019-05-26 17:35:37 +02:00
croneter 7e676eb043 Fix playback startup 2019-05-26 17:28:05 +02:00
croneter a650c42cfd Lock playqueue activities 2019-05-26 13:13:13 +02:00
croneter 0f9e754815 Cleanup 2019-05-26 12:52:32 +02:00
croneter bb2fff5909 Fix resume when user chose to not resume 2019-05-26 12:51:04 +02:00
croneter 725416751f Revert "Fix resume when user chose to not resume"
This reverts commit 6e692d22c2.
2019-05-26 12:30:49 +02:00
croneter 6e692d22c2 Fix resume when user chose to not resume 2019-05-26 12:06:27 +02:00
croneter fb21bc7d71 Cleanup 2019-05-26 11:53:59 +02:00
croneter d3752e1958 Rename to PlayqueueError 2019-05-26 11:26:14 +02:00
croneter 3d4bde878e Cleanup 2019-05-25 20:52:41 +02:00
croneter 9d517c2c3d Refactoring 2019-05-25 20:49:29 +02:00
croneter d397fb5b20 Cleanup 2019-05-25 13:35:14 +02:00
croneter f7237d7033 Cleanup 2019-05-25 13:12:29 +02:00
croneter 9d79f78190 Fix TypeError 2019-05-21 18:08:09 +02:00
croneter 7725af5a6f Fix resume from Plex Companion 2019-05-21 17:56:48 +02:00
croneter 0ce29dc0ce Fix Plex Companion telling the wrong item is playing 2019-05-19 18:47:09 +02:00
croneter ea4a062aac Big update 2019-05-12 14:38:31 +02:00
croneter a1f4960bca Revert "Set kodi_type for PlaylistItem automatically from plex_type"
This reverts commit cef07c3598.
2019-05-05 12:11:43 +02:00
croneter 1d01f4794e Fixup 2019-05-05 11:31:00 +02:00
croneter cef07c3598 Set kodi_type for PlaylistItem automatically from plex_type 2019-05-05 11:29:43 +02:00
croneter 7616d6dc26 Fixup 2019-05-05 11:14:27 +02:00
croneter b586ac09c4 Fix moving of items in Plex playqueue 2019-05-05 11:00:41 +02:00
croneter dc56c2a6a2 Improve code 2019-05-05 10:42:44 +02:00
croneter 1123a2ee3c Fix widget playback not starting up 2019-05-05 10:29:04 +02:00
croneter 8ad6d1bcce Clear playqueue on playback startup 2019-05-05 10:21:59 +02:00
croneter 45fc9fa8be Better way to detect video widget playback 2019-05-04 16:13:26 +02:00
croneter 353cb04532 New functions 2019-05-04 14:29:18 +02:00
croneter 48cda467c3 Move method 2019-05-04 14:26:18 +02:00
croneter 6bd98fcefd Cleanup 2019-05-04 13:14:34 +02:00
croneter 1218cde0a2 Big update 2019-04-28 18:03:20 +02:00
croneter 578ced789f Fix arg 2019-04-27 13:23:01 +02:00
croneter 0d8b3b3ba7 Remove arg 2019-04-27 13:23:01 +02:00
croneter 8660b12d15 Fix position 2019-04-27 13:23:01 +02:00
croneter bbd8e18002 Increase timeout 2019-04-27 13:23:01 +02:00
croneter 7753903c05 Be more resiliant when manipulating Plex playqueues 2019-04-27 13:23:01 +02:00
croneter 4fa1f48b43 Improve logging 2019-04-27 13:23:01 +02:00
croneter 130ec674e5 Fix companion playback crashing 2019-04-27 13:23:01 +02:00
croneter 2dac26ffc4 Fix force transcoding 2019-04-27 13:23:01 +02:00
croneter 3aa5c87ca0 Skip force close connection error messages 2019-04-27 13:23:01 +02:00
croneter c63d9ad4d6 Some more switches to webservice, away from plugin playback 2019-04-27 13:23:00 +02:00
croneter 95b37b51f5 Fix resume 2019-04-27 13:23:00 +02:00
croneter d380aa8ac3 Drop filename for url arg, but add kodi_type 2019-04-27 13:23:00 +02:00
croneter 885e8dd581 Fix PKC wanting to initiate playback when it should not 2019-04-27 13:23:00 +02:00
croneter ac285467c4 Fix main movie being added as trailer 2019-04-27 13:23:00 +02:00
croneter 61ff2b72f3 Increase logging 2019-04-27 13:23:00 +02:00
croneter 0acf470343 Optimize logging 2019-04-27 13:23:00 +02:00
croneter 9a9bc9f0eb Optimize code 2019-04-27 13:23:00 +02:00
croneter 643e6171c4 Revamp monitor 2019-04-27 13:23:00 +02:00
croneter ad6c160524 Don't sleep 2019-04-27 13:23:00 +02:00
croneter b11ca48294 Enable playqueue elements comparison 2019-04-27 13:23:00 +02:00
croneter fe52efd88e Less playlist logging 2019-04-27 13:23:00 +02:00
croneter 8c614f3e47 Don't automatically look up kodi_id from Plex DB 2019-04-27 13:22:59 +02:00
croneter 0d36a2a3b9 Simplify code 2019-04-27 13:22:59 +02:00
croneter f4c3674bc2 Increase logging 2019-04-27 13:22:59 +02:00
croneter 5428dafe59 Simplify code 2019-04-27 13:22:59 +02:00
croneter 4ed17f1a5b Fix widgets not updating 2019-04-27 13:22:59 +02:00
croneter 484b03482e Fix resume flags for ListItems 2019-04-27 13:22:59 +02:00
croneter 797a58a3d5 Rewire plex.playlist.audio 2019-04-27 13:22:59 +02:00
croneter 439857a9ce Rewire autoplay flag 2019-04-27 13:22:59 +02:00
croneter 12befecc4a Rewire video resume info 2019-04-27 13:22:59 +02:00
croneter 20bffc1b41 Enable webservice playback for shows 2019-04-27 13:22:59 +02:00
croneter d7541b7f74 TO BE CHECKED: better method to delete obsolete fileIds 2019-04-27 13:22:59 +02:00
croneter dfcfa0edab Better detect videos playing for playback cleanup 2019-04-27 13:22:59 +02:00
croneter 20c1c6e502 Add missing default.py option 2019-04-27 13:22:58 +02:00
croneter 875d704e5a Don't store filename in Kodi db 2019-04-27 13:22:58 +02:00
croneter c0035c84a6 Fix monitor's playlist.onadd 2019-04-27 13:22:58 +02:00
croneter 4a3b38f5b6 Increase logging 2019-04-27 13:22:58 +02:00
croneter 16423e18ec Ignore suspends for webservice 2019-04-27 13:22:58 +02:00
croneter 059ed7a5f0 Switch to stream playback, part II 2019-04-27 13:22:58 +02:00
croneter 7c6fdad770 Fixup 2019-04-27 13:22:58 +02:00
croneter 9b4584e7df Switch to stream playback, part I 2019-04-27 13:22:58 +02:00
157 changed files with 9993 additions and 22973 deletions

1
.github/FUNDING.yml vendored
View File

@ -1 +0,0 @@
ko_fi: A8182EB

View File

@ -1,14 +1,12 @@
[![Kodi Leia stable version](https://img.shields.io/badge/Kodi_Leia_STABLE-latest-blue.svg?maxAge=60&style=flat) ](https://croneter.github.io/pkc-source/repository.plexkodiconnect.Kodi-Leia.STABLE.zip)
[![Kodi Leia beta version](https://img.shields.io/badge/Kodi_Leia_BETA-latest-red.svg?maxAge=60&style=flat) ](https://croneter.github.io/pkc-source/repository.plexkodiconnect.Kodi-Leia.BETA.zip)
[![Kodi Matrix stable version](https://img.shields.io/badge/Kodi_Matrix_STABLE-latest-blue.svg?maxAge=60&style=flat) ](https://croneter.github.io/pkc-source/repository.plexkodiconnect.Kodi-Matrix.STABLE.zip)
[![Kodi Matrix beta version](https://img.shields.io/badge/Kodi_Matrix_BETA-latest-red.svg?maxAge=60&style=flat) ](https://croneter.github.io/pkc-source/repository.plexkodiconnect.Kodi-Matrix.BETA.zip)
[![stable version](https://img.shields.io/badge/stable_version-2.7.14-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip)
[![beta version](https://img.shields.io/badge/beta_version-2.7.14-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip)
[![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation)
[![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq)
[![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)
[![Donate](https://img.shields.io/badge/donate-kofi-blue.svg)](https://ko-fi.com/A8182EB)
[![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)
[![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) [![HitCount](http://hits.dwyl.io/croneter/PlexKodiConnect.svg)](http://hits.dwyl.io/croneter/PlexKodiConnect) [![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)
@ -39,7 +37,11 @@ Unfortunately, the PKC Kodi repository had to move because it stopped working (t
### Download and Installation
Using the Kodi file manager, add [https://croneter.github.io/pkc-source](https://croneter.github.io/pkc-source) as a new Kodi `Web server directory (HTTPS)` source, then install the PlexKodiConnect repository from this new source "from ZIP file". See the [github wiki installation manual](https://github.com/croneter/PlexKodiConnect/wiki/Installation) for a detailed guide. Kodi will update PKC automatically.
Install PKC via the PlexKodiConnect Kodi repository download button just below (do NOT use the standard GitHub download!). 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.
| Stable version | Beta version |
|----------------|--------------|
| [![stable version](https://img.shields.io/badge/stable_version-latest-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) | [![beta version](https://img.shields.io/badge/beta_version-latest-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) |
### 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 ;-)).
@ -48,9 +50,8 @@ Some people argue that PKC is 'hacky' because of the way it directly accesses th
### PKC Features
- Support for Kodi 18 Leia and Kodi 19 Matrix
- Preliminary support for Kodi 20 Nexus. Keep in mind that development for Kodi Nexus has not even officially reached alpha stage - any issues you encounter are probably caused by that
- [Skip intros](https://support.plex.tv/articles/skip-content/)
- Support for Kodi 18 Leia
- Support for Kodi 17 Krypton
- [Amazon Alexa voice recognition](https://www.plex.tv/apps/streaming-devices/amazon-alexa)
- [Cinema Trailers & Extras](https://support.plex.tv/articles/202934883-cinema-trailers-extras/)
- [Plex Watch Later / Plex It!](https://support.plex.tv/hc/en-us/sections/200211783-Plex-It-)
@ -77,8 +78,6 @@ Some people argue that PKC is 'hacky' because of the way it directly accesses th
+ Russian, thanks @UncleStark
+ Hungarian, thanks @savage93
+ Ukrainian, thanks @uniss
+ Lithuanian, thanks @egidusm
+ Korean, thanks @so-o-bima
### Additional Artwork
PKC uses additional artwork for free from [TheMovieDB](https://www.themoviedb.org). Many thanks for lettings us use the API, guys!

697
addon.xml
View File

@ -1,12 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.15.0" provider-name="croneter">
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.7.14" provider-name="croneter">
<requires>
<import addon="xbmc.python" version="2.1.0"/>
<import addon="script.module.requests" version="2.9.1" />
<import addon="script.module.defusedxml" version="0.5.0"/>
<import addon="script.module.six" />
<import addon="plugin.video.plexkodiconnect.movies" version="2.1.3" />
<import addon="plugin.video.plexkodiconnect.tvshows" version="2.1.3" />
<import addon="plugin.video.plexkodiconnect.movies" version="2.0.9" />
<import addon="plugin.video.plexkodiconnect.tvshows" version="2.0.10" />
</requires>
<extension point="xbmc.python.pluginsource" library="default.py">
<provides>video audio image</provides>
@ -78,203 +77,549 @@
<summary lang="uk_UA">Нативна інтеграція Plex в Kodi</summary>
<description lang="uk_UA">Підключає Kodi до серверу Plex. Цей плагін передбачає, що ви керуєте всіма своїми відео за допомогою Plex (і ніяк не Kodi). Ви можете втратити дані, які вже зберігаються у відео та музичних БД Kodi (оскільки цей плагін безпосередньо їх змінює). Використовуйте на свій страх і ризик!</description>
<disclaimer lang="uk_UA">Використовуйте на свій ризик</disclaimer>
<disclaimer lang="lv_LV">Lieto uz savu atbildību</disclaimer>
<summary lang="sv_SE">Inbyggd integrering av Plex i Kodi</summary>
<description lang="sv_SE">Anslut Kodi till din Plex Media Server. Detta tillägg antar att du hanterar alla dina filmer med Plex (och ingen med Kodi). Du kan förlora data redan sparad i Kodis video och musik databaser (eftersom detta tillägg direkt ändrar dem). Använd på egen risk!</description>
<disclaimer lang="sv_SE">Använd på egen risk</disclaimer>
<summary lang="lt_LT">Natūralioji „Plex“ integracija į „Kodi“</summary>
<description lang="lt_LT">Prijunkite „Kodi“ prie „Plex Medija Serverio“. Šiame papildinyje daroma prielaida, kad valdote visus savo vaizdo įrašus naudodami „Plex“ (ir nė vieno su „Kodi“). Galite prarasti jau saugomus „Kodi“ vaizdo įrašų ir muzikos duomenų bazių duomenis (kadangi šis papildinys juos tiesiogiai pakeičia). Naudokite savo pačių rizika!</description>
<disclaimer lang="lt_LT">Naudokite savo pačių rizika</disclaimer>
<summary lang="ko_KR">Plex를 Kodi에 기본 통합</summary>
<description lang="ko_KR">Kodi를 Plex Media Server에 연결합니다. 이 플러그인은 Plex로 모든 비디오를 관리하고 Kodi로는 관리하지 않는다고 가정합니다. Kodi 비디오 및 음악 데이터베이스에 이미 저장된 데이터가 손실 될 수 있습니다 (이 플러그인이 직접 변경하므로). 자신의 책임하에 사용하십시오!</description>
<disclaimer lang="ko_KR">자신의 책임하에 사용</disclaimer>
<news>version 2.15.0:
- versions 2.14.3-2.14.4 for everyone
- Direct Paths: Fix TypeError: "element indices must be integers" on playback startup [backport]
- Refactor stream code and fix Kodi not activating subtitle when it should [backport]
- Add playback settings to let the user choose whether Plex or Kodi provides the default audio or subtitle stream on playback start [backport]
- Update translations from Transifex [backport]
<news>version 2.7.14:
- Correctly clear window variables e.g. on user switch
- Reload skin on resetting PKC video nodes
- Fix last-played node value to ensure a playcount greater than zero
- 2.7.11-2.7.13 for everyone
version 2.14.4 (beta only):
- Tell the PMS if a video's audio stream or potentially subtitle stream has changed. For subtitles, this functionality is broken due to a Kodi bug
- Transcoding: Fix Plex burning-in subtitles when it should not
- Fix logging if fanart.tv lookup fails: be less verbose
- Large refactoring of playlist and playqueue code
- Refactor usage of a media part's id
version 2.7.13 (beta only):
- Fix transcoding not working
- Fix 4k H265 not being transcoded
- Fix some appearance tweak settings
- Fix music and picture nodes pointing to video library
- Fix unequality when comparing sections
- Fix Plex Companion logging error messages
version 2.14.3 (beta only):
- Implement "Reset resume position" from the Kodi context menu
version 2.7.12 (beta only):
- Fix UnicodeEncodeError on playback startup for direct paths
- Attempt to fix rare Kodi crash on PKC exit
version 2.14.2:
- version 2.14.1 for everyone
version 2.7.11 (beta only):
- Fixes to unicode
- Cleanup code, remove some obsolet methods and functions
- Fix FutureWarning
version 2.14.1 (beta only):
- Use Plex settings for audio and subtitle stream selection. This is a best guess regarding subtitles as Plex and Kodi are not sharing much info
- Fix PlexKodiConnect setting the Plex subtitle to None
- Download landscape artwork from fanart.tv, thanks @geropan
- Revert "Fix PlexKodiConnect changing subtitles for all videos on the PMS"
version 2.14.0:
- Fix PlexKodiConnect changing or removing subtitles for every video on the PMS
- version 2.13.1-2.13.2 for everyone
version 2.13.2 (beta only):
- Fix a racing condition that could lead to the sync getting stuck
- Fix RecursionError: maximum recursion depth exceeded
- Websocket Fix AttributeError: 'NoneType' object has no attribute 'is_ssl'
version 2.13.1 (beta only):
- Fix a racing condition that could lead to the sync process getting stuck
- Fix likelyhood of `database is locked` error occuring
version 2.13.0:
- Support for the Plex HAMA agent to let Kodi identify animes (using Kodi's uniqueID 'anidb')
- Support forced HAMA IDs when using tvdb uniqueID
- version 2.12.26 for everyone
version 2.12.26 (beta only):
- Add an additional Plex Hub "PKC Continue Watching" that merges the Plex Continue Watching with On Deck
- Fix auto-picking of video stream if several video versions are available
version 2.7.10:
- Fix duplicate music entries for direct paths (you will need to manually reset the Kodi database)
- Fix UnicodeEncodeError for Direct Paths and some PKC video nodes
- Fix playback sometimes not being reported for direct paths
- Update translations
version 2.12.25:
- Update websocket client to 0.59.0. Fix threading issues and AttributeErrors
version 2.7.9:
- Wait for PKC to authorize before loading widgets
- Fix UnicodeDecodeError for libraries with non-ASCII paths
- Fix TypeError on Kodi start
- Fix Kodi Masterlock for nfs paths (requires restart)
version 2.12.24:
- version 2.12.23 for everyone
version 2.7.8:
- Fix widgets not working in some cases like NVidia Shield
- Fix appending of show title, season and episode number
- Fix node paths for skins
version 2.12.23 (beta only):
- Fix Alexa and RuntimeError: dictionary keys changed during iteration
- Fix a rare AttributeError when using playlists
version 2.7.7:
- Fix sync not working due to non-ASCII Plex library names
- Fix PKC synching playstate to wrong user on profile switch. Be aware that Kodi profile switches are error-prone
- Fix playback sometimes not being reported for direct paths
- Fix float() argument must be a string or a number
- Fix nodes for skin use
- Fix 'NoneType' object has no attribute 'kodi_path'
version 2.12.22:
- version 2.12.20 and 2.12.21 for everyone
version 2.7.6:
- Make 2.7.5 available for everyone
version 2.12.21 (beta only):
- Switch to new websocket implementation
- Hopefully fix RuntimeError: no add-on id "plugin.video.plexkodiconnect"
version 2.7.5:
- Giant overhaul of widgets
- Fix some KeyErrors when playing songs
- Fix rare cases where playlists were being created
version 2.7.4:
- Fix PKC not synching new items if an older Kodi db is present
version 2.7.3:
- Fix PKC trying to initialize playqueues over and over again
- Fix PKC not starting due to a higher version Kodi database
version 2.7.2:
- Fix Kodi profile switch not working correctly and PKC not exiting cleanly
version 2.7.1:
- Fix playback not starting at all
- Fix rare TypeError: unsupported operand type(s) for /: 'NoneType' and 'int' on playback startup
- Improve plex db lookups by creating better db indicees
- Fix background sync crashing in rare cases
- Update translations
- Add Ko-fi donate button
version 2.7.0:
- WARNING: You will need to reset the Kodi database if you're using the stable version of PKC!
- Version 2.6.6-9 for everyone
- Choose which Plex libraries get synched to Kodi
version 2.6.9 (beta only):
- Fix PKC crashing on resetting the database
version 2.6.8 (beta only):
- Choose which Plex libraries get synched to Kodi
- Fix PKC becoming unresponsive
- Fix rare case where thousands of identical playlists could be generated
- Fix movies or shows disappearing in fringe cases
- Fix processing of collections in special cases
- Implement Codacy suggestions
version 2.6.7 (beta only):
- Fix "Unauthorized for PMS" e.g. on switching Plex users
- Improve error messages when playback failes
version 2.6.6 (beta only):
- WARNING: You will need to reset the Kodi database!
- Greatly speed up sync for episodes, especially for large libraries
- Allow websocket redirects. Never allow insecure HTTPs connections for Kodi Leia
- Optimize headers for communication with PMS to appear like a Plex Media Player
- Fix PMS log entries 'Unable to find client profile for device'
- Improve sync dialog
version 2.6.5:
- Fix extras not playing
- Hide "Verify SSL certificate" setting for Kodi 18 Krypton
- Improve logging
- Update translations
version 2.12.20 (beta only):
- Add information to PKC settings for background sync and Alexa whether a connection has been successfully made
version 2.6.4:
- Fix music items getting deleted on startup
- Never ignore SSL certificate errors for Kodi >= 18 - just like Kodi
- Fix playback not starting at the beginning
- Improve dialog to manually enter PMS IP and port
- Show logged in Plex home user in the settings and allow changing it
- Update German strings
- Implement Codacy suggestions
version 2.12.19:
- 2.12.17 and 2.12.18 for everyone
- Rename skip intro skin file
version 2.6.3:
- Fix PKC crashing on Xbox
version 2.12.18 (beta only):
- Quickly sync recently watched items before synching the playstates of the entire Plex library
- Improve logging for websocket JSON loads
version 2.6.2:
- Fix playlist sync: sequence item 0: expected string or unicode
- Fix PKC not deleting all the items it should
- Fix keyError 'sessionKey' for weird PMS messages
- Fix artwork caching AttributeError: 'ImageCachingThread' object has no attribute 'cancel'
- Improve pop-up "Searching for PMS"
- Fix FutureWarning
version 2.12.17 (beta only):
- Sync name and user rating of a TV show season to Kodi
- Fix rare TypeError: expected string or buffer on playback start
version 2.12.16:
- versions 2.12.14 and 2.12.15 for everyone
version 2.12.15 (beta only):
- Fix skip intros sometimes not working due to a RuntimeError
version 2.6.1:
- WARNING: You will need to reset the Kodi database!
- Fix TV sections not being deleted e.g. after user switch
- Don't show a library sync error pop-up when full sync is interrupted
- Fix to correctly escape paths
- Update translations
version 2.12.14:
- Add skip intro functionality
version 2.12.13:
- Fix KeyError: u'game' if Plex Arcade has been activated
- Fix AttributeError: 'App' object has no attribute 'threads' when sync is cancelled
version 2.12.12:
- Hopefully fix rare case when sync would get stuck indefinitely
- Fix ValueError: invalid literal for int() for invalid dates sent by Plex
- version 2.12.11 for everyone
version 2.12.11 (beta only):
- Fix PKC not auto-picking audio/subtitle stream when transcoding
- Fix ValueError when deleting a music album
- Fix OSError: Invalid argument when Plex returns an invalid timestamp
version 2.12.10:
- Fix pictures from Plex picture libraries not working/displaying
version 2.12.9:
- Fix Local variable 'user' referenced before assignement
version 2.12.8:
- version 2.12.7 for everyone
version 2.12.7 (beta only):
- Fix PKC suddenly using main Plex user's credentials, e.g. when the PMS address changed
- Fix missing Kodi tags for movie collections/sets
version 2.12.6:
- Fix rare KeyError when using PKC widgets
- Fix suspension of artwork caching and PKC becoming unresponsive
- Update translations
- Versions 2.12.4 and 2.12.5 for everyone
version 2.12.5 (beta only):
- Greatly improve matching logic for The Movie Database if Plex does not provide an appropriate id
- Fix high transcoding resolutions not being available for Win10
- Fix rare playback progress report failing and KeyError: u'containerKey'
- Fix rare KeyError: None when trying to sync playlists
- Fix TypeError when canceling Plex sync section dialog
version 2.12.4 (beta only):
- Hopefully fix freeze during sync: Don't assign multiple sets/collections for a specific movie
- Support metadata provider ids (e.g. for IMDB) for the new Plex Movie Agent
version 2.12.3:
- Fix playback failing due to caching of subtitles with non-ascii chars
- Fix ValueError: invalid literal for int() with base 10 during show sync
- Fix UnboundLocalError when certain Plex sections are deleted or being un-synched
- New method to install PlexKodiConnect directly via an URL. You thus do not need to upload a ZIP file to Kodi anymore.
version 2.12.2:
- version 2.12.0 and 2.12.1 for everyone
- Fix regression: sync dialog not showing up when it should
version 2.12.1 (beta only):
- Fix PKC shutdown on Kodi profile switch
- Fix Kodi content type for images/photos
- Added support for custom set of safe characters when escaping paths (thanks @geropan)
- Revert "Don't allow spaces in devicename"
- Fix sync dialog showing in certain cases even though user opted out
version 2.12.0 (beta only):
- Fix websocket threads; enable PKC background sync for all Plex Home users!
- Fix PKC incorrectly marking a video as unwatched if an external player has been used
version 2.6.0:
- Support for Kodi 18 Leia
- Big overhaul of the synching process, it's now much faster
- PKC now supports really big Plex and Kodi libraries
- Too many other improvements to recount. See the changelog for the 2.5.x versions
Furthermore:
- Don't lock Plex DB when processing websocket messages
- Fix KeyError: u'kodi_fileid' for some Plex websocket messages
- Update translations
version 2.11.7:
- Fix PKC crashing on devices running Microsoft UWP, e.g. XBox
version 2.5.23 (beta only):
- Hopefully fix slow playback startup just after Kodi startup
- Better, safer way to enter network credentials for Direct Paths
- Fix check whether a direct path is accessible
- Fix OperationalError: no such table on database reset
- Fix widgets not displaying correct playstate after PKC startup
- Fix 'NoneType' object has no attribute 'execute' when Plex artwork is not synced and an item is deleted
- Update translations
- Log whether Plex artwork is synced to Kodi
version 2.11.6:
- Fix rare sync crash when queue was full
- Set "Auto-adjust transcoding quality" to false by default
version 2.5.22 (beta only):
- Fix rare EOFError and PKC starting wrong video as a consequence
version 2.11.5:
- Versions 2.11.0-2.11.4 for everyone
version 2.5.21 (beta only):
- Fix KodiVideoDB object has no attribute kodiconn
- Fix local variable 'set_api' referenced before assignment
version 2.11.4 (beta only):
- Fix another TypeError: 'NoneType' object has no attribute '__getitem__', e.g. when trying to play trailers
version 2.5.20 (beta only):
- Begin a new transaction when database was locked
- Fix browsing to show from info dialog
- Fix rare KeyError if user is playing something somewhere else
version 2.11.3 (beta only):
- Fix TypeError: 'NoneType' object has no attribute '__getitem__', e.g. when displaying albums
version 2.5.19 (beta only):
- Fix crash on startup-sync due to missing albums
- Fix browsing to show from info dialog
version 2.11.2 (beta only):
- Refactor direct and add-on paths. Enables use of Plex music playlists synched to Kodi
version 2.5.18 (beta only):
- Fix playback start: Don't lock databases when starting playback
- Refresh Kodi view only once on full syncs
- Ignore playstate updates for full sync time stamps croneter committed
- Try even longer to write to Kodi database
- Fix some items rarely not being synced
version 2.11.1 (beta only):
- Rewire the set-up of audio and subtitle streams, esp. before starting a transcoding session. Fixes playback not starting at all
version 2.5.17 (beta only):
- Fix playback not starting for really large libraries
version 2.11.0 (beta only):
- Fix PKC not burning in (and thus not showing) subtitles when transcoding
- When transcoding, only let user choose to burn-in subtitles that can't be displayed otherwise by Kodi
- Improve PKC automatically connecting to local PMS
- Ensure that our only video transcoding target is h264
- Fix adjusted subtitle size not working when burning in subtitles
- Fix regression: burn-in subtitles picking up the last user setting instead of the current one
</news>
version 2.5.16 (beta only):
- Fix KeyError due to malformed PMS messages
- Fix to database parameter must be string
version 2.5.15 (beta only):
- Make PKC potentially compatible with several database schemas
- Support for Kodi 18 Leia RC 5.2
- Increase number of attempts to write to Kodi DB
- Further increase database sync resiliance
version 2.5.14 (beta only):
Fix rare OperationalError: Locked Database
version 2.5.13 (beta only):
- Fix playback not starting up
- Fix Plex channels and watch later not working
- Hopefully fix playstate not being synced to PMS
version 2.5.12 (beta only):
- WARNING: You will need to reset the Kodi database!
- New option to not use Plex artwork
- Add-on paths: Fix resume if playback not initiated with PKC
- Increase database resiliance with sqlite WAL mode
version 2.5.11 (beta only):
- Direct Paths: Fix AttributeError for widgets
version 2.5.10 (beta only):
- Enable Plex Hub listings to be used for widgets
- Finally fix deleteting of items from PMS not working
- Catch sqlite OperationalError for websocket messages
- Revert "Increase database timeouts"
version 2.5.9 (beta only):
- Compatibility with Kodi 18 RC 4
- New setting to escape paths e.g. for HTTP direct paths
- Ensure path replacement never contains trailing (back)slash
- Leia: fix resetting of videoplayer autoplay next item
- Don't store identical show artwork for seasons
- Close DB connections while caching images
- Increase database timeouts
- Improve logging for seasons
version 2.5.8 (beta only):
- Hopefully fix Kodi crashing on playback start
- Fix video resuming from old resume point
- Fix database is locked
- Faster way to initialize playlists on the Plex side
- Fix PKC recreating playlists too often
- Shutdown playlist sync if necessary
version 2.5.7 (beta only):
- WARNING: You will need to reset the Kodi database!
- Increase timeout for database connections
- Fix music DB not being wiped on database reset
- Improve Plex playQueue resiliance
version 2.5.6 (beta only):
- Fix many items not getting synced
- Fix episodes not being synced to due a missing season
- Fix some very few items not being synced
- Fix ValueError during sync due to missing Plex timestamp
- Fix resume for episodes for add-on paths
- Fix movies not showing up on switching PMS
- Finish full syncs during playbacks, don't start new ones
- Fix AttributeError when a playlist disappeared
- Close sync dialog if video playback starts
- Don't show sync messages while Kodi is playing something
- Only marking full sync as successful if that is indeed the case
- Optimize code
version 2.5.5 (beta only):
- Fix OperationalError and PKC not starting up
version 2.5.4 (beta only):
- Fix a couple of issues related to episodes
- Fix permanent missing library items if PMS failed to send a single response
- Fix OperationalError: enforce Kodi restart with clean DB once
- Fix switching PMS not recognizing when old PMS is selected
- Fix PKC not automatically connecting to changed PMS IP on startup
- Remove message "Full library sync finished"
- Fix PKC not automatically connecting to changed PMS IP on startup
- Remove cProfile program metrics measurements
version 2.5.3 (beta only):
- Fix Plex sections not showing up or disappearing
version 2.5.2 (beta only):
- Rewire library sync
- Optimize sqlite transactions
- Replace annoying sync message with PKC settings info
- Add PKC settings status indication for caching
- Fix KeyError when synching playlists
- Fix ImportError for Plex Companion gdm issues
- Increase database connection cache size
- Force-Reboot Kodi immediately if sqlite PRAGMA WAL causes errors
- Force a full sync on switching Plex username
- Fix wierd behavior upon switching to another PMS
- More bugfixes and code optimizations
version 2.5.1 (beta only):
- Fix OSError on resetting the database
version 2.5.0 (beta only):
- Huge rewrite of the sync mechanism - it should now be faster and more stable
- Sync huge Plex libraries now: the sync will load all data bit by bit
- Rewrote code for the main program loop, reducing the need for separate Python threads
- Rewrote and sped up code to access and change Kodi and Plex databases
- Fixes to Kodi 18 Leia music library
- Tons of other small fixes I can't remember
version 2.4.10 (beta only):
- Use xml.etree.cElementTree whenever possible to avoid memory leaks
version 2.4.9:
- Fix Kodi crashing due to PKC memory leak
version 2.4.8:
- Make 2.4.4-2.4.7 available for everyone
version 2.4.7 (beta only):
- Try to fix PKC for Enigma 2
- Fix Kodi 18 wanting to scan tags for songs all the time (you will need to reset the database in the PKC settings)
- Optimize resetting of Kodi and Plex databases
version 2.4.6 (beta only):
- Fix PKC not starting up on Enigma
- Fix sync issues if video lies in root of file system
- Make sure we retain a dummy first music artist entry
- Increase logging
version 2.4.5 (beta only):
- Fix playback not starting up at all
- Rewire Kodi library refreshs
- Wipe Kodi database on first PKC run to more reliably install PKC
version 2.4.4 (beta only):
- Fix rare case when playback would not start-up
- Increase logging
version 2.4.3:
- Fix Kodi addons throwing jsonrpc errors (database reset needed)
version 2.4.2:
- Make version 2.4.1 available for everyone
version 2.4.1 (beta only):
- Hopefully fix endless playlist sync loops
- Ensure shows are deleted before seasons before episodes
- Fix library sync crash on deleting episode with missing season
- Fix numbering of already existing playlist files
- Optimize logging
version 2.4.0:
- Use pretty Plex dialogs for everyone!
version 2.3.14 (beta only):
- Fix AttributeError on forcing texture caching
- Switch to Plex style dialogs
- Include PKC info in plex.tv dialogs
- Include PKC info in user selection dialog
version 2.3.13 (beta only):
- Pretty Plex dialogs for plex.tv sign-in and user selection
- Fix UnicodeDecodeError for PMS with non ASCII chars on local LAN discovery
- Fix add-on settings not opening on installation
- Greatly speed up deleting of items on the Kodi side
- Safely parse XMLs using defusedxml
- Fix PKC trying to sync audio playlists even when audio sync disabled
- Some code cleanup
version 2.3.12:
- Fix Kodi hanging if media stream selection is aborted
- Fix potential sync crash
- Revert "Fix Kodi crash by committing to DB frequently"
version 2.3.11 (beta only):
- Fix Kodi crash by committing to DB frequently
version 2.3.10:
- Compatibility with Kodi 18 Leia Beta 1
- Update translations
- Make version 2.3.9 available for everyone
version 2.3.9 (beta only):
- Fix playback not resuming (Kodi 18 ignores listitem "StartOffset")
- Fix playerid not being retrieved for Kodi 18
- Prefer local trailers; new setting to list extras instead of playing trailer
version 2.3.8:
- Fix typo
- Make version 2.3.4-2.3.7 available for everyone
version 2.3.7 (beta only):
- Fix library sync crash due to exotic playlist characters
- Force-deactivate playlist sync for Microsoft UWP for Kodi 18
version 2.3.6 (beta only):
- Fix PKC not starting by decoupling watchdog/subprocess modules
version 2.3.5 (beta only):
- Fix PKC not starting by importing playlist module only when sync enabled
version 2.3.4 (beta only):
- Fix playback sometimes not starting and UnicodeEncodeError for logging
version 2.3.3:
- Choose trailer if several are present (DB reset required)
version 2.3.2:
- Fix casting to PKC failing
version 2.3.1:
- Fix library sync crashing due to Plex photo albums
version 2.3.0:
Major stable version bump. Highlights:
- Sync Plex playlists to Kodi and Kodi playlists to Plex!
- Support for Plex collection/set artwork
- Many bug fixes, especially Plex Companion
- Tons of code improvements in the hope that someone else will help with developing PKC
Warning: the 2 helper add-ons for movies and tv shows also received an upgrade from 2.0.4 to 2.0.5. If you want to downgrade PKC, be sure to downgrade these add-ons as well!
version 2.2.18 (beta only):
- Fix PKC tv show node "all"
- Move PKC playlist shortcut
version 2.2.17 (beta only):
- Access Plex Hubs. Listing will be different depending on Kodi section!
- Fix year for songs missing
- Fix Plex extras not playing
- Fix rare library sync crash
version 2.2.16 (beta only):
- Enable Kodi libraries for Plex Music libraries
- New Playlists menu item for video libraries
- Only show Plex libraries in the applicable Kodi media category
- Optimize code
version 2.2.15 (beta only):
- Fix ImportError on first PKC run
version 2.2.14 (beta only):
- Hopefully fix playlist sync loops
version 2.2.13 (beta only):
- Fix library sync crash
- Fix switching to __future__ module
- Fix "Prefer Kodi Artwork" toggle doing the exact opposite
- Fix "Prefer Kodi artwork" setting not being visible
version 2.2.12 (beta only):
- Fix slow sync. Fix endless sync of corrupted PMS elements
- Refactor playlist code
- Fix FutureWarning
version 2.2.11 (beta only):
- Fix OnDeck widget for Direct Paths
- Fix Plex Companion crashing when connected to Plex Web
- Fix Plex Companion crash when connected to Plex Web playing playlist music
- Improve Plex playback report when playing music playlist
- Improve reliability in Kodi song playback
- Catch some errors if user mixes audio and video in Kodi playqueue
version 2.2.10 (beta only):
- Fix playlists getting recreated and deleted in an endless loop
- Add some safety nets for playlist sync
- Fix FutureWarning
- Fix playlist sync settings not disappearing
- Optimize code
version 2.2.9 (beta only):
- Hopefully fix Kodi and Plex playlists getting out of sync
- Fix and optimize startup of playlist sync
- Hide certain playlist settings under certain conditions
- Fix errors in Kodi log
version 2.2.8 (beta only):
- Support for Plex collection artwork (PKC settings toggle under Artwork)
- Fix hard PKC reset not working (OSError: no such file)
- Deduplication
- Catch exception
- Update translations
- Extend Kodi metadata
- Update readme
version 2.2.7 (beta only):
- Allow to only sync specific Plex or Kodi playlists
- Don't show artwork sync progress, reduce setting-writes
- Fix playback sometimes not starting up
- Use __future__ for contextmenu.py
- Fix imports
version 2.2.6 (beta only):
- Fix default settings string, only show in English, hopefully fixes PKC loosing its settings
version 2.2.5 (beta only):
- Fix AttributeError and add_update has crashed
version 2.2.4 (beta only):
- Fix LibrarySync crashing due to Plex Companion messages
version 2.2.3 (beta only):
- Compatibility with Kodi Krypton Alpha 2
- Append tv show and SxxExx to episode playlist entries
version 2.2.2 (beta only):
- Fixes to locking mechanisms which resulted in weird behavior in some cases
- Switch to Python __future__ unicode_literals and absolute paths
- Fix UnboundLocalError for playlists
- Check all Kodi database versions before starting PKC
- Fix KeyError on non-PKC playback startup
- Speed up subtitle download to Kodi
- Update translations
- PEP-8 stuff
version 2.2.1 (beta only):
- Fix library sync crash due to PMS sending string, not unicode
- Fix playback from playlists for add-on paths
- Detect playback from a Kodi playlist for add-on paths - because we need some hacks due to Kodi bugs
- Fix add-on paths playstate and Plex Companion for playlists
- Fix Kodi telling Plex companion false playqueue position
- Don't try to get a Kodi library items for Plex clips
- Update translations
version 2.2.0 (beta only):
- Support for syncing Plex playlists to Kodi and vice-versa! (Kodi mixed music and video playlists cannot be supported as Plex does not support them)
version 2.1.6:
- Fix slow sync. Fix endless sync of corrupted PMS elements
version 2.1.5:
- Fix OnDeck widget for Direct Paths
version 2.1.4:
- Fix PKC settings suddenly getting lost
- Don't show artwork sync progress, reduce setting-writes
version 2.1.3:
- Fix default settings string, only show in English, hopefully fixes PKC loosing its settings
version 2.1.2:
- Compatibility with Kodi Krypton Alpha 2
- Check all Kodi database versions before starting PKC
- Fix KeyError on non-PKC playback startup
- PEP-8 stuff
version 2.1.1:
- Fix Library Sync crash on Android
version 2.1.0:
Finally a new update for the stable version. You will need to reconnect to your PMS and reset the Kodi database once. Highlights of v2 include:
- Support for Plex extras
- Huge improvements to Plex Companion
- Fixes to Alexa voice control
- Kodi 18 Leia Alpha 1 support
- Improvements to playback start-up
- Improvements to the syncing mechanism, which should get rid of a ton of small bugs
- Fixes to widgets and resuming playback
- Use of plex.direct paths instead of local IP addresses to ensure the SSL certificates shown by the PMS are deemed valid
- Fix Kodi screensaver
- Faster PKC startup
- And tons of other stuff...</news>
</extension>
</addon>

View File

@ -1,412 +1,3 @@
version 2.15.0:
- versions 2.14.3-2.14.4 for everyone
- Direct Paths: Fix TypeError: "element indices must be integers" on playback startup [backport]
- Refactor stream code and fix Kodi not activating subtitle when it should [backport]
- Add playback settings to let the user choose whether Plex or Kodi provides the default audio or subtitle stream on playback start [backport]
- Update translations from Transifex [backport]
version 2.14.4 (beta only):
- Tell the PMS if a video's audio stream or potentially subtitle stream has changed. For subtitles, this functionality is broken due to a Kodi bug
- Transcoding: Fix Plex burning-in subtitles when it should not
- Fix logging if fanart.tv lookup fails: be less verbose
- Large refactoring of playlist and playqueue code
- Refactor usage of a media part's id
version 2.14.3 (beta only):
- Implement "Reset resume position" from the Kodi context menu
version 2.14.2:
- version 2.14.1 for everyone
version 2.14.1 (beta only):
- Use Plex settings for audio and subtitle stream selection. This is a best guess regarding subtitles as Plex and Kodi are not sharing much info
- Fix PlexKodiConnect setting the Plex subtitle to None
- Download landscape artwork from fanart.tv, thanks @geropan
- Revert "Fix PlexKodiConnect changing subtitles for all videos on the PMS"
version 2.14.0:
- Fix PlexKodiConnect changing or removing subtitles for every video on the PMS
- version 2.13.1-2.13.2 for everyone
version 2.13.2 (beta only):
- Fix a racing condition that could lead to the sync getting stuck
- Fix RecursionError: maximum recursion depth exceeded
- Websocket Fix AttributeError: 'NoneType' object has no attribute 'is_ssl'
version 2.13.1 (beta only):
- Fix a racing condition that could lead to the sync process getting stuck
- Fix likelyhood of `database is locked` error occuring
version 2.13.0:
- Support for the Plex HAMA agent to let Kodi identify animes (using Kodi's uniqueID 'anidb')
- Support forced HAMA IDs when using tvdb uniqueID
- version 2.12.26 for everyone
version 2.12.26 (beta only):
- Add an additional Plex Hub "PKC Continue Watching" that merges the Plex Continue Watching with On Deck
- Fix auto-picking of video stream if several video versions are available
- Update translations
version 2.12.25:
- Update websocket client to 0.59.0. Fix threading issues and AttributeErrors
version 2.12.24:
- version 2.12.23 for everyone
version 2.12.23 (beta only):
- Fix Alexa and RuntimeError: dictionary keys changed during iteration
- Fix a rare AttributeError when using playlists
version 2.12.22:
- version 2.12.20 and 2.12.21 for everyone
version 2.12.21 (beta only):
- Switch to new websocket implementation
- Hopefully fix RuntimeError: no add-on id "plugin.video.plexkodiconnect"
- Update translations
version 2.12.20 (beta only):
- Add information to PKC settings for background sync and Alexa whether a connection has been successfully made
version 2.12.19:
- 2.12.17 and 2.12.18 for everyone
- Rename skip intro skin file
version 2.12.18 (beta only):
- Quickly sync recently watched items before synching the playstates of the entire Plex library
- Improve logging for websocket JSON loads
version 2.12.17 (beta only):
- Sync name and user rating of a TV show season to Kodi
- Fix rare TypeError: expected string or buffer on playback start
version 2.12.16:
- versions 2.12.14 and 2.12.15 for everyone
version 2.12.15 (beta only):
- Fix skip intros sometimes not working due to a RuntimeError
- Update translations
version 2.12.14 (beta only):
- Add skip intro functionality
version 2.12.13:
- Fix KeyError: u'game' if Plex Arcade has been activated
- Fix AttributeError: 'App' object has no attribute 'threads' when sync is cancelled
version 2.12.12:
- Hopefully fix rare case when sync would get stuck indefinitely
- Fix ValueError: invalid literal for int() for invalid dates sent by Plex
- version 2.12.11 for everyone
version 2.12.11 (beta only):
- Fix PKC not auto-picking audio/subtitle stream when transcoding
- Fix ValueError when deleting a music album
- Fix OSError: Invalid argument when Plex returns an invalid timestamp
version 2.12.10:
- Fix pictures from Plex picture libraries not working/displaying
version 2.12.9:
- Fix Local variable 'user' referenced before assignement
version 2.12.8:
- version 2.12.7 for everyone
version 2.12.7 (beta only):
- Fix PKC suddenly using main Plex user's credentials, e.g. when the PMS address changed
- Fix missing Kodi tags for movie collections/sets
version 2.12.6:
- Fix rare KeyError when using PKC widgets
- Fix suspension of artwork caching and PKC becoming unresponsive
- Update translations
- Versions 2.12.4 and 2.12.5 for everyone
version 2.12.5 (beta only):
- Greatly improve matching logic for The Movie Database if Plex does not provide an appropriate id
- Fix high transcoding resolutions not being available for Win10
- Fix rare playback progress report failing and KeyError: u'containerKey'
- Fix rare KeyError: None when trying to sync playlists
- Fix TypeError when canceling Plex sync section dialog
version 2.12.4 (beta only):
- Hopefully fix freeze during sync: Don't assign multiple sets/collections for a specific movie
- Support metadata provider ids (e.g. for IMDB) for the new Plex Movie Agent
version 2.12.3:
- Fix playback failing due to caching of subtitles with non-ascii chars
- Fix ValueError: invalid literal for int() with base 10 during show sync
- Fix UnboundLocalError when certain Plex sections are deleted or being un-synched
- New method to install PlexKodiConnect directly via an URL. You thus do not need to upload a ZIP file to Kodi anymore.
version 2.12.2:
- version 2.12.0 and 2.12.1 for everyone
- Fix regression: sync dialog not showing up when it should
version 2.12.1 (beta only):
- Fix PKC shutdown on Kodi profile switch
- Fix Kodi content type for images/photos
- Added support for custom set of safe characters when escaping paths (thanks @geropan)
- Revert "Don't allow spaces in devicename"
- Fix sync dialog showing in certain cases even though user opted out
version 2.12.0 (beta only):
- Fix websocket threads; enable PKC background sync for all Plex Home users!
- Fix PKC incorrectly marking a video as unwatched if an external player has been used
- Update translations
version 2.11.7:
- Fix PKC crashing on devices running Microsoft UWP, e.g. XBox
version 2.11.6:
- Fix rare sync crash when queue was full
- Set "Auto-adjust transcoding quality" to false by default
version 2.11.5:
- Versions 2.11.0-2.11.4 for everyone
version 2.11.4 (beta only):
- Fix another TypeError: 'NoneType' object has no attribute '__getitem__', e.g. when trying to play trailers
version 2.11.3 (beta only):
- Fix TypeError: 'NoneType' object has no attribute '__getitem__', e.g. when displaying albums
version 2.11.2 (beta only):
- Refactor direct and add-on paths. Enables use of Plex music playlists synched to Kodi
version 2.11.1 (beta only):
- Rewire the set-up of audio and subtitle streams, esp. before starting a transcoding session. Fixes playback not starting at all
version 2.11.0 (beta only):
- Fix PKC not burning in (and thus not showing) subtitles when transcoding
- When transcoding, only let user choose to burn-in subtitles that can't be displayed otherwise by Kodi
- Improve PKC automatically connecting to local PMS
- Ensure that our only video transcoding target is h264
- Fix adjusted subtitle size not working when burning in subtitles
- Fix regression: burn-in subtitles picking up the last user setting instead of the current one
version 2.10.12:
- versions 2.10.5-11 for everyone
version 2.10.11 (beta only):
- Fix yet another rare but annoying bug where PKC becomes unresponsive during sync
version 2.10.10 (beta only):
- Fix rare but annoying bug where PKC becomes unresponsive during sync
- Fix PKC background sync not working in some cases
version 2.10.9 (beta only):
- Other Kodi add-ons can now search for Plex items using plugin://plugin.video.plexkodiconnect?mode=search&query=YOUR SEARCH STRING HERE
version 2.10.8 (beta only):
- Improve thread pool management to render PKC snappier
- Attempt to fix broken pipe error
- Fix DirectPaths when a video's folder name is identical to a video's filename (you will need to manually reset the Kodi database)
version 2.10.7 (beta only):
- Fix PKC not starting up on iOS
- Optimize the new sync process and fix some bugs that were introduced
- Fix PKC becoming unresponsive e.g. when switching the PMS
version 2.10.6 (beta only):
- Fix AttributeError if user enters an invalid pin code
- Fix OperationalError when starting with a fresh PKC installation
- Fix IndexError
version 2.10.5 (beta only):
- Rewire library sync to speed it up and fix sync getting stuck in rare cases
- Optimize threads by using events instead of a polling mechanism. Fixes PKC becoming unresponsive, e.g. when switching users
- Optimize adding values to Kodi databases by not using sqlite COALESCE command
- Fix OperationalError when resetting PKC
- Improve sync resiliance when certain items are not to be synced to Kodi or PKC skipped an item in the past
- Make sure bool is returned instead of an int
- Don't use WAL mode for sqlite connections, it is not making any difference
version 2.10.4:
- version 2.10.3 for everyone
- Fix to correctly wipe Kodi databases
version 2.10.3 (beta only):
- Fix a couple of issues with music when using direct paths: correctly escape music paths for Kodi regex matching
- Fix Recently Added Albums sort order (you will have to reset the Kodi database manually)
- Fix database being locked in rare cases
- Increase batch size for library sync from 500 to 2000 to increase sync speed
- Optimize some code
- Fix KeyError when using Plex search capabilities
- Check faster for available Plex Media Server to connect to
version 2.10.2:
- Fix Kodi playback jumping to the beginning of a video that just started
- Fix transcoding quality degenerating quickly while playing with a new setting to deactivate auto quality for transcoding (applicable e.g. for Chromecast)
- Update translations
version 2.10.1:
- Fix resume for Kodi on low powered devices, e.g. Raspberry Pi
- Fix resume when using an external player
- Fix UnicodeWarning: Unicode equal comparison failed to convert both arguments to Unicode - interpreting them as being unequal
version 2.10.0:
- version 2.9.12 - 2.9.14 for everyone
- Get rid of some obsolete code for the ContextMonitor we dropped
version 2.9.14 (beta only):
- Fix resume when starting playback via PMS or when force transcoding
- Get rid of ContextMonitor and the dedicated Python thread - with new resume mechanics, this is not needed anymore
- Optimize clean-up of file table in the Kodi video database after stopping playback
- Get rid of some obsolete imports
version 2.9.13 (beta only):
- Fix PKC resuming instead of playing from the beginning
version 2.9.12 (beta only):
- Fix resume not working in some cases
- Support Plex search across all media and Plex Media Servers: Navigate to the PlexKodiConnect Add-on, then "Search"
- Always use the current Kodi language when communicating with the PMS (restart Kodi when changing the language!)
- Fix Kodi crashing when casting from e.g. Plex Web or Plex for Windows
- Fix PKC throwing error if m3u playlist contains resume information
version 2.9.11:
- version 2.9.10 for everyone
version 2.9.10 (beta only):
- Add tmdb provider sync
- Fix external subtitles not being available
- Fix PKC increasing the Plex watch count by 2 instead of 1
- Improve subtitle naming
- Delete temporary subtitles on playback stop
- Fix a missleading string
version 2.9.9:
- Versions 2.9.6 - 2.9.8 for everyone
version 2.9.8 (beta only):
- Fix Play Error in scenarios (older PMS version?) where posting playqueues using an uri `server://` is not possible and `library://` is necessary
- Fix rare AttributeError on PKC startup when modifying advancedsettings.xml
- Update translations
version 2.9.7 (beta only):
- Correctly escape URLs for Direct Paths
- Update settings to inform user that reboot is necessary
- Optimize code
- Don't migrate PKC settings if we're dealing with a clean new PKC installation
- Force-scan every single item in the library - seems like we could lose some recently added items otherwise when updating PKC
version 2.9.6 (beta only):
- Rework logic for using direct paths, direct play, direct streaming and transcoding, using the PMS StreamingBrain: Let PMS StreamingBrain decide on whether we need to force-transcode, New setting to choose "Direct Streaming", Allow for 4k transcoding and direct streaming, New setting to force transcode only 4K and above
- Fix PKC background sync synching items to Kodi even though entire section should not be synched
- Force a full sync of all items after choosing a new PMS, changing a PMS' address and changing which Plex libraries to sync
- Only enforce advancedsettings.xml 'cleanonupdate' to be false for PKC add-on paths
- Never give up trying to connect to the PMS or Alexa using websockets
- Fix resume when force-transcoding
version 2.9.5:
- Version 2.9.4 for everyone
version 2.9.4 (beta only):
- Fix extras not playing when path substitution is enabled
- Fix Plex Companion device restarting playback when reconnecting to PKC
- Fix playback report not working after having played a non-Plex video file
- Change how items are added to Plex playqueues by using PMS machine identifier
- Optimize code for playqueue items
- Fix rare AttributeError when shutting down Kodi
version 2.9.3:
- version 2.9.2 for everyone
version 2.9.2 (beta only):
- Fix Plex Companion casting from iOS and Android
- Faster sync of playlists
- Sync playlists immediately after synching new/changed items and show an info dialog
- Fix potential playlist sync issues if there is a dot in the playlist name
- Correctly detect whether we already synched a Kodi playlist
- Remove obsolete check if path is indeed in unicode
- Add unicode representation to Playlist() class
- Separate function to wipe all synched Plex playlists
- Less logging when comparing PKC versions
version 2.9.1:
- Fix On Deck and Recently Added Episodes for shows not appending showname and season and episode number
version 2.9.0:
WARNING: You might have to manually select your PKC widgets again
- versions 2.8.8 - 2.8.11 for everyone
- Fix AttributeError: 'NoneType' object has no attribute 'attrib' on playback startup
- Add new Lithuanian translations (thanks @egidusm)
version 2.8.11 (beta only):
- Support for the Up Next Kodi add-on
- Fix casting to PlexKodiConnect always starting the first episode
- Rename video nodes for ondeck
version 2.8.10 (beta only):
- Fix broken PKC update
version 2.8.9 (beta only):
- Fix sections that are not synced not displaying menu but entire library
- Provide more metadata for unsynced directory-like items like a tv show
- Fix 'Plex.nodes.<id>.path' not linking directly to entire library
version 2.8.8 (beta only):
WARNING: You might have to manually select your PKC widgets again
- Ensure correct Kodi Container.Type is set for PKC widgets
- Fix missing cast artwork if an actor also acted as director or writer for another movie. You will have to manually reset the Kodi DB.
version 2.8.7:
- Fix PKC potentially marking a video as watched on startup; don't sync time by toggling a video watch status but use PMS epoch time
version 2.8.6:
- Fix PKC creating thousands of playlists if a single Kodi playlist wasn't unique
- Fix FutureWarning
version 2.8.5:
- Fix Trakt add-on not recognizing id of tv shows (you will need to manually reset the Kodi database in the PKC settings under Advanced)
- Update translations
version 2.8.4:
- Fix for Kodi 17 Krypton TypeError on playback start: 'offscreen' is an invalid keyword argument for this function
- Fix widgets not being populated after very first PlexKodiConnect library sync without a restart of Kodi
- Don't restart Kodi if user chose to enter PKC settings on install
version 2.8.3:
- Versions 2.8.1-2.8.2 for everyone
version 2.8.2 (beta only):
- Add an additional, faster On Deck node for movies (for tv shows, this is impossible, unfortunately)
- Introduce limits to the number of videos shown in PKC widgets to speed them up
- Fix TypeError for Direct Paths: init() got an unexpected keyword argument item
- Fix In Progress widgets being broken and tv shows showing up as completely watched
- Update translations
version 2.8.1 (beta only):
- Fix playback startup and RuntimeError: Unknown exception thrown from the call "XBMCAddon::xbmcplugin::setResolvedUrl"
- Refactor Plex API
- Fix TV Show clearlogo not displaying during playback
- Fix rare UnicodeDecodeError on library sync
- Add additional info dialog for PKC synching playlists
- Update translations
version 2.8.0:
- Finally fix Kodi crashing on playback startup for add-on paths!
- All the good stuff from 2.7.15-2.7.18 for everyone
version 2.7.18 (beta only):
- Fix Kodi always playing the same file version of a video if several are present
- Also play trailers if user chose to resume movie from the beginning
- Ask user whether to resume if using Direct Paths and user initiated playback via PMS
- Fix video thrown by Plex Companion not resuming
version 2.7.17 (beta only):
- Another attempt to keep Kodi from crashing on playback startup
version 2.7.16 (beta only):
- Hopefully fix Kodi crashing on playback startup for good
version 2.7.15 (beta only):
- Hopefully fix Kodi crashing on playback startup
- Refresh widgets only on homescreen to prevent cursor from jumping within libraries
- Don't refresh container when user chose to delete or refresh an item from the context menu
version 2.7.14:
- Correctly clear window variables e.g. on user switch
- Reload skin on resetting PKC video nodes

View File

@ -39,7 +39,21 @@ class Main():
mode = params.get('mode', '')
itemid = params.get('id', '')
if mode == 'play':
if mode == 'playstrm':
while not utils.window('plex.playlist.play'):
xbmc.sleep(25)
if utils.window('plex.playlist.aborted'):
LOG.info("playback aborted")
break
else:
LOG.info("Playback started")
xbmcplugin.setResolvedUrl(int(argv[1]),
False,
xbmcgui.ListItem())
utils.window('plex.playlist.play', clear=True)
utils.window('plex.playlist.aborted', clear=True)
elif mode == 'play':
self.play()
elif mode == 'plex_node':
@ -50,11 +64,7 @@ class Main():
plex_type=params.get('plex_type'),
section_id=params.get('section_id'),
synched=params.get('synched') != 'false',
prompt=params.get('prompt'),
query=params.get('query'))
elif mode == 'show_section':
entrypoint.show_section(params.get('section_index'))
prompt=params.get('prompt'))
elif mode == 'watchlater':
entrypoint.watchlater()
@ -62,14 +72,6 @@ class Main():
elif mode == 'channels':
entrypoint.browse_plex(key='/channels/all')
elif mode == 'search':
# "Search"
entrypoint.browse_plex(key='/hubs/search',
args={'includeCollections': 1,
'includeExternalMedia': 1},
prompt=utils.lang(137),
query=params.get('query'))
elif mode == 'route_to_extras':
# Hack so we can store this path in the Kodi DB
handle = ('plugin://%s?mode=extras&plex_id=%s'
@ -169,11 +171,17 @@ class Main():
# Handle -1 received, not waiting for main thread
return
# Wait for the result from the main PKC thread
result = transfer.wait_for_transfer(source='main')
if result is True:
result = transfer.wait_for_transfer()
if result is None:
LOG.error('Error encountered, aborting')
utils.dialog('notification',
heading='{plex}',
message=utils.lang(30128),
icon='{error}',
time=3000)
xbmcplugin.setResolvedUrl(HANDLE, False, xbmcgui.ListItem())
# Tell main thread that we're done
transfer.send(True, target='main')
elif result is True:
pass
else:
# Received a xbmcgui.ListItem()
xbmcplugin.setResolvedUrl(HANDLE, True, result)

View File

@ -1,7 +1,7 @@
# XBMC Media Center language file
# Translators:
# Croneter None <croneter@gmail.com>, 2017
# Michal Kuncl <michal.kuncl@gmail.com>, 2020
# Michal Kuncl <michal.kuncl@gmail.com>, 2019
#
msgid ""
msgstr ""
@ -9,7 +9,7 @@ msgstr ""
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
"Last-Translator: Michal Kuncl <michal.kuncl@gmail.com>, 2020\n"
"Last-Translator: Michal Kuncl <michal.kuncl@gmail.com>, 2019\n"
"Language-Team: Czech (Czech Republic) (https://www.transifex.com/croneter/teams/73837/cs_CZ/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -45,13 +45,6 @@ msgstr ""
"Varování: Máte v Kodi zapnuté nastavení \"Automaticky přehrát další video\"."
" Toto může narušit funkčnost PKC. Deaktivovat?"
msgctxt "#30004"
msgid ""
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
"random password automatically if you haven't done so already. Please confirm"
" the next dialog that you want to enable the webserver now with Yes."
msgstr ""
msgctxt "#30005"
msgid "Username: "
msgstr "Uživ. jméno: "
@ -168,14 +161,6 @@ msgctxt "#30028"
msgid "PKC-only image caching completed"
msgstr "Stahování obrázků PKC dokončeno"
# Warning shown when PKC switches to the Kodi default skin Estuary
msgctxt "#30029"
msgid ""
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
"database resets. Continue?"
msgstr ""
msgctxt "#30030"
msgid "Port Number"
msgstr "Číslo portu"
@ -275,10 +260,6 @@ msgctxt "#30160"
msgid "Video Quality if Transcoding necessary"
msgstr "Kvalita videa při překódování"
msgctxt "#30161"
msgid "Auto-adjust transcoding quality (deactivate for Chromecast)"
msgstr "Automaticky upravit kvalitu překódování (deaktivujte pro Chromecast)"
msgctxt "#30165"
msgid "Direct Play"
msgstr "Přímé přehrávání"
@ -611,11 +592,6 @@ msgctxt "#30524"
msgid "Select Plex libraries to sync"
msgstr "Zvolte knihovny Plexu k synchronizaci"
# PKC Settings - Playback
msgctxt "#30525"
msgid "Skip intro"
msgstr ""
# PKC Settings - Playback
msgctxt "#30527"
msgid "Ignore specials in next episodes"
@ -680,8 +656,8 @@ msgstr "Stahovat obrázky filmových kolekcí z FanArtTV"
# PKC Settings - Playback
msgctxt "#30541"
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
msgstr ""
msgid "Don't ask to pick a certain stream/quality"
msgstr "Nepožadovat výběr proudu nebo kvality"
# PKC Settings - Playback
msgctxt "#30542"
@ -702,21 +678,6 @@ msgctxt "#30545"
msgid "Force transcode pictures"
msgstr "Vynutit překódování obrázků"
# PKC Settings - Playback
msgctxt "#30546"
msgid "Pick the first video if several versions are present"
msgstr ""
# PKC Settings - Playback
msgctxt "#30547"
msgid "Who picks the audio stream on playback start?"
msgstr ""
# PKC Settings - Playback
msgctxt "#30548"
msgid "Who picks subtitles on playback start?"
msgstr ""
# Welcome to Plex notification
msgctxt "#33000"
msgid "Welcome"
@ -737,17 +698,6 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Server je online"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr "Překódování vynucené PMS"
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr "Přímé streamování vynucené PMS"
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -985,11 +935,6 @@ msgctxt "#39036"
msgid "Escape special characters in path (e.g. space to %20)"
msgstr "Nahrazovat speciální znaky v cestě (např. z mezery na %20)"
# PKC Settings - Customize Paths
msgctxt "#39090"
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
msgstr ""
# PKC Settings - Customize Paths
msgctxt "#39037"
msgid "Original Plex MOVIE path to replace:"
@ -1095,17 +1040,19 @@ msgstr "Vyhledávám Plex servery"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
"Použito při synchronizaci a při pokusu o přehrávání přes přímé cesty. "
"Restartujte Kodi po změně!"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Použito při synchronizaci a při pokusu o přímé přehrávání"
# PKC Settings, category name
msgctxt "#39057"
msgid "Customize Paths"
msgstr "Přizpůsobit cesty"
# PKC Settings - Appearance Tweaks
msgctxt "#39058"
msgid "Extend Plex TV Series \"On Deck\" view to all shows"
msgstr "Rozšířit seriály na obrazovce \"Aktuální\" na všechny seriály"
# PKC Settings - Appearance Tweaks
msgctxt "#39059"
msgid "Recently Added: Append show title to episode"
@ -1145,8 +1092,12 @@ msgstr "Vynutit obnovení vzhledu Kodi po skončení přehrávání"
# PKC Settings - Appearance Tweaks
msgctxt "#39066"
msgid "Recently Added: Also show already watched movies"
msgstr "Naposledy přidané: Zobrazovat také shlédnuté filmy"
msgid ""
"Recently Added: Also show already watched movies (Refresh Plex "
"playlist/nodes!)"
msgstr ""
"Naposledy přidané: Zobrazovat také už shlédnuté filmy (Obnovte playlisty "
"Plexu!)"
# PKC Settings - Connection
msgctxt "#39067"
@ -1173,11 +1124,6 @@ msgctxt "#39071"
msgid "Current plex.tv status:"
msgstr "Současný stav plex.tv:"
# PKC Settings - Connection
msgctxt "#39072"
msgid "Background sync connection:"
msgstr ""
# PKC Settings, category name
msgctxt "#39073"
msgid "Appearance Tweaks"
@ -1188,10 +1134,10 @@ msgctxt "#39074"
msgid "TV Shows"
msgstr "Seriály"
# PKC Settings - Sync
# PKC Settings - Playback
msgctxt "#39075"
msgid "Verify access to media files while synching"
msgstr ""
msgid "Always use default Plex subtitle if possible"
msgstr "Vždy použít výchozí titulky Plexu, pokud je to možné"
# Pop-up during initial sync
msgctxt "#39076"
@ -1205,8 +1151,8 @@ msgstr ""
# PKC Settings - Appearance Tweaks
msgctxt "#39077"
msgid "Maximum number of videos to show in widgets"
msgstr "Maximální počet videí zobrazovaných ve widgetech"
msgid "Number of PMS items to show in widgets (e.g. \"On Deck\")"
msgstr "Počet položek pro zobrazení ve widgetech (např. \"Aktuální\")"
# PKC Settings - Plex
msgctxt "#39078"
@ -1253,36 +1199,6 @@ msgctxt "#39084"
msgid "Enter PMS port"
msgstr "Zadejte port PMS"
# PKC settings - Appearance Tweaks
msgctxt "#39085"
msgid "Reload Kodi node files to apply all the settings below"
msgstr "Znovu načíst Kodi pro aplikování nastavení níže"
# PKC Settings - Connection - Background sync connection status
msgctxt "#39089"
msgid "Alexa connection status:"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39091"
msgid "Timeout - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39092"
msgid "IOError - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39093"
msgid "Suspended - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39094"
msgid "Managed Plex User - not connected"
msgstr ""
msgctxt "#39200"
msgid "Log-out Plex Home User "
msgstr "Odhlásit uživatele Plex Home "
@ -1340,8 +1256,11 @@ msgid "Enter your Plex Media Server's IP or URL, Examples are:"
msgstr "Zadejte IP adresu nebo URL vašeho Plex Media Serveru. Např.:"
msgctxt "#39217"
msgid "Use HTTPS (SSL) connections? Answer should probably be yes."
msgstr "Použít připojení HTTPS (SSL)? Odpověď by měla nejspíš ano."
msgid ""
"Use HTTPS (SSL) connections? With Kodi 18 or later, HTTPS will likely not "
"work!"
msgstr ""
"Použít připojení přes HTTPS (SSL)? V Kodi 18 nejspíš HTTPS nebude fungovat!"
msgctxt "#39218"
msgid "Error contacting PMS"
@ -1524,10 +1443,6 @@ msgctxt "#39501"
msgid "Collections"
msgstr "Kolekce"
msgctxt "#39502"
msgid "PKC On Deck (faster)"
msgstr "PKC Aktuální (rychlejší)"
msgctxt "#39600"
msgid ""
"Are you sure you want to reset your local Kodi database? A re-sync of the "
@ -1586,10 +1501,11 @@ msgctxt "#39705"
msgid "Use at your own risk"
msgstr "Používejte na vlastní nebezpečí"
# If user gets prompted to choose between several subtitles to burn in
# If user gets prompted to choose between several subtitles. Leave the number
# one at the beginning of the string!
msgctxt "#39706"
msgid "Don't burn-in any subtitle"
msgstr "Nevpalovat žádné titulky"
msgid "1 No subtitles"
msgstr "1 Žádné titulky"
# If user gets prompted to choose between several audio/subtitle tracks and
# language is unknown
@ -1643,8 +1559,8 @@ msgstr "Synchronizuji"
# Shown during sync process
msgctxt "#39715"
msgid "Synching playlists"
msgstr "Synchronizuji playlisty"
msgid "items"
msgstr "položek"
# Error message if an xml, e.g. advancedsettings.xml cannot be parsed (xml is
# screwed up; formated the wrong way). Do NOT replace {0} and {1}!

View File

@ -1,8 +1,8 @@
# XBMC Media Center language file
# Translators:
# Croneter None <croneter@gmail.com>, 2017
# Christian Sallerup <sallerup2001@gmail.com>, 2019
# Thomas H. <dontspampls@gmail.com>, 2019
# coz2001 <sallerup2001@gmail.com>, 2019
#
msgid ""
msgstr ""
@ -10,7 +10,7 @@ msgstr ""
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
"Last-Translator: coz2001 <sallerup2001@gmail.com>, 2019\n"
"Last-Translator: Thomas H. <dontspampls@gmail.com>, 2019\n"
"Language-Team: Danish (Denmark) (https://www.transifex.com/croneter/teams/73837/da_DK/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -46,13 +46,6 @@ msgstr ""
"Advarsel: Kodi indstillingen \"Afspil næste video automatisk\" er aktiveret."
" Dette kan ødelægge PKC funktionalitet. Deaktiver? "
msgctxt "#30004"
msgid ""
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
"random password automatically if you haven't done so already. Please confirm"
" the next dialog that you want to enable the webserver now with Yes."
msgstr ""
msgctxt "#30005"
msgid "Username: "
msgstr "Brugernavn: "
@ -168,14 +161,6 @@ msgctxt "#30028"
msgid "PKC-only image caching completed"
msgstr "PKC billede caching er færdiggjort"
# Warning shown when PKC switches to the Kodi default skin Estuary
msgctxt "#30029"
msgid ""
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
"database resets. Continue?"
msgstr ""
msgctxt "#30030"
msgid "Port Number"
msgstr "Portnummer"
@ -275,10 +260,6 @@ msgctxt "#30160"
msgid "Video Quality if Transcoding necessary"
msgstr "Videokvalitet hvis trancoding er nødvendigt"
msgctxt "#30161"
msgid "Auto-adjust transcoding quality (deactivate for Chromecast)"
msgstr ""
msgctxt "#30165"
msgid "Direct Play"
msgstr "Direkte afspilning"
@ -514,8 +495,6 @@ msgstr "Synkronisér Plex artwork fra PMS (anbefalet)"
msgctxt "#30503"
msgid "SSL certificate failed to validate. Please check {0} for solutions."
msgstr ""
"SSL certifikat fejl. \n"
"Se {0} for løsninger"
# PKC Settings, category name
msgctxt "#30506"
@ -609,11 +588,6 @@ msgstr "Vis også synkronseringsproces for playstate og brugerdata"
# PKC Settings - Sync Options
msgctxt "#30524"
msgid "Select Plex libraries to sync"
msgstr "Vælg Plex biblioteker der skal synkroniseres"
# PKC Settings - Playback
msgctxt "#30525"
msgid "Skip intro"
msgstr ""
# PKC Settings - Playback
@ -680,8 +654,8 @@ msgstr "Download film sæt/samling info fra FanArtTV"
# PKC Settings - Playback
msgctxt "#30541"
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
msgstr ""
msgid "Don't ask to pick a certain stream/quality"
msgstr "Spørg ikke at vælge en bestemt stream/kvalitet"
# PKC Settings - Playback
msgctxt "#30542"
@ -702,21 +676,6 @@ msgctxt "#30545"
msgid "Force transcode pictures"
msgstr "Transcode billeder"
# PKC Settings - Playback
msgctxt "#30546"
msgid "Pick the first video if several versions are present"
msgstr ""
# PKC Settings - Playback
msgctxt "#30547"
msgid "Who picks the audio stream on playback start?"
msgstr ""
# PKC Settings - Playback
msgctxt "#30548"
msgid "Who picks subtitles on playback start?"
msgstr ""
# Welcome to Plex notification
msgctxt "#33000"
msgid "Welcome"
@ -737,17 +696,6 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Serveren er online"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -987,11 +935,6 @@ msgstr ""
# PKC Settings - Customize Paths
msgctxt "#39036"
msgid "Escape special characters in path (e.g. space to %20)"
msgstr "Escape special characters in path (e.g. space to %20)"
# PKC Settings - Customize Paths
msgctxt "#39090"
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
msgstr ""
# PKC Settings - Customize Paths
@ -1099,15 +1042,19 @@ msgstr "Søg efter Plex Server"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Brugt af Sync og når du forsøger at direkte spille"
# PKC Settings, category name
msgctxt "#39057"
msgid "Customize Paths"
msgstr "Tilpasse stier"
# PKC Settings - Appearance Tweaks
msgctxt "#39058"
msgid "Extend Plex TV Series \"On Deck\" view to all shows"
msgstr "Udvide Plex TV serie \"Senest Tilføjet\" Til vis alle shows"
# PKC Settings - Appearance Tweaks
msgctxt "#39059"
msgid "Recently Added: Append show title to episode"
@ -1148,8 +1095,11 @@ msgstr "Gennemtving genindlæsning af Kodi skin når afspilning stopper"
# PKC Settings - Appearance Tweaks
msgctxt "#39066"
msgid "Recently Added: Also show already watched movies"
msgstr "Nyligt tilføjede: Vis også allerede sete film"
msgid ""
"Recently Added: Also show already watched movies (Refresh Plex "
"playlist/nodes!)"
msgstr ""
"Senest tilføjet: Vis allerede set film (Opdater Plex spilleliste/noder!)"
# PKC Settings - Connection
msgctxt "#39067"
@ -1176,11 +1126,6 @@ msgctxt "#39071"
msgid "Current plex.tv status:"
msgstr "Nuværende plex.tv status:"
# PKC Settings - Connection
msgctxt "#39072"
msgid "Background sync connection:"
msgstr ""
# PKC Settings, category name
msgctxt "#39073"
msgid "Appearance Tweaks"
@ -1191,10 +1136,10 @@ msgctxt "#39074"
msgid "TV Shows"
msgstr "TV-udsendelser"
# PKC Settings - Sync
# PKC Settings - Playback
msgctxt "#39075"
msgid "Verify access to media files while synching"
msgstr ""
msgid "Always use default Plex subtitle if possible"
msgstr "Brug altid standard Plex undertekst hvis muligt"
# Pop-up during initial sync
msgctxt "#39076"
@ -1207,8 +1152,8 @@ msgstr ""
# PKC Settings - Appearance Tweaks
msgctxt "#39077"
msgid "Maximum number of videos to show in widgets"
msgstr "Maximum antal af videoer vist i widgets"
msgid "Number of PMS items to show in widgets (e.g. \"On Deck\")"
msgstr "Antallet af PMS elementer at vise i widgets (fx \"Igangværende\")"
# PKC Settings - Plex
msgctxt "#39078"
@ -1249,43 +1194,11 @@ msgstr "Direct Paths"
# Dialog for manually entering PMS
msgctxt "#39083"
msgid "Enter PMS IP or URL"
msgstr "PMS IP eller URL"
msgstr ""
# Dialog for manually entering PMS
msgctxt "#39084"
msgid "Enter PMS port"
msgstr "PMS port"
# PKC settings - Appearance Tweaks
msgctxt "#39085"
msgid "Reload Kodi node files to apply all the settings below"
msgstr ""
"Reload Kodi node filer for alle indstillinger\n"
"nedeunder"
# PKC Settings - Connection - Background sync connection status
msgctxt "#39089"
msgid "Alexa connection status:"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39091"
msgid "Timeout - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39092"
msgid "IOError - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39093"
msgid "Suspended - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39094"
msgid "Managed Plex User - not connected"
msgstr ""
msgctxt "#39200"
@ -1340,14 +1253,16 @@ msgstr "Se senere"
# e.g. the PMS' name
msgctxt "#39213"
msgid "{0} offline"
msgstr "{0} Offline"
msgstr ""
msgctxt "#39215"
msgid "Enter your Plex Media Server's IP or URL, Examples are:"
msgstr "Indtast din Plex Media Server IP eller URL, eksempler er:"
msgctxt "#39217"
msgid "Use HTTPS (SSL) connections? Answer should probably be yes."
msgid ""
"Use HTTPS (SSL) connections? With Kodi 18 or later, HTTPS will likely not "
"work!"
msgstr ""
msgctxt "#39218"
@ -1400,7 +1315,7 @@ msgstr "Logget på plex.tv"
# Message in the PKC settings to display the plex.tv username
msgctxt "#39228"
msgid "Plex admin user"
msgstr "Plex admin bruger"
msgstr ""
# Error message if user could not log in; the actual user name will be
# appended at the end of the string
@ -1411,12 +1326,12 @@ msgstr "Login fejlede med plex.tv for bruger"
# Message in the PKC settings to display the plex.tv username
msgctxt "#39230"
msgid "Logged in Plex home user"
msgstr "Log ind Plex home bruger"
msgstr ""
# Message in the PKC settings to change the logged in Plex home user
msgctxt "#39231"
msgid "Change logged in Plex home user"
msgstr "Ændre loggede ind Plex home brugere"
msgstr ""
msgctxt "#39250"
msgid ""
@ -1532,10 +1447,6 @@ msgctxt "#39501"
msgid "Collections"
msgstr "Samlinger"
msgctxt "#39502"
msgid "PKC On Deck (faster)"
msgstr "PKC On Deck (hurtigere)"
msgctxt "#39600"
msgid ""
"Are you sure you want to reset your local Kodi database? A re-sync of the "
@ -1594,10 +1505,11 @@ msgctxt "#39705"
msgid "Use at your own risk"
msgstr "Brug på eget ansvar"
# If user gets prompted to choose between several subtitles to burn in
# If user gets prompted to choose between several subtitles. Leave the number
# one at the beginning of the string!
msgctxt "#39706"
msgid "Don't burn-in any subtitle"
msgstr ""
msgid "1 No subtitles"
msgstr "1No undertekster"
# If user gets prompted to choose between several audio/subtitle tracks and
# language is unknown
@ -1651,8 +1563,8 @@ msgstr "Synkronisér"
# Shown during sync process
msgctxt "#39715"
msgid "Synching playlists"
msgstr "Synkronisér playlister"
msgid "items"
msgstr "elementer"
# Error message if an xml, e.g. advancedsettings.xml cannot be parsed (xml is
# screwed up; formated the wrong way). Do NOT replace {0} and {1}!

View File

@ -1,6 +1,6 @@
# XBMC Media Center language file
# Translators:
# Croneter None <croneter@gmail.com>, 2021
# Croneter None <croneter@gmail.com>, 2019
#
msgid ""
msgstr ""
@ -8,7 +8,7 @@ msgstr ""
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
"Last-Translator: Croneter None <croneter@gmail.com>, 2021\n"
"Last-Translator: Croneter None <croneter@gmail.com>, 2019\n"
"Language-Team: German (Germany) (https://www.transifex.com/croneter/teams/73837/de_DE/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -44,17 +44,6 @@ msgstr ""
"Achtung: Kodi Einstellung \"Nächsten Video automatisch abspielen\" ist "
"aktiviert. Dies kann PKC stören. Deaktivieren?"
msgctxt "#30004"
msgid ""
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
"random password automatically if you haven't done so already. Please confirm"
" the next dialog that you want to enable the webserver now with Yes."
msgstr ""
"Der Kodi-Webserver wird für Artwork-Caching benötigt. PKC hat bereits "
"automatisch ein starkes, zufälliges Passwort gesetzt, falls Sie dies nicht "
"schon getan haben. Bitte bestätigen Sie den nächsten Dialog mit Ja, dass der"
" Webserver aktiviert werden kann."
msgctxt "#30005"
msgid "Username: "
msgstr "Benutzername: "
@ -170,17 +159,6 @@ msgctxt "#30028"
msgid "PKC-only image caching completed"
msgstr "PKC Bilder-Caching beendet"
# Warning shown when PKC switches to the Kodi default skin Estuary
msgctxt "#30029"
msgid ""
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
"database resets. Continue?"
msgstr ""
"Um ein reibungsloses PlexKodiConnect-Erlebnis zu gewährleisten, wird "
"DRINGEND empfohlen, für die Ersteinrichtung und für mögliche Datenbank-"
"Resets den Standard-Skin \"Estuary\" von Kodi zu verwenden. Weiterfahren?"
msgctxt "#30030"
msgid "Port Number"
msgstr "Portnummer"
@ -280,11 +258,6 @@ msgctxt "#30160"
msgid "Video Quality if Transcoding necessary"
msgstr "Videoqualität falls Transkodierung nötig"
msgctxt "#30161"
msgid "Auto-adjust transcoding quality (deactivate for Chromecast)"
msgstr ""
"Transcoding Qualität automatisch anpassen (deaktivieren für Chromecast)"
msgctxt "#30165"
msgid "Direct Play"
msgstr "Direkte Wiedergabe"
@ -624,11 +597,6 @@ msgctxt "#30524"
msgid "Select Plex libraries to sync"
msgstr "Zu synchronisierende Plex Bibliotheken auswählen"
# PKC Settings - Playback
msgctxt "#30525"
msgid "Skip intro"
msgstr "Intro überspringen"
# PKC Settings - Playback
msgctxt "#30527"
msgid "Ignore specials in next episodes"
@ -692,9 +660,8 @@ msgstr "FanArtTV Bilder für Film-Sets/Collections herunterladen"
# PKC Settings - Playback
msgctxt "#30541"
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
msgstr ""
"Transkodierung: Plex-Standards für Audio- und Untertitel-Streams verwenden"
msgid "Don't ask to pick a certain stream/quality"
msgstr "Nicht nachfragen, welcher Stream oder Qualität gespielt werden soll"
# PKC Settings - Playback
msgctxt "#30542"
@ -715,21 +682,6 @@ msgctxt "#30545"
msgid "Force transcode pictures"
msgstr "Bilder immer transkodieren"
# PKC Settings - Playback
msgctxt "#30546"
msgid "Pick the first video if several versions are present"
msgstr "Ersten Videostream wählen, wenn mehrere Versionen vorhanden sind"
# PKC Settings - Playback
msgctxt "#30547"
msgid "Who picks the audio stream on playback start?"
msgstr "Wer wählt den Audiotrack beim Start der Wiedergabe?"
# PKC Settings - Playback
msgctxt "#30548"
msgid "Who picks subtitles on playback start?"
msgstr "Wer wählt Untertitel beim Start der Wiedergabe?"
# Welcome to Plex notification
msgctxt "#33000"
msgid "Welcome"
@ -750,17 +702,6 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Server ist online"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr "PMS muss transkodieren"
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr "PMS muss Direct Streamen"
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -1009,11 +950,6 @@ msgctxt "#39036"
msgid "Escape special characters in path (e.g. space to %20)"
msgstr "Sonderzeichen im Pfad escapen (z.B. Leerzeichen zu %20)"
# PKC Settings - Customize Paths
msgctxt "#39090"
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
msgstr "Sichere Zeichen für http(s), dav(s) und (s)ftp urls"
# PKC Settings - Customize Paths
msgctxt "#39037"
msgid "Original Plex MOVIE path to replace:"
@ -1119,17 +1055,20 @@ msgstr "Suche Plex Server"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgid "Used by Sync and when attempting to Direct Play"
msgstr ""
"Verwendet für Synchronisierung und Direct Paths. Bei Änderungen Kodi neu "
"starten!"
"Verwendet für Synchronisierung sowie beim Versuch, Direct Play zu nutzen"
# PKC Settings, category name
msgctxt "#39057"
msgid "Customize Paths"
msgstr "Pfade ändern"
# PKC Settings - Appearance Tweaks
msgctxt "#39058"
msgid "Extend Plex TV Series \"On Deck\" view to all shows"
msgstr "Standard Plex Ansicht \"Aktuell\" auf alle TV Shows erweitern"
# PKC Settings - Appearance Tweaks
msgctxt "#39059"
msgid "Recently Added: Append show title to episode"
@ -1169,8 +1108,12 @@ msgstr "Kodi Skin nach Playback-Stop neu laden"
# PKC Settings - Appearance Tweaks
msgctxt "#39066"
msgid "Recently Added: Also show already watched movies"
msgstr "\"Zuletzt hinzugefügt\": gesehene Filme anzeigen"
msgid ""
"Recently Added: Also show already watched movies (Refresh Plex "
"playlist/nodes!)"
msgstr ""
"\"Zuletzt hinzugefügt\": gesehene Filme anzeigen (Plex Playlisten und Nodes "
"zurücksetzen!)"
# PKC Settings - Connection
msgctxt "#39067"
@ -1197,11 +1140,6 @@ msgctxt "#39071"
msgid "Current plex.tv status:"
msgstr "Aktueller plex.tv Status:"
# PKC Settings - Connection
msgctxt "#39072"
msgid "Background sync connection:"
msgstr "Verbindungsstatus Hintergrund-Synchronisation:"
# PKC Settings, category name
msgctxt "#39073"
msgid "Appearance Tweaks"
@ -1212,10 +1150,10 @@ msgctxt "#39074"
msgid "TV Shows"
msgstr "Serien"
# PKC Settings - Sync
# PKC Settings - Playback
msgctxt "#39075"
msgid "Verify access to media files while synching"
msgstr "Zugriff auf Mediendateien während der Synchronisierung überprüfen"
msgid "Always use default Plex subtitle if possible"
msgstr "Falls möglich, Plex Standard-Untertitel anzeigen"
# Pop-up during initial sync
msgctxt "#39076"
@ -1228,8 +1166,8 @@ msgstr ""
# PKC Settings - Appearance Tweaks
msgctxt "#39077"
msgid "Maximum number of videos to show in widgets"
msgstr "Maximale Anzahl anzuzeigende Videos in Widgets"
msgid "Number of PMS items to show in widgets (e.g. \"On Deck\")"
msgstr "Anzahl anzuzeigender PMS Einträge in Widgets (z.B. \"Aktuell\")"
# PKC Settings - Plex
msgctxt "#39078"
@ -1277,36 +1215,6 @@ msgctxt "#39084"
msgid "Enter PMS port"
msgstr "PMS Port eingeben"
# PKC settings - Appearance Tweaks
msgctxt "#39085"
msgid "Reload Kodi node files to apply all the settings below"
msgstr "Kodi neu laden um Einstellungen unten zu übernehmen"
# PKC Settings - Connection - Background sync connection status
msgctxt "#39089"
msgid "Alexa connection status:"
msgstr "Alexa Verbindungsstatus:"
# PKC Settings - Connection - Background sync connection status
msgctxt "#39091"
msgid "Timeout - not connected"
msgstr "Timeout - nicht verbunden"
# PKC Settings - Connection - Background sync connection status
msgctxt "#39092"
msgid "IOError - not connected"
msgstr "IOError - nicht verbunden"
# PKC Settings - Connection - Background sync connection status
msgctxt "#39093"
msgid "Suspended - not connected"
msgstr "Angehalten - nicht verbunden"
# PKC Settings - Connection - Background sync connection status
msgctxt "#39094"
msgid "Managed Plex User - not connected"
msgstr "Managed Plex User - nicht verbunden"
msgctxt "#39200"
msgid "Log-out Plex Home User "
msgstr "Plex Home Benutzer abmelden: "
@ -1368,8 +1276,12 @@ msgid "Enter your Plex Media Server's IP or URL, Examples are:"
msgstr "Plex Media Server IP oder URL eingeben. Zum Beispiel:"
msgctxt "#39217"
msgid "Use HTTPS (SSL) connections? Answer should probably be yes."
msgstr "HTTPS (SSL) verwenden? Die Antwort sollte wahrscheinlich ja sein."
msgid ""
"Use HTTPS (SSL) connections? With Kodi 18 or later, HTTPS will likely not "
"work!"
msgstr ""
"HTTPS (SSL) Verbindungen nutzen? Dies funktioniert u.U. nicht mit Kodi 18 "
"oder späteren Versionen!"
msgctxt "#39218"
msgid "Error contacting PMS"
@ -1558,10 +1470,6 @@ msgctxt "#39501"
msgid "Collections"
msgstr "Kollektionen"
msgctxt "#39502"
msgid "PKC On Deck (faster)"
msgstr "PKC Aktuell (schneller)"
msgctxt "#39600"
msgid ""
"Are you sure you want to reset your local Kodi database? A re-sync of the "
@ -1622,10 +1530,11 @@ msgctxt "#39705"
msgid "Use at your own risk"
msgstr "Benutzung auf eigene Gefahr"
# If user gets prompted to choose between several subtitles to burn in
# If user gets prompted to choose between several subtitles. Leave the number
# one at the beginning of the string!
msgctxt "#39706"
msgid "Don't burn-in any subtitle"
msgstr "Keinen Untertitel einbrennen"
msgid "1 No subtitles"
msgstr "1 Untertitel deaktivieren"
# If user gets prompted to choose between several audio/subtitle tracks and
# language is unknown
@ -1679,8 +1588,8 @@ msgstr "Sync"
# Shown during sync process
msgctxt "#39715"
msgid "Synching playlists"
msgstr "Synchronisiere Wiedergabelisten"
msgid "items"
msgstr "Einträge"
# Error message if an xml, e.g. advancedsettings.xml cannot be parsed (xml is
# screwed up; formated the wrong way). Do NOT replace {0} and {1}!

File diff suppressed because it is too large Load Diff

View File

@ -36,10 +36,6 @@ msgctxt "#30003"
msgid "Warning: Kodi setting \"Play next video automatically\" is enabled. This could break PKC. Deactivate?"
msgstr ""
msgctxt "#30004"
msgid "The Kodi webserver is needed for artwork caching. PKC already set a strong, random password automatically if you haven't done so already. Please confirm the next dialog that you want to enable the webserver now with Yes."
msgstr ""
msgctxt "#30005"
msgid "Username: "
msgstr ""
@ -254,10 +250,6 @@ msgctxt "#30160"
msgid "Video Quality if Transcoding necessary"
msgstr ""
msgctxt "#30161"
msgid "Auto-adjust transcoding quality (deactivate for Chromecast)"
msgstr ""
msgctxt "#30165"
msgid "Direct Play"
msgstr ""
@ -571,10 +563,6 @@ msgctxt "#30524"
msgid "Select Plex libraries to sync"
msgstr ""
# PKC Settings - Playback
msgctxt "#30525"
msgid "Skip intro"
msgstr ""
# PKC Settings - Playback
msgctxt "#30527"
@ -638,7 +626,7 @@ msgstr ""
# PKC Settings - Playback
msgctxt "#30541"
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
msgid "Don't ask to pick a certain stream/quality"
msgstr ""
# PKC Settings - Playback
@ -660,21 +648,6 @@ msgctxt "#30545"
msgid "Force transcode pictures"
msgstr ""
# PKC Settings - Playback
msgctxt "#30546"
msgid "Pick the first video if several versions are present"
msgstr ""
# PKC Settings - Playback
msgctxt "#30547"
msgid "Who picks the audio stream on playback start?"
msgstr ""
# PKC Settings - Playback
msgctxt "#30548"
msgid "Who picks subtitles on playback start?"
msgstr ""
# Welcome to Plex notification
msgctxt "#33000"
msgid "Welcome"
@ -695,16 +668,6 @@ msgctxt "#33003"
msgid "Server is online"
msgstr ""
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -903,11 +866,6 @@ msgctxt "#39036"
msgid "Escape special characters in path (e.g. space to %20)"
msgstr ""
# PKC Settings - Customize Paths
msgctxt "#39090"
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
msgstr ""
# PKC Settings - Customize Paths
msgctxt "#39037"
msgid "Original Plex MOVIE path to replace:"
@ -1005,7 +963,7 @@ msgstr ""
# PKC Settings - Customize paths
msgctxt "#39056"
msgid "Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgid "Used by Sync and when attempting to Direct Play"
msgstr ""
# PKC Settings, category name
@ -1078,11 +1036,6 @@ msgctxt "#39071"
msgid "Current plex.tv status:"
msgstr ""
# PKC Settings - Connection
msgctxt "#39072"
msgid "Background sync connection:"
msgstr ""
# PKC Settings, category name
msgctxt "#39073"
msgid "Appearance Tweaks"
@ -1093,6 +1046,11 @@ msgctxt "#39074"
msgid "TV Shows"
msgstr ""
# PKC Settings - Playback
msgctxt "#39075"
msgid "Always use default Plex subtitle if possible"
msgstr ""
# Pop-up during initial sync
msgctxt "#39076"
msgid "If you use several Plex libraries of one kind, e.g. \"Kids Movies\" and \"Parents Movies\", be sure to check the Wiki: https://goo.gl/JFtQV9"
@ -1100,7 +1058,7 @@ msgstr ""
# PKC Settings - Appearance Tweaks
msgctxt "#39077"
msgid "Maximum number of videos to show in widgets"
msgid "Number of PMS items to show in widgets (e.g. \"On Deck\")"
msgstr ""
# PKC Settings - Plex
@ -1144,31 +1102,6 @@ msgctxt "#39085"
msgid "Reload Kodi node files to apply all the settings below"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39089"
msgid "Alexa connection status:"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39091"
msgid "Timeout - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39092"
msgid "IOError - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39093"
msgid "Suspended - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39094"
msgid "Managed Plex User - not connected"
msgstr ""
msgctxt "#39200"
msgid "Log-out Plex Home User "
msgstr ""
@ -1221,7 +1154,7 @@ msgid "Enter your Plex Media Server's IP or URL, Examples are:"
msgstr ""
msgctxt "#39217"
msgid "Use HTTPS (SSL) connections? Answer should probably be yes."
msgid "Use HTTPS (SSL) connections? With Kodi 18 or later, HTTPS will likely not work!"
msgstr ""
msgctxt "#39218"
@ -1375,10 +1308,6 @@ msgctxt "#39501"
msgid "Collections"
msgstr ""
msgctxt "#39502"
msgid "PKC On Deck (faster)"
msgstr ""
msgctxt "#39600"
msgid "Are you sure you want to reset your local Kodi database? A re-sync of the Plex data will take time afterwards."
msgstr ""
@ -1422,9 +1351,9 @@ msgid "Use at your own risk"
msgstr ""
# If user gets prompted to choose between several subtitles to burn in
# If user gets prompted to choose between several subtitles. Leave the number one at the beginning of the string!
msgctxt "#39706"
msgid "Don't burn-in any subtitle"
msgid "1 No subtitles"
msgstr ""
# If user gets prompted to choose between several audio/subtitle tracks and language is unknown
@ -1469,7 +1398,7 @@ msgstr ""
# Shown during sync process
msgctxt "#39715"
msgid "Synching playlists"
msgid "items"
msgstr ""
# Error message if an xml, e.g. advancedsettings.xml cannot be parsed (xml is screwed up; formated the wrong way). Do NOT replace {0} and {1}!

View File

@ -1,6 +1,6 @@
# XBMC Media Center language file
# Translators:
# Croneter None <croneter@gmail.com>, 2020
# Croneter None <croneter@gmail.com>, 2019
#
msgid ""
msgstr ""
@ -8,7 +8,7 @@ msgstr ""
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
"Last-Translator: Croneter None <croneter@gmail.com>, 2020\n"
"Last-Translator: Croneter None <croneter@gmail.com>, 2019\n"
"Language-Team: Spanish (Argentina) (https://www.transifex.com/croneter/teams/73837/es_AR/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -44,13 +44,6 @@ msgstr ""
"Advertencia: El ajuste de Kodi \"Reproducir el siguiente video "
"automáticamente\" está activado. Esto puede dañar PKC. ¿Desactivar?"
msgctxt "#30004"
msgid ""
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
"random password automatically if you haven't done so already. Please confirm"
" the next dialog that you want to enable the webserver now with Yes."
msgstr ""
msgctxt "#30005"
msgid "Username: "
msgstr "Usuario: "
@ -171,14 +164,6 @@ msgctxt "#30028"
msgid "PKC-only image caching completed"
msgstr "El caché de imágenes solo-PKC fue completado"
# Warning shown when PKC switches to the Kodi default skin Estuary
msgctxt "#30029"
msgid ""
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
"database resets. Continue?"
msgstr ""
msgctxt "#30030"
msgid "Port Number"
msgstr "Número de puerto"
@ -278,10 +263,6 @@ msgctxt "#30160"
msgid "Video Quality if Transcoding necessary"
msgstr "Calidad de vídeo si es necesario Transcodificar"
msgctxt "#30161"
msgid "Auto-adjust transcoding quality (deactivate for Chromecast)"
msgstr ""
msgctxt "#30165"
msgid "Direct Play"
msgstr "Reproducción Directa"
@ -518,7 +499,6 @@ msgstr "Sincronizar el arte de Plex desde PMS (recomendado)"
msgctxt "#30503"
msgid "SSL certificate failed to validate. Please check {0} for solutions."
msgstr ""
"Fallo al validar el certificado SSL. Consulte {0} para ver soluciones."
# PKC Settings, category name
msgctxt "#30506"
@ -616,11 +596,6 @@ msgstr ""
# PKC Settings - Sync Options
msgctxt "#30524"
msgid "Select Plex libraries to sync"
msgstr "Seleccionar librerias de Plex para sincronizar"
# PKC Settings - Playback
msgctxt "#30525"
msgid "Skip intro"
msgstr ""
# PKC Settings - Playback
@ -688,8 +663,8 @@ msgstr "Descargar arte de sagas de FanArtTV"
# PKC Settings - Playback
msgctxt "#30541"
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
msgstr ""
msgid "Don't ask to pick a certain stream/quality"
msgstr "No solicitar elegir un stream o una calidad en particular"
# PKC Settings - Playback
msgctxt "#30542"
@ -710,21 +685,6 @@ msgctxt "#30545"
msgid "Force transcode pictures"
msgstr "Obligar transcodificar fotografías"
# PKC Settings - Playback
msgctxt "#30546"
msgid "Pick the first video if several versions are present"
msgstr ""
# PKC Settings - Playback
msgctxt "#30547"
msgid "Who picks the audio stream on playback start?"
msgstr ""
# PKC Settings - Playback
msgctxt "#30548"
msgid "Who picks subtitles on playback start?"
msgstr ""
# Welcome to Plex notification
msgctxt "#33000"
msgid "Welcome"
@ -745,17 +705,6 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Servidor está en línea"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -970,7 +919,7 @@ msgid ""
"Kodi cannot locate the file %s. Please verify your PKC settings. Stop "
"syncing?"
msgstr ""
"Kodi no puede localizer el archivo %s. Por favor verificar sus ajustes de "
"Kodi no puede localizer el archive %s. Por favoer verificar sus ajustes de "
"PKC. ¿Detener la sincronización?"
# Pop-up on initial sync
@ -999,12 +948,7 @@ msgstr ""
# PKC Settings - Customize Paths
msgctxt "#39036"
msgid "Escape special characters in path (e.g. space to %20)"
msgstr "Escapar caracteres especiales en la ruta (p. ej. espacio a %20)"
# PKC Settings - Customize Paths
msgctxt "#39090"
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
msgstr "Caracteres seguros para urls http(s), dav(s) y (s)ftp"
msgstr "Escapar caracteres especiales en la ruta (i.e. espacio a %20)"
# PKC Settings - Customize Paths
msgctxt "#39037"
@ -1112,15 +1056,19 @@ msgstr "Buscando servidor Plex"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Utilizado por la Sincronización al intentar Reproducción Directa"
# PKC Settings, category name
msgctxt "#39057"
msgid "Customize Paths"
msgstr "Personalizar rutas"
# PKC Settings - Appearance Tweaks
msgctxt "#39058"
msgid "Extend Plex TV Series \"On Deck\" view to all shows"
msgstr "Extender vista de series \"On Deck\" de Plex a todoas las series"
# PKC Settings - Appearance Tweaks
msgctxt "#39059"
msgid "Recently Added: Append show title to episode"
@ -1161,8 +1109,12 @@ msgstr "Refrescar el skin de Kodi al detener la reproducción"
# PKC Settings - Appearance Tweaks
msgctxt "#39066"
msgid "Recently Added: Also show already watched movies"
msgstr "Recién Añadido: Mostrar tambien películas ya vistas"
msgid ""
"Recently Added: Also show already watched movies (Refresh Plex "
"playlist/nodes!)"
msgstr ""
"Añadidos Recientemente: También mostrar películas ya vistas (¡Actualize las "
"listas de reproducción/nodos Plex!)"
# PKC Settings - Connection
msgctxt "#39067"
@ -1189,11 +1141,6 @@ msgctxt "#39071"
msgid "Current plex.tv status:"
msgstr "Estado actual de plex.tv:"
# PKC Settings - Connection
msgctxt "#39072"
msgid "Background sync connection:"
msgstr ""
# PKC Settings, category name
msgctxt "#39073"
msgid "Appearance Tweaks"
@ -1204,10 +1151,10 @@ msgctxt "#39074"
msgid "TV Shows"
msgstr "Series"
# PKC Settings - Sync
# PKC Settings - Playback
msgctxt "#39075"
msgid "Verify access to media files while synching"
msgstr ""
msgid "Always use default Plex subtitle if possible"
msgstr "Siempre use subtítulos de Plex por defecto si es posible"
# Pop-up during initial sync
msgctxt "#39076"
@ -1221,8 +1168,9 @@ msgstr ""
# PKC Settings - Appearance Tweaks
msgctxt "#39077"
msgid "Maximum number of videos to show in widgets"
msgstr "Número máximo de videos a mostrar en los widgets"
msgid "Number of PMS items to show in widgets (e.g. \"On Deck\")"
msgstr ""
"Número de elementos del PMS a mostrar en widgets (por ejemplo \"On Deck\")"
# PKC Settings - Plex
msgctxt "#39078"
@ -1264,41 +1212,11 @@ msgstr "Direct Paths"
# Dialog for manually entering PMS
msgctxt "#39083"
msgid "Enter PMS IP or URL"
msgstr "Introduzca la URL o IP del PMS"
msgstr ""
# Dialog for manually entering PMS
msgctxt "#39084"
msgid "Enter PMS port"
msgstr "Introduzca el puerto del PMS"
# PKC settings - Appearance Tweaks
msgctxt "#39085"
msgid "Reload Kodi node files to apply all the settings below"
msgstr "Recargar Kodi para aplicar todos los ajustes."
# PKC Settings - Connection - Background sync connection status
msgctxt "#39089"
msgid "Alexa connection status:"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39091"
msgid "Timeout - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39092"
msgid "IOError - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39093"
msgid "Suspended - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39094"
msgid "Managed Plex User - not connected"
msgstr ""
msgctxt "#39200"
@ -1353,14 +1271,16 @@ msgstr "Ver Luego"
# e.g. the PMS' name
msgctxt "#39213"
msgid "{0} offline"
msgstr "{0} fuera de linea"
msgstr ""
msgctxt "#39215"
msgid "Enter your Plex Media Server's IP or URL, Examples are:"
msgstr "Escriba el IP o URL de su Plex Media Server, por ejemplo:"
msgctxt "#39217"
msgid "Use HTTPS (SSL) connections? Answer should probably be yes."
msgid ""
"Use HTTPS (SSL) connections? With Kodi 18 or later, HTTPS will likely not "
"work!"
msgstr ""
msgctxt "#39218"
@ -1413,7 +1333,7 @@ msgstr "Conectado a plex.tv"
# Message in the PKC settings to display the plex.tv username
msgctxt "#39228"
msgid "Plex admin user"
msgstr "Administrador de Plex"
msgstr ""
# Error message if user could not log in; the actual user name will be
# appended at the end of the string
@ -1424,12 +1344,12 @@ msgstr "Inicio de session con plex.tv falló para el usuario"
# Message in the PKC settings to display the plex.tv username
msgctxt "#39230"
msgid "Logged in Plex home user"
msgstr "Usuario de Plex home registrado"
msgstr ""
# Message in the PKC settings to change the logged in Plex home user
msgctxt "#39231"
msgid "Change logged in Plex home user"
msgstr "Cambiar usuario de Plex"
msgstr ""
msgctxt "#39250"
msgid ""
@ -1505,7 +1425,7 @@ msgid ""
"The current Kodi version is not supported by PKC. Please consult the Plex "
"forum."
msgstr ""
"La versión actual de Kodi no está soportada por PKC. Por favor consultar el"
"La version actual de Kodi no está soportada por PKC. Por favor consultar el"
" fórum de Plex."
msgctxt "#39405"
@ -1547,10 +1467,6 @@ msgctxt "#39501"
msgid "Collections"
msgstr "Sagas"
msgctxt "#39502"
msgid "PKC On Deck (faster)"
msgstr "On Deck de PKC (más rápido)"
msgctxt "#39600"
msgid ""
"Are you sure you want to reset your local Kodi database? A re-sync of the "
@ -1609,10 +1525,11 @@ msgctxt "#39705"
msgid "Use at your own risk"
msgstr "Usar a su propio riesgo"
# If user gets prompted to choose between several subtitles to burn in
# If user gets prompted to choose between several subtitles. Leave the number
# one at the beginning of the string!
msgctxt "#39706"
msgid "Don't burn-in any subtitle"
msgstr ""
msgid "1 No subtitles"
msgstr "1 Sin subtitulos"
# If user gets prompted to choose between several audio/subtitle tracks and
# language is unknown
@ -1666,8 +1583,8 @@ msgstr "Sincronizar"
# Shown during sync process
msgctxt "#39715"
msgid "Synching playlists"
msgstr "Sincronizando listas"
msgid "items"
msgstr "objetos"
# Error message if an xml, e.g. advancedsettings.xml cannot be parsed (xml is
# screwed up; formated the wrong way). Do NOT replace {0} and {1}!
@ -1691,8 +1608,8 @@ msgid ""
"Do you want to replace your custom user ratings with an indicator of how "
"many versions of a media item you posses?"
msgstr ""
"¿Quiere reemplazar su valoración personalizada por cuántas versiones posee "
"de un elemento de medios?"
"¿Quiere reemplazar su valoración personalizada con cuántas versione posee de"
" un elemento de medios?"
# In PKC Settings under Sync
msgctxt "#39719"

View File

@ -2,7 +2,6 @@
# Translators:
# Dani <danichispa@gmail.com>, 2019
# Bartolome Soriano <bsoriano@gmail.com>, 2019
# Croneter None <croneter@gmail.com>, 2020
#
msgid ""
msgstr ""
@ -10,7 +9,7 @@ msgstr ""
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
"Last-Translator: Croneter None <croneter@gmail.com>, 2020\n"
"Last-Translator: Bartolome Soriano <bsoriano@gmail.com>, 2019\n"
"Language-Team: Spanish (Spain) (https://www.transifex.com/croneter/teams/73837/es_ES/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -46,13 +45,6 @@ msgstr ""
"Advertencia: El ajuste de Kodi \"Reproducir el siguiente video "
"automáticamente\" está activado. Esto puede dañar PKC. ¿Desactivar?"
msgctxt "#30004"
msgid ""
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
"random password automatically if you haven't done so already. Please confirm"
" the next dialog that you want to enable the webserver now with Yes."
msgstr ""
msgctxt "#30005"
msgid "Username: "
msgstr "Usuario: "
@ -173,14 +165,6 @@ msgctxt "#30028"
msgid "PKC-only image caching completed"
msgstr "El caché de imágenes solo-PKC fue completado"
# Warning shown when PKC switches to the Kodi default skin Estuary
msgctxt "#30029"
msgid ""
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
"database resets. Continue?"
msgstr ""
msgctxt "#30030"
msgid "Port Number"
msgstr "Número de puerto"
@ -280,10 +264,6 @@ msgctxt "#30160"
msgid "Video Quality if Transcoding necessary"
msgstr "Calidad de vídeo si es necesario Transcodificar"
msgctxt "#30161"
msgid "Auto-adjust transcoding quality (deactivate for Chromecast)"
msgstr ""
msgctxt "#30165"
msgid "Direct Play"
msgstr "Reproducción Directa"
@ -520,7 +500,6 @@ msgstr "Sincronizar el arte de Plex desde PMS (recomendado)"
msgctxt "#30503"
msgid "SSL certificate failed to validate. Please check {0} for solutions."
msgstr ""
"Fallo al validar el certificado SSL. Consulte {0} para ver soluciones."
# PKC Settings, category name
msgctxt "#30506"
@ -618,11 +597,6 @@ msgstr ""
# PKC Settings - Sync Options
msgctxt "#30524"
msgid "Select Plex libraries to sync"
msgstr "Seleccionar librerias de Plex para sincronizar"
# PKC Settings - Playback
msgctxt "#30525"
msgid "Skip intro"
msgstr ""
# PKC Settings - Playback
@ -690,8 +664,8 @@ msgstr "Descargar arte de sagas de FanArtTV"
# PKC Settings - Playback
msgctxt "#30541"
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
msgstr ""
msgid "Don't ask to pick a certain stream/quality"
msgstr "No solicitar elegir un stream o una calidad en particular"
# PKC Settings - Playback
msgctxt "#30542"
@ -712,21 +686,6 @@ msgctxt "#30545"
msgid "Force transcode pictures"
msgstr "Obligar transcodificar fotografías"
# PKC Settings - Playback
msgctxt "#30546"
msgid "Pick the first video if several versions are present"
msgstr ""
# PKC Settings - Playback
msgctxt "#30547"
msgid "Who picks the audio stream on playback start?"
msgstr ""
# PKC Settings - Playback
msgctxt "#30548"
msgid "Who picks subtitles on playback start?"
msgstr ""
# Welcome to Plex notification
msgctxt "#33000"
msgid "Welcome"
@ -747,17 +706,6 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Servidor está en línea"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -972,7 +920,7 @@ msgid ""
"Kodi cannot locate the file %s. Please verify your PKC settings. Stop "
"syncing?"
msgstr ""
"Kodi no puede localizer el archivo %s. Por favor verificar sus ajustes de "
"Kodi no puede localizer el archive %s. Por favoer verificar sus ajustes de "
"PKC. ¿Detener la sincronización?"
# Pop-up on initial sync
@ -1001,12 +949,7 @@ msgstr ""
# PKC Settings - Customize Paths
msgctxt "#39036"
msgid "Escape special characters in path (e.g. space to %20)"
msgstr "Escapar caracteres especiales en la ruta (p. ej. espacio a %20)"
# PKC Settings - Customize Paths
msgctxt "#39090"
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
msgstr "Caracteres seguros para urls http(s), dav(s) y (s)ftp"
msgstr "Escapar caracteres especiales en la ruta (i.e. espacio a %20)"
# PKC Settings - Customize Paths
msgctxt "#39037"
@ -1114,15 +1057,19 @@ msgstr "Buscando servidor Plex"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Utilizado por la Sincronización al intentar Reproducción Directa"
# PKC Settings, category name
msgctxt "#39057"
msgid "Customize Paths"
msgstr "Personalizar rutas"
# PKC Settings - Appearance Tweaks
msgctxt "#39058"
msgid "Extend Plex TV Series \"On Deck\" view to all shows"
msgstr "Extender vista de series \"On Deck\" de Plex a todoas las series"
# PKC Settings - Appearance Tweaks
msgctxt "#39059"
msgid "Recently Added: Append show title to episode"
@ -1163,8 +1110,12 @@ msgstr "Refrescar el skin de Kodi al detener la reproducción"
# PKC Settings - Appearance Tweaks
msgctxt "#39066"
msgid "Recently Added: Also show already watched movies"
msgstr "Recién Añadido: Mostrar tambien películas ya vistas"
msgid ""
"Recently Added: Also show already watched movies (Refresh Plex "
"playlist/nodes!)"
msgstr ""
"Añadidos Recientemente: También mostrar películas ya vistas (¡Actualize las "
"listas de reproducción/nodos Plex!)"
# PKC Settings - Connection
msgctxt "#39067"
@ -1191,11 +1142,6 @@ msgctxt "#39071"
msgid "Current plex.tv status:"
msgstr "Estado actual de plex.tv:"
# PKC Settings - Connection
msgctxt "#39072"
msgid "Background sync connection:"
msgstr ""
# PKC Settings, category name
msgctxt "#39073"
msgid "Appearance Tweaks"
@ -1206,10 +1152,10 @@ msgctxt "#39074"
msgid "TV Shows"
msgstr "Series"
# PKC Settings - Sync
# PKC Settings - Playback
msgctxt "#39075"
msgid "Verify access to media files while synching"
msgstr ""
msgid "Always use default Plex subtitle if possible"
msgstr "Siempre use subtítulos de Plex por defecto si es posible"
# Pop-up during initial sync
msgctxt "#39076"
@ -1223,8 +1169,9 @@ msgstr ""
# PKC Settings - Appearance Tweaks
msgctxt "#39077"
msgid "Maximum number of videos to show in widgets"
msgstr "Número máximo de videos a mostrar en los widgets"
msgid "Number of PMS items to show in widgets (e.g. \"On Deck\")"
msgstr ""
"Número de elementos del PMS a mostrar en widgets (por ejemplo \"On Deck\")"
# PKC Settings - Plex
msgctxt "#39078"
@ -1266,41 +1213,11 @@ msgstr "Direct Paths"
# Dialog for manually entering PMS
msgctxt "#39083"
msgid "Enter PMS IP or URL"
msgstr "Introduzca la URL o IP del PMS"
msgstr ""
# Dialog for manually entering PMS
msgctxt "#39084"
msgid "Enter PMS port"
msgstr "Introduzca el puerto del PMS"
# PKC settings - Appearance Tweaks
msgctxt "#39085"
msgid "Reload Kodi node files to apply all the settings below"
msgstr "Recargar Kodi para aplicar todos los ajustes."
# PKC Settings - Connection - Background sync connection status
msgctxt "#39089"
msgid "Alexa connection status:"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39091"
msgid "Timeout - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39092"
msgid "IOError - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39093"
msgid "Suspended - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39094"
msgid "Managed Plex User - not connected"
msgstr ""
msgctxt "#39200"
@ -1355,14 +1272,16 @@ msgstr "Ver Luego"
# e.g. the PMS' name
msgctxt "#39213"
msgid "{0} offline"
msgstr "{0} fuera de linea"
msgstr ""
msgctxt "#39215"
msgid "Enter your Plex Media Server's IP or URL, Examples are:"
msgstr "Escriba el IP o URL de su Plex Media Server, por ejemplo:"
msgctxt "#39217"
msgid "Use HTTPS (SSL) connections? Answer should probably be yes."
msgid ""
"Use HTTPS (SSL) connections? With Kodi 18 or later, HTTPS will likely not "
"work!"
msgstr ""
msgctxt "#39218"
@ -1415,7 +1334,7 @@ msgstr "Conectado a plex.tv"
# Message in the PKC settings to display the plex.tv username
msgctxt "#39228"
msgid "Plex admin user"
msgstr "Administrador de Plex"
msgstr ""
# Error message if user could not log in; the actual user name will be
# appended at the end of the string
@ -1426,12 +1345,12 @@ msgstr "Inicio de session con plex.tv falló para el usuario"
# Message in the PKC settings to display the plex.tv username
msgctxt "#39230"
msgid "Logged in Plex home user"
msgstr "Usuario de Plex home registrado"
msgstr ""
# Message in the PKC settings to change the logged in Plex home user
msgctxt "#39231"
msgid "Change logged in Plex home user"
msgstr "Cambiar usuario de Plex"
msgstr ""
msgctxt "#39250"
msgid ""
@ -1507,7 +1426,7 @@ msgid ""
"The current Kodi version is not supported by PKC. Please consult the Plex "
"forum."
msgstr ""
"La versión actual de Kodi no está soportada por PKC. Por favor consultar el"
"La version actual de Kodi no está soportada por PKC. Por favor consultar el"
" fórum de Plex."
msgctxt "#39405"
@ -1549,10 +1468,6 @@ msgctxt "#39501"
msgid "Collections"
msgstr "Sagas"
msgctxt "#39502"
msgid "PKC On Deck (faster)"
msgstr "On Deck de PKC (más rápido)"
msgctxt "#39600"
msgid ""
"Are you sure you want to reset your local Kodi database? A re-sync of the "
@ -1611,10 +1526,11 @@ msgctxt "#39705"
msgid "Use at your own risk"
msgstr "Usar a su propio riesgo"
# If user gets prompted to choose between several subtitles to burn in
# If user gets prompted to choose between several subtitles. Leave the number
# one at the beginning of the string!
msgctxt "#39706"
msgid "Don't burn-in any subtitle"
msgstr ""
msgid "1 No subtitles"
msgstr "1 Sin subtitulos"
# If user gets prompted to choose between several audio/subtitle tracks and
# language is unknown
@ -1668,8 +1584,8 @@ msgstr "Sincronizar"
# Shown during sync process
msgctxt "#39715"
msgid "Synching playlists"
msgstr "Sincronizando listas"
msgid "items"
msgstr "objetos"
# Error message if an xml, e.g. advancedsettings.xml cannot be parsed (xml is
# screwed up; formated the wrong way). Do NOT replace {0} and {1}!
@ -1693,8 +1609,8 @@ msgid ""
"Do you want to replace your custom user ratings with an indicator of how "
"many versions of a media item you posses?"
msgstr ""
"¿Quiere reemplazar su valoración personalizada por cuántas versiones posee "
"de un elemento de medios?"
"¿Quiere reemplazar su valoración personalizada con cuántas versione posee de"
" un elemento de medios?"
# In PKC Settings under Sync
msgctxt "#39719"

View File

@ -1,6 +1,6 @@
# XBMC Media Center language file
# Translators:
# Croneter None <croneter@gmail.com>, 2020
# Croneter None <croneter@gmail.com>, 2019
#
msgid ""
msgstr ""
@ -8,7 +8,7 @@ msgstr ""
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
"Last-Translator: Croneter None <croneter@gmail.com>, 2020\n"
"Last-Translator: Croneter None <croneter@gmail.com>, 2019\n"
"Language-Team: Spanish (Mexico) (https://www.transifex.com/croneter/teams/73837/es_MX/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -44,13 +44,6 @@ msgstr ""
"Advertencia: El ajuste de Kodi \"Reproducir el siguiente video "
"automáticamente\" está activado. Esto puede dañar PKC. ¿Desactivar?"
msgctxt "#30004"
msgid ""
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
"random password automatically if you haven't done so already. Please confirm"
" the next dialog that you want to enable the webserver now with Yes."
msgstr ""
msgctxt "#30005"
msgid "Username: "
msgstr "Usuario: "
@ -171,14 +164,6 @@ msgctxt "#30028"
msgid "PKC-only image caching completed"
msgstr "El caché de imágenes solo-PKC fue completado"
# Warning shown when PKC switches to the Kodi default skin Estuary
msgctxt "#30029"
msgid ""
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
"database resets. Continue?"
msgstr ""
msgctxt "#30030"
msgid "Port Number"
msgstr "Número de puerto"
@ -278,10 +263,6 @@ msgctxt "#30160"
msgid "Video Quality if Transcoding necessary"
msgstr "Calidad de vídeo si es necesario Transcodificar"
msgctxt "#30161"
msgid "Auto-adjust transcoding quality (deactivate for Chromecast)"
msgstr ""
msgctxt "#30165"
msgid "Direct Play"
msgstr "Reproducción Directa"
@ -518,7 +499,6 @@ msgstr "Sincronizar el arte de Plex desde PMS (recomendado)"
msgctxt "#30503"
msgid "SSL certificate failed to validate. Please check {0} for solutions."
msgstr ""
"Fallo al validar el certificado SSL. Consulte {0} para ver soluciones."
# PKC Settings, category name
msgctxt "#30506"
@ -616,11 +596,6 @@ msgstr ""
# PKC Settings - Sync Options
msgctxt "#30524"
msgid "Select Plex libraries to sync"
msgstr "Seleccionar librerias de Plex para sincronizar"
# PKC Settings - Playback
msgctxt "#30525"
msgid "Skip intro"
msgstr ""
# PKC Settings - Playback
@ -688,8 +663,8 @@ msgstr "Descargar arte de sagas de FanArtTV"
# PKC Settings - Playback
msgctxt "#30541"
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
msgstr ""
msgid "Don't ask to pick a certain stream/quality"
msgstr "No solicitar elegir un stream o una calidad en particular"
# PKC Settings - Playback
msgctxt "#30542"
@ -710,21 +685,6 @@ msgctxt "#30545"
msgid "Force transcode pictures"
msgstr "Obligar transcodificar fotografías"
# PKC Settings - Playback
msgctxt "#30546"
msgid "Pick the first video if several versions are present"
msgstr ""
# PKC Settings - Playback
msgctxt "#30547"
msgid "Who picks the audio stream on playback start?"
msgstr ""
# PKC Settings - Playback
msgctxt "#30548"
msgid "Who picks subtitles on playback start?"
msgstr ""
# Welcome to Plex notification
msgctxt "#33000"
msgid "Welcome"
@ -745,17 +705,6 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Servidor está en línea"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -970,7 +919,7 @@ msgid ""
"Kodi cannot locate the file %s. Please verify your PKC settings. Stop "
"syncing?"
msgstr ""
"Kodi no puede localizer el archivo %s. Por favor verificar sus ajustes de "
"Kodi no puede localizer el archive %s. Por favoer verificar sus ajustes de "
"PKC. ¿Detener la sincronización?"
# Pop-up on initial sync
@ -999,12 +948,7 @@ msgstr ""
# PKC Settings - Customize Paths
msgctxt "#39036"
msgid "Escape special characters in path (e.g. space to %20)"
msgstr "Escapar caracteres especiales en la ruta (p. ej. espacio a %20)"
# PKC Settings - Customize Paths
msgctxt "#39090"
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
msgstr "Caracteres seguros para urls http(s), dav(s) y (s)ftp"
msgstr "Escapar caracteres especiales en la ruta (i.e. espacio a %20)"
# PKC Settings - Customize Paths
msgctxt "#39037"
@ -1112,15 +1056,19 @@ msgstr "Buscando servidor Plex"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Utilizado por la Sincronización al intentar Reproducción Directa"
# PKC Settings, category name
msgctxt "#39057"
msgid "Customize Paths"
msgstr "Personalizar rutas"
# PKC Settings - Appearance Tweaks
msgctxt "#39058"
msgid "Extend Plex TV Series \"On Deck\" view to all shows"
msgstr "Extender vista de series \"On Deck\" de Plex a todoas las series"
# PKC Settings - Appearance Tweaks
msgctxt "#39059"
msgid "Recently Added: Append show title to episode"
@ -1161,8 +1109,12 @@ msgstr "Refrescar el skin de Kodi al detener la reproducción"
# PKC Settings - Appearance Tweaks
msgctxt "#39066"
msgid "Recently Added: Also show already watched movies"
msgstr "Recién Añadido: Mostrar tambien películas ya vistas"
msgid ""
"Recently Added: Also show already watched movies (Refresh Plex "
"playlist/nodes!)"
msgstr ""
"Añadidos Recientemente: También mostrar películas ya vistas (¡Actualize las "
"listas de reproducción/nodos Plex!)"
# PKC Settings - Connection
msgctxt "#39067"
@ -1189,11 +1141,6 @@ msgctxt "#39071"
msgid "Current plex.tv status:"
msgstr "Estado actual de plex.tv:"
# PKC Settings - Connection
msgctxt "#39072"
msgid "Background sync connection:"
msgstr ""
# PKC Settings, category name
msgctxt "#39073"
msgid "Appearance Tweaks"
@ -1204,10 +1151,10 @@ msgctxt "#39074"
msgid "TV Shows"
msgstr "Series"
# PKC Settings - Sync
# PKC Settings - Playback
msgctxt "#39075"
msgid "Verify access to media files while synching"
msgstr ""
msgid "Always use default Plex subtitle if possible"
msgstr "Siempre use subtítulos de Plex por defecto si es posible"
# Pop-up during initial sync
msgctxt "#39076"
@ -1221,8 +1168,9 @@ msgstr ""
# PKC Settings - Appearance Tweaks
msgctxt "#39077"
msgid "Maximum number of videos to show in widgets"
msgstr "Número máximo de videos a mostrar en los widgets"
msgid "Number of PMS items to show in widgets (e.g. \"On Deck\")"
msgstr ""
"Número de elementos del PMS a mostrar en widgets (por ejemplo \"On Deck\")"
# PKC Settings - Plex
msgctxt "#39078"
@ -1264,41 +1212,11 @@ msgstr "Direct Paths"
# Dialog for manually entering PMS
msgctxt "#39083"
msgid "Enter PMS IP or URL"
msgstr "Introduzca la URL o IP del PMS"
msgstr ""
# Dialog for manually entering PMS
msgctxt "#39084"
msgid "Enter PMS port"
msgstr "Introduzca el puerto del PMS"
# PKC settings - Appearance Tweaks
msgctxt "#39085"
msgid "Reload Kodi node files to apply all the settings below"
msgstr "Recargar Kodi para aplicar todos los ajustes."
# PKC Settings - Connection - Background sync connection status
msgctxt "#39089"
msgid "Alexa connection status:"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39091"
msgid "Timeout - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39092"
msgid "IOError - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39093"
msgid "Suspended - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39094"
msgid "Managed Plex User - not connected"
msgstr ""
msgctxt "#39200"
@ -1353,14 +1271,16 @@ msgstr "Ver Luego"
# e.g. the PMS' name
msgctxt "#39213"
msgid "{0} offline"
msgstr "{0} fuera de linea"
msgstr ""
msgctxt "#39215"
msgid "Enter your Plex Media Server's IP or URL, Examples are:"
msgstr "Escriba el IP o URL de su Plex Media Server, por ejemplo:"
msgctxt "#39217"
msgid "Use HTTPS (SSL) connections? Answer should probably be yes."
msgid ""
"Use HTTPS (SSL) connections? With Kodi 18 or later, HTTPS will likely not "
"work!"
msgstr ""
msgctxt "#39218"
@ -1413,7 +1333,7 @@ msgstr "Conectado a plex.tv"
# Message in the PKC settings to display the plex.tv username
msgctxt "#39228"
msgid "Plex admin user"
msgstr "Administrador de Plex"
msgstr ""
# Error message if user could not log in; the actual user name will be
# appended at the end of the string
@ -1424,12 +1344,12 @@ msgstr "Inicio de session con plex.tv falló para el usuario"
# Message in the PKC settings to display the plex.tv username
msgctxt "#39230"
msgid "Logged in Plex home user"
msgstr "Usuario de Plex home registrado"
msgstr ""
# Message in the PKC settings to change the logged in Plex home user
msgctxt "#39231"
msgid "Change logged in Plex home user"
msgstr "Cambiar usuario de Plex"
msgstr ""
msgctxt "#39250"
msgid ""
@ -1505,7 +1425,7 @@ msgid ""
"The current Kodi version is not supported by PKC. Please consult the Plex "
"forum."
msgstr ""
"La versión actual de Kodi no está soportada por PKC. Por favor consultar el"
"La version actual de Kodi no está soportada por PKC. Por favor consultar el"
" fórum de Plex."
msgctxt "#39405"
@ -1547,10 +1467,6 @@ msgctxt "#39501"
msgid "Collections"
msgstr "Sagas"
msgctxt "#39502"
msgid "PKC On Deck (faster)"
msgstr "On Deck de PKC (más rápido)"
msgctxt "#39600"
msgid ""
"Are you sure you want to reset your local Kodi database? A re-sync of the "
@ -1609,10 +1525,11 @@ msgctxt "#39705"
msgid "Use at your own risk"
msgstr "Usar a su propio riesgo"
# If user gets prompted to choose between several subtitles to burn in
# If user gets prompted to choose between several subtitles. Leave the number
# one at the beginning of the string!
msgctxt "#39706"
msgid "Don't burn-in any subtitle"
msgstr ""
msgid "1 No subtitles"
msgstr "1 Sin subtitulos"
# If user gets prompted to choose between several audio/subtitle tracks and
# language is unknown
@ -1666,8 +1583,8 @@ msgstr "Sincronizar"
# Shown during sync process
msgctxt "#39715"
msgid "Synching playlists"
msgstr "Sincronizando listas"
msgid "items"
msgstr "objetos"
# Error message if an xml, e.g. advancedsettings.xml cannot be parsed (xml is
# screwed up; formated the wrong way). Do NOT replace {0} and {1}!
@ -1691,8 +1608,8 @@ msgid ""
"Do you want to replace your custom user ratings with an indicator of how "
"many versions of a media item you posses?"
msgstr ""
"¿Quiere reemplazar su valoración personalizada por cuántas versiones posee "
"de un elemento de medios?"
"¿Quiere reemplazar su valoración personalizada con cuántas versione posee de"
" un elemento de medios?"
# In PKC Settings under Sync
msgctxt "#39719"

View File

@ -1,8 +1,7 @@
# XBMC Media Center language file
# Translators:
# Croneter None <croneter@gmail.com>, 2019
# Elixir59, 2019
# Croneter None <croneter@gmail.com>, 2020
# Raph Mell, 2020
#
msgid ""
msgstr ""
@ -10,7 +9,7 @@ msgstr ""
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
"Last-Translator: Raph Mell, 2020\n"
"Last-Translator: Elixir59, 2019\n"
"Language-Team: French (Canada) (https://www.transifex.com/croneter/teams/73837/fr_CA/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -43,15 +42,8 @@ msgid ""
"Warning: Kodi setting \"Play next video automatically\" is enabled. This "
"could break PKC. Deactivate?"
msgstr ""
"Avertissement : Le paramètre Kodi \"Lecture automatique de la vidéo "
"suivante\" est activé. Cela pourrait casser PKC. Désactiver ?"
msgctxt "#30004"
msgid ""
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
"random password automatically if you haven't done so already. Please confirm"
" the next dialog that you want to enable the webserver now with Yes."
msgstr ""
"Attention, les paramètres de Kodi sont réglés pour joueur automatiquement la"
" vidéo suivante. Cette option va casser PlexKodiConnect. Désactiver ?"
msgctxt "#30005"
msgid "Username: "
@ -76,8 +68,8 @@ msgstr "Activer les notifications pour la mise en cache d'images"
msgctxt "#30009"
msgid "Enable image caching during Kodi playback (restart Kodi!)"
msgstr ""
"Activer la mise en cache des images pendant la lecture Kodi (redémarrez "
"Kodi!)"
"Activer la mise en cache des images pendant la lecture Kodi (redémarrez Kodi"
" !)"
# PKC settings - Artwork
msgctxt "#30010"
@ -130,7 +122,7 @@ msgstr "Recherche sur FanartTV terminée"
# PKC settings sync options
msgctxt "#30020"
msgid "Sync Plex playlists (reboot Kodi!)"
msgstr "Synchroniser les playlists de Plex (redémarrez Kodi!)"
msgstr "Synchroniser les playlists de Plex (redémarrez Kodi !)"
# PKC settings sync options
msgctxt "#30021"
@ -172,14 +164,6 @@ msgctxt "#30028"
msgid "PKC-only image caching completed"
msgstr "Mise en cache de images pour PKC-seulement terminée"
# Warning shown when PKC switches to the Kodi default skin Estuary
msgctxt "#30029"
msgid ""
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
"database resets. Continue?"
msgstr ""
msgctxt "#30030"
msgid "Port Number"
msgstr "Numéro de port"
@ -240,7 +224,7 @@ msgstr "Pour les films"
msgctxt "#30125"
msgid "Done"
msgstr "Effectué"
msgstr "L'action a été effectuée"
# Error popup message text
msgctxt "#30128"
@ -279,10 +263,6 @@ msgctxt "#30160"
msgid "Video Quality if Transcoding necessary"
msgstr "Qualité vidéo si un transcodage est nécessaire"
msgctxt "#30161"
msgid "Auto-adjust transcoding quality (deactivate for Chromecast)"
msgstr "Auto-ajuster la qualité du transcodage (désactivé pour Chromecast)"
msgctxt "#30165"
msgid "Direct Play"
msgstr "Lecture directe"
@ -293,7 +273,7 @@ msgstr "Transcodage"
msgctxt "#30170"
msgid "Recently Added TV Shows"
msgstr "Série(s) récemment ajoutée(s)"
msgstr "Série(s) récemment ajoutée"
msgctxt "#30171"
msgid "In Progress TV Shows"
@ -305,7 +285,7 @@ msgstr "Chaînes"
msgctxt "#30174"
msgid "Recently Added"
msgstr "Récemment ajouté"
msgstr "Récemment ajoutée"
msgctxt "#30177"
msgid "In Progress Movies"
@ -457,7 +437,7 @@ msgstr "Evaluer la chanson"
# contextmenu entry
msgctxt "#30408"
msgid "Plex addon settings"
msgstr "Paramètres dextension de plex"
msgstr "Paramètres daddon de plex"
# contextmenu entry
msgctxt "#30409"
@ -481,7 +461,7 @@ msgid ""
"Server?"
msgstr ""
"La suppression n'a pas pu être effectuée. La suppression est-elle activée "
"sur le serveur de médias Plex?"
"sur le serveur de médias Plex ?"
# contextmenu entry
msgctxt "#30415"
@ -567,13 +547,13 @@ msgstr ""
msgctxt "#30514"
msgid "Show all Plex extras instead of immediately playing trailers"
msgstr ""
"Afficher tous les extras Plex au lieu de lire immédiatement les bandes-"
"Afficher tous les extras Plex au lieu de jouer immédiatement les bandes-"
"annonces."
# PKC Settings - Sync Options
msgctxt "#30515"
msgid "Maximum items to request from the server at once"
msgstr "Nombre maximum d'éléments à demander au serveur en même temps"
msgstr "Nombre maximum d'éléments concurrents à demander au serveur"
# PKC Settings, category name
msgctxt "#30516"
@ -593,7 +573,7 @@ msgstr "Activer les bandes-annonces Plex (nécessite PlexPass)"
# PKC Settings - Playback
msgctxt "#30519"
msgid "Ask to play trailers"
msgstr "Demander de lire les bandes-annonces"
msgstr "Demander de jouer les bandes-annonces"
# PKC Settings - Plex
msgctxt "#30520"
@ -616,16 +596,11 @@ msgctxt "#30523"
msgid "Also show sync progress for playstate and user data"
msgstr ""
"Afficher également la progression de la synchronisation pour l'état de "
"lecture et des données utilisateur"
"lecture et les données utilisateur"
# PKC Settings - Sync Options
msgctxt "#30524"
msgid "Select Plex libraries to sync"
msgstr "Sélectionner les bibliothèques Plex à synchroniser"
# PKC Settings - Playback
msgctxt "#30525"
msgid "Skip intro"
msgstr ""
# PKC Settings - Playback
@ -666,8 +641,7 @@ msgstr "Générer un nouvel ID unique Plex (Ex: pour cloner Kodi)"
# PKC Settings - Connection
msgctxt "#30536"
msgid "Users must log in every time Kodi restarts"
msgstr ""
"Les utilisateurs doivent se connecter à chaque fois que Kodi redémarre"
msgstr "Les utilisateurs doivent se connectent chaque fois que Kodi redémarre"
# PKC Settings warning
msgctxt "#30537"
@ -693,8 +667,8 @@ msgstr "Télécharger les affiches des sagas sur FanArtTV"
# PKC Settings - Playback
msgctxt "#30541"
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
msgstr ""
msgid "Don't ask to pick a certain stream/quality"
msgstr "Ne pas demander de choisir un certain flux/qualité"
# PKC Settings - Playback
msgctxt "#30542"
@ -715,21 +689,6 @@ msgctxt "#30545"
msgid "Force transcode pictures"
msgstr "Forcer le transcodage des images"
# PKC Settings - Playback
msgctxt "#30546"
msgid "Pick the first video if several versions are present"
msgstr ""
# PKC Settings - Playback
msgctxt "#30547"
msgid "Who picks the audio stream on playback start?"
msgstr ""
# PKC Settings - Playback
msgctxt "#30548"
msgid "Who picks subtitles on playback start?"
msgstr ""
# Welcome to Plex notification
msgctxt "#33000"
msgid "Welcome"
@ -750,17 +709,6 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Le serveur est en ligne"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr "PMS impose le transcodage"
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr "PMS impose le streaming en direct"
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -788,7 +736,7 @@ msgstr "Choisissez le fichier de sous-titres"
# Dialog before playback
msgctxt "#33016"
msgid "Play trailers?"
msgstr "Lire les bandes-annonces?"
msgstr "Jouer les bandes-annonces ?"
# Error message
msgctxt "#33032"
@ -809,23 +757,23 @@ msgid ""
"Delete file(s) from Plex Server? This will also delete the file(s) from "
"disk!"
msgstr ""
"Supprimer les fichiers du serveur de Plex? Cela va également supprimer le(s)"
" fichier(s) du disque!"
"Supprimer les fichiers du serveur de Plex ? Cela va également supprimer "
"le(s) fichier(s) du disque !"
# PKC Settings - Playback
msgctxt "#39000"
msgid "- Number of trailers to play before a movie"
msgstr "-Nombre de bandes-annonces à lire avant un film"
msgstr "-Nombre de bandes-annonces à jouer avant un film"
# PKC Settings - Playback
msgctxt "#39001"
msgid "Boost audio when transcoding"
msgstr "Augmenter l'audio pendant le transcodage"
msgstr "Booster l'audio pendant le transcodage"
# PKC Settings - Playback
msgctxt "#39002"
msgid "Burnt-in subtitle size"
msgstr "Taille des sous-titres incrustés"
msgstr "Taille des sous-titres incorporés"
# PKC Settings - Sync
msgctxt "#39003"
@ -835,27 +783,27 @@ msgstr "Nombre de processus de téléchargement simultanés"
# PKC Settings - Plex
msgctxt "#39004"
msgid "Enable Plex Companion (restart Kodi!)"
msgstr "Activer le compagnon de Plex (redémarrer Kodi!)"
msgstr "Activer le compagnon de Plex (redémarrage de Kodi !)"
# PKC Settings - Plex
msgctxt "#39005"
msgid "Plex Companion Port (change only if needed)"
msgstr "Plex Companion Port (changement uniquement si nécessaire)"
msgstr "Plex Companion Port (change only if needed)"
# PKC Settings - Plex
msgctxt "#39008"
msgid "Plex Companion: Allows flinging media to Kodi through Plex"
msgstr "Plex Companion: Permet de lancer des médias vers Kodi via Plex"
msgstr "Plex Companion: Allows flinging media to Kodi through Plex"
# Error message
msgctxt "#39009"
msgid "Could not login to plex.tv. Please try signing in again."
msgstr "Impossible de se logguer à plex.tv. Veuillez essayer à nouveau"
msgstr "Could not login to plex.tv. Please try signing in again."
# Error message
msgctxt "#39010"
msgid "Problems connecting to plex.tv. Network or internet issue?"
msgstr "Problèmes de connexion à plex.tv. Problème réseau ou dinternet ?"
msgstr "Problèmes de connexion à plex.tv. Problème de réseau ou dinternet ?"
# Error message
msgctxt "#39011"
@ -875,12 +823,12 @@ msgstr "Pas encore autorisée pour le serveur Plex "
# Error message
msgctxt "#39014"
msgid "Please sign in to plex.tv."
msgstr "Veuillez vous connexter à plex.tv."
msgstr "Sil vous plaît connectez-vous à plex.tv."
# Error message
msgctxt "#39015"
msgid "Problems connecting to server. Pick another server?"
msgstr "Problèmes de connexion au serveur. Choisir un autre serveur?"
msgstr "Problèmes de connexion au serveur. Choisir un autre serveur ?"
# Pop-up on initial sync
msgctxt "#39016"
@ -888,7 +836,7 @@ msgid ""
"Disable Plex music library? (It is HIGHLY recommended to use Plex music only"
" with direct paths for large music libraries. Kodi might crash otherwise)"
msgstr ""
"Désactiver la bibliothèque musicale de Plex? (Il est fortement recommandé "
"Désactiver la bibliothèque musicale de Plex ? (Il est fortement recommandé "
"dutiliser Plex musique seulement avec des chemins daccès directs pour les "
"grandes bibliothèques musicale. Sinon Kodi peut sinterrompre)"
@ -898,8 +846,8 @@ msgid ""
"Would you now like to go to the plugin's settings to fine-tune PKC? You will"
" need to RESTART Kodi!"
msgstr ""
"Voulez-vous maintenant aller dans les paramètres du plugin pour affiner "
"PKC? Vous devrez redémarrer Kodi!"
"Aimeriez-vous maintenant aller dans les paramètres du plugin pour affiner la"
" PKC ? Vous devrez redémarrer Kodi !"
# PKC Settings - Advanced
msgctxt "#39018"
@ -928,12 +876,12 @@ msgstr "local"
msgctxt "#39023"
msgid "Failed to authenticate. Did you login to plex.tv?"
msgstr ""
"Impossible deffectuer l'authentification. Êtes-vous connectez à plex.tv?"
"Impossible deffectuer l'authentification. Êtes-vous connectez à plex.tv ?"
# PKC Settings - Plex
msgctxt "#39025"
msgid "Automatically log into plex.tv on startup"
msgstr "Connecter automatiquement plex.tv au démarrage"
msgstr "Vous connecter automatiquement plex.tv au démarrage"
# PKC Settings - Sync
msgctxt "#39026"
@ -948,11 +896,11 @@ msgid ""
"shares need to use direct paths (e.g. smb://myNAS/mymovie.mkv or "
"\\\\myNAS/mymovie.mkv)!"
msgstr ""
"MISE EN GARDE! Si vous choisissez le mode «Natif», vous pourriez perdre "
"accès à certaines fonctionnalités de Plex tels que: Plex trailers et les "
"MISE EN GARDE ! Si vous choisissez le mode « Natif », vous pourriez perdre "
"accès à certaines fonctionnalités de Plex tels que : Plex trailers et les "
"options de transcodage. Toutes les actions de Plex ont besoin dutiliser des"
" chemins daccès directs (par exemple smb://myNAS/mymovie.mkv ou "
"\\myNAS/mymovie.mkv)!"
"\\myNAS/mymovie.mkv) !"
# Pop-up on initial sync
msgctxt "#39029"
@ -967,7 +915,7 @@ msgid ""
" Kodi can't locate your content."
msgstr ""
"Ajouter des informations didentification réseau pour permettre daccéder à "
"votre contenu Kodi? Remarque: Sauter cette étape peut générer un message "
"votre contenu Kodi ? Remarque : Sauter cette étape peut générer un message "
"lors de lanalyse initiale de votre contenu si Kodi ne peut pas localiser "
"votre contenu."
@ -1010,11 +958,6 @@ msgstr ""
"Echapper les caractères spéciaux dans le chemin (ex: %20 au lieu des "
"espaces)"
# PKC Settings - Customize Paths
msgctxt "#39090"
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
msgstr "Caractères sûrs pour les urls http(s), dav(s) et (s)ftp"
# PKC Settings - Customize Paths
msgctxt "#39037"
msgid "Original Plex MOVIE path to replace:"
@ -1051,9 +994,8 @@ msgid ""
"Go a step further and completely replace all original Plex library paths "
"(/volume1/media) with custom SMB paths (smb://NAS/MyStuff)?"
msgstr ""
"Continuer et remplacer complètement tous les chemins d'accès originaux de la"
" bibliothèque Plex (/volume1/media) par des chemins d'accès SMB "
"personnalisés (smb://NAS/MyStuff)?"
"Go a step further and completely replace all original Plex library paths "
"(/volume1/media) with custom SMB paths (smb://NAS/MyStuff)?"
# Pop-up on initial sync
msgctxt "#39044"
@ -1061,8 +1003,8 @@ msgid ""
"Please enter your custom smb paths in the settings under \"Sync Options\" "
"and then restart Kodi"
msgstr ""
"Veuillez entrer vos chemins smb personnalisés dans les paramètres sous "
"\"Sync Options\" et ensuite redémarrer Kodi"
"Please enter your custom smb paths in the settings under \"Sync Options\" "
"and then restart Kodi"
# PKC Settings - Customize Paths
msgctxt "#39045"
@ -1087,7 +1029,7 @@ msgstr "On Deck: Append season- and episode-number SxxExx"
# PKC Settings - Advanced
msgctxt "#39049"
msgid "Nothing works? Try a full reset!"
msgstr "Rien ne fonctionne? Essayez une réinitialisation complète!"
msgstr "Rien ne fonctionne ? Essayez une réinitialisation complète !"
# PKC Settings - Connection
msgctxt "#39050"
@ -1123,17 +1065,19 @@ msgstr "Recherche d'un serveur Plex"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
"Utilisé pour la synchronisation et les chemins directs. Redémarrez Kodi "
"après les changements!"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Used by Sync and when attempting to Direct Play"
# PKC Settings, category name
msgctxt "#39057"
msgid "Customize Paths"
msgstr "Personnalisation des chemins"
# PKC Settings - Appearance Tweaks
msgctxt "#39058"
msgid "Extend Plex TV Series \"On Deck\" view to all shows"
msgstr "Extend Plex TV Series \"On Deck\" view to all shows"
# PKC Settings - Appearance Tweaks
msgctxt "#39059"
msgid "Recently Added: Append show title to episode"
@ -1151,7 +1095,7 @@ msgid ""
"background?"
msgstr ""
"Vous souhaitez télécharger des illustrations supplémentaires de FanArtTV en "
"arrière-plan?"
"arrière-plan ?"
# PKC Settings - Sync
msgctxt "#39062"
@ -1166,22 +1110,26 @@ msgstr "Forcer le transcodage Hi10P"
# PKC Settings - Appearance Tweaks
msgctxt "#39064"
msgid "Recently Added: Also show already watched episodes"
msgstr "Ajouté récemment: Montre également les épisodes déjà visionnés"
msgstr "Recently Added: Also show already watched episodes"
# PKC Settings - Appearance Tweaks
msgctxt "#39065"
msgid "Force-refresh Kodi skin on stopping playback"
msgstr "Forcer le rafraîchissement du skin lors de l'arrêt de la lecture"
msgstr "Forcer le rafraîchissement du skin de Kodi va arrêter le playback."
# PKC Settings - Appearance Tweaks
msgctxt "#39066"
msgid "Recently Added: Also show already watched movies"
msgstr "Récemment ajouté: Montrer aussi les films déjà visionnés"
msgid ""
"Recently Added: Also show already watched movies (Refresh Plex "
"playlist/nodes!)"
msgstr ""
"Recently Added: Also show already watched movies (Refresh Plex "
"playlist/nodes!)"
# PKC Settings - Connection
msgctxt "#39067"
msgid "Your current Plex Media Server:"
msgstr "Votre serveur Plex actuel: "
msgstr "Votre serveur Plex actuel :"
# PKC Settings - Connection
msgctxt "#39068"
@ -1191,22 +1139,17 @@ msgstr "Entrez manuellement l'adresse du Plex Media Server"
# PKC Settings - Connection
msgctxt "#39069"
msgid "Current address:"
msgstr "Adresse actuelle: "
msgstr "Adresse actuelle :"
# PKC Settings - Connection
msgctxt "#39070"
msgid "Current port:"
msgstr "Port actuel: "
msgstr "Port actuel :"
# PKC Settings - Connection
msgctxt "#39071"
msgid "Current plex.tv status:"
msgstr "État actuel de plex.tv: "
# PKC Settings - Connection
msgctxt "#39072"
msgid "Background sync connection:"
msgstr ""
msgstr "État actuel de plex.tv :"
# PKC Settings, category name
msgctxt "#39073"
@ -1218,10 +1161,10 @@ msgctxt "#39074"
msgid "TV Shows"
msgstr "Séries TV"
# PKC Settings - Sync
# PKC Settings - Playback
msgctxt "#39075"
msgid "Verify access to media files while synching"
msgstr ""
msgid "Always use default Plex subtitle if possible"
msgstr "Utilisez toujours le sous-titre Plex par défaut si possible"
# Pop-up during initial sync
msgctxt "#39076"
@ -1229,19 +1172,19 @@ msgid ""
"If you use several Plex libraries of one kind, e.g. \"Kids Movies\" and "
"\"Parents Movies\", be sure to check the Wiki: https://goo.gl/JFtQV9"
msgstr ""
"Si vous utilisez plusieurs bibliothèques de Plex dun type, par exemple "
"«Films enfants» et «Films parents», assurez-vous de consulter le Wiki: "
"Si vous utilisez plusieurs bibliothèques de Plex dun type, par exemple « "
"Films enfants » et « Films parents », assurez-vous de consulter le Wiki : "
"https://goo.gl/JFtQV9"
# PKC Settings - Appearance Tweaks
msgctxt "#39077"
msgid "Maximum number of videos to show in widgets"
msgstr "Nombre maximum de vidéos à afficher dans les widgets"
msgid "Number of PMS items to show in widgets (e.g. \"On Deck\")"
msgstr "Number of PMS items to show in widgets (e.g. \"On Deck\")"
# PKC Settings - Plex
msgctxt "#39078"
msgid "Plex Companion Update Port (change only if needed)"
msgstr "Plex Companion Update Port (à modifier uniquement si nécessaire)"
msgstr "Plex Companion Update Port (change only if needed)"
# Error message
msgctxt "#39079"
@ -1249,8 +1192,8 @@ msgid ""
"Plex Companion could not open the GDM port. Please change it in the PKC "
"settings."
msgstr ""
"Plex Companion n'a pas pu ouvrir le port GDM. Veuillez le modifier dans les "
"paramètres du PKC."
"Plex Companion could not open the GDM port. Please change it in the PKC "
"settings."
# Pop-up on initial sync.
# Check that next translations for Add-on Paths and Direct Paths are
@ -1260,9 +1203,8 @@ msgid ""
"Use Add-on Paths (default, easy) or Direct Paths? Choose Add-on Paths if "
"you're unsure. PKC will not work if your Direct Paths setup is wrong!"
msgstr ""
"Utiliser les chemins de l'Add-on (par défaut, simple) ou les chemins "
"Directs? Choisissez les chemins de l'Add-on si vous n'êtes pas sûr. PKC ne "
"fonctionnera pas si vos réglages chemins Directs ne sont pas corrects."
"Utiliser les chemins de l'Add-on (par défaut, simple) ou les chemins Directs? \n"
"Choisissez les chemins de l'Add-on si vous n'êtes pas sûr. PKC ne fonctionnera pas si vos réglages chemins Directs ne sont pas corrects."
# Button text for choosing PKC mode
msgctxt "#39081"
@ -1284,37 +1226,6 @@ msgctxt "#39084"
msgid "Enter PMS port"
msgstr "Entrez le port de PMS"
# PKC settings - Appearance Tweaks
msgctxt "#39085"
msgid "Reload Kodi node files to apply all the settings below"
msgstr ""
"Recharger les fichiers de Kodi pour appliquer tous les paramètres ci-dessous"
# PKC Settings - Connection - Background sync connection status
msgctxt "#39089"
msgid "Alexa connection status:"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39091"
msgid "Timeout - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39092"
msgid "IOError - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39093"
msgid "Suspended - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39094"
msgid "Managed Plex User - not connected"
msgstr ""
msgctxt "#39200"
msgid "Log-out Plex Home User "
msgstr "Log-out Plex Home User "
@ -1331,24 +1242,24 @@ msgstr "Synchroniser la bibliothèque manuellement"
msgctxt "#39205"
msgid "Unable to run the sync, the add-on is not connected to a Plex server."
msgstr ""
"Impossible de lancer la synchronisation, lextension nest pas connecté à un"
" serveur Plex."
"Impossible de lancer la synchronisation, lAdd-on nest pas connecté à un "
"serveur Plex."
msgctxt "#39206"
msgid ""
"Plex might lock your account if you fail to log in too many times. Proceed "
"anyway?"
msgstr ""
"Plex pourrait bloquer votre compte si vous vous connecter trop de fois. "
"Poursuivre quand même ?"
"Plex pourrait bloquer votre compte si vous ne parvenez pas à vous connecter "
"trop de fois. Continuer néanmoins ?"
msgctxt "#39207"
msgid "Resetting PMS connections, please wait"
msgstr "Réinitialisation des connexions PMS, veuillez patienter"
msgstr "Resetting PMS connections, please wait"
msgctxt "#39208"
msgid "Failed to reset PKC. Try to restart Kodi."
msgstr "Impossible de réinitialiser PKC. Essayez de redémarrer Kodi."
msgstr "Failed to reset PKC. Try to restart Kodi."
# PKC Settings - Plex
msgctxt "#39209"
@ -1357,7 +1268,7 @@ msgstr "Interrupteur de connexion à plex.tv (se connecter ou se déconnecter)"
msgctxt "#39210"
msgid "Not yet connected to Plex Server"
msgstr "Pas encore connecté au serveur Plex"
msgstr "Not yet connected to Plex Server"
msgctxt "#39211"
msgid "Watch later"
@ -1367,15 +1278,19 @@ msgstr "Regarder plus tard"
# e.g. the PMS' name
msgctxt "#39213"
msgid "{0} offline"
msgstr "{0} hors-ligne"
msgstr ""
msgctxt "#39215"
msgid "Enter your Plex Media Server's IP or URL, Examples are:"
msgstr "Entrez l'IP ou l'URL du Plex Media Server, exemples: "
msgstr "Enter your Plex Media Server's IP or URL, Examples are:"
msgctxt "#39217"
msgid "Use HTTPS (SSL) connections? Answer should probably be yes."
msgstr "Utiliser le HTTPS (SSL) ? La réponse devrait être oui."
msgid ""
"Use HTTPS (SSL) connections? With Kodi 18 or later, HTTPS will likely not "
"work!"
msgstr ""
"Utiliser des connexions HTTPS (SSL) ? Avec Kodi 18 ou après, HTTPS ne "
"marchera probablement pas !"
msgctxt "#39218"
msgid "Error contacting PMS"
@ -1383,7 +1298,7 @@ msgstr "Error contacting PMS"
msgctxt "#39219"
msgid "Abort (Yes) or save address anyway (No)?"
msgstr "Abandonner (Oui) ou enregistrer quand même l'adresse (Non) ?"
msgstr "Abort (Yes) or save address anyway (No)?"
# String attached at the end to get something like "PMS Name is offline"
msgctxt "#39220"
@ -1403,8 +1318,8 @@ msgid ""
"Only look for missing fanart or refresh all fanart? The scan will take quite"
" a while and happen in the background."
msgstr ""
"Cherchez seulement le fanart manquant ou rafraîchissez tous les fanart ? Le "
"balayage prendra un certain temps et se fera en arrière-plan."
"Only look for missing fanart or refresh all fanart? The scan will take quite"
" a while and happen in the background."
msgctxt "#39224"
msgid "Refresh all"
@ -1412,7 +1327,7 @@ msgstr "Tout rafraîchir"
msgctxt "#39225"
msgid "Missing only"
msgstr "Manquant seulement"
msgstr "Missing only"
# Message in the PKC settings if user has not logged in to plex.tv
msgctxt "#39226"
@ -1450,20 +1365,20 @@ msgid ""
"Running the image cache process can take some time. It will happen in the "
"background. Are you sure you want continue?"
msgstr ""
"L'exécution du processus de mise en cache des images peut prendre un certain"
" temps. Il se fera en arrière-plan. Êtes-vous sûr de vouloir continuer ?"
"Running the image cache process can take some time. It will happen in the "
"background. Are you sure you want continue?"
msgctxt "#39251"
msgid "Reset all existing cache data first?"
msgstr "Supprimer d'abord tout le cache des données ?"
msgstr "Reset all existing cache data first?"
msgctxt "#39303"
msgid "Problems trying to contact plex.tv. Try again later"
msgstr "Problèmes en essayant de connecter plex.tv. Réessayez plus tard"
msgstr "Problèmes en essayant de contacter plex.tv. Réessayez plus tard"
msgctxt "#39304"
msgid "Go to https://plex.tv/pin and enter the code: "
msgstr "Allez sur https://plex.tv/pin et entrez le code: "
msgstr "Allez sur https://plex.tv/pin et entrez le code : "
msgctxt "#39305"
msgid "Could not sign in to plex.tv. Try again later"
@ -1498,8 +1413,8 @@ msgid ""
"Library sync thread has crashed. You should restart Kodi now. Please report "
"this on the forum"
msgstr ""
"La synchronisation de la bibliothèque s'est interrompue. Vous devriez "
"redémarrer Kodi maintenant. Veuillez le signaler sur le forum"
"Library sync thread has crashed. You should restart Kodi now. Please report "
"this on the forum"
msgctxt "#39401"
msgid ""
@ -1546,10 +1461,9 @@ msgid ""
"returned ERRORS. Try lowering the number of sync download threads in the "
"settings. Skipped some items for now."
msgstr ""
"Le serveur Plex n'a pas aimé que vous demandiez autant de données d'un seul "
"coup et a renvoyé des ERREURS. Essayez de réduire le nombre de threads de "
"téléchargement de synchronisation dans les paramètres. Certains éléments ont"
" été ignorés pour l'instant."
"The Plex Server did not like you asking for so much data at once and "
"returned ERRORS. Try lowering the number of sync download threads in the "
"settings. Skipped some items for now."
msgctxt "#39410"
msgid "ERROR in library sync"
@ -1563,10 +1477,6 @@ msgctxt "#39501"
msgid "Collections"
msgstr "Sagas"
msgctxt "#39502"
msgid "PKC On Deck (faster)"
msgstr "PKC On Deck (faster)"
msgctxt "#39600"
msgid ""
"Are you sure you want to reset your local Kodi database? A re-sync of the "
@ -1578,20 +1488,19 @@ msgstr ""
msgctxt "#39601"
msgid "Could not stop the database from running. Please try again later."
msgstr ""
"Impossible de stopper la base de donnée. Veuillez réessayer plus tard."
msgstr "Could not stop the database from running. Please try again later."
msgctxt "#39603"
msgid ""
"Reset all PlexKodiConnect Addon settings? (this is usually NOT recommended "
"and unnecessary!)"
msgstr ""
"Réinitialiser tous les paramètres de PlexKodiConnect Addon? (ceci nest "
"généralement pas recommandée et inutiles!)"
"Réinitialiser tous les paramètres de PlexKodiConnect Addon ? (ceci nest "
"généralement pas recommandée et inutiles !)"
msgctxt "#39700"
msgid "Amazon Alexa (Voice Recognition)"
msgstr "Amazon Alexa (Reconnaissance vocale)"
msgstr "Amazon Alexa (Voice Recognition)"
msgctxt "#39701"
msgid "Activate Alexa"
@ -1599,7 +1508,7 @@ msgstr "Activer Alexa"
msgctxt "#39702"
msgid "Browse by folder"
msgstr "Parcourir par dossier"
msgstr "Parcourir en suivant les répertoires"
# For use with addon.xml (PKC metadata for Kodi, e.g. description)
# Addon Summary
@ -1628,10 +1537,11 @@ msgctxt "#39705"
msgid "Use at your own risk"
msgstr "A utiliser à vos propres risques"
# If user gets prompted to choose between several subtitles to burn in
# If user gets prompted to choose between several subtitles. Leave the number
# one at the beginning of the string!
msgctxt "#39706"
msgid "Don't burn-in any subtitle"
msgstr "Ne pas incruster de sous-titres"
msgid "1 No subtitles"
msgstr "1 pas de sous titre"
# If user gets prompted to choose between several audio/subtitle tracks and
# language is unknown
@ -1685,8 +1595,8 @@ msgstr "Synchro"
# Shown during sync process
msgctxt "#39715"
msgid "Synching playlists"
msgstr "Synchronisation des listes de lecture"
msgid "items"
msgstr "éléments"
# Error message if an xml, e.g. advancedsettings.xml cannot be parsed (xml is
# screwed up; formated the wrong way). Do NOT replace {0} and {1}!
@ -1696,7 +1606,7 @@ msgid ""
" correct your file!"
msgstr ""
"Kodi ne peut pas traiter {0}. PKC ne fonctionnera pas correctement. Merci de"
" visiter {1} et de corriger votre fichier!"
" visiter {1} et de corriger votre fichier !"
# Shown once on first installation to comply with the terms of use of
# themoviedb.org
@ -1704,7 +1614,7 @@ msgctxt "#39717"
msgid "PKC uses free additional artwork from www.themoviedb.org. Many thanks!"
msgstr ""
"PKC utilise gratuitement des artwork additionnels provenant de "
"www.themoviedb.org. Merci beaucoup!"
"www.themoviedb.org. Merci beaucoup !"
# Shown during very first PKC setup only
msgctxt "#39718"
@ -1713,7 +1623,7 @@ msgid ""
"many versions of a media item you posses?"
msgstr ""
"Voulez-vous remplacer vos notes d'utilisateurs personnalisées avec un "
"indicateur du nombre de versions d'un média que vous possédez?"
"indicateur du nombre de versions d'un média que vous possédez ?"
# In PKC Settings under Sync
msgctxt "#39719"

View File

@ -1,12 +1,10 @@
# XBMC Media Center language file
# Translators:
# Croneter None <croneter@gmail.com>, 2017
# Nat Le Scouarnec <nat@blogotheque.net>, 2017
# scorpio686 <gced@live.be>, 2018
# Janek B. <janek-transifex@les3j.net>, 2019
# Janek B. <janek.berne_transifex@ykw.im>, 2019
# Elixir59, 2019
# julien benoist <jbnitro@hotmail.fr>, 2019
# Croneter None <croneter@gmail.com>, 2020
# Raph Mell, 2020
#
msgid ""
msgstr ""
@ -14,7 +12,7 @@ msgstr ""
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
"Last-Translator: Raph Mell, 2020\n"
"Last-Translator: Elixir59, 2019\n"
"Language-Team: French (France) (https://www.transifex.com/croneter/teams/73837/fr_FR/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -47,15 +45,8 @@ msgid ""
"Warning: Kodi setting \"Play next video automatically\" is enabled. This "
"could break PKC. Deactivate?"
msgstr ""
"Avertissement : Le paramètre Kodi \"Lecture automatique de la vidéo "
"suivante\" est activé. Cela pourrait casser PKC. Désactiver ?"
msgctxt "#30004"
msgid ""
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
"random password automatically if you haven't done so already. Please confirm"
" the next dialog that you want to enable the webserver now with Yes."
msgstr ""
"Attention, les paramètres de Kodi sont réglés pour joueur automatiquement la"
" vidéo suivante. Cette option va casser PlexKodiConnect. Désactiver ?"
msgctxt "#30005"
msgid "Username: "
@ -80,8 +71,8 @@ msgstr "Activer les notifications pour la mise en cache d'images"
msgctxt "#30009"
msgid "Enable image caching during Kodi playback (restart Kodi!)"
msgstr ""
"Activer la mise en cache des images pendant la lecture Kodi (redémarrez "
"Kodi!)"
"Activer la mise en cache des images pendant la lecture Kodi (redémarrez Kodi"
" !)"
# PKC settings - Artwork
msgctxt "#30010"
@ -134,7 +125,7 @@ msgstr "Recherche sur FanartTV terminée"
# PKC settings sync options
msgctxt "#30020"
msgid "Sync Plex playlists (reboot Kodi!)"
msgstr "Synchroniser les playlists de Plex (redémarrez Kodi!)"
msgstr "Synchroniser les playlists de Plex (redémarrez Kodi !)"
# PKC settings sync options
msgctxt "#30021"
@ -176,14 +167,6 @@ msgctxt "#30028"
msgid "PKC-only image caching completed"
msgstr "Mise en cache de images pour PKC-seulement terminée"
# Warning shown when PKC switches to the Kodi default skin Estuary
msgctxt "#30029"
msgid ""
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
"database resets. Continue?"
msgstr ""
msgctxt "#30030"
msgid "Port Number"
msgstr "Numéro de port"
@ -244,7 +227,7 @@ msgstr "Pour les films"
msgctxt "#30125"
msgid "Done"
msgstr "Effectué"
msgstr "L'action a été effectuée"
# Error popup message text
msgctxt "#30128"
@ -283,10 +266,6 @@ msgctxt "#30160"
msgid "Video Quality if Transcoding necessary"
msgstr "Qualité vidéo si un transcodage est nécessaire"
msgctxt "#30161"
msgid "Auto-adjust transcoding quality (deactivate for Chromecast)"
msgstr "Auto-ajuster la qualité du transcodage (désactivé pour Chromecast)"
msgctxt "#30165"
msgid "Direct Play"
msgstr "Lecture directe"
@ -297,7 +276,7 @@ msgstr "Transcodage"
msgctxt "#30170"
msgid "Recently Added TV Shows"
msgstr "Série(s) récemment ajoutée(s)"
msgstr "Série(s) récemment ajoutée"
msgctxt "#30171"
msgid "In Progress TV Shows"
@ -309,7 +288,7 @@ msgstr "Chaînes"
msgctxt "#30174"
msgid "Recently Added"
msgstr "Récemment ajouté"
msgstr "Récemment ajoutée"
msgctxt "#30177"
msgid "In Progress Movies"
@ -461,7 +440,7 @@ msgstr "Evaluer la chanson"
# contextmenu entry
msgctxt "#30408"
msgid "Plex addon settings"
msgstr "Paramètres dextension de plex"
msgstr "Paramètres daddon de plex"
# contextmenu entry
msgctxt "#30409"
@ -485,7 +464,7 @@ msgid ""
"Server?"
msgstr ""
"La suppression n'a pas pu être effectuée. La suppression est-elle activée "
"sur le serveur de médias Plex?"
"sur le serveur de médias Plex ?"
# contextmenu entry
msgctxt "#30415"
@ -571,13 +550,13 @@ msgstr ""
msgctxt "#30514"
msgid "Show all Plex extras instead of immediately playing trailers"
msgstr ""
"Afficher tous les extras Plex au lieu de lire immédiatement les bandes-"
"Afficher tous les extras Plex au lieu de jouer immédiatement les bandes-"
"annonces."
# PKC Settings - Sync Options
msgctxt "#30515"
msgid "Maximum items to request from the server at once"
msgstr "Nombre maximum d'éléments à demander au serveur en même temps"
msgstr "Nombre maximum d'éléments concurrents à demander au serveur"
# PKC Settings, category name
msgctxt "#30516"
@ -597,7 +576,7 @@ msgstr "Activer les bandes-annonces Plex (nécessite PlexPass)"
# PKC Settings - Playback
msgctxt "#30519"
msgid "Ask to play trailers"
msgstr "Demander de lire les bandes-annonces"
msgstr "Demander de jouer les bandes-annonces"
# PKC Settings - Plex
msgctxt "#30520"
@ -620,16 +599,11 @@ msgctxt "#30523"
msgid "Also show sync progress for playstate and user data"
msgstr ""
"Afficher également la progression de la synchronisation pour l'état de "
"lecture et des données utilisateur"
"lecture et les données utilisateur"
# PKC Settings - Sync Options
msgctxt "#30524"
msgid "Select Plex libraries to sync"
msgstr "Sélectionner les bibliothèques Plex à synchroniser"
# PKC Settings - Playback
msgctxt "#30525"
msgid "Skip intro"
msgstr ""
# PKC Settings - Playback
@ -670,8 +644,7 @@ msgstr "Générer un nouvel ID unique Plex (Ex: pour cloner Kodi)"
# PKC Settings - Connection
msgctxt "#30536"
msgid "Users must log in every time Kodi restarts"
msgstr ""
"Les utilisateurs doivent se connecter à chaque fois que Kodi redémarre"
msgstr "Les utilisateurs doivent se connectent chaque fois que Kodi redémarre"
# PKC Settings warning
msgctxt "#30537"
@ -697,8 +670,8 @@ msgstr "Télécharger les affiches des sagas sur FanArtTV"
# PKC Settings - Playback
msgctxt "#30541"
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
msgstr ""
msgid "Don't ask to pick a certain stream/quality"
msgstr "Ne pas demander de choisir un certain flux/qualité"
# PKC Settings - Playback
msgctxt "#30542"
@ -719,21 +692,6 @@ msgctxt "#30545"
msgid "Force transcode pictures"
msgstr "Forcer le transcodage des images"
# PKC Settings - Playback
msgctxt "#30546"
msgid "Pick the first video if several versions are present"
msgstr ""
# PKC Settings - Playback
msgctxt "#30547"
msgid "Who picks the audio stream on playback start?"
msgstr ""
# PKC Settings - Playback
msgctxt "#30548"
msgid "Who picks subtitles on playback start?"
msgstr ""
# Welcome to Plex notification
msgctxt "#33000"
msgid "Welcome"
@ -754,17 +712,6 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Le serveur est en ligne"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr "PMS impose le transcodage"
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr "PMS impose le streaming en direct"
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -792,7 +739,7 @@ msgstr "Choisissez le fichier de sous-titres"
# Dialog before playback
msgctxt "#33016"
msgid "Play trailers?"
msgstr "Lire les bandes-annonces?"
msgstr "Jouer les bandes-annonces ?"
# Error message
msgctxt "#33032"
@ -813,23 +760,23 @@ msgid ""
"Delete file(s) from Plex Server? This will also delete the file(s) from "
"disk!"
msgstr ""
"Supprimer les fichiers du serveur de Plex? Cela va également supprimer le(s)"
" fichier(s) du disque!"
"Supprimer les fichiers du serveur de Plex ? Cela va également supprimer "
"le(s) fichier(s) du disque !"
# PKC Settings - Playback
msgctxt "#39000"
msgid "- Number of trailers to play before a movie"
msgstr "-Nombre de bandes-annonces à lire avant un film"
msgstr "-Nombre de bandes-annonces à jouer avant un film"
# PKC Settings - Playback
msgctxt "#39001"
msgid "Boost audio when transcoding"
msgstr "Augmenter l'audio pendant le transcodage"
msgstr "Booster l'audio pendant le transcodage"
# PKC Settings - Playback
msgctxt "#39002"
msgid "Burnt-in subtitle size"
msgstr "Taille des sous-titres incrustés"
msgstr "Taille des sous-titres incorporés"
# PKC Settings - Sync
msgctxt "#39003"
@ -839,27 +786,27 @@ msgstr "Nombre de processus de téléchargement simultanés"
# PKC Settings - Plex
msgctxt "#39004"
msgid "Enable Plex Companion (restart Kodi!)"
msgstr "Activer le compagnon de Plex (redémarrer Kodi!)"
msgstr "Activer le compagnon de Plex (redémarrage de Kodi !)"
# PKC Settings - Plex
msgctxt "#39005"
msgid "Plex Companion Port (change only if needed)"
msgstr "Plex Companion Port (changement uniquement si nécessaire)"
msgstr "Plex Companion Port (change only if needed)"
# PKC Settings - Plex
msgctxt "#39008"
msgid "Plex Companion: Allows flinging media to Kodi through Plex"
msgstr "Plex Companion: Permet de lancer des médias vers Kodi via Plex"
msgstr "Plex Companion: Allows flinging media to Kodi through Plex"
# Error message
msgctxt "#39009"
msgid "Could not login to plex.tv. Please try signing in again."
msgstr "Impossible de se logguer à plex.tv. Veuillez essayer à nouveau"
msgstr "Could not login to plex.tv. Please try signing in again."
# Error message
msgctxt "#39010"
msgid "Problems connecting to plex.tv. Network or internet issue?"
msgstr "Problèmes de connexion à plex.tv. Problème réseau ou dinternet ?"
msgstr "Problèmes de connexion à plex.tv. Problème de réseau ou dinternet ?"
# Error message
msgctxt "#39011"
@ -879,7 +826,7 @@ msgstr "Pas encore autorisée pour le serveur Plex "
# Error message
msgctxt "#39014"
msgid "Please sign in to plex.tv."
msgstr "Veuillez vous connexter à plex.tv."
msgstr "Sil vous plaît connectez-vous à plex.tv."
# Error message
msgctxt "#39015"
@ -892,7 +839,7 @@ msgid ""
"Disable Plex music library? (It is HIGHLY recommended to use Plex music only"
" with direct paths for large music libraries. Kodi might crash otherwise)"
msgstr ""
"Désactiver la bibliothèque musicale de Plex? (Il est fortement recommandé "
"Désactiver la bibliothèque musicale de Plex ? (Il est fortement recommandé "
"dutiliser Plex musique seulement avec des chemins daccès directs pour les "
"grandes bibliothèques musicale. Sinon Kodi peut sinterrompre)"
@ -902,8 +849,8 @@ msgid ""
"Would you now like to go to the plugin's settings to fine-tune PKC? You will"
" need to RESTART Kodi!"
msgstr ""
"Voulez-vous maintenant aller dans les paramètres du plugin pour affiner "
"PKC? Vous devrez redémarrer Kodi!"
"Aimeriez-vous maintenant aller dans les paramètres du plugin pour affiner la"
" PKC ? Vous devrez redémarrer Kodi !"
# PKC Settings - Advanced
msgctxt "#39018"
@ -932,12 +879,12 @@ msgstr "local"
msgctxt "#39023"
msgid "Failed to authenticate. Did you login to plex.tv?"
msgstr ""
"Impossible deffectuer l'authentification. Êtes-vous connectez à plex.tv?"
"Impossible deffectuer l'authentification. Êtes-vous connectez à plex.tv ?"
# PKC Settings - Plex
msgctxt "#39025"
msgid "Automatically log into plex.tv on startup"
msgstr "Connecter automatiquement plex.tv au démarrage"
msgstr "Vous connecter automatiquement plex.tv au démarrage"
# PKC Settings - Sync
msgctxt "#39026"
@ -952,11 +899,11 @@ msgid ""
"shares need to use direct paths (e.g. smb://myNAS/mymovie.mkv or "
"\\\\myNAS/mymovie.mkv)!"
msgstr ""
"MISE EN GARDE! Si vous choisissez le mode «Natif», vous pourriez perdre "
"accès à certaines fonctionnalités de Plex tels que: Plex trailers et les "
"MISE EN GARDE ! Si vous choisissez le mode « Natif », vous pourriez perdre "
"accès à certaines fonctionnalités de Plex tels que : Plex trailers et les "
"options de transcodage. Toutes les actions de Plex ont besoin dutiliser des"
" chemins daccès directs (par exemple smb://myNAS/mymovie.mkv ou "
"\\myNAS/mymovie.mkv)!"
"\\myNAS/mymovie.mkv) !"
# Pop-up on initial sync
msgctxt "#39029"
@ -971,7 +918,7 @@ msgid ""
" Kodi can't locate your content."
msgstr ""
"Ajouter des informations didentification réseau pour permettre daccéder à "
"votre contenu Kodi? Remarque: Sauter cette étape peut générer un message "
"votre contenu Kodi ? Remarque : Sauter cette étape peut générer un message "
"lors de lanalyse initiale de votre contenu si Kodi ne peut pas localiser "
"votre contenu."
@ -1014,11 +961,6 @@ msgstr ""
"Echapper les caractères spéciaux dans le chemin (ex: %20 au lieu des "
"espaces)"
# PKC Settings - Customize Paths
msgctxt "#39090"
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
msgstr "Caractères sûrs pour les urls http(s), dav(s) et (s)ftp"
# PKC Settings - Customize Paths
msgctxt "#39037"
msgid "Original Plex MOVIE path to replace:"
@ -1055,9 +997,8 @@ msgid ""
"Go a step further and completely replace all original Plex library paths "
"(/volume1/media) with custom SMB paths (smb://NAS/MyStuff)?"
msgstr ""
"Continuer et remplacer complètement tous les chemins d'accès originaux de la"
" bibliothèque Plex (/volume1/media) par des chemins d'accès SMB "
"personnalisés (smb://NAS/MyStuff)?"
"Go a step further and completely replace all original Plex library paths "
"(/volume1/media) with custom SMB paths (smb://NAS/MyStuff)?"
# Pop-up on initial sync
msgctxt "#39044"
@ -1065,8 +1006,8 @@ msgid ""
"Please enter your custom smb paths in the settings under \"Sync Options\" "
"and then restart Kodi"
msgstr ""
"Veuillez entrer vos chemins smb personnalisés dans les paramètres sous "
"\"Sync Options\" et ensuite redémarrer Kodi"
"Please enter your custom smb paths in the settings under \"Sync Options\" "
"and then restart Kodi"
# PKC Settings - Customize Paths
msgctxt "#39045"
@ -1091,7 +1032,7 @@ msgstr "On Deck: Append season- and episode-number SxxExx"
# PKC Settings - Advanced
msgctxt "#39049"
msgid "Nothing works? Try a full reset!"
msgstr "Rien ne fonctionne? Essayez une réinitialisation complète!"
msgstr "Rien ne fonctionne ? Essayez une réinitialisation complète !"
# PKC Settings - Connection
msgctxt "#39050"
@ -1127,17 +1068,19 @@ msgstr "Recherche d'un serveur Plex"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
"Utilisé pour la synchronisation et les chemins directs. Redémarrez Kodi "
"après les changements!"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Used by Sync and when attempting to Direct Play"
# PKC Settings, category name
msgctxt "#39057"
msgid "Customize Paths"
msgstr "Personnalisation des chemins"
# PKC Settings - Appearance Tweaks
msgctxt "#39058"
msgid "Extend Plex TV Series \"On Deck\" view to all shows"
msgstr "Extend Plex TV Series \"On Deck\" view to all shows"
# PKC Settings - Appearance Tweaks
msgctxt "#39059"
msgid "Recently Added: Append show title to episode"
@ -1155,7 +1098,7 @@ msgid ""
"background?"
msgstr ""
"Vous souhaitez télécharger des illustrations supplémentaires de FanArtTV en "
"arrière-plan?"
"arrière-plan ?"
# PKC Settings - Sync
msgctxt "#39062"
@ -1170,22 +1113,26 @@ msgstr "Forcer le transcodage Hi10P"
# PKC Settings - Appearance Tweaks
msgctxt "#39064"
msgid "Recently Added: Also show already watched episodes"
msgstr "Ajouté récemment: Montre également les épisodes déjà visionnés"
msgstr "Recently Added: Also show already watched episodes"
# PKC Settings - Appearance Tweaks
msgctxt "#39065"
msgid "Force-refresh Kodi skin on stopping playback"
msgstr "Forcer le rafraîchissement du skin lors de l'arrêt de la lecture"
msgstr "Forcer le rafraîchissement du skin de Kodi va arrêter le playback."
# PKC Settings - Appearance Tweaks
msgctxt "#39066"
msgid "Recently Added: Also show already watched movies"
msgstr "Récemment ajouté: Montrer aussi les films déjà visionnés"
msgid ""
"Recently Added: Also show already watched movies (Refresh Plex "
"playlist/nodes!)"
msgstr ""
"Recently Added: Also show already watched movies (Refresh Plex "
"playlist/nodes!)"
# PKC Settings - Connection
msgctxt "#39067"
msgid "Your current Plex Media Server:"
msgstr "Votre serveur Plex actuel: "
msgstr "Votre serveur Plex actuel :"
# PKC Settings - Connection
msgctxt "#39068"
@ -1195,22 +1142,17 @@ msgstr "Entrez manuellement l'adresse du Plex Media Server"
# PKC Settings - Connection
msgctxt "#39069"
msgid "Current address:"
msgstr "Adresse actuelle: "
msgstr "Adresse actuelle :"
# PKC Settings - Connection
msgctxt "#39070"
msgid "Current port:"
msgstr "Port actuel: "
msgstr "Port actuel :"
# PKC Settings - Connection
msgctxt "#39071"
msgid "Current plex.tv status:"
msgstr "État actuel de plex.tv: "
# PKC Settings - Connection
msgctxt "#39072"
msgid "Background sync connection:"
msgstr ""
msgstr "État actuel de plex.tv :"
# PKC Settings, category name
msgctxt "#39073"
@ -1222,10 +1164,10 @@ msgctxt "#39074"
msgid "TV Shows"
msgstr "Séries TV"
# PKC Settings - Sync
# PKC Settings - Playback
msgctxt "#39075"
msgid "Verify access to media files while synching"
msgstr ""
msgid "Always use default Plex subtitle if possible"
msgstr "Utilisez toujours le sous-titre Plex par défaut si possible"
# Pop-up during initial sync
msgctxt "#39076"
@ -1233,19 +1175,19 @@ msgid ""
"If you use several Plex libraries of one kind, e.g. \"Kids Movies\" and "
"\"Parents Movies\", be sure to check the Wiki: https://goo.gl/JFtQV9"
msgstr ""
"Si vous utilisez plusieurs bibliothèques de Plex dun type, par exemple "
"«Films enfants» et «Films parents», assurez-vous de consulter le Wiki: "
"Si vous utilisez plusieurs bibliothèques de Plex dun type, par exemple « "
"Films enfants » et « Films parents », assurez-vous de consulter le Wiki : "
"https://goo.gl/JFtQV9"
# PKC Settings - Appearance Tweaks
msgctxt "#39077"
msgid "Maximum number of videos to show in widgets"
msgstr "Nombre maximum de vidéos à afficher dans les widgets"
msgid "Number of PMS items to show in widgets (e.g. \"On Deck\")"
msgstr "Number of PMS items to show in widgets (e.g. \"On Deck\")"
# PKC Settings - Plex
msgctxt "#39078"
msgid "Plex Companion Update Port (change only if needed)"
msgstr "Plex Companion Update Port (à modifier uniquement si nécessaire)"
msgstr "Plex Companion Update Port (change only if needed)"
# Error message
msgctxt "#39079"
@ -1253,8 +1195,8 @@ msgid ""
"Plex Companion could not open the GDM port. Please change it in the PKC "
"settings."
msgstr ""
"Plex Companion n'a pas pu ouvrir le port GDM. Veuillez le modifier dans les "
"paramètres du PKC."
"Plex Companion could not open the GDM port. Please change it in the PKC "
"settings."
# Pop-up on initial sync.
# Check that next translations for Add-on Paths and Direct Paths are
@ -1264,9 +1206,8 @@ msgid ""
"Use Add-on Paths (default, easy) or Direct Paths? Choose Add-on Paths if "
"you're unsure. PKC will not work if your Direct Paths setup is wrong!"
msgstr ""
"Utiliser les chemins de l'Add-on (par défaut, simple) ou les chemins "
"Directs? Choisissez les chemins de l'Add-on si vous n'êtes pas sûr. PKC ne "
"fonctionnera pas si vos réglages chemins Directs ne sont pas corrects."
"Utiliser les chemins de l'Add-on (par défaut, simple) ou les chemins Directs? \n"
"Choisissez les chemins de l'Add-on si vous n'êtes pas sûr. PKC ne fonctionnera pas si vos réglages chemins Directs ne sont pas corrects."
# Button text for choosing PKC mode
msgctxt "#39081"
@ -1288,37 +1229,6 @@ msgctxt "#39084"
msgid "Enter PMS port"
msgstr "Entrez le port de PMS"
# PKC settings - Appearance Tweaks
msgctxt "#39085"
msgid "Reload Kodi node files to apply all the settings below"
msgstr ""
"Recharger les fichiers de Kodi pour appliquer tous les paramètres ci-dessous"
# PKC Settings - Connection - Background sync connection status
msgctxt "#39089"
msgid "Alexa connection status:"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39091"
msgid "Timeout - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39092"
msgid "IOError - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39093"
msgid "Suspended - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39094"
msgid "Managed Plex User - not connected"
msgstr ""
msgctxt "#39200"
msgid "Log-out Plex Home User "
msgstr "Log-out Plex Home User "
@ -1335,24 +1245,24 @@ msgstr "Synchroniser la bibliothèque manuellement"
msgctxt "#39205"
msgid "Unable to run the sync, the add-on is not connected to a Plex server."
msgstr ""
"Impossible de lancer la synchronisation, lextension nest pas connecté à un"
" serveur Plex."
"Impossible de lancer la synchronisation, lAdd-on nest pas connecté à un "
"serveur Plex."
msgctxt "#39206"
msgid ""
"Plex might lock your account if you fail to log in too many times. Proceed "
"anyway?"
msgstr ""
"Plex pourrait bloquer votre compte si vous vous connecter trop de fois. "
"Poursuivre quand même ?"
"Plex pourrait bloquer votre compte si vous ne parvenez pas à vous connecter "
"trop de fois. Continuer néanmoins ?"
msgctxt "#39207"
msgid "Resetting PMS connections, please wait"
msgstr "Réinitialisation des connexions PMS, veuillez patienter"
msgstr "Resetting PMS connections, please wait"
msgctxt "#39208"
msgid "Failed to reset PKC. Try to restart Kodi."
msgstr "Impossible de réinitialiser PKC. Essayez de redémarrer Kodi."
msgstr "Failed to reset PKC. Try to restart Kodi."
# PKC Settings - Plex
msgctxt "#39209"
@ -1361,7 +1271,7 @@ msgstr "Interrupteur de connexion à plex.tv (se connecter ou se déconnecter)"
msgctxt "#39210"
msgid "Not yet connected to Plex Server"
msgstr "Pas encore connecté au serveur Plex"
msgstr "Not yet connected to Plex Server"
msgctxt "#39211"
msgid "Watch later"
@ -1371,15 +1281,19 @@ msgstr "Regarder plus tard"
# e.g. the PMS' name
msgctxt "#39213"
msgid "{0} offline"
msgstr "{0} hors-ligne"
msgstr ""
msgctxt "#39215"
msgid "Enter your Plex Media Server's IP or URL, Examples are:"
msgstr "Entrez l'IP ou l'URL du Plex Media Server, exemples: "
msgstr "Enter your Plex Media Server's IP or URL, Examples are:"
msgctxt "#39217"
msgid "Use HTTPS (SSL) connections? Answer should probably be yes."
msgstr "Utiliser le HTTPS (SSL) ? La réponse devrait être oui."
msgid ""
"Use HTTPS (SSL) connections? With Kodi 18 or later, HTTPS will likely not "
"work!"
msgstr ""
"Utiliser des connexions HTTPS (SSL) ? Avec Kodi 18 ou après, HTTPS ne "
"marchera probablement pas !"
msgctxt "#39218"
msgid "Error contacting PMS"
@ -1387,7 +1301,7 @@ msgstr "Error contacting PMS"
msgctxt "#39219"
msgid "Abort (Yes) or save address anyway (No)?"
msgstr "Abandonner (Oui) ou enregistrer quand même l'adresse (Non) ?"
msgstr "Abort (Yes) or save address anyway (No)?"
# String attached at the end to get something like "PMS Name is offline"
msgctxt "#39220"
@ -1407,8 +1321,8 @@ msgid ""
"Only look for missing fanart or refresh all fanart? The scan will take quite"
" a while and happen in the background."
msgstr ""
"Cherchez seulement le fanart manquant ou rafraîchissez tous les fanart ? Le "
"balayage prendra un certain temps et se fera en arrière-plan."
"Only look for missing fanart or refresh all fanart? The scan will take quite"
" a while and happen in the background."
msgctxt "#39224"
msgid "Refresh all"
@ -1416,7 +1330,7 @@ msgstr "Tout rafraîchir"
msgctxt "#39225"
msgid "Missing only"
msgstr "Manquant seulement"
msgstr "Missing only"
# Message in the PKC settings if user has not logged in to plex.tv
msgctxt "#39226"
@ -1454,20 +1368,20 @@ msgid ""
"Running the image cache process can take some time. It will happen in the "
"background. Are you sure you want continue?"
msgstr ""
"L'exécution du processus de mise en cache des images peut prendre un certain"
" temps. Il se fera en arrière-plan. Êtes-vous sûr de vouloir continuer ?"
"Running the image cache process can take some time. It will happen in the "
"background. Are you sure you want continue?"
msgctxt "#39251"
msgid "Reset all existing cache data first?"
msgstr "Supprimer d'abord tout le cache des données ?"
msgstr "Reset all existing cache data first?"
msgctxt "#39303"
msgid "Problems trying to contact plex.tv. Try again later"
msgstr "Problèmes en essayant de connecter plex.tv. Réessayez plus tard"
msgstr "Problèmes en essayant de contacter plex.tv. Réessayez plus tard"
msgctxt "#39304"
msgid "Go to https://plex.tv/pin and enter the code: "
msgstr "Allez sur https://plex.tv/pin et entrez le code: "
msgstr "Allez sur https://plex.tv/pin et entrez le code : "
msgctxt "#39305"
msgid "Could not sign in to plex.tv. Try again later"
@ -1502,8 +1416,8 @@ msgid ""
"Library sync thread has crashed. You should restart Kodi now. Please report "
"this on the forum"
msgstr ""
"La synchronisation de la bibliothèque s'est interrompue. Vous devriez "
"redémarrer Kodi maintenant. Veuillez le signaler sur le forum"
"Library sync thread has crashed. You should restart Kodi now. Please report "
"this on the forum"
msgctxt "#39401"
msgid ""
@ -1550,10 +1464,9 @@ msgid ""
"returned ERRORS. Try lowering the number of sync download threads in the "
"settings. Skipped some items for now."
msgstr ""
"Le serveur Plex n'a pas aimé que vous demandiez autant de données d'un seul "
"coup et a renvoyé des ERREURS. Essayez de réduire le nombre de threads de "
"téléchargement de synchronisation dans les paramètres. Certains éléments ont"
" été ignorés pour l'instant."
"The Plex Server did not like you asking for so much data at once and "
"returned ERRORS. Try lowering the number of sync download threads in the "
"settings. Skipped some items for now."
msgctxt "#39410"
msgid "ERROR in library sync"
@ -1567,10 +1480,6 @@ msgctxt "#39501"
msgid "Collections"
msgstr "Sagas"
msgctxt "#39502"
msgid "PKC On Deck (faster)"
msgstr "PKC On Deck (faster)"
msgctxt "#39600"
msgid ""
"Are you sure you want to reset your local Kodi database? A re-sync of the "
@ -1582,20 +1491,19 @@ msgstr ""
msgctxt "#39601"
msgid "Could not stop the database from running. Please try again later."
msgstr ""
"Impossible de stopper la base de donnée. Veuillez réessayer plus tard."
msgstr "Could not stop the database from running. Please try again later."
msgctxt "#39603"
msgid ""
"Reset all PlexKodiConnect Addon settings? (this is usually NOT recommended "
"and unnecessary!)"
msgstr ""
"Réinitialiser tous les paramètres de PlexKodiConnect Addon? (ceci nest "
"généralement pas recommandée et inutiles!)"
"Réinitialiser tous les paramètres de PlexKodiConnect Addon ? (ceci nest "
"généralement pas recommandée et inutiles !)"
msgctxt "#39700"
msgid "Amazon Alexa (Voice Recognition)"
msgstr "Amazon Alexa (Reconnaissance vocale)"
msgstr "Amazon Alexa (Voice Recognition)"
msgctxt "#39701"
msgid "Activate Alexa"
@ -1603,7 +1511,7 @@ msgstr "Activer Alexa"
msgctxt "#39702"
msgid "Browse by folder"
msgstr "Parcourir par dossier"
msgstr "Parcourir en suivant les répertoires"
# For use with addon.xml (PKC metadata for Kodi, e.g. description)
# Addon Summary
@ -1632,10 +1540,11 @@ msgctxt "#39705"
msgid "Use at your own risk"
msgstr "A utiliser à vos propres risques"
# If user gets prompted to choose between several subtitles to burn in
# If user gets prompted to choose between several subtitles. Leave the number
# one at the beginning of the string!
msgctxt "#39706"
msgid "Don't burn-in any subtitle"
msgstr "Ne pas incruster de sous-titres"
msgid "1 No subtitles"
msgstr "1 pas de sous titre"
# If user gets prompted to choose between several audio/subtitle tracks and
# language is unknown
@ -1689,8 +1598,8 @@ msgstr "Synchro"
# Shown during sync process
msgctxt "#39715"
msgid "Synching playlists"
msgstr "Synchronisation des listes de lecture"
msgid "items"
msgstr "éléments"
# Error message if an xml, e.g. advancedsettings.xml cannot be parsed (xml is
# screwed up; formated the wrong way). Do NOT replace {0} and {1}!
@ -1700,7 +1609,7 @@ msgid ""
" correct your file!"
msgstr ""
"Kodi ne peut pas traiter {0}. PKC ne fonctionnera pas correctement. Merci de"
" visiter {1} et de corriger votre fichier!"
" visiter {1} et de corriger votre fichier !"
# Shown once on first installation to comply with the terms of use of
# themoviedb.org
@ -1708,7 +1617,7 @@ msgctxt "#39717"
msgid "PKC uses free additional artwork from www.themoviedb.org. Many thanks!"
msgstr ""
"PKC utilise gratuitement des artwork additionnels provenant de "
"www.themoviedb.org. Merci beaucoup!"
"www.themoviedb.org. Merci beaucoup !"
# Shown during very first PKC setup only
msgctxt "#39718"
@ -1717,7 +1626,7 @@ msgid ""
"many versions of a media item you posses?"
msgstr ""
"Voulez-vous remplacer vos notes d'utilisateurs personnalisées avec un "
"indicateur du nombre de versions d'un média que vous possédez?"
"indicateur du nombre de versions d'un média que vous possédez ?"
# In PKC Settings under Sync
msgctxt "#39719"

View File

@ -1,7 +1,6 @@
# XBMC Media Center language file
# Translators:
# Croneter None <croneter@gmail.com>, 2019
# Savage93 <savageistheking@gmail.com>, 2021
#
msgid ""
msgstr ""
@ -9,7 +8,7 @@ msgstr ""
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
"Last-Translator: Savage93 <savageistheking@gmail.com>, 2021\n"
"Last-Translator: Croneter None <croneter@gmail.com>, 2019\n"
"Language-Team: Hungarian (Hungary) (https://www.transifex.com/croneter/teams/73837/hu_HU/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -45,17 +44,6 @@ msgstr ""
"Figyelem: \"A következő videó automatikus lejátszása\" be van kapcsolva. Ez "
"megakadályozhatja a PKC megfelelő működését. Kikapcsolja?"
msgctxt "#30004"
msgid ""
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
"random password automatically if you haven't done so already. Please confirm"
" the next dialog that you want to enable the webserver now with Yes."
msgstr ""
"A művészképek gyorsítótárazásához szükség van a Kodi webszerverének "
"bekapcsolására. A PKC beállított egy erős, véletlenszerű jelszót ehhez, "
"amennyiben ezt korábban nem tette meg. Kérem erősítse meg a következő "
"dialógusablakban, hogy be kívánja kapcsolni a webszervert."
msgctxt "#30005"
msgid "Username: "
msgstr "Felhasználónév: "
@ -173,14 +161,6 @@ msgctxt "#30028"
msgid "PKC-only image caching completed"
msgstr "PKC képek gyorsítótárazása befejeződött"
# Warning shown when PKC switches to the Kodi default skin Estuary
msgctxt "#30029"
msgid ""
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
"database resets. Continue?"
msgstr ""
msgctxt "#30030"
msgid "Port Number"
msgstr "Portszám"
@ -280,12 +260,6 @@ msgctxt "#30160"
msgid "Video Quality if Transcoding necessary"
msgstr "Videóminőség, ha transzkódolás szükséges"
msgctxt "#30161"
msgid "Auto-adjust transcoding quality (deactivate for Chromecast)"
msgstr ""
"Transzkódolás minőségének automatikus beállítása (kapcsolja ki Chromecasttal"
" való használatkor)"
msgctxt "#30165"
msgid "Direct Play"
msgstr "Közvetlen lejátszás"
@ -522,8 +496,6 @@ msgstr "Plex művészképek szinkronizálása a szerverről (ajánlott)"
msgctxt "#30503"
msgid "SSL certificate failed to validate. Please check {0} for solutions."
msgstr ""
"Az SSL tanúsítvány hitelesítése sikertelen. Kérem ellenőrizze a(z) {0}-t a "
"megoldásokért."
# PKC Settings, category name
msgctxt "#30506"
@ -623,12 +595,7 @@ msgstr ""
# PKC Settings - Sync Options
msgctxt "#30524"
msgid "Select Plex libraries to sync"
msgstr "Szinkronizálni kívánt Plex könyvtárak kiválasztása"
# PKC Settings - Playback
msgctxt "#30525"
msgid "Skip intro"
msgstr "Bevezető kihagyása"
msgstr ""
# PKC Settings - Playback
msgctxt "#30527"
@ -694,10 +661,8 @@ msgstr "Film-szett/kollekció képek letöltése a FanArtTV-ről"
# PKC Settings - Playback
msgctxt "#30541"
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
msgstr ""
"Transzkódolás: hang- és feliratsávok automatikus kiválasztása a Plex "
"alapértelmezések alapján"
msgid "Don't ask to pick a certain stream/quality"
msgstr "Ne kérdezze meg melyik stream/minőség kerüljön lejátszásra"
# PKC Settings - Playback
msgctxt "#30542"
@ -718,21 +683,6 @@ msgctxt "#30545"
msgid "Force transcode pictures"
msgstr "Képek transzkódolásának kényszerítése"
# PKC Settings - Playback
msgctxt "#30546"
msgid "Pick the first video if several versions are present"
msgstr ""
# PKC Settings - Playback
msgctxt "#30547"
msgid "Who picks the audio stream on playback start?"
msgstr ""
# PKC Settings - Playback
msgctxt "#30548"
msgid "Who picks subtitles on playback start?"
msgstr ""
# Welcome to Plex notification
msgctxt "#33000"
msgid "Welcome"
@ -753,17 +703,6 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "A szerver elérhető"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr "PMS kényszerített transzkódolás"
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr "PMS kényszerített közvetlen streamelés"
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -1009,11 +948,6 @@ msgid "Escape special characters in path (e.g. space to %20)"
msgstr ""
"A speciális karakterek feloldása az elérési útban (pl. szóköz helyett %20)"
# PKC Settings - Customize Paths
msgctxt "#39090"
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
msgstr "Biztonságos karakterek http(s), dav(s) és (s)ftp elérési utakhoz"
# PKC Settings - Customize Paths
msgctxt "#39037"
msgid "Original Plex MOVIE path to replace:"
@ -1119,17 +1053,21 @@ msgstr "Plex szerver keresése"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
"Szinkronizáció és közvetlen elérési utak használata esetén szükséges. "
"Módosítás esetén indítsa újra a Kodi-t!"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Szinkronizáció és közvetlen lejátszás esetén használt"
# PKC Settings, category name
msgctxt "#39057"
msgid "Customize Paths"
msgstr "Útvonalak testreszabása"
# PKC Settings - Appearance Tweaks
msgctxt "#39058"
msgid "Extend Plex TV Series \"On Deck\" view to all shows"
msgstr ""
"A Plex TV sorozatok \"A fedélzeten\" nézetének kiterjesztése az összes "
"sorozatra"
# PKC Settings - Appearance Tweaks
msgctxt "#39059"
msgid "Recently Added: Append show title to episode"
@ -1169,8 +1107,12 @@ msgstr "Kodi felület kényszerített frissítése a lejátszás megállításak
# PKC Settings - Appearance Tweaks
msgctxt "#39066"
msgid "Recently Added: Also show already watched movies"
msgstr "Mostanában hozzáadott: már látott filmek mutatása"
msgid ""
"Recently Added: Also show already watched movies (Refresh Plex "
"playlist/nodes!)"
msgstr ""
"Mostanában hozzáadott: már látott filmek mutatása (frissítse a Plex "
"lejátszási listát/csomópontokat!)"
# PKC Settings - Connection
msgctxt "#39067"
@ -1197,11 +1139,6 @@ msgctxt "#39071"
msgid "Current plex.tv status:"
msgstr "Jelenlegi plex.tv állapot:"
# PKC Settings - Connection
msgctxt "#39072"
msgid "Background sync connection:"
msgstr ""
# PKC Settings, category name
msgctxt "#39073"
msgid "Appearance Tweaks"
@ -1212,10 +1149,11 @@ msgctxt "#39074"
msgid "TV Shows"
msgstr "TV sorozatok"
# PKC Settings - Sync
# PKC Settings - Playback
msgctxt "#39075"
msgid "Verify access to media files while synching"
msgid "Always use default Plex subtitle if possible"
msgstr ""
"Mindig használja az alapértelmezett Plex feliratot amennyiben lehetséges"
# Pop-up during initial sync
msgctxt "#39076"
@ -1229,8 +1167,8 @@ msgstr ""
# PKC Settings - Appearance Tweaks
msgctxt "#39077"
msgid "Maximum number of videos to show in widgets"
msgstr "A widgetekben megjelenített videók maximális száma"
msgid "Number of PMS items to show in widgets (e.g. \"On Deck\")"
msgstr "Megjelenítendő elemek száma a widgetekben (pl. \"A fedélzeten\")"
# PKC Settings - Plex
msgctxt "#39078"
@ -1272,42 +1210,11 @@ msgstr "Közvetlen elérési utak"
# Dialog for manually entering PMS
msgctxt "#39083"
msgid "Enter PMS IP or URL"
msgstr "Adja meg a PMS IP-címét vagy URL-jét"
msgstr ""
# Dialog for manually entering PMS
msgctxt "#39084"
msgid "Enter PMS port"
msgstr "Adja meg a PMS portját"
# PKC settings - Appearance Tweaks
msgctxt "#39085"
msgid "Reload Kodi node files to apply all the settings below"
msgstr ""
"Kodi csomópont fájlok újratöltése az alábbi beállítások alkalmazásához"
# PKC Settings - Connection - Background sync connection status
msgctxt "#39089"
msgid "Alexa connection status:"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39091"
msgid "Timeout - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39092"
msgid "IOError - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39093"
msgid "Suspended - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39094"
msgid "Managed Plex User - not connected"
msgstr ""
msgctxt "#39200"
@ -1362,17 +1269,17 @@ msgstr "Később nézendő"
# e.g. the PMS' name
msgctxt "#39213"
msgid "{0} offline"
msgstr "a(z) {0} nem elérhető"
msgstr ""
msgctxt "#39215"
msgid "Enter your Plex Media Server's IP or URL, Examples are:"
msgstr "Adja meg a Plex médiaszerver IP-címét vagy URL-jét, például:"
msgctxt "#39217"
msgid "Use HTTPS (SSL) connections? Answer should probably be yes."
msgid ""
"Use HTTPS (SSL) connections? With Kodi 18 or later, HTTPS will likely not "
"work!"
msgstr ""
"Kíván HTTPS (SSL) kapcsolatokat használni? Ha nem biztos benne válassza az "
"igent."
msgctxt "#39218"
msgid "Error contacting PMS"
@ -1424,7 +1331,7 @@ msgstr "Be van jelentkezve a plex.tv-be"
# Message in the PKC settings to display the plex.tv username
msgctxt "#39228"
msgid "Plex admin user"
msgstr "Plex adminisztrátor felhasználó"
msgstr ""
# Error message if user could not log in; the actual user name will be
# appended at the end of the string
@ -1435,12 +1342,12 @@ msgstr "Sikertelen bejelentkezés a plex.tv-re a következő felhasználóval"
# Message in the PKC settings to display the plex.tv username
msgctxt "#39230"
msgid "Logged in Plex home user"
msgstr "Bejelentkezett otthoni Plex felhasználó"
msgstr ""
# Message in the PKC settings to change the logged in Plex home user
msgctxt "#39231"
msgid "Change logged in Plex home user"
msgstr "Bejelentkezett otthoni Plex felhasználó megváltoztatása"
msgstr ""
msgctxt "#39250"
msgid ""
@ -1558,10 +1465,6 @@ msgctxt "#39501"
msgid "Collections"
msgstr "Gyűjtemények"
msgctxt "#39502"
msgid "PKC On Deck (faster)"
msgstr "PKC \"a fedélzeten\" (gyorsabb)"
msgctxt "#39600"
msgid ""
"Are you sure you want to reset your local Kodi database? A re-sync of the "
@ -1621,10 +1524,11 @@ msgctxt "#39705"
msgid "Use at your own risk"
msgstr "Csak saját felelősségre használja"
# If user gets prompted to choose between several subtitles to burn in
# If user gets prompted to choose between several subtitles. Leave the number
# one at the beginning of the string!
msgctxt "#39706"
msgid "Don't burn-in any subtitle"
msgstr "Ne égessen be feliratot"
msgid "1 No subtitles"
msgstr "1 Feliratok kikapcsolása"
# If user gets prompted to choose between several audio/subtitle tracks and
# language is unknown
@ -1678,8 +1582,8 @@ msgstr "Szinkronizáció"
# Shown during sync process
msgctxt "#39715"
msgid "Synching playlists"
msgstr "Lejátszási listák szinkronizálása"
msgid "items"
msgstr "elemek"
# Error message if an xml, e.g. advancedsettings.xml cannot be parsed (xml is
# screwed up; formated the wrong way). Do NOT replace {0} and {1}!

View File

@ -47,13 +47,6 @@ msgstr ""
"Attenzione: l'impostazione Kodi \"Avvia il video successivo "
"automaticamente\" è attivata. Questo può interrompere PKC. Disattivare?"
msgctxt "#30004"
msgid ""
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
"random password automatically if you haven't done so already. Please confirm"
" the next dialog that you want to enable the webserver now with Yes."
msgstr ""
msgctxt "#30005"
msgid "Username: "
msgstr "Nome utente:"
@ -171,14 +164,6 @@ msgctxt "#30028"
msgid "PKC-only image caching completed"
msgstr "Cache delle immagini di PKC completato"
# Warning shown when PKC switches to the Kodi default skin Estuary
msgctxt "#30029"
msgid ""
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
"database resets. Continue?"
msgstr ""
msgctxt "#30030"
msgid "Port Number"
msgstr "Porta"
@ -278,10 +263,6 @@ msgctxt "#30160"
msgid "Video Quality if Transcoding necessary"
msgstr "Qualità Video se la Transcodifica è necessaria"
msgctxt "#30161"
msgid "Auto-adjust transcoding quality (deactivate for Chromecast)"
msgstr ""
msgctxt "#30165"
msgid "Direct Play"
msgstr "Riproduzione diretta"
@ -518,7 +499,6 @@ msgstr "Sincronizza le locandine Plex da PMS (raccomandato)"
msgctxt "#30503"
msgid "SSL certificate failed to validate. Please check {0} for solutions."
msgstr ""
"Certificato SSL non valido. Per favore controlla su {0} per risolvere."
# PKC Settings, category name
msgctxt "#30506"
@ -616,11 +596,6 @@ msgstr "Mostra l'avanzamento dello stato di lettura e dei dati utente"
# PKC Settings - Sync Options
msgctxt "#30524"
msgid "Select Plex libraries to sync"
msgstr "Seleziona le librerie Plex da sincronizzazare"
# PKC Settings - Playback
msgctxt "#30525"
msgid "Skip intro"
msgstr ""
# PKC Settings - Playback
@ -688,8 +663,8 @@ msgstr "Scarica collezioni/cofanetti film da FanartTV"
# PKC Settings - Playback
msgctxt "#30541"
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
msgstr ""
msgid "Don't ask to pick a certain stream/quality"
msgstr "Non chiedere di scegliere la qualità dello stream"
# PKC Settings - Playback
msgctxt "#30542"
@ -710,21 +685,6 @@ msgctxt "#30545"
msgid "Force transcode pictures"
msgstr "Forza transcodifica immagini"
# PKC Settings - Playback
msgctxt "#30546"
msgid "Pick the first video if several versions are present"
msgstr ""
# PKC Settings - Playback
msgctxt "#30547"
msgid "Who picks the audio stream on playback start?"
msgstr ""
# PKC Settings - Playback
msgctxt "#30548"
msgid "Who picks subtitles on playback start?"
msgstr ""
# Welcome to Plex notification
msgctxt "#33000"
msgid "Welcome"
@ -745,17 +705,6 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Il server è online"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -1002,11 +951,6 @@ msgstr ""
"Esegui l'escape dei caratteri speciali nel percorso (es. \"spazio\" "
"trasformato in \"%20\")"
# PKC Settings - Customize Paths
msgctxt "#39090"
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
msgstr ""
# PKC Settings - Customize Paths
msgctxt "#39037"
msgid "Original Plex MOVIE path to replace:"
@ -1114,15 +1058,21 @@ msgstr "Ricerca del server Plex"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgid "Used by Sync and when attempting to Direct Play"
msgstr ""
"Utilizzato dalla sincronizzazione e quando si utilizza la riproduzione "
"diretta"
# PKC Settings, category name
msgctxt "#39057"
msgid "Customize Paths"
msgstr "Personalizza i percorsi"
# PKC Settings - Appearance Tweaks
msgctxt "#39058"
msgid "Extend Plex TV Series \"On Deck\" view to all shows"
msgstr "Estendi la vista \"On Deck\" delle Serie TV a tutte le serie"
# PKC Settings - Appearance Tweaks
msgctxt "#39059"
msgid "Recently Added: Append show title to episode"
@ -1164,8 +1114,12 @@ msgstr "Forza aggiornamento della skin Kodi dopo lo stop di un contenuto"
# PKC Settings - Appearance Tweaks
msgctxt "#39066"
msgid "Recently Added: Also show already watched movies"
msgstr "Aggiunti di recente: Mostra anche film già visti"
msgid ""
"Recently Added: Also show already watched movies (Refresh Plex "
"playlist/nodes!)"
msgstr ""
"Aggiunti di recente: mostra anche i film già visti (Aggiornare playlist/nodi"
" Plex!)"
# PKC Settings - Connection
msgctxt "#39067"
@ -1192,11 +1146,6 @@ msgctxt "#39071"
msgid "Current plex.tv status:"
msgstr "Stato attuale di plex.tv:"
# PKC Settings - Connection
msgctxt "#39072"
msgid "Background sync connection:"
msgstr ""
# PKC Settings, category name
msgctxt "#39073"
msgid "Appearance Tweaks"
@ -1207,10 +1156,10 @@ msgctxt "#39074"
msgid "TV Shows"
msgstr "Serie TV"
# PKC Settings - Sync
# PKC Settings - Playback
msgctxt "#39075"
msgid "Verify access to media files while synching"
msgstr ""
msgid "Always use default Plex subtitle if possible"
msgstr "Usa sempre i sottotitoli predefiniti di Plex se possibile"
# Pop-up during initial sync
msgctxt "#39076"
@ -1223,8 +1172,8 @@ msgstr ""
# PKC Settings - Appearance Tweaks
msgctxt "#39077"
msgid "Maximum number of videos to show in widgets"
msgstr "Numero massimo di video da mostrare nei widget"
msgid "Number of PMS items to show in widgets (e.g. \"On Deck\")"
msgstr "Numero di elementi PMS da mostrare nei widget (es. \"On Deck\")"
# PKC Settings - Plex
msgctxt "#39078"
@ -1265,43 +1214,11 @@ msgstr "Percorsi Diretti"
# Dialog for manually entering PMS
msgctxt "#39083"
msgid "Enter PMS IP or URL"
msgstr "Inserisci l'URL o l'IP del PMS"
msgstr ""
# Dialog for manually entering PMS
msgctxt "#39084"
msgid "Enter PMS port"
msgstr "Inserisci la porta del PMS"
# PKC settings - Appearance Tweaks
msgctxt "#39085"
msgid "Reload Kodi node files to apply all the settings below"
msgstr ""
"Ricarica i nodi di file di Kodi per applicare tutte le impostazioni di "
"sotto"
# PKC Settings - Connection - Background sync connection status
msgctxt "#39089"
msgid "Alexa connection status:"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39091"
msgid "Timeout - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39092"
msgid "IOError - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39093"
msgid "Suspended - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39094"
msgid "Managed Plex User - not connected"
msgstr ""
msgctxt "#39200"
@ -1358,14 +1275,16 @@ msgstr "Guarda più tardi"
# e.g. the PMS' name
msgctxt "#39213"
msgid "{0} offline"
msgstr "{0} offline"
msgstr ""
msgctxt "#39215"
msgid "Enter your Plex Media Server's IP or URL, Examples are:"
msgstr "Inserisci l'IP o l'URL del tuo Plex Media Server. Ad esempio:"
msgctxt "#39217"
msgid "Use HTTPS (SSL) connections? Answer should probably be yes."
msgid ""
"Use HTTPS (SSL) connections? With Kodi 18 or later, HTTPS will likely not "
"work!"
msgstr ""
msgctxt "#39218"
@ -1418,7 +1337,7 @@ msgstr "Autenticazione concessa su plex.tv"
# Message in the PKC settings to display the plex.tv username
msgctxt "#39228"
msgid "Plex admin user"
msgstr "Utente di amministrazione Plex"
msgstr ""
# Error message if user could not log in; the actual user name will be
# appended at the end of the string
@ -1429,12 +1348,12 @@ msgstr "Autenticazione dell'utente fallita su plex.tv "
# Message in the PKC settings to display the plex.tv username
msgctxt "#39230"
msgid "Logged in Plex home user"
msgstr "Utente plex autenticato"
msgstr ""
# Message in the PKC settings to change the logged in Plex home user
msgctxt "#39231"
msgid "Change logged in Plex home user"
msgstr "Cambia utente Plex"
msgstr ""
msgctxt "#39250"
msgid ""
@ -1554,10 +1473,6 @@ msgctxt "#39501"
msgid "Collections"
msgstr "Collezioni"
msgctxt "#39502"
msgid "PKC On Deck (faster)"
msgstr "PKC On Desk (più veloce)"
msgctxt "#39600"
msgid ""
"Are you sure you want to reset your local Kodi database? A re-sync of the "
@ -1618,10 +1533,11 @@ msgctxt "#39705"
msgid "Use at your own risk"
msgstr "Usa a tuo rischio e pericolo"
# If user gets prompted to choose between several subtitles to burn in
# If user gets prompted to choose between several subtitles. Leave the number
# one at the beginning of the string!
msgctxt "#39706"
msgid "Don't burn-in any subtitle"
msgstr ""
msgid "1 No subtitles"
msgstr "1 No sottotitoli"
# If user gets prompted to choose between several audio/subtitle tracks and
# language is unknown
@ -1675,8 +1591,8 @@ msgstr "Sync"
# Shown during sync process
msgctxt "#39715"
msgid "Synching playlists"
msgstr "Sincronizzazione delle playlist"
msgid "items"
msgstr "contenuti"
# Error message if an xml, e.g. advancedsettings.xml cannot be parsed (xml is
# screwed up; formated the wrong way). Do NOT replace {0} and {1}!

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
# XBMC Media Center language file
# Translators:
# marcisbe <marcisbe@gmail.com>, 2020
# marcisbe <marcisbe@gmail.com>, 2019
#
msgid ""
msgstr ""
@ -8,7 +8,7 @@ msgstr ""
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
"Last-Translator: marcisbe <marcisbe@gmail.com>, 2020\n"
"Last-Translator: marcisbe <marcisbe@gmail.com>, 2019\n"
"Language-Team: Latvian (Latvia) (https://www.transifex.com/croneter/teams/73837/lv_LV/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -44,13 +44,6 @@ msgstr ""
"Brīdinājums: Kodi iestatījums \"Atskaņot nākamo video automātiski\" ir "
"ieslēgts. Tas var salauzt PKC. Izslēgt?"
msgctxt "#30004"
msgid ""
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
"random password automatically if you haven't done so already. Please confirm"
" the next dialog that you want to enable the webserver now with Yes."
msgstr ""
msgctxt "#30005"
msgid "Username: "
msgstr "Lietotājvārds:"
@ -102,7 +95,7 @@ msgstr "Savienojums"
# Pop-up notification if user tried to manually initiate fanart download
msgctxt "#30015"
msgid "Fanart download already running"
msgstr "Fanart lejupielāde jau notiek"
msgstr ""
msgctxt "#30016"
msgid "Device Name"
@ -164,14 +157,6 @@ msgstr "Prefikss Kodi spēļsaraksta nosaukumā, lai iniciētu sinhronizēšanu"
# PKC settings artwork options: status info
msgctxt "#30028"
msgid "PKC-only image caching completed"
msgstr "Tikai-PKC attēlu kešošana pabeigta"
# Warning shown when PKC switches to the Kodi default skin Estuary
msgctxt "#30029"
msgid ""
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
"database resets. Continue?"
msgstr ""
msgctxt "#30030"
@ -273,10 +258,6 @@ msgctxt "#30160"
msgid "Video Quality if Transcoding necessary"
msgstr "Video Kvalitāte, ja nepieciešama Pārkodēšana"
msgctxt "#30161"
msgid "Auto-adjust transcoding quality (deactivate for Chromecast)"
msgstr "Automātiski pielāgot transkodēšanas kvalitāti (deaktivizē Chromecast)"
msgctxt "#30165"
msgid "Direct Play"
msgstr "Tiešā Atskaņošana"
@ -339,14 +320,12 @@ msgid ""
"In the following window, enter the server's hostname (or IP) where your Plex"
" media resides. Mind the case!"
msgstr ""
"Sekojošajā logā ievadi servera, kur atrodas tavs Plex, vārdu (vai IP). Ņem "
"vērā lielos mazos burtus!"
# For setting up direct paths and adding network credentials - input window
# for hostname
msgctxt "#30201"
msgid "Enter server hostname (or IP)"
msgstr "Ievadi servera vārdu (vai IP)"
msgstr ""
# For setting up direct paths and adding network credentials
msgctxt "#30202"
@ -354,24 +333,22 @@ msgid ""
"In the following window, enter the network protocol you would like to use. "
"This is likely 'smb'."
msgstr ""
"Sekojošajā logā ievadi tīkla protokolu, kuru vēlies izmanot. Visdrīzāk tas "
"ir 'smb'."
# For setting up direct paths and adding network credentials - input window
# protocol
msgctxt "#30203"
msgid "Enter network protocol"
msgstr "Ievadi tīkla protokolu"
msgstr ""
# For setting up direct paths and adding network credentials
msgctxt "#30204"
msgid "The hostname or IP '{0}' that you entered is not valid."
msgstr "Servera vārds vai IP '{0}', kuru ievadīji, nav pareizs."
msgstr ""
# For setting up direct paths and adding network credentials
msgctxt "#30205"
msgid "The protocol '{0}' that you entered is not supported."
msgstr "Protokols '{0}', kuru ievadīji nav atbalstīts."
msgstr ""
# Video node naming for random e.g. movies
msgctxt "#30227"
@ -491,8 +468,6 @@ msgid ""
"Could not change the Kodi settings file {0}. PKC might not work correctly. "
"Error: {1}"
msgstr ""
"Nevar izmainīt Kodi iestatījumu failu {0}. PKC var nestrādāt pareizi. Kļūda:"
" {1}"
# PKC Settings - Connection
msgctxt "#30500"
@ -507,12 +482,12 @@ msgstr "Klienta SSL sertifikāts"
# PKC Settings - Artwork
msgctxt "#30502"
msgid "Sync Plex artwork from the PMS (recommended)"
msgstr "Sinhronizēt Plex attēlus no PMS (ieteikts)"
msgstr ""
# Message shown if SSL HTTPS certificate fails
msgctxt "#30503"
msgid "SSL certificate failed to validate. Please check {0} for solutions."
msgstr "Neizdevās pārbaudīt SSL sertifikātu. Lūdzu skaties {0} risinājumus."
msgstr ""
# PKC Settings, category name
msgctxt "#30506"
@ -606,11 +581,6 @@ msgstr ""
# PKC Settings - Sync Options
msgctxt "#30524"
msgid "Select Plex libraries to sync"
msgstr "Izvēlies kuras Plex bibliotēkas sinhronizēt"
# PKC Settings - Playback
msgctxt "#30525"
msgid "Skip intro"
msgstr ""
# PKC Settings - Playback
@ -677,8 +647,8 @@ msgstr "Lejupielādēt filmu komplektu/kolekciju attēlus no FanArtTV"
# PKC Settings - Playback
msgctxt "#30541"
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
msgstr ""
msgid "Don't ask to pick a certain stream/quality"
msgstr "Nejautāt par konkrētas kvalitātes/straumes izvēli"
# PKC Settings - Playback
msgctxt "#30542"
@ -699,21 +669,6 @@ msgctxt "#30545"
msgid "Force transcode pictures"
msgstr "Uzspiest attēlu pārkodēšanu"
# PKC Settings - Playback
msgctxt "#30546"
msgid "Pick the first video if several versions are present"
msgstr ""
# PKC Settings - Playback
msgctxt "#30547"
msgid "Who picks the audio stream on playback start?"
msgstr ""
# PKC Settings - Playback
msgctxt "#30548"
msgid "Who picks subtitles on playback start?"
msgstr ""
# Welcome to Plex notification
msgctxt "#33000"
msgid "Welcome"
@ -734,17 +689,6 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Serveris ir tiešsaistē"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr "PMS uzspiestā transkodēšana"
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr "PMS uzspiestā tiešā straumēšana"
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -752,11 +696,11 @@ msgstr "Nederīgs lietotājvārds vai parole"
msgctxt "#33010"
msgid "User is unauthorized for server {0}"
msgstr "Lietotājs ir neautorizēts serverī {0}"
msgstr ""
msgctxt "#33011"
msgid "Plex.tv did not provide us a valid list of Plex users, sorry."
msgstr "Plex.tv mums nedeva derīgu Plex lietotāju sarakstu, piedod."
msgstr ""
# Dialog before playback
msgctxt "#33013"
@ -893,7 +837,7 @@ msgstr "Atiestatīt Kodi datubāzi un iespējams atiestatīt PlexKodiConnect"
# PKC Settings - Artwork
msgctxt "#39020"
msgid "Cache all images to Kodi texture cache now"
msgstr "Iekešot visus attēlus Kodi tekstūru kešatmiņā tagad"
msgstr ""
# Appended to a listed PMS if it is in the same LAN network as PKC
msgctxt "#39022"
@ -981,11 +925,6 @@ msgctxt "#39036"
msgid "Escape special characters in path (e.g. space to %20)"
msgstr ""
# PKC Settings - Customize Paths
msgctxt "#39090"
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
msgstr ""
# PKC Settings - Customize Paths
msgctxt "#39037"
msgid "Original Plex MOVIE path to replace:"
@ -1062,7 +1001,7 @@ msgstr "Nekas nestrādā? Pamēģini pilnu atiestatīšanu!"
# PKC Settings - Connection
msgctxt "#39050"
msgid "Choose Plex Server from a list"
msgstr "Izvēlies Plex serveri no saraksta"
msgstr ""
# PKC Settings - Sync
msgctxt "#39051"
@ -1091,15 +1030,19 @@ msgstr "Meklē Plex Serveri"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Izmanto Sinhronizējot un tad, kad mēģina Tiešo Atskaņošanu"
# PKC Settings, category name
msgctxt "#39057"
msgid "Customize Paths"
msgstr "Pielāgot Ceļus"
# PKC Settings - Appearance Tweaks
msgctxt "#39058"
msgid "Extend Plex TV Series \"On Deck\" view to all shows"
msgstr "Paplašināt Plext TV Seriālu \"Izcelts\" skatījumu uz visiem seriāliem"
# PKC Settings - Appearance Tweaks
msgctxt "#39059"
msgid "Recently Added: Append show title to episode"
@ -1139,8 +1082,12 @@ msgstr "Uzspiest atjaunošanu Kodi ādiņai apturot atskaņošanu"
# PKC Settings - Appearance Tweaks
msgctxt "#39066"
msgid "Recently Added: Also show already watched movies"
msgstr "Nesen Pievienots: Rādīt arī jau skatītas filmas"
msgid ""
"Recently Added: Also show already watched movies (Refresh Plex "
"playlist/nodes!)"
msgstr ""
"Nesen Pievienots: Rādīt arī jau noskatītas filmas (Atjaunināt Plex "
"spēļsarakstu/mezglus!)"
# PKC Settings - Connection
msgctxt "#39067"
@ -1150,7 +1097,7 @@ msgstr "Tavs pašreizējais Plex Media Serveris:"
# PKC Settings - Connection
msgctxt "#39068"
msgid "Manually enter Plex Media Server address"
msgstr "Pats ievadi Plex Media Server adresi"
msgstr ""
# PKC Settings - Connection
msgctxt "#39069"
@ -1167,11 +1114,6 @@ msgctxt "#39071"
msgid "Current plex.tv status:"
msgstr "Pašreizējais plex.tv statuss:"
# PKC Settings - Connection
msgctxt "#39072"
msgid "Background sync connection:"
msgstr ""
# PKC Settings, category name
msgctxt "#39073"
msgid "Appearance Tweaks"
@ -1182,10 +1124,10 @@ msgctxt "#39074"
msgid "TV Shows"
msgstr "Seriāli"
# PKC Settings - Sync
# PKC Settings - Playback
msgctxt "#39075"
msgid "Verify access to media files while synching"
msgstr ""
msgid "Always use default Plex subtitle if possible"
msgstr "Vienmēr lietot noklusētos Plex titrus, ja iespējams"
# Pop-up during initial sync
msgctxt "#39076"
@ -1196,8 +1138,8 @@ msgstr ""
# PKC Settings - Appearance Tweaks
msgctxt "#39077"
msgid "Maximum number of videos to show in widgets"
msgstr ""
msgid "Number of PMS items to show in widgets (e.g. \"On Deck\")"
msgstr "PMS vienumu skaits, kurus rādīt logrīkos (piem. \"Izcelts\")"
# PKC Settings - Plex
msgctxt "#39078"
@ -1223,51 +1165,21 @@ msgstr ""
# Button text for choosing PKC mode
msgctxt "#39081"
msgid "Add-on Paths"
msgstr "Spraudņu Ceļš"
msgstr ""
# Button text for choosing PKC mode
msgctxt "#39082"
msgid "Direct Paths"
msgstr "Tiešie Ceļi"
msgstr ""
# Dialog for manually entering PMS
msgctxt "#39083"
msgid "Enter PMS IP or URL"
msgstr "Ievadi PMS IP vai URL"
msgstr ""
# Dialog for manually entering PMS
msgctxt "#39084"
msgid "Enter PMS port"
msgstr "Ievadi PMS portu"
# PKC settings - Appearance Tweaks
msgctxt "#39085"
msgid "Reload Kodi node files to apply all the settings below"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39089"
msgid "Alexa connection status:"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39091"
msgid "Timeout - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39092"
msgid "IOError - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39093"
msgid "Suspended - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39094"
msgid "Managed Plex User - not connected"
msgstr ""
msgctxt "#39200"
@ -1280,7 +1192,7 @@ msgstr "Iestatījumi"
msgctxt "#39204"
msgid "Perform manual library sync"
msgstr "Veikt manuālu bibliotēkas sinhronizēšanu"
msgstr ""
# Error message
msgctxt "#39205"
@ -1318,14 +1230,16 @@ msgstr "Skatīties vēlāk"
# e.g. the PMS' name
msgctxt "#39213"
msgid "{0} offline"
msgstr "{0} bezaistē"
msgstr ""
msgctxt "#39215"
msgid "Enter your Plex Media Server's IP or URL, Examples are:"
msgstr ""
msgctxt "#39217"
msgid "Use HTTPS (SSL) connections? Answer should probably be yes."
msgid ""
"Use HTTPS (SSL) connections? With Kodi 18 or later, HTTPS will likely not "
"work!"
msgstr ""
msgctxt "#39218"
@ -1357,32 +1271,32 @@ msgstr ""
msgctxt "#39224"
msgid "Refresh all"
msgstr "Atjaunot visu"
msgstr ""
msgctxt "#39225"
msgid "Missing only"
msgstr "Tikai trūkstošo"
msgstr ""
# Message in the PKC settings if user has not logged in to plex.tv
msgctxt "#39226"
msgid "Not logged in to plex.tv"
msgstr "Nav pieteicies plex.tv"
msgstr ""
# Message in the PKC settings if user is logged in to plex.tv
msgctxt "#39227"
msgid "Logged in to plex.tv"
msgstr "Pieteicies plex.tv"
msgstr ""
# Message in the PKC settings to display the plex.tv username
msgctxt "#39228"
msgid "Plex admin user"
msgstr "Plex admin user"
msgstr ""
# Error message if user could not log in; the actual user name will be
# appended at the end of the string
msgctxt "#39229"
msgid "Login failed with plex.tv for user"
msgstr "Lietotāja pieteikšanās plex.tv neizdevās"
msgstr ""
# Message in the PKC settings to display the plex.tv username
msgctxt "#39230"
@ -1430,11 +1344,11 @@ msgstr ""
msgctxt "#39309"
msgid "Please try again."
msgstr "Lūdzu mēģini vēlreiz"
msgstr ""
msgctxt "#39310"
msgid "unknown"
msgstr "nezināms"
msgstr ""
msgctxt "#39311"
msgid "or press No to not sign in."
@ -1485,7 +1399,7 @@ msgstr ""
msgctxt "#39410"
msgid "ERROR in library sync"
msgstr "KĻŪDA sinhronizējot bibliotēku"
msgstr ""
msgctxt "#39500"
msgid "On Deck"
@ -1493,11 +1407,7 @@ msgstr "Izcelts"
msgctxt "#39501"
msgid "Collections"
msgstr "Kolekcijas"
msgctxt "#39502"
msgid "PKC On Deck (faster)"
msgstr "PKC On Deck (ātrāk)"
msgstr ""
msgctxt "#39600"
msgid ""
@ -1547,30 +1457,31 @@ msgstr ""
# Addon Disclaimer
msgctxt "#39705"
msgid "Use at your own risk"
msgstr "Lieto uz savu atbildību"
msgstr ""
# If user gets prompted to choose between several subtitles to burn in
# If user gets prompted to choose between several subtitles. Leave the number
# one at the beginning of the string!
msgctxt "#39706"
msgid "Don't burn-in any subtitle"
msgid "1 No subtitles"
msgstr ""
# If user gets prompted to choose between several audio/subtitle tracks and
# language is unknown
msgctxt "#39707"
msgid "unknown"
msgstr "nezināms"
msgstr ""
# If user gets prompted to choose between several subtitles and Plex adds the
# "default" flag
msgctxt "#39708"
msgid "Default"
msgstr "Noklusējums"
msgstr ""
# If user gets prompted to choose between several subtitles and Plex adds the
# "forced" flag
msgctxt "#39709"
msgid "Forced"
msgstr "Uzspiests"
msgstr ""
# If user gets prompted to choose between several subtitles the subtitle
# cannot be downloaded (has no 'key' attribute from the PMS), the subtitle
@ -1590,22 +1501,22 @@ msgstr ""
# Shown during sync process
msgctxt "#39712"
msgid "downloaded"
msgstr "lejupielādēts"
msgstr ""
# Shown during sync process
msgctxt "#39713"
msgid "processed"
msgstr "apstrādāts"
msgstr ""
# Shown during sync process
msgctxt "#39714"
msgid "Sync"
msgstr "Sinhronizēt"
msgstr ""
# Shown during sync process
msgctxt "#39715"
msgid "Synching playlists"
msgstr "Sinhronizē spēļsarakstus"
msgid "items"
msgstr ""
# Error message if an xml, e.g. advancedsettings.xml cannot be parsed (xml is
# screwed up; formated the wrong way). Do NOT replace {0} and {1}!

View File

@ -2,9 +2,8 @@
# Translators:
# Croneter None <croneter@gmail.com>, 2017
# Michiel van Baak <michiel@vanbaak.info>, 2019
# Panja0 <panja0@protonmail.com>, 2019
# Panja Nul <panja0@protonmail.com>, 2019
# Nick Corthals <corthals.nick@gmail.com>, 2019
# Rick van Soest <r.vansoest@gmail.com>, 2019
#
msgid ""
msgstr ""
@ -12,7 +11,7 @@ msgstr ""
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
"Last-Translator: Rick van Soest <r.vansoest@gmail.com>, 2019\n"
"Last-Translator: Nick Corthals <corthals.nick@gmail.com>, 2019\n"
"Language-Team: Dutch (Netherlands) (https://www.transifex.com/croneter/teams/73837/nl_NL/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -48,13 +47,6 @@ msgstr ""
"Waarschuwing: De kodi instelling 'Automatisch volgende video afspelen' is "
"actief. Dit kan voor problemen zorgen. Instelling deactiveren?"
msgctxt "#30004"
msgid ""
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
"random password automatically if you haven't done so already. Please confirm"
" the next dialog that you want to enable the webserver now with Yes."
msgstr ""
msgctxt "#30005"
msgid "Username: "
msgstr "Gebruikersnaam: "
@ -170,14 +162,6 @@ msgctxt "#30028"
msgid "PKC-only image caching completed"
msgstr "PKC afbeelding caching voltooid"
# Warning shown when PKC switches to the Kodi default skin Estuary
msgctxt "#30029"
msgid ""
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
"database resets. Continue?"
msgstr ""
msgctxt "#30030"
msgid "Port Number"
msgstr "Poortnummer"
@ -277,10 +261,6 @@ msgctxt "#30160"
msgid "Video Quality if Transcoding necessary"
msgstr "Videokwaliteit als Transcoden nodig is"
msgctxt "#30161"
msgid "Auto-adjust transcoding quality (deactivate for Chromecast)"
msgstr ""
msgctxt "#30165"
msgid "Direct Play"
msgstr "Direct Play"
@ -616,11 +596,6 @@ msgctxt "#30524"
msgid "Select Plex libraries to sync"
msgstr "Selecteer Plex-bibliotheken om te synchroniseren"
# PKC Settings - Playback
msgctxt "#30525"
msgid "Skip intro"
msgstr ""
# PKC Settings - Playback
msgctxt "#30527"
msgid "Ignore specials in next episodes"
@ -685,8 +660,8 @@ msgstr "Download film set/collectie artwork van FanArtTV"
# PKC Settings - Playback
msgctxt "#30541"
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
msgstr ""
msgid "Don't ask to pick a certain stream/quality"
msgstr "Niet vragen om een bepaalde stream/kwaliteit te kiezen"
# PKC Settings - Playback
msgctxt "#30542"
@ -707,21 +682,6 @@ msgctxt "#30545"
msgid "Force transcode pictures"
msgstr "Forceer transcoden van foto's"
# PKC Settings - Playback
msgctxt "#30546"
msgid "Pick the first video if several versions are present"
msgstr ""
# PKC Settings - Playback
msgctxt "#30547"
msgid "Who picks the audio stream on playback start?"
msgstr ""
# PKC Settings - Playback
msgctxt "#30548"
msgid "Who picks subtitles on playback start?"
msgstr ""
# Welcome to Plex notification
msgctxt "#33000"
msgid "Welcome"
@ -742,17 +702,6 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Server is online"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -992,11 +941,6 @@ msgctxt "#39036"
msgid "Escape special characters in path (e.g. space to %20)"
msgstr "Pas speciale tekens aan in pad (b.v. spatie naar %20)"
# PKC Settings - Customize Paths
msgctxt "#39090"
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
msgstr ""
# PKC Settings - Customize Paths
msgctxt "#39037"
msgid "Original Plex MOVIE path to replace:"
@ -1103,15 +1047,19 @@ msgstr "Plex Server zoeken"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Gebruikt door Sync en bij Direct Play"
# PKC Settings, category name
msgctxt "#39057"
msgid "Customize Paths"
msgstr "Aanpassen van paden"
# PKC Settings - Appearance Tweaks
msgctxt "#39058"
msgid "Extend Plex TV Series \"On Deck\" view to all shows"
msgstr "On Deck van TV-series laat alle series zien"
# PKC Settings - Appearance Tweaks
msgctxt "#39059"
msgid "Recently Added: Append show title to episode"
@ -1151,8 +1099,12 @@ msgstr "Forceer Kodi thema reset bij afspelen stoppen"
# PKC Settings - Appearance Tweaks
msgctxt "#39066"
msgid "Recently Added: Also show already watched movies"
msgstr "Pas toegevoegd: Toon tevens de reeds bekeken films."
msgid ""
"Recently Added: Also show already watched movies (Refresh Plex "
"playlist/nodes!)"
msgstr ""
"Onlangs Toegevoegd: Toon ook al bekeken films (Ververs Plex "
"afspeellijst/nodes!)"
# PKC Settings - Connection
msgctxt "#39067"
@ -1179,11 +1131,6 @@ msgctxt "#39071"
msgid "Current plex.tv status:"
msgstr "Huidige status van de plex.tv:"
# PKC Settings - Connection
msgctxt "#39072"
msgid "Background sync connection:"
msgstr ""
# PKC Settings, category name
msgctxt "#39073"
msgid "Appearance Tweaks"
@ -1194,10 +1141,10 @@ msgctxt "#39074"
msgid "TV Shows"
msgstr "TV series"
# PKC Settings - Sync
# PKC Settings - Playback
msgctxt "#39075"
msgid "Verify access to media files while synching"
msgstr ""
msgid "Always use default Plex subtitle if possible"
msgstr "Gebruik indien mogelijk altijd standaard Plex ondertitels"
# Pop-up during initial sync
msgctxt "#39076"
@ -1211,8 +1158,8 @@ msgstr ""
# PKC Settings - Appearance Tweaks
msgctxt "#39077"
msgid "Maximum number of videos to show in widgets"
msgstr "Maximale aantal video's getoond in widgets"
msgid "Number of PMS items to show in widgets (e.g. \"On Deck\")"
msgstr "Aantal PMS items in widgets laten zien (bijv. \"On Deck\")"
# PKC Settings - Plex
msgctxt "#39078"
@ -1260,38 +1207,6 @@ msgctxt "#39084"
msgid "Enter PMS port"
msgstr "Kies PMS poort"
# PKC settings - Appearance Tweaks
msgctxt "#39085"
msgid "Reload Kodi node files to apply all the settings below"
msgstr ""
"Herlaad de Kodi node bestanden om alles onderstaande instellingen door te "
"voeren"
# PKC Settings - Connection - Background sync connection status
msgctxt "#39089"
msgid "Alexa connection status:"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39091"
msgid "Timeout - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39092"
msgid "IOError - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39093"
msgid "Suspended - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39094"
msgid "Managed Plex User - not connected"
msgstr ""
msgctxt "#39200"
msgid "Log-out Plex Home User "
msgstr "Log-out Plex Home gebruiker "
@ -1349,8 +1264,12 @@ msgid "Enter your Plex Media Server's IP or URL, Examples are:"
msgstr "Voer Plex Media Server adres in. Voorbeelden zijn:"
msgctxt "#39217"
msgid "Use HTTPS (SSL) connections? Answer should probably be yes."
msgid ""
"Use HTTPS (SSL) connections? With Kodi 18 or later, HTTPS will likely not "
"work!"
msgstr ""
"Gebruik HTTPS (SSL) connectie? HTTPS zal waarschijnlijk niet werken met Kodi"
" 18 of hoger!"
msgctxt "#39218"
msgid "Error contacting PMS"
@ -1531,10 +1450,6 @@ msgctxt "#39501"
msgid "Collections"
msgstr "Verzamelingen"
msgctxt "#39502"
msgid "PKC On Deck (faster)"
msgstr "PKC On Deck (sneller)"
msgctxt "#39600"
msgid ""
"Are you sure you want to reset your local Kodi database? A re-sync of the "
@ -1594,10 +1509,11 @@ msgctxt "#39705"
msgid "Use at your own risk"
msgstr "Gebruik op eigen risico"
# If user gets prompted to choose between several subtitles to burn in
# If user gets prompted to choose between several subtitles. Leave the number
# one at the beginning of the string!
msgctxt "#39706"
msgid "Don't burn-in any subtitle"
msgstr ""
msgid "1 No subtitles"
msgstr "1 Geen ondertiteling"
# If user gets prompted to choose between several audio/subtitle tracks and
# language is unknown
@ -1651,8 +1567,8 @@ msgstr "Sync"
# Shown during sync process
msgctxt "#39715"
msgid "Synching playlists"
msgstr "Afspeellijsten synchroniseren"
msgid "items"
msgstr "items"
# Error message if an xml, e.g. advancedsettings.xml cannot be parsed (xml is
# screwed up; formated the wrong way). Do NOT replace {0} and {1}!

View File

@ -46,13 +46,6 @@ msgstr ""
"Advarsel: Kodi instilling \"Automatisk avspilling av neste video\" er "
"aktivert. Det kan medføre problemer med PKC. Ønsker du å deaktivere?"
msgctxt "#30004"
msgid ""
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
"random password automatically if you haven't done so already. Please confirm"
" the next dialog that you want to enable the webserver now with Yes."
msgstr ""
msgctxt "#30005"
msgid "Username: "
msgstr "Brukernavn:"
@ -172,14 +165,6 @@ msgctxt "#30028"
msgid "PKC-only image caching completed"
msgstr "PKC mellomlagring av bilder gjennomført"
# Warning shown when PKC switches to the Kodi default skin Estuary
msgctxt "#30029"
msgid ""
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
"database resets. Continue?"
msgstr ""
msgctxt "#30030"
msgid "Port Number"
msgstr "Portnummer"
@ -279,10 +264,6 @@ msgctxt "#30160"
msgid "Video Quality if Transcoding necessary"
msgstr "Videokvalitet hvis transkoding er nødvendig"
msgctxt "#30161"
msgid "Auto-adjust transcoding quality (deactivate for Chromecast)"
msgstr ""
msgctxt "#30165"
msgid "Direct Play"
msgstr "Direct Play"
@ -615,11 +596,6 @@ msgctxt "#30524"
msgid "Select Plex libraries to sync"
msgstr "Velg Plex bibliotek som skal synkroniseres"
# PKC Settings - Playback
msgctxt "#30525"
msgid "Skip intro"
msgstr ""
# PKC Settings - Playback
msgctxt "#30527"
msgid "Ignore specials in next episodes"
@ -682,8 +658,8 @@ msgstr "Last ned filmsamling-kunst fra FanArtTV"
# PKC Settings - Playback
msgctxt "#30541"
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
msgstr ""
msgid "Don't ask to pick a certain stream/quality"
msgstr "Ikke spør om å velge en utvalgt strøm/kvalitet"
# PKC Settings - Playback
msgctxt "#30542"
@ -704,21 +680,6 @@ msgctxt "#30545"
msgid "Force transcode pictures"
msgstr "Tving transkoding av bilde"
# PKC Settings - Playback
msgctxt "#30546"
msgid "Pick the first video if several versions are present"
msgstr ""
# PKC Settings - Playback
msgctxt "#30547"
msgid "Who picks the audio stream on playback start?"
msgstr ""
# PKC Settings - Playback
msgctxt "#30548"
msgid "Who picks subtitles on playback start?"
msgstr ""
# Welcome to Plex notification
msgctxt "#33000"
msgid "Welcome"
@ -739,17 +700,6 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Server er tilgjengelig"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -986,11 +936,6 @@ msgctxt "#39036"
msgid "Escape special characters in path (e.g. space to %20)"
msgstr "Unngå spesielle tegn i stier (eksempel mellomrom til %20)"
# PKC Settings - Customize Paths
msgctxt "#39090"
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
msgstr ""
# PKC Settings - Customize Paths
msgctxt "#39037"
msgid "Original Plex MOVIE path to replace:"
@ -1096,15 +1041,19 @@ msgstr "Søker etter Plex Media Server"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Brukes av Sync og ved direkte avspilling"
# PKC Settings, category name
msgctxt "#39057"
msgid "Customize Paths"
msgstr "Egendefinerte stier"
# PKC Settings - Appearance Tweaks
msgctxt "#39058"
msgid "Extend Plex TV Series \"On Deck\" view to all shows"
msgstr "Utvid visning av Plex TV serier \"On Deck\" til alle"
# PKC Settings - Appearance Tweaks
msgctxt "#39059"
msgid "Recently Added: Append show title to episode"
@ -1144,8 +1093,12 @@ msgstr "Oppdater Kodi skin ved stopping av avspilling"
# PKC Settings - Appearance Tweaks
msgctxt "#39066"
msgid "Recently Added: Also show already watched movies"
msgid ""
"Recently Added: Also show already watched movies (Refresh Plex "
"playlist/nodes!)"
msgstr ""
"Nylig lagt til: Vis også allerede sette filmer (Oppdater Plex "
"spilliste/noder!)"
# PKC Settings - Connection
msgctxt "#39067"
@ -1172,11 +1125,6 @@ msgctxt "#39071"
msgid "Current plex.tv status:"
msgstr "Aktuell plex.tv statys:"
# PKC Settings - Connection
msgctxt "#39072"
msgid "Background sync connection:"
msgstr ""
# PKC Settings, category name
msgctxt "#39073"
msgid "Appearance Tweaks"
@ -1187,10 +1135,10 @@ msgctxt "#39074"
msgid "TV Shows"
msgstr "TV-show"
# PKC Settings - Sync
# PKC Settings - Playback
msgctxt "#39075"
msgid "Verify access to media files while synching"
msgstr ""
msgid "Always use default Plex subtitle if possible"
msgstr "Bruk alltid standard Plex undertekst når mulig"
# Pop-up during initial sync
msgctxt "#39076"
@ -1203,8 +1151,8 @@ msgstr ""
# PKC Settings - Appearance Tweaks
msgctxt "#39077"
msgid "Maximum number of videos to show in widgets"
msgstr ""
msgid "Number of PMS items to show in widgets (e.g. \"On Deck\")"
msgstr "Antall PMS elementer som skal vises i widgets (eksempel \"On Deck\")"
# PKC Settings - Plex
msgctxt "#39078"
@ -1252,36 +1200,6 @@ msgctxt "#39084"
msgid "Enter PMS port"
msgstr "Legg til PMS port"
# PKC settings - Appearance Tweaks
msgctxt "#39085"
msgid "Reload Kodi node files to apply all the settings below"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39089"
msgid "Alexa connection status:"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39091"
msgid "Timeout - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39092"
msgid "IOError - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39093"
msgid "Suspended - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39094"
msgid "Managed Plex User - not connected"
msgstr ""
msgctxt "#39200"
msgid "Log-out Plex Home User "
msgstr "Logg av Plex Home User"
@ -1341,8 +1259,12 @@ msgid "Enter your Plex Media Server's IP or URL, Examples are:"
msgstr "Legg til Plex Media Server IP eller URL. Eksempel:"
msgctxt "#39217"
msgid "Use HTTPS (SSL) connections? Answer should probably be yes."
msgid ""
"Use HTTPS (SSL) connections? With Kodi 18 or later, HTTPS will likely not "
"work!"
msgstr ""
"Benytte HTTPS (SSL) forbindelse? Med Kodi >= 18, HTTPS vil muligens ikke "
"fungere! "
msgctxt "#39218"
msgid "Error contacting PMS"
@ -1525,10 +1447,6 @@ msgctxt "#39501"
msgid "Collections"
msgstr "Samlinger"
msgctxt "#39502"
msgid "PKC On Deck (faster)"
msgstr ""
msgctxt "#39600"
msgid ""
"Are you sure you want to reset your local Kodi database? A re-sync of the "
@ -1587,10 +1505,11 @@ msgctxt "#39705"
msgid "Use at your own risk"
msgstr "Bruk på eget ansvar"
# If user gets prompted to choose between several subtitles to burn in
# If user gets prompted to choose between several subtitles. Leave the number
# one at the beginning of the string!
msgctxt "#39706"
msgid "Don't burn-in any subtitle"
msgstr ""
msgid "1 No subtitles"
msgstr "1 Ingen undertekst"
# If user gets prompted to choose between several audio/subtitle tracks and
# language is unknown
@ -1644,8 +1563,8 @@ msgstr "Synk"
# Shown during sync process
msgctxt "#39715"
msgid "Synching playlists"
msgstr ""
msgid "items"
msgstr "Elementer"
# Error message if an xml, e.g. advancedsettings.xml cannot be parsed (xml is
# screwed up; formated the wrong way). Do NOT replace {0} and {1}!

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,6 @@
# XBMC Media Center language file
# Translators:
# Croneter None <croneter@gmail.com>, 2017
# Daniel Leite <danieldefreitasleite@gmail.com>, 2019
#
msgid ""
msgstr ""
@ -9,7 +8,7 @@ msgstr ""
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
"Last-Translator: Daniel Leite <danieldefreitasleite@gmail.com>, 2019\n"
"Last-Translator: Croneter None <croneter@gmail.com>, 2017\n"
"Language-Team: Portuguese (Brazil) (https://www.transifex.com/croneter/teams/73837/pt_BR/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -28,7 +27,7 @@ msgstr "Endereço do servidor (IP)"
msgctxt "#30001"
msgid "Searching for PMS"
msgstr "Buscando PMS"
msgstr ""
msgctxt "#30002"
msgid "Preferred playback method"
@ -42,15 +41,6 @@ msgid ""
"Warning: Kodi setting \"Play next video automatically\" is enabled. This "
"could break PKC. Deactivate?"
msgstr ""
"Atenção: Configuração \"Iniciar próximo vídeo automaticamente\" está ativada"
" no Kodi. Isto pode travar o PKC. Desativar?"
msgctxt "#30004"
msgid ""
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
"random password automatically if you haven't done so already. Please confirm"
" the next dialog that you want to enable the webserver now with Yes."
msgstr ""
msgctxt "#30005"
msgid "Username: "
@ -59,34 +49,32 @@ msgstr "Utilizador: "
# Sync notification displayed if there is still artwork to be cached to Kodi
msgctxt "#30006"
msgid "Caching %s Plex images"
msgstr "Armazenando %s imagens Plex"
msgstr ""
# Sync notification displayed if syncing of major artwork is done
msgctxt "#30007"
msgid "Plex image caching done"
msgstr "Armazenamento de imagens Plex finalizado"
msgstr ""
# PKC settings artwork: Enable notifications for artwork image sync
msgctxt "#30008"
msgid "Enable notifications for image caching"
msgstr "Ativar notificações para armazenamento de imagens"
msgstr ""
# PKC settings artwork: Enable image caching during Kodi playback
msgctxt "#30009"
msgid "Enable image caching during Kodi playback (restart Kodi!)"
msgstr ""
"Ativar armazenamento de imagens durante reprodução de media no Kodi "
"(necessário reiniciar o Kodi!)"
# PKC settings - Artwork
msgctxt "#30010"
msgid "Approximate progress"
msgstr "Progresso aproximado"
msgstr ""
# PKC settings - Artwork
msgctxt "#30011"
msgid "Artwork left to cache:"
msgstr "Artwork pendente de armazenamento: "
msgstr ""
# Button text
msgctxt "#30012"
@ -105,7 +93,7 @@ msgstr "Ligação"
# Pop-up notification if user tried to manually initiate fanart download
msgctxt "#30015"
msgid "Fanart download already running"
msgstr "Download de Fanart já em andamento"
msgstr ""
msgctxt "#30016"
msgid "Device Name"
@ -119,22 +107,22 @@ msgstr "Não autorizado para o PMS"
# Sync notification displayed for the number of fanart.tv lookups left
msgctxt "#30018"
msgid "Checking FanartTV for %s items"
msgstr "Verificando em FanartTV %s item(s)"
msgstr ""
# PKC settings artwork options: status info
msgctxt "#30019"
msgid "FanartTV lookup completed"
msgstr "Verificação em FanartTV finalizado"
msgstr ""
# PKC settings sync options
msgctxt "#30020"
msgid "Sync Plex playlists (reboot Kodi!)"
msgstr "Sincronizar playlists do Plex (necessário reiniciar o Kodi!)"
msgstr ""
# PKC settings sync options
msgctxt "#30021"
msgid "Only sync specific Plex playlists to Kodi"
msgstr "Sincronizar somente playlists especificas do Plex para o Kodi"
msgstr ""
# PKC settings category
msgctxt "#30022"
@ -144,7 +132,7 @@ msgstr "Avançado"
# PKC settings sync options
msgctxt "#30023"
msgid "Only sync specific Kodi playlists to Plex"
msgstr "Sincronizar somente playlists especificas do Kodi para o Plex"
msgstr ""
msgctxt "#30024"
msgid "Username"
@ -157,24 +145,16 @@ msgstr "Exibir mensagem se o PMS ficar offline"
# PKC settings sync options
msgctxt "#30026"
msgid "Prefix in Plex playlist name to trigger sync"
msgstr "Prefixo do nome da playlist do Plex para ativar a sincronização"
msgstr ""
# PKC settings sync options
msgctxt "#30027"
msgid "Prefix in Kodi playlist name to trigger sync"
msgstr "Prefixo do nome da playlist do Kodi para ativar a sincronização"
msgstr ""
# PKC settings artwork options: status info
msgctxt "#30028"
msgid "PKC-only image caching completed"
msgstr "Armazenamento PKC somente imagens finalizado"
# Warning shown when PKC switches to the Kodi default skin Estuary
msgctxt "#30029"
msgid ""
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
"database resets. Continue?"
msgstr ""
msgctxt "#30030"
@ -188,7 +168,7 @@ msgstr "Eu possuo este Servidor Plex Media"
# Kodi context menu entry for movie and episode information screen
msgctxt "#30032"
msgid "Information"
msgstr "Informação"
msgstr ""
msgctxt "#30042"
msgid "Refresh"
@ -276,10 +256,6 @@ msgctxt "#30160"
msgid "Video Quality if Transcoding necessary"
msgstr "Qualidade de vídeo se a transcodificação for necessária"
msgctxt "#30161"
msgid "Auto-adjust transcoding quality (deactivate for Chromecast)"
msgstr ""
msgctxt "#30165"
msgid "Direct Play"
msgstr "Reprodução Direta"
@ -605,11 +581,6 @@ msgctxt "#30524"
msgid "Select Plex libraries to sync"
msgstr ""
# PKC Settings - Playback
msgctxt "#30525"
msgid "Skip intro"
msgstr ""
# PKC Settings - Playback
msgctxt "#30527"
msgid "Ignore specials in next episodes"
@ -672,8 +643,8 @@ msgstr "Descarregar arte para o conjunto/coleção de filmes da FanArtTV "
# PKC Settings - Playback
msgctxt "#30541"
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
msgstr ""
msgid "Don't ask to pick a certain stream/quality"
msgstr "Não perguntar para escolher uma certa qualidade/transmissão"
# PKC Settings - Playback
msgctxt "#30542"
@ -694,21 +665,6 @@ msgctxt "#30545"
msgid "Force transcode pictures"
msgstr "Forçar transcodificação de imagens"
# PKC Settings - Playback
msgctxt "#30546"
msgid "Pick the first video if several versions are present"
msgstr ""
# PKC Settings - Playback
msgctxt "#30547"
msgid "Who picks the audio stream on playback start?"
msgstr ""
# PKC Settings - Playback
msgctxt "#30548"
msgid "Who picks subtitles on playback start?"
msgstr ""
# Welcome to Plex notification
msgctxt "#33000"
msgid "Welcome"
@ -729,17 +685,6 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Servidor está on-line"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -980,11 +925,6 @@ msgctxt "#39036"
msgid "Escape special characters in path (e.g. space to %20)"
msgstr ""
# PKC Settings - Customize Paths
msgctxt "#39090"
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
msgstr ""
# PKC Settings - Customize Paths
msgctxt "#39037"
msgid "Original Plex MOVIE path to replace:"
@ -1091,15 +1031,20 @@ msgstr "A procurar o(s) servidor(es) Plex"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Usado pela sincronização e quando tentando Reprodução Direta"
# PKC Settings, category name
msgctxt "#39057"
msgid "Customize Paths"
msgstr "Personalizar Caminhos"
# PKC Settings - Appearance Tweaks
msgctxt "#39058"
msgid "Extend Plex TV Series \"On Deck\" view to all shows"
msgstr ""
"Extender Plex Séries de TV na vista \"Na Plataforma\" a todos os programas"
# PKC Settings - Appearance Tweaks
msgctxt "#39059"
msgid "Recently Added: Append show title to episode"
@ -1140,8 +1085,12 @@ msgstr ""
# PKC Settings - Appearance Tweaks
msgctxt "#39066"
msgid "Recently Added: Also show already watched movies"
msgid ""
"Recently Added: Also show already watched movies (Refresh Plex "
"playlist/nodes!)"
msgstr ""
"Adicionado Recentemente: Mostrar também filmes visualizados (Actualize "
"listas de reprodução/nós do Plex!)"
# PKC Settings - Connection
msgctxt "#39067"
@ -1168,11 +1117,6 @@ msgctxt "#39071"
msgid "Current plex.tv status:"
msgstr "Estado atual da plex.tv:"
# PKC Settings - Connection
msgctxt "#39072"
msgid "Background sync connection:"
msgstr ""
# PKC Settings, category name
msgctxt "#39073"
msgid "Appearance Tweaks"
@ -1183,10 +1127,10 @@ msgctxt "#39074"
msgid "TV Shows"
msgstr "Programas de TV"
# PKC Settings - Sync
# PKC Settings - Playback
msgctxt "#39075"
msgid "Verify access to media files while synching"
msgstr ""
msgid "Always use default Plex subtitle if possible"
msgstr "Utilize sempre as legendas Plex se possível"
# Pop-up during initial sync
msgctxt "#39076"
@ -1199,8 +1143,9 @@ msgstr ""
# PKC Settings - Appearance Tweaks
msgctxt "#39077"
msgid "Maximum number of videos to show in widgets"
msgid "Number of PMS items to show in widgets (e.g. \"On Deck\")"
msgstr ""
"Numero de items do PMS a aparecer nas aplicacções (e.x. \"Na plataforma\")"
# PKC Settings - Plex
msgctxt "#39078"
@ -1245,36 +1190,6 @@ msgctxt "#39084"
msgid "Enter PMS port"
msgstr ""
# PKC settings - Appearance Tweaks
msgctxt "#39085"
msgid "Reload Kodi node files to apply all the settings below"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39089"
msgid "Alexa connection status:"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39091"
msgid "Timeout - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39092"
msgid "IOError - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39093"
msgid "Suspended - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39094"
msgid "Managed Plex User - not connected"
msgstr ""
msgctxt "#39200"
msgid "Log-out Plex Home User "
msgstr "Sair da sessão do Utilizador Caseiro Plex"
@ -1334,7 +1249,9 @@ msgid "Enter your Plex Media Server's IP or URL, Examples are:"
msgstr "Insira o seu IP ou URL do Servidor Plex Media, Exemplos são:"
msgctxt "#39217"
msgid "Use HTTPS (SSL) connections? Answer should probably be yes."
msgid ""
"Use HTTPS (SSL) connections? With Kodi 18 or later, HTTPS will likely not "
"work!"
msgstr ""
msgctxt "#39218"
@ -1518,10 +1435,6 @@ msgctxt "#39501"
msgid "Collections"
msgstr "Coleções"
msgctxt "#39502"
msgid "PKC On Deck (faster)"
msgstr ""
msgctxt "#39600"
msgid ""
"Are you sure you want to reset your local Kodi database? A re-sync of the "
@ -1582,10 +1495,11 @@ msgctxt "#39705"
msgid "Use at your own risk"
msgstr "Use por risco de conta própria"
# If user gets prompted to choose between several subtitles to burn in
# If user gets prompted to choose between several subtitles. Leave the number
# one at the beginning of the string!
msgctxt "#39706"
msgid "Don't burn-in any subtitle"
msgstr ""
msgid "1 No subtitles"
msgstr "1 Sem legendas"
# If user gets prompted to choose between several audio/subtitle tracks and
# language is unknown
@ -1639,8 +1553,8 @@ msgstr "Sincronizar "
# Shown during sync process
msgctxt "#39715"
msgid "Synching playlists"
msgstr ""
msgid "items"
msgstr "items"
# Error message if an xml, e.g. advancedsettings.xml cannot be parsed (xml is
# screwed up; formated the wrong way). Do NOT replace {0} and {1}!

View File

@ -44,13 +44,6 @@ msgid ""
"could break PKC. Deactivate?"
msgstr ""
msgctxt "#30004"
msgid ""
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
"random password automatically if you haven't done so already. Please confirm"
" the next dialog that you want to enable the webserver now with Yes."
msgstr ""
msgctxt "#30005"
msgid "Username: "
msgstr "Utilizador: "
@ -170,14 +163,6 @@ msgctxt "#30028"
msgid "PKC-only image caching completed"
msgstr ""
# Warning shown when PKC switches to the Kodi default skin Estuary
msgctxt "#30029"
msgid ""
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
"database resets. Continue?"
msgstr ""
msgctxt "#30030"
msgid "Port Number"
msgstr "Número da Porta"
@ -277,10 +262,6 @@ msgctxt "#30160"
msgid "Video Quality if Transcoding necessary"
msgstr "Qualidade de vídeo se a transcodificação for necessária"
msgctxt "#30161"
msgid "Auto-adjust transcoding quality (deactivate for Chromecast)"
msgstr ""
msgctxt "#30165"
msgid "Direct Play"
msgstr "Reprodução Direta"
@ -608,11 +589,6 @@ msgctxt "#30524"
msgid "Select Plex libraries to sync"
msgstr ""
# PKC Settings - Playback
msgctxt "#30525"
msgid "Skip intro"
msgstr ""
# PKC Settings - Playback
msgctxt "#30527"
msgid "Ignore specials in next episodes"
@ -675,8 +651,8 @@ msgstr "Descarregar arte para o conjunto/coleção de filmes da FanArtTV "
# PKC Settings - Playback
msgctxt "#30541"
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
msgstr ""
msgid "Don't ask to pick a certain stream/quality"
msgstr "Não perguntar para escolher uma certa qualidade/transmissão"
# PKC Settings - Playback
msgctxt "#30542"
@ -697,21 +673,6 @@ msgctxt "#30545"
msgid "Force transcode pictures"
msgstr "Forçar transcodificação de imagens"
# PKC Settings - Playback
msgctxt "#30546"
msgid "Pick the first video if several versions are present"
msgstr ""
# PKC Settings - Playback
msgctxt "#30547"
msgid "Who picks the audio stream on playback start?"
msgstr ""
# PKC Settings - Playback
msgctxt "#30548"
msgid "Who picks subtitles on playback start?"
msgstr ""
# Welcome to Plex notification
msgctxt "#33000"
msgid "Welcome"
@ -732,17 +693,6 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Servidor está on-line"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -983,11 +933,6 @@ msgctxt "#39036"
msgid "Escape special characters in path (e.g. space to %20)"
msgstr ""
# PKC Settings - Customize Paths
msgctxt "#39090"
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
msgstr ""
# PKC Settings - Customize Paths
msgctxt "#39037"
msgid "Original Plex MOVIE path to replace:"
@ -1094,15 +1039,20 @@ msgstr "A procurar o(s) servidor(es) Plex"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Usado pela sincronização e quando tentando Reprodução Direta"
# PKC Settings, category name
msgctxt "#39057"
msgid "Customize Paths"
msgstr "Personalizar Caminhos"
# PKC Settings - Appearance Tweaks
msgctxt "#39058"
msgid "Extend Plex TV Series \"On Deck\" view to all shows"
msgstr ""
"Extender Plex Séries de TV na vista \"Na Plataforma\" a todos os programas"
# PKC Settings - Appearance Tweaks
msgctxt "#39059"
msgid "Recently Added: Append show title to episode"
@ -1143,8 +1093,12 @@ msgstr ""
# PKC Settings - Appearance Tweaks
msgctxt "#39066"
msgid "Recently Added: Also show already watched movies"
msgid ""
"Recently Added: Also show already watched movies (Refresh Plex "
"playlist/nodes!)"
msgstr ""
"Adicionado Recentemente: Mostrar também filmes visualizados (Actualize "
"listas de reprodução/nós do Plex!)"
# PKC Settings - Connection
msgctxt "#39067"
@ -1171,11 +1125,6 @@ msgctxt "#39071"
msgid "Current plex.tv status:"
msgstr "Estado atual da plex.tv:"
# PKC Settings - Connection
msgctxt "#39072"
msgid "Background sync connection:"
msgstr ""
# PKC Settings, category name
msgctxt "#39073"
msgid "Appearance Tweaks"
@ -1186,10 +1135,10 @@ msgctxt "#39074"
msgid "TV Shows"
msgstr "Programas de TV"
# PKC Settings - Sync
# PKC Settings - Playback
msgctxt "#39075"
msgid "Verify access to media files while synching"
msgstr ""
msgid "Always use default Plex subtitle if possible"
msgstr "Utilize sempre as legendas Plex se possível"
# Pop-up during initial sync
msgctxt "#39076"
@ -1202,8 +1151,9 @@ msgstr ""
# PKC Settings - Appearance Tweaks
msgctxt "#39077"
msgid "Maximum number of videos to show in widgets"
msgid "Number of PMS items to show in widgets (e.g. \"On Deck\")"
msgstr ""
"Numero de items do PMS a aparecer nas aplicacções (e.x. \"Na plataforma\")"
# PKC Settings - Plex
msgctxt "#39078"
@ -1248,36 +1198,6 @@ msgctxt "#39084"
msgid "Enter PMS port"
msgstr ""
# PKC settings - Appearance Tweaks
msgctxt "#39085"
msgid "Reload Kodi node files to apply all the settings below"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39089"
msgid "Alexa connection status:"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39091"
msgid "Timeout - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39092"
msgid "IOError - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39093"
msgid "Suspended - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39094"
msgid "Managed Plex User - not connected"
msgstr ""
msgctxt "#39200"
msgid "Log-out Plex Home User "
msgstr "Sair da sessão do Utilizador Caseiro Plex"
@ -1337,7 +1257,9 @@ msgid "Enter your Plex Media Server's IP or URL, Examples are:"
msgstr "Insira o seu IP ou URL do Servidor Plex Media, Exemplos são:"
msgctxt "#39217"
msgid "Use HTTPS (SSL) connections? Answer should probably be yes."
msgid ""
"Use HTTPS (SSL) connections? With Kodi 18 or later, HTTPS will likely not "
"work!"
msgstr ""
msgctxt "#39218"
@ -1521,10 +1443,6 @@ msgctxt "#39501"
msgid "Collections"
msgstr "Coleções"
msgctxt "#39502"
msgid "PKC On Deck (faster)"
msgstr ""
msgctxt "#39600"
msgid ""
"Are you sure you want to reset your local Kodi database? A re-sync of the "
@ -1585,10 +1503,11 @@ msgctxt "#39705"
msgid "Use at your own risk"
msgstr "Use por risco de conta própria"
# If user gets prompted to choose between several subtitles to burn in
# If user gets prompted to choose between several subtitles. Leave the number
# one at the beginning of the string!
msgctxt "#39706"
msgid "Don't burn-in any subtitle"
msgstr ""
msgid "1 No subtitles"
msgstr "1 Sem legendas"
# If user gets prompted to choose between several audio/subtitle tracks and
# language is unknown
@ -1642,8 +1561,8 @@ msgstr "Sincronizar "
# Shown during sync process
msgctxt "#39715"
msgid "Synching playlists"
msgstr ""
msgid "items"
msgstr "items"
# Error message if an xml, e.g. advancedsettings.xml cannot be parsed (xml is
# screwed up; formated the wrong way). Do NOT replace {0} and {1}!

View File

@ -1,11 +1,11 @@
# XBMC Media Center language file
# Translators:
# Croneter None <croneter@gmail.com>, 2017
# Alexey Korobcov <korobcoff@gmail.com>, 2017
# Алексей Коробцов <korobcoff@gmail.com>, 2017
# Павел Хоменко, 2017
# Vlad Anisimov <uniss@ua.fm>, 2018
# Alex Freit <alex.nx@mail.ru>, 2019
# Vladimir Supranenok <stark_v@mail.ru>, 2019
# Vlad Anisimov <uniss@ua.fm>, 2019
#
msgid ""
msgstr ""
@ -13,7 +13,7 @@ msgstr ""
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
"Last-Translator: Vlad Anisimov <uniss@ua.fm>, 2019\n"
"Last-Translator: Vladimir Supranenok <stark_v@mail.ru>, 2019\n"
"Language-Team: Russian (Russia) (https://www.transifex.com/croneter/teams/73837/ru_RU/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -49,13 +49,6 @@ msgstr ""
"Предупреждение: включена настройка Kodi «Воспроизвести следующее видео "
"автоматически». Это может сломать PKC. Деактивировать?"
msgctxt "#30004"
msgid ""
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
"random password automatically if you haven't done so already. Please confirm"
" the next dialog that you want to enable the webserver now with Yes."
msgstr ""
msgctxt "#30005"
msgid "Username: "
msgstr "Имя пользователя: "
@ -133,7 +126,7 @@ msgstr "Поиск FanartTV завершен"
# PKC settings sync options
msgctxt "#30020"
msgid "Sync Plex playlists (reboot Kodi!)"
msgstr "Синхронизация плейлистов Plex (перезапустите Kodi!)"
msgstr "Синхронизация плейлистов Plex (перезагрузка Kodi!)"
# PKC settings sync options
msgctxt "#30021"
@ -173,14 +166,6 @@ msgctxt "#30028"
msgid "PKC-only image caching completed"
msgstr "Кеширование изображений PKC завершено"
# Warning shown when PKC switches to the Kodi default skin Estuary
msgctxt "#30029"
msgid ""
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
"database resets. Continue?"
msgstr ""
msgctxt "#30030"
msgid "Port Number"
msgstr "Порт"
@ -280,10 +265,6 @@ msgctxt "#30160"
msgid "Video Quality if Transcoding necessary"
msgstr "Качество при транскодинге"
msgctxt "#30161"
msgid "Auto-adjust transcoding quality (deactivate for Chromecast)"
msgstr ""
msgctxt "#30165"
msgid "Direct Play"
msgstr "Прямое воспроизведение"
@ -518,7 +499,6 @@ msgstr "Синхронизировать иллюстрации с Plex. (рек
msgctxt "#30503"
msgid "SSL certificate failed to validate. Please check {0} for solutions."
msgstr ""
"Ошибка проверки SSL сертификата. Пожалуйста просмотрите {0} для решения"
# PKC Settings, category name
msgctxt "#30506"
@ -618,11 +598,6 @@ msgstr ""
# PKC Settings - Sync Options
msgctxt "#30524"
msgid "Select Plex libraries to sync"
msgstr "Выбор библиотек Plex для синхронизации"
# PKC Settings - Playback
msgctxt "#30525"
msgid "Skip intro"
msgstr ""
# PKC Settings - Playback
@ -669,7 +644,7 @@ msgstr "Пользователь должен входить при каждом
# PKC Settings warning
msgctxt "#30537"
msgid "RESTART KODI IF YOU MAKE ANY CHANGES"
msgstr "ПЕРЕЗАПУСТИТЕ KODI, ЕСЛИ ВНОСИЛИ ИЗМЕНЕНИЯ"
msgstr "ПЕРЕЗАПУСТИТЕ KODI, ЕСЛИ ВНОСИЛИ КАКИЕ-ТО ИЗМЕНЕНИЯ"
# PKC Settings warning
msgctxt "#30538"
@ -689,8 +664,8 @@ msgstr "Загружать иллюстрации сборников с FanArtTV
# PKC Settings - Playback
msgctxt "#30541"
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
msgstr ""
msgid "Don't ask to pick a certain stream/quality"
msgstr "Не просить выбрать качество потока"
# PKC Settings - Playback
msgctxt "#30542"
@ -711,21 +686,6 @@ msgctxt "#30545"
msgid "Force transcode pictures"
msgstr "Принудительно транскодировать изображения"
# PKC Settings - Playback
msgctxt "#30546"
msgid "Pick the first video if several versions are present"
msgstr ""
# PKC Settings - Playback
msgctxt "#30547"
msgid "Who picks the audio stream on playback start?"
msgstr ""
# PKC Settings - Playback
msgctxt "#30548"
msgid "Who picks subtitles on playback start?"
msgstr ""
# Welcome to Plex notification
msgctxt "#33000"
msgid "Welcome"
@ -746,17 +706,6 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Сервер в сети"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr "PMS принудительное транскодирование"
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr "PMS принудительное прямое вещание"
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -996,11 +945,6 @@ msgctxt "#39036"
msgid "Escape special characters in path (e.g. space to %20)"
msgstr "Преобразуйте специальные символы в пути. (например пробел в %20)"
# PKC Settings - Customize Paths
msgctxt "#39090"
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
msgstr ""
# PKC Settings - Customize Paths
msgctxt "#39037"
msgid "Original Plex MOVIE path to replace:"
@ -1106,17 +1050,19 @@ msgstr "Поиск сервера Plex"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
"Используется при синхронизации и прямом воспроизведении. Перезапустите Kodi "
"для применения изменений!"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Используется при синхронизации и прямом воспроизведении"
# PKC Settings, category name
msgctxt "#39057"
msgid "Customize Paths"
msgstr "Изменить пути"
# PKC Settings - Appearance Tweaks
msgctxt "#39058"
msgid "Extend Plex TV Series \"On Deck\" view to all shows"
msgstr "В \"Текущем\" показывать все сериалы"
# PKC Settings - Appearance Tweaks
msgctxt "#39059"
msgid "Recently Added: Append show title to episode"
@ -1156,8 +1102,12 @@ msgstr "Принудительное обновление обложки Kodi п
# PKC Settings - Appearance Tweaks
msgctxt "#39066"
msgid "Recently Added: Also show already watched movies"
msgstr "Недавно добавлено: также показывать просмотренные фильмы"
msgid ""
"Recently Added: Also show already watched movies (Refresh Plex "
"playlist/nodes!)"
msgstr ""
"Недавно добавлено: также показывать просмотренные фильмы (обновите "
"плейлисты/списки Plex)"
# PKC Settings - Connection
msgctxt "#39067"
@ -1184,11 +1134,6 @@ msgctxt "#39071"
msgid "Current plex.tv status:"
msgstr "Текущий статус на plex.tv:"
# PKC Settings - Connection
msgctxt "#39072"
msgid "Background sync connection:"
msgstr ""
# PKC Settings, category name
msgctxt "#39073"
msgid "Appearance Tweaks"
@ -1199,10 +1144,10 @@ msgctxt "#39074"
msgid "TV Shows"
msgstr "Сериалы"
# PKC Settings - Sync
# PKC Settings - Playback
msgctxt "#39075"
msgid "Verify access to media files while synching"
msgstr ""
msgid "Always use default Plex subtitle if possible"
msgstr "Использовать субтитры по умолчанию из Plex, если доступны"
# Pop-up during initial sync
msgctxt "#39076"
@ -1216,13 +1161,13 @@ msgstr ""
# PKC Settings - Appearance Tweaks
msgctxt "#39077"
msgid "Maximum number of videos to show in widgets"
msgstr "Максимальное количество видео для отображения в виджетах"
msgid "Number of PMS items to show in widgets (e.g. \"On Deck\")"
msgstr "Количество элементов, отображаемых в виджетах (например \"Текущие\")"
# PKC Settings - Plex
msgctxt "#39078"
msgid "Plex Companion Update Port (change only if needed)"
msgstr "Порт обновления Plex Companion (если необходимо)"
msgstr "Порт обновления Plex Companion (меняйте только если необходимо)"
# Error message
msgctxt "#39079"
@ -1257,41 +1202,11 @@ msgstr "Прямые пути"
# Dialog for manually entering PMS
msgctxt "#39083"
msgid "Enter PMS IP or URL"
msgstr "Введите IP или адрес PMS"
msgstr ""
# Dialog for manually entering PMS
msgctxt "#39084"
msgid "Enter PMS port"
msgstr "Введите порт PMS"
# PKC settings - Appearance Tweaks
msgctxt "#39085"
msgid "Reload Kodi node files to apply all the settings below"
msgstr "Перезаписать узлы БД Kodi, чтобы применить следующие настройки"
# PKC Settings - Connection - Background sync connection status
msgctxt "#39089"
msgid "Alexa connection status:"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39091"
msgid "Timeout - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39092"
msgid "IOError - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39093"
msgid "Suspended - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39094"
msgid "Managed Plex User - not connected"
msgstr ""
msgctxt "#39200"
@ -1345,14 +1260,16 @@ msgstr "Смотреть позже"
# e.g. the PMS' name
msgctxt "#39213"
msgid "{0} offline"
msgstr "{0} недоступен"
msgstr ""
msgctxt "#39215"
msgid "Enter your Plex Media Server's IP or URL, Examples are:"
msgstr "Введите IP или URL Вашего Plex-сервера. Например:"
msgctxt "#39217"
msgid "Use HTTPS (SSL) connections? Answer should probably be yes."
msgid ""
"Use HTTPS (SSL) connections? With Kodi 18 or later, HTTPS will likely not "
"work!"
msgstr ""
msgctxt "#39218"
@ -1405,7 +1322,7 @@ msgstr "Вы вошли в plex.tv"
# Message in the PKC settings to display the plex.tv username
msgctxt "#39228"
msgid "Plex admin user"
msgstr "Администратор Plex"
msgstr ""
# Error message if user could not log in; the actual user name will be
# appended at the end of the string
@ -1416,12 +1333,12 @@ msgstr "Вход в plex.tv неудачен"
# Message in the PKC settings to display the plex.tv username
msgctxt "#39230"
msgid "Logged in Plex home user"
msgstr "Учетная запись Plex home"
msgstr ""
# Message in the PKC settings to change the logged in Plex home user
msgctxt "#39231"
msgid "Change logged in Plex home user"
msgstr "Изменить учетную запись Plex home"
msgstr ""
msgctxt "#39250"
msgid ""
@ -1538,10 +1455,6 @@ msgctxt "#39501"
msgid "Collections"
msgstr "Коллекции"
msgctxt "#39502"
msgid "PKC On Deck (faster)"
msgstr "PKC \"Текущие\" (быстрее)"
msgctxt "#39600"
msgid ""
"Are you sure you want to reset your local Kodi database? A re-sync of the "
@ -1600,10 +1513,11 @@ msgctxt "#39705"
msgid "Use at your own risk"
msgstr "Используйте на свой страх и риск"
# If user gets prompted to choose between several subtitles to burn in
# If user gets prompted to choose between several subtitles. Leave the number
# one at the beginning of the string!
msgctxt "#39706"
msgid "Don't burn-in any subtitle"
msgstr ""
msgid "1 No subtitles"
msgstr "1 Без субтитров"
# If user gets prompted to choose between several audio/subtitle tracks and
# language is unknown
@ -1657,8 +1571,8 @@ msgstr "Синхронизация"
# Shown during sync process
msgctxt "#39715"
msgid "Synching playlists"
msgstr "Синхронизация плейлистов"
msgid "items"
msgstr "элементов"
# Error message if an xml, e.g. advancedsettings.xml cannot be parsed (xml is
# screwed up; formated the wrong way). Do NOT replace {0} and {1}!

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
# XBMC Media Center language file
# Translators:
# Vlad Anisimov <uniss@ua.fm>, 2020
# Vlad Anisimov <uniss@ua.fm>, 2018
#
msgid ""
msgstr ""
@ -8,7 +8,7 @@ msgstr ""
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
"Last-Translator: Vlad Anisimov <uniss@ua.fm>, 2020\n"
"Last-Translator: Vlad Anisimov <uniss@ua.fm>, 2018\n"
"Language-Team: Ukrainian (Ukraine) (https://www.transifex.com/croneter/teams/73837/uk_UA/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -44,13 +44,6 @@ msgstr ""
"Попередження: налаштування Kodi \"відтворювати наступне відео автоматично\" "
"включено. Це може перервати роботу PKC. Вимкнути?"
msgctxt "#30004"
msgid ""
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
"random password automatically if you haven't done so already. Please confirm"
" the next dialog that you want to enable the webserver now with Yes."
msgstr ""
msgctxt "#30005"
msgid "Username: "
msgstr "Ім'я користувача:"
@ -103,7 +96,7 @@ msgstr "З'єднання"
# Pop-up notification if user tried to manually initiate fanart download
msgctxt "#30015"
msgid "Fanart download already running"
msgstr "Завантаження фан-арту вже розпочато"
msgstr ""
msgctxt "#30016"
msgid "Device Name"
@ -165,14 +158,6 @@ msgstr "Префікс в іменах плейлистів Kodi для вмик
# PKC settings artwork options: status info
msgctxt "#30028"
msgid "PKC-only image caching completed"
msgstr "Кешування зображень PKC завершено"
# Warning shown when PKC switches to the Kodi default skin Estuary
msgctxt "#30029"
msgid ""
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
"database resets. Continue?"
msgstr ""
msgctxt "#30030"
@ -274,11 +259,6 @@ msgctxt "#30160"
msgid "Video Quality if Transcoding necessary"
msgstr "Якість відео якщо перекодування потрібне"
msgctxt "#30161"
msgid "Auto-adjust transcoding quality (deactivate for Chromecast)"
msgstr ""
"Автоматичне налаштування якості перекодування (деактивуйте для Chromecast)"
msgctxt "#30165"
msgid "Direct Play"
msgstr "Пряме відтворення"
@ -341,13 +321,12 @@ msgid ""
"In the following window, enter the server's hostname (or IP) where your Plex"
" media resides. Mind the case!"
msgstr ""
"У наступному вікні введіть ім'я хосту (або IP), де знаходяться файли Plex."
# For setting up direct paths and adding network credentials - input window
# for hostname
msgctxt "#30201"
msgid "Enter server hostname (or IP)"
msgstr "Введіть ім'я хосту серверу (або його адресу IP)"
msgstr ""
# For setting up direct paths and adding network credentials
msgctxt "#30202"
@ -355,24 +334,22 @@ msgid ""
"In the following window, enter the network protocol you would like to use. "
"This is likely 'smb'."
msgstr ""
"У наступному вікні введіть мережевий протокол, котрий ви бажаєте "
"використовувати, наприклад, 'smb'."
# For setting up direct paths and adding network credentials - input window
# protocol
msgctxt "#30203"
msgid "Enter network protocol"
msgstr "Введіть мережевий протокол"
msgstr ""
# For setting up direct paths and adding network credentials
msgctxt "#30204"
msgid "The hostname or IP '{0}' that you entered is not valid."
msgstr "Ім'я хосту або адреса IP '{0}', котру ви ввели, не є правильною."
msgstr ""
# For setting up direct paths and adding network credentials
msgctxt "#30205"
msgid "The protocol '{0}' that you entered is not supported."
msgstr "Протокол '{0}', котрий ви ввели, не підтримується."
msgstr ""
# Video node naming for random e.g. movies
msgctxt "#30227"
@ -492,8 +469,6 @@ msgid ""
"Could not change the Kodi settings file {0}. PKC might not work correctly. "
"Error: {1}"
msgstr ""
"Неможливо змінити файлу налаштування Kodi {0}. PKC може працювати "
"некоректно. Помилка: {1}"
# PKC Settings - Connection
msgctxt "#30500"
@ -508,14 +483,12 @@ msgstr "SSL сертифікат клієнта"
# PKC Settings - Artwork
msgctxt "#30502"
msgid "Sync Plex artwork from the PMS (recommended)"
msgstr "Синхронізувати арт-файли Plex із PMS (рекомендовано)"
msgstr ""
# Message shown if SSL HTTPS certificate fails
msgctxt "#30503"
msgid "SSL certificate failed to validate. Please check {0} for solutions."
msgstr ""
"Сертифікат SSL не пройшов перевірку. Будь ласка, перевірте {0} для "
"вирішення."
# PKC Settings, category name
msgctxt "#30506"
@ -561,7 +534,6 @@ msgstr ""
msgctxt "#30514"
msgid "Show all Plex extras instead of immediately playing trailers"
msgstr ""
"Показувати всі екстра-файли Plex замість негайного відтворення трейлерів"
# PKC Settings - Sync Options
msgctxt "#30515"
@ -576,7 +548,7 @@ msgstr "Відтворення"
# PKC Settings - Connection
msgctxt "#30517"
msgid "Set network credentials for Direct Paths and direct play"
msgstr "Встановлення мережевих даних для прямих шляхів та прямого відтворення"
msgstr ""
# PKC Settings - Playback
msgctxt "#30518"
@ -607,17 +579,10 @@ msgstr "Примусове перекодування h265/HEVC"
msgctxt "#30523"
msgid "Also show sync progress for playstate and user data"
msgstr ""
"Також показувати процес синхронізації для стану відтворення та даних "
"користувача"
# PKC Settings - Sync Options
msgctxt "#30524"
msgid "Select Plex libraries to sync"
msgstr "Обрати бібліотеки Plex для синхронізації"
# PKC Settings - Playback
msgctxt "#30525"
msgid "Skip intro"
msgstr ""
# PKC Settings - Playback
@ -684,8 +649,8 @@ msgstr "Завантажувати матеріали набору фільмі
# PKC Settings - Playback
msgctxt "#30541"
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
msgstr ""
msgid "Don't ask to pick a certain stream/quality"
msgstr "Не запитувати обирання певного потоку або якості"
# PKC Settings - Playback
msgctxt "#30542"
@ -706,21 +671,6 @@ msgctxt "#30545"
msgid "Force transcode pictures"
msgstr "Примусове перекодування зображень"
# PKC Settings - Playback
msgctxt "#30546"
msgid "Pick the first video if several versions are present"
msgstr ""
# PKC Settings - Playback
msgctxt "#30547"
msgid "Who picks the audio stream on playback start?"
msgstr ""
# PKC Settings - Playback
msgctxt "#30548"
msgid "Who picks subtitles on playback start?"
msgstr ""
# Welcome to Plex notification
msgctxt "#33000"
msgid "Welcome"
@ -741,17 +691,6 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "Сервер онлайн"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr "PMS примусове перекодування"
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr "PMS примусова пряма трансляція"
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -759,11 +698,11 @@ msgstr "Невірне ім'я користувача або пароль"
msgctxt "#33010"
msgid "User is unauthorized for server {0}"
msgstr "Користувач не є авторизованим на сервері {0}"
msgstr ""
msgctxt "#33011"
msgid "Plex.tv did not provide us a valid list of Plex users, sorry."
msgstr "Plex.tv не дає нам дійсний список користувачів Plex, вибачте."
msgstr ""
# Dialog before playback
msgctxt "#33013"
@ -903,7 +842,7 @@ msgstr "Скинути БД Kodi та опціонально and optionally ск
# PKC Settings - Artwork
msgctxt "#39020"
msgid "Cache all images to Kodi texture cache now"
msgstr "Кешувати зараз всі зображення до кешу текстур Kodi"
msgstr ""
# Appended to a listed PMS if it is in the same LAN network as PKC
msgctxt "#39022"
@ -990,12 +929,7 @@ msgstr ""
# PKC Settings - Customize Paths
msgctxt "#39036"
msgid "Escape special characters in path (e.g. space to %20)"
msgstr "Замінювати спеціальні символи у шляхах (наприклад, пробіл у %20)"
# PKC Settings - Customize Paths
msgctxt "#39090"
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
msgstr "Безпечні символи для URL-адрес http(s), dav(s) та (s)ftp"
msgstr ""
# PKC Settings - Customize Paths
msgctxt "#39037"
@ -1073,7 +1007,7 @@ msgstr "Нічого не працює? Спробуйте повне скида
# PKC Settings - Connection
msgctxt "#39050"
msgid "Choose Plex Server from a list"
msgstr "Обрати сервер Plex зі списку"
msgstr ""
# PKC Settings - Sync
msgctxt "#39051"
@ -1102,17 +1036,19 @@ msgstr "Пошук PMS"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
"Використовувати при синхронізації та прямому відтворенню. Перезапустіть Kodi"
" для змін!"
msgid "Used by Sync and when attempting to Direct Play"
msgstr "Використовувати синхронізацією та при спробі прямого відтворення"
# PKC Settings, category name
msgctxt "#39057"
msgid "Customize Paths"
msgstr "Налаштування шляхів"
# PKC Settings - Appearance Tweaks
msgctxt "#39058"
msgid "Extend Plex TV Series \"On Deck\" view to all shows"
msgstr "Поширити відображення серій у Поточному на весь серіал"
# PKC Settings - Appearance Tweaks
msgctxt "#39059"
msgid "Recently Added: Append show title to episode"
@ -1153,8 +1089,12 @@ msgstr "Примусово оновлювати обкладинку Kodi піс
# PKC Settings - Appearance Tweaks
msgctxt "#39066"
msgid "Recently Added: Also show already watched movies"
msgstr "Нещодавно додане: також відображати вже переглянуті фільми"
msgid ""
"Recently Added: Also show already watched movies (Refresh Plex "
"playlist/nodes!)"
msgstr ""
"Нещодавно додане: також відображати вже переглянуті фільми (оновити Plex "
"вузли/листи відтворювання!)"
# PKC Settings - Connection
msgctxt "#39067"
@ -1164,7 +1104,7 @@ msgstr "Ваш поточний Plex Media Server:"
# PKC Settings - Connection
msgctxt "#39068"
msgid "Manually enter Plex Media Server address"
msgstr "Ввести вручну адресу сервера Plex"
msgstr ""
# PKC Settings - Connection
msgctxt "#39069"
@ -1181,11 +1121,6 @@ msgctxt "#39071"
msgid "Current plex.tv status:"
msgstr "Поточний plex.tv статус:"
# PKC Settings - Connection
msgctxt "#39072"
msgid "Background sync connection:"
msgstr ""
# PKC Settings, category name
msgctxt "#39073"
msgid "Appearance Tweaks"
@ -1196,10 +1131,10 @@ msgctxt "#39074"
msgid "TV Shows"
msgstr "Серіали"
# PKC Settings - Sync
# PKC Settings - Playback
msgctxt "#39075"
msgid "Verify access to media files while synching"
msgstr ""
msgid "Always use default Plex subtitle if possible"
msgstr "Завжди використовувати субтитри Plex за замовчуванням якщо можливо"
# Pop-up during initial sync
msgctxt "#39076"
@ -1213,8 +1148,8 @@ msgstr ""
# PKC Settings - Appearance Tweaks
msgctxt "#39077"
msgid "Maximum number of videos to show in widgets"
msgstr "Максимальна кількість відео для відображення у віджеті"
msgid "Number of PMS items to show in widgets (e.g. \"On Deck\")"
msgstr "Кількість елементів PMS для показування у віджетах (типу \"Поточне\")"
# PKC Settings - Plex
msgctxt "#39078"
@ -1238,59 +1173,25 @@ msgid ""
"Use Add-on Paths (default, easy) or Direct Paths? Choose Add-on Paths if "
"you're unsure. PKC will not work if your Direct Paths setup is wrong!"
msgstr ""
"Використовувати Шляхи додавань (за замовчуванням, легко) або Прямі Шляхи? "
"Оберіть Шляхи додавань якщо ви не впевнені. PKC не буде працювати якщо "
"налаштування Прямих Шляхів не коректні!"
# Button text for choosing PKC mode
msgctxt "#39081"
msgid "Add-on Paths"
msgstr "Шляхи додавань"
msgstr ""
# Button text for choosing PKC mode
msgctxt "#39082"
msgid "Direct Paths"
msgstr "Прямі Шляхи"
msgstr ""
# Dialog for manually entering PMS
msgctxt "#39083"
msgid "Enter PMS IP or URL"
msgstr "Введіть адресу IP або посилання сервера Plex"
msgstr ""
# Dialog for manually entering PMS
msgctxt "#39084"
msgid "Enter PMS port"
msgstr "Введіть порт PMS"
# PKC settings - Appearance Tweaks
msgctxt "#39085"
msgid "Reload Kodi node files to apply all the settings below"
msgstr ""
"Перезавантажити файли вузла Kodi для застосування всіх наступних налаштувань"
# PKC Settings - Connection - Background sync connection status
msgctxt "#39089"
msgid "Alexa connection status:"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39091"
msgid "Timeout - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39092"
msgid "IOError - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39093"
msgid "Suspended - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39094"
msgid "Managed Plex User - not connected"
msgstr ""
msgctxt "#39200"
@ -1330,7 +1231,7 @@ msgstr "Збій скидання РКС. Спробуйте рестартув
# PKC Settings - Plex
msgctxt "#39209"
msgid "Toggle plex.tv login (sign in or sign out)"
msgstr "Перемкнути логін plex.tv (увійти або вийти)"
msgstr ""
msgctxt "#39210"
msgid "Not yet connected to Plex Server"
@ -1344,16 +1245,17 @@ msgstr "Переглянути пізніше"
# e.g. the PMS' name
msgctxt "#39213"
msgid "{0} offline"
msgstr "{0} не доступний"
msgstr ""
msgctxt "#39215"
msgid "Enter your Plex Media Server's IP or URL, Examples are:"
msgstr "Введіть IP адресу або URL вашого серверу Plex, наприклад:"
msgctxt "#39217"
msgid "Use HTTPS (SSL) connections? Answer should probably be yes."
msgid ""
"Use HTTPS (SSL) connections? With Kodi 18 or later, HTTPS will likely not "
"work!"
msgstr ""
"Використовувати з'єднання HTTPS (SSL)? Відповідь, мабуть, має бути так."
msgctxt "#39218"
msgid "Error contacting PMS"
@ -1374,7 +1276,7 @@ msgstr "перемикання plex.tv завершено"
msgctxt "#39222"
msgid "Look for missing fanart on FanartTV now"
msgstr "Шукати зараз відсутні фан-арти на FanartTV"
msgstr ""
msgctxt "#39223"
msgid ""
@ -1405,23 +1307,23 @@ msgstr "Автентифіковано у plex.tv"
# Message in the PKC settings to display the plex.tv username
msgctxt "#39228"
msgid "Plex admin user"
msgstr "Адмініструючий користувач Plex"
msgstr ""
# Error message if user could not log in; the actual user name will be
# appended at the end of the string
msgctxt "#39229"
msgid "Login failed with plex.tv for user"
msgstr "Помилка входу у plex.tv для користувача"
msgstr ""
# Message in the PKC settings to display the plex.tv username
msgctxt "#39230"
msgid "Logged in Plex home user"
msgstr "Залоговані у домівці користувачі Plex"
msgstr ""
# Message in the PKC settings to change the logged in Plex home user
msgctxt "#39231"
msgid "Change logged in Plex home user"
msgstr "Змінити залогованого користувача у домівці Plex"
msgstr ""
msgctxt "#39250"
msgid ""
@ -1496,8 +1398,6 @@ msgid ""
"The current Kodi version is not supported by PKC. Please consult the Plex "
"forum."
msgstr ""
"Поточна версія Kodi не підтримується PKC. Будь ласка, скористайтеся форумами"
" Plex."
msgctxt "#39405"
msgid "Plex playlists/nodes refreshed"
@ -1538,10 +1438,6 @@ msgctxt "#39501"
msgid "Collections"
msgstr "Колекції"
msgctxt "#39502"
msgid "PKC On Deck (faster)"
msgstr "PKC на панелі (швидкіше)"
msgctxt "#39600"
msgid ""
"Are you sure you want to reset your local Kodi database? A re-sync of the "
@ -1600,10 +1496,11 @@ msgctxt "#39705"
msgid "Use at your own risk"
msgstr "Використовуйте на свій ризик"
# If user gets prompted to choose between several subtitles to burn in
# If user gets prompted to choose between several subtitles. Leave the number
# one at the beginning of the string!
msgctxt "#39706"
msgid "Don't burn-in any subtitle"
msgstr "Не виводити жодних субтитрів"
msgid "1 No subtitles"
msgstr "1 Немає субтитрів"
# If user gets prompted to choose between several audio/subtitle tracks and
# language is unknown
@ -1657,8 +1554,8 @@ msgstr "Синхронізація"
# Shown during sync process
msgctxt "#39715"
msgid "Synching playlists"
msgstr "Синхронізувати списки відтворення"
msgid "items"
msgstr "елементів"
# Error message if an xml, e.g. advancedsettings.xml cannot be parsed (xml is
# screwed up; formated the wrong way). Do NOT replace {0} and {1}!

View File

@ -44,13 +44,6 @@ msgid ""
"could break PKC. Deactivate?"
msgstr ""
msgctxt "#30004"
msgid ""
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
"random password automatically if you haven't done so already. Please confirm"
" the next dialog that you want to enable the webserver now with Yes."
msgstr ""
msgctxt "#30005"
msgid "Username: "
msgstr "用户名 "
@ -166,14 +159,6 @@ msgctxt "#30028"
msgid "PKC-only image caching completed"
msgstr ""
# Warning shown when PKC switches to the Kodi default skin Estuary
msgctxt "#30029"
msgid ""
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
"database resets. Continue?"
msgstr ""
msgctxt "#30030"
msgid "Port Number"
msgstr "端口号"
@ -273,10 +258,6 @@ msgctxt "#30160"
msgid "Video Quality if Transcoding necessary"
msgstr "如须转码视频质量"
msgctxt "#30161"
msgid "Auto-adjust transcoding quality (deactivate for Chromecast)"
msgstr ""
msgctxt "#30165"
msgid "Direct Play"
msgstr "直接播放"
@ -600,11 +581,6 @@ msgctxt "#30524"
msgid "Select Plex libraries to sync"
msgstr ""
# PKC Settings - Playback
msgctxt "#30525"
msgid "Skip intro"
msgstr ""
# PKC Settings - Playback
msgctxt "#30527"
msgid "Ignore specials in next episodes"
@ -667,8 +643,8 @@ msgstr "从FanArtTV下载额外的电影集/收藏art"
# PKC Settings - Playback
msgctxt "#30541"
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
msgstr ""
msgid "Don't ask to pick a certain stream/quality"
msgstr "无需询问挑选特定的串流/质量"
# PKC Settings - Playback
msgctxt "#30542"
@ -689,21 +665,6 @@ msgctxt "#30545"
msgid "Force transcode pictures"
msgstr "强制图片转码"
# PKC Settings - Playback
msgctxt "#30546"
msgid "Pick the first video if several versions are present"
msgstr ""
# PKC Settings - Playback
msgctxt "#30547"
msgid "Who picks the audio stream on playback start?"
msgstr ""
# PKC Settings - Playback
msgctxt "#30548"
msgid "Who picks subtitles on playback start?"
msgstr ""
# Welcome to Plex notification
msgctxt "#33000"
msgid "Welcome"
@ -724,17 +685,6 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "服务器在线"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -953,11 +903,6 @@ msgctxt "#39036"
msgid "Escape special characters in path (e.g. space to %20)"
msgstr ""
# PKC Settings - Customize Paths
msgctxt "#39090"
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
msgstr ""
# PKC Settings - Customize Paths
msgctxt "#39037"
msgid "Original Plex MOVIE path to replace:"
@ -1061,15 +1006,19 @@ msgstr "正搜索Plex服务器"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
msgid "Used by Sync and when attempting to Direct Play"
msgstr "用于同步和何时尝试直接播放"
# PKC Settings, category name
msgctxt "#39057"
msgid "Customize Paths"
msgstr "自定义路径"
# PKC Settings - Appearance Tweaks
msgctxt "#39058"
msgid "Extend Plex TV Series \"On Deck\" view to all shows"
msgstr "扩展Plex TV Series \"On Deck\"视图到所有节目"
# PKC Settings - Appearance Tweaks
msgctxt "#39059"
msgid "Recently Added: Append show title to episode"
@ -1109,8 +1058,10 @@ msgstr ""
# PKC Settings - Appearance Tweaks
msgctxt "#39066"
msgid "Recently Added: Also show already watched movies"
msgstr ""
msgid ""
"Recently Added: Also show already watched movies (Refresh Plex "
"playlist/nodes!)"
msgstr "最近添加:同时显示已观看电影(Refresh Plex 列表/节点!)"
# PKC Settings - Connection
msgctxt "#39067"
@ -1137,11 +1088,6 @@ msgctxt "#39071"
msgid "Current plex.tv status:"
msgstr "当前plex.tv状态"
# PKC Settings - Connection
msgctxt "#39072"
msgid "Background sync connection:"
msgstr ""
# PKC Settings, category name
msgctxt "#39073"
msgid "Appearance Tweaks"
@ -1152,10 +1098,10 @@ msgctxt "#39074"
msgid "TV Shows"
msgstr "电视节目"
# PKC Settings - Sync
# PKC Settings - Playback
msgctxt "#39075"
msgid "Verify access to media files while synching"
msgstr ""
msgid "Always use default Plex subtitle if possible"
msgstr "如可能始终使用默认Plex字幕"
# Pop-up during initial sync
msgctxt "#39076"
@ -1166,8 +1112,8 @@ msgstr "如果您使用了同一类多个Plex库e.g. “儿童电影”和”
# PKC Settings - Appearance Tweaks
msgctxt "#39077"
msgid "Maximum number of videos to show in widgets"
msgstr ""
msgid "Number of PMS items to show in widgets (e.g. \"On Deck\")"
msgstr "小部件上显示的PMS项目数(e.g. \"On Deck\")"
# PKC Settings - Plex
msgctxt "#39078"
@ -1210,36 +1156,6 @@ msgctxt "#39084"
msgid "Enter PMS port"
msgstr ""
# PKC settings - Appearance Tweaks
msgctxt "#39085"
msgid "Reload Kodi node files to apply all the settings below"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39089"
msgid "Alexa connection status:"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39091"
msgid "Timeout - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39092"
msgid "IOError - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39093"
msgid "Suspended - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39094"
msgid "Managed Plex User - not connected"
msgstr ""
msgctxt "#39200"
msgid "Log-out Plex Home User "
msgstr "退出Plex家庭用户 "
@ -1295,7 +1211,9 @@ msgid "Enter your Plex Media Server's IP or URL, Examples are:"
msgstr "输入您的Plex媒体服务器的 IP 或 URL例如"
msgctxt "#39217"
msgid "Use HTTPS (SSL) connections? Answer should probably be yes."
msgid ""
"Use HTTPS (SSL) connections? With Kodi 18 or later, HTTPS will likely not "
"work!"
msgstr ""
msgctxt "#39218"
@ -1465,10 +1383,6 @@ msgctxt "#39501"
msgid "Collections"
msgstr "收藏"
msgctxt "#39502"
msgid "PKC On Deck (faster)"
msgstr ""
msgctxt "#39600"
msgid ""
"Are you sure you want to reset your local Kodi database? A re-sync of the "
@ -1519,9 +1433,10 @@ msgctxt "#39705"
msgid "Use at your own risk"
msgstr ""
# If user gets prompted to choose between several subtitles to burn in
# If user gets prompted to choose between several subtitles. Leave the number
# one at the beginning of the string!
msgctxt "#39706"
msgid "Don't burn-in any subtitle"
msgid "1 No subtitles"
msgstr ""
# If user gets prompted to choose between several audio/subtitle tracks and
@ -1574,7 +1489,7 @@ msgstr ""
# Shown during sync process
msgctxt "#39715"
msgid "Synching playlists"
msgid "items"
msgstr ""
# Error message if an xml, e.g. advancedsettings.xml cannot be parsed (xml is

View File

@ -42,13 +42,6 @@ msgid ""
"could break PKC. Deactivate?"
msgstr ""
msgctxt "#30004"
msgid ""
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
"random password automatically if you haven't done so already. Please confirm"
" the next dialog that you want to enable the webserver now with Yes."
msgstr ""
msgctxt "#30005"
msgid "Username: "
msgstr "使用者: "
@ -164,14 +157,6 @@ msgctxt "#30028"
msgid "PKC-only image caching completed"
msgstr ""
# Warning shown when PKC switches to the Kodi default skin Estuary
msgctxt "#30029"
msgid ""
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
"database resets. Continue?"
msgstr ""
msgctxt "#30030"
msgid "Port Number"
msgstr "埠號"
@ -271,10 +256,6 @@ msgctxt "#30160"
msgid "Video Quality if Transcoding necessary"
msgstr "轉碼影像品質"
msgctxt "#30161"
msgid "Auto-adjust transcoding quality (deactivate for Chromecast)"
msgstr ""
msgctxt "#30165"
msgid "Direct Play"
msgstr "直播"
@ -598,11 +579,6 @@ msgctxt "#30524"
msgid "Select Plex libraries to sync"
msgstr ""
# PKC Settings - Playback
msgctxt "#30525"
msgid "Skip intro"
msgstr ""
# PKC Settings - Playback
msgctxt "#30527"
msgid "Ignore specials in next episodes"
@ -665,8 +641,8 @@ msgstr "從 FanArtTV 下載電影合輯海報"
# PKC Settings - Playback
msgctxt "#30541"
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
msgstr ""
msgid "Don't ask to pick a certain stream/quality"
msgstr "不要要求挑選特定的 串流/品質"
# PKC Settings - Playback
msgctxt "#30542"
@ -687,21 +663,6 @@ msgctxt "#30545"
msgid "Force transcode pictures"
msgstr "強制圖片轉碼"
# PKC Settings - Playback
msgctxt "#30546"
msgid "Pick the first video if several versions are present"
msgstr ""
# PKC Settings - Playback
msgctxt "#30547"
msgid "Who picks the audio stream on playback start?"
msgstr ""
# PKC Settings - Playback
msgctxt "#30548"
msgid "Who picks subtitles on playback start?"
msgstr ""
# Welcome to Plex notification
msgctxt "#33000"
msgid "Welcome"
@ -722,17 +683,6 @@ msgctxt "#33003"
msgid "Server is online"
msgstr "伺服器已上線"
# Plex notification when we need to transcode
msgctxt "#33004"
msgid "PMS enforced transcoding"
msgstr ""
# Plex notification when we need to use direct streaming (instead of
# transcoding)
msgctxt "#33005"
msgid "PMS enforced direct streaming"
msgstr ""
# Error notification
msgctxt "#33009"
msgid "Invalid username or password"
@ -951,11 +901,6 @@ msgctxt "#39036"
msgid "Escape special characters in path (e.g. space to %20)"
msgstr ""
# PKC Settings - Customize Paths
msgctxt "#39090"
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
msgstr ""
# PKC Settings - Customize Paths
msgctxt "#39037"
msgid "Original Plex MOVIE path to replace:"
@ -1057,15 +1002,19 @@ msgstr "正在搜索Plex伺服器"
# PKC Settings - Customize paths
msgctxt "#39056"
msgid ""
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
msgstr ""
msgid "Used by Sync and when attempting to Direct Play"
msgstr "通過同步和嘗試使用直接播放"
# PKC Settings, category name
msgctxt "#39057"
msgid "Customize Paths"
msgstr "自訂路徑"
# PKC Settings - Appearance Tweaks
msgctxt "#39058"
msgid "Extend Plex TV Series \"On Deck\" view to all shows"
msgstr "延伸plex電視節目系列\"上架\"視圖,到所有節目"
# PKC Settings - Appearance Tweaks
msgctxt "#39059"
msgid "Recently Added: Append show title to episode"
@ -1105,8 +1054,10 @@ msgstr ""
# PKC Settings - Appearance Tweaks
msgctxt "#39066"
msgid "Recently Added: Also show already watched movies"
msgstr ""
msgid ""
"Recently Added: Also show already watched movies (Refresh Plex "
"playlist/nodes!)"
msgstr "最近添加︰ 也顯示已觀看的電影 (刷新Plex播放清單/節點!)"
# PKC Settings - Connection
msgctxt "#39067"
@ -1133,11 +1084,6 @@ msgctxt "#39071"
msgid "Current plex.tv status:"
msgstr "plex.tv 狀態︰"
# PKC Settings - Connection
msgctxt "#39072"
msgid "Background sync connection:"
msgstr ""
# PKC Settings, category name
msgctxt "#39073"
msgid "Appearance Tweaks"
@ -1148,10 +1094,10 @@ msgctxt "#39074"
msgid "TV Shows"
msgstr "電視節目"
# PKC Settings - Sync
# PKC Settings - Playback
msgctxt "#39075"
msgid "Verify access to media files while synching"
msgstr ""
msgid "Always use default Plex subtitle if possible"
msgstr "如果可能的話,使用預設 Plex 字幕"
# Pop-up during initial sync
msgctxt "#39076"
@ -1162,8 +1108,8 @@ msgstr "如果您使用多個同類的Plex資料庫例如\"兒童電影\"和\
# PKC Settings - Appearance Tweaks
msgctxt "#39077"
msgid "Maximum number of videos to show in widgets"
msgstr ""
msgid "Number of PMS items to show in widgets (e.g. \"On Deck\")"
msgstr "PMS 中顯示在小工具集 (例如\"上架\") 品項的數目"
# PKC Settings - Plex
msgctxt "#39078"
@ -1206,36 +1152,6 @@ msgctxt "#39084"
msgid "Enter PMS port"
msgstr ""
# PKC settings - Appearance Tweaks
msgctxt "#39085"
msgid "Reload Kodi node files to apply all the settings below"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39089"
msgid "Alexa connection status:"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39091"
msgid "Timeout - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39092"
msgid "IOError - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39093"
msgid "Suspended - not connected"
msgstr ""
# PKC Settings - Connection - Background sync connection status
msgctxt "#39094"
msgid "Managed Plex User - not connected"
msgstr ""
msgctxt "#39200"
msgid "Log-out Plex Home User "
msgstr "登出Plex Home用戶 "
@ -1291,7 +1207,9 @@ msgid "Enter your Plex Media Server's IP or URL, Examples are:"
msgstr "輸入您的Plex媒體伺服器的 IP 或 URL例子"
msgctxt "#39217"
msgid "Use HTTPS (SSL) connections? Answer should probably be yes."
msgid ""
"Use HTTPS (SSL) connections? With Kodi 18 or later, HTTPS will likely not "
"work!"
msgstr ""
msgctxt "#39218"
@ -1461,10 +1379,6 @@ msgctxt "#39501"
msgid "Collections"
msgstr "收藏"
msgctxt "#39502"
msgid "PKC On Deck (faster)"
msgstr ""
msgctxt "#39600"
msgid ""
"Are you sure you want to reset your local Kodi database? A re-sync of the "
@ -1515,9 +1429,10 @@ msgctxt "#39705"
msgid "Use at your own risk"
msgstr ""
# If user gets prompted to choose between several subtitles to burn in
# If user gets prompted to choose between several subtitles. Leave the number
# one at the beginning of the string!
msgctxt "#39706"
msgid "Don't burn-in any subtitle"
msgid "1 No subtitles"
msgstr ""
# If user gets prompted to choose between several audio/subtitle tracks and
@ -1570,7 +1485,7 @@ msgstr ""
# Shown during sync process
msgctxt "#39715"
msgid "Synching playlists"
msgid "items"
msgstr ""
# Error message if an xml, e.g. advancedsettings.xml cannot be parsed (xml is

View File

@ -30,11 +30,3 @@ def init(entrypoint=False):
SYNC = Sync(entrypoint)
if not entrypoint:
PLAYSTATE = PlayState()
def reload():
"""
Reload PKC settings from xml file, e.g. on user-switch
"""
global APP, SYNC
APP.reload()
SYNC.reload()

View File

@ -15,8 +15,6 @@ class Account(object):
self.plex_username = None
self.plex_user_id = None
self.plex_token = None
# Personal access token per specific user and PMS
# As a rule of thumb, always use this token!
self.pms_token = None
self.avatar = None
self.myplexlogin = None

View File

@ -19,18 +19,14 @@ class App(object):
def __init__(self, entrypoint=False):
self.fetch_pms_item_number = None
self.force_reload_skin = None
# All thread instances
self.threads = []
if entrypoint:
self.load_entrypoint()
else:
self.reload()
self.load()
# Quit PKC?
self.stop_pkc = False
# This will suspend the main thread also
self.suspend = False
# Update Kodi widgets
self.update_widgets = False
# Need to lock all methods and functions messing with Plex Companion subscribers
self.lock_subscriber = RLock()
# Need to lock everything messing with Kodi/PKC playqueues
@ -47,27 +43,26 @@ class App(object):
self.monitor = None
# xbmc.Player() instance
self.player = None
# All thread instances
self.threads = []
# Instance of FanartThread()
self.fanart_thread = None
# Instance of ImageCachingThread()
self.caching_thread = None
# Dialog to skip intro
self.skip_intro_dialog = None
@property
def is_playing(self):
return self.player.isPlaying() == 1
return self.player.isPlaying()
@property
def is_playing_video(self):
return self.player.isPlayingVideo() == 1
return self.player.isPlayingVideo()
def register_fanart_thread(self, thread):
self.fanart_thread = thread
self.threads.append(thread)
def deregister_fanart_thread(self, thread):
self.fanart_thread.unblock_callers()
self.fanart_thread = None
self.threads.remove(thread)
@ -88,7 +83,6 @@ class App(object):
self.threads.append(thread)
def deregister_caching_thread(self, thread):
self.caching_thread.unblock_callers()
self.caching_thread = None
self.threads.remove(thread)
@ -115,7 +109,6 @@ class App(object):
"""
Sync thread has done it's work and is e.g. about to die
"""
thread.unblock_callers()
self.threads.remove(thread)
def suspend_threads(self, block=True):
@ -129,16 +122,19 @@ class App(object):
if block:
while True:
for thread in self.threads:
if not thread.is_suspended():
if not thread.suspend_reached:
LOG.debug('Waiting for thread to suspend: %s', thread)
# Send suspend signal again in case self.threads
# changed
thread.suspend(block=True)
thread.suspend()
if self.monitor.waitForAbort(0.1):
return True
break
else:
break
return self.monitor.abortRequested()
return xbmc.abortRequested
def resume_threads(self):
def resume_threads(self, block=True):
"""
Resume all thread activity with or without blocking.
Returns True only if PKC shutdown requested
@ -146,7 +142,17 @@ class App(object):
LOG.debug('Resuming threads: %s', self.threads)
for thread in self.threads:
thread.resume()
return self.monitor.abortRequested()
if block:
while True:
for thread in self.threads:
if thread.suspend_reached:
LOG.debug('Waiting for thread to resume: %s', thread)
if self.monitor.waitForAbort(0.1):
return True
break
else:
break
return xbmc.abortRequested
def stop_threads(self, block=True):
"""
@ -155,14 +161,14 @@ class App(object):
"""
LOG.debug('Killing threads: %s', self.threads)
for thread in self.threads:
thread.cancel()
thread.abort()
if block:
while self.threads:
LOG.debug('Waiting for threads to exit: %s', self.threads)
if xbmc.sleep(100):
return True
def reload(self):
def load(self):
# Number of items to fetch and display in widgets
self.fetch_pms_item_number = int(utils.settings('fetch_pms_item_number'))
# Hack to force Kodi widget for "in progress" to show up if it was empty

View File

@ -42,7 +42,6 @@ class Sync(object):
self.remapSMBphotoNew = None
# Escape path?
self.escape_path = None
self.escape_path_safe_chars = None
# Shall we replace custom user ratings with the number of versions available?
self.indicate_media_versions = None
# Will sync movie trailer differently: either play trailer directly or show
@ -57,6 +56,8 @@ class Sync(object):
# How often shall we sync?
self.full_sync_intervall = None
# Background Sync disabled?
self.background_sync_disabled = None
# How long shall we wait with synching a new item to make sure Plex got all
# metadata?
self.backgroundsync_saftymargin = None
@ -74,30 +75,15 @@ class Sync(object):
# Could we access the paths?
self.path_verified = False
# List of Section() items representing Plex library sections
self._sections = []
# List of section_ids we're synching to Kodi - will be automatically
# re-built if sections are set a-new
self.section_ids = set()
self.load()
@property
def sections(self):
return self._sections
@sections.setter
def sections(self, sections):
self._sections = sections
# Sets are faster when using "in" test than lists
self.section_ids = set([x.section_id for x in sections if x.sync_to_kodi])
def load(self):
self.direct_paths = utils.settings('useDirectPaths') == '1'
self.enable_music = utils.settings('enableMusic') == 'true'
self.artwork = utils.settings('usePlexArtwork') == 'true'
self.replace_smb_path = utils.settings('replaceSMB') == 'true'
self.remap_path = utils.settings('remapSMB') == 'true'
self.force_transcode_pix = utils.settings('force_transcode_pix') == 'true'
self.remapSMBmovieOrg = remove_trailing_slash(utils.settings('remapSMBmovieOrg'))
self.remapSMBmovieNew = remove_trailing_slash(utils.settings('remapSMBmovieNew'))
self.remapSMBtvOrg = remove_trailing_slash(utils.settings('remapSMBtvOrg'))
@ -107,24 +93,15 @@ class Sync(object):
self.remapSMBphotoOrg = remove_trailing_slash(utils.settings('remapSMBphotoOrg'))
self.remapSMBphotoNew = remove_trailing_slash(utils.settings('remapSMBphotoNew'))
self.escape_path = utils.settings('escapePath') == 'true'
self.escape_path_safe_chars = utils.settings('escapePathSafeChars').encode('utf-8')
self.indicate_media_versions = utils.settings('indicate_media_versions') == "true"
self.show_extras_instead_of_playing_trailer = utils.settings('showExtrasInsteadOfTrailer') == 'true'
self.sync_specific_plex_playlists = utils.settings('syncSpecificPlexPlaylists') == 'true'
self.sync_specific_kodi_playlists = utils.settings('syncSpecificKodiPlaylists') == 'true'
self.sync_thread_number = int(utils.settings('syncThreadNumber'))
self.reload()
def reload(self):
"""
Any settings unrelated to syncs to the Kodi database - can thus be
safely reset without a Kodi reboot
"""
self.sync_dialog = utils.settings('dbSyncIndicator') == 'true'
self.full_sync_intervall = int(utils.settings('fullSyncInterval')) * 60
self.background_sync_disabled = utils.settings('enableBackgroundSync') == 'false'
self.backgroundsync_saftymargin = int(utils.settings('backgroundsync_saftyMargin'))
self.sync_thread_number = int(utils.settings('syncThreadNumber'))
self.image_sync_notifications = utils.settings('imageSyncNotifications') == 'true'
self.force_transcode_pix = utils.settings('force_transcode_pix') == 'true'
# Trailers in Kodi DB will remain UNTIL DB is reset!
self.show_extras_instead_of_playing_trailer = utils.settings('showExtrasInsteadOfTrailer') == 'true'

View File

@ -35,9 +35,7 @@ class PlayState(object):
'volume': 100,
'muted': False,
'playmethod': None,
'playcount': None,
'external_player': False, # bool - xbmc.Player().isExternalPlayer()
'intro_markers': [],
'playcount': None
}
def __init__(self):
@ -55,12 +53,25 @@ class PlayState(object):
}
self.played_info = {}
# Currently playing PKC item, a PlaylistItem()
self.item = None
# Set by SpecialMonitor - did user choose to resume playback or start
# from the beginning?
# Do set to None if NO resume dialog is displayed! True/False otherwise
self.resume_playback = None
# Don't ask user whether to resume but immediatly resume
self.autoplay = False
# Was the playback initiated by the user using the Kodi context menu?
self.context_menu_play = False
# Set by context menu - shall we force-transcode the next playing item?
self.force_transcode = False
# Which Kodi player is/has been active? (either int 1, 2 or 3)
# Which Kodi player is/has been active? (either int 0, 1, 2)
self.active_players = set()
# Have we initiated playback via Plex Companion or Alexa - so from the
# Plex side of things?
self.initiated_by_plex = False
# PKC adds/replaces items in the playqueue. We need to use
# xbmcplugin.setResolvedUrl() AFTER an item has successfully been added
# This flag is set by Kodimonitor/xbmc.Monitor() and the Playlist.OnAdd
# signal only when the currently playing item that called the
# webservice has successfully been processed
self.playlist_ready = False
# Flag for Kodimonitor to check when the correct item has been
# processed and the Playlist.OnAdd signal has been received
self.playlist_start_pos = None

View File

@ -33,8 +33,8 @@ class ImageCachingThread(backgroundthread.KillableThread):
if not utils.settings('imageSyncDuringPlayback') == 'true':
self.suspend_points.append((app.APP, 'is_playing_video'))
def should_suspend(self):
return any(getattr(obj, attrib) for obj, attrib in self.suspend_points)
def isSuspended(self):
return any(getattr(obj, txt) for obj, txt in self.suspend_points)
@staticmethod
def _url_generator(kind, kodi_type):
@ -73,29 +73,21 @@ class ImageCachingThread(backgroundthread.KillableThread):
app.APP.deregister_caching_thread(self)
LOG.info("---===### Stopped ImageCachingThread ###===---")
def _loop(self):
def _run(self):
kinds = [KodiVideoDB]
if app.SYNC.enable_music:
kinds.append(KodiMusicDB)
for kind in kinds:
for kodi_type in ('poster', 'fanart'):
for url in self._url_generator(kind, kodi_type):
if self.should_suspend() or self.should_cancel():
return False
cache_url(url, self.should_suspend)
if self.wait_while_suspended():
return
cache_url(url)
# Toggles Image caching completed to Yes
utils.settings('plex_status_image_caching', value=utils.lang(107))
return True
def _run(self):
while True:
if self._loop():
break
if self.wait_while_suspended():
break
def cache_url(url, should_suspend=None):
def cache_url(url):
url = double_urlencode(url)
sleeptime = 0
while True:
@ -113,11 +105,11 @@ def cache_url(url, should_suspend=None):
# download. All is well
break
except requests.ConnectionError:
if app.APP.stop_pkc or (should_suspend and should_suspend()):
if app.APP.stop_pkc:
# Kodi terminated
break
# Server thinks its a DOS attack, ('error 10053')
# Wait before trying again
# OR: Kodi refuses Webserver connection (no password set)
if sleeptime > 5:
LOG.error('Repeatedly got ConnectionError for url %s',
double_urldecode(url))

View File

@ -2,246 +2,142 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from time import time as _time
import threading
import Queue
import heapq
from collections import deque
import xbmc
from . import utils, app, variables as v
from . import utils, app
WORKER_COUNT = 3
LOG = getLogger('PLEX.threads')
class KillableThread(threading.Thread):
'''A thread class that supports raising exception in the thread from
another thread.
'''
# def _get_my_tid(self):
# """determines this (self's) thread id
# CAREFUL : this function is executed in the context of the caller
# thread, to get the identity of the thread represented by this
# instance.
# """
# if not self.isAlive():
# raise threading.ThreadError("the thread is not active")
# return self.ident
# def _raiseExc(self, exctype):
# """Raises the given exception type in the context of this thread.
# If the thread is busy in a system call (time.sleep(),
# socket.accept(), ...), the exception is simply ignored.
# If you are sure that your exception should terminate the thread,
# one way to ensure that it works is:
# t = ThreadWithExc( ... )
# ...
# t.raiseExc( SomeException )
# while t.isAlive():
# time.sleep( 0.1 )
# t.raiseExc( SomeException )
# If the exception is to be caught by the thread, you need a way to
# check that your thread has caught it.
# CAREFUL : this function is executed in the context of the
# caller thread, to raise an excpetion in the context of the
# thread represented by this instance.
# """
# _async_raise(self._get_my_tid(), exctype)
def kill(self, force_and_wait=False):
pass
# try:
# self._raiseExc(KillThreadException)
# if force_and_wait:
# time.sleep(0.1)
# while self.isAlive():
# self._raiseExc(KillThreadException)
# time.sleep(0.1)
# except threading.ThreadError:
# pass
# def onKilled(self):
# pass
# def run(self):
# try:
# self._Thread__target(*self._Thread__args, **self._Thread__kwargs)
# except KillThreadException:
# self.onKilled()
def __init__(self, group=None, target=None, name=None, args=(), kwargs={}):
self._canceled = False
# Set to True to set the thread to suspended
self._suspended = False
self._is_not_suspended = threading.Event()
self._is_not_suspended.set()
self._suspension_reached = threading.Event()
self._is_not_asleep = threading.Event()
self._is_not_asleep.set()
self.suspension_timeout = None
# Thread will return True only if suspended state is reached
self.suspend_reached = False
super(KillableThread, self).__init__(group, target, name, args, kwargs)
def should_cancel(self):
def isCanceled(self):
"""
Returns True if the thread should be stopped immediately
Returns True if the thread is stopped
"""
return self._canceled or app.APP.stop_pkc
if self._canceled or xbmc.abortRequested:
return True
return False
def cancel(self):
def abort(self):
"""
Call from another thread to stop this current thread
Call to stop this thread
"""
self._canceled = True
# Make sure thread is running in order to exit quickly
self._is_not_asleep.set()
self._is_not_suspended.set()
def should_suspend(self):
def suspend(self, block=False):
"""
Returns True if the current thread should be suspended immediately
Call to suspend this thread
"""
return self._suspended
def suspend(self, block=False, timeout=None):
"""
Call from another thread to suspend the current thread. Provide a
timeout [float] in seconds optionally. block=True will block the caller
until the thread-to-be-suspended is indeed suspended
Will wake a thread that is asleep!
"""
self.suspension_timeout = timeout
self._suspended = True
self._is_not_suspended.clear()
# Make sure thread wakes up in order to suspend
self._is_not_asleep.set()
if block:
self._suspension_reached.wait()
while not self.suspend_reached:
LOG.debug('Waiting for thread to suspend: %s', self)
if app.APP.monitor.waitForAbort(0.1):
return
def resume(self):
"""
Call from another thread to revive a suspended or asleep current thread
back to life
Call to revive a suspended thread back to life
"""
self._suspended = False
self._is_not_asleep.set()
self._is_not_suspended.set()
def wait_while_suspended(self):
"""
Blocks until thread is not suspended anymore or the thread should
exit or for a period of self.suspension_timeout (set by the caller of
suspend())
Returns the value of should_cancel()
exit.
Returns True only if the thread should exit (=isCanceled())
"""
self._suspension_reached.set()
self._is_not_suspended.wait(self.suspension_timeout)
self._suspension_reached.clear()
return self.should_cancel()
while self.isSuspended():
try:
self.suspend_reached = True
# Set in service.py
if self.isCanceled():
# Abort was requested while waiting. We should exit
return True
if app.APP.monitor.waitForAbort(0.1):
return True
finally:
self.suspend_reached = False
return self.isCanceled()
def is_suspended(self):
def isSuspended(self):
"""
Check from another thread whether the current thread is suspended
Returns True if the thread is suspended
"""
return self._suspension_reached.is_set()
def sleep(self, timeout):
"""
Only call from the current thread in order to sleep for a period of
timeout [float, seconds]. Will unblock immediately if thread should
cancel (should_cancel()) or the thread should_suspend
"""
self._is_not_asleep.clear()
self._is_not_asleep.wait(timeout)
self._is_not_asleep.set()
def is_asleep(self):
"""
Check from another thread whether the current thread is asleep
"""
return not self._is_not_asleep.is_set()
def unblock_callers(self):
"""
Ensures that any other thread that requested this thread's suspension
is released
"""
self._suspension_reached.set()
class ProcessingQueue(Queue.Queue, object):
"""
Queue of queues that processes a queue completely before moving on to the
next queue. There's one queue per Section(). You need to initialize each
section with add_section(section) first.
Put tuples (count, item) into this queue, with count being the respective
position of the item in the queue, starting with 0 (zero).
(None, None) is the sentinel for a single queue being exhausted, added by
add_sentinel()
"""
def _init(self, maxsize):
self.queue = deque()
self._sections = deque()
self._queues = deque()
self._current_section = None
self._current_queue = None
# Item-index for the currently active queue
self._counter = 0
def _qsize(self):
return self._current_queue._qsize() if self._current_queue else 0
def _put(self, item):
for i, section in enumerate(self._sections):
if item[1]['section'] == section:
self._queues[i]._put(item)
break
else:
raise RuntimeError('Could not find section for item %s' % item[1])
def add_sentinel(self, section):
"""
Adds a new empty section as a sentinel. Call with an empty Section()
object. Call this method immediately after having added all sections
with add_section().
Once the get()-method returns None, you've received the sentinel and
you've thus exhausted the queue
"""
with self.not_full:
section.number_of_items = 1
self._add_section(section)
# Add the actual sentinel to the queue we just added
self._queues[-1]._put((None, None))
self.unfinished_tasks += 1
self.not_empty.notify()
def add_section(self, section):
"""
Add a new Section() to this Queue. Each section will be entirely
processed before moving on to the next section.
Be sure to set section.number_of_items correctly as it will signal
when processing is completely done for a specific section!
"""
with self.mutex:
self._add_section(section)
def change_section_number_of_items(self, section, number_of_items):
"""
Hit this method if you've reset section.number_of_items to make
sure we're not blocking
"""
with self.mutex:
self._change_section_number_of_items(section, number_of_items)
def _change_section_number_of_items(self, section, number_of_items):
section.number_of_items = number_of_items
if (self._current_section == section
and self._counter == number_of_items):
# We were actually waiting for more items to come in - but there
# aren't any!
self._init_next_section()
if self._qsize() > 0:
self.not_empty.notify()
def _add_section(self, section):
self._sections.append(section)
self._queues.append(
OrderedQueue() if section.plex_type == v.PLEX_TYPE_ALBUM
else Queue.Queue())
if self._current_section is None:
self._activate_next_section()
def _init_next_section(self):
"""
Call only when a section has been completely exhausted
"""
self._sections.popleft()
self._queues.popleft()
self._activate_next_section()
def _activate_next_section(self):
self._counter = 0
self._current_section = self._sections[0] if self._sections else None
self._current_queue = self._queues[0] if self._queues else None
def _get(self):
item = self._current_queue._get()
self._counter += 1
if self._counter == self._current_section.number_of_items:
self._init_next_section()
return item[1]
class OrderedQueue(Queue.PriorityQueue, object):
"""
Queue that enforces an order on the items it returns. An item you push
onto the queue must be a tuple
(index, item)
where index=-1 is the item that will be returned first. The Queue will block
until index=-1, 0, 1, 2, 3, ... is then made available
maxsize will be rather fuzzy, as _qsize returns 0 if we're still waiting
for the next smalles index. put() thus might not block always when it
should.
"""
def __init__(self, maxsize=0):
self.next_index = 0
super(OrderedQueue, self).__init__(maxsize)
def _qsize(self, len=len):
try:
return len(self.queue) if self.queue[0][0] == self.next_index else 0
except IndexError:
return 0
def _get(self, heappop=heapq.heappop):
self.next_index += 1
return heappop(self.queue)
return self._suspended
class Tasks(list):
@ -282,18 +178,13 @@ class Task(object):
def cancel(self):
self._canceled = True
def should_cancel(self):
return self._canceled or app.APP.monitor.abortRequested()
def isCanceled(self):
return self._canceled or xbmc.abortRequested
def isValid(self):
return not self.finished and not self._canceled
class ShutdownSentinel(Task):
def run(self):
pass
class FunctionAsTask(Task):
def __init__(self, function, callback, *args, **kwargs):
self._function = function
@ -320,7 +211,7 @@ class MutablePriorityQueue(Queue.PriorityQueue):
lowest = self.queue and min(self.queue) or None
except Exception:
lowest = None
utils.ERROR(notify=True)
utils.ERROR()
finally:
self.mutex.release()
return lowest
@ -341,14 +232,14 @@ class BackgroundWorker(object):
try:
task._run()
except Exception:
utils.ERROR(notify=True)
utils.ERROR()
def abort(self):
self._abort = True
return self
def aborted(self):
return self._abort or app.APP.monitor.abortRequested()
return self._abort or xbmc.abortRequested
def start(self):
if self._thread and self._thread.isAlive():
@ -371,13 +262,13 @@ class BackgroundWorker(object):
except Queue.Empty:
LOG.debug('(%s): Idle', self.name)
def shutdown(self, block=True):
def shutdown(self):
self.abort()
if self._task:
self._task.cancel()
if block and self._thread and self._thread.isAlive():
if self._thread and self._thread.isAlive():
LOG.debug('thread (%s): Waiting...', self.name)
self._thread.join()
LOG.debug('thread (%s): Done', self.name)
@ -392,17 +283,16 @@ class NonstoppingBackgroundWorker(BackgroundWorker):
super(NonstoppingBackgroundWorker, self).__init__(queue, name)
def _queueLoop(self):
LOG.debug('Starting Worker %s', self.name)
while not self.aborted():
self._task = self._queue.get()
if self._task is ShutdownSentinel:
break
self._working = True
self._runTask(self._task)
self._working = False
self._queue.task_done()
self._task = None
LOG.debug('Exiting Worker %s', self.name)
try:
self._task = self._queue.get_nowait()
self._working = True
self._runTask(self._task)
self._working = False
self._queue.task_done()
self._task = None
except Queue.Empty:
app.APP.monitor.waitForAbort(0.05)
def working(self):
return self._working
@ -414,10 +304,7 @@ class BackgroundThreader:
self._queue = MutablePriorityQueue()
self._abort = False
self.priority = -1
self.workers = [
worker(self._queue, 'queue.{0}:worker.{1}'.format(self.name, x))
for x in range(worker_count)
]
self.workers = [worker(self._queue, 'queue.{0}:worker.{1}'.format(self.name, x)) for x in range(worker_count)]
def _nextPriority(self):
self.priority += 1
@ -430,13 +317,13 @@ class BackgroundThreader:
return self
def aborted(self):
return self._abort or app.APP.monitor.abortRequested()
return self._abort or xbmc.abortRequested
def shutdown(self, block=True):
def shutdown(self):
self.abort()
self.addTasksToFront([ShutdownSentinel() for _ in self.workers])
for w in self.workers:
w.shutdown(block)
w.shutdown()
def addTask(self, task):
task.priority = self._nextPriority()
@ -489,9 +376,7 @@ class BackgroundThreader:
class ThreaderManager:
def __init__(self,
worker=NonstoppingBackgroundWorker,
worker_count=WORKER_COUNT):
def __init__(self, worker=BackgroundWorker, worker_count=6):
self.index = 0
self.abandoned = []
self._workerhandler = worker
@ -511,10 +396,10 @@ class ThreaderManager:
self.threader = BackgroundThreader(name=str(self.index),
worker=self._workerhandler)
def shutdown(self, block=True):
self.threader.shutdown(block)
def shutdown(self):
self.threader.shutdown()
for a in self.abandoned:
a.shutdown(block)
a.shutdown()
BGThreader = ThreaderManager()

View File

@ -3,8 +3,6 @@
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
import xbmc
from . import utils
from . import variables as v
@ -33,7 +31,7 @@ def getXArgsDeviceInfo(options=None, include_token=True):
'Connection': 'keep-alive',
"Content-Type": "application/x-www-form-urlencoded",
# "Access-Control-Allow-Origin": "*",
'Accept-Language': xbmc.getLanguage(xbmc.ISO_639_1),
# 'X-Plex-Language': 'en',
'X-Plex-Device': v.DEVICE,
'X-Plex-Model': v.MODEL,
'X-Plex-Device-Name': v.DEVICENAME,

View File

@ -49,7 +49,7 @@ def convert_alexa_to_companion(dictionary):
"""
The params passed by Alexa must first be converted to Companion talk
"""
for key in list(dictionary):
for key in dictionary:
if key in v.ALEXA_TO_COMPANION:
dictionary[v.ALEXA_TO_COMPANION[key]] = dictionary[key]
del dictionary[key]

View File

@ -7,7 +7,7 @@ import xbmcgui
from .plex_api import API
from .plex_db import PlexDB
from . import context, plex_functions as PF, playqueue as PQ
from . import context, plex_functions as PF
from . import utils, variables as v, app
###############################################################################
@ -60,6 +60,11 @@ class ContextMenu(object):
self.api = API(xml[0])
if self._select_menu():
self._action_menu()
if self._selected_option in (OPTIONS['Delete'],
OPTIONS['Refresh']):
LOG.info("refreshing container")
app.APP.monitor.waitForAbort(0.5)
xbmc.executebuiltin('Container.Refresh')
@staticmethod
def _get_plex_id(kodi_id, kodi_type):
@ -107,8 +112,7 @@ class ContextMenu(object):
"""
selected = self._selected_option
if selected == OPTIONS['Transcode']:
app.PLAYSTATE.force_transcode = True
self._PMS_play()
self._PMS_play(transcode=True)
elif selected == OPTIONS['PMS_Play']:
self._PMS_play()
elif selected == OPTIONS['Extras']:
@ -134,17 +138,21 @@ class ContextMenu(object):
if PF.delete_item_from_pms(self.plex_id) is False:
utils.dialog("ok", heading="{plex}", line1=utils.lang(30414))
def _PMS_play(self):
def _PMS_play(self, transcode=False):
"""
For using direct paths: Initiates playback using the PMS
"""
playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_KODI_TYPE[self.kodi_type])
playqueue.clear()
app.PLAYSTATE.context_menu_play = True
handle = self.api.fullpath(force_addon=True)[0]
handle = 'RunPlugin(%s)' % handle
xbmc.executebuiltin(handle.encode('utf-8'))
path = ('http://127.0.0.1:%s/plex/play/file.strm?plex_id=%s'
% (v.WEBSERVICE_PORT, self.plex_id))
if self.plex_type:
path += '&plex_type=%s' % self.plex_type
if self.kodi_id:
path += '&kodi_id=%s' % self.kodi_id
if self.kodi_type:
path += '&kodi_type=%s' % self.kodi_type
if transcode:
path += '&transcode=true'
xbmc.executebuiltin(('PlayMedia(%s)' % path).encode('utf-8'))
def _extras(self):
"""

View File

@ -1,97 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sqlite3
from functools import wraps
from . import variables as v, app
from .exceptions import LockedDatabase
DB_WRITE_ATTEMPTS = 100
DB_WRITE_ATTEMPTS_TIMEOUT = 1 # in seconds
DB_CONNECTION_TIMEOUT = 10
def catch_operationalerrors(method):
"""
sqlite.OperationalError is raised immediately if another DB connection
is open, reading something that we're trying to change
So let's catch it and try again
Also see https://github.com/mattn/go-sqlite3/issues/274
"""
@wraps(method)
def wrapper(self, *args, **kwargs):
attempts = DB_WRITE_ATTEMPTS
while True:
try:
return method(self, *args, **kwargs)
except sqlite3.OperationalError as err:
if 'database is locked' not in err:
# Not an error we want to catch, so reraise it
raise
attempts -= 1
if attempts == 0:
# Reraise in order to NOT catch nested OperationalErrors
raise LockedDatabase('Database is locked')
# Need to close the transactions and begin new ones
self.kodiconn.commit()
if self.artconn:
self.artconn.commit()
if app.APP.monitor.waitForAbort(DB_WRITE_ATTEMPTS_TIMEOUT):
# PKC needs to quit
return
# Start new transactions
self.kodiconn.execute('BEGIN')
if self.artconn:
self.artconn.execute('BEGIN')
return wrapper
def _initial_db_connection_setup(conn):
"""
Set-up DB e.g. for WAL journal mode, if that hasn't already been done
before. Also start a transaction
"""
conn.execute('PRAGMA journal_mode = WAL;')
conn.execute('PRAGMA cache_size = -8000;')
conn.execute('PRAGMA synchronous = NORMAL;')
conn.execute('BEGIN')
def connect(media_type=None):
"""
Open a connection to the Kodi database.
media_type: 'video' (standard if not passed), 'plex', 'music', 'texture'
"""
if media_type == "plex":
db_path = v.DB_PLEX_PATH
elif media_type == 'plex-copy':
db_path = v.DB_PLEX_COPY_PATH
elif media_type == "music":
db_path = v.DB_MUSIC_PATH
elif media_type == "texture":
db_path = v.DB_TEXTURE_PATH
else:
db_path = v.DB_VIDEO_PATH
conn = sqlite3.connect(db_path,
timeout=DB_CONNECTION_TIMEOUT,
isolation_level=None)
attempts = DB_WRITE_ATTEMPTS
while True:
try:
_initial_db_connection_setup(conn)
except sqlite3.OperationalError as err:
if 'database is locked' not in err:
# Not an error we want to catch, so reraise it
raise
attempts -= 1
if attempts == 0:
# Reraise in order to NOT catch nested OperationalErrors
raise LockedDatabase('Database is locked')
if app.APP.monitor.waitForAbort(0.05):
# PKC needs to quit
raise LockedDatabase('Database was locked and we need to exit')
else:
break
return conn

View File

@ -224,11 +224,7 @@ class DownloadUtils():
if r.status_code != 401:
self.count_unauthorized = 0
if return_response is True:
# return the entire response object
return r
elif r.status_code == 204:
if r.status_code == 204:
# No body in the response
# But read (empty) content to release connection back to pool
# (see requests: keep-alive documentation)
@ -262,6 +258,9 @@ class DownloadUtils():
elif r.status_code in (200, 201):
# 200: OK
# 201: Created
if return_response is True:
# return the entire response object
return r
try:
# xml response
r = utils.defused_etree.fromstring(r.content)
@ -292,8 +291,8 @@ class DownloadUtils():
return
else:
r.encoding = 'utf-8'
LOG.warn('Unknown answer from PMS %s with status code %s: %s',
url, r.status_code, r.text)
LOG.warn('Unknown answer from PMS %s with status code %s. ',
url, r.status_code)
return True
finally:

View File

@ -7,7 +7,6 @@ e.g. plugin://... calls. Hence be careful to only rely on window variables.
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
import sys
import copy
import xbmc
import xbmcplugin
@ -16,18 +15,16 @@ from xbmcgui import ListItem
from . import utils
from . import path_ops
from .downloadutils import DownloadUtils as DU
from .plex_api import API, mass_api
from .plex_api import API
from . import plex_functions as PF
from . import variables as v
# Be careful - your using app in another Python instance!
from . import app, widgets
from .library_sync.nodes import NODE_TYPES
LOG = getLogger('PLEX.entrypoint')
def guess_video_or_audio():
def guess_content_type():
"""
Returns either 'video', 'audio' or 'image', based how the user navigated to
the current view.
@ -67,7 +64,8 @@ def _wait_for_auth():
xbmcplugin.endOfDirectory(int(argv[1]), False) if failed
WARNING - this will potentially stall the shutdown of Kodi since we cannot
poll xbmc.Monitor().abortRequested() or waitForAbort()
poll xbmc.Monitor().abortRequested() or waitForAbort() or
xbmc.abortRequested
"""
counter = 0
startupdelay = int(utils.settings('startupDelay') or 0)
@ -104,9 +102,9 @@ def show_main_menu(content_type=None):
"""
Shows the main PKC menu listing with all libraries, Channel, settings, etc.
"""
content_type = content_type or guess_video_or_audio()
LOG.debug('Do main listing for %s', content_type)
xbmcplugin.setContent(int(sys.argv[1]), v.CONTENT_TYPE_FILE)
content_type = content_type or guess_content_type()
LOG.debug('Do main listing for content_type: %s', content_type)
xbmcplugin.setContent(int(sys.argv[1]), 'files')
# Get nodes from the window props
totalnodes = int(utils.window('Plex.nodes.total') or 0)
for i in range(totalnodes):
@ -119,21 +117,30 @@ def show_main_menu(content_type=None):
# we need to figure out which items to show in each listing. 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
if node_type == v.CONTENT_TYPE_PHOTO and content_type == 'image':
if node_type == 'photos' and content_type == 'image':
directory_item(label, path)
elif node_type in (v.CONTENT_TYPE_ARTIST,
v.CONTENT_TYPE_ALBUM,
v.CONTENT_TYPE_SONG) and content_type == 'audio':
elif node_type in ('artists',
'albums',
'songs') and content_type == 'audio':
directory_item(label, path)
elif node_type in (v.CONTENT_TYPE_MOVIE,
v.CONTENT_TYPE_SHOW,
v.CONTENT_TYPE_MUSICVIDEO) and content_type == 'video':
elif node_type in ('movies',
'tvshows',
'homevideos',
'musicvideos') and content_type == 'video':
directory_item(label, path)
elif content_type is None:
# To let the user pick this node as a WIDGET (content_type is None)
# Should only be called if the user selects widgets
LOG.info('Detected user selecting widgets')
directory_item(label, path)
if not path.startswith('library://'):
# Already using add-on paths (e.g. section not synched)
continue
# Add ANOTHER menu item that uses add-on paths instead of direct
# paths in order to let the user navigate into all submenus
addon_index = utils.window('Plex.nodes.%s.addon_index' % i)
# Append "(More...)" to the label
directory_item('%s (%s)' % (label, utils.lang(22082)), addon_index)
# Playlists
if content_type != 'image':
path = 'plugin://%s?mode=playlists' % v.ADDON_ID
@ -145,8 +152,6 @@ def show_main_menu(content_type=None):
if content_type:
path += '&content_type=%s' % content_type
directory_item('Plex Hub', path)
# Plex Search "Search"
directory_item(utils.lang(137), "plugin://%s?mode=search" % v.ADDON_ID)
# Plex Watch later
if content_type not in ('image', 'audio'):
directory_item(utils.lang(39211),
@ -164,82 +169,46 @@ def show_main_menu(content_type=None):
xbmcplugin.endOfDirectory(int(sys.argv[1]))
def show_section(section_index):
"""
Displays menu for an entire Plex section. We're using add-on paths instead
of Kodi video library xmls to be able to use type="filter" library xmls
and thus set the "content"
Only used for synched Plex sections - otherwise, PMS xml for the section
is used directly
"""
LOG.debug('Do section listing for section index %s', section_index)
xbmcplugin.setContent(int(sys.argv[1]), v.CONTENT_TYPE_FILE)
# Get nodes from the window props
node = 'Plex.nodes.%s' % section_index
content = utils.window('%s.type' % node)
plex_type = v.PLEX_TYPE_MOVIE if content == v.CONTENT_TYPE_MOVIE \
else v.PLEX_TYPE_SHOW
for node_type, _, _, _, _ in NODE_TYPES[plex_type]:
label = utils.window('%s.%s.title' % (node, node_type))
path = utils.window('%s.%s.index' % (node, node_type))
directory_item(label, path)
xbmcplugin.endOfDirectory(int(sys.argv[1]))
def show_listing(xml, plex_type=None, section_id=None, synched=True, key=None):
def show_listing(xml, plex_type=None, section_id=None, synched=True, key=None,
content_type=None):
"""
Pass synched=False if the items have not been synched to the Kodi DB
Kodi content type will be set using the very first item returned by the PMS
"""
content_type = content_type or guess_content_type()
LOG.debug('show_listing: content_type %s, section_id %s, synched %s, '
'key %s, plex_type %s', content_type, section_id, synched, key,
plex_type)
try:
xml[0]
except IndexError:
LOG.info('xml received from the PMS is empty: %s, %s',
xml.tag, xml.attrib)
xbmcplugin.endOfDirectory(int(sys.argv[1]))
LOG.info('xml received from the PMS is empty: %s', xml.attrib)
xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
return
api = API(xml[0])
# Determine content type for Kodi's Container.content
if key == '/hubs/home/continueWatching':
# Mix of movies and episodes
plex_type = v.PLEX_TYPE_VIDEO
elif key == '/hubs/home/recentlyAdded?type=2':
# "Recently Added TV", potentially a mix of Seasons and Episodes
plex_type = v.PLEX_TYPE_VIDEO
elif api.plex_type is None and api.fast_key and '?collection=' in api.fast_key:
# Collections/Kodi sets
plex_type = v.PLEX_TYPE_SET
elif api.plex_type is None and plex_type:
# e.g. browse by folder - folders will be listed first
# Retain plex_type
pass
if content_type == 'video':
xbmcplugin.setContent(int(sys.argv[1]), 'videos')
elif content_type == 'audio':
xbmcplugin.setContent(int(sys.argv[1]), 'artists')
elif plex_type in (v.PLEX_TYPE_PLAYLIST, v.PLEX_TYPE_CHANNEL):
xbmcplugin.setContent(int(sys.argv[1]), 'videos')
elif plex_type:
xbmcplugin.setContent(int(sys.argv[1]),
v.MEDIATYPE_FROM_PLEX_TYPE[plex_type])
else:
plex_type = api.plex_type
content_type = v.CONTENT_FROM_PLEX_TYPE[plex_type]
LOG.debug('show_listing: section_id %s, synched %s, key %s, plex_type %s, '
'content type %s',
section_id, synched, key, plex_type, content_type)
xbmcplugin.setContent(int(sys.argv[1]), content_type)
xbmcplugin.setContent(int(sys.argv[1]), 'files')
# Initialization
widgets.PLEX_TYPE = plex_type
widgets.SYNCHED = synched
if plex_type == v.PLEX_TYPE_EPISODE and key and 'onDeck' in key:
if plex_type == v.PLEX_TYPE_SHOW and key and 'onDeck' in key:
widgets.APPEND_SHOW_TITLE = utils.settings('OnDeckTvAppendShow') == 'true'
widgets.APPEND_SXXEXX = utils.settings('OnDeckTvAppendSeason') == 'true'
if plex_type == v.PLEX_TYPE_EPISODE and key and 'recentlyAdded' in key:
if plex_type == v.PLEX_TYPE_SHOW and key and 'recentlyAdded' in key:
widgets.APPEND_SHOW_TITLE = utils.settings('RecentTvAppendShow') == 'true'
widgets.APPEND_SXXEXX = utils.settings('RecentTvAppendSeason') == 'true'
if api.tag == 'Playlist':
# Only show video playlists if navigation started for videos
# and vice-versa for audio playlists
content = guess_video_or_audio()
if content:
for entry in reversed(xml):
tmp_api = API(entry)
if tmp_api.playlist_type() != content:
xml.remove(entry)
if content_type and xml[0].tag == 'Playlist':
# Certain views mix playlist types audio and video
for entry in reversed(xml):
if entry.get('playlistType') != content_type:
xml.remove(entry)
if xml.get('librarySectionID'):
widgets.SECTION_ID = utils.cast(int, xml.get('librarySectionID'))
elif section_id:
@ -248,13 +217,14 @@ def show_listing(xml, plex_type=None, section_id=None, synched=True, key=None):
# Need to chain keys for navigation
widgets.KEY = key
# Process all items to show
all_items = mass_api(xml)
all_items = utils.process_method_on_list(widgets.generate_item, all_items)
all_items = utils.process_method_on_list(widgets.prepare_listitem,
all_items)
if synched:
widgets.attach_kodi_ids(xml)
all_items = widgets.process_method_on_list(widgets.generate_item, xml)
all_items = widgets.process_method_on_list(widgets.prepare_listitem,
all_items)
# fill that listing...
all_items = utils.process_method_on_list(widgets.create_listitem,
all_items)
all_items = widgets.process_method_on_list(widgets.create_listitem,
all_items)
xbmcplugin.addDirectoryItems(int(sys.argv[1]), all_items, len(all_items))
# end directory listing
xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_UNSORTED)
@ -386,14 +356,15 @@ def playlists(content_type):
Lists all Plex playlists of the media type plex_playlist_type
content_type: 'audio', 'video'
"""
LOG.debug('Listing Plex playlists for content type %s', content_type)
content_type = content_type or guess_content_type()
LOG.debug('Listing Plex %s playlists', content_type)
if not _wait_for_auth():
return xbmcplugin.endOfDirectory(int(sys.argv[1]), False)
app.init(entrypoint=True)
from .playlists.pms import all_playlists
xml = all_playlists()
if xml is None:
return xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
return
if content_type is not None:
# This will be skipped if user selects a widget
# Buggy xml.remove(child) requires reversed()
@ -401,7 +372,7 @@ def playlists(content_type):
api = API(entry)
if not api.playlist_type() == content_type:
xml.remove(entry)
show_listing(xml)
show_listing(xml, content_type=content_type)
def hub(content_type):
@ -410,7 +381,7 @@ def hub(content_type):
content_type:
audio, video, image
"""
content_type = content_type or guess_video_or_audio()
content_type = content_type or guess_content_type()
LOG.debug('Showing Plex Hub entries for %s', content_type)
if not _wait_for_auth():
return xbmcplugin.endOfDirectory(int(sys.argv[1]), False)
@ -424,39 +395,23 @@ def hub(content_type):
# We need to make sure that only entries that WORK are displayed
# WARNING: using xml.remove(child) in for-loop requires traversing from
# the end!
pkc_cont_watching = None
for entry in reversed(xml):
api = API(entry)
append = False
if content_type == 'video' and api.plex_type in v.PLEX_VIDEOTYPES:
if content_type == 'video' and api.plex_type() in v.PLEX_VIDEOTYPES:
append = True
elif content_type == 'audio' and api.plex_type in v.PLEX_AUDIOTYPES:
elif content_type == 'audio' and api.plex_type() in v.PLEX_AUDIOTYPES:
append = True
elif content_type == 'image' and api.plex_type == v.PLEX_TYPE_PHOTO:
elif content_type == 'image' and api.plex_type() == v.PLEX_TYPE_PHOTO:
append = True
elif content_type != 'image' and api.plex_type == v.PLEX_TYPE_PLAYLIST:
elif content_type != 'image' and api.plex_type() == v.PLEX_TYPE_PLAYLIST:
append = True
elif content_type is None:
# Needed for widgets, where no content_type is provided
append = True
if not append:
xml.remove(entry)
# HACK ##################
# Merge Plex's "Continue watching" with "On deck"
if entry.get('key') == '/hubs/home/continueWatching':
pkc_cont_watching = copy.deepcopy(entry)
pkc_cont_watching.set('key', '/hubs/continueWatching')
title = pkc_cont_watching.get('title') or 'Continue Watching'
pkc_cont_watching.set('title', 'PKC %s' % title)
if pkc_cont_watching:
for i, entry in enumerate(xml):
if entry.get('key') == '/hubs/home/continueWatching':
xml.insert(i + 1, pkc_cont_watching)
break
# END HACK ##################
show_listing(xml)
show_listing(xml, content_type=content_type)
def watchlater():
@ -483,7 +438,7 @@ def watchlater():
def browse_plex(key=None, plex_type=None, section_id=None, synched=True,
args=None, prompt=None, query=None):
prompt=None):
"""
Lists the content of a Plex folder, e.g. channels. Either pass in key (to
be used directly for PMS url {server}<key>) or the section_id
@ -491,45 +446,27 @@ def browse_plex(key=None, plex_type=None, section_id=None, synched=True,
Pass synched=False if the items have NOT been synched to the Kodi DB
"""
LOG.debug('Browsing to key %s, section %s, plex_type: %s, synched: %s, '
'prompt "%s", args %s', key, section_id, plex_type, synched,
prompt, args)
'prompt "%s"', key, section_id, plex_type, synched, prompt)
if not _wait_for_auth():
xbmcplugin.endOfDirectory(int(sys.argv[1]), False)
return
return xbmcplugin.endOfDirectory(int(sys.argv[1]), False)
app.init(entrypoint=True)
args = args or {}
if query:
args['query'] = query
elif prompt:
if prompt:
prompt = utils.dialog('input', prompt)
if prompt is None:
# User cancelled
return
prompt = prompt.strip().decode('utf-8')
args['query'] = prompt
xml = DU().downloadUrl(utils.extend_url('{server}%s' % key, args))
if '?' not in key:
key = '%s?query=%s' % (key, prompt)
else:
key = '%s&query=%s' % (key, prompt)
xml = DU().downloadUrl('{server}%s' % key)
try:
xml[0].attrib
except (TypeError, IndexError, AttributeError):
xml.attrib
except AttributeError:
LOG.error('Could not browse to key %s, section %s',
key, section_id)
return
if xml[0].tag == 'Hub':
# E.g. when hitting the endpoint '/hubs/search'
answ = utils.etree.Element(xml.tag, attrib=xml.attrib)
for hub in xml:
if not utils.cast(int, hub.get('size')):
# Empty category
continue
for entry in hub:
api = API(entry)
if api.plex_type == v.PLEX_TYPE_TAG:
# Append the type before the actual element for all "tags"
# like genres, actors, etc.
entry.attrib['tag'] = '%s: %s' % (hub.get('title'),
api.tag_label())
answ.append(entry)
xml = answ
show_listing(xml, plex_type, section_id, synched, key)
@ -547,7 +484,7 @@ def extras(plex_id):
xbmcplugin.endOfDirectory(int(sys.argv[1]))
return
extras = API(xml[0]).extras()
if extras is None:
if not extras:
return
for child in xml:
xml.remove(child)

View File

@ -1,32 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
class PlaylistError(Exception):
"""
Exception for our playlist constructs
"""
pass
class LockedDatabase(Exception):
"""
Dedicated class to make sure we're not silently catching locked DBs.
"""
pass
class SubtitleError(Exception):
"""
Exceptions relating to subtitles
"""
pass
class ProcessingNotDone(Exception):
"""
Exception to detect whether we've completed our sync and did not have to
abort or suspend.
"""
pass

View File

@ -85,7 +85,8 @@ class InitialSetup(object):
if not port:
return False
url = '%s:%s' % (address, port)
# "Use HTTPS (SSL) connections? Answer should probably be yes."
# "Use HTTPS (SSL) connections? With Kodi 18 or later, HTTPS will likely
# not work!"
https = utils.yesno_dialog(utils.lang(29999), utils.lang(39217))
if https:
url = 'https://%s' % url
@ -241,18 +242,20 @@ class InitialSetup(object):
"""
Checks for server's connectivity. Returns check_connection result
"""
# Re-direct via plex if remote - will lead to the correct SSL
# certificate
if server['local']:
url = ('%s://%s:%s'
% (server['scheme'], server['ip'], server['port']))
# Deactive SSL verification if the server is local for Kodi 17
verifySSL = True if v.KODIVERSION >= 18 else False
else:
url = server['baseURL']
verifySSL = True
if not server['token']:
# Plex GDM: we only get the token from plex.tv after
# Sign-in to plex.tv
server['token'] = utils.settings('plexToken') or None
return PF.check_connection(server['baseURL'],
token=server['token'],
verifySSL=verifySSL)
chk = PF.check_connection(url,
token=server['token'],
verifySSL=verifySSL)
return chk
def pick_pms(self, showDialog=False, inform_of_search=False):
"""
@ -287,6 +290,7 @@ class InitialSetup(object):
}
or None if unsuccessful
"""
server = None
# If no server is set, let user choose one
if not app.CONN.server or not app.CONN.machine_identifier:
showDialog = True
@ -434,6 +438,7 @@ class InitialSetup(object):
utils.settings('plex_servername', server['name'])
utils.settings('plex_serverowned',
'true' if server['owned'] else 'false')
utils.settings('accessToken', server['token'])
# Careful to distinguish local from remote PMS
if server['local']:
scheme = server['scheme']
@ -504,13 +509,9 @@ class InitialSetup(object):
# (still used by Kodi, even though the Wiki says otherwise)
xml.set_setting(['musiclibrary', 'backgroundupdate'],
value='true')
cleanonupdate = xml.get_setting(
['videolibrary', 'cleanonupdate']) == 'true'
if utils.settings('useDirectPaths') != '1':
# Disable cleaning of library - not compatible with PKC
# Only do this for add-on paths
xml.set_setting(['videolibrary', 'cleanonupdate'],
value='false')
# Disable cleaning of library - not compatible with PKC
xml.set_setting(['videolibrary', 'cleanonupdate'],
value='false')
# Set completely watched point same as plex (and not 92%)
xml.set_setting(['video', 'ignorepercentatend'], value='10')
xml.set_setting(['video', 'playcountminimumpercent'],
@ -521,7 +522,6 @@ class InitialSetup(object):
except utils.ParseError:
cache = None
reboot = False
cleanonupdate = False
# Kodi default cache if no setting is set
cache = str(cache.text) if cache is not None else '20971520'
LOG.info('Current Kodi video memory cache in bytes: %s', cache)
@ -644,11 +644,6 @@ class InitialSetup(object):
utils.lang(39081), utils.lang(39082)) == 1:
LOG.debug("User opted to use direct paths.")
utils.settings('useDirectPaths', value="1")
if cleanonupdate:
# Re-enable cleanonupdate
with utils.XmlKodiSetting('advancedsettings.xml') as xml:
xml.set_setting(['videolibrary', 'cleanonupdate'],
value='true')
# Are you on a system where you would like to replace paths
# \\NAS\mymovie.mkv with smb://NAS/mymovie.mkv? (e.g. Windows)
if utils.yesno_dialog(utils.lang(29999), utils.lang(39033)):
@ -698,12 +693,14 @@ class InitialSetup(object):
# Open Settings page now? You will need to restart!
goto_settings = utils.yesno_dialog(utils.lang(29999),
utils.lang(39017))
# New installation - make sure we start with a clean slate
utils.wipe_database(reboot=False)
if goto_settings:
LOG.info('User chose to go to the PKC settings - suspending PKC')
app.APP.stop_pkc = True
executebuiltin(
'Addon.OpenSettings(plugin.video.plexkodiconnect)')
return
utils.reboot_kodi()
# New installation - make sure we start with a clean slate
# Will trigger a reboot, usually
utils.wipe_database()
# Reload relevant settings if that is not the case
app.CONN.load()
app.ACCOUNT.load()
app.SYNC.load()

View File

@ -6,7 +6,7 @@ from ntpath import dirname
from ..plex_db import PlexDB, PLEXDB_LOCK
from ..kodi_db import KodiVideoDB, KODIDB_LOCK
from .. import db, timing, app
from .. import utils, timing
LOG = getLogger('PLEX.itemtypes.common')
@ -57,11 +57,11 @@ class ItemBase(object):
if self.lock:
PLEXDB_LOCK.acquire()
KODIDB_LOCK.acquire()
self.plexconn = db.connect('plex')
self.plexconn = utils.kodi_sql('plex')
self.plexcursor = self.plexconn.cursor()
self.kodiconn = db.connect('video')
self.kodiconn = utils.kodi_sql('video')
self.kodicursor = self.kodiconn.cursor()
self.artconn = db.connect('texture')
self.artconn = utils.kodi_sql('texture')
self.artcursor = self.artconn.cursor()
self.plexdb = PlexDB(plexconn=self.plexconn, lock=False)
self.kodidb = KodiVideoDB(texture_db=True,
@ -136,36 +136,3 @@ class ItemBase(object):
duration,
view_count,
timing.plex_date_to_kodi(lastViewedAt))
@staticmethod
def sync_this_item(section_id):
"""
Returns False if we are NOT synching the corresponding Plex library
with section_id [int] to Kodi or if this sections has not yet been
encountered by PKC
"""
return section_id in app.SYNC.section_ids
def update_provider_ids(self, api, kodi_id):
"""
Updates the unique metadata provider ids (such as the IMDB id). Returns
a dict of the Kodi unique ids
"""
# We might have an old provider id stored!
self.kodidb.remove_uniqueid(kodi_id, api.kodi_type)
return self.add_provider_ids(api, kodi_id)
def add_provider_ids(self, api, kodi_id):
"""
Adds the unique ids for all metadata providers to the Kodi database,
such as IMDB or The Movie Database TMDB.
Returns a dict of the Kodi ids: {<provider>: <kodi_unique_id>}
"""
kodi_unique_ids = api.guids.copy()
for provider, provider_id in api.guids.iteritems():
kodi_unique_ids[provider] = self.kodidb.add_uniqueid(
kodi_id,
api.kodi_type,
provider_id,
provider)
return kodi_unique_ids

View File

@ -14,18 +14,16 @@ class Movie(ItemBase):
"""
Used for plex library-type movies
"""
def add_update(self, xml, section_name=None, section_id=None,
children=None):
def add_update(self, xml, section, children=None):
"""
Process single movie
"""
api = API(xml)
if not self.sync_this_item(section_id or api.library_section_id()):
LOG.debug('Skipping sync of %s %s: %s - section %s not synched to '
'Kodi', api.plex_type, api.plex_id, api.title(),
section_id or api.library_section_id())
plex_id = api.plex_id()
# Cannot parse XML, abort
if not plex_id:
LOG.error('Cannot parse XML data for movie: %s', xml.attrib)
return
plex_id = api.plex_id
movie = self.plexdb.movie(plex_id)
if movie:
update_item = True
@ -36,12 +34,51 @@ class Movie(ItemBase):
update_item = False
kodi_id = self.kodidb.new_movie_id()
fullpath, path, filename = api.fullpath()
if app.SYNC.direct_paths and not fullpath.startswith('http'):
kodi_pathid = self.kodidb.add_path(path,
content='movies',
scraper='metadata.local')
else:
userdata = api.userdata()
playcount = userdata['PlayCount']
dateplayed = userdata['LastPlayedDate']
resume = userdata['Resume']
runtime = userdata['Runtime']
rating = userdata['Rating']
title = api.title()
people = api.people()
genres = api.genre_list()
collections = api.collection_list()
countries = api.country_list()
studios = api.music_studio_list()
# GET THE FILE AND PATH #####
do_indirect = not app.SYNC.direct_paths
if app.SYNC.direct_paths:
# Direct paths is set the Kodi way
playurl = api.file_path(force_first_media=True)
if playurl is None:
# Something went wrong, trying to use non-direct paths
do_indirect = True
else:
playurl = api.validate_playurl(playurl, api.plex_type())
if playurl is None:
return False
if '\\' in playurl:
# Local path
filename = playurl.rsplit("\\", 1)[1]
else:
# Network share
filename = playurl.rsplit("/", 1)[1]
path = playurl.replace(filename, "")
kodi_pathid = self.kodidb.add_path(path,
content='movies',
scraper='metadata.local')
if do_indirect:
# Set plugin path and media flags using real filename
path = 'http://127.0.0.1:%s/plex/kodi/movies/' % v.WEBSERVICE_PORT
filename = '{0}/file.strm?kodi_id={1}&kodi_type={2}&plex_id={0}&plex_type={3}'
filename = filename.format(plex_id,
kodi_id,
v.KODI_TYPE_MOVIE,
v.PLEX_TYPE_MOVIE)
playurl = filename
kodi_pathid = self.kodidb.get_path(path)
if update_item:
@ -51,85 +88,138 @@ class Movie(ItemBase):
api.date_created())
if file_id != old_kodi_fileid:
self.kodidb.remove_file(old_kodi_fileid)
rating_id = self.kodidb.update_ratings(kodi_id,
v.KODI_TYPE_MOVIE,
"default",
api.rating(),
api.votecount())
unique_id = self.update_provider_ids(api, kodi_id)
rating_id = self.kodidb.get_ratingid(kodi_id,
v.KODI_TYPE_MOVIE)
self.kodidb.update_ratings(kodi_id,
v.KODI_TYPE_MOVIE,
"default",
rating,
api.votecount(),
rating_id)
# update new uniqueid Kodi 17
if api.provider('imdb') is not None:
uniqueid = self.kodidb.get_uniqueid(kodi_id,
v.KODI_TYPE_MOVIE)
self.kodidb.update_uniqueid(kodi_id,
v.KODI_TYPE_MOVIE,
api.provider('imdb'),
"imdb",
uniqueid)
else:
self.kodidb.remove_uniqueid(kodi_id, v.KODI_TYPE_MOVIE)
uniqueid = -1
self.kodidb.modify_people(kodi_id,
v.KODI_TYPE_MOVIE,
api.people())
api.people_list())
if app.SYNC.artwork:
self.kodidb.modify_artwork(api.artwork(),
kodi_id,
v.KODI_TYPE_MOVIE)
else:
LOG.info("ADD movie plex_id: %s - %s", plex_id, api.title())
LOG.info("ADD movie plex_id: %s - %s", plex_id, title)
file_id = self.kodidb.add_file(filename,
kodi_pathid,
api.date_created())
rating_id = self.kodidb.add_ratings(kodi_id,
v.KODI_TYPE_MOVIE,
"default",
api.rating(),
api.votecount())
unique_id = self.add_provider_ids(api, kodi_id)
rating_id = self.kodidb.add_ratingid()
self.kodidb.add_ratings(rating_id,
kodi_id,
v.KODI_TYPE_MOVIE,
"default",
rating,
api.votecount())
if api.provider('imdb') is not None:
uniqueid = self.kodidb.add_uniqueid_id()
self.kodidb.add_uniqueid(uniqueid,
kodi_id,
v.KODI_TYPE_MOVIE,
api.provider('imdb'),
"imdb")
else:
uniqueid = -1
self.kodidb.add_people(kodi_id,
v.KODI_TYPE_MOVIE,
api.people())
api.people_list())
if app.SYNC.artwork:
self.kodidb.add_artwork(api.artwork(),
kodi_id,
v.KODI_TYPE_MOVIE)
unique_id = self._prioritize_provider_id(unique_id)
# Update Kodi's main entry
self.kodidb.add_movie(kodi_id,
file_id,
api.title(),
title,
api.plot(),
api.shortplot(),
api.tagline(),
api.votecount(),
rating_id,
api.list_to_string(api.writers()),
api.list_to_string(people['Writer']),
api.year(),
unique_id,
uniqueid,
api.sorttitle(),
api.runtime(),
runtime,
api.content_rating(),
api.list_to_string(api.genres()),
api.list_to_string(api.directors()),
api.title(),
api.list_to_string(api.studios()),
api.list_to_string(genres),
api.list_to_string(people['Director']),
title,
api.list_to_string(studios),
api.trailer(),
api.list_to_string(api.countries()),
fullpath,
api.list_to_string(countries),
playurl,
kodi_pathid,
api.premiere_date(),
api.userrating())
userdata['UserRating'])
self.kodidb.modify_countries(kodi_id,
v.KODI_TYPE_MOVIE,
api.countries())
self.kodidb.modify_genres(kodi_id, v.KODI_TYPE_MOVIE, api.genres())
self.kodidb.modify_countries(kodi_id, v.KODI_TYPE_MOVIE, countries)
self.kodidb.modify_genres(kodi_id, v.KODI_TYPE_MOVIE, genres)
self.kodidb.modify_streams(file_id, api.mediastreams(), api.runtime())
self.kodidb.modify_studios(kodi_id, v.KODI_TYPE_MOVIE, api.studios())
tags = [section_name]
self._process_collections(api, tags, kodi_id, section_id, children)
self.kodidb.modify_streams(file_id, api.mediastreams(), runtime)
self.kodidb.modify_studios(kodi_id, v.KODI_TYPE_MOVIE, studios)
tags = [section.name]
if collections:
for plex_set_id, set_name in collections:
set_api = None
tags.append(set_name)
# Add any sets from Plex collection tags
kodi_set_id = self.kodidb.create_collection(set_name)
self.kodidb.assign_collection(kodi_set_id, kodi_id)
if not app.SYNC.artwork:
# Rest below is to get collection artwork
continue
if children is None:
# e.g. when added via websocket
LOG.debug('Costly looking up Plex collection %s: %s',
plex_set_id, set_name)
for index, coll_plex_id in api.collections_match(section.id):
# Get Plex artwork for collections - a pain
if index == plex_set_id:
set_xml = PF.GetPlexMetadata(coll_plex_id)
try:
set_xml.attrib
except AttributeError:
LOG.error('Could not get set metadata %s',
coll_plex_id)
continue
set_api = API(set_xml[0])
break
elif plex_set_id in children:
# Provided by get_metadata thread
set_api = API(children[plex_set_id][0])
if set_api:
self.kodidb.modify_artwork(set_api.artwork(),
kodi_set_id,
v.KODI_TYPE_SET)
self.kodidb.modify_tags(kodi_id, v.KODI_TYPE_MOVIE, tags)
# Process playstate
self.kodidb.set_resume(file_id,
api.resume_point(),
api.runtime(),
api.viewcount(),
api.lastplayed())
resume,
runtime,
playcount,
dateplayed)
self.plexdb.add_movie(plex_id=plex_id,
checksum=api.checksum(),
section_id=section_id,
section_id=section.id,
section_uuid=section.uuid,
kodi_id=kodi_id,
kodi_fileid=file_id,
kodi_pathid=kodi_pathid,
@ -179,67 +269,19 @@ class Movie(ItemBase):
"""
api = API(xml_element)
# Get key and db entry on the Kodi db side
db_item = self.plexdb.item_by_id(api.plex_id, plex_type)
db_item = self.plexdb.item_by_id(api.plex_id(), plex_type)
if not db_item:
LOG.info('Item not yet synced: %s', xml_element.attrib)
return False
# Grab the user's viewcount, resume points etc. from PMS' answer
userdata = api.userdata()
# Write to Kodi DB
self.kodidb.set_resume(db_item['kodi_fileid'],
api.resume_point(),
api.runtime(),
api.viewcount(),
api.lastplayed())
userdata['Resume'],
userdata['Runtime'],
userdata['PlayCount'],
userdata['LastPlayedDate'])
self.kodidb.update_userrating(db_item['kodi_id'],
db_item['kodi_type'],
api.userrating())
userdata['UserRating'])
return True
def _process_collections(self, api, tags, kodi_id, section_id, children):
for _, set_name in api.collections():
tags.append(set_name)
for plex_set_id, set_name in api.collections():
set_api = None
# Add any sets from Plex collection tags
kodi_set_id = self.kodidb.create_collection(set_name)
self.kodidb.assign_collection(kodi_set_id, kodi_id)
if not app.SYNC.artwork:
# Rest below is to get collection artwork
# TODO: continue instead of break (see TODO/break below)
break
if children is None:
# e.g. when added via websocket
LOG.debug('Costly looking up Plex collection %s: %s',
plex_set_id, set_name)
for index, coll_plex_id in api.collections_match(section_id):
# Get Plex artwork for collections - a pain
if index == plex_set_id:
set_xml = PF.GetPlexMetadata(coll_plex_id)
try:
set_xml.attrib
except AttributeError:
LOG.error('Could not get set metadata %s',
coll_plex_id)
continue
set_api = API(set_xml[0])
break
elif plex_set_id in children:
# Provided by get_metadata thread
set_api = API(children[plex_set_id][0])
if set_api:
self.kodidb.modify_artwork(set_api.artwork(),
kodi_set_id,
v.KODI_TYPE_SET)
# TODO: Once Kodi (19?) supports SEVERAL sets/collections per
# movie, support that. For now, we only take the very first
# collection/set that Plex returns
break
@staticmethod
def _prioritize_provider_id(unique_ids):
"""
Prioritize which ID ends up in the SHOW table (there can only be 1)
tvdb > imdb > tmdb
"""
return unique_ids.get('imdb',
unique_ids.get('tmdb',
unique_ids.get('tvdb')))

View File

@ -7,7 +7,7 @@ from .common import ItemBase
from ..plex_api import API
from ..plex_db import PlexDB, PLEXDB_LOCK
from ..kodi_db import KodiMusicDB, KODIDB_LOCK
from .. import plex_functions as PF, db, timing, app, variables as v
from .. import plex_functions as PF, utils, timing, app, variables as v
LOG = getLogger('PLEX.music')
@ -20,11 +20,11 @@ class MusicMixin(object):
if self.lock:
PLEXDB_LOCK.acquire()
KODIDB_LOCK.acquire()
self.plexconn = db.connect('plex')
self.plexconn = utils.kodi_sql('plex')
self.plexcursor = self.plexconn.cursor()
self.kodiconn = db.connect('music')
self.kodiconn = utils.kodi_sql('music')
self.kodicursor = self.kodiconn.cursor()
self.artconn = db.connect('texture')
self.artconn = utils.kodi_sql('texture')
self.artcursor = self.artconn.cursor()
self.plexdb = PlexDB(plexconn=self.plexconn, lock=False)
self.kodidb = KodiMusicDB(texture_db=True,
@ -42,17 +42,18 @@ class MusicMixin(object):
"""
api = API(xml_element)
# Get key and db entry on the Kodi db side
db_item = self.plexdb.item_by_id(api.plex_id, plex_type)
db_item = self.plexdb.item_by_id(api.plex_id(), plex_type)
if not db_item:
LOG.info('Item not yet synced: %s', xml_element.attrib)
return False
# Grab the user's viewcount, resume points etc. from PMS' answer
userdata = api.userdata()
self.kodidb.update_userrating(db_item['kodi_id'],
db_item['kodi_type'],
api.userrating())
userdata['UserRating'])
if plex_type == v.PLEX_TYPE_SONG:
self.kodidb.set_playcount(api.viewcount(),
api.lastplayed(),
self.kodidb.set_playcount(userdata['PlayCount'],
userdata['LastPlayedDate'],
db_item['kodi_id'],)
return True
@ -135,6 +136,7 @@ class MusicMixin(object):
'''
Remove an album
'''
self.kodidb.delete_album_from_discography(kodi_id)
if v.KODIVERSION < 18:
self.kodidb.delete_album_from_album_genre(kodi_id)
self.kodidb.remove_album(kodi_id)
@ -152,18 +154,15 @@ class Artist(MusicMixin, ItemBase):
"""
For Plex library-type artists
"""
def add_update(self, xml, section_name=None, section_id=None,
children=None):
def add_update(self, xml, section, children=None):
"""
Process a single artist
"""
api = API(xml)
if not self.sync_this_item(section_id or api.library_section_id()):
LOG.debug('Skipping sync of %s %s: %s - section %s not synched to '
'Kodi', api.plex_type, api.plex_id, api.title(),
section_id or api.library_section_id())
plex_id = api.plex_id()
if not plex_id:
LOG.error('Cannot process artist %s', xml.attrib)
return
plex_id = api.plex_id
artist = self.plexdb.artist(plex_id)
if not artist:
update_item = False
@ -198,7 +197,7 @@ class Artist(MusicMixin, ItemBase):
# Kodi doesn't allow that. In case that happens we just merge the
# artist entries.
kodi_id = self.kodidb.add_artist(api.title(), musicBrainzId)
self.kodidb.update_artist(api.list_to_string(api.genres()),
self.kodidb.update_artist(api.list_to_string(api.genre_list()),
api.plot(),
thumb,
fanart,
@ -210,26 +209,24 @@ class Artist(MusicMixin, ItemBase):
v.KODI_TYPE_ARTIST)
self.plexdb.add_artist(plex_id,
api.checksum(),
section_id,
section.id,
section.uuid,
kodi_id,
self.last_sync)
class Album(MusicMixin, ItemBase):
def add_update(self, xml, section_name=None, section_id=None,
children=None, scan_children=True):
def add_update(self, xml, section, children=None, scan_children=True):
"""
Process a single album
scan_children: set to False if you don't want to add children, e.g. to
avoid infinite loops
"""
api = API(xml)
if not self.sync_this_item(section_id or api.library_section_id()):
LOG.debug('Skipping sync of %s %s: %s - section %s not synched to '
'Kodi', api.plex_type, api.plex_id, api.title(),
section_id or api.library_section_id())
plex_id = api.plex_id()
if not plex_id:
LOG.error('Error processing album: %s', xml.attrib)
return
plex_id = api.plex_id
album = self.plexdb.album(plex_id)
if album:
update_item = True
@ -252,8 +249,7 @@ class Album(MusicMixin, ItemBase):
Artist(self.last_sync,
plexdb=self.plexdb,
kodidb=self.kodidb).add_update(artist_xml[0],
section_name,
section_id)
section=section)
artist = self.plexdb.artist(parent_id)
if not artist:
LOG.error('Adding artist %s failed for %s',
@ -276,9 +272,11 @@ class Album(MusicMixin, ItemBase):
compilation = 1
break
name = api.title()
userdata = api.userdata()
# Not yet implemented by Plex, let's use unique last.fm or gracenote
musicBrainzId = None
genre = api.list_to_string(api.genres())
genres = api.genre_list()
genre = api.list_to_string(genres)
if app.SYNC.artwork:
artworks = api.artwork()
if 'poster' in artworks:
@ -300,8 +298,8 @@ class Album(MusicMixin, ItemBase):
compilation,
api.plot(),
thumb,
api.list_to_string(api.studios()),
api.userrating(),
api.music_studio(),
userdata['UserRating'],
timing.unix_date_to_kodi(self.last_sync),
'album',
kodi_id)
@ -314,8 +312,8 @@ class Album(MusicMixin, ItemBase):
compilation,
api.plot(),
thumb,
api.list_to_string(api.studios()),
api.userrating(),
api.music_studio(),
userdata['UserRating'],
timing.unix_date_to_kodi(self.last_sync),
'album',
kodi_id)
@ -333,8 +331,8 @@ class Album(MusicMixin, ItemBase):
compilation,
api.plot(),
thumb,
api.list_to_string(api.studios()),
api.userrating(),
api.music_studio(),
userdata['UserRating'],
timing.unix_date_to_kodi(self.last_sync),
'album')
else:
@ -347,18 +345,24 @@ class Album(MusicMixin, ItemBase):
compilation,
api.plot(),
thumb,
api.list_to_string(api.studios()),
api.userrating(),
api.music_studio(),
userdata['UserRating'],
timing.unix_date_to_kodi(self.last_sync),
'album')
self.kodidb.add_albumartist(artist_id, kodi_id, api.artist_name())
if v.KODIVERSION < 18:
self.kodidb.add_discography(artist_id, name, api.year())
self.kodidb.add_music_genres(kodi_id,
genres,
v.KODI_TYPE_ALBUM)
if app.SYNC.artwork:
self.kodidb.modify_artwork(artworks,
kodi_id,
v.KODI_TYPE_ALBUM)
self.plexdb.add_album(plex_id,
api.checksum(),
section_id,
section.id,
section.uuid,
artist_id,
parent_id,
kodi_id,
@ -370,28 +374,24 @@ class Album(MusicMixin, ItemBase):
kodidb=self.kodidb)
for song in children:
context.add_update(song,
section_name=section_name,
section_id=section_id,
section=section,
album_xml=xml,
genres=api.genres(),
genres=genres,
genre=genre,
compilation=compilation)
class Song(MusicMixin, ItemBase):
def add_update(self, xml, section_name=None, section_id=None,
children=None, album_xml=None, genres=None, genre=None,
compilation=None):
def add_update(self, xml, section, children=None, album_xml=None,
genres=None, genre=None, compilation=None):
"""
Process single song/track
"""
api = API(xml)
if not self.sync_this_item(section_id or api.library_section_id()):
LOG.debug('Skipping sync of %s %s: %s - section %s not synched to '
'Kodi', api.plex_type, api.plex_id, api.title(),
section_id or api.library_section_id())
plex_id = api.plex_id()
if not plex_id:
LOG.error('Error processing song: %s', xml.attrib)
return
plex_id = api.plex_id
song = self.plexdb.song(plex_id)
if song:
update_item = True
@ -418,8 +418,7 @@ class Song(MusicMixin, ItemBase):
Artist(self.last_sync,
plexdb=self.plexdb,
kodidb=self.kodidb).add_update(artist_xml[0],
section_name,
section_id)
section=section)
artist = self.plexdb.artist(artist_id)
if not artist:
LOG.error('Still could not find grandparent artist %s for %s',
@ -474,8 +473,7 @@ class Song(MusicMixin, ItemBase):
Album(self.last_sync,
plexdb=self.plexdb,
kodidb=self.kodidb).add_update(album_xml[0],
section_name,
section_id,
section=section,
children=[xml],
scan_children=False)
album = self.plexdb.album(album_id)
@ -489,6 +487,11 @@ class Song(MusicMixin, ItemBase):
# Not yet implemented by Plex
musicBrainzId = None
comment = None
userdata = api.userdata()
playcount = userdata['PlayCount']
if playcount is None:
# This is different to Video DB!
playcount = 0
# Getting artists name is complicated
if compilation is not None:
if compilation == 0:
@ -498,7 +501,7 @@ class Song(MusicMixin, ItemBase):
else:
# compilation not set
artists = xml.get('originalTitle', api.grandparent_title())
tracknumber = api.index() or 0
tracknumber = api.track_number() or 0
disc = api.disc_number() or 1
if disc == 1:
track = tracknumber
@ -514,7 +517,33 @@ class Song(MusicMixin, ItemBase):
if entry.tag == 'Mood':
moods.append(entry.attrib['tag'])
mood = api.list_to_string(moods)
_, path, filename = api.fullpath()
# GET THE FILE AND PATH #####
do_indirect = not app.SYNC.direct_paths
if app.SYNC.direct_paths:
# Direct paths is set the Kodi way
playurl = api.file_path(force_first_media=True)
if playurl is None:
# Something went wrong, trying to use non-direct paths
do_indirect = True
else:
playurl = api.validate_playurl(playurl, api.plex_type())
if playurl is None:
return False
if "\\" in playurl:
# Local path
filename = playurl.rsplit("\\", 1)[1]
else:
# Network share
filename = playurl.rsplit("/", 1)[1]
path = playurl.replace(filename, "")
if do_indirect:
# Plex works a bit differently
path = "%s%s" % (app.CONN.server, xml[0][0].get('key'))
path = api.attach_plex_token_to_url(path)
filename = path.rsplit('/', 1)[1]
path = path.replace(filename, '')
# UPDATE THE SONG #####
if update_item:
LOG.info("UPDATE song plex_id: %s - %s", plex_id, title)
@ -528,12 +557,12 @@ class Song(MusicMixin, ItemBase):
genre,
title,
track,
api.runtime(),
userdata['Runtime'],
year,
filename,
api.viewcount(),
api.lastplayed(),
api.userrating(),
playcount,
userdata['LastPlayedDate'],
userdata['UserRating'],
comment,
mood,
api.date_created(),
@ -544,12 +573,12 @@ class Song(MusicMixin, ItemBase):
genre,
title,
track,
api.runtime(),
userdata['Runtime'],
year,
filename,
api.viewcount(),
api.lastplayed(),
api.userrating(),
playcount,
userdata['LastPlayedDate'],
userdata['UserRating'],
comment,
mood,
api.date_created(),
@ -569,13 +598,13 @@ class Song(MusicMixin, ItemBase):
genre,
title,
track,
api.runtime(),
userdata['Runtime'],
year,
filename,
musicBrainzId,
api.viewcount(),
api.lastplayed(),
api.userrating(),
playcount,
userdata['LastPlayedDate'],
userdata['UserRating'],
0,
0,
mood,
@ -588,13 +617,13 @@ class Song(MusicMixin, ItemBase):
genre,
title,
track,
api.runtime(),
userdata['Runtime'],
year,
filename,
musicBrainzId,
api.viewcount(),
api.lastplayed(),
api.userrating(),
playcount,
userdata['LastPlayedDate'],
userdata['UserRating'],
0,
0,
mood,
@ -605,7 +634,7 @@ class Song(MusicMixin, ItemBase):
parent_id,
track,
title,
api.runtime())
userdata['Runtime'])
# Link song to artists
artist_name = api.grandparent_title()
# Do the actual linking
@ -625,7 +654,8 @@ class Song(MusicMixin, ItemBase):
v.KODI_TYPE_ALBUM)
self.plexdb.add_song(plex_id,
api.checksum(),
section_id,
section.id,
section.uuid,
artist_id,
grandparent_id,
album_id,

View File

@ -18,26 +18,27 @@ class TvShowMixin(object):
"""
api = API(xml_element)
# Get key and db entry on the Kodi db side
db_item = self.plexdb.item_by_id(api.plex_id, plex_type)
db_item = self.plexdb.item_by_id(api.plex_id(), plex_type)
if not db_item:
LOG.info('Item not yet synced: %s', xml_element.attrib)
return False
# Grab the user's viewcount, resume points etc. from PMS' answer
userdata = api.userdata()
self.kodidb.update_userrating(db_item['kodi_id'],
db_item['kodi_type'],
api.userrating())
userdata['UserRating'])
if plex_type == v.PLEX_TYPE_EPISODE:
self.kodidb.set_resume(db_item['kodi_fileid'],
api.resume_point(),
api.runtime(),
api.viewcount(),
api.lastplayed())
userdata['Resume'],
userdata['Runtime'],
userdata['PlayCount'],
userdata['LastPlayedDate'])
if db_item['kodi_fileid_2']:
self.kodidb.set_resume(db_item['kodi_fileid_2'],
api.resume_point(),
api.runtime(),
api.viewcount(),
api.lastplayed())
userdata['Resume'],
userdata['Runtime'],
userdata['PlayCount'],
userdata['LastPlayedDate'])
return True
def remove(self, plex_id, plex_type=None):
@ -142,18 +143,15 @@ class Show(TvShowMixin, ItemBase):
"""
For Plex library-type TV shows
"""
def add_update(self, xml, section_name=None, section_id=None,
children=None):
def add_update(self, xml, section, children=None):
"""
Process a single show
"""
api = API(xml)
if not self.sync_this_item(section_id or api.library_section_id()):
LOG.debug('Skipping sync of %s %s: %s - section %s not synched to '
'Kodi', api.plex_type, api.plex_id, api.title(),
section_id or api.library_section_id())
plex_id = api.plex_id()
if not plex_id:
LOG.error("Cannot parse XML data for TV show: %s", xml.attrib)
return
plex_id = api.plex_id
show = self.plexdb.show(plex_id)
if not show:
update_item = False
@ -163,11 +161,16 @@ class Show(TvShowMixin, ItemBase):
kodi_id = show['kodi_id']
kodi_pathid = show['kodi_pathid']
genres = api.genre_list()
genre = api.list_to_string(genres)
studios = api.music_studio_list()
studio = api.list_to_string(studios)
# GET THE FILE AND PATH #####
if app.SYNC.direct_paths:
# Direct paths is set the Kodi way
playurl = api.validate_playurl(api.tv_show_path(),
api.plex_type,
api.plex_type(),
folder=True)
if playurl is None:
return
@ -177,7 +180,8 @@ class Show(TvShowMixin, ItemBase):
scraper='metadata.local')
else:
# Set plugin path
toplevelpath = "plugin://%s.tvshows/" % v.ADDON_ID
toplevelpath = ('http://127.0.0.1:%s/plex/kodi/shows/'
% v.WEBSERVICE_PORT)
path = "%s%s/" % (toplevelpath, plex_id)
# Do NOT set a parent id because addon-path cannot be "stacked"
toppathid = None
@ -189,16 +193,27 @@ class Show(TvShowMixin, ItemBase):
if update_item:
LOG.info("UPDATE tvshow plex_id: %s - %s", plex_id, api.title())
# update new ratings Kodi 17
rating_id = self.kodidb.update_ratings(kodi_id,
v.KODI_TYPE_SHOW,
"default",
api.rating(),
api.votecount())
unique_id = self._prioritize_provider_id(
self.update_provider_ids(api, kodi_id))
rating_id = self.kodidb.get_ratingid(kodi_id, v.KODI_TYPE_SHOW)
self.kodidb.update_ratings(kodi_id,
v.KODI_TYPE_SHOW,
"default",
api.audience_rating(),
api.votecount(),
rating_id)
if api.provider('tvdb') is not None:
uniqueid = self.kodidb.get_uniqueid(kodi_id,
v.KODI_TYPE_SHOW)
self.kodidb.update_uniqueid(kodi_id,
v.KODI_TYPE_SHOW,
api.provider('tvdb'),
"unknown",
uniqueid)
else:
self.kodidb.remove_uniqueid(kodi_id, v.KODI_TYPE_SHOW)
uniqueid = -1
self.kodidb.modify_people(kodi_id,
v.KODI_TYPE_SHOW,
api.people())
api.people_list())
if app.SYNC.artwork:
self.kodidb.modify_artwork(api.artwork(),
kodi_id,
@ -208,11 +223,11 @@ class Show(TvShowMixin, ItemBase):
api.plot(),
rating_id,
api.premiere_date(),
api.list_to_string(api.genres()),
genre,
api.title(),
unique_id,
uniqueid,
api.content_rating(),
api.list_to_string(api.studios()),
studio,
api.sorttitle(),
kodi_id)
# OR ADD THE TVSHOW #####
@ -220,16 +235,25 @@ class Show(TvShowMixin, ItemBase):
LOG.info("ADD tvshow plex_id: %s - %s", plex_id, api.title())
# Link the path
self.kodidb.add_showlinkpath(kodi_id, kodi_pathid)
rating_id = self.kodidb.add_ratings(kodi_id,
v.KODI_TYPE_SHOW,
"default",
api.rating(),
api.votecount())
unique_id = self._prioritize_provider_id(
self.add_provider_ids(api, kodi_id))
rating_id = self.kodidb.get_ratingid(kodi_id, v.KODI_TYPE_SHOW)
self.kodidb.add_ratings(rating_id,
kodi_id,
v.KODI_TYPE_SHOW,
"default",
api.audience_rating(),
api.votecount())
if api.provider('tvdb'):
uniqueid = self.kodidb.add_uniqueid_id()
self.kodidb.add_uniqueid(uniqueid,
kodi_id,
v.KODI_TYPE_SHOW,
api.provider('tvdb'),
"unknown")
else:
uniqueid = -1
self.kodidb.add_people(kodi_id,
v.KODI_TYPE_SHOW,
api.people())
api.people_list())
if app.SYNC.artwork:
self.kodidb.add_artwork(api.artwork(),
kodi_id,
@ -240,50 +264,39 @@ class Show(TvShowMixin, ItemBase):
api.plot(),
rating_id,
api.premiere_date(),
api.list_to_string(api.genres()),
genre,
api.title(),
unique_id,
uniqueid,
api.content_rating(),
api.list_to_string(api.studios()),
studio,
api.sorttitle())
self.kodidb.modify_genres(kodi_id, v.KODI_TYPE_SHOW, api.genres())
self.kodidb.modify_genres(kodi_id, v.KODI_TYPE_SHOW, genres)
# Process studios
self.kodidb.modify_studios(kodi_id, v.KODI_TYPE_SHOW, api.studios())
self.kodidb.modify_studios(kodi_id, v.KODI_TYPE_SHOW, studios)
# Process tags: view, PMS collection tags
tags = [section_name]
tags.extend([i for _, i in api.collections()])
tags = [section.name]
tags.extend([i for _, i in api.collection_list()])
self.kodidb.modify_tags(kodi_id, v.KODI_TYPE_SHOW, tags)
self.plexdb.add_show(plex_id=plex_id,
checksum=api.checksum(),
section_id=section_id,
section_id=section.id,
section_uuid=section.uuid,
kodi_id=kodi_id,
kodi_pathid=kodi_pathid,
last_sync=self.last_sync)
@staticmethod
def _prioritize_provider_id(unique_ids):
"""
Prioritize which ID ends up in the SHOW table (there can only be 1)
tvdb > imdb > tmdb
"""
return unique_ids.get('tvdb',
unique_ids.get('imdb',
unique_ids.get('tmdb')))
class Season(TvShowMixin, ItemBase):
def add_update(self, xml, section_name=None, section_id=None,
children=None):
def add_update(self, xml, section, children=None):
"""
Process a single season of a certain tv show
"""
api = API(xml)
if not self.sync_this_item(section_id or api.library_section_id()):
LOG.debug('Skipping sync of %s %s: %s - section %s not synched to '
'Kodi', api.plex_type, api.plex_id, api.season_name(),
section_id or api.library_section_id())
plex_id = api.plex_id()
if not plex_id:
LOG.error('Error getting plex_id for season, skipping: %s',
xml.attrib)
return
plex_id = api.plex_id
season = self.plexdb.season(plex_id)
if not season:
update_item = False
@ -302,8 +315,7 @@ class Season(TvShowMixin, ItemBase):
Show(self.last_sync,
plexdb=self.plexdb,
kodidb=self.kodidb).add_update(show_xml[0],
section_name,
section_id)
section=section)
show = self.plexdb.show(show_id)
if not show:
LOG.error('Still could not find parent tv show %s', show_id)
@ -318,31 +330,23 @@ class Season(TvShowMixin, ItemBase):
if key in artwork and artwork[key] == parent_artwork[key]:
del artwork[key]
if update_item:
LOG.info('UPDATE season plex_id %s - %s',
plex_id, api.season_name())
LOG.info('UPDATE season plex_id %s - %s', plex_id, api.title())
kodi_id = season['kodi_id']
self.kodidb.update_season(kodi_id,
parent_id,
api.index(),
api.season_name(),
api.userrating() or None)
if app.SYNC.artwork:
self.kodidb.modify_artwork(artwork,
kodi_id,
v.KODI_TYPE_SEASON)
else:
LOG.info('ADD season plex_id %s - %s', plex_id, api.season_name())
kodi_id = self.kodidb.add_season(parent_id,
api.index(),
api.season_name(),
api.userrating() or None)
LOG.info('ADD season plex_id %s - %s', plex_id, api.title())
kodi_id = self.kodidb.add_season(parent_id, api.season_number())
if app.SYNC.artwork:
self.kodidb.add_artwork(artwork,
kodi_id,
v.KODI_TYPE_SEASON)
self.plexdb.add_season(plex_id=plex_id,
checksum=api.checksum(),
section_id=section_id,
section_id=section.id,
section_uuid=section.uuid,
show_id=show_id,
parent_id=parent_id,
kodi_id=kodi_id,
@ -350,18 +354,16 @@ class Season(TvShowMixin, ItemBase):
class Episode(TvShowMixin, ItemBase):
def add_update(self, xml, section_name=None, section_id=None,
children=None):
def add_update(self, xml, section, children=None):
"""
Process single episode
"""
api = API(xml)
if not self.sync_this_item(section_id or api.library_section_id()):
LOG.debug('Skipping sync of %s %s: %s - section %s not synched to '
'Kodi', api.plex_type, api.plex_id, api.title(),
section_id or api.library_section_id())
plex_id = api.plex_id()
if not plex_id:
LOG.error('Error getting plex_id for episode, skipping: %s',
xml.attrib)
return
plex_id = api.plex_id
episode = self.plexdb.episode(plex_id)
if not episode:
update_item = False
@ -373,64 +375,96 @@ class Episode(TvShowMixin, ItemBase):
old_kodi_fileid_2 = episode['kodi_fileid_2']
kodi_pathid = episode['kodi_pathid']
peoples = api.people()
director = api.list_to_string(peoples['Director'])
writer = api.list_to_string(peoples['Writer'])
userdata = api.userdata()
show_id, season_id, _, season_no, episode_no = api.episode_data()
if season_no is None:
season_no = -1
if episode_no is None:
episode_no = -1
airs_before_season = "-1"
airs_before_episode = "-1"
# The grandparent TV show
show = self.plexdb.show(api.show_id())
show = self.plexdb.show(show_id)
if not show:
LOG.warn('Grandparent TV show %s not found in DB, adding it', api.show_id())
show_xml = PF.GetPlexMetadata(api.show_id())
LOG.warn('Grandparent TV show %s not found in DB, adding it', show_id)
show_xml = PF.GetPlexMetadata(show_id)
try:
show_xml[0].attrib
except (TypeError, IndexError, AttributeError):
LOG.error("Grandparent tvshow %s xml download failed", api.show_id())
LOG.error("Grandparent tvshow %s xml download failed", show_id)
return False
Show(self.last_sync,
plexdb=self.plexdb,
kodidb=self.kodidb).add_update(show_xml[0],
section_name,
section_id)
show = self.plexdb.show(api.show_id())
section=section)
show = self.plexdb.show(show_id)
if not show:
LOG.error('Still could not find grandparent tv show %s', api.show_id())
LOG.error('Still could not find grandparent tv show %s', show_id)
return
grandparent_id = show['kodi_id']
# The parent Season
season = self.plexdb.season(api.season_id())
if not season and api.season_id():
LOG.warn('Parent season %s not found in DB, adding it', api.season_id())
season_xml = PF.GetPlexMetadata(api.season_id())
season = self.plexdb.season(season_id)
if not season and season_id:
LOG.warn('Parent season %s not found in DB, adding it', season_id)
season_xml = PF.GetPlexMetadata(season_id)
try:
season_xml[0].attrib
except (TypeError, IndexError, AttributeError):
LOG.error("Parent season %s xml download failed", api.season_id())
LOG.error("Parent season %s xml download failed", season_id)
return False
Season(self.last_sync,
plexdb=self.plexdb,
kodidb=self.kodidb).add_update(season_xml[0],
section_name,
section_id)
season = self.plexdb.season(api.season_id())
section=section)
season = self.plexdb.season(season_id)
if not season:
LOG.error('Still could not find parent season %s', api.season_id())
LOG.error('Still could not find parent season %s', season_id)
return
parent_id = season['kodi_id'] if season else None
fullpath, path, filename = api.fullpath()
if app.SYNC.direct_paths and not fullpath.startswith('http'):
parent_path_id = self.kodidb.parent_path_id(path)
kodi_pathid = self.kodidb.add_path(path,
id_parent_path=parent_path_id)
else:
# GET THE FILE AND PATH #####
do_indirect = not app.SYNC.direct_paths
if app.SYNC.direct_paths:
playurl = api.file_path(force_first_media=True)
if playurl is None:
do_indirect = True
else:
playurl = api.validate_playurl(playurl, v.PLEX_TYPE_EPISODE)
if "\\" in playurl:
# Local path
filename = playurl.rsplit("\\", 1)[1]
else:
# Network share
filename = playurl.rsplit("/", 1)[1]
path = playurl.replace(filename, "")
parent_path_id = self.kodidb.parent_path_id(path)
kodi_pathid = self.kodidb.add_path(path,
id_parent_path=parent_path_id)
if do_indirect:
# Set plugin path - do NOT use "intermediate" paths for the show
# as with direct paths!
# Set plugin path and media flags using real filename
path = ('http://127.0.0.1:%s/plex/kodi/shows/%s/'
% (v.WEBSERVICE_PORT, show_id))
filename = '{0}/file.strm?kodi_id={1}&kodi_type={2}&plex_id={0}&plex_type={3}'
filename = filename.format(plex_id,
kodi_id,
v.KODI_TYPE_EPISODE,
v.PLEX_TYPE_EPISODE)
playurl = filename
# Root path tvshows/ already saved in Kodi DB
kodi_pathid = self.kodidb.add_path(path)
kodi_pathid = self.kodidb.get_path(path)
# HACK
# need to set a 2nd file entry for a path without plex show id
# This fixes e.g. context menu and widgets working as they
# should
# A dirty hack, really
path_2 = 'plugin://%s.tvshows/' % v.ADDON_ID
path_2 = 'http://127.0.0.1:%s/plex/kodi/shows/' % v.WEBSERVICE_PORT
# filename_2 is exactly the same as filename
# so WITH plex show id!
kodi_pathid_2 = self.kodidb.add_path(path_2)
@ -452,16 +486,28 @@ class Episode(TvShowMixin, ItemBase):
self.kodidb.remove_file(old_kodi_fileid)
if not app.SYNC.direct_paths:
self.kodidb.remove_file(old_kodi_fileid_2)
ratingid = self.kodidb.update_ratings(kodi_id,
v.KODI_TYPE_EPISODE,
"default",
api.rating(),
api.votecount())
unique_id = self._prioritize_provider_id(
self.update_provider_ids(api, kodi_id))
ratingid = self.kodidb.get_ratingid(kodi_id,
v.KODI_TYPE_EPISODE)
self.kodidb.update_ratings(kodi_id,
v.KODI_TYPE_EPISODE,
"default",
userdata['Rating'],
api.votecount(),
ratingid)
if api.provider('tvdb'):
uniqueid = self.kodidb.get_uniqueid(kodi_id,
v.KODI_TYPE_EPISODE)
self.kodidb.update_uniqueid(kodi_id,
v.KODI_TYPE_EPISODE,
api.provider('tvdb'),
"tvdb",
uniqueid)
else:
self.kodidb.remove_uniqueid(kodi_id, v.KODI_TYPE_EPISODE)
uniqueid = -1
self.kodidb.modify_people(kodi_id,
v.KODI_TYPE_EPISODE,
api.people())
api.people_list())
if app.SYNC.artwork:
self.kodidb.modify_artwork(api.artwork(),
kodi_id,
@ -469,39 +515,39 @@ class Episode(TvShowMixin, ItemBase):
self.kodidb.update_episode(api.title(),
api.plot(),
ratingid,
api.list_to_string(api.writers()),
writer,
api.premiere_date(),
api.runtime(),
api.list_to_string(api.directors()),
api.season_number(),
api.index(),
director,
season_no,
episode_no,
api.title(),
airs_before_season,
airs_before_episode,
fullpath,
playurl,
kodi_pathid,
unique_id,
kodi_fileid, # and NOT kodi_fileid_2
parent_id,
api.userrating(),
userdata['UserRating'],
kodi_id)
self.kodidb.set_resume(kodi_fileid,
api.resume_point(),
api.runtime(),
api.viewcount(),
api.lastplayed())
userdata['PlayCount'],
userdata['LastPlayedDate'])
if not app.SYNC.direct_paths:
self.kodidb.set_resume(kodi_fileid_2,
api.resume_point(),
api.runtime(),
api.viewcount(),
api.lastplayed())
userdata['PlayCount'],
userdata['LastPlayedDate'])
self.plexdb.add_episode(plex_id=plex_id,
checksum=api.checksum(),
section_id=section_id,
show_id=api.show_id(),
section_id=section.id,
section_uuid=section.uuid,
show_id=show_id,
grandparent_id=grandparent_id,
season_id=api.season_id(),
season_id=season_id,
parent_id=parent_id,
kodi_id=kodi_id,
kodi_fileid=kodi_fileid,
@ -521,16 +567,23 @@ class Episode(TvShowMixin, ItemBase):
else:
kodi_fileid_2 = None
rating_id = self.kodidb.add_ratings(kodi_id,
v.KODI_TYPE_EPISODE,
"default",
api.rating(),
api.votecount())
unique_id = self._prioritize_provider_id(
self.add_provider_ids(api, kodi_id))
rating_id = self.kodidb.add_ratingid()
self.kodidb.add_ratings(rating_id,
kodi_id,
v.KODI_TYPE_EPISODE,
"default",
userdata['Rating'],
api.votecount())
if api.provider('tvdb'):
uniqueid = self.kodidb.add_uniqueid_id()
self.kodidb.add_uniqueid(uniqueid,
kodi_id,
v.KODI_TYPE_EPISODE,
api.provider('tvdb'),
"tvdb")
self.kodidb.add_people(kodi_id,
v.KODI_TYPE_EPISODE,
api.people())
api.people_list())
if app.SYNC.artwork:
self.kodidb.add_artwork(api.artwork(),
kodi_id,
@ -540,38 +593,38 @@ class Episode(TvShowMixin, ItemBase):
api.title(),
api.plot(),
rating_id,
api.list_to_string(api.writers()),
writer,
api.premiere_date(),
api.runtime(),
api.list_to_string(api.directors()),
api.season_number(),
api.index(),
director,
season_no,
episode_no,
api.title(),
grandparent_id,
airs_before_season,
airs_before_episode,
fullpath,
playurl,
kodi_pathid,
unique_id,
parent_id,
api.userrating())
userdata['UserRating'])
self.kodidb.set_resume(kodi_fileid,
api.resume_point(),
api.runtime(),
api.viewcount(),
api.lastplayed())
userdata['PlayCount'],
userdata['LastPlayedDate'])
if not app.SYNC.direct_paths:
self.kodidb.set_resume(kodi_fileid_2,
api.resume_point(),
api.runtime(),
api.viewcount(),
api.lastplayed())
userdata['PlayCount'],
userdata['LastPlayedDate'])
self.plexdb.add_episode(plex_id=plex_id,
checksum=api.checksum(),
section_id=section_id,
show_id=api.show_id(),
section_id=section.id,
section_uuid=section.uuid,
show_id=show_id,
grandparent_id=grandparent_id,
season_id=api.season_id(),
season_id=season_id,
parent_id=parent_id,
kodi_id=kodi_id,
kodi_fileid=kodi_fileid,
@ -582,13 +635,3 @@ class Episode(TvShowMixin, ItemBase):
self.kodidb.modify_streams(kodi_fileid, # and NOT kodi_fileid_2
api.mediastreams(),
api.runtime())
@staticmethod
def _prioritize_provider_id(unique_ids):
"""
Prioritize which ID ends up in the SHOW table (there can only be 1)
tvdb > imdb > tmdb
"""
return unique_ids.get('tvdb',
unique_ids.get('imdb',
unique_ids.get('tmdb')))

View File

@ -170,10 +170,10 @@ def stop():
def seek_to(offset):
"""
Seeks all Kodi players to offset [int] in milliseconds
Seeks all Kodi players to offset [int]
"""
for playerid in get_player_ids():
return JsonRPC("Player.Seek").execute(
JsonRPC("Player.Seek").execute(
{"playerid": playerid,
"value": timing.millis_to_kodi_time(offset)})
@ -421,41 +421,6 @@ def get_item(playerid):
'properties': ['title', 'file']})['result']['item']
def get_current_audio_stream_index(playerid):
"""
Returns the currently active audio stream index [int]
"""
return JsonRPC('Player.GetProperties').execute({
'playerid': playerid,
'properties': ['currentaudiostream']})['result']['currentaudiostream']['index']
def get_current_subtitle_stream_index(playerid):
"""
Returns the currently active subtitle stream index [int] or None if there
are no subs
PICKING UP CHANGES ON SUBTITLES IS CURRENTLY BROKEN ON THE KODI SIDE! The
JSON reply won't change even though subtitles are changed :-(
"""
try:
return JsonRPC('Player.GetProperties').execute({
'playerid': playerid,
'properties': ['currentsubtitle', ]})['result']['currentsubtitle']['index']
except KeyError:
pass
def get_subtitle_enabled(playerid):
"""
Returns True if a subtitle is currently enabled, False otherwise.
PICKING UP CHANGES ON SUBTITLES IS CURRENTLY BROKEN ON THE KODI SIDE! The
JSON reply won't change even though subtitles are changed :-(
"""
return JsonRPC('Player.GetProperties').execute({
'playerid': playerid,
'properties': ['subtitleenabled', ]})['result']['subtitleenabled']
def get_player_props(playerid):
"""
Returns a dict for the active Kodi player with the following values:

View File

@ -8,7 +8,7 @@ from .video import KodiVideoDB
from .music import KodiMusicDB
from .texture import KodiTextureDB
from .. import path_ops, utils, variables as v
from .. import path_ops, utils, timing, variables as v
LOG = getLogger('PLEX.kodi_db')
@ -56,15 +56,35 @@ def setup_kodi_default_entries():
"""
if utils.settings('enableMusic') == 'true':
with KodiMusicDB() as kodidb:
kodidb.setup_kodi_default_entries()
kodidb.cursor.execute('''
INSERT OR REPLACE INTO artist(
idArtist,
strArtist,
strMusicBrainzArtistID)
VALUES (?, ?, ?)
''', (1, '[Missing Tag]', 'Artist Tag Missing'))
kodidb.cursor.execute('''
INSERT OR REPLACE INTO role(
idRole,
strRole)
VALUES (?, ?)
''', (1, 'Artist'))
if v.KODIVERSION >= 18:
kodidb.cursor.execute('DELETE FROM versiontagscan')
kodidb.cursor.execute('''
INSERT INTO versiontagscan(
idVersion,
iNeedsScan,
lastscanned)
VALUES (?, ?, ?)
''', (v.DB_MUSIC_VERSION,
0,
timing.kodi_now()))
def reset_cached_images():
LOG.info('Resetting cached artwork')
LOG.debug('Resetting the Kodi texture DB')
with KodiTextureDB() as kodidb:
kodidb.wipe()
LOG.debug('Deleting all cached image files')
# Remove all existing textures first
path = path_ops.translate_path('special://thumbnails/')
if path_ops.exists(path):
path_ops.rmtree(path, ignore_errors=True)
@ -75,10 +95,13 @@ def reset_cached_images():
new_path = path_ops.translate_path('special://thumbnails/%s' % path)
try:
path_ops.makedirs(path_ops.encode_path(new_path))
except OSError as err:
LOG.warn('Could not create thumbnail directory %s: %s',
new_path, err)
LOG.info('Done resetting cached artwork')
except OSError:
pass
with KodiTextureDB() as kodidb:
for row in kodidb.cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type=?',
('table', )):
if row[0] != 'version':
kodidb.cursor.execute("DELETE FROM %s" % row[0])
def wipe_dbs(music=True):
@ -86,18 +109,28 @@ def wipe_dbs(music=True):
Completely resets the Kodi databases 'video', 'texture' and 'music' (if
music sync is enabled)
We need to connect without sqlite WAL mode as Kodi might still be accessing
the dbs and we need to prevent that
DO NOT use context menu as we need to connect without WAL mode - if Kodi
is still accessing the DB
"""
from sqlite3 import connect
LOG.warn('Wiping Kodi databases!')
LOG.info('Wiping Kodi video database')
with KodiVideoDB() as kodidb:
kodidb.wipe()
kinds = [v.DB_VIDEO_PATH, v.DB_TEXTURE_PATH]
if music:
LOG.info('Wiping Kodi music database')
with KodiMusicDB() as kodidb:
kodidb.wipe()
reset_cached_images()
kinds.append(v.DB_MUSIC_PATH)
for path in kinds:
conn = connect(path, timeout=30.0)
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table'")
tables = cursor.fetchall()
tables = [i[0] for i in tables]
if 'version' in tables:
tables.remove('version')
if 'versiontagscan' in tables:
tables.remove('versiontagscan')
for table in tables:
cursor.execute('DELETE FROM %s' % table)
conn.commit()
conn.close()
setup_kodi_default_entries()
# Delete SQLITE wal files
import xbmc
@ -107,14 +140,6 @@ def wipe_dbs(music=True):
xbmc.executebuiltin('UpdateLibrary(music)')
def create_kodi_db_indicees():
"""
Index the "actors" because we got a TON - speed up SELECT and WHEN
"""
with KodiVideoDB() as kodidb:
kodidb.create_kodi_db_indicees()
KODIDB_FROM_PLEXTYPE = {
v.PLEX_TYPE_MOVIE: KodiVideoDB,
v.PLEX_TYPE_SHOW: KodiVideoDB,

View File

@ -2,20 +2,63 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from threading import Lock
from functools import wraps
from .. import db, path_ops
from .. import utils, path_ops, app
KODIDB_LOCK = Lock()
# Names of tables we generally leave untouched and e.g. don't wipe
UNTOUCHED_TABLES = ('version', 'versiontagscan')
DB_WRITE_ATTEMPTS = 100
class LockedKodiDatabase(Exception):
"""
Dedicated class to make sure we're not silently catching locked DBs.
"""
pass
def catch_operationalerrors(method):
"""
sqlite.OperationalError is raised immediately if another DB connection
is open, reading something that we're trying to change
So let's catch it and try again
Also see https://github.com/mattn/go-sqlite3/issues/274
"""
@wraps(method)
def wrapper(self, *args, **kwargs):
attempts = DB_WRITE_ATTEMPTS
while True:
try:
return method(self, *args, **kwargs)
except utils.OperationalError as err:
if 'database is locked' not in err:
# Not an error we want to catch, so reraise it
raise
attempts -= 1
if attempts == 0:
# Reraise in order to NOT catch nested OperationalErrors
raise LockedKodiDatabase('Kodi database locked')
# Need to close the transactions and begin new ones
self.kodiconn.commit()
if self.artconn:
self.artconn.commit()
if app.APP.monitor.waitForAbort(0.1):
# PKC needs to quit
return
# Start new transactions
self.kodiconn.execute('BEGIN')
if self.artconn:
self.artconn.execute('BEGIN')
return wrapper
class KodiDBBase(object):
"""
Kodi database methods used for all types of items
"""
def __init__(self, texture_db=False, kodiconn=None, artconn=None,
lock=True):
def __init__(self, texture_db=False, kodiconn=None, artconn=None, lock=True):
"""
Allows direct use with a cursor instead of context mgr
"""
@ -29,10 +72,9 @@ class KodiDBBase(object):
def __enter__(self):
if self.lock:
KODIDB_LOCK.acquire()
self.kodiconn = db.connect(self.db_kind)
self.kodiconn = utils.kodi_sql(self.db_kind)
self.cursor = self.kodiconn.cursor()
self.artconn = db.connect('texture') if self._texture_db \
else None
self.artconn = utils.kodi_sql('texture') if self._texture_db else None
self.artcursor = self.artconn.cursor() if self._texture_db else None
return self
@ -68,7 +110,7 @@ class KodiDBBase(object):
for kodi_art, url in artworks.iteritems():
self.add_art(url, kodi_id, kodi_type, kodi_art)
@db.catch_operationalerrors
@catch_operationalerrors
def add_art(self, url, kodi_id, kodi_type, kodi_art):
"""
Adds or modifies the artwork of kind kodi_art (e.g. 'poster') in the
@ -87,7 +129,7 @@ class KodiDBBase(object):
for kodi_art, url in artworks.iteritems():
self.modify_art(url, kodi_id, kodi_type, kodi_art)
@db.catch_operationalerrors
@catch_operationalerrors
def modify_art(self, url, kodi_id, kodi_type, kodi_art):
"""
Adds or modifies the artwork of kind kodi_art (e.g. 'poster') in the
@ -124,7 +166,7 @@ class KodiDBBase(object):
for row in self.cursor.fetchall():
self.delete_cached_artwork(row[0])
@db.catch_operationalerrors
@catch_operationalerrors
def delete_cached_artwork(self, url):
try:
self.artcursor.execute("SELECT cachedurl FROM texture WHERE url = ? LIMIT 1",
@ -140,16 +182,3 @@ class KodiDBBase(object):
if path_ops.exists(path):
path_ops.rmtree(path, ignore_errors=True)
self.artcursor.execute("DELETE FROM texture WHERE url = ?", (url, ))
@db.catch_operationalerrors
def wipe(self):
"""
Completely wipes the corresponding Kodi database
"""
self.cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table'")
tables = [i[0] for i in self.cursor.fetchall()]
for table in UNTOUCHED_TABLES:
if table in tables:
tables.remove(table)
for table in tables:
self.cursor.execute('DELETE FROM %s' % table)

View File

@ -4,7 +4,7 @@ from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from . import common
from .. import db, variables as v, app, timing
from .. import variables as v, app
LOG = getLogger('PLEX.kodi_db.music')
@ -12,7 +12,7 @@ LOG = getLogger('PLEX.kodi_db.music')
class KodiMusicDB(common.KodiDBBase):
db_kind = 'music'
@db.catch_operationalerrors
@common.catch_operationalerrors
def add_path(self, path):
"""
Add the path (unicode) to the music DB, if it does not exist already.
@ -25,43 +25,16 @@ class KodiMusicDB(common.KodiDBBase):
try:
pathid = self.cursor.fetchone()[0]
except TypeError:
self.cursor.execute('INSERT INTO path(strPath, strHash) VALUES (?, ?)',
(path, '123'))
pathid = self.cursor.lastrowid
self.cursor.execute("SELECT COALESCE(MAX(idPath),0) FROM path")
pathid = self.cursor.fetchone()[0] + 1
self.cursor.execute('''
INSERT INTO path(idPath, strPath, strHash)
VALUES (?, ?, ?)
''',
(pathid, path, '123'))
return pathid
@db.catch_operationalerrors
def setup_kodi_default_entries(self):
"""
Makes sure that we retain the Kodi standard databases. E.g. that there
is a dummy artist with ID 1
"""
self.cursor.execute('''
INSERT OR REPLACE INTO artist(
idArtist,
strArtist,
strMusicBrainzArtistID)
VALUES (?, ?, ?)
''', (1, '[Missing Tag]', 'Artist Tag Missing'))
self.cursor.execute('''
INSERT OR REPLACE INTO role(
idRole,
strRole)
VALUES (?, ?)
''', (1, 'Artist'))
if v.KODIVERSION >= 18:
self.cursor.execute('DELETE FROM versiontagscan')
self.cursor.execute('''
INSERT INTO versiontagscan(
idVersion,
iNeedsScan,
lastscanned)
VALUES (?, ?, ?)
''', (v.DB_MUSIC_VERSION,
0,
timing.kodi_now()))
@db.catch_operationalerrors
@common.catch_operationalerrors
def update_path(self, path, kodi_pathid):
self.cursor.execute('''
UPDATE path
@ -89,7 +62,7 @@ class KodiMusicDB(common.KodiDBBase):
return
return song_ids[0][0]
@db.catch_operationalerrors
@common.catch_operationalerrors
def delete_song_from_song_artist(self, song_id):
"""
Deletes son from song_artist table and possibly orphaned roles
@ -106,7 +79,27 @@ class KodiMusicDB(common.KodiDBBase):
self.cursor.execute('DELETE FROM song_artist WHERE idSong = ?',
(song_id, ))
@db.catch_operationalerrors
@common.catch_operationalerrors
def delete_album_from_discography(self, album_id):
"""
Removes the album with id album_id from the table discography
"""
# Need to get the album name as a string first!
self.cursor.execute('SELECT strAlbum, iYear FROM album WHERE idAlbum = ? LIMIT 1',
(album_id, ))
try:
name, year = self.cursor.fetchone()
except TypeError:
return
self.cursor.execute('SELECT idArtist FROM album_artist WHERE idAlbum = ? LIMIT 1',
(album_id, ))
artist = self.cursor.fetchone()
if not artist:
return
self.cursor.execute('DELETE FROM discography WHERE idArtist = ? AND strAlbum = ? AND strYear = ?',
(artist[0], name, year))
@common.catch_operationalerrors
def delete_song_from_song_genre(self, song_id):
"""
Deletes the one entry with id song_id from the song_genre table.
@ -127,7 +120,7 @@ class KodiMusicDB(common.KodiDBBase):
if not self.cursor.fetchone():
self.delete_genre(genre[0])
@db.catch_operationalerrors
@common.catch_operationalerrors
def delete_genre(self, genre_id):
"""
Dedicated method in order to catch OperationalErrors correctly
@ -135,7 +128,7 @@ class KodiMusicDB(common.KodiDBBase):
self.cursor.execute('DELETE FROM genre WHERE idGenre = ?',
(genre_id, ))
@db.catch_operationalerrors
@common.catch_operationalerrors
def delete_album_from_album_genre(self, album_id):
"""
Deletes the one entry with id album_id from the album_genre table.
@ -160,7 +153,7 @@ class KodiMusicDB(common.KodiDBBase):
self.cursor.execute('SELECT COALESCE(MAX(idAlbum), 0) FROM album')
return self.cursor.fetchone()[0] + 1
@db.catch_operationalerrors
@common.catch_operationalerrors
def add_album_17(self, *args):
"""
strReleaseType: 'album' or 'single'
@ -203,7 +196,7 @@ class KodiMusicDB(common.KodiDBBase):
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (args))
@db.catch_operationalerrors
@common.catch_operationalerrors
def update_album_17(self, *args):
if app.SYNC.artwork:
self.cursor.execute('''
@ -241,7 +234,7 @@ class KodiMusicDB(common.KodiDBBase):
WHERE idAlbum = ?
''', (args))
@db.catch_operationalerrors
@common.catch_operationalerrors
def add_album(self, *args):
"""
strReleaseType: 'album' or 'single'
@ -284,7 +277,7 @@ class KodiMusicDB(common.KodiDBBase):
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (args))
@db.catch_operationalerrors
@common.catch_operationalerrors
def update_album(self, *args):
if app.SYNC.artwork:
self.cursor.execute('''
@ -322,7 +315,7 @@ class KodiMusicDB(common.KodiDBBase):
WHERE idAlbum = ?
''', (args))
@db.catch_operationalerrors
@common.catch_operationalerrors
def add_albumartist(self, artist_id, kodi_id, artistname):
self.cursor.execute('''
INSERT OR REPLACE INTO album_artist(
@ -332,7 +325,17 @@ class KodiMusicDB(common.KodiDBBase):
VALUES (?, ?, ?)
''', (artist_id, kodi_id, artistname))
@db.catch_operationalerrors
@common.catch_operationalerrors
def add_discography(self, artist_id, albumname, year):
self.cursor.execute('''
INSERT OR REPLACE INTO discography(
idArtist,
strAlbum,
strYear)
VALUES (?, ?, ?)
''', (artist_id, albumname, year))
@common.catch_operationalerrors
def add_music_genres(self, kodiid, genres, mediatype):
"""
Adds a list of genres (list of unicode) for a certain Kodi item
@ -348,9 +351,10 @@ class KodiMusicDB(common.KodiDBBase):
genreid = self.cursor.fetchone()[0]
except TypeError:
# Create the genre
self.cursor.execute('INSERT INTO genre(strGenre) VALUES(?)',
(genre, ))
genreid = self.cursor.lastrowid
self.cursor.execute('SELECT COALESCE(MAX(idGenre),0) FROM genre')
genreid = self.cursor.fetchone()[0] + 1
self.cursor.execute('INSERT INTO genre(idGenre, strGenre) VALUES(?, ?)',
(genreid, genre))
self.cursor.execute('''
INSERT OR REPLACE INTO album_genre(
idGenre,
@ -368,9 +372,10 @@ class KodiMusicDB(common.KodiDBBase):
genreid = self.cursor.fetchone()[0]
except TypeError:
# Create the genre
self.cursor.execute('INSERT INTO genre(strGenre) VALUES (?)',
(genre, ))
genreid = self.cursor.lastrowid
self.cursor.execute('SELECT COALESCE(MAX(idGenre),0) FROM genre')
genreid = self.cursor.fetchone()[0] + 1
self.cursor.execute('INSERT INTO genre(idGenre, strGenre) values(?, ?)',
(genreid, genre))
self.cursor.execute('''
INSERT OR REPLACE INTO song_genre(
idGenre,
@ -383,7 +388,7 @@ class KodiMusicDB(common.KodiDBBase):
self.cursor.execute('SELECT COALESCE(MAX(idSong),0) FROM song')
return self.cursor.fetchone()[0] + 1
@db.catch_operationalerrors
@common.catch_operationalerrors
def add_song(self, *args):
self.cursor.execute('''
INSERT INTO song(
@ -408,7 +413,7 @@ class KodiMusicDB(common.KodiDBBase):
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (args))
@db.catch_operationalerrors
@common.catch_operationalerrors
def add_song_17(self, *args):
self.cursor.execute('''
INSERT INTO song(
@ -433,7 +438,7 @@ class KodiMusicDB(common.KodiDBBase):
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (args))
@db.catch_operationalerrors
@common.catch_operationalerrors
def update_song(self, *args):
self.cursor.execute('''
UPDATE song
@ -454,7 +459,7 @@ class KodiMusicDB(common.KodiDBBase):
WHERE idSong = ?
''', (args))
@db.catch_operationalerrors
@common.catch_operationalerrors
def set_playcount(self, *args):
self.cursor.execute('''
UPDATE song
@ -463,7 +468,7 @@ class KodiMusicDB(common.KodiDBBase):
WHERE idSong = ?
''', (args))
@db.catch_operationalerrors
@common.catch_operationalerrors
def update_song_17(self, *args):
self.cursor.execute('''
UPDATE song
@ -492,7 +497,7 @@ class KodiMusicDB(common.KodiDBBase):
except TypeError:
pass
@db.catch_operationalerrors
@common.catch_operationalerrors
def add_artist(self, name, musicbrainz):
"""
Adds a single artist's name to the db
@ -514,18 +519,22 @@ class KodiMusicDB(common.KodiDBBase):
except TypeError:
# Krypton has a dummy first entry idArtist: 1 strArtist:
# [Missing Tag] strMusicBrainzArtistID: Artist Tag Missing
self.cursor.execute('SELECT COALESCE(MAX(idArtist),1) FROM artist')
artistid = self.cursor.fetchone()[0] + 1
self.cursor.execute('''
INSERT INTO artist(strArtist, strMusicBrainzArtistID)
VALUES (?, ?)
''', (name, musicbrainz))
artistid = self.cursor.lastrowid
INSERT INTO artist(
idArtist,
strArtist,
strMusicBrainzArtistID)
VALUES (?, ?, ?)
''', (artistid, name, musicbrainz))
else:
if artistname != name:
self.cursor.execute('UPDATE artist SET strArtist = ? WHERE idArtist = ?',
(name, artistid,))
return artistid
@db.catch_operationalerrors
@common.catch_operationalerrors
def update_artist(self, *args):
if app.SYNC.artwork:
self.cursor.execute('''
@ -548,15 +557,15 @@ class KodiMusicDB(common.KodiDBBase):
WHERE idArtist = ?
''', (args))
@db.catch_operationalerrors
@common.catch_operationalerrors
def remove_song(self, kodi_id):
self.cursor.execute('DELETE FROM song WHERE idSong = ?', (kodi_id, ))
@db.catch_operationalerrors
@common.catch_operationalerrors
def remove_path(self, path_id):
self.cursor.execute('DELETE FROM path WHERE idPath = ?', (path_id, ))
@db.catch_operationalerrors
@common.catch_operationalerrors
def add_song_artist(self, artist_id, song_id, artist_name):
self.cursor.execute('''
INSERT OR REPLACE INTO song_artist(
@ -568,7 +577,7 @@ class KodiMusicDB(common.KodiDBBase):
VALUES (?, ?, ?, ?, ?)
''', (artist_id, song_id, 1, 0, artist_name))
@db.catch_operationalerrors
@common.catch_operationalerrors
def add_albuminfosong(self, song_id, album_id, track_no, track_title,
runtime):
"""
@ -584,7 +593,7 @@ class KodiMusicDB(common.KodiDBBase):
VALUES (?, ?, ?, ?, ?)
''', (song_id, album_id, track_no, track_title, runtime))
@db.catch_operationalerrors
@common.catch_operationalerrors
def update_userrating(self, kodi_id, kodi_type, userrating):
"""
Updates userrating for songs and albums
@ -601,7 +610,7 @@ class KodiMusicDB(common.KodiDBBase):
% (kodi_type, column),
(userrating, identifier, kodi_id))
@db.catch_operationalerrors
@common.catch_operationalerrors
def remove_albuminfosong(self, kodi_id):
"""
Kodi 17 only
@ -609,7 +618,7 @@ class KodiMusicDB(common.KodiDBBase):
self.cursor.execute('DELETE FROM albuminfosong WHERE idAlbumInfoSong = ?',
(kodi_id, ))
@db.catch_operationalerrors
@common.catch_operationalerrors
def remove_album(self, kodi_id):
if v.KODIVERSION < 18:
self.cursor.execute('DELETE FROM albuminfosong WHERE idAlbumInfo = ?',
@ -618,7 +627,7 @@ class KodiMusicDB(common.KodiDBBase):
(kodi_id, ))
self.cursor.execute('DELETE FROM album WHERE idAlbum = ?', (kodi_id, ))
@db.catch_operationalerrors
@common.catch_operationalerrors
def remove_artist(self, kodi_id):
self.cursor.execute('DELETE FROM album_artist WHERE idArtist = ?',
(kodi_id, ))
@ -626,3 +635,5 @@ class KodiMusicDB(common.KodiDBBase):
(kodi_id, ))
self.cursor.execute('DELETE FROM song_artist WHERE idArtist = ?',
(kodi_id, ))
self.cursor.execute('DELETE FROM discography WHERE idArtist = ?',
(kodi_id, ))

View File

@ -5,30 +5,18 @@ from logging import getLogger
from sqlite3 import IntegrityError
from . import common
from .. import db, path_ops, timing, variables as v
from .. import path_ops, timing, variables as v
LOG = getLogger('PLEX.kodi_db.video')
MOVIE_PATH = 'plugin://%s.movies/' % v.ADDON_ID
SHOW_PATH = 'plugin://%s.tvshows/' % v.ADDON_ID
MOVIE_PATH = 'http://127.0.0.1:%s/plex/kodi/movies/' % v.WEBSERVICE_PORT
SHOW_PATH = 'http://127.0.0.1:%s/plex/kodi/shows/' % v.WEBSERVICE_PORT
class KodiVideoDB(common.KodiDBBase):
db_kind = 'video'
@db.catch_operationalerrors
def create_kodi_db_indicees(self):
"""
Index the "actors" because we got a TON - speed up SELECT and WHEN
"""
commands = (
'CREATE UNIQUE INDEX IF NOT EXISTS ix_actor_2 ON actor (actor_id);',
'CREATE UNIQUE INDEX IF NOT EXISTS ix_files_2 ON files (idFile);',
)
for cmd in commands:
self.cursor.execute(cmd)
@db.catch_operationalerrors
@common.catch_operationalerrors
def setup_path_table(self):
"""
Use with Kodi video DB
@ -38,24 +26,47 @@ class KodiVideoDB(common.KodiDBBase):
For some reason, Kodi ignores this if done via itemtypes while e.g.
adding or updating items. (addPath method does NOT work)
"""
for path, kind in ((MOVIE_PATH, 'movies'), (SHOW_PATH, 'tvshows')):
path_id = self.get_path(path)
if path_id is None:
query = '''
INSERT INTO path(strPath,
strContent,
strScraper,
noUpdate,
exclude)
VALUES (?, ?, ?, ?, ?)
'''
self.cursor.execute(query, (path,
kind,
'metadata.local',
1,
0))
path_id = self.get_path(MOVIE_PATH)
if path_id is None:
self.cursor.execute("SELECT COALESCE(MAX(idPath),0) FROM path")
path_id = self.cursor.fetchone()[0] + 1
query = '''
INSERT INTO path(idPath,
strPath,
strContent,
strScraper,
noUpdate,
exclude)
VALUES (?, ?, ?, ?, ?, ?)
'''
self.cursor.execute(query, (path_id,
MOVIE_PATH,
'movies',
'metadata.local',
1,
0))
# And TV shows
path_id = self.get_path(SHOW_PATH)
if path_id is None:
self.cursor.execute("SELECT COALESCE(MAX(idPath),0) FROM path")
path_id = self.cursor.fetchone()[0] + 1
query = '''
INSERT INTO path(idPath,
strPath,
strContent,
strScraper,
noUpdate,
exclude)
VALUES (?, ?, ?, ?, ?, ?)
'''
self.cursor.execute(query, (path_id,
SHOW_PATH,
'tvshows',
'metadata.local',
1,
0))
@db.catch_operationalerrors
@common.catch_operationalerrors
def parent_path_id(self, path):
"""
Video DB: Adds all subdirectories to path table while setting a "trail"
@ -66,19 +77,20 @@ class KodiVideoDB(common.KodiDBBase):
path_ops.decode_path(path_ops.path.pardir)))
pathid = self.get_path(parentpath)
if pathid is None:
self.cursor.execute("SELECT COALESCE(MAX(idPath),0) FROM path")
pathid = self.cursor.fetchone()[0] + 1
self.cursor.execute('''
INSERT INTO path(strPath, dateAdded)
VALUES (?, ?)
INSERT INTO path(idPath, strPath, dateAdded)
VALUES (?, ?, ?)
''',
(parentpath, timing.kodi_now()))
pathid = self.cursor.lastrowid
(pathid, parentpath, timing.kodi_now()))
if parentpath != path:
# In case we end up having media in the filesystem root, C:\
parent_id = self.parent_path_id(parentpath)
self.update_parentpath_id(parent_id, pathid)
return pathid
@db.catch_operationalerrors
@common.catch_operationalerrors
def update_parentpath_id(self, parent_id, pathid):
"""
Dedicated method in order to catch OperationalErrors correctly
@ -86,7 +98,7 @@ class KodiVideoDB(common.KodiDBBase):
self.cursor.execute('UPDATE path SET idParentPath = ? WHERE idPath = ?',
(parent_id, pathid))
@db.catch_operationalerrors
@common.catch_operationalerrors
def add_path(self, path, date_added=None, id_parent_path=None,
content=None, scraper=None):
"""
@ -103,19 +115,21 @@ class KodiVideoDB(common.KodiDBBase):
try:
pathid = self.cursor.fetchone()[0]
except TypeError:
self.cursor.execute("SELECT COALESCE(MAX(idPath),0) FROM path")
pathid = self.cursor.fetchone()[0] + 1
self.cursor.execute('''
INSERT INTO path(
idPath,
strPath,
dateAdded,
idParentPath,
strContent,
strScraper,
noUpdate)
VALUES (?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?)
''',
(path, date_added, id_parent_path, content,
scraper, 1))
pathid = self.cursor.lastrowid
(pathid, path, date_added, id_parent_path,
content, scraper, 1))
return pathid
def get_path(self, path):
@ -129,18 +143,24 @@ class KodiVideoDB(common.KodiDBBase):
except TypeError:
pass
@db.catch_operationalerrors
@common.catch_operationalerrors
def add_file(self, filename, path_id, date_added):
"""
Adds the filename [unicode] to the table files if not already added
and returns the idFile.
"""
self.cursor.execute('SELECT COALESCE(MAX(idFile), 0) FROM files')
file_id = self.cursor.fetchone()[0] + 1
self.cursor.execute('''
INSERT INTO files(idPath, strFilename, dateAdded)
VALUES (?, ?, ?)
INSERT INTO files(
idFile,
idPath,
strFilename,
dateAdded)
VALUES (?, ?, ?, ?)
''',
(path_id, filename, date_added))
return self.cursor.lastrowid
(file_id, path_id, filename, date_added))
return file_id
def modify_file(self, filename, path_id, date_added):
self.cursor.execute('SELECT idFile FROM files WHERE idPath = ? AND strFilename = ?',
@ -154,15 +174,17 @@ class KodiVideoDB(common.KodiDBBase):
def obsolete_file_ids(self):
"""
Returns a generator for idFile of all Kodi file ids that do not have a
dateAdded set (dateAdded NULL) and the filename start with
'plugin://plugin.video.plexkodiconnect'
These entries should be deleted as they're created falsely by Kodi.
dateAdded set (dateAdded NULL) and the associated path entry has
a field noUpdate of NULL as well as dateAdded of NULL
"""
return (x[0] for x in self.cursor.execute('''
SELECT idFile FROM files
WHERE dateAdded IS NULL
AND strFilename LIKE \'plugin://plugin.video.plexkodiconnect%\'
'''))
return (x[0] for x in self.cursor.execute("""
SELECT files.idFile
FROM files
LEFT JOIN path ON path.idPath = files.idPath
WHERE files.dateAdded IS NULL
AND path.noUpdate IS NULL
AND path.dateAdded IS NULL
"""))
def show_id_from_path(self, path):
"""
@ -181,7 +203,7 @@ class KodiVideoDB(common.KodiDBBase):
except TypeError:
pass
@db.catch_operationalerrors
@common.catch_operationalerrors
def remove_file(self, file_id, remove_orphans=True):
"""
Removes the entry for file_id from the files table. Will also delete
@ -217,7 +239,7 @@ class KodiVideoDB(common.KodiDBBase):
'''
self.cursor.execute(query, (path_id, MOVIE_PATH, SHOW_PATH))
@db.catch_operationalerrors
@common.catch_operationalerrors
def _modify_link_and_table(self, kodi_id, kodi_type, entries, link_table,
table, key, first_id=None):
first_id = first_id if first_id is not None else 1
@ -229,9 +251,11 @@ class KodiVideoDB(common.KodiDBBase):
try:
entry_id = self.cursor.fetchone()[0]
except TypeError:
self.cursor.execute('INSERT INTO %s(name) VALUES(?)' % table,
(entry, ))
entry_id = self.cursor.lastrowid
self.cursor.execute('SELECT COALESCE(MAX(%s), %s) FROM %s'
% (key, first_id - 1, table))
entry_id = self.cursor.fetchone()[0] + 1
self.cursor.execute('INSERT INTO %s(%s, name) values(?, ?)'
% (table, key), (entry_id, entry))
finally:
entry_ids.append(entry_id)
# Now process the ids obtained from the names
@ -321,19 +345,13 @@ class KodiVideoDB(common.KodiDBBase):
for kind, people_list in people.iteritems():
self._add_people_kind(kodi_id, kodi_type, kind, people_list)
@db.catch_operationalerrors
@common.catch_operationalerrors
def _add_people_kind(self, kodi_id, kodi_type, kind, people_list):
# Save new people to Kodi DB by iterating over the remaining entries
if kind == 'actor':
for person in people_list:
# Make sure the person entry in table actor exists
actor_id, new = self._get_actor_id(person[0],
art_url=person[1])
if not new and person[1]:
# Person might have shown up as a director or writer first
# WITHOUT an art url from the Plex side!
# Check here if we need to set the actor's art url
self._check_actor_art(actor_id, person[1])
actor_id = self._get_actor_id(person[0], art_url=person[1])
# Link the person with the media element
try:
self.cursor.execute('INSERT INTO actor_link VALUES (?, ?, ?, ?, ?)',
@ -345,7 +363,7 @@ class KodiVideoDB(common.KodiDBBase):
else:
for person in people_list:
# Make sure the person entry in table actor exists:
actor_id, _ = self._get_actor_id(person[0])
actor_id = self._get_actor_id(person[0])
# Link the person with the media element
try:
self.cursor.execute('INSERT INTO %s_link VALUES (?, ?, ?)' % kind,
@ -366,7 +384,7 @@ class KodiVideoDB(common.KodiDBBase):
'writer': []}).iteritems():
self._modify_people_kind(kodi_id, kodi_type, kind, people_list)
@db.catch_operationalerrors
@common.catch_operationalerrors
def _modify_people_kind(self, kodi_id, kodi_type, kind, people_list):
# Get the people already saved in the DB for this specific item
if kind == 'actor':
@ -421,22 +439,21 @@ class KodiVideoDB(common.KodiDBBase):
# Save new people to Kodi DB by iterating over the remaining entries
self._add_people_kind(kodi_id, kodi_type, kind, people_list)
@db.catch_operationalerrors
@common.catch_operationalerrors
def _new_actor_id(self, name, art_url):
# Not yet in actor DB, add person
self.cursor.execute('INSERT INTO actor(name) VALUES (?)', (name, ))
actor_id = self.cursor.lastrowid
self.cursor.execute('SELECT COALESCE(MAX(actor_id), 0) FROM actor')
actor_id = self.cursor.fetchone()[0] + 1
self.cursor.execute('INSERT INTO actor(actor_id, name) VALUES (?, ?)',
(actor_id, name))
if art_url:
self.add_art(art_url, actor_id, 'actor', 'thumb')
return actor_id
def _get_actor_id(self, name, art_url=None):
"""
Returns the tuple
(actor_id [int], new_entry [bool])
for name [unicode] in table actor (without ensuring that the name
matches)."new_entry" will be True if a new DB entry has just been
created.
Returns the actor_id [int] for name [unicode] in table actor (without
ensuring that the name matches).
If not, will create a new record with actor_id, name, art_url
Uses Plex ids and thus assumes that Plex person id is unique!
@ -444,21 +461,9 @@ class KodiVideoDB(common.KodiDBBase):
self.cursor.execute('SELECT actor_id FROM actor WHERE name=? LIMIT 1',
(name,))
try:
return (self.cursor.fetchone()[0], False)
return self.cursor.fetchone()[0]
except TypeError:
return (self._new_actor_id(name, art_url), True)
def _check_actor_art(self, actor_id, url):
"""
Sets the actor's art url [unicode] for actor_id [int]
"""
self.cursor.execute('''
SELECT EXISTS(SELECT 1 FROM art
WHERE media_id = ? AND media_type = 'actor'
LIMIT 1)''', (actor_id, ))
if not self.cursor.fetchone()[0]:
# We got a new artwork url for this actor!
self.add_art(url, actor_id, 'actor', 'thumb')
return self._new_actor_id(name, art_url)
def get_art(self, kodi_id, kodi_type):
"""
@ -479,7 +484,7 @@ class KodiVideoDB(common.KodiDBBase):
(kodi_id, kodi_type))
return dict(self.cursor.fetchall())
@db.catch_operationalerrors
@common.catch_operationalerrors
def modify_streams(self, fileid, streamdetails=None, runtime=None):
"""
Leave streamdetails and runtime empty to delete all stream entries for
@ -575,22 +580,6 @@ class KodiVideoDB(common.KodiDBBase):
return
return movie_id, typus
def file_id_from_id(self, kodi_id, kodi_type):
"""
Returns the Kodi file_id for the item with kodi_id and kodi_type or
None
"""
if kodi_type == v.KODI_TYPE_MOVIE:
identifier = 'idMovie'
elif kodi_type == v.KODI_TYPE_EPISODE:
identifier = 'idEpisode'
self.cursor.execute('SELECT idFile FROM %s WHERE %s = ? LIMIT 1'
% (kodi_type, identifier), (kodi_id, ))
try:
return self.cursor.fetchone()[0]
except TypeError:
pass
def get_resume(self, file_id):
"""
Returns the first resume point in seconds (int) if found, else None for
@ -614,7 +603,7 @@ class KodiVideoDB(common.KodiDBBase):
except TypeError:
pass
@db.catch_operationalerrors
@common.catch_operationalerrors
def set_resume(self, file_id, resume_seconds, total_seconds, playcount,
dateplayed):
"""
@ -624,13 +613,16 @@ class KodiVideoDB(common.KodiDBBase):
# Delete existing resume point
self.cursor.execute('DELETE FROM bookmark WHERE idFile = ?', (file_id,))
# Set watched count
# Be careful to set playCount to None, NOT the int zero!
self.cursor.execute('UPDATE files SET playCount = ?, lastPlayed = ? WHERE idFile = ?',
(playcount or None, dateplayed, file_id))
(playcount, dateplayed, file_id))
# Set the resume bookmark
if resume_seconds:
self.cursor.execute(
'SELECT COALESCE(MAX(idBookmark), 0) FROM bookmark')
bookmark_id = self.cursor.fetchone()[0] + 1
self.cursor.execute('''
INSERT INTO bookmark(
idBookmark,
idFile,
timeInSeconds,
totalTimeInSeconds,
@ -638,8 +630,9 @@ class KodiVideoDB(common.KodiDBBase):
player,
playerState,
type)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (file_id,
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (bookmark_id,
file_id,
resume_seconds,
total_seconds,
'',
@ -647,7 +640,7 @@ class KodiVideoDB(common.KodiDBBase):
'',
1))
@db.catch_operationalerrors
@common.catch_operationalerrors
def create_tag(self, name):
"""
Will create a new tag if needed and return the tag_id
@ -657,11 +650,13 @@ class KodiVideoDB(common.KodiDBBase):
try:
tag_id = self.cursor.fetchone()[0]
except TypeError:
self.cursor.execute('INSERT INTO tag(name) VALUES(?)', (name, ))
tag_id = self.cursor.lastrowid
self.cursor.execute("SELECT COALESCE(MAX(tag_id), 0) FROM tag")
tag_id = self.cursor.fetchone()[0] + 1
self.cursor.execute('INSERT INTO tag(tag_id, name) VALUES(?, ?)',
(tag_id, name))
return tag_id
@db.catch_operationalerrors
@common.catch_operationalerrors
def update_tag(self, oldtag, newtag, kodiid, mediatype):
"""
Updates the tag_id by replaying oldtag with newtag
@ -680,7 +675,7 @@ class KodiVideoDB(common.KodiDBBase):
WHERE media_id = ? AND media_type = ? AND tag_id = ?
''', (kodiid, mediatype, oldtag,))
@db.catch_operationalerrors
@common.catch_operationalerrors
def create_collection(self, set_name):
"""
Returns the collection/set id for set_name [unicode]
@ -690,11 +685,13 @@ class KodiVideoDB(common.KodiDBBase):
try:
setid = self.cursor.fetchone()[0]
except TypeError:
self.cursor.execute('INSERT INTO sets(strSet) VALUES(?)', (set_name, ))
setid = self.cursor.lastrowid
self.cursor.execute("SELECT COALESCE(MAX(idSet), 0) FROM sets")
setid = self.cursor.fetchone()[0] + 1
self.cursor.execute('INSERT INTO sets(idSet, strSet) VALUES(?, ?)',
(setid, set_name))
return setid
@db.catch_operationalerrors
@common.catch_operationalerrors
def assign_collection(self, setid, movieid):
"""
Assign the movie to one set/collection
@ -702,7 +699,7 @@ class KodiVideoDB(common.KodiDBBase):
self.cursor.execute('UPDATE movie SET idSet = ? WHERE idMovie = ?',
(setid, movieid,))
@db.catch_operationalerrors
@common.catch_operationalerrors
def remove_from_set(self, movieid):
"""
Remove the movie with movieid [int] from an associated movie set, movie
@ -722,7 +719,7 @@ class KodiVideoDB(common.KodiDBBase):
except TypeError:
pass
@db.catch_operationalerrors
@common.catch_operationalerrors
def delete_possibly_empty_set(self, set_id):
"""
Checks whether there are other movies in the set set_id. If not,
@ -733,37 +730,25 @@ class KodiVideoDB(common.KodiDBBase):
if self.cursor.fetchone() is None:
self.cursor.execute('DELETE FROM sets WHERE idSet = ?', (set_id,))
@db.catch_operationalerrors
def add_season(self, showid, seasonnumber, name, userrating):
@common.catch_operationalerrors
def add_season(self, showid, seasonnumber):
"""
Adds a TV show season to the Kodi video DB or simply returns the ID,
if there already is an entry in the DB
"""
self.cursor.execute("SELECT COALESCE(MAX(idSeason),0) FROM seasons")
seasonid = self.cursor.fetchone()[0] + 1
self.cursor.execute('''
INSERT INTO seasons(
idShow, season, name, userrating)
VALUES (?, ?, ?, ?)
''', (showid, seasonnumber, name, userrating))
return self.cursor.lastrowid
INSERT INTO seasons(idSeason, idShow, season)
VALUES (?, ?, ?)
''', (seasonid, showid, seasonnumber))
return seasonid
@db.catch_operationalerrors
def update_season(self, seasonid, showid, seasonnumber, name, userrating):
"""
Updates a TV show season with a certain seasonid
"""
self.cursor.execute('''
UPDATE seasons
SET idShow = ?,
season = ?,
name = ?,
userrating = ?
WHERE idSeason = ?
''', (showid, seasonnumber, name, userrating, seasonid))
@db.catch_operationalerrors
@common.catch_operationalerrors
def add_uniqueid(self, *args):
"""
Feed with:
uniqueid_id: int
media_id: int
media_type: string
value: string
@ -771,26 +756,41 @@ class KodiVideoDB(common.KodiDBBase):
"""
self.cursor.execute('''
INSERT INTO uniqueid(
uniqueid_id,
media_id,
media_type,
value,
type)
VALUES (?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?)
''', (args))
return self.cursor.lastrowid
@db.catch_operationalerrors
def add_uniqueid_id(self):
self.cursor.execute('SELECT COALESCE(MAX(uniqueid_id), 0) FROM uniqueid')
return self.cursor.fetchone()[0] + 1
def get_uniqueid(self, kodi_id, kodi_type):
"""
Returns the uniqueid_id
"""
self.cursor.execute('SELECT uniqueid_id FROM uniqueid WHERE media_id = ? AND media_type =?',
(kodi_id, kodi_type))
try:
return self.cursor.fetchone()[0]
except TypeError:
return self.add_uniqueid_id()
@common.catch_operationalerrors
def update_uniqueid(self, *args):
"""
Pass in value, media_id, media_type, type
Pass in media_id, media_type, value, type, uniqueid_id
"""
self.cursor.execute('''
INSERT OR REPLACE INTO uniqueid(media_id, media_type, type, value)
VALUES(?, ?, ?, ?)
UPDATE uniqueid
SET media_id = ?, media_type = ?, value = ?, type = ?
WHERE uniqueid_id = ?
''', (args))
return self.cursor.lastrowid
@db.catch_operationalerrors
@common.catch_operationalerrors
def remove_uniqueid(self, kodi_id, kodi_type):
"""
Deletes the entry from the uniqueid table for the item
@ -798,38 +798,56 @@ class KodiVideoDB(common.KodiDBBase):
self.cursor.execute('DELETE FROM uniqueid WHERE media_id = ? AND media_type = ?',
(kodi_id, kodi_type))
@db.catch_operationalerrors
def add_ratingid(self):
self.cursor.execute('SELECT COALESCE(MAX(rating_id),0) FROM rating')
return self.cursor.fetchone()[0] + 1
def get_ratingid(self, kodi_id, kodi_type):
"""
Create if needed and return the unique rating_id from rating table
"""
self.cursor.execute('SELECT rating_id FROM rating WHERE media_id = ? AND media_type = ?',
(kodi_id, kodi_type))
try:
return self.cursor.fetchone()[0]
except TypeError:
return self.add_ratingid()
@common.catch_operationalerrors
def update_ratings(self, *args):
"""
Feed with media_id, media_type, rating_type, rating, votes, rating_id
"""
self.cursor.execute('''
INSERT OR REPLACE INTO
rating(media_id, media_type, rating_type, rating, votes)
VALUES (?, ?, ?, ?, ?)
UPDATE rating
SET media_id = ?,
media_type = ?,
rating_type = ?,
rating = ?,
votes = ?
WHERE rating_id = ?
''', (args))
return self.cursor.lastrowid
@db.catch_operationalerrors
@common.catch_operationalerrors
def add_ratings(self, *args):
"""
feed with:
media_id, media_type, rating_type, rating, votes
rating_id, media_id, media_type, rating_type, rating, votes
rating_type = 'default'
"""
self.cursor.execute('''
INSERT INTO rating(
rating_id,
media_id,
media_type,
rating_type,
rating,
votes)
VALUES (?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?)
''', (args))
return self.cursor.lastrowid
@db.catch_operationalerrors
@common.catch_operationalerrors
def remove_ratings(self, kodi_id, kodi_type):
"""
Removes all ratings from the rating table for the item
@ -845,7 +863,7 @@ class KodiVideoDB(common.KodiDBBase):
self.cursor.execute('SELECT COALESCE(MAX(idEpisode), 0) FROM episode')
return self.cursor.fetchone()[0] + 1
@db.catch_operationalerrors
@common.catch_operationalerrors
def add_episode(self, *args):
self.cursor.execute(
'''
@ -867,14 +885,13 @@ class KodiVideoDB(common.KodiDBBase):
c16,
c18,
c19,
c20,
idSeason,
userrating)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (args))
@db.catch_operationalerrors
@common.catch_operationalerrors
def update_episode(self, *args):
self.cursor.execute(
'''
@ -893,14 +910,13 @@ class KodiVideoDB(common.KodiDBBase):
c16 = ?,
c18 = ?,
c19 = ?,
c20 = ?,
idFile=?,
idSeason = ?,
userrating = ?
WHERE idEpisode = ?
''', (args))
@db.catch_operationalerrors
@common.catch_operationalerrors
def add_show(self, *args):
self.cursor.execute(
'''
@ -919,7 +935,7 @@ class KodiVideoDB(common.KodiDBBase):
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (args))
@db.catch_operationalerrors
@common.catch_operationalerrors
def update_show(self, *args):
self.cursor.execute(
'''
@ -937,21 +953,21 @@ class KodiVideoDB(common.KodiDBBase):
WHERE idShow = ?
''', (args))
@db.catch_operationalerrors
@common.catch_operationalerrors
def add_showlinkpath(self, kodi_id, kodi_pathid):
self.cursor.execute('INSERT INTO tvshowlinkpath(idShow, idPath) VALUES (?, ?)',
(kodi_id, kodi_pathid))
@db.catch_operationalerrors
@common.catch_operationalerrors
def remove_show(self, kodi_id):
self.cursor.execute('DELETE FROM tvshow WHERE idShow = ?', (kodi_id,))
@db.catch_operationalerrors
@common.catch_operationalerrors
def remove_season(self, kodi_id):
self.cursor.execute('DELETE FROM seasons WHERE idSeason = ?',
(kodi_id,))
@db.catch_operationalerrors
@common.catch_operationalerrors
def remove_episode(self, kodi_id):
self.cursor.execute('DELETE FROM episode WHERE idEpisode = ?',
(kodi_id,))
@ -960,7 +976,7 @@ class KodiVideoDB(common.KodiDBBase):
self.cursor.execute('SELECT COALESCE(MAX(idMovie), 0) FROM movie')
return self.cursor.fetchone()[0] + 1
@db.catch_operationalerrors
@common.catch_operationalerrors
def add_movie(self, *args):
self.cursor.execute(
'''
@ -994,11 +1010,11 @@ class KodiVideoDB(common.KodiDBBase):
?, ?, ?, ?)
''', (args))
@db.catch_operationalerrors
@common.catch_operationalerrors
def remove_movie(self, kodi_id):
self.cursor.execute('DELETE FROM movie WHERE idMovie = ?', (kodi_id,))
@db.catch_operationalerrors
@common.catch_operationalerrors
def update_userrating(self, kodi_id, kodi_type, userrating):
"""
Updates userrating

View File

@ -7,36 +7,44 @@ from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from json import loads
import copy
import json
import binascii
import xbmc
import xbmcgui
from .plex_api import API
from .plex_db import PlexDB
from .kodi_db import KodiVideoDB
from . import kodi_db
from .downloadutils import DownloadUtils as DU
from . import utils, timing, plex_functions as PF
from . import json_rpc as js, playqueue as PQ, playlist_func as PL
from . import backgroundthread, app, variables as v
from . import exceptions
from . import utils, timing, plex_functions as PF, json_rpc as js
from . import playqueue as PQ, backgroundthread, app, variables as v
LOG = getLogger('PLEX.kodimonitor')
# "Start from beginning", "Play from beginning"
STRINGS = (utils.lang(12021).encode('utf-8'),
utils.lang(12023).encode('utf-8'))
class MonitorError(Exception):
"""
Exception we raise for all errors associated with xbmc.Monitor
"""
pass
class KodiMonitor(xbmc.Monitor):
"""
PKC implementation of the Kodi Monitor class. Invoke only once.
"""
def __init__(self):
self._already_slept = False
self._switched_to_plex_streams = True
xbmc.Monitor.__init__(self)
# Info to the currently playing item
self.playerid = None
self.playlistid = None
self.playqueue = None
for playerid in app.PLAYSTATE.player_states:
app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
app.PLAYSTATE.old_player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
xbmc.Monitor.__init__(self)
LOG.info("Kodi monitor started.")
def onScanStarted(self, library):
@ -56,6 +64,9 @@ class KodiMonitor(xbmc.Monitor):
Monitor the PKC settings for changes made by the user
"""
LOG.debug('PKC settings change detected')
# Assume that the user changed something so we can try to reconnect
# app.APP.suspend = False
# app.APP.resume_threads(block=False)
def onNotification(self, sender, method, data):
"""
@ -67,30 +78,44 @@ class KodiMonitor(xbmc.Monitor):
if method == "Player.OnPlay":
with app.APP.lock_playqueues:
self.PlayBackStart(data)
elif method == 'Player.OnAVChange':
with app.APP.lock_playqueues:
self._on_av_change(data)
self.on_play(data)
elif method == "Player.OnStop":
with app.APP.lock_playqueues:
_playback_cleanup(ended=data.get('end'))
if data.get('end'):
with app.APP.lock_playqueues:
_playback_cleanup(ended=True)
else:
with app.APP.lock_playqueues:
_playback_cleanup()
elif method == 'Playlist.OnAdd':
if 'item' in data and data['item'].get('type') == v.KODI_TYPE_SHOW:
# Hitting the "browse" button on tv show info dialog
# Hence show the tv show directly
xbmc.executebuiltin("Dialog.Close(all, true)")
js.activate_window('videos',
'videodb://tvshows/titles/%s/' % data['item']['id'])
with app.APP.lock_playqueues:
self._playlist_onadd(data)
self._playlist_onadd(data)
elif method == 'Playlist.OnRemove':
self._playlist_onremove(data)
elif method == 'Playlist.OnClear':
with app.APP.lock_playqueues:
self._playlist_onclear(data)
self._playlist_onclear(data)
elif method == "VideoLibrary.OnUpdate":
with app.APP.lock_playqueues:
_videolibrary_onupdate(data)
# Manually marking as watched/unwatched
playcount = data.get('playcount')
item = data.get('item')
if playcount is None or item is None:
return
try:
kodi_id = item['id']
kodi_type = item['type']
except (KeyError, TypeError):
LOG.info("Item is invalid for playstate update.")
return
# Send notification to the server.
with PlexDB() as plexdb:
db_item = plexdb.item_by_kodi_id(kodi_id, kodi_type)
if not db_item:
LOG.error("Could not find plex_id in plex database for a "
"video library update")
else:
# notify the server
if playcount > 0:
PF.scrobble(db_item['plex_id'], 'watched')
else:
PF.scrobble(db_item['plex_id'], 'unwatched')
elif method == "VideoLibrary.OnRemove":
pass
elif method == "System.OnSleep":
@ -107,22 +132,25 @@ class KodiMonitor(xbmc.Monitor):
elif method == "System.OnQuit":
LOG.info('Kodi OnQuit detected - shutting down')
app.APP.stop_pkc = True
elif method == 'Other.plugin.video.plexkodiconnect_play_action':
self._start_next_episode(data)
def _playlist_onadd(self, data):
"""
Called if an item is added to a Kodi playlist. Example data dict:
{
u'item': {
u'type': u'movie',
u'id': 2},
u'playlistid': 1,
u'position': 0
}
Will NOT be called if playback initiated by Kodi widgets
"""
pass
'''
Called when a new item is added to a Kodi playqueue
'''
if 'item' in data and data['item'].get('type') == v.KODI_TYPE_SHOW:
# Hitting the "browse" button on tv show info dialog
# Hence show the tv show directly
xbmc.executebuiltin("Dialog.Close(all, true)")
js.activate_window('videos',
'videodb://tvshows/titles/%s/' % data['item']['id'])
return
if data['position'] == 0:
self.playlistid = data['playlistid']
if app.PLAYSTATE.playlist_start_pos == data['position']:
LOG.debug('Playlist ready')
app.PLAYSTATE.playlist_ready = True
app.PLAYSTATE.playlist_start_pos = None
def _playlist_onremove(self, data):
"""
@ -134,20 +162,25 @@ class KodiMonitor(xbmc.Monitor):
"""
pass
@staticmethod
def _playlist_onclear(data):
def _playlist_onclear(self, data):
"""
Called if a Kodi playlist is cleared. Example data dict:
{
u'playlistid': 1,
}
Let's NOT use this as Kodi's responses when e.g. playing an entire
folder are NOT threadsafe: Playlist.OnAdd might be added first, then
Playlist.OnClear might be received LATER
"""
playqueue = PQ.PLAYQUEUES[data['playlistid']]
if not playqueue.is_pkc_clear():
playqueue.pkc_edit = True
playqueue.clear(kodi=False)
else:
LOG.debug('Detected PKC clear - ignoring')
if self.playlistid == data['playlistid']:
LOG.debug('Resetting autoplay')
app.PLAYSTATE.autoplay = False
# playqueue = PQ.PLAYQUEUES[data['playlistid']]
# if not playqueue.is_pkc_clear():
# playqueue.pkc_edit = True
# playqueue.clear(kodi=False)
# else:
# LOG.debug('Detected PKC clear - ignoring')
@staticmethod
def _get_ids(kodi_id, kodi_type, path):
@ -168,24 +201,6 @@ class KodiMonitor(xbmc.Monitor):
plex_type = db_item['plex_type']
return plex_id, plex_type
@staticmethod
def _add_remaining_items_to_playlist(playqueue):
"""
Adds all but the very first item of the Kodi playlist to the Plex
playqueue
"""
items = js.playlist_get_items(playqueue.playlistid)
if not items:
LOG.error('Could not retrieve Kodi playlist items')
return
# Remove first item
items.pop(0)
try:
for i, item in enumerate(items):
PL.add_item_to_plex_playqueue(playqueue, i + 1, kodi_item=item)
except exceptions.PlaylistError:
LOG.info('Could not build Plex playlist for: %s', items)
def _json_item(self, playerid):
"""
Uses JSON RPC to get the playing item's info and returns the tuple
@ -208,19 +223,73 @@ class KodiMonitor(xbmc.Monitor):
json_item.get('type'),
json_item.get('file'))
@staticmethod
def _start_next_episode(data):
def _get_playerid(self, data):
"""
Used for the add-on Upnext to start playback of the next episode
Sets self.playerid with an int 0, 1 [or 2] or raises MonitorError
0: usually video
1: usually audio
"""
LOG.info('Upnext: Start playback of the next episode')
play_info = binascii.unhexlify(data[0])
play_info = json.loads(play_info)
app.APP.player.stop()
handle = 'RunPlugin(%s)' % play_info.get('handle')
xbmc.executebuiltin(handle.encode('utf-8'))
try:
self.playerid = data['player']['playerid']
except (TypeError, KeyError):
LOG.info('Aborting playback report - data invalid for updates: %s',
data)
raise MonitorError()
if self.playerid == -1:
# Kodi might return -1 for "last player"
try:
self.playerid = js.get_player_ids()[0]
except IndexError:
LOG.error('Coud not get playerid for data: %s', data)
raise MonitorError()
def PlayBackStart(self, data):
def _check_playing_item(self, data):
"""
Returns a PF.PlaylistItem() for the currently playing item
Raises MonitorError or IndexError if we need to init the PKC playqueue
"""
info = js.get_player_props(self.playerid)
LOG.debug('Current info for player %s: %s', self.playerid, info)
position = info['position'] if info['position'] != -1 else 0
kodi_playlist = js.playlist_get_items(self.playerid)
LOG.debug('Current Kodi playlist: %s', kodi_playlist)
playlistitem = PQ.PlaylistItem(kodi_item=kodi_playlist[position])
if isinstance(self.playqueue.items[0], PQ.PlaylistItemDummy):
# This dummy item will be deleted by webservice soon - it won't
# play
LOG.debug('Dummy item detected')
position = 1
elif playlistitem != self.playqueue.items[position]:
LOG.debug('Different playqueue items: %s vs. %s ',
playlistitem, self.playqueue.items[position])
raise MonitorError()
# Return the PKC playqueue item - contains more info
return self.playqueue.items[position]
def _load_playerstate(self, item):
"""
Pass in a PF.PlaylistItem(). Will then set the currently playing
state with app.PLAYSTATE.player_states[self.playerid]
"""
if self.playqueue.id:
container_key = '/playQueues/%s' % self.playqueue.id
else:
container_key = '/library/metadata/%s' % item.plex_id
status = app.PLAYSTATE.player_states[self.playerid]
# Remember that this player has been active
app.PLAYSTATE.active_players.add(self.playerid)
status.update(js.get_player_props(self.playerid))
status['container_key'] = container_key
status['file'] = item.file
status['kodi_id'] = item.kodi_id
status['kodi_type'] = item.kodi_type
status['plex_id'] = item.plex_id
status['plex_type'] = item.plex_type
status['playmethod'] = item.playmethod
status['playcount'] = item.playcount
LOG.debug('Set player state for player %s: %s', self.playerid, status)
def on_play(self, data):
"""
Called whenever playback is started. Example data:
{
@ -229,87 +298,25 @@ class KodiMonitor(xbmc.Monitor):
}
Unfortunately when using Widgets, Kodi doesn't tell us shit
"""
# Some init
self._already_slept = False
self.playerid = None
# Get the type of media we're playing
try:
playerid = data['player']['playerid']
except (TypeError, KeyError):
LOG.info('Aborting playback report - item invalid for updates %s',
data)
self._get_playerid(data)
except MonitorError:
return
kodi_id = data['item'].get('id') if 'item' in data else None
kodi_type = data['item'].get('type') if 'item' in data else None
path = data['item'].get('file') if 'item' in data else None
if playerid == -1:
# Kodi might return -1 for "last player"
# Getting the playerid is really a PITA
try:
playerid = js.get_player_ids()[0]
except IndexError:
# E.g. Kodi 18 doesn't tell us anything useful
if kodi_type in v.KODI_VIDEOTYPES:
playlist_type = v.KODI_TYPE_VIDEO_PLAYLIST
elif kodi_type in v.KODI_AUDIOTYPES:
playlist_type = v.KODI_TYPE_AUDIO_PLAYLIST
else:
LOG.error('Unexpected type %s, data %s', kodi_type, data)
return
playerid = js.get_playlist_id(playlist_type)
if not playerid:
LOG.error('Coud not get playerid for data %s', data)
return
playqueue = PQ.PLAYQUEUES[playerid]
info = js.get_player_props(playerid)
if playqueue.kodi_playlist_playback:
# Kodi will tell us the wrong position - of the playlist, not the
# playqueue, when user starts playing from a playlist :-(
pos = 0
LOG.debug('Detected playback from a Kodi playlist')
else:
pos = info['position'] if info['position'] != -1 else 0
LOG.debug('Detected position %s for %s', pos, playqueue)
status = app.PLAYSTATE.player_states[playerid]
self.playqueue = PQ.PLAYQUEUES[self.playerid]
LOG.debug('Current PKC playqueue: %s', self.playqueue)
item = None
try:
item = playqueue.items[pos]
LOG.debug('PKC playqueue item is: %s', item)
except IndexError:
# PKC playqueue not yet initialized
LOG.debug('Position %s not in PKC playqueue yet', pos)
initialize = True
else:
if not kodi_id:
kodi_id, kodi_type, path = self._json_item(playerid)
if kodi_id and item.kodi_id:
if item.kodi_id != kodi_id or item.kodi_type != kodi_type:
LOG.debug('Detected different Kodi id')
initialize = True
else:
initialize = False
else:
# E.g. clips set-up previously with no Kodi DB entry
if not path:
kodi_id, kodi_type, path = self._json_item(playerid)
if path == '':
LOG.debug('Detected empty path: aborting playback report')
return
if item.file != path:
# Clips will get a new path
LOG.debug('Detected different path')
try:
tmp_plex_id = int(utils.REGEX_PLEX_ID.findall(path)[0])
except (IndexError, TypeError):
LOG.debug('No Plex id in path, need to init playqueue')
initialize = True
else:
if tmp_plex_id == item.plex_id:
LOG.debug('Detected different path for the same id')
initialize = False
else:
LOG.debug('Different Plex id, need to init playqueue')
initialize = True
else:
initialize = False
if initialize:
item = self._check_playing_item(data)
except (MonitorError, IndexError):
LOG.debug('Detected that we need to initialize the PKC playqueue')
if not item:
# Initialize the PKC playqueue
# Yet TODO
LOG.debug('Need to initialize Plex and PKC playqueue')
if not kodi_id or not kodi_type:
kodi_id, kodi_type, path = self._json_item(playerid)
@ -318,12 +325,10 @@ class KodiMonitor(xbmc.Monitor):
LOG.debug('No Plex id obtained - aborting playback report')
app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
return
try:
item = PL.init_plex_playqueue(playqueue, plex_id=plex_id)
except exceptions.PlaylistError:
LOG.info('Could not initialize the Plex playlist')
return
item.file = path
playlistitem = PQ.PlaylistItem(plex_id=plex_id,
grab_xml=True)
playlistitem.file = path
self.playqueue.init(playlistitem)
# Set the Plex container key (e.g. using the Plex playqueue)
container_key = None
if info['playlistid'] != -1:
@ -333,71 +338,7 @@ class KodiMonitor(xbmc.Monitor):
container_key = '/playQueues/%s' % container_key
elif plex_id is not None:
container_key = '/library/metadata/%s' % plex_id
else:
LOG.debug('No need to initialize playqueues')
kodi_id = item.kodi_id
kodi_type = item.kodi_type
plex_id = item.plex_id
plex_type = item.plex_type
if playqueue.id:
container_key = '/playQueues/%s' % playqueue.id
else:
container_key = '/library/metadata/%s' % plex_id
# Mechanik for Plex skip intro feature
if utils.settings('enableSkipIntro') == 'true':
status['intro_markers'] = item.api.intro_markers()
# Remember the currently playing item
app.PLAYSTATE.item = item
# Remember that this player has been active
app.PLAYSTATE.active_players.add(playerid)
status.update(info)
LOG.debug('Set the Plex container_key to: %s', container_key)
status['container_key'] = container_key
status['file'] = path
status['kodi_id'] = kodi_id
status['kodi_type'] = kodi_type
status['plex_id'] = plex_id
status['plex_type'] = plex_type
status['playmethod'] = item.playmethod
status['playcount'] = item.playcount
status['external_player'] = app.APP.player.isExternalPlayer() == 1
LOG.debug('Set the player state: %s', status)
# Workaround for the Kodi add-on Up Next
if not app.SYNC.direct_paths:
_notify_upnext(item)
self._switched_to_plex_streams = False
def _on_av_change(self, data):
"""
Will be called when Kodi has a video, audio or subtitle stream. Also
happens when the stream changes.
Example data as returned by Kodi:
{'item': {'id': 5, 'type': 'movie'},
'player': {'playerid': 1, 'speed': 1}}
PICKING UP CHANGES ON SUBTITLES IS CURRENTLY BROKEN ON THE KODI SIDE!
Kodi subs will never change. Also see json_rpc.py
"""
playerid = data['player']['playerid']
if not playerid == v.KODI_VIDEO_PLAYER_ID:
# We're just messing with Kodi's videoplayer
return
item = app.PLAYSTATE.item
if item is None:
# Player might've quit
return
if not self._switched_to_plex_streams:
# We need to switch to the Plex streams ONCE upon playback start
# after onavchange has been fired
if utils.settings('audioStreamPick') == '0':
item.switch_to_plex_stream('audio')
if utils.settings('subtitleStreamPick') == '0':
item.switch_to_plex_stream('subtitle')
self._switched_to_plex_streams = True
else:
item.on_av_change(playerid)
self._load_playerstate(item)
def _playback_cleanup(ended=False):
@ -408,24 +349,22 @@ def _playback_cleanup(ended=False):
"""
LOG.debug('playback_cleanup called. Active players: %s',
app.PLAYSTATE.active_players)
if app.APP.skip_intro_dialog:
app.APP.skip_intro_dialog.close()
app.APP.skip_intro_dialog = None
# We might have saved a transient token from a user flinging media via
# Companion (if we could not use the playqueue to store the token)
app.CONN.plex_transient_token = None
LOG.debug('Playstate is: %s', app.PLAYSTATE.player_states)
for playerid in app.PLAYSTATE.active_players:
status = app.PLAYSTATE.player_states[playerid]
# Remember the last played item later
app.PLAYSTATE.old_player_states[playerid] = copy.deepcopy(status)
# Stop transcoding
if status['playmethod'] == v.PLAYBACK_METHOD_TRANSCODE:
if status['playmethod'] == 'Transcode':
LOG.debug('Tell the PMS to stop transcoding')
DU().downloadUrl(
'{server}/video/:/transcode/universal/stop',
parameters={'session': v.PKC_MACHINE_IDENTIFIER})
if playerid == 1:
# Bookmarks might not be pickup up correctly, so let's do them
if status['plex_type'] in v.PLEX_VIDEOTYPES:
# Bookmarks are not be pickup up correctly, so let's do them
# manually. Applies to addon paths, but direct paths might have
# started playback via PMS
_record_playstate(status, ended)
@ -433,9 +372,7 @@ def _playback_cleanup(ended=False):
app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
# As all playback has halted, reset the players that have been active
app.PLAYSTATE.active_players = set()
app.PLAYSTATE.item = None
utils.delete_temporary_subtitles()
LOG.debug('Finished PKC playback cleanup')
LOG.info('Finished PKC playback cleanup')
def _record_playstate(status, ended):
@ -452,28 +389,17 @@ def _record_playstate(status, ended):
LOG.debug('No playstate update due to Plex id not found: %s', status)
return
totaltime = float(timing.kodi_time_to_millis(status['totaltime'])) / 1000
if status['external_player']:
# video has either been entirely watched - or not.
# "ended" won't work, need a workaround
ended = _external_player_correct_plex_watch_count(db_item)
if ended:
progress = 0.99
time = v.IGNORE_SECONDS_AT_START + 1
else:
progress = 0.0
time = 0.0
if ended:
progress = 0.99
time = v.IGNORE_SECONDS_AT_START + 1
else:
if ended:
progress = 0.99
time = v.IGNORE_SECONDS_AT_START + 1
else:
time = float(timing.kodi_time_to_millis(status['time'])) / 1000
try:
progress = time / totaltime
except ZeroDivisionError:
progress = 0.0
LOG.debug('Playback progress %s (%s of %s seconds)',
progress, time, totaltime)
time = float(timing.kodi_time_to_millis(status['time'])) / 1000
try:
progress = time / totaltime
except ZeroDivisionError:
progress = 0.0
LOG.debug('Playback progress %s (%s of %s seconds)',
progress, time, totaltime)
playcount = status['playcount']
last_played = timing.kodi_now()
if playcount is None:
@ -506,30 +432,23 @@ def _record_playstate(status, ended):
totaltime,
playcount,
last_played)
# We might need to reconsider cleaning the file/path table in the future
# _clean_file_table()
# Update the current view to show e.g. an up-to-date progress bar and use
# the latest resume point info
if xbmc.getCondVisibility('Container.Content(musicvideos)'):
# Prevent cursor from moving
xbmc.executebuiltin('Container.Refresh')
else:
# Update widgets
xbmc.executebuiltin('UpdateLibrary(video)')
if xbmc.getCondVisibility('Window.IsMedia'):
xbmc.executebuiltin('Container.Refresh')
# Hack to force "in progress" widget to appear if it wasn't visible before
if (app.APP.force_reload_skin and
xbmc.getCondVisibility('Window.IsVisible(Home.xml)')):
LOG.debug('Refreshing skin to update widgets')
xbmc.executebuiltin('ReloadSkin()')
task = backgroundthread.FunctionAsTask(_clean_file_table, None)
backgroundthread.BGThreader.addTasksToFront([task])
def _external_player_correct_plex_watch_count(db_item):
"""
Kodi won't safe playstate at all for external players
There's currently no way to get a resumpoint if an external player is
in use We are just checking whether we should mark video as
completely watched or completely unwatched (according to
playcountminimumtime set in playercorefactory.xml)
See https://kodi.wiki/view/External_players
"""
with kodi_db.KodiVideoDB() as kodidb:
playcount = kodidb.get_playcount(db_item['kodi_fileid'])
LOG.debug('External player detected. Playcount: %s', playcount)
PF.scrobble(db_item['plex_id'], 'watched' if playcount else 'unwatched')
return True if playcount else False
def _clean_file_table():
@ -540,146 +459,47 @@ def _clean_file_table():
This function tries for at most 5 seconds to clean the file table.
"""
LOG.debug('Start cleaning Kodi files table')
if app.APP.monitor.waitForAbort(2):
# PKC should exit
return
# app.APP.monitor.waitForAbort(1)
try:
with kodi_db.KodiVideoDB() as kodidb:
obsolete_file_ids = list(kodidb.obsolete_file_ids())
for file_id in obsolete_file_ids:
LOG.debug('Removing obsolete Kodi file_id %s', file_id)
kodidb.remove_file(file_id, remove_orphans=False)
file_ids = list(kodidb.obsolete_file_ids())
LOG.debug('Obsolete kodi file_ids: %s', file_ids)
for file_id in file_ids:
kodidb.remove_file(file_id)
except utils.OperationalError:
LOG.debug('Database was locked, unable to clean file table')
else:
LOG.debug('Done cleaning up Kodi file table')
def _next_episode(current_api):
class ContextMonitor(backgroundthread.KillableThread):
"""
Returns the xml for the next episode after the current one
Returns None if something went wrong or there is no next episode
"""
xml = PF.show_episodes(current_api.grandparent_id())
if xml is None:
return
for counter, episode in enumerate(xml):
api = API(episode)
if api.plex_id == current_api.plex_id:
break
else:
LOG.error('Did not find the episode with Plex id %s for show %s: %s',
current_api.plex_id, current_api.grandparent_id(),
current_api.grandparent_title())
return
try:
return API(xml[counter + 1])
except IndexError:
# Was the last episode
pass
Detect the resume dialog for widgets. Could also be used to detect
external players (see Emby implementation)
def _complete_artwork_keys(info):
Let's not register this thread because it won't quit due to
xbmc.getCondVisibility
It should still exit at some point due to xbmc.abortRequested
"""
Make sure that the minimum set of keys is present in the info dict
"""
for key in ('tvshow.poster',
'tvshow.fanart',
'tvshow.landscape',
'tvshow.clearart',
'tvshow.clearlogo',
'thumb'):
if key not in info['art']:
info['art'][key] = ''
def run(self):
LOG.info("----===## Starting ContextMonitor ##===----")
# app.APP.register_thread(self)
try:
self._run()
finally:
# app.APP.deregister_thread(self)
LOG.info("##===---- ContextMonitor Stopped ----===##")
def _notify_upnext(item):
"""
Signals to the Kodi add-on Upnext that there is another episode after this
one.
Needed for add-on paths in order to prevent crashes when Upnext does this
by itself
"""
if not item.plex_type == v.PLEX_TYPE_EPISODE:
return
this_api = item.api
next_api = _next_episode(this_api)
if next_api is None:
return
info = {}
for key, api in (('current_episode', this_api),
('next_episode', next_api)):
info[key] = {
'episodeid': api.plex_id,
'tvshowid': api.grandparent_id(),
'title': api.title(),
'showtitle': api.grandparent_title(),
'plot': api.plot(),
'playcount': api.viewcount(),
'season': api.season_number(),
'episode': api.index(),
'firstaired': api.year(),
'rating': api.rating(),
'art': api.artwork(kodi_id=api.kodi_id,
kodi_type=api.kodi_type,
full_artwork=True)
}
_complete_artwork_keys(info[key])
info['play_info'] = {'handle': next_api.fullpath(force_addon=True)[0]}
sender = v.ADDON_ID.encode('utf-8')
method = 'upnext_data'.encode('utf-8')
data = binascii.hexlify(json.dumps(info))
data = '\\"[\\"{0}\\"]\\"'.format(data)
xbmc.executebuiltin('NotifyAll(%s, %s, %s)' % (sender, method, data))
def _videolibrary_onupdate(data):
"""
A specific Kodi library item has been updated. This seems to happen if the
user marks an item as watched/unwatched or if playback of the item just
stopped
2 kinds of messages possible, e.g.
Method: VideoLibrary.OnUpdate Data: ("Reset resume position" and also
fired just after stopping playback - BEFORE OnStop fires)
{'id': 1, 'type': 'movie'}
Method: VideoLibrary.OnUpdate Data: ("Mark as watched")
{'item': {'id': 1, 'type': 'movie'}, 'playcount': 1}
"""
item = data.get('item') if 'item' in data else data
try:
kodi_id = item['id']
kodi_type = item['type']
except (KeyError, TypeError):
LOG.debug("Item is invalid for a Plex playstate update")
return
playcount = data.get('playcount')
if playcount is None:
# "Reset resume position"
# Kodi might set as watched or unwatched!
with KodiVideoDB(lock=False) as kodidb:
file_id = kodidb.file_id_from_id(kodi_id, kodi_type)
if file_id is None:
return
if kodidb.get_resume(file_id):
# We do have an existing bookmark entry - not toggling to
# either watched or unwatched on the Plex side
return
playcount = kodidb.get_playcount(file_id) or 0
if app.PLAYSTATE.item and kodi_id == app.PLAYSTATE.item.kodi_id and \
kodi_type == app.PLAYSTATE.item.kodi_type:
# Kodi updates an item immediately after playback. Hence we do NOT
# increase or decrease the viewcount
return
# Send notification to the server.
with PlexDB(lock=False) as plexdb:
db_item = plexdb.item_by_kodi_id(kodi_id, kodi_type)
if not db_item:
LOG.error("Could not find plex_id in plex database for a "
"video library update")
return
# notify the server
if playcount > 0:
PF.scrobble(db_item['plex_id'], 'watched')
else:
PF.scrobble(db_item['plex_id'], 'unwatched')
def _run(self):
while not self.isCanceled():
# The following function will block if called while PKC should
# exit!
if xbmc.getCondVisibility('Window.IsVisible(DialogContextMenu.xml)'):
if xbmc.getInfoLabel('Control.GetLabel(1002)') in STRINGS:
# Remember that the item IS indeed resumable
control = int(xbmcgui.Window(10106).getFocusId())
app.PLAYSTATE.resume_playback = True if control == 1001 else False
else:
# Different context menu is displayed
app.PLAYSTATE.resume_playback = None
xbmc.sleep(100)

View File

@ -2,6 +2,7 @@
from __future__ import absolute_import, division, unicode_literals
from .full_sync import start
from .time import sync_pms_time
from .websocket import store_websocket_message, process_websocket_messages, \
WEBSOCKET_MESSAGES, PLAYSTATE_SESSIONS
from .common import update_kodi_library, PLAYLIST_SYNC_ENABLED

View File

@ -1,41 +1,40 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
import xbmc
from .. import utils, app, variables as v
LOG = getLogger('PLEX.sync')
from .. import utils, variables as v
PLAYLIST_SYNC_ENABLED = (v.DEVICE != 'Microsoft UWP' and
utils.settings('enablePlaylistSync') == 'true')
class LibrarySyncMixin(object):
def suspend(self, block=False, timeout=None):
"""
Let's NOT suspend sync threads but immediately terminate them
"""
self.cancel()
class fullsync_mixin(object):
def __init__(self):
self._canceled = False
def wait_while_suspended(self):
"""
Return immediately
"""
return self.should_cancel()
def abort(self):
"""Hit method to terminate the thread"""
self._canceled = True
# Let's NOT suspend sync threads but immediately terminate them
suspend = abort
def run(self):
app.APP.register_thread(self)
LOG.debug('##===--- Starting %s ---===##', self.__class__.__name__)
try:
self._run()
except Exception as err:
LOG.error('Exception encountered: %s', err)
utils.ERROR(notify=True)
finally:
app.APP.deregister_thread(self)
LOG.debug('##===--- %s Stopped ---===##', self.__class__.__name__)
@property
def suspend_reached(self):
"""Since we're not suspending, we'll never set it to True"""
return False
@suspend_reached.setter
def suspend_reached(self):
pass
def resume(self):
"""Obsolete since we're not suspending"""
pass
def isCanceled(self):
"""Check whether we should exit this thread"""
return self._canceled
def update_kodi_library(video=True, music=True):
@ -43,12 +42,7 @@ def update_kodi_library(video=True, music=True):
Updates the Kodi library and thus refreshes the Kodi views and widgets
"""
if video:
if not xbmc.getCondVisibility('Window.IsMedia'):
xbmc.executebuiltin('UpdateLibrary(video)')
else:
# Prevent cursor from moving - refresh later
xbmc.executebuiltin('Container.Refresh')
app.APP.update_widgets = True
xbmc.executebuiltin('UpdateLibrary(video)')
if music:
xbmc.executebuiltin('UpdateLibrary(music)')

View File

@ -27,51 +27,48 @@ class FanartThread(backgroundthread.KillableThread):
self.refresh = refresh
super(FanartThread, self).__init__()
def should_suspend(self):
def isSuspended(self):
return self._suspended or app.APP.is_playing_video
def run(self):
LOG.info('Starting FanartThread')
app.APP.register_fanart_thread(self)
try:
self._run()
self._run_internal()
except Exception:
utils.ERROR(notify=True)
finally:
app.APP.deregister_fanart_thread(self)
def _loop(self):
for typus in SUPPORTED_TYPES:
offset = 0
while True:
with PlexDB() as plexdb:
# Keep DB connection open only for a short period of time!
if self.refresh:
batch = list(plexdb.every_plex_id(typus,
offset,
BATCH_SIZE))
else:
batch = list(plexdb.missing_fanart(typus,
offset,
BATCH_SIZE))
for plex_id in batch:
# Do the actual, time-consuming processing
if self.should_suspend() or self.should_cancel():
return False
process_fanart(plex_id, typus, self.refresh)
if len(batch) < BATCH_SIZE:
break
offset += BATCH_SIZE
return True
def _run(self):
def _run_internal(self):
finished = False
while not finished:
finished = self._loop()
if self.wait_while_suspended():
break
LOG.info('FanartThread finished: %s', finished)
self.callback(finished)
try:
for typus in SUPPORTED_TYPES:
offset = 0
while True:
with PlexDB() as plexdb:
# Keep DB connection open only for a short period of time!
if self.refresh:
batch = list(plexdb.every_plex_id(typus,
offset,
BATCH_SIZE))
else:
batch = list(plexdb.missing_fanart(typus,
offset,
BATCH_SIZE))
for plex_id in batch:
# Do the actual, time-consuming processing
if self.wait_while_suspended():
return
process_fanart(plex_id, typus, self.refresh)
if len(batch) < BATCH_SIZE:
break
offset += BATCH_SIZE
else:
finished = True
finally:
LOG.info('FanartThread finished: %s', finished)
self.callback(finished)
class FanartTask(backgroundthread.Task):
@ -132,7 +129,7 @@ def process_fanart(plex_id, plex_type, refresh=False):
db_item['kodi_type'])
# Additional fanart for sets/collections
if plex_type == v.PLEX_TYPE_MOVIE:
for _, setname in api.collections():
for _, setname in api.collection_list():
LOG.debug('Getting artwork for movie set %s', setname)
with KodiVideoDB() as kodidb:
setid = kodidb.create_collection(setname)

View File

@ -1,80 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from Queue import Full
from . import common, sections
from ..plex_db import PlexDB
from .. import backgroundthread
LOG = getLogger('PLEX.sync.fill_metadata_queue')
QUEUE_TIMEOUT = 60 # seconds
class FillMetadataQueue(common.LibrarySyncMixin,
backgroundthread.KillableThread):
"""
Determines which plex_ids we need to sync and puts these ids in a separate
queue. Will use a COPIED plex.db file (plex-copy.db) in order to read much
faster without the writing thread stalling
"""
def __init__(self, repair, section_queue, get_metadata_queue,
processing_queue):
self.repair = repair
self.section_queue = section_queue
self.get_metadata_queue = get_metadata_queue
self.processing_queue = processing_queue
super(FillMetadataQueue, self).__init__()
def _process_section(self, section):
# Initialize only once to avoid loosing the last value before we're
# breaking the for loop
LOG.debug('Process section %s with %s items',
section, section.number_of_items)
count = 0
do_process_section = False
with PlexDB(lock=False, copy=True) as plexdb:
for xml in section.iterator:
if self.should_cancel():
break
plex_id = int(xml.get('ratingKey'))
checksum = int('{}{}'.format(
plex_id,
abs(int(xml.get('updatedAt',
xml.get('addedAt', '1541572987'))))))
if (not self.repair and
plexdb.checksum(plex_id, section.plex_type) == checksum):
continue
if not do_process_section:
do_process_section = True
self.processing_queue.add_section(section)
LOG.debug('Put section in processing queue: %s', section)
try:
self.get_metadata_queue.put((count, plex_id, section),
timeout=QUEUE_TIMEOUT)
except Full:
LOG.error('Putting %s in get_metadata_queue timed out - '
'aborting sync now', plex_id)
section.sync_successful = False
break
else:
count += 1
# We might have received LESS items from the PMS than anticipated.
# Ensures that our queues finish
self.processing_queue.change_section_number_of_items(section,
count)
LOG.debug('%s items to process for section %s',
section.number_of_items, section)
def _run(self):
while not self.should_cancel():
section = self.section_queue.get()
self.section_queue.task_done()
if section is None:
break
self._process_section(section)
# Signal the download metadata threads to stop with a sentinel
self.get_metadata_queue.put(None)
# Sentinel for the process_thread once we added everything else
self.processing_queue.add_sentinel(sections.Section())

View File

@ -3,271 +3,360 @@
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
import Queue
import copy
import xbmcgui
from .get_metadata import GetMetadataThread
from .fill_metadata_queue import FillMetadataQueue
from .process_metadata import ProcessMetadataThread
from .get_metadata import GetMetadataTask, reset_collections
from . import common, sections
from .. import utils, timing, backgroundthread as bg, variables as v, app
from .. import plex_functions as PF, itemtypes, path_ops
from .. import utils, timing, backgroundthread, variables as v, app
from .. import plex_functions as PF, itemtypes
from ..plex_db import PlexDB
if common.PLAYLIST_SYNC_ENABLED:
from .. import playlists
LOG = getLogger('PLEX.sync.full_sync')
DELETION_BATCH_SIZE = 250
PLAYSTATE_BATCH_SIZE = 5000
# Max. number of plex_ids held in memory for later processing
BACKLOG_QUEUE_SIZE = 10000
# Max number of xmls held in memory
XML_QUEUE_SIZE = 500
# How many items will be put through the processing chain at once?
BATCH_SIZE = 500
# Safety margin to filter PMS items - how many seconds to look into the past?
UPDATED_AT_SAFETY = 60 * 5
LAST_VIEWED_AT_SAFETY = 60 * 5
class FullSync(common.LibrarySyncMixin, bg.KillableThread):
class InitNewSection(sections.Section):
"""
Throw this into the queue used for ProcessMetadata to tell it which
Plex library section we're looking at
"""
def __init__(self, total_number_of_items, section):
super(InitNewSection, self).__init__()
# Copy all section attributes to this instance
self.__dict__.update(section.__dict__)
self.total = total_number_of_items
class FullSync(common.fullsync_mixin):
def __init__(self, repair, callback, show_dialog):
"""
repair=True: force sync EVERY item
"""
self.successful = True
self.repair = repair
self.callback = callback
self.queue = None
self.process_thread = None
self.current_sync = None
self.plexdb = None
self.plex_type = None
self.section_type = None
self.worker_count = int(utils.settings('syncThreadNumber'))
self.item_count = 0
# For progress dialog
self.show_dialog = show_dialog
self.show_dialog_userdata = utils.settings('playstate_sync_indicator') == 'true'
if self.show_dialog:
self.dialog = xbmcgui.DialogProgressBG()
self.dialog.create(utils.lang(39714))
else:
self.dialog = None
self.current_time = timing.plex_now()
self.last_section = sections.Section()
self.dialog = None
self.total = 0
self.current = 0
self.processed = 0
self.title = ''
self.section = None
self.section_name = None
self.section_type_text = None
self.context = None
self.get_children = None
self.successful = None
self.section_success = None
self.install_sync_done = utils.settings('SyncInstallRunDone') == 'true'
self.threader = backgroundthread.ThreaderManager(
worker=backgroundthread.NonstoppingBackgroundWorker,
worker_count=self.worker_count)
super(FullSync, self).__init__()
def update_progressbar(self, section, title, current):
if not self.dialog:
def update_progressbar(self):
if self.dialog:
try:
progress = int(float(self.current) / float(self.total) * 100.0)
except ZeroDivisionError:
progress = 0
self.dialog.update(progress,
'%s (%s)' % (self.section_name, self.section_type_text),
'%s %s/%s'
% (self.title, self.current, self.total))
if app.APP.is_playing_video:
self.dialog.close()
self.dialog = None
def process_item(self, xml_item):
"""
Processes a single library item
"""
plex_id = int(xml_item.get('ratingKey'))
if not self.repair and self.plexdb.checksum(plex_id, self.plex_type) == \
int('%s%s' % (plex_id,
xml_item.get('updatedAt',
xml_item.get('addedAt', 1541572987)))):
return
current += 1
try:
progress = int(float(current) / float(section.number_of_items) * 100.0)
except ZeroDivisionError:
progress = 0
self.dialog.update(progress,
'%s (%s)' % (section.name, section.section_type_text),
'%s %s/%s'
% (title, current, section.number_of_items))
if app.APP.is_playing_video:
self.dialog.close()
self.dialog = None
self.threader.addTask(GetMetadataTask(self.queue,
plex_id,
self.plex_type,
self.get_children))
self.item_count += 1
@staticmethod
def copy_plex_db():
"""
Takes the current plex.db file and copies it to plex-copy.db
This will allow us to have "concurrent" connections during adding/
updating items, increasing sync speed tremendously.
Using the same DB with e.g. WAL mode did not really work out...
"""
path_ops.copyfile(v.DB_PLEX_PATH, v.DB_PLEX_COPY_PATH)
@utils.log_time
def process_new_and_changed_items(self, section_queue, processing_queue):
LOG.debug('Start working')
get_metadata_queue = Queue.Queue(maxsize=BACKLOG_QUEUE_SIZE)
scanner_thread = FillMetadataQueue(self.repair,
section_queue,
get_metadata_queue,
processing_queue)
scanner_thread.start()
metadata_threads = [
GetMetadataThread(get_metadata_queue, processing_queue)
for _ in range(int(utils.settings('syncThreadNumber')))
]
for t in metadata_threads:
t.start()
process_thread = ProcessMetadataThread(self.current_time,
processing_queue,
self.update_progressbar)
process_thread.start()
LOG.debug('Waiting for scanner thread to finish up')
scanner_thread.join()
LOG.debug('Waiting for metadata download threads to finish up')
for t in metadata_threads:
t.join()
LOG.debug('Download metadata threads finished')
process_thread.join()
self.successful = process_thread.successful
LOG.debug('threads finished work. successful: %s', self.successful)
@utils.log_time
def processing_loop_playstates(self, section_queue):
while not self.should_cancel():
section = section_queue.get()
section_queue.task_done()
if section is None:
def update_library(self):
LOG.debug('Writing changes to Kodi library now')
i = 0
if not self.section:
self.section = self.queue.get()
self.queue.task_done()
while not self.isCanceled() and self.item_count > 0:
section = self.section
if not section:
break
self.playstate_per_section(section)
def playstate_per_section(self, section):
LOG.debug('Processing %s playstates for library section %s',
section.number_of_items, section)
try:
with section.context(self.current_time) as context:
for xml in section.iterator:
section.count += 1
if not context.update_userdata(xml, section.plex_type):
# Somehow did not sync this item yet
context.add_update(xml,
section_name=section.name,
section_id=section.section_id)
context.plexdb.update_last_sync(int(xml.attrib['ratingKey']),
section.plex_type,
self.current_time)
self.update_progressbar(section, '', section.count - 1)
if section.count % PLAYSTATE_BATCH_SIZE == 0:
LOG.debug('Start or continue processing section %s (%ss)',
section.name, section.plex_type)
self.processed = 0
self.total = section.total
self.section_name = section.name
self.section_type_text = utils.lang(
v.TRANSLATION_FROM_PLEXTYPE[section.plex_type])
with section.context(self.current_sync) as context:
while not self.isCanceled() and self.item_count > 0:
try:
item = self.queue.get(block=False)
except backgroundthread.Queue.Empty:
if self.threader.threader.working():
app.APP.monitor.waitForAbort(0.02)
continue
else:
# Try again, in case a thread just finished
i += 1
if i == 3:
break
continue
i = 0
self.queue.task_done()
if isinstance(item, dict):
context.add_update(item['xml'][0],
section=section,
children=item['children'])
self.title = item['xml'][0].get('title')
self.processed += 1
elif isinstance(item, InitNewSection) or item is None:
self.section = item
break
else:
raise ValueError('Unknown type %s' % type(item))
self.item_count -= 1
self.current += 1
self.update_progressbar()
if self.processed == 500:
self.processed = 0
context.commit()
LOG.debug('Done writing changes to Kodi library')
@utils.log_time
def addupdate_section(self, section):
LOG.debug('Processing library section for new or changed items %s',
section)
if not self.install_sync_done:
app.SYNC.path_verified = False
try:
# Sync new, updated and deleted items
iterator = section.iterator
# Tell the processing thread about this new section
queue_info = InitNewSection(iterator.total, section)
self.queue.put(queue_info)
last = True
# To keep track of the item-number in order to kill while loops
self.item_count = 0
self.current = 0
# Initialize only once to avoid loosing the last value before
# we're breaking the for loop
loop = common.tag_last(iterator)
while True:
# Check Plex DB to see what we need to add/update
with PlexDB() as self.plexdb:
for last, xml_item in loop:
if self.isCanceled():
return False
self.process_item(xml_item)
if self.item_count == BATCH_SIZE:
break
# Make sure Plex DB above is closed before adding/updating
if self.item_count == BATCH_SIZE:
self.update_library()
if last:
break
self.update_library()
reset_collections()
return True
except RuntimeError:
LOG.error('Could not entirely process section %s', section)
self.successful = False
return False
def threaded_get_generators(self, kinds, section_queue, items):
@utils.log_time
def playstate_per_section(self, section):
LOG.debug('Processing %s playstates for library section %s',
section.iterator.total, section)
try:
# Sync new, updated and deleted items
iterator = section.iterator
# Tell the processing thread about this new section
queue_info = InitNewSection(iterator.total, section)
self.queue.put(queue_info)
self.total = iterator.total
self.section_name = section.name
self.section_type_text = utils.lang(
v.TRANSLATION_FROM_PLEXTYPE[section.plex_type])
self.current = 0
last = True
loop = common.tag_last(iterator)
while True:
with section.context(self.current_sync) as itemtype:
for i, (last, xml_item) in enumerate(loop):
if self.isCanceled():
return False
if not itemtype.update_userdata(xml_item, section.plex_type):
# Somehow did not sync this item yet
itemtype.add_update(xml_item,
section=section)
itemtype.plexdb.update_last_sync(int(xml_item.attrib['ratingKey']),
section.plex_type,
self.current_sync)
self.current += 1
self.update_progressbar()
if (i + 1) % (10 * BATCH_SIZE) == 0:
break
if last:
break
return True
except RuntimeError:
LOG.error('Could not entirely process section %s', section)
return False
def threaded_get_iterators(self, kinds, queue, all_items=False):
"""
Getting iterators is costly, so let's do it in a dedicated thread
PF.SectionItems is costly, so let's do it asynchronous
"""
LOG.debug('Start threaded_get_generators')
try:
for kind in kinds:
for section in (x for x in app.SYNC.sections
for section in (x for x in sections.SECTIONS
if x.section_type == kind[1]):
if self.should_cancel():
if self.isCanceled():
LOG.debug('Need to exit now')
return
if not section.sync_to_kodi:
LOG.info('User chose to not sync section %s', section)
continue
section = sections.get_sync_section(section,
plex_type=kind[0])
timestamp = section.last_sync - UPDATED_AT_SAFETY \
if section.last_sync else None
if items == 'all':
element = copy.deepcopy(section)
element.plex_type = kind[0]
element.section_type = element.plex_type
element.context = kind[2]
element.get_children = kind[3]
if self.repair or all_items:
updated_at = None
last_viewed_at = None
elif items == 'watched':
if not timestamp:
# No need to sync playstate updates since section
# has not yet been synched
continue
else:
updated_at = None
last_viewed_at = timestamp
elif items == 'updated':
updated_at = timestamp
last_viewed_at = None
try:
section.iterator = PF.get_section_iterator(
section.section_id,
plex_type=section.plex_type,
updated_at=updated_at,
last_viewed_at=last_viewed_at)
except RuntimeError:
LOG.error('Sync at least partially unsuccessful!')
LOG.error('Error getting section iterator %s', section)
else:
section.number_of_items = section.iterator.total
if section.number_of_items > 0:
section_queue.put(section)
LOG.debug('Put section in queue with %s items: %s',
section.number_of_items, section)
updated_at = section.last_sync - UPDATED_AT_SAFETY \
if section.last_sync else None
try:
element.iterator = PF.SectionItems(section.id,
plex_type=element.plex_type,
updated_at=updated_at,
last_viewed_at=None)
except RuntimeError:
LOG.warn('Sync at least partially unsuccessful')
self.successful = False
self.section_success = False
else:
queue.put(element)
except Exception:
utils.ERROR(notify=True)
finally:
# Sentinel for the section queue
section_queue.put(None)
LOG.debug('Exiting threaded_get_generators')
queue.put(None)
def full_library_sync(self):
section_queue = Queue.Queue()
processing_queue = bg.ProcessingQueue(maxsize=XML_QUEUE_SIZE)
"""
"""
kinds = [
(v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_MOVIE),
(v.PLEX_TYPE_SHOW, v.PLEX_TYPE_SHOW),
(v.PLEX_TYPE_SEASON, v.PLEX_TYPE_SHOW),
(v.PLEX_TYPE_EPISODE, v.PLEX_TYPE_SHOW)
(v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_MOVIE, itemtypes.Movie, False),
(v.PLEX_TYPE_SHOW, v.PLEX_TYPE_SHOW, itemtypes.Show, False),
(v.PLEX_TYPE_SEASON, v.PLEX_TYPE_SHOW, itemtypes.Season, False),
(v.PLEX_TYPE_EPISODE, v.PLEX_TYPE_SHOW, itemtypes.Episode, False)
]
if app.SYNC.enable_music:
kinds.extend([
(v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_ARTIST),
(v.PLEX_TYPE_ALBUM, v.PLEX_TYPE_ARTIST),
(v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_ARTIST, itemtypes.Artist, False),
(v.PLEX_TYPE_ALBUM, v.PLEX_TYPE_ARTIST, itemtypes.Album, True),
])
# ADD NEW ITEMS
# We need to enforce syncing e.g. show before season before episode
bg.FunctionAsTask(self.threaded_get_generators,
None,
kinds,
section_queue,
items='all' if self.repair else 'updated').start()
# Do the heavy lifting
self.process_new_and_changed_items(section_queue, processing_queue)
# Already start setting up the iterators. We need to enforce
# syncing e.g. show before season before episode
iterator_queue = Queue.Queue()
task = backgroundthread.FunctionAsTask(self.threaded_get_iterators,
None,
kinds,
iterator_queue)
backgroundthread.BGThreader.addTask(task)
while True:
self.section_success = True
section = iterator_queue.get()
iterator_queue.task_done()
if section is None:
break
# Setup our variables
self.plex_type = section.plex_type
self.section_type = section.section_type
self.context = section.context
self.get_children = section.get_children
# Now do the heavy lifting
if self.isCanceled() or not self.addupdate_section(section):
return False
if self.section_success:
# Need to check because a thread might have missed to get
# some items from the PMS
with PlexDB() as plexdb:
# Set the new time mark for the next delta sync
plexdb.update_section_last_sync(section.id,
self.current_sync)
common.update_kodi_library(video=True, music=True)
if self.should_cancel() or not self.successful:
return
# In order to not delete all your songs again for playstate synch
# In order to not delete all your songs again
if app.SYNC.enable_music:
kinds.extend([
(v.PLEX_TYPE_SONG, v.PLEX_TYPE_ARTIST),
(v.PLEX_TYPE_SONG, v.PLEX_TYPE_ARTIST, itemtypes.Song, True),
])
# Update playstate progress since last sync - especially useful for
# users of very large libraries since this step is very fast
# These playstates will be synched twice
LOG.debug('Start synching playstate for last watched items')
bg.FunctionAsTask(self.threaded_get_generators,
None,
kinds,
section_queue,
items='watched').start()
self.processing_loop_playstates(section_queue)
if self.should_cancel() or not self.successful:
return
# Sync Plex playlists to Kodi and vice-versa
if common.PLAYLIST_SYNC_ENABLED:
LOG.debug('Start playlist sync')
if self.show_dialog:
if self.dialog:
self.dialog.close()
self.dialog = xbmcgui.DialogProgressBG()
# "Synching playlists"
self.dialog.create(utils.lang(39715))
if not playlists.full_sync() or self.should_cancel():
return
# SYNC PLAYSTATE of ALL items (otherwise we won't pick up on items that
# were set to unwatched or changed user ratings). Also mark all items on
# the PMS to be able to delete the ones still in Kodi
LOG.debug('Start synching playstate and userdata for every item')
# were set to unwatched). Also mark all items on the PMS to be able
# to delete the ones still in Kodi
LOG.info('Start synching playstate and userdata for every item')
# Make sure we're not showing an item's title in the sync dialog
self.title = ''
self.threader.shutdown()
self.threader = None
if not self.show_dialog_userdata and self.dialog:
# Close the progress indicator dialog
self.dialog.close()
self.dialog = None
bg.FunctionAsTask(self.threaded_get_generators,
None,
kinds,
section_queue,
items='all').start()
self.processing_loop_playstates(section_queue)
if self.should_cancel() or not self.successful:
return
task = backgroundthread.FunctionAsTask(self.threaded_get_iterators,
None,
kinds,
iterator_queue,
all_items=True)
backgroundthread.BGThreader.addTask(task)
while True:
section = iterator_queue.get()
iterator_queue.task_done()
if section is None:
break
# Setup our variables
self.plex_type = section.plex_type
self.section_type = section.section_type
self.context = section.context
self.get_children = section.get_children
# Now do the heavy lifting
if self.isCanceled() or not self.playstate_per_section(section):
return False
# Delete movies that are not on Plex anymore
LOG.debug('Looking for items to delete')
@ -286,40 +375,65 @@ class FullSync(common.LibrarySyncMixin, bg.KillableThread):
for plex_type, context in kinds:
# Delete movies that are not on Plex anymore
while True:
with context(self.current_time) as ctx:
plex_ids = list(
ctx.plexdb.plex_id_by_last_sync(plex_type,
self.current_time,
DELETION_BATCH_SIZE))
with context(self.current_sync) as ctx:
plex_ids = list(ctx.plexdb.plex_id_by_last_sync(plex_type,
self.current_sync,
BATCH_SIZE))
for plex_id in plex_ids:
if self.should_cancel():
return
if self.isCanceled():
return False
ctx.remove(plex_id, plex_type)
if len(plex_ids) < DELETION_BATCH_SIZE:
if len(plex_ids) < BATCH_SIZE:
break
LOG.debug('Done looking for items to delete')
LOG.debug('Done deleting')
return True
def run(self):
app.APP.register_thread(self)
try:
self._run()
finally:
app.APP.deregister_thread(self)
LOG.info('Done full_sync')
@utils.log_time
def _run(self):
self.current_sync = timing.plex_now()
# Get latest Plex libraries and build playlist and video node files
if self.isCanceled() or not sections.sync_from_pms(self):
return
self.successful = True
try:
# Get latest Plex libraries and build playlist and video node files
if self.should_cancel() or not sections.sync_from_pms(self):
self.queue = backgroundthread.Queue.Queue()
if self.show_dialog:
self.dialog = xbmcgui.DialogProgressBG()
self.dialog.create(utils.lang(39714))
# Actual syncing - do only new items first
LOG.info('Running full_library_sync with repair=%s',
self.repair)
if self.isCanceled() or not self.full_library_sync():
self.successful = False
return
if common.PLAYLIST_SYNC_ENABLED and not playlists.full_sync():
self.successful = False
return
self.copy_plex_db()
self.full_library_sync()
finally:
common.update_kodi_library(video=True, music=True)
if self.dialog:
self.dialog.close()
if not self.successful and not self.should_cancel():
if self.threader:
self.threader.shutdown()
self.threader = None
if not self.successful and not self.isCanceled():
# "ERROR in library sync"
utils.dialog('notification',
heading='{plex}',
message=utils.lang(39410),
icon='{error}')
self.callback(self.successful)
if self.callback:
self.callback(self.successful)
def start(show_dialog, repair=False, callback=None):
# Call run() and NOT start in order to not spawn another thread
FullSync(repair, callback, show_dialog).run()

View File

@ -4,43 +4,64 @@ from logging import getLogger
from . import common
from ..plex_api import API
from .. import backgroundthread, plex_functions as PF, utils, variables as v
from .. import plex_functions as PF, backgroundthread, utils, variables as v
LOG = getLogger("PLEX." + __name__)
LOG = getLogger('PLEX.sync.get_metadata')
LOCK = backgroundthread.threading.Lock()
# List of tuples: (collection index [as in an item's metadata with "Collection
# id"], collection plex id)
COLLECTION_MATCH = None
# Dict with entries of the form <collection index>: <collection xml>
COLLECTION_XMLS = {}
class GetMetadataThread(common.LibrarySyncMixin,
backgroundthread.KillableThread):
def reset_collections():
"""
Collections seem unique to Plex sections
"""
global LOCK, COLLECTION_MATCH, COLLECTION_XMLS
with LOCK:
COLLECTION_MATCH = None
COLLECTION_XMLS = {}
class GetMetadataTask(common.fullsync_mixin, backgroundthread.Task):
"""
Threaded download of Plex XML metadata for a certain library item.
Fills the queue with the downloaded etree XML objects
Input:
queue Queue.Queue() object where this thread will store
the downloaded metadata XMLs as etree objects
"""
def __init__(self, get_metadata_queue, processing_queue):
self.get_metadata_queue = get_metadata_queue
self.processing_queue = processing_queue
super(GetMetadataThread, self).__init__()
def __init__(self, queue, plex_id, plex_type, get_children=False):
self.queue = queue
self.plex_id = plex_id
self.plex_type = plex_type
self.get_children = get_children
super(GetMetadataTask, self).__init__()
def _collections(self, item):
global COLLECTION_MATCH, COLLECTION_XMLS
api = API(item['xml'][0])
collection_match = item['section'].collection_match
collection_xmls = item['section'].collection_xmls
if collection_match is None:
collection_match = PF.collections(api.library_section_id())
if collection_match is None:
if COLLECTION_MATCH is None:
COLLECTION_MATCH = PF.collections(api.library_section_id())
if COLLECTION_MATCH is None:
LOG.error('Could not download collections')
return
# Extract what we need to know
collection_match = \
COLLECTION_MATCH = \
[(utils.cast(int, x.get('index')),
utils.cast(int, x.get('ratingKey'))) for x in collection_match]
utils.cast(int, x.get('ratingKey'))) for x in COLLECTION_MATCH]
item['children'] = {}
for plex_set_id, set_name in api.collections():
if self.should_cancel():
for plex_set_id, set_name in api.collection_list():
if self.isCanceled():
return
if plex_set_id not in collection_xmls:
if plex_set_id not in COLLECTION_XMLS:
# Get Plex metadata for collections - a pain
for index, collection_plex_id in collection_match:
for index, collection_plex_id in COLLECTION_MATCH:
if index == plex_set_id:
collection_xml = PF.GetPlexMetadata(collection_plex_id)
try:
@ -49,75 +70,54 @@ class GetMetadataThread(common.LibrarySyncMixin,
LOG.error('Could not get collection %s %s',
collection_plex_id, set_name)
continue
collection_xmls[plex_set_id] = collection_xml
COLLECTION_XMLS[plex_set_id] = collection_xml
break
else:
LOG.error('Did not find Plex collection %s %s',
plex_set_id, set_name)
continue
item['children'][plex_set_id] = collection_xmls[plex_set_id]
item['children'][plex_set_id] = COLLECTION_XMLS[plex_set_id]
def _process_abort(self, count, section):
# Make sure other threads will also receive sentinel
self.get_metadata_queue.put(None)
if count is not None:
self._process_skipped_item(count, section)
def _process_skipped_item(self, count, section):
section.sync_successful = False
# Add a "dummy" item so we're not skipping a beat
self.processing_queue.put((count, {'section': section, 'xml': None}))
def _run(self):
while True:
item = self.get_metadata_queue.get()
def run(self):
"""
Do the work
"""
if self.isCanceled():
return
# Download Metadata
item = {
'xml': PF.GetPlexMetadata(self.plex_id),
'children': None
}
if item['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", self.plex_id)
return
elif item['xml'] == 401:
LOG.error('HTTP 401 returned by PMS. Too much strain? '
'Cancelling sync for now')
utils.window('plex_scancrashed', value='401')
return
if not self.isCanceled() and self.plex_type == v.PLEX_TYPE_MOVIE:
# Check for collections/sets
collections = False
for child in item['xml'][0]:
if child.tag == 'Collection':
collections = True
break
if collections:
global LOCK
with LOCK:
self._collections(item)
if not self.isCanceled() and self.get_children:
children_xml = PF.GetAllPlexChildren(self.plex_id)
try:
if item is None or self.should_cancel():
self._process_abort(item[0] if item else None,
item[2] if item else None)
break
count, plex_id, section = item
item = {
'xml': PF.GetPlexMetadata(plex_id), # This will block
'children': None,
'section': section
}
if item['xml'] is None:
# Did not receive a valid XML - skip that item for now
LOG.error("Could not get metadata for %s. Skipping item "
"for now", plex_id)
self._process_skipped_item(count, section)
continue
elif item['xml'] == 401:
LOG.error('HTTP 401 returned by PMS. Too much strain? '
'Cancelling sync for now')
utils.window('plex_scancrashed', value='401')
self._process_abort(count, section)
break
if section.plex_type == v.PLEX_TYPE_MOVIE:
# Check for collections/sets
collections = False
for child in item['xml'][0]:
if child.tag == 'Collection':
collections = True
break
if collections:
with LOCK:
self._collections(item)
if section.get_children:
if self.should_cancel():
self._process_abort(count, section)
break
children_xml = PF.GetAllPlexChildren(plex_id) # Will block
try:
children_xml[0].attrib
except (TypeError, IndexError, AttributeError):
LOG.error('Could not get children for Plex id %s',
plex_id)
self._process_skipped_item(count, section)
continue
else:
item['children'] = children_xml
self.processing_queue.put((count, item))
finally:
self.get_metadata_queue.task_done()
children_xml[0].attrib
except (TypeError, IndexError, AttributeError):
LOG.error('Could not get children for Plex id %s',
self.plex_id)
else:
item['children'] = children_xml
if not self.isCanceled():
self.queue.put(item)

View File

@ -2,7 +2,6 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
import urllib
import copy
from ..utils import etree
from .. import variables as v, utils
@ -19,37 +18,35 @@ RECOMMENDED_SCORE_LOWER_BOUND = 7
# )
NODE_TYPES = {
v.PLEX_TYPE_MOVIE: (
('plex_ondeck',
('ondeck',
utils.lang(39500), # "On Deck"
{
'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/onDeck',
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}'
},
v.CONTENT_TYPE_MOVIE,
'movies',
True),
('ondeck',
utils.lang(39502), # "PKC On Deck (faster)"
{},
v.CONTENT_TYPE_MOVIE,
False),
('recent',
utils.lang(30174), # "Recently Added"
{
'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/recentlyAdded',
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}'
},
v.CONTENT_TYPE_MOVIE,
'movies',
False),
('all',
'{self.name}', # We're using this section's name
{
'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/all',
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}'
},
v.CONTENT_TYPE_MOVIE,
'movies',
False),
('recommended',
utils.lang(30230), # "Recommended"
@ -57,27 +54,30 @@ NODE_TYPES = {
'mode': 'browseplex',
'key': ('/library/sections/{self.section_id}&%s'
% urllib.urlencode({'sort': 'rating:desc'})),
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}'
},
v.CONTENT_TYPE_MOVIE,
'movies',
False),
('genres',
utils.lang(135), # "Genres"
{
'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/genre',
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}'
},
v.CONTENT_TYPE_MOVIE,
'movies',
False),
('sets',
utils.lang(39501), # "Collections"
{
'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/collection',
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}'
},
v.CONTENT_TYPE_MOVIE,
'movies',
False),
('random',
utils.lang(30227), # "Random"
@ -85,18 +85,20 @@ NODE_TYPES = {
'mode': 'browseplex',
'key': ('/library/sections/{self.section_id}&%s'
% urllib.urlencode({'sort': 'random'})),
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}'
},
v.CONTENT_TYPE_MOVIE,
'movies',
False),
('lastplayed',
utils.lang(568), # "Last played"
{
'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/recentlyViewed',
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}'
},
v.CONTENT_TYPE_MOVIE,
'movies',
False),
('browse',
utils.lang(39702), # "Browse by folder"
@ -104,20 +106,19 @@ NODE_TYPES = {
'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/folder',
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}',
'folder': True
'section_id': '{self.section_id}'
},
v.CONTENT_TYPE_MOVIE,
'movies',
True),
('more',
utils.lang(22082), # "More..."
{
'mode': 'browseplex',
'key': '/library/sections/{self.section_id}',
'section_id': '{self.section_id}',
'folder': True
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}'
},
v.CONTENT_TYPE_FILE,
'movies',
True),
),
###########################################################
@ -127,27 +128,30 @@ NODE_TYPES = {
{
'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/onDeck',
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}'
},
v.CONTENT_TYPE_EPISODE,
'episodes',
True),
('recent',
utils.lang(30174), # "Recently Added"
{
'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/recentlyAdded',
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}'
},
v.CONTENT_TYPE_EPISODE,
'episodes',
False),
('all',
'{self.name}', # We're using this section's name
{
'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/all',
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}'
},
v.CONTENT_TYPE_SHOW,
'tvshows',
False),
('recommended',
utils.lang(30230), # "Recommended"
@ -155,27 +159,30 @@ NODE_TYPES = {
'mode': 'browseplex',
'key': ('/library/sections/{self.section_id}&%s'
% urllib.urlencode({'sort': 'rating:desc'})),
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}'
},
v.CONTENT_TYPE_SHOW,
'tvshows',
False),
('genres',
utils.lang(135), # "Genres"
{
'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/genre',
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}'
},
v.CONTENT_TYPE_SHOW,
'tvshows',
False),
('sets',
utils.lang(39501), # "Collections"
{
'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/collection',
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}'
},
v.CONTENT_TYPE_SHOW,
'tvshows',
True), # There are no sets/collections for shows with Kodi
('random',
utils.lang(30227), # "Random"
@ -183,9 +190,10 @@ NODE_TYPES = {
'mode': 'browseplex',
'key': ('/library/sections/{self.section_id}&%s'
% urllib.urlencode({'sort': 'random'})),
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}'
},
v.CONTENT_TYPE_SHOW,
'tvshows',
False),
('lastplayed',
utils.lang(568), # "Last played"
@ -193,29 +201,30 @@ NODE_TYPES = {
'mode': 'browseplex',
'key': ('/library/sections/{self.section_id}/recentlyViewed&%s'
% urllib.urlencode({'type': v.PLEX_TYPE_NUMBER_FROM_PLEX_TYPE[v.PLEX_TYPE_EPISODE]})),
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}'
},
v.CONTENT_TYPE_EPISODE,
'episodes',
False),
('browse',
utils.lang(39702), # "Browse by folder"
{
'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/folder',
'section_id': '{self.section_id}',
'folder': True
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}'
},
v.CONTENT_TYPE_EPISODE,
'episodes',
True),
('more',
utils.lang(22082), # "More..."
{
'mode': 'browseplex',
'key': '/library/sections/{self.section_id}',
'section_id': '{self.section_id}',
'folder': True
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}'
},
v.CONTENT_TYPE_FILE,
'episodes',
True),
),
}
@ -225,19 +234,9 @@ def node_pms(section, node_name, args):
"""
Nodes where the logic resides with the PMS - we're NOT building an
xml that filters and sorts, but point to PKC add-on path
Be sure to set args['folder'] = True if the listing is a folder and does
not contain playable elements like movies, episodes or tracks
"""
if 'folder' in args:
args = copy.deepcopy(args)
args.pop('folder')
folder = True
else:
folder = False
xml = etree.Element('node',
attrib={'order': unicode(section.order),
'type': 'folder' if folder else 'filter'})
xml = etree.Element('node', attrib={'order': unicode(section.order),
'type': 'folder'})
etree.SubElement(xml, 'label').text = node_name
etree.SubElement(xml, 'icon').text = ICON_PATH
etree.SubElement(xml, 'content').text = section.content
@ -245,29 +244,6 @@ def node_pms(section, node_name, args):
return xml
def node_ondeck(section, node_name):
"""
For movies only - returns in-progress movies sorted by last played
"""
xml = etree.Element('node', attrib={'order': unicode(section.order),
'type': 'filter'})
etree.SubElement(xml, 'match').text = 'all'
rule = etree.SubElement(xml, 'rule', attrib={'field': 'tag',
'operator': 'is'})
etree.SubElement(rule, 'value').text = section.name
etree.SubElement(xml, 'rule', attrib={'field': 'inprogress',
'operator': 'true'})
etree.SubElement(xml, 'label').text = node_name
etree.SubElement(xml, 'icon').text = ICON_PATH
etree.SubElement(xml, 'content').text = section.content
etree.SubElement(xml, 'limit').text = utils.settings('widgetLimit')
etree.SubElement(xml,
'order',
attrib={'direction':
'descending'}).text = 'lastplayed'
return xml
def node_recent(section, node_name):
xml = etree.Element('node',
attrib={'order': unicode(section.order),
@ -290,7 +266,6 @@ def node_recent(section, node_name):
etree.SubElement(xml, 'label').text = node_name
etree.SubElement(xml, 'icon').text = ICON_PATH
etree.SubElement(xml, 'content').text = section.content
etree.SubElement(xml, 'limit').text = utils.settings('widgetLimit')
etree.SubElement(xml,
'order',
attrib={'direction':
@ -328,7 +303,6 @@ def node_recommended(section, node_name):
etree.SubElement(xml, 'label').text = node_name
etree.SubElement(xml, 'icon').text = ICON_PATH
etree.SubElement(xml, 'content').text = section.content
etree.SubElement(xml, 'limit').text = utils.settings('widgetLimit')
etree.SubElement(xml,
'order',
attrib={'direction':
@ -383,7 +357,6 @@ def node_random(section, node_name):
etree.SubElement(xml, 'label').text = node_name
etree.SubElement(xml, 'icon').text = ICON_PATH
etree.SubElement(xml, 'content').text = section.content
etree.SubElement(xml, 'limit').text = utils.settings('widgetLimit')
etree.SubElement(xml,
'order',
attrib={'direction':
@ -404,7 +377,6 @@ def node_lastplayed(section, node_name):
etree.SubElement(xml, 'label').text = node_name
etree.SubElement(xml, 'icon').text = ICON_PATH
etree.SubElement(xml, 'content').text = section.content
etree.SubElement(xml, 'limit').text = utils.settings('widgetLimit')
etree.SubElement(xml,
'order',
attrib={'direction':

View File

@ -1,92 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from . import common, sections
from ..plex_db import PlexDB
from .. import backgroundthread, app
LOG = getLogger('PLEX.sync.process_metadata')
COMMIT_TO_DB_EVERY_X_ITEMS = 500
class ProcessMetadataThread(common.LibrarySyncMixin,
backgroundthread.KillableThread):
"""
Invoke once in order to process the received PMS metadata xmls
"""
def __init__(self, current_time, processing_queue, update_progressbar):
self.current_time = current_time
self.processing_queue = processing_queue
self.update_progressbar = update_progressbar
self.last_section = sections.Section()
self.successful = True
super(ProcessMetadataThread, self).__init__()
def start_section(self, section):
if section != self.last_section:
if self.last_section:
self.finish_last_section()
LOG.debug('Start or continue processing section %s', section)
self.last_section = section
# Warn the user for this new section if we cannot access a file
app.SYNC.path_verified = False
else:
LOG.debug('Resume processing section %s', section)
def finish_last_section(self):
if (not self.should_cancel() and self.last_section and
self.last_section.sync_successful):
# Check for should_cancel() because we cannot be sure that we
# processed every item of the section
with PlexDB() as plexdb:
# Set the new time mark for the next delta sync
plexdb.update_section_last_sync(self.last_section.section_id,
self.current_time)
LOG.info('Finished processing section successfully: %s',
self.last_section)
elif self.last_section and not self.last_section.sync_successful:
LOG.warn('Sync not successful for section %s', self.last_section)
self.successful = False
def _get(self):
item = {'xml': None}
while item and item['xml'] is None:
item = self.processing_queue.get()
self.processing_queue.task_done()
return item
def _run(self):
# There are 2 sentinels: None for aborting/ending this thread, the dict
# {'section': section, 'xml': None} for skipped/invalid items
item = self._get()
if item:
section = item['section']
processed = 0
self.start_section(section)
while not self.should_cancel():
if item is None:
break
elif item['section'] != section:
# We received an entirely new section
self.start_section(item['section'])
section = item['section']
with section.context(self.current_time) as context:
while not self.should_cancel():
if item is None or item['section'] != section:
break
self.update_progressbar(section,
item['xml'][0].get('title'),
section.count)
context.add_update(item['xml'][0],
section_name=section.name,
section_id=section.section_id,
children=item['children'])
processed += 1
section.count += 1
if processed == COMMIT_TO_DB_EVERY_X_ITEMS:
processed = 0
context.commit()
item = self._get()
self.finish_last_section()

View File

@ -15,8 +15,9 @@ from ..utils import etree
LOG = getLogger('PLEX.sync.sections')
BATCH_SIZE = 500
SECTIONS = []
# Need a way to interrupt our synching process
SHOULD_CANCEL = None
IS_CANCELED = None
LIBRARY_PATH = path_ops.translate_path('special://profile/library/video/')
# The video library might not yet exist for this user - create it
@ -40,10 +41,12 @@ class Section(object):
"""
def __init__(self, index=None, xml_element=None, section_db_element=None):
# Unique Plex id of this Plex library section
self._section_id = None # int
self._id = None # int
# Plex librarySectionUUID, unique for this section
self.uuid = None
# Building block for window variable
self._node = None # unicode
# Index of this section (as section_id might not be subsequent)
# Index of this section (as id might not be subsequent)
# This follows 1:1 the sequence in with the PMS returns the sections
self._index = None # Codacy-bug
self.index = index # int
@ -54,14 +57,9 @@ class Section(object):
self.content = None # unicode
# Setting the section_type WILL re_set sync_to_kodi!
self._section_type = None # unicode
# E.g. "season" or "movie" (translated)
self.section_type_text = None
# Do we sync all items of this section to the Kodi DB?
# This will be set with section_type!!
self.sync_to_kodi = None # bool
# For sections to be synched, the section name will be recorded as a
# tag. This is the corresponding id for this tag
self.kodi_tagid = None # int
# When was this section last successfully/completely synched to the
# Kodi database?
self.last_sync = None # int
@ -79,53 +77,53 @@ class Section(object):
self.order = None
# Original PMS xml for this section, including children
self.xml = None
# Attributes that will be initialized later by full_sync.py
self.iterator = None
self.context = None
self.get_children = None
# A section_type encompasses possible several plex_types! E.g. shows
# contain shows, seasons, episodes
self._plex_type = None
self.plex_type = None
if xml_element is not None:
self.from_xml(xml_element)
elif section_db_element:
self.from_db_element(section_db_element)
def __repr__(self):
def __unicode__(self):
return ("{{"
"'index': {self.index}, "
"'name': '{self.name}', "
"'section_id': {self.section_id}, "
"'id': {self.id}, "
"'section_type': '{self.section_type}', "
"'plex_type': '{self.plex_type}', "
"'sync_to_kodi': {self.sync_to_kodi}, "
"'last_sync': {self.last_sync}"
"}}").format(self=self).encode('utf-8')
__str__ = __repr__
"'last_sync': {self.last_sync}, "
"'uuid': {self.uuid}"
"}}").format(self=self)
def __str__(self):
return unicode(self).encode('utf-8')
__repr__ = __str__
def __nonzero__(self):
return (self.section_id is not None and
return (self.id is not None and
self.name is not None and
self.section_type is not None)
def __eq__(self, section):
"""
Sections compare equal if their section_id, name and plex_type (first
prio) OR section_type (if there is no plex_type is set) compare equal
"""
if not isinstance(section, Section):
return False
return (self.section_id == section.section_id and
return (self.id == section.id and
self.name == section.name and
(self.plex_type == section.plex_type if self.plex_type else
self.section_type == section.section_type))
self.section_type == section.section_type)
def __ne__(self, section):
return not self == section
@property
def section_id(self):
return self._section_id
def id(self):
return self._id
@section_id.setter
def section_id(self, value):
self._section_id = value
@id.setter
def id(self, value):
self._id = value
self._path = path_ops.path.join(LIBRARY_PATH, 'Plex-%s' % value, '')
self._playlist_path = path_ops.path.join(PLAYLISTS_PATH,
'Plex %s.xsp' % value)
@ -137,7 +135,7 @@ class Section(object):
@section_type.setter
def section_type(self, value):
self._section_type = value
self.content = v.CONTENT_FROM_PLEX_TYPE[value]
self.content = v.MEDIATYPE_FROM_PLEX_TYPE[value]
# Default values whether we sync or not based on the Plex type
if value == v.PLEX_TYPE_PHOTO:
self.sync_to_kodi = False
@ -146,15 +144,6 @@ class Section(object):
else:
self.sync_to_kodi = True
@property
def plex_type(self):
return self._plex_type
@plex_type.setter
def plex_type(self, value):
self._plex_type = value
self.section_type_text = utils.lang(v.TRANSLATION_FROM_PLEXTYPE[value])
@property
def index(self):
return self._index
@ -177,10 +166,10 @@ class Section(object):
return self._playlist_path
def from_db_element(self, section_db_element):
self.section_id = section_db_element['section_id']
self.id = section_db_element['section_id']
self.uuid = section_db_element['uuid']
self.name = section_db_element['section_name']
self.section_type = section_db_element['plex_type']
self.kodi_tagid = section_db_element['kodi_tagid']
self.sync_to_kodi = section_db_element['sync_to_kodi']
self.last_sync = section_db_element['last_sync']
@ -189,9 +178,10 @@ class Section(object):
Reads section from a PMS xml (Plex id, name, Plex type)
"""
api = API(xml_element)
self.section_id = utils.cast(int, xml_element.get('key'))
self.id = utils.cast(int, xml_element.get('key'))
self.uuid = xml_element.get('uuid')
self.name = api.title()
self.section_type = api.plex_type
self.section_type = api.plex_type()
self.icon = api.one_artwork('composite')
self.artwork = api.one_artwork('art')
self.thumb = api.one_artwork('thumb')
@ -217,18 +207,18 @@ class Section(object):
if not self:
raise RuntimeError('Section not clearly defined: %s' % self)
if plexdb:
plexdb.add_section(self.section_id,
plexdb.add_section(self.id,
self.uuid,
self.name,
self.section_type,
self.kodi_tagid,
self.sync_to_kodi,
self.last_sync)
else:
with PlexDB(lock=False) as plexdb:
plexdb.add_section(self.section_id,
plexdb.add_section(self.id,
self.uuid,
self.name,
self.section_type,
self.kodi_tagid,
self.sync_to_kodi,
self.last_sync)
@ -253,41 +243,29 @@ class Section(object):
raise RuntimeError('Index not initialized')
# Main list entry for this section - which will show the different
# nodes as "submenus" once the user navigates into this section
args = {
'mode': 'browseplex',
'key': '/library/sections/%s' % self.id,
'plex_type': self.section_type,
'section_id': unicode(self.id)
}
if not self.sync_to_kodi:
args['synched'] = 'false'
addon_index = self.addon_path(args)
if self.sync_to_kodi and self.section_type in v.PLEX_VIDEOTYPES:
# Node showing a menu for this section
args = {
'mode': 'show_section',
'section_index': self.index
}
index = utils.extend_url('plugin://%s' % v.ADDON_ID, args)
# Node directly displaying all content
path = 'library://video/Plex-{0}/{0}_all.xml'
path = path.format(self.section_id)
path = path.format(self.id)
index = 'library://video/Plex-%s' % self.id
else:
# Node showing a menu for this section
args = {
'mode': 'browseplex',
'key': '/library/sections/%s' % self.section_id,
'section_id': unicode(self.section_id)
}
if not self.sync_to_kodi:
args['synched'] = 'false'
# No library xmls to speed things up
# Immediately show the PMS options for this section
index = self.addon_path(args)
# Node directly displaying all content
args = {
'mode': 'browseplex',
'key': '/library/sections/%s/all' % self.section_id,
'section_id': unicode(self.section_id)
}
if not self.sync_to_kodi:
args['synched'] = 'false'
# No xmls to link to - let's show the listings on the fly
index = addon_index
args['key'] = '/library/sections/%s/all' % self.id
path = self.addon_path(args)
# .index will list all possible nodes for this library
utils.window('%s.index' % self.node, value=index)
utils.window('%s.title' % self.node, value=self.name)
utils.window('%s.type' % self.node, value=self.content)
utils.window('%s.content' % self.node, value=index)
utils.window('%s.content' % self.node, value=path)
# .path leads to all elements of this library
if self.section_type in v.PLEX_VIDEOTYPES:
utils.window('%s.path' % self.node,
@ -299,7 +277,9 @@ class Section(object):
# Pictures
utils.window('%s.path' % self.node,
value='ActivateWindow(pictures,%s,return)' % path)
utils.window('%s.id' % self.node, value=str(self.section_id))
utils.window('%s.id' % self.node, value=str(self.id))
# To let the user navigate into this node when selecting widgets
utils.window('%s.addon_index' % self.node, value=addon_index)
if not self.sync_to_kodi:
self.remove_files_from_kodi()
return
@ -315,7 +295,7 @@ class Section(object):
path_ops.makedirs(self.path)
# Create a tag just like the section name in the Kodi DB
with kodi_db.KodiVideoDB(lock=False) as kodidb:
self.kodi_tagid = kodidb.create_tag(self.name)
kodidb.create_tag(self.name)
# The xmls are numbered in order of appearance
self.order = 0
if not path_ops.exists(path_ops.path.join(self.path, 'index.xml')):
@ -336,22 +316,18 @@ class Section(object):
def _build_node(self, node_type, node_name, args, content, pms_node):
self.content = content
node_name = node_name.format(self=self)
if pms_node:
# Do NOT write a Kodi video library xml - can't use type="filter"
# to point back to plugin://plugin.video.plexkodiconnect
xml = nodes.node_pms(self, node_name, args)
args.pop('folder', None)
path = self.addon_path(args)
else:
# Write a Kodi video library xml
xml_name = '%s_%s.xml' % (self.section_id, node_type)
path = path_ops.path.join(self.path, xml_name)
if not path_ops.exists(path):
xml_name = '%s_%s.xml' % (self.id, node_type)
path = path_ops.path.join(self.path, xml_name)
if not path_ops.exists(path):
if pms_node:
# Even the xml will point back to the PKC add-on
xml = nodes.node_pms(self, node_name, args)
else:
# Let's use Kodi's logic to sort/filter the Kodi library
xml = getattr(nodes, 'node_%s' % node_type)(self, node_name)
self._write_xml(xml, xml_name)
path = 'library://video/Plex-%s/%s' % (self.section_id, xml_name)
self._write_xml(xml, xml_name)
self.order += 1
path = 'library://video/Plex-%s/%s' % (self.id, xml_name)
self._window_node(path, node_name, node_type, pms_node)
def _write_xml(self, xml, xml_name):
@ -365,7 +341,7 @@ class Section(object):
LOG.debug('Creating smart playlist for section %s: %s',
self.name, self.playlist_path)
xml = etree.Element('smartplaylist',
attrib={'type': v.CONTENT_FROM_PLEX_TYPE[self.section_type]})
attrib={'type': v.MEDIATYPE_FROM_PLEX_TYPE[self.section_type]})
etree.SubElement(xml, 'name').text = self.name
etree.SubElement(xml, 'match').text = 'all'
rule = etree.SubElement(xml, 'rule', attrib={'field': 'tag',
@ -390,13 +366,13 @@ class Section(object):
# if node_type == 'all':
# var = self.node
# utils.window('%s.index' % var,
# value=path.replace('%s_all.xml' % self.section_id, ''))
# value=path.replace('%s_all.xml' % self.id, ''))
# utils.window('%s.title' % var, value=self.name)
# else:
var = '%s.%s' % (self.node, node_type)
utils.window('%s.index' % var, value=path)
utils.window('%s.title' % var, value=node_name)
utils.window('%s.id' % var, value=str(self.section_id))
utils.window('%s.id' % var, value=str(self.id))
utils.window('%s.path' % var, value=window_path)
utils.window('%s.type' % var, value=self.content)
utils.window('%s.content' % var, value=path)
@ -431,10 +407,10 @@ class Section(object):
Removes this sections completely from the Plex DB
"""
if plexdb:
plexdb.remove_section(self.section_id)
plexdb.remove_section(self.id)
else:
with PlexDB(lock=False) as plexdb:
plexdb.remove_section(self.section_id)
plexdb.remove_section(self.id)
def remove(self):
"""
@ -446,39 +422,6 @@ class Section(object):
self.remove_from_plex()
def _get_children(plex_type):
if plex_type == v.PLEX_TYPE_ALBUM:
return True
else:
return False
def get_sync_section(section, plex_type):
"""
Deep-copies section and adds certain arguments in order to prep section
for the library sync
"""
section = copy.deepcopy(section)
section.plex_type = plex_type
section.context = itemtypes.ITEMTYPE_FROM_PLEXTYPE[plex_type]
section.get_children = _get_children(plex_type)
# Some more init stuff
# Has sync for this section been successful?
section.sync_successful = True
# List of tuples: (collection index [as in an item's metadata with
# "Collection id"], collection plex id)
section.collection_match = None
# Dict with entries of the form <collection index>: <collection xml>
section.collection_xmls = {}
# Keep count during sync
section.count = 0
# Total number of items that we need to sync
section.number_of_items = 0
# Iterator to get one sync item after the other
section.iterator = None
return section
def force_full_sync():
"""
Resets the sync timestamp for all sections to 0, thus forcing a subsequent
@ -502,7 +445,6 @@ def _retrieve_old_settings(sections, old_sections):
Thus sets to the old values:
section.last_sync
section.kodi_tagid
section.sync_to_kodi
section.last_sync
"""
@ -510,7 +452,6 @@ def _retrieve_old_settings(sections, old_sections):
for old_section in old_sections:
if section == old_section:
section.last_sync = old_section.last_sync
section.kodi_tagid = old_section.kodi_tagid
section.sync_to_kodi = old_section.sync_to_kodi
section.last_sync = old_section.last_sync
@ -529,19 +470,16 @@ def _delete_kodi_db_items(section):
types = ((v.PLEX_TYPE_ARTIST, itemtypes.Artist),
(v.PLEX_TYPE_ALBUM, itemtypes.Album),
(v.PLEX_TYPE_SONG, itemtypes.Song))
else:
types = ()
LOG.debug('Skipping deletion of DB elements for section %s', section)
for plex_type, context in types:
while True:
with PlexDB() as plexdb:
plex_ids = list(plexdb.plexid_by_sectionid(section.section_id,
plex_ids = list(plexdb.plexid_by_sectionid(section.id,
plex_type,
BATCH_SIZE))
with kodi_context(texture_db=True) as kodidb:
typus = context(None, plexdb=plexdb, kodidb=kodidb)
for plex_id in plex_ids:
if SHOULD_CANCEL():
if IS_CANCELED():
return False
typus.remove(plex_id)
if len(plex_ids) < BATCH_SIZE:
@ -581,7 +519,7 @@ def _choose_libraries(sections):
selectable_sections,
preselect=preselected,
useDetails=False)
if selected_sections is None:
if selectable_sections is None:
LOG.info('User chose not to select which libraries to sync')
return False
index = 0
@ -633,17 +571,18 @@ def sync_from_pms(parent_self, pick_libraries=False):
pick_libraries=True will prompt the user the select the libraries he
wants to sync
"""
global SHOULD_CANCEL
global IS_CANCELED
LOG.info('Starting synching sections from the PMS')
SHOULD_CANCEL = parent_self.should_cancel
IS_CANCELED = parent_self.isCanceled
try:
return _sync_from_pms(pick_libraries)
finally:
SHOULD_CANCEL = None
LOG.info('Done synching sections from the PMS: %s', app.SYNC.sections)
IS_CANCELED = None
LOG.info('Done synching sections from the PMS: %s', SECTIONS)
def _sync_from_pms(pick_libraries):
global SECTIONS
# Re-set value in order to make sure we got the lastest user input
app.SYNC.enable_music = utils.settings('enableMusic') == 'true'
xml = PF.get_plex_sections()
@ -653,9 +592,6 @@ def _sync_from_pms(pick_libraries):
sections = []
old_sections = []
for i, xml_element in enumerate(xml.findall('Directory')):
api = API(xml_element)
if api.plex_type in v.UNSUPPORTED_PLEX_TYPES:
continue
sections.append(Section(index=i, xml_element=xml_element))
with PlexDB() as plexdb:
for section_db in plexdb.all_sections():
@ -701,7 +637,7 @@ def _sync_from_pms(pick_libraries):
# Counter that tells us how many sections we have - e.g. for skins and
# listings
utils.window('Plex.nodes.total', str(len(sections)))
app.SYNC.sections = sections
SECTIONS = sections
return True
@ -713,6 +649,7 @@ def _clear_window_vars(index):
utils.window('%s.content' % node, clear=True)
utils.window('%s.path' % node, clear=True)
utils.window('%s.id' % node, clear=True)
utils.window('%s.addon_index' % node, clear=True)
# Just clear everything here, ignore the plex_type
for typus in (x[0] for y in nodes.NODE_TYPES.values() for x in y):
for kind in WINDOW_ARGS:

View File

@ -0,0 +1,104 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from .. import plex_functions as PF, utils, timing, variables as v, app
LOG = getLogger('PLEX.sync.time')
def sync_pms_time():
"""
PMS does not provide a means to get a server timestamp. This is a work-
around - because the PMS might be in another time zone
In general, everything saved to Kodi shall be in Kodi time.
Any info with a PMS timestamp is in Plex time, naturally
"""
LOG.info('Synching time with PMS server')
# Find a PMS item where we can toggle the view state to enforce a
# change in lastViewedAt
# Get all Plex libraries
sections = PF.get_plex_sections()
if sections is None:
LOG.error("Error download PMS views, abort sync_pms_time")
return False
plex_id = None
typus = (
(v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_MOVIE,),
(v.PLEX_TYPE_SHOW, v.PLEX_TYPE_EPISODE),
(v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_SONG)
)
for section_type, plex_type in typus:
if plex_id:
break
for section in sections:
if plex_id:
break
if not section.attrib['type'] == section_type:
continue
library_id = section.attrib['key']
try:
iterator = PF.SectionItems(library_id, plex_type=plex_type)
for item in iterator:
if item.get('viewCount'):
# Don't want to mess with items that have playcount>0
continue
if item.get('viewOffset'):
# Don't mess with items with a resume point
continue
plex_id = utils.cast(int, item.get('ratingKey'))
LOG.info('Found a %s item to sync with: %s',
plex_type, plex_id)
break
except RuntimeError:
pass
if plex_id is None:
LOG.error("Could not find an item to sync time with")
LOG.error("Aborting PMS-Kodi time sync")
return False
# Get the Plex item's metadata
xml = PF.GetPlexMetadata(plex_id)
if xml in (None, 401):
LOG.error("Could not download metadata, aborting time sync")
return False
timestamp = xml[0].get('lastViewedAt')
if timestamp is None:
timestamp = xml[0].get('updatedAt')
LOG.debug('Using items updatedAt=%s', timestamp)
if timestamp is None:
timestamp = xml[0].get('addedAt')
LOG.debug('Using items addedAt=%s', timestamp)
if timestamp is None:
timestamp = 0
LOG.debug('No timestamp; using 0')
timestamp = utils.cast(int, timestamp)
# Set the timer
koditime = timing.unix_timestamp()
# Toggle watched state
PF.scrobble(plex_id, 'watched')
# Let the PMS process this first!
app.APP.monitor.waitForAbort(1)
# Get updated metadata
xml = PF.GetPlexMetadata(plex_id)
# Toggle watched state back
PF.scrobble(plex_id, 'unwatched')
try:
plextime = xml[0].attrib['lastViewedAt']
except (IndexError, TypeError, AttributeError, KeyError):
LOG.warn('Could not get lastViewedAt - aborting')
return False
# Calculate time offset Kodi-PMS
timing.KODI_PLEX_TIME_OFFSET = float(koditime) - float(plextime)
utils.settings('kodiplextimeoffset',
value=str(timing.KODI_PLEX_TIME_OFFSET))
LOG.info("Time offset Koditime - Plextime in seconds: %s",
timing.KODI_PLEX_TIME_OFFSET)
return True

View File

@ -3,6 +3,7 @@
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from . import sections
from .common import update_kodi_library, PLAYLIST_SYNC_ENABLED
from .fanart import SYNC_FANART, FanartTask
from ..plex_api import API
@ -122,10 +123,16 @@ def process_new_item_message(message):
LOG.error('Could not download metadata for %s', message['plex_id'])
return False, False, False
LOG.debug("Processing new/updated PMS item: %s", message['plex_id'])
section_id = utils.cast(int, xml.get('librarySectionID'))
for section in sections.SECTIONS:
if section.id == section_id:
break
else:
LOG.error('Section id %s not yet encountered', section_id)
return False, False, False
with itemtypes.ITEMTYPE_FROM_PLEXTYPE[plex_type](timing.unix_timestamp()) as typus:
typus.add_update(xml[0],
section_name=xml.get('librarySectionTitle'),
section_id=utils.cast(int, xml.get('librarySectionID')))
section=section)
cache_artwork(message['plex_id'], plex_type)
return True, plex_type in v.PLEX_VIDEOTYPES, plex_type in v.PLEX_AUDIOTYPES
@ -313,8 +320,9 @@ def process_playing(data):
plex_id)
continue
api = API(xml[0])
session['duration'] = api.runtime()
session['viewCount'] = api.viewcount()
userdata = api.userdata()
session['duration'] = userdata['Runtime']
session['viewCount'] = userdata['PlayCount']
# Sometimes, Plex tells us resume points in milliseconds and
# not in seconds - thank you very much!
if message['viewOffset'] > session['duration']:

View File

@ -13,14 +13,10 @@ LOG = getLogger('PLEX.migration')
def check_migration():
LOG.info('Checking whether we need to migrate something')
last_migration = utils.settings('last_migrated_PKC_version')
# Ensure later migration if user downgraded PKC!
utils.settings('last_migrated_PKC_version', value=v.ADDON_VERSION)
if last_migration == '':
LOG.info('New, clean PKC installation - no migration necessary')
return
elif last_migration == v.ADDON_VERSION:
if last_migration == v.ADDON_VERSION:
LOG.info('Already migrated to PKC version %s' % v.ADDON_VERSION)
# Ensure later migration if user downgraded PKC!
utils.settings('last_migrated_PKC_version', value=v.ADDON_VERSION)
return
if not utils.compare_version(last_migration, '1.8.2'):
@ -40,63 +36,4 @@ def check_migration():
from .library_sync.sections import delete_files
delete_files()
if not utils.compare_version(last_migration, '2.8.3'):
LOG.info('Migrating to version 2.8.2')
from .library_sync import sections
sections.clear_window_vars()
sections.delete_videonode_files()
if not utils.compare_version(last_migration, '2.8.7'):
LOG.info('Migrating to version 2.8.6')
# Need to delete the UNIQUE index that prevents creating several
# playlist entries with the same kodi_hash
from .plex_db import PlexDB
with PlexDB() as plexdb:
plexdb.cursor.execute('DROP INDEX IF EXISTS ix_playlists_3')
# Index will be automatically recreated on next PKC startup
if not utils.compare_version(last_migration, '2.8.9'):
LOG.info('Migrating to version 2.8.8')
from .library_sync import sections
sections.clear_window_vars()
sections.delete_videonode_files()
if not utils.compare_version(last_migration, '2.9.3'):
LOG.info('Migrating to version 2.9.2')
# Re-sync all playlists to Kodi
from .playlists import remove_synced_playlists
remove_synced_playlists()
if not utils.compare_version(last_migration, '2.9.7'):
LOG.info('Migrating to version 2.9.6')
# Allow for a new "Direct Stream" setting (number 2), so shift the
# last setting for "force transcoding"
current_playback_type = utils.cast(int, utils.settings('playType')) or 0
if current_playback_type == 2:
current_playback_type = 3
utils.settings('playType', value=str(current_playback_type))
if not utils.compare_version(last_migration, '2.9.8'):
LOG.info('Migrating to version 2.9.7')
# Force-scan every single item in the library - seems like we could
# loose some recently added items otherwise
# Caused by 65a921c3cc2068c4a34990d07289e2958f515156
from . import library_sync
library_sync.force_full_sync()
if not utils.compare_version(last_migration, '2.11.3'):
LOG.info('Migrating to version 2.11.2')
# Re-sync all playlists to Kodi
from .playlists import remove_synced_playlists
remove_synced_playlists()
if not utils.compare_version(last_migration, '2.12.2'):
LOG.info('Migrating to version 2.12.1')
# Sign user out to make sure he needs to sign in again
utils.settings('username', value='')
utils.settings('userid', value='')
utils.settings('plex_restricteduser', value='')
utils.settings('accessToken', value='')
utils.settings('plexAvatar', value='')
utils.settings('last_migrated_PKC_version', value=v.ADDON_VERSION)

View File

@ -2,9 +2,8 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
import re
from .plex_api.media import Media
from .plex_api import API
from . import utils
from . import variables as v
@ -24,7 +23,7 @@ def excludefromscan_music_folders(sections):
"""
paths = []
reboot = False
api = Media()
api = API(item=None)
for section in sections:
if section.section_type != v.PLEX_TYPE_ARTIST:
# Only look at music libraries
@ -86,7 +85,7 @@ def _turn_to_regex(path):
else:
if not path.endswith('\\'):
path = '%s\\' % path
# Escape all characters that could cause problems
path = re.escape(path)
# Need to escape backslashes
path = path.replace('\\', '\\\\')
# Beginning of path only needs to be similar
return '^%s' % path

View File

@ -19,8 +19,6 @@ import shutil
import os
from os import path # allows to use path_ops.path.join, for example
from distutils import dir_util
import re
import xbmc
import xbmcvfs
@ -28,7 +26,6 @@ from .tools import unicode_paths
# Kodi seems to encode in utf-8 in ALL cases (unlike e.g. the OS filesystem)
KODI_ENCODING = 'utf-8'
REGEX_FILE_NUMBERING = re.compile(r'''_(\d\d)\.\w+$''')
def encode_path(path):
@ -219,25 +216,3 @@ def basename(path):
return path.rsplit('\\', 1)[1]
except IndexError:
return ''
def create_unique_path(directory, filename, extension):
"""
Checks whether 'directory/filename.extension' exists. If so, will start
numbering the filename until the file does not exist yet (up to 99)
"""
res = path.join(directory, '.'.join((filename, extension)))
while exists(res):
occurance = REGEX_FILE_NUMBERING.search(res)
if not occurance:
filename = '{}_00'.format(filename[:min(len(filename),
251 - len(extension))])
res = path.join(directory, '.'.join((filename, extension)))
else:
number = int(occurance.group(1)) + 1
if number > 99:
raise RuntimeError('Could not create unique file: {} {} {}'.format(
directory, filename, extension))
basename = re.sub(REGEX_FILE_NUMBERING, '', res)
res = '{}_{:02d}.{}'.format(basename, number, extension)
return res

View File

@ -1,615 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Used to kick off Kodi playback
"""
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from threading import Thread
import datetime
import xbmc
from .plex_api import API
from .plex_db import PlexDB
from .kodi_db import KodiVideoDB
from . import plex_functions as PF, playlist_func as PL, playqueue as PQ
from . import json_rpc as js, variables as v, utils, transfer
from . import playback_decision, app
from . import exceptions
###############################################################################
LOG = getLogger('PLEX.playback')
# Do we need to return ultimately with a setResolvedUrl?
RESOLVE = True
TRY_TO_SEEK_FOR = 300 # =30 seconds
IGNORE_SECONDS_AT_START = 15
###############################################################################
def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True,
resume=False):
"""
Hit this function for addon path playback, Plex trailers, etc.
Will setup playback first, then on second call complete playback.
Will set Playback_Successful() with potentially a PKCListItem() attached
(to be consumed by setResolvedURL in default.py)
If trailers or additional (movie-)parts are added, default.py is released
and a completely new player instance is called with a new playlist. This
circumvents most issues with Kodi & playqueues
Set resolve to False if you do not want setResolvedUrl to be called on
the first pass - e.g. if you're calling this function from the original
service.py Python instance
"""
try:
_playback_triage(plex_id, plex_type, path, resolve, resume)
finally:
# Reset some playback variables the user potentially set to init
# playback
app.PLAYSTATE.context_menu_play = False
app.PLAYSTATE.force_transcode = False
def _playback_triage(plex_id, plex_type, path, resolve, resume):
plex_id = utils.cast(int, plex_id)
LOG.debug('playback_triage called with plex_id %s, plex_type %s, path %s, '
'resolve %s, resume %s', plex_id, plex_type, path, resolve, resume)
global RESOLVE
# If started via Kodi context menu, we never resolve
RESOLVE = resolve if not app.PLAYSTATE.context_menu_play else False
if not app.CONN.online or not app.ACCOUNT.authenticated:
if not app.CONN.online:
LOG.error('PMS not online for playback')
# "{0} offline"
utils.dialog('notification',
utils.lang(29999),
utils.lang(39213).format(app.CONN.server_name),
icon='{plex}')
else:
LOG.error('Not yet authenticated for PMS, abort starting playback')
# "Unauthorized for PMS"
utils.dialog('notification', utils.lang(29999), utils.lang(30017))
_ensure_resolve(abort=True)
return
with app.APP.lock_playqueues:
playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type])
try:
pos = js.get_position(playqueue.playlistid)
except KeyError:
# Kodi bug - Playlist plays (not Playqueue) will ALWAYS be audio for
# add-on paths
LOG.debug('No position returned from player! Assuming playlist')
playqueue = PQ.get_playqueue_from_type(v.KODI_PLAYLIST_TYPE_AUDIO)
try:
pos = js.get_position(playqueue.playlistid)
except KeyError:
LOG.debug('Assuming video instead of audio playlist playback')
playqueue = PQ.get_playqueue_from_type(v.KODI_PLAYLIST_TYPE_VIDEO)
try:
pos = js.get_position(playqueue.playlistid)
except KeyError:
LOG.error('Still no position - abort')
# "Play error"
utils.dialog('notification',
utils.lang(29999),
utils.lang(30128),
icon='{error}')
_ensure_resolve(abort=True)
return
# HACK to detect playback of playlists for add-on paths
items = js.playlist_get_items(playqueue.playlistid)
try:
item = items[pos]
except IndexError:
LOG.debug('Could not apply playlist hack! Probably Widget playback')
else:
if ('id' not in item and
item.get('type') == 'unknown' and item.get('title') == ''):
LOG.debug('Kodi playlist play detected')
_playlist_playback(plex_id)
return
# Can return -1 (as in "no playlist")
pos = pos if pos != -1 else 0
LOG.debug('playQueue position %s for %s', pos, playqueue)
# Have we already initiated playback?
try:
item = playqueue.items[pos]
except IndexError:
LOG.debug('PKC playqueue yet empty, need to initialize playback')
initiate = True
else:
if item.plex_id != plex_id:
LOG.debug('Received new plex_id%s, expected %s',
plex_id, item.plex_id)
initiate = True
else:
initiate = False
if initiate:
_playback_init(plex_id, plex_type, playqueue, pos, resume)
else:
# kick off playback on second pass, resume was already set on first
# pass (threaded_playback will seek to resume)
_conclude_playback(playqueue, pos)
def _playlist_playback(plex_id):
"""
Really annoying Kodi behavior: Kodi will throw the ENTIRE playlist some-
where, causing Playlist.onAdd to fire for each item like this:
Playlist.OnAdd Data: {u'item': {u'type': u'episode', u'id': 164},
u'playlistid': 0,
u'position': 2}
This does NOT work for Addon paths, type and id will be unknown:
{u'item': {u'type': u'unknown'},
u'playlistid': 0,
u'position': 7}
At the end, only the element being played actually shows up in the Kodi
playqueue.
Hence: if we fail the first addon paths call, Kodi will start playback
for the next item in line :-)
(by the way: trying to get active Kodi player id will return [])
"""
xml = PF.GetPlexMetadata(plex_id, reraise=True)
if xml in (None, 401):
_ensure_resolve(abort=True)
return
# Kodi bug: playqueue will ALWAYS be audio playqueue UNTIL playback
# has actually started. Need to tell Kodimonitor
playqueue = PQ.get_playqueue_from_type(v.KODI_PLAYLIST_TYPE_AUDIO)
playqueue.clear(kodi=False)
# Set the flag for the potentially WRONG audio playlist so Kodimonitor
# can pick up on it
playqueue.kodi_playlist_playback = True
playlist_item = PL.playlist_item_from_xml(xml[0])
playqueue.items.append(playlist_item)
_conclude_playback(playqueue, pos=0)
def _playback_init(plex_id, plex_type, playqueue, pos, resume):
"""
Playback setup if Kodi starts playing an item for the first time.
"""
LOG.debug('Initializing PKC playback')
# Stop playback so we don't get an error message that the last item of the
# queue failed to play
app.APP.player.stop()
xml = PF.GetPlexMetadata(plex_id, reraise=True)
if xml in (None, 401):
LOG.error('Could not get a PMS xml for plex id %s', plex_id)
_ensure_resolve(abort=True)
return
if (xbmc.getCondVisibility('Window.IsVisible(Home.xml)') and
plex_type in v.PLEX_VIDEOTYPES and
playqueue.kodi_pl.size() > 1):
# playqueue.kodi_pl.size() could return more than one - since playback
# was initiated from the audio queue!
LOG.debug('Detected widget playback for videos')
elif playqueue.kodi_pl.size() > 1:
# Special case - we already got a filled Kodi playqueue
try:
_init_existing_kodi_playlist(playqueue, pos)
except exceptions.PlaylistError:
LOG.error('Playback_init for existing Kodi playlist failed')
_ensure_resolve(abort=True)
return
# Now we need to use setResolvedUrl for the item at position ZERO
# playqueue.py will pick up the missing items
_conclude_playback(playqueue, 0)
return
# "Usual" case - consider trailers and parts and build both Kodi and Plex
# playqueues
# Release default.py
_ensure_resolve()
api = API(xml[0])
if (app.PLAYSTATE.context_menu_play and
api.resume_point() and
api.plex_type in v.PLEX_VIDEOTYPES):
# User chose to either play via PMS or to force transcode
# Need to prompt whether we should resume_playback
resume = resume_dialog(int(api.resume_point()))
if resume is None:
# User cancelled dialog
return
LOG.debug('Using resume %s', resume)
resume = resume or False
trailers = False
if (not resume and plex_type == v.PLEX_TYPE_MOVIE and
utils.settings('enableCinema') == "true"):
if utils.settings('askCinema') == "true":
# "Play trailers?"
trailers = utils.yesno_dialog(utils.lang(29999), utils.lang(33016))
else:
trailers = True
LOG.debug('Resuming: %s. Playing trailers: %s', resume, trailers)
playqueue.clear()
if plex_type != v.PLEX_TYPE_CLIP:
# Post to the PMS to create a playqueue - in any case due to Companion
xml = PF.init_plex_playqueue(plex_id,
plex_type,
xml.get('librarySectionUUID'),
trailers=trailers)
if xml is None:
LOG.error('Could not get a playqueue xml for plex id %s', plex_id)
# "Play error"
utils.dialog('notification',
utils.lang(29999),
utils.lang(30128),
icon='{error}')
# Do NOT use _ensure_resolve() because we resolved above already
return
PL.get_playlist_details_from_xml(playqueue, xml)
stack = _prep_playlist_stack(xml, resume)
_process_stack(playqueue, stack)
offset = _use_kodi_db_offset(playqueue.items[pos].plex_id,
playqueue.items[pos].plex_type,
playqueue.items[pos].offset) if resume else 0
# New thread to release this one sooner (e.g. harddisk spinning up)
thread = Thread(target=threaded_playback,
args=(playqueue.kodi_pl, pos, offset))
thread.setDaemon(True)
LOG.debug('Done initializing playback, starting Kodi player at pos %s and '
'offset %s', pos, offset)
# Ensure that PKC playqueue monitor ignores the changes we just made
playqueue.pkc_edit = True
# By design, PKC will start Kodi playback using Player().play(). Kodi
# caches paths like our plugin://pkc. If we use Player().play() between
# 2 consecutive startups of exactly the same Kodi library item, Kodi's
# cache will have been flushed for some reason. Hence the 2nd call for
# plugin://pkc will be lost; Kodi will try to startup playback for an empty
# path: log entry is "CGUIWindowVideoBase::OnPlayMedia <missing path>"
thread.start()
def _ensure_resolve(abort=False):
"""
Will check whether RESOLVE=True and if so, fail Kodi playback startup
with the path 'PKC_Dummy_Path_Which_Fails' using setResolvedUrl (and some
pickling)
This way we're making sure that other Python instances (calling default.py)
will be destroyed.
"""
if RESOLVE:
# Releases the other Python thread without a ListItem
transfer.send(True)
# Wait for default.py to have completed xbmcplugin.setResolvedUrl()
transfer.wait_for_transfer(source='default')
if abort:
utils.dialog('notification',
heading='{plex}',
message=utils.lang(30128),
icon='{error}',
time=3000)
def resume_dialog(resume):
"""
Pass the resume [int] point in seconds. Returns True if user chose to
resume. Returns None if user cancelled
"""
# "Resume from {0:s}"
# "Start from beginning"
resume = datetime.timedelta(seconds=resume)
LOG.debug('Showing PKC resume dialog for resume: %s', resume)
answ = utils.dialog('contextmenu',
[utils.lang(12022).replace('{0:s}', '{0}').format(unicode(resume)),
utils.lang(12021)])
if answ == -1:
return
return answ == 0
def _init_existing_kodi_playlist(playqueue, pos):
"""
Will take the playqueue's kodi_pl with MORE than 1 element and initiate
playback (without adding trailers)
"""
LOG.debug('Kodi playlist size: %s', playqueue.kodi_pl.size())
kodi_items = js.playlist_get_items(playqueue.playlistid)
if not kodi_items:
LOG.error('No Kodi items returned')
raise exceptions.PlaylistError('No Kodi items returned')
item = PL.init_plex_playqueue(playqueue, kodi_item=kodi_items[pos])
item.force_transcode = app.PLAYSTATE.force_transcode
# playqueue.py will add the rest - this will likely put the PMS under
# a LOT of strain if the following Kodi setting is enabled:
# Settings -> Player -> Videos -> Play next video automatically
LOG.debug('Done init_existing_kodi_playlist')
def _prep_playlist_stack(xml, resume):
"""
resume [bool] will set the resume point of the LAST item of the stack, for
part 1 only
"""
stack = []
for i, item in enumerate(xml):
api = API(item)
if (app.PLAYSTATE.context_menu_play is False and
api.plex_type not in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_EPISODE)):
# If user chose to play via PMS or force transcode, do not
# use the item path stored in the Kodi DB
with PlexDB(lock=False) as plexdb:
db_item = plexdb.item_by_id(api.plex_id, api.plex_type)
kodi_id = db_item['kodi_id'] if db_item else None
kodi_type = db_item['kodi_type'] if db_item else None
else:
# We will never store clips (trailers) in the Kodi DB.
# Also set kodi_id to None for playback via PMS, so that we're
# using add-on paths.
# Also do NOT associate episodes with library items for addon paths
# as artwork lookup is broken (episode path does not link back to
# season and show)
kodi_id = None
kodi_type = None
for part, _ in enumerate(item[0]):
api.part = part
if kodi_id is None:
# Need to redirect again to PKC to conclude playback
path = api.fullpath(force_addon=True)[0]
# Using different paths than the ones saved in the Kodi DB
# fixes Kodi immediately resuming the video if one restarts
# the same video again after playback
# WARNING: This fixes startup, but renders Kodi unstable
# path = path.replace('plugin.video.plexkodiconnect.tvshows',
# 'plugin.video.plexkodiconnect', 1)
# path = path.replace('plugin.video.plexkodiconnect.movies',
# 'plugin.video.plexkodiconnect', 1)
listitem = api.listitem()
listitem.setPath(path.encode('utf-8'))
else:
# Will add directly via the Kodi DB
path = None
listitem = None
stack.append({
'kodi_id': kodi_id,
'kodi_type': kodi_type,
'file': path,
'xml_video_element': item,
'listitem': listitem,
'part': part,
'playcount': api.viewcount(),
'offset': api.resume_point(),
'resume': resume if part == 0 and i + 1 == len(xml) else None,
'id': api.item_id()
})
return stack
def _process_stack(playqueue, stack):
"""
Takes our stack and adds the items to the PKC and Kodi playqueues.
"""
# getposition() can return -1
pos = max(playqueue.kodi_pl.getposition(), 0) + 1
for item in stack:
if item['kodi_id'] is None:
playlist_item = PL.add_listitem_to_Kodi_playlist(
playqueue,
pos,
item['listitem'],
file=item['file'],
xml_video_element=item['xml_video_element'])
else:
# Directly add element so we have full metadata
playlist_item = PL.add_item_to_kodi_playlist(
playqueue,
pos,
kodi_id=item['kodi_id'],
kodi_type=item['kodi_type'],
xml_video_element=item['xml_video_element'])
playlist_item.playcount = item['playcount']
playlist_item.offset = item['offset']
playlist_item.part = item['part']
playlist_item.id = item['id']
playlist_item.force_transcode = app.PLAYSTATE.force_transcode
playlist_item.resume = item['resume']
pos += 1
def _use_kodi_db_offset(plex_id, plex_type, plex_offset):
"""
Do NOT use item.offset directly but get it from the Kodi DB (Plex might not
have gotten the last resume point)
"""
if plex_type not in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_EPISODE):
return plex_offset
with PlexDB(lock=False) as plexdb:
db_item = plexdb.item_by_id(plex_id, plex_type)
if db_item:
with KodiVideoDB(lock=False) as kodidb:
return kodidb.get_resume(db_item['kodi_fileid'])
else:
return plex_offset
def _conclude_playback(playqueue, pos):
"""
ONLY if actually being played (e.g. at 5th position of a playqueue).
Decide on direct play, direct stream, transcoding
path to
direct paths: file itself
PMS URL
Web URL
audiostream (e.g. let user choose)
subtitle stream (e.g. let user choose)
Init Kodi Playback (depending on situation):
start playback
return PKC listitem attached to result
"""
LOG.debug('Concluding playback for playqueue position %s', pos)
item = playqueue.items[pos]
if item.api.mediastream_number() is None:
# E.g. user could choose between several media streams and cancelled
LOG.debug('Did not get a mediastream_number')
_ensure_resolve()
return
item.api.part = item.part or 0
playback_decision.set_pkc_playmethod(item.api, item)
if not playback_decision.audio_subtitle_prefs(item.api, item):
LOG.info('Did not set audio subtitle prefs, aborting silently')
_ensure_resolve()
return
playback_decision.set_playurl(item.api, item)
if not item.file:
LOG.info('Did not get a playurl, aborting playback silently')
_ensure_resolve()
return
listitem = item.api.listitem(listitem=transfer.PKCListItem, resume=False)
listitem.setPath(item.file.encode('utf-8'))
if item.playmethod != v.PLAYBACK_METHOD_DIRECT_PATH:
listitem.setSubtitles(item.api.cache_external_subs())
transfer.send(listitem)
LOG.debug('Done concluding playback')
def process_indirect(key, offset, resolve=True):
"""
Called e.g. for Plex "Play later" - Plex items where we need to fetch an
additional xml for the actual playurl. In the PMS metadata, indirect="1" is
set.
Will release default.py with setResolvedUrl
Set resolve to False if playback should be kicked off directly, not via
setResolvedUrl
"""
LOG.debug('process_indirect called with key: %s, offset: %s, resolve: %s',
key, offset, resolve)
global RESOLVE
RESOLVE = resolve
offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset)) if offset != '0' else None
if key.startswith('http') or key.startswith('{server}'):
xml = PF.get_playback_xml(key, app.CONN.server_name)
elif key.startswith('/system/services'):
xml = PF.get_playback_xml('http://node.plexapp.com:32400%s' % key,
'plexapp.com',
authenticate=False,
token=app.ACCOUNT.plex_token)
else:
xml = PF.get_playback_xml('{server}%s' % key, app.CONN.server_name)
if xml is None:
_ensure_resolve(abort=True)
return
api = API(xml[0])
listitem = api.listitem(listitem=transfer.PKCListItem, resume=False)
playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type])
playqueue.clear()
item = PL.playlist_item_from_xml(xml[0])
item.offset = offset
item.playmethod = v.PLAYBACK_METHOD_DIRECT_PLAY
# Need to get yet another xml to get the final playback url
try:
xml = PF.get_playback_xml('http://node.plexapp.com:32400%s'
% xml[0][0][0].attrib['key'],
'plexapp.com',
authenticate=False,
token=app.ACCOUNT.plex_token)
except (TypeError, IndexError, AttributeError):
LOG.error('XML malformed: %s', xml.attrib)
xml = None
if xml is None:
_ensure_resolve(abort=True)
return
try:
playurl = xml[0].attrib['key']
except (TypeError, IndexError, AttributeError):
LOG.error('Last xml malformed: %s', xml.attrib)
_ensure_resolve(abort=True)
return
item.file = playurl
listitem.setPath(utils.try_encode(playurl))
playqueue.items.append(item)
if resolve is True:
transfer.send(listitem)
else:
thread = Thread(target=app.APP.player.play,
args={'item': utils.try_encode(playurl),
'listitem': listitem})
thread.setDaemon(True)
LOG.debug('Done initializing PKC playback, starting Kodi player')
thread.start()
def play_xml(playqueue, xml, offset=None, start_plex_id=None):
"""
Play all items contained in the xml passed in. Called by Plex Companion.
Either supply the ratingKey of the starting Plex element. Or set
playqueue.selectedItemID
"""
offset = int(offset) / 1000 if offset else None
LOG.debug("play_xml called with offset %s, start_plex_id %s",
offset, start_plex_id)
start_item = start_plex_id if start_plex_id is not None \
else playqueue.selectedItemID
for startpos, video in enumerate(xml):
api = API(video)
if api.plex_id == start_item:
break
else:
startpos = 0
stack = _prep_playlist_stack(xml, resume=False)
if offset:
stack[startpos]['resume'] = True
_process_stack(playqueue, stack)
LOG.debug('Playqueue after play_xml update: %s', playqueue)
thread = Thread(target=threaded_playback,
args=(playqueue.kodi_pl, startpos, offset))
LOG.debug('Done play_xml, starting Kodi player at position %s', startpos)
thread.start()
def threaded_playback(kodi_playlist, startpos, offset):
"""
Seek immediately after kicking off playback is not reliable. We even seek
to 0 (starting position) in case Kodi wants to resume but we want to start
over.
offset: resume position in seconds [int/float]
"""
LOG.debug('threaded_playback with startpos %s, offset %s',
startpos, offset)
app.APP.player.play(kodi_playlist, None, False, startpos)
offset = offset if offset else 0
i = 0
while not app.APP.is_playing or not js.get_player_ids():
if app.APP.monitor.waitForAbort(0.1):
# PKC needs to quit
return
i += 1
if i > TRY_TO_SEEK_FOR:
LOG.error('Could not seek to %s', offset)
return
try:
if offset == 0 and app.APP.player.getTime() < IGNORE_SECONDS_AT_START:
LOG.debug('Avoiding small jump to the very start of the video')
return
except RuntimeError:
# RuntimeError: XBMC is not playing any media file
pass
i = 0
answ = js.seek_to(offset * 1000)
while 'error' in answ:
# Kodi sometimes returns {u'message': u'Failed to execute method.',
# u'code': -32100} if user quickly switches videos
if app.APP.monitor.waitForAbort(0.1):
# PKC needs to quit
return
i += 1
if i > TRY_TO_SEEK_FOR:
LOG.error('Failed to seek to %s. Error: %s', offset, answ)
return
answ = js.seek_to(offset * 1000)
LOG.debug('Seek to offset %s successful', offset)

View File

@ -1,447 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from requests import exceptions
from .downloadutils import DownloadUtils as DU
from .plex_api import API
from . import plex_functions as PF, utils, variables as v
LOG = getLogger('PLEX.playback_decision')
# largest signed 32bit integer: 2147483
MAX_SIGNED_INT = int(2**31 - 1)
# PMS answer codes
DIRECT_PLAY_OK = 1000
CONVERSION_OK = 1001 # PMS can either direct stream or transcode
def set_pkc_playmethod(api, item):
item.playmethod = int(utils.settings('playType'))
LOG.info('User chose playback method %s in PKC settings',
v.EXPLICIT_PLAYBACK_METHOD[item.playmethod])
_initial_best_playback_method(api, item)
LOG.info('PKC decided on playback method %s',
v.EXPLICIT_PLAYBACK_METHOD[item.playmethod])
def set_playurl(api, item):
try:
if item.playmethod == v.PLAYBACK_METHOD_DIRECT_PATH:
# No need to ask the PMS whether we can play - we circumvent
# the PMS entirely
return
LOG.info('Lets ask the PMS next')
try:
_pms_playback_decision(api, item)
except (exceptions.RequestException,
AttributeError,
IndexError,
SystemExit):
LOG.warn('Could not find suitable settings for playback, aborting')
utils.ERROR(notify=True)
item.playmethod = None
item.file = None
else:
item.file = api.transcode_video_path(item.playmethod,
quality=item.quality)
finally:
LOG.info('The playurl for %s is: %s',
v.EXPLICIT_PLAYBACK_METHOD[item.playmethod], item.file)
def _initial_best_playback_method(api, item):
"""
Sets the highest available playback method without talking to the PMS
Also sets self.path for a direct path, if available and accessible
"""
item.file = api.file_path()
item.file = api.validate_playurl(item.file, api.plex_type, force_check=True)
# Check whether we have a strm file that we need to throw at Kodi 1:1
if item.file is not None and item.file.endswith('.strm'):
# Use direct path in any case, regardless of user setting
LOG.debug('.strm file detected')
item.playmethod = v.PLAYBACK_METHOD_DIRECT_PATH
elif _must_transcode(api, item):
item.playmethod = v.PLAYBACK_METHOD_TRANSCODE
elif item.playmethod in (v.PLAYBACK_METHOD_DIRECT_PLAY,
v.PLAYBACK_METHOD_DIRECT_STREAM):
pass
elif item.file is None:
# E.g. direct path was not possible to access
item.playmethod = v.PLAYBACK_METHOD_DIRECT_PLAY
else:
item.playmethod = v.PLAYBACK_METHOD_DIRECT_PATH
def _pms_playback_decision(api, item):
"""
We CANNOT distinguish direct playing from direct streaming from the PMS'
answer
"""
ask_for_user_quality_settings = False
if item.playmethod <= 2:
LOG.info('Asking PMS with maximal quality settings')
item.quality = _max_quality()
decision_api = _ask_pms(api, item)
if decision_api.decision_code() > CONVERSION_OK:
ask_for_user_quality_settings = True
else:
ask_for_user_quality_settings = True
if ask_for_user_quality_settings:
item.quality = _transcode_quality()
LOG.info('Asking PMS with user quality settings')
decision_api = _ask_pms(api, item)
# Process the PMS answer
if decision_api.decision_code() > CONVERSION_OK:
LOG.error('Neither DirectPlay, DirectStream nor transcoding possible')
error = '%s\n%s' % (decision_api.general_play_decision_text(),
decision_api.transcode_decision_text())
utils.messageDialog(heading=utils.lang(29999),
msg=error)
raise AttributeError('Neither DirectPlay, DirectStream nor transcoding possible')
if (item.playmethod == v.PLAYBACK_METHOD_DIRECT_PLAY and
decision_api.decision_code() == DIRECT_PLAY_OK):
# All good
return
LOG.info('PMS video stream decision: %s, PMS audio stream decision: %s, '
'PMS subtitle stream decision: %s',
decision_api.video_decision(),
decision_api.audio_decision(),
decision_api.subtitle_decision())
# Only look at the video stream since that'll be most CPU-intensive for
# the PMS
video_direct_streaming = decision_api.video_decision() == 'copy'
if video_direct_streaming:
if item.playmethod < v.PLAYBACK_METHOD_DIRECT_STREAM:
LOG.warn('The PMS forces us to direct stream')
# "PMS enforced direct streaming"
utils.dialog('notification',
utils.lang(29999),
utils.lang(33005),
icon='{plex}')
item.playmethod = v.PLAYBACK_METHOD_DIRECT_STREAM
else:
if item.playmethod < v.PLAYBACK_METHOD_TRANSCODE:
LOG.warn('The PMS forces us to transcode')
# "PMS enforced transcoding"
utils.dialog('notification',
utils.lang(29999),
utils.lang(33004),
icon='{plex}')
item.playmethod = v.PLAYBACK_METHOD_TRANSCODE
def _ask_pms(api, item):
xml = PF.playback_decision(path=api.path_and_plex_id(),
media=api.mediastream,
part=api.part,
playmethod=item.playmethod,
video=api.plex_type in v.PLEX_VIDEOTYPES,
args=item.quality)
decision_api = API(xml)
LOG.info('PMS general decision %s: %s',
decision_api.general_play_decision_code(),
decision_api.general_play_decision_text())
LOG.info('PMS Direct Play decision %s: %s',
decision_api.direct_play_decision_code(),
decision_api.direct_play_decision_text())
LOG.info('PMS MDE decision %s: %s',
decision_api.mde_play_decision_code(),
decision_api.mde_play_decision_text())
LOG.info('PMS transcoding decision %s: %s',
decision_api.transcode_decision_code(),
decision_api.transcode_decision_text())
return decision_api
def _must_transcode(api, item):
"""
Returns True if we need to transcode because
- codec is in h265
- 10bit video codec
- HEVC codec
- playqueue_item force_transcode is set to True
- state variable FORCE_TRANSCODE set to True
(excepting trailers etc.)
- video bitrate above specified settings bitrate
if the corresponding file settings are set to 'true'
"""
if api.plex_type in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_SONG):
LOG.info('Plex clip or music track, not transcoding')
return False
if item.playmethod == v.PLAYBACK_METHOD_TRANSCODE:
return True
videoCodec = api.video_codec()
LOG.debug("videoCodec received from the PMS: %s", videoCodec)
if item.force_transcode is True:
LOG.info('User chose to force-transcode')
return True
codec = videoCodec['videocodec']
if codec is None:
# e.g. trailers. Avoids TypeError with "'h265' in codec"
LOG.info('No codec from PMS, not transcoding.')
return False
if ((utils.settings('transcodeHi10P') == 'true' and
videoCodec['bitDepth'] == '10') and
('h264' in codec)):
LOG.info('Option to transcode 10bit h264 video content enabled.')
return True
try:
bitrate = int(videoCodec['bitrate'])
except (TypeError, ValueError):
LOG.info('No video bitrate from PMS, not transcoding.')
return False
if bitrate > _get_max_bitrate():
LOG.info('Video bitrate of %s is higher than the maximal video'
'bitrate of %s that the user chose. Transcoding',
bitrate, _get_max_bitrate())
return True
try:
resolution = int(videoCodec['resolution'])
except (TypeError, ValueError):
if videoCodec['resolution'] == '4k':
resolution = 2160
else:
LOG.info('No video resolution from PMS, not transcoding.')
return False
if 'h265' in codec or 'hevc' in codec:
if resolution >= _getH265():
LOG.info('Option to transcode h265/HEVC enabled. Resolution '
'of the media: %s, transcoding limit resolution: %s',
resolution, _getH265())
return True
return False
def _transcode_quality():
return {
'maxVideoBitrate': get_bitrate(),
'videoResolution': get_resolution(),
'videoQuality': 100,
'mediaBufferSize': int(float(utils.settings('kodi_video_cache')) / 1024.0),
}
def _max_quality():
return {
'maxVideoBitrate': MAX_SIGNED_INT,
'videoResolution': '3840x2160', # 4K
'videoQuality': 100,
'mediaBufferSize': int(float(utils.settings('kodi_video_cache')) / 1024.0),
}
def get_bitrate():
"""
Get the desired transcoding bitrate from the settings
"""
videoQuality = utils.settings('transcoderVideoQualities')
bitrate = {
'0': 320,
'1': 720,
'2': 1500,
'3': 2000,
'4': 3000,
'5': 4000,
'6': 8000,
'7': 10000,
'8': 12000,
'9': 20000,
'10': 40000,
'11': 35000,
'12': 50000
}
# max bit rate supported by server (max signed 32bit integer)
return bitrate.get(videoQuality, MAX_SIGNED_INT)
def get_resolution():
"""
Get the desired transcoding resolutions from the settings
"""
chosen = utils.settings('transcoderVideoQualities')
res = {
'0': '720x480',
'1': '1024x768',
'2': '1280x720',
'3': '1280x720',
'4': '1920x1080',
'5': '1920x1080',
'6': '1920x1080',
'7': '1920x1080',
'8': '1920x1080',
'9': '3840x2160',
'10': '3840x2160'
}
return res[chosen]
def _get_max_bitrate():
max_bitrate = utils.settings('maxVideoQualities')
bitrate = {
'0': 320,
'1': 720,
'2': 1500,
'3': 2000,
'4': 3000,
'5': 4000,
'6': 8000,
'7': 10000,
'8': 12000,
'9': 20000,
'10': 40000,
'11': MAX_SIGNED_INT # deactivated
}
# max bit rate supported by server (max signed 32bit integer)
return bitrate.get(max_bitrate, MAX_SIGNED_INT)
def _getH265():
"""
Returns the user settings for transcoding h265: boundary resolutions
of 480, 720 or 1080 as an int
OR 2147483 (MAX_SIGNED_INT, int) if user chose not to transcode
"""
H265 = {
'0': MAX_SIGNED_INT,
'1': 480,
'2': 720,
'3': 1080,
'4': 2160
}
return H265[utils.settings('transcodeH265')]
def audio_subtitle_prefs(api, item):
"""
Sets the stage for transcoding, letting the user potentially choose both
audio and subtitle streams; subtitle streams to burn-into the video file.
Uses a PUT request to the PMS, simulating e.g. the user using Plex Web,
choosing a different stream in the video's metadata and THEN initiating
playback.
Returns None if user cancelled or we need to abort, True otherwise
"""
# Set media and part where we're at
if api.mediastream is None and api.mediastream_number() is None:
return
if item.playmethod != v.PLAYBACK_METHOD_TRANSCODE:
return True
return setup_transcoding_audio_subtitle_prefs(api.plex_media_streams(),
api.part_id())
def setup_transcoding_audio_subtitle_prefs(mediastreams, part_id):
audio_streams_list = []
audio_streams = []
audio_default = None
subtitle_default = None
subtitle_streams_list = []
# "Don't burn-in any subtitle"
subtitle_streams = ['1 %s' % utils.lang(39706)]
# selectAudioIndex = ""
select_subs_index = ""
audio_numb = 0
# Remember 'no subtitles'
sub_num = 1
for stream in mediastreams:
# Since Plex returns all possible tracks together, have to sort
# them.
index = stream.get('id')
typus = stream.get('streamType')
# Audio
if typus == "2":
codec = stream.get('codec')
channellayout = stream.get('audioChannelLayout', "")
try:
track = "%s %s - %s %s" % (audio_numb + 1,
stream.attrib['language'],
codec,
channellayout)
except KeyError:
track = "%s %s - %s %s" % (audio_numb + 1,
utils.lang(39707), # unknown
codec,
channellayout)
if stream.get('default'):
audio_default = audio_numb
audio_streams_list.append(index)
audio_streams.append(track.encode('utf-8'))
audio_numb += 1
# Subtitles
elif typus == "3":
if stream.get('key'):
# Subtitle can and will be downloaded - don't let user choose
# this subtitle to burn-in
continue
# Subtitle is available within the video file
# Burn in the subtitle, if user chooses to do so
forced = stream.get('forced')
try:
track = '{} {}'.format(sub_num + 1,
stream.attrib['displayTitle'])
except KeyError:
track = '{} {} ({})'.format(sub_num + 1,
utils.lang(39707), # unknown
stream.get('codec'))
if stream.get('default'):
subtitle_default = sub_num
track = "%s - %s" % (track, utils.lang(39708)) # Default
if forced:
track = "%s - %s" % (track, utils.lang(39709)) # Forced
track = "%s (%s)" % (track, utils.lang(39710)) # burn-in
subtitle_streams_list.append(index)
subtitle_streams.append(track.encode('utf-8'))
sub_num += 1
if audio_numb > 1:
# "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
if utils.settings('bestQuality') == 'true' and audio_default is not None:
resp = audio_default
else:
resp = utils.dialog('select', utils.lang(33013), audio_streams)
if resp == -1:
LOG.info('User aborted dialog to select audio stream')
return
args = {
'audioStreamID': audio_streams_list[resp],
'allParts': 1
}
DU().downloadUrl('{server}/library/parts/%s' % part_id,
action_type='PUT',
parameters=args)
# Zero telling the PMS to deactivate subs altogether
select_subs_index = 0
if sub_num == 1:
# Note: we DO need to tell the PMS that we DONT want any sub
# Otherwise, the PMS might pick-up the last one
LOG.info('No subtitles to burn-in')
else:
# "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
if utils.settings('bestQuality') == 'true' and subtitle_default is not None:
resp = subtitle_default
else:
resp = utils.dialog('select', utils.lang(33014), subtitle_streams)
if resp == -1:
LOG.info('User aborted dialog to select subtitle stream')
return
elif resp == 0:
# User did not select a subtitle or backed out of the dialog
LOG.info('User chose to not burn-in any subtitles')
else:
LOG.info('User chose to burn-in subtitle %s: %s',
select_subs_index,
subtitle_streams[resp].decode('utf-8'))
select_subs_index = subtitle_streams_list[resp - 1]
# Now prep the PMS for our choice
PF.change_subtitle(select_subs_index, part_id)
return True

View File

@ -3,7 +3,9 @@
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from . import utils, playback, context_entry, transfer, backgroundthread
from .plex_api import API
from . import utils, context_entry, transfer, backgroundthread, variables as v
from . import app, plex_functions as PF, playqueue as PQ
###############################################################################
@ -29,28 +31,107 @@ class PlaybackTask(backgroundthread.Task):
# E.g. other add-ons scanning for Extras folder
LOG.debug('Detected 3rd party add-on call - ignoring')
transfer.send(True)
# Wait for default.py to have completed xbmcplugin.setResolvedUrl()
transfer.wait_for_transfer(source='default')
return
params = dict(utils.parse_qsl(params))
mode = params.get('mode')
resolve = False if params.get('handle') == '-1' else True
LOG.debug('Received mode: %s, params: %s', mode, params)
if mode == 'play':
if params.get('resume'):
resume = params.get('resume') == '1'
else:
resume = None
playback.playback_triage(plex_id=params.get('plex_id'),
plex_type=params.get('plex_type'),
path=params.get('path'),
resolve=resolve,
resume=resume)
elif mode == 'plex_node':
playback.process_indirect(params['key'],
params['offset'],
resolve=resolve)
if mode == 'plex_node':
process_indirect(params['key'],
params['offset'],
resolve=resolve)
elif mode == 'context_menu':
context_entry.ContextMenu(kodi_id=params.get('kodi_id'),
kodi_type=params.get('kodi_type'))
LOG.debug('Finished PlaybackTask')
def process_indirect(key, offset, resolve=True):
"""
Called e.g. for Plex "Play later" - Plex items where we need to fetch an
additional xml for the actual playurl. In the PMS metadata, indirect="1" is
set.
Will release default.py with setResolvedUrl
Set resolve to False if playback should be kicked off directly, not via
setResolvedUrl
"""
LOG.info('process_indirect called with key: %s, offset: %s, resolve: %s',
key, offset, resolve)
global RESOLVE
RESOLVE = resolve
offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset)) if offset != '0' else None
if key.startswith('http') or key.startswith('{server}'):
xml = PF.get_playback_xml(key, app.CONN.server_name)
elif key.startswith('/system/services'):
xml = PF.get_playback_xml('http://node.plexapp.com:32400%s' % key,
'plexapp.com',
authenticate=False,
token=app.ACCOUNT.plex_token)
else:
xml = PF.get_playback_xml('{server}%s' % key, app.CONN.server_name)
if xml is None:
_ensure_resolve(abort=True)
return
api = API(xml[0])
listitem = transfer.PKCListItem()
api.create_listitem(listitem)
playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()])
playqueue.clear()
item = PQ.PlaylistItem(xml_video_element=xml[0])
item.offset = offset
item.playmethod = 'DirectStream'
# Need to get yet another xml to get the final playback url
try:
xml = PF.get_playback_xml('http://node.plexapp.com:32400%s'
% xml[0][0][0].attrib['key'],
'plexapp.com',
authenticate=False,
token=app.ACCOUNT.plex_token)
except (TypeError, IndexError, AttributeError):
LOG.error('XML malformed: %s', xml.attrib)
xml = None
if xml is None:
_ensure_resolve(abort=True)
return
try:
playurl = xml[0].attrib['key']
except (TypeError, IndexError, AttributeError):
LOG.error('Last xml malformed: %s\n%s', xml.tag, xml.attrib)
_ensure_resolve(abort=True)
return
item.file = playurl
listitem.setPath(playurl.encode('utf-8'))
playqueue.items.append(item)
if resolve is True:
transfer.send(listitem)
else:
LOG.info('Done initializing PKC playback, starting Kodi player')
app.APP.player.play(item=playurl.encode('utf-8'),
listitem=listitem)
def _ensure_resolve(abort=False):
"""
Will check whether RESOLVE=True and if so, fail Kodi playback startup
with the path 'PKC_Dummy_Path_Which_Fails' using setResolvedUrl (and some
pickling)
This way we're making sure that other Python instances (calling default.py)
will be destroyed.
"""
if RESOLVE:
# Releases the other Python thread without a ListItem
transfer.send(True)
# Shows PKC error message
# transfer.send(None)
if abort:
# Reset some playback variables
app.PLAYSTATE.context_menu_play = False
app.PLAYSTATE.force_transcode = False
app.PLAYSTATE.resume_playback = False

View File

@ -1,924 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Collection of functions associated with Kodi and Plex playlists and playqueues
"""
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from .plex_api import API
from .plex_db import PlexDB
from . import plex_functions as PF
from .kodi_db import kodiid_from_filename
from .downloadutils import DownloadUtils as DU
from . import utils
from .utils import cast
from . import json_rpc as js
from . import variables as v
from . import app
from .exceptions import PlaylistError
from .subtitles import accessible_plex_subtitles
LOG = getLogger('PLEX.playlist_func')
class Playqueue_Object(object):
"""
PKC object to represent PMS playQueues and Kodi playlist for queueing
playlistid = None [int] Kodi playlist id (0, 1, 2)
type = None [str] Kodi type: 'audio', 'video', 'picture'
kodi_pl = None Kodi xbmc.PlayList object
items = [] [list] of Playlist_Items
id = None [str] Plex playQueueID, unique Plex identifier
version = None [int] Plex version of the playQueue
selectedItemID = None
[str] Plex selectedItemID, playing element in queue
selectedItemOffset = None
[str] Offset of the playing element in queue
shuffled = 0 [int] 0: not shuffled, 1: ??? 2: ???
repeat = 0 [int] 0: not repeated, 1: ??? 2: ???
If Companion playback is initiated by another user:
plex_transient_token = None
"""
kind = 'playQueue'
def __init__(self):
self.id = None
self.type = None
self.playlistid = None
self.kodi_pl = None
self.items = []
self.version = None
self.selectedItemID = None
self.selectedItemOffset = None
self.shuffled = 0
self.repeat = 0
self.plex_transient_token = None
# Need a hack for detecting swaps of elements
self.old_kodi_pl = []
# Did PKC itself just change the playqueue so the PKC playqueue monitor
# should not pick up any changes?
self.pkc_edit = False
# Workaround to avoid endless loops of detecting PL clears
self._clear_list = []
# To keep track if Kodi playback was initiated from a Kodi playlist
# There are a couple of pitfalls, unfortunately...
self.kodi_playlist_playback = False
def __repr__(self):
answ = ("{{"
"'playlistid': {self.playlistid}, "
"'id': {self.id}, "
"'version': {self.version}, "
"'type': '{self.type}', "
"'selectedItemID': {self.selectedItemID}, "
"'selectedItemOffset': {self.selectedItemOffset}, "
"'shuffled': {self.shuffled}, "
"'repeat': {self.repeat}, "
"'kodi_playlist_playback': {self.kodi_playlist_playback}, "
"'pkc_edit': {self.pkc_edit}, ".format(self=self))
answ = answ.encode('utf-8')
# Since list.__repr__ will return string, not unicode
return answ + b"'items': {self.items}}}".format(self=self)
def __str__(self):
return self.__repr__()
def is_pkc_clear(self):
"""
Returns True if PKC has cleared the Kodi playqueue just recently.
Then this clear will be ignored from now on
"""
try:
self._clear_list.pop()
except IndexError:
return False
else:
return True
def clear(self, kodi=True):
"""
Resets the playlist object to an empty playlist.
Pass kodi=False in order to NOT clear the Kodi playqueue
"""
# kodi monitor's on_clear method will only be called if there were some
# items to begin with
if kodi and self.kodi_pl.size() != 0:
self._clear_list.append(None)
self.kodi_pl.clear() # Clear Kodi playlist object
self.items = []
self.id = None
self.version = None
self.selectedItemID = None
self.selectedItemOffset = None
self.shuffled = 0
self.repeat = 0
self.plex_transient_token = None
self.old_kodi_pl = []
self.kodi_playlist_playback = False
LOG.debug('Playlist cleared: %s', self)
def position_from_plex_id(self, plex_id):
"""
Returns the position [int] for the very first item with plex_id [int]
(Plex seems uncapable of adding the same element multiple times to a
playqueue or playlist)
Raises KeyError if not found
"""
for position, item in enumerate(self.items):
if item.plex_id == plex_id:
break
else:
raise KeyError('Did not find plex_id %s in %s', plex_id, self)
return position
class PlaylistItem(object):
"""
Object to fill our playqueues and playlists with.
id = None [int] Plex playlist/playqueue id, e.g. playQueueItemID
plex_id = None [int] Plex unique item id, "ratingKey"
plex_type = None [str] Plex type, e.g. 'movie', 'clip'
kodi_id = None [int] Kodi unique kodi id (unique only within type!)
kodi_type = None [str] Kodi type: 'movie'
file = None [str] Path to the item's file. STRING!!
uri = None [str] PMS path to item; will be auto-set with plex_id
guid = None [str] Weird Plex guid
api = None [API] API of xml 1 lvl below <MediaContainer>
playmethod = None [str] either 'DirectPath', 'DirectStream', 'Transcode'
playcount = None [int] how many times the item has already been played
offset = None [int] the item's view offset UPON START in Plex time
part = 0 [int] part number if Plex video consists of mult. parts
force_transcode [bool] defaults to False
"""
def __init__(self):
self.id = None
self._plex_id = None
self.plex_type = None
self.kodi_id = None
self.kodi_type = None
self.file = None
self._uri = None
self.guid = None
self.api = None
self.playmethod = None
self.playcount = None
self.offset = None
# Transcoding quality, if needed
self.quality = None
# If Plex video consists of several parts; part number
self.part = 0
self.force_transcode = False
# Shall we ask user to resume this item?
# None: ask user to resume
# False: do NOT resume, don't ask user
# True: do resume, don't ask user
self.resume = None
# Get the Plex audio and subtitle streams in the same order as Kodi
# uses them (Kodi uses indexes to activate them, not ids like Plex)
self._streams_have_been_processed = False
self._audio_streams = None
self._subtitle_streams = None
# Which Kodi streams are active?
self.current_kodi_audio_stream = None
# False means "deactivated", None means "we do not have a Kodi
# equivalent for this Plex subtitle"
self.current_kodi_sub_stream = None
@property
def plex_id(self):
return self._plex_id
@plex_id.setter
def plex_id(self, value):
self._plex_id = value
self._uri = ('server://%s/com.plexapp.plugins.library/library/metadata/%s' %
(app.CONN.machine_identifier, value))
@property
def uri(self):
return self._uri
@property
def audio_streams(self):
if not self._streams_have_been_processed:
self._process_streams()
return self._audio_streams
@property
def subtitle_streams(self):
if not self._streams_have_been_processed:
self._process_streams()
return self._subtitle_streams
def __unicode__(self):
return ("{{"
"'id': {self.id}, "
"'plex_id': {self.plex_id}, "
"'plex_type': '{self.plex_type}', "
"'kodi_id': {self.kodi_id}, "
"'kodi_type': '{self.kodi_type}', "
"'file': '{self.file}', "
"'guid': '{self.guid}', "
"'playmethod': '{self.playmethod}', "
"'playcount': {self.playcount}, "
"'resume': {self.resume},"
"'offset': {self.offset}, "
"'force_transcode': {self.force_transcode}, "
"'part': {self.part}".format(self=self))
def __repr__(self):
return self.__unicode__().encode('utf-8')
def _process_streams(self):
"""
Builds audio and subtitle streams and enables matching between Plex
and Kodi using self.audio_streams and self.subtitle_streams
"""
# The playqueue response from the PMS does not contain a stream filename
# thanks Plex
self._subtitle_streams = accessible_plex_subtitles(
self.playmethod,
self.file,
self.api.plex_media_streams())
# Audio streams are much easier - they're always available and sorted
# the same in Kodi and Plex
self._audio_streams = [x for x in self.api.plex_media_streams()
if x.get('streamType') == '2']
self._streams_have_been_processed = True
def _get_iterator(self, stream_type):
if stream_type == 'audio':
return self.audio_streams
elif stream_type == 'subtitle':
return self.subtitle_streams
def plex_stream_index(self, kodi_stream_index, stream_type):
"""
Pass in the kodi_stream_index [int] in order to receive the Plex stream
index [int].
stream_type: 'video', 'audio', 'subtitle'
Returns None if unsuccessful
"""
if stream_type == 'audio':
return int(self.audio_streams[kodi_stream_index].get('id'))
elif stream_type == 'subtitle':
try:
return int(self.subtitle_streams[kodi_stream_index].get('id'))
except (IndexError, TypeError):
pass
def kodi_stream_index(self, plex_stream_index, stream_type):
"""
Pass in the plex_stream_index [int] in order to receive the Kodi stream
index [int].
stream_type: 'video', 'audio', 'subtitle'
Returns None if unsuccessful
"""
if plex_stream_index is None:
return
for i, stream in enumerate(self._get_iterator(stream_type)):
if cast(int, stream.get('id')) == plex_stream_index:
return i
def active_plex_stream_index(self, stream_type):
"""
Returns the following tuple for the active stream on the Plex side:
(Plex stream id [int], languageTag [str] or None)
Returns None if no stream has been selected
"""
for i, stream in enumerate(self._get_iterator(stream_type)):
if stream.get('selected') == '1':
return (int(stream.get('id')), stream.get('languageTag'))
def on_kodi_subtitle_stream_change(self, kodi_stream_index, subs_enabled):
"""
Call this method if Kodi changed its subtitle and you want Plex to
know.
"""
if subs_enabled:
try:
plex_stream_index = int(self.subtitle_streams[kodi_stream_index].get('id'))
except (IndexError, TypeError):
LOG.debug('Kodi subtitle change detected to a sub %s that is '
'NOT available on the Plex side', kodi_stream_index)
self.current_kodi_sub_stream = None
return
LOG.debug('Kodi subtitle change detected: telling Plex about '
'switch to index %s, Plex stream id %s',
kodi_stream_index, plex_stream_index)
self.current_kodi_sub_stream = kodi_stream_index
else:
plex_stream_index = 0
LOG.debug('Kodi subtitle has been deactivated, telling Plex')
self.current_kodi_sub_stream = False
PF.change_subtitle(plex_stream_index, self.api.part_id())
def on_kodi_audio_stream_change(self, kodi_stream_index):
"""
Call this method if Kodi changed its audio stream and you want Plex to
know. kodi_stream_index [int]
"""
plex_stream_index = int(self.audio_streams[kodi_stream_index].get('id'))
LOG.debug('Changing Plex audio stream to %s, Kodi index %s',
plex_stream_index, kodi_stream_index)
PF.change_audio_stream(plex_stream_index, self.api.part_id())
self.current_kodi_audio_stream = kodi_stream_index
def switch_to_plex_streams(self):
self.switch_to_plex_stream('audio')
self.switch_to_plex_stream('subtitle')
def switch_to_plex_stream(self, typus):
try:
plex_index, language_tag = self.active_plex_stream_index(typus)
except TypeError:
LOG.debug('Deactivating Kodi subtitles because the PMS '
'told us to not show any subtitles')
app.APP.player.showSubtitles(False)
self.current_kodi_sub_stream = False
return
LOG.debug('The PMS wants to display %s stream with Plex id %s and '
'languageTag %s', typus, plex_index, language_tag)
kodi_index = self.kodi_stream_index(plex_index, typus)
if kodi_index is None:
LOG.debug('Leaving Kodi %s stream settings untouched since we '
'could not parse Plex %s stream with id %s to a Kodi'
' index', typus, typus, plex_index)
else:
LOG.debug('Switching to Kodi %s stream number %s because the '
'PMS told us to show stream with Plex id %s',
typus, kodi_index, plex_index)
# If we're choosing an "illegal" index, this function does
# need seem to fail nor log any errors
if typus == 'audio':
app.APP.player.setAudioStream(kodi_index)
else:
app.APP.player.setSubtitleStream(kodi_index)
app.APP.player.showSubtitles(True)
if typus == 'audio':
self.current_kodi_audio_stream = kodi_index
else:
self.current_kodi_sub_stream = kodi_index
def on_av_change(self, playerid):
kodi_audio_stream = js.get_current_audio_stream_index(playerid)
sub_enabled = js.get_subtitle_enabled(playerid)
kodi_sub_stream = js.get_current_subtitle_stream_index(playerid)
# Audio
if kodi_audio_stream != self.current_kodi_audio_stream:
self.on_kodi_audio_stream_change(kodi_audio_stream)
# Subtitles - CURRENTLY BROKEN ON THE KODI SIDE!
# current_kodi_sub_stream may also be zero
subs_off = (None, False)
if ((sub_enabled and self.current_kodi_sub_stream in subs_off)
or (not sub_enabled and self.current_kodi_sub_stream not in subs_off)
or (kodi_sub_stream is not None
and kodi_sub_stream != self.current_kodi_sub_stream)):
self.on_kodi_subtitle_stream_change(kodi_sub_stream, sub_enabled)
def playlist_item_from_kodi(kodi_item):
"""
Turns the JSON answer from Kodi into a playlist element
Supply with data['item'] as returned from Kodi JSON-RPC interface.
kodi_item dict contains keys 'id', 'type', 'file' (if applicable)
"""
item = PlaylistItem()
item.kodi_id = kodi_item.get('id')
item.kodi_type = kodi_item.get('type')
if item.kodi_id:
with PlexDB(lock=False) as plexdb:
db_item = plexdb.item_by_kodi_id(kodi_item['id'], kodi_item['type'])
if db_item:
item.plex_id = db_item['plex_id']
item.plex_type = db_item['plex_type']
item.file = kodi_item.get('file')
if item.plex_id is None and item.file is not None:
try:
query = item.file.split('?', 1)[1]
except IndexError:
query = ''
query = dict(utils.parse_qsl(query))
item.plex_id = cast(int, query.get('plex_id'))
item.plex_type = query.get('itemType')
LOG.debug('Made playlist item from Kodi: %s', item)
return item
def verify_kodi_item(plex_id, kodi_item):
"""
Tries to lookup kodi_id and kodi_type for kodi_item (with kodi_item['file']
supplied) - if and only if plex_id is None.
Returns the kodi_item with kodi_item['id'] and kodi_item['type'] possibly
set to None if unsuccessful.
Will raise a PlaylistError if plex_id is None and kodi_item['file'] starts
with either 'plugin' or 'http'.
Will raise KeyError if neither plex_id nor kodi_id are found
"""
if plex_id is not None or kodi_item.get('id') is not None:
# Got all the info we need
return kodi_item
# Special case playlist startup - got type but no id
if (not app.SYNC.direct_paths and app.SYNC.enable_music and
kodi_item.get('type') == v.KODI_TYPE_SONG and
kodi_item['file'].startswith('http')):
kodi_item['id'], _ = kodiid_from_filename(kodi_item['file'],
v.KODI_TYPE_SONG)
LOG.debug('Detected song. Research results: %s', kodi_item)
return kodi_item
# Need more info since we don't have kodi_id nor type. Use file path.
if ((kodi_item['file'].startswith('plugin') and
not kodi_item['file'].startswith('plugin://%s' % v.ADDON_ID)) or
kodi_item['file'].startswith('http')):
LOG.debug('kodi_item cannot be used for Plex playback: %s', kodi_item)
raise PlaylistError('kodi_item cannot be used for Plex playback')
LOG.debug('Starting research for Kodi id since we didnt get one: %s',
kodi_item)
# Try the VIDEO DB first - will find both movies and episodes
kodi_id, kodi_type = kodiid_from_filename(kodi_item['file'],
db_type='video')
if not kodi_id:
# No movie or episode found - try MUSIC DB now for songs
kodi_id, kodi_type = kodiid_from_filename(kodi_item['file'],
db_type='music')
kodi_item['id'] = kodi_id
kodi_item['type'] = None if kodi_id is None else kodi_type
if plex_id is None and kodi_id is None:
raise KeyError('Neither Plex nor Kodi id found for %s' % kodi_item)
LOG.debug('Research results for kodi_item: %s', kodi_item)
return kodi_item
def playlist_item_from_plex(plex_id):
"""
Returns a playlist element providing the plex_id ("ratingKey")
Returns a Playlist_Item
"""
item = PlaylistItem()
item.plex_id = plex_id
with PlexDB(lock=False) as plexdb:
db_item = plexdb.item_by_id(plex_id)
if db_item:
item.plex_type = db_item['plex_type']
item.kodi_id = db_item['kodi_id']
item.kodi_type = db_item['kodi_type']
else:
raise KeyError('Could not find plex_id %s in database' % plex_id)
LOG.debug('Made playlist item from plex: %s', item)
return item
def playlist_item_from_xml(xml_video_element, kodi_id=None, kodi_type=None):
"""
Returns a playlist element for the playqueue using the Plex xml
xml_video_element: etree xml piece 1 level underneath <MediaContainer>
"""
item = PlaylistItem()
api = API(xml_video_element)
item.plex_id = api.plex_id
item.plex_type = api.plex_type
# item.id will only be set if you passed in an xml_video_element from e.g.
# a playQueue
item.id = api.item_id()
if kodi_id is not None:
item.kodi_id = kodi_id
item.kodi_type = kodi_type
elif item.plex_type != v.PLEX_TYPE_CLIP:
with PlexDB(lock=False) as plexdb:
db_element = plexdb.item_by_id(item.plex_id,
plex_type=item.plex_type)
if db_element:
item.kodi_id = db_element['kodi_id']
item.kodi_type = db_element['kodi_type']
item.guid = api.guid_html_escaped()
item.playcount = api.viewcount()
item.offset = api.resume_point()
item.api = api
LOG.debug('Created new playlist item from xml: %s', item)
return item
def _update_playlist_version(playlist, xml):
"""
Takes a PMS xml (one level above the xml-depth where we're usually applying
API()) as input to overwrite the playlist version (e.g. Plex
playQueueVersion).
Raises PlaylistError if unsuccessful
"""
try:
playlist.version = int(xml.get('%sVersion' % playlist.kind))
except (AttributeError, TypeError):
raise PlaylistError('Could not get new playlist Version for playlist '
'%s' % playlist)
def get_playlist_details_from_xml(playlist, xml):
"""
Takes a PMS xml as input and overwrites all the playlist's details, e.g.
playlist.id with the XML's playQueueID
Raises PlaylistError if something went wrong.
"""
if xml is None:
raise PlaylistError('No playlist received for playlist %s' % playlist)
playlist.id = cast(int, xml.get('%sID' % playlist.kind))
playlist.version = cast(int, xml.get('%sVersion' % playlist.kind))
playlist.shuffled = cast(int, xml.get('%sShuffled' % playlist.kind))
playlist.selectedItemID = cast(int, xml.get('%sSelectedItemID'
% playlist.kind))
playlist.selectedItemOffset = cast(int, xml.get('%sSelectedItemOffset'
% playlist.kind))
LOG.debug('Updated playlist from xml: %s', playlist)
def update_playlist_from_PMS(playlist, playlist_id=None, xml=None):
"""
Updates Kodi playlist using a new PMS playlist. Pass in playlist_id if we
need to fetch a new playqueue
If an xml is passed in, the playlist will be overwritten with its info
Raises PlaylistError if something went wront
"""
if xml is None:
xml = get_PMS_playlist(playlist, playlist_id)
# Clear our existing playlist and the associated Kodi playlist
playlist.clear()
# Set new values
get_playlist_details_from_xml(playlist, xml)
for plex_item in xml:
playlist_item = add_to_Kodi_playlist(playlist, plex_item)
if playlist_item is not None:
playlist.items.append(playlist_item)
def init_plex_playqueue(playlist, plex_id=None, kodi_item=None):
"""
Initializes the Plex side without changing the Kodi playlists
WILL ALSO UPDATE OUR PLAYLISTS.
Returns the first PKC playlist item or raises PlaylistError
"""
LOG.debug('Initializing the playqueue on the Plex side: %s', playlist)
verify_kodi_item(plex_id, kodi_item)
playlist.clear(kodi=False)
try:
if plex_id:
item = playlist_item_from_plex(plex_id)
else:
item = playlist_item_from_kodi(kodi_item)
params = {
'next': 0,
'type': playlist.type,
'uri': item.uri,
'includeMarkers': 1, # e.g. start + stop of intros
}
xml = DU().downloadUrl(url="{server}/%ss" % playlist.kind,
action_type="POST",
parameters=params)
if xml in (None, 401):
raise PlaylistError('Did not receive a valid xml from the PMS')
get_playlist_details_from_xml(playlist, xml)
# Need to get the details for the playlist item
item = playlist_item_from_xml(xml[0])
except (KeyError, IndexError, TypeError):
LOG.error('Could not init Plex playlist: plex_id %s, kodi_item %s',
plex_id, kodi_item)
raise PlaylistError
playlist.items.append(item)
LOG.debug('Initialized the playqueue on the Plex side: %s', playlist)
return item
def add_listitem_to_playlist(playlist, pos, listitem, kodi_id=None,
kodi_type=None, plex_id=None, file=None):
"""
Adds a listitem to both the Kodi and Plex playlist at position pos [int].
If file is not None, file will overrule kodi_id!
file: str!!
"""
LOG.debug('add_listitem_to_playlist at position %s. Playlist before add: '
'%s', pos, playlist)
kodi_item = {'id': kodi_id, 'type': kodi_type, 'file': file}
if playlist.id is None:
init_plex_playqueue(playlist, plex_id, kodi_item)
else:
add_item_to_plex_playqueue(playlist, pos, plex_id, kodi_item)
if kodi_id is None and playlist.items[pos].kodi_id:
kodi_id = playlist.items[pos].kodi_id
kodi_type = playlist.items[pos].kodi_type
if file is None:
file = playlist.items[pos].file
# Otherwise we double the item!
del playlist.items[pos]
kodi_item = {'id': kodi_id, 'type': kodi_type, 'file': file}
add_listitem_to_Kodi_playlist(playlist,
pos,
listitem,
file,
kodi_item=kodi_item)
def add_item_to_playlist(playlist, pos, kodi_id=None, kodi_type=None,
plex_id=None, file=None):
"""
Adds an item to BOTH the Kodi and Plex playlist at position pos [int]
file: str!
Raises PlaylistError if something went wrong
"""
LOG.debug('add_item_to_playlist. Playlist before adding: %s', playlist)
kodi_item = {'id': kodi_id, 'type': kodi_type, 'file': file}
if playlist.id is None:
item = init_plex_playqueue(playlist, plex_id, kodi_item)
else:
item = add_item_to_plex_playqueue(playlist, pos, plex_id, kodi_item)
params = {
'playlistid': playlist.playlistid,
'position': pos
}
if item.kodi_id is not None:
params['item'] = {'%sid' % item.kodi_type: int(item.kodi_id)}
else:
params['item'] = {'file': item.file}
reply = js.playlist_insert(params)
if reply.get('error') is not None:
raise PlaylistError('Could not add item to playlist. Kodi reply. %s'
% reply)
return item
def add_item_to_plex_playqueue(playlist, pos, plex_id=None, kodi_item=None):
"""
Adds a new item to the playlist at position pos [int] only on the Plex
side of things (e.g. because the user changed the Kodi side)
WILL ALSO UPDATE OUR PLAYLISTS
Returns the PKC PlayList item or raises PlaylistError
"""
verify_kodi_item(plex_id, kodi_item)
if plex_id:
item = playlist_item_from_plex(plex_id)
else:
item = playlist_item_from_kodi(kodi_item)
url = "{server}/%ss/%s" % (playlist.kind, playlist.id)
parameters = {
'uri': item.uri,
'includeMarkers': 1, # e.g. start + stop of intros
}
# Will always put the new item at the end of the Plex playlist
xml = DU().downloadUrl(url,
action_type="PUT",
parameters=parameters)
try:
xml[-1].attrib
except (TypeError, AttributeError, KeyError, IndexError):
raise PlaylistError('Could not add item %s to playlist %s'
% (kodi_item, playlist))
api = API(xml[-1])
item.api = api
item.id = api.item_id()
item.guid = api.guid_html_escaped()
item.offset = api.resume_point()
item.playcount = api.viewcount()
playlist.items.append(item)
if pos == len(playlist.items) - 1:
# Item was added at the end
_update_playlist_version(playlist, xml)
else:
# Move the new item to the correct position
move_playlist_item(playlist,
len(playlist.items) - 1,
pos)
LOG.debug('Successfully added item on the Plex side: %s', playlist)
return item
def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None,
file=None, xml_video_element=None):
"""
Adds an item to the KODI playlist only. WILL ALSO UPDATE OUR PLAYLISTS
Returns the playlist item that was just added or raises PlaylistError
file: str!
"""
LOG.debug('Adding new item kodi_id: %s, kodi_type: %s, file: %s to Kodi '
'only at position %s for %s',
kodi_id, kodi_type, file, pos, playlist)
params = {
'playlistid': playlist.playlistid,
'position': pos
}
if kodi_id is not None:
params['item'] = {'%sid' % kodi_type: int(kodi_id)}
else:
params['item'] = {'file': file}
reply = js.playlist_insert(params)
if reply.get('error') is not None:
raise PlaylistError('Could not add item to playlist. Kodi reply. %s',
reply)
if xml_video_element is not None:
item = playlist_item_from_xml(xml_video_element)
item.kodi_id = kodi_id
item.kodi_type = kodi_type
item.file = file
elif kodi_id is not None:
item = playlist_item_from_kodi(
{'id': kodi_id, 'type': kodi_type, 'file': file})
if item.plex_id is not None:
xml = PF.GetPlexMetadata(item.plex_id)
item.api = API(xml[-1])
playlist.items.insert(pos, item)
return item
def move_playlist_item(playlist, before_pos, after_pos):
"""
Moves playlist item from before_pos [int] to after_pos [int] for Plex only.
WILL ALSO CHANGE OUR PLAYLISTS.
"""
LOG.debug('Moving item from %s to %s on the Plex side for %s',
before_pos, after_pos, playlist)
if after_pos == 0:
url = "{server}/%ss/%s/items/%s/move?after=0" % \
(playlist.kind,
playlist.id,
playlist.items[before_pos].id)
else:
url = "{server}/%ss/%s/items/%s/move?after=%s" % \
(playlist.kind,
playlist.id,
playlist.items[before_pos].id,
playlist.items[after_pos - 1].id)
# Tell the PMS that we're moving items around
xml = DU().downloadUrl(url, action_type="PUT")
# We need to increment the playlist version for communicating with the PMS
_update_playlist_version(playlist, xml)
# Move our item's position in our internal playlist
playlist.items.insert(after_pos, playlist.items.pop(before_pos))
LOG.debug('Done moving for %s', playlist)
def get_PMS_playlist(playlist, playlist_id=None):
"""
Fetches the PMS playlist/playqueue as an XML. Pass in playlist_id if we
need to fetch a new playlist
Raises PlaylistError if something went wrong
"""
playlist_id = playlist_id if playlist_id else playlist.id
parameters = {'includeMarkers': 1}
if playlist.kind == 'playList':
xml = DU().downloadUrl("{server}/playlists/%s/items" % playlist_id,
parameters=parameters)
else:
xml = DU().downloadUrl("{server}/playQueues/%s" % playlist_id,
parameters=parameters)
try:
xml.attrib
except AttributeError:
raise PlaylistError('Did not get a valid xml')
return xml
def refresh_playlist_from_PMS(playlist):
"""
Only updates the selected item from the PMS side (e.g.
playQueueSelectedItemID). Will NOT check whether items still make sense.
"""
get_playlist_details_from_xml(playlist, get_PMS_playlist(playlist))
def delete_playlist_item_from_PMS(playlist, pos):
"""
Delete the item at position pos [int] on the Plex side and our playlists
"""
LOG.debug('Deleting position %s for %s on the Plex side', pos, playlist)
xml = DU().downloadUrl("{server}/%ss/%s/items/%s?repeat=%s" %
(playlist.kind,
playlist.id,
playlist.items[pos].id,
playlist.repeat),
action_type="DELETE")
del playlist.items[pos]
_update_playlist_version(playlist, xml)
# Functions operating on the Kodi playlist objects ##########
def add_to_Kodi_playlist(playlist, xml_video_element):
"""
Adds a new item to the Kodi playlist via JSON (at the end of the playlist).
Pass in the PMS xml's video element (one level underneath MediaContainer).
Returns a Playlist_Item or raises PlaylistError
"""
item = playlist_item_from_xml(xml_video_element)
if item.kodi_id:
json_item = {'%sid' % item.kodi_type: item.kodi_id}
else:
json_item = {'file': item.file}
reply = js.playlist_add(playlist.playlistid, json_item)
if reply.get('error') is not None:
raise PlaylistError('Could not add item %s to Kodi playlist. Error: '
'%s', xml_video_element, reply)
return item
def add_listitem_to_Kodi_playlist(playlist, pos, listitem, file,
xml_video_element=None, kodi_item=None):
"""
Adds an xbmc listitem to the Kodi playlist.xml_video_element
WILL NOT UPDATE THE PLEX SIDE, BUT WILL UPDATE OUR PLAYLISTS
file: string!
"""
LOG.debug('Insert listitem at position %s for Kodi only for %s',
pos, playlist)
# Add the item into Kodi playlist
playlist.kodi_pl.add(url=file, listitem=listitem, index=pos)
# We need to add this to our internal queue as well
if xml_video_element is not None:
item = playlist_item_from_xml(xml_video_element)
else:
item = playlist_item_from_kodi(kodi_item)
if file is not None:
item.file = file
playlist.items.insert(pos, item)
LOG.debug('Done inserting for %s', playlist)
return item
def remove_from_kodi_playlist(playlist, pos):
"""
Removes the item at position pos from the Kodi playlist using JSON.
WILL NOT UPDATE THE PLEX SIDE, BUT WILL UPDATE OUR PLAYLISTS
"""
LOG.debug('Removing position %s from Kodi only from %s', pos, playlist)
reply = js.playlist_remove(playlist.playlistid, pos)
if reply.get('error') is not None:
LOG.error('Could not delete the item from the playlist. Error: %s',
reply)
return
try:
del playlist.items[pos]
except IndexError:
LOG.error('Cannot delete position %s for %s', pos, playlist)
def get_pms_playqueue(playqueue_id):
"""
Returns the Plex playqueue as an etree XML or None if unsuccessful
"""
parameters = {'includeMarkers': 1}
xml = DU().downloadUrl("{server}/playQueues/%s" % playqueue_id,
parameters=parameters,
headerOptions={'Accept': 'application/xml'})
try:
xml.attrib
except AttributeError:
LOG.error('Could not download Plex playqueue %s', playqueue_id)
xml = None
return xml
def get_plextype_from_xml(xml):
"""
Needed if PMS returns an empty playqueue. Will get the Plex type from the
empty playlist playQueueSourceURI. Feed with (empty) etree xml
returns None if unsuccessful
"""
try:
plex_id = utils.REGEX_PLEX_ID_FROM_URL.findall(
xml.attrib['playQueueSourceURI'])[0]
except IndexError:
LOG.error('Could not get plex_id from xml: %s', xml.attrib)
return
new_xml = PF.GetPlexMetadata(plex_id)
try:
new_xml[0].attrib
except (TypeError, IndexError, AttributeError):
LOG.error('Could not get plex metadata for plex id %s', plex_id)
return
return new_xml[0].attrib.get('type').decode('utf-8')

View File

@ -13,15 +13,13 @@
"""
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from sqlite3 import OperationalError
from .common import Playlist, PlaylistObserver, kodi_playlist_hash
from .common import Playlist, PlaylistError, PlaylistObserver
from . import pms, db, kodi_pl, plex_pl
from ..watchdog import events
from ..plex_api import API
from .. import utils, path_ops, variables as v, app
from ..exceptions import PlaylistError
###############################################################################
LOG = getLogger('PLEX.playlists')
@ -39,7 +37,7 @@ SUPPORTED_FILETYPES = (
###############################################################################
def should_cancel():
def isCanceled():
return app.APP.stop_pkc or app.SYNC.stop_sync
@ -69,22 +67,6 @@ def kodi_playlist_monitor():
return observer
def remove_synced_playlists():
"""
Deletes all synched playlists on the Kodi side, not on the Plex side
"""
LOG.info('Removing all playlists that we synced to Kodi')
with app.APP.lock_playlists:
try:
paths = db.get_all_kodi_playlist_paths()
except OperationalError:
LOG.info('Playlists table has not yet been set-up')
return
kodi_pl.delete_kodi_playlists(paths)
db.wipe_table()
LOG.info('Done removing all synced playlists')
def websocket(plex_id, status):
"""
Call this function to process websocket messages from the PMS
@ -184,39 +166,39 @@ def _full_sync():
# before. If yes, make sure that hashes are identical. If not, sync it.
old_plex_ids = db.plex_playlist_ids()
for xml_playlist in xml:
if should_cancel():
if isCanceled():
return False
api = API(xml_playlist)
try:
old_plex_ids.remove(api.plex_id)
old_plex_ids.remove(api.plex_id())
except ValueError:
pass
if not sync_plex_playlist(xml=xml_playlist):
continue
playlist = db.get_playlist(plex_id=api.plex_id)
playlist = db.get_playlist(plex_id=api.plex_id())
if not playlist:
LOG.debug('New Plex playlist %s discovered: %s',
api.plex_id, api.title())
api.plex_id(), api.title())
try:
kodi_pl.create(api.plex_id)
kodi_pl.create(api.plex_id())
except PlaylistError:
LOG.info('Skipping creation of playlist %s', api.plex_id)
LOG.info('Skipping creation of playlist %s', api.plex_id())
elif playlist.plex_updatedat != api.updated_at():
LOG.debug('Detected changed Plex playlist %s: %s',
api.plex_id, api.title())
api.plex_id(), api.title())
# Since we are DELETING a playlist, we need to catch with path!
try:
kodi_pl.delete(playlist)
except PlaylistError:
LOG.info('Skipping recreation of playlist %s', api.plex_id)
LOG.info('Skipping recreation of playlist %s', api.plex_id())
else:
try:
kodi_pl.create(api.plex_id)
kodi_pl.create(api.plex_id())
except PlaylistError:
LOG.info('Could not recreate playlist %s', api.plex_id)
LOG.info('Could not recreate playlist %s', api.plex_id())
# Get rid of old Plex playlists that were deleted on the Plex side
for plex_id in old_plex_ids:
if should_cancel():
if isCanceled():
return False
playlist = db.get_playlist(plex_id=plex_id)
LOG.debug('Removing outdated Plex playlist from Kodi: %s', playlist)
@ -230,7 +212,7 @@ def _full_sync():
old_kodi_paths = db.kodi_playlist_paths()
for root, _, files in path_ops.walk(v.PLAYLIST_PATH):
for f in files:
if should_cancel():
if isCanceled():
return False
path = path_ops.path.join(root, f)
try:
@ -239,7 +221,7 @@ def _full_sync():
pass
if not sync_kodi_playlist(path):
continue
kodi_hash = kodi_playlist_hash(path)
kodi_hash = utils.generate_file_md5(path)
playlist = db.get_playlist(path=path)
if playlist and playlist.kodi_hash == kodi_hash:
continue
@ -261,7 +243,7 @@ def _full_sync():
except PlaylistError:
LOG.info('Skipping Kodi playlist %s', path)
for kodi_path in old_kodi_paths:
if should_cancel():
if isCanceled():
return False
playlist = db.get_playlist(path=kodi_path)
if not playlist:
@ -357,10 +339,6 @@ def sync_plex_playlist(playlist=None, xml=None, plex_id=None):
if api.playlist_type() == v.PLEX_TYPE_PHOTO_PLAYLIST:
# Not supported by Kodi
return False
elif api.playlist_type() is None:
# Encountered in logs, seems to be a malformed answer
LOG.error('Playlist type is missing: %s', api.xml.attrib)
return False
name = api.title()
typus = v.KODI_PLAYLIST_TYPE_FROM_PLEX[api.playlist_type()]
if (not app.SYNC.enable_music and typus == v.PLEX_PLAYLIST_TYPE_AUDIO):
@ -391,25 +369,25 @@ class PlaylistEventhandler(events.FileSystemEventHandler):
"""
path = event.dest_path if event.event_type == events.EVENT_TYPE_MOVED \
else event.src_path
if not sync_kodi_playlist(path):
return
if path in kodi_pl.IGNORE_KODI_PLAYLIST_CHANGE:
LOG.debug('Ignoring event %s', event)
kodi_pl.IGNORE_KODI_PLAYLIST_CHANGE.remove(path)
return
_method_map = {
events.EVENT_TYPE_MODIFIED: self.on_modified,
events.EVENT_TYPE_MOVED: self.on_moved,
events.EVENT_TYPE_CREATED: self.on_created,
events.EVENT_TYPE_DELETED: self.on_deleted,
}
with app.APP.lock_playlists:
if not sync_kodi_playlist(path):
return
if path in kodi_pl.IGNORE_KODI_PLAYLIST_CHANGE:
LOG.debug('Ignoring event %s', event)
kodi_pl.IGNORE_KODI_PLAYLIST_CHANGE.remove(path)
return
_method_map = {
events.EVENT_TYPE_MODIFIED: self.on_modified,
events.EVENT_TYPE_MOVED: self.on_moved,
events.EVENT_TYPE_CREATED: self.on_created,
events.EVENT_TYPE_DELETED: self.on_deleted,
}
_method_map[event.event_type](event)
def on_created(self, event):
LOG.debug('on_created: %s', event.src_path)
old_playlist = db.get_playlist(path=event.src_path)
kodi_hash = kodi_playlist_hash(event.src_path)
kodi_hash = utils.generate_file_md5(event.src_path)
if old_playlist and old_playlist.kodi_hash == kodi_hash:
LOG.debug('Playlist already in DB - skipping')
return
@ -428,7 +406,7 @@ class PlaylistEventhandler(events.FileSystemEventHandler):
def on_modified(self, event):
LOG.debug('on_modified: %s', event.src_path)
old_playlist = db.get_playlist(path=event.src_path)
kodi_hash = kodi_playlist_hash(event.src_path)
kodi_hash = utils.generate_file_md5(event.src_path)
if old_playlist and old_playlist.kodi_hash == kodi_hash:
LOG.debug('Nothing modified, playlist already in DB - skipping')
return
@ -447,7 +425,7 @@ class PlaylistEventhandler(events.FileSystemEventHandler):
def on_moved(self, event):
LOG.debug('on_moved: %s to %s', event.src_path, event.dest_path)
kodi_hash = kodi_playlist_hash(event.dest_path)
kodi_hash = utils.generate_file_md5(event.dest_path)
# First check whether we don't already have destination playlist in
# our DB. Just in case....
old_playlist = db.get_playlist(path=event.dest_path)

View File

@ -4,16 +4,12 @@ from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
import Queue
import time
import os
import hashlib
from ..watchdog import events
from ..watchdog.observers import Observer
from ..watchdog.utils.bricks import OrderedSetQueue
from .. import path_ops, variables as v, app
from ..exceptions import PlaylistError
###############################################################################
LOG = getLogger('PLEX.playlists.common')
@ -22,6 +18,13 @@ SIMILAR_EVENTS = (events.EVENT_TYPE_CREATED, events.EVENT_TYPE_MODIFIED)
###############################################################################
class PlaylistError(Exception):
"""
The one main exception thrown if anything goes awry
"""
pass
class Playlist(object):
"""
Class representing a synced Playlist with info for both Kodi and Plex.
@ -60,7 +63,7 @@ class Playlist(object):
self.kodi_type = None
self.kodi_hash = None
def __unicode__(self):
def __repr__(self):
return ("{{"
"'plex_id': {self.plex_id}, "
"'plex_name': '{self.plex_name}', "
@ -71,9 +74,6 @@ class Playlist(object):
"'kodi_hash': '{self.kodi_hash}'"
"}}").format(self=self)
def __repr__(self):
return self.__unicode__().encode('utf-8')
def __str__(self):
return self.__repr__()
@ -101,9 +101,11 @@ class Playlist(object):
@kodi_path.setter
def kodi_path(self, path):
if not isinstance(path, unicode):
raise RuntimeError('Path not in unicode: %s' % path)
f = path_ops.path.basename(path)
try:
self.kodi_filename, self.kodi_extension = f.rsplit('.', 1)
self.kodi_filename, self.kodi_extension = f.split('.', 1)
except ValueError:
LOG.error('Trying to set invalid path: %s', path)
raise PlaylistError('Invalid path: %s' % path)
@ -119,22 +121,6 @@ class Playlist(object):
self._kodi_path = path
def kodi_playlist_hash(path):
"""
Returns a md5 hash [unicode] using os.stat() st_size and st_mtime for the
playlist located at path [unicode]
(size of file in bytes and time of most recent content modification)
There are probably way more efficient ways out there to do this
"""
stat = os.stat(path_ops.encode_path(path))
# stat.st_size is of type long; stat.st_mtime is of type float - hash both
m = hashlib.md5()
m.update(repr(stat.st_size))
m.update(repr(stat.st_mtime))
return m.hexdigest().decode('utf-8')
class PlaylistQueue(OrderedSetQueue):
"""
OrderedSetQueue that drops all directory events immediately

View File

@ -7,11 +7,10 @@ module
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from .common import Playlist
from .common import Playlist, PlaylistError
from ..plex_db import PlexDB
from ..kodi_db import kodiid_from_filename
from .. import path_ops, utils, variables as v
from ..exceptions import PlaylistError
###############################################################################
LOG = getLogger('PLEX.playlists.db')
@ -48,45 +47,27 @@ def update_playlist(playlist, delete=False):
plexdb.add_playlist(playlist)
def get_playlist(path=None, plex_id=None):
def get_playlist(path=None, kodi_hash=None, plex_id=None):
"""
Returns the playlist as a Playlist for either the plex_id or path
Returns the playlist as a Playlist for either the plex_id, path or
kodi_hash. kodi_hash will be more reliable as it includes path and file
content.
"""
playlist = Playlist()
with PlexDB() as plexdb:
playlist = plexdb.playlist(playlist, plex_id, path)
playlist = plexdb.playlist(playlist, plex_id, path, kodi_hash)
return playlist
def get_all_kodi_playlist_paths():
"""
Returns a list with all paths for the playlists on the Kodi side
"""
with PlexDB() as plexdb:
paths = list(plexdb.all_kodi_paths())
return paths
def wipe_table():
"""
Deletes all playlists entries in the Plex DB
"""
with PlexDB() as plexdb:
plexdb.wipe_playlists()
def _m3u_iterator(text):
"""
Yields e.g. plugin://plugin.video.plexkodiconnect.movies/?plex_id=xxx
Yields e.g.
http://127.0.0.1:<port>/plex/kodi/movies/file.strm?plex_id=...
"""
lines = iter(text.split('\n'))
for line in lines:
if line.startswith('#EXTINF:'):
next_line = next(lines).strip()
if next_line.startswith('#EXT-KX-OFFSET:'):
yield next(lines).strip()
else:
yield next_line
yield next(lines).strip()
def m3u_to_plex_ids(playlist):
@ -122,7 +103,7 @@ def m3u_to_plex_ids(playlist):
def playlist_file_to_plex_ids(playlist):
"""
Takes the playlist file located at path [unicode] and parses it.
Returns a list of plex_ids (str) or raises PlaylistError if a single
Returns a list of plex_ids (str) or raises PL.PlaylistError if a single
item cannot be parsed from Kodi to Plex.
"""
if playlist.kodi_extension == 'm3u':

View File

@ -7,13 +7,11 @@ from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
import re
from .common import Playlist, kodi_playlist_hash
from .common import Playlist, PlaylistError
from . import db, pms
from ..plex_api import API
from .. import utils, path_ops, variables as v
from ..exceptions import PlaylistError
###############################################################################
LOG = getLogger('PLEX.playlists.kodi_pl')
REGEX_FILE_NUMBERING = re.compile(r'''_(\d\d)\.\w+$''')
@ -37,7 +35,7 @@ def create(plex_id):
raise PlaylistError('Could not get Plex playlist %s' % plex_id)
api = API(xml_metadata[0])
playlist = Playlist()
playlist.plex_id = api.plex_id
playlist.plex_id = api.plex_id()
playlist.kodi_type = v.KODI_PLAYLIST_TYPE_FROM_PLEX[api.playlist_type()]
playlist.plex_name = api.title()
playlist.plex_updatedat = api.updated_at()
@ -73,7 +71,7 @@ def create(plex_id):
except Exception:
IGNORE_KODI_PLAYLIST_CHANGE.remove(playlist.kodi_path)
raise
playlist.kodi_hash = kodi_playlist_hash(path)
playlist.kodi_hash = utils.generate_file_md5(path)
db.update_playlist(playlist)
LOG.debug('Created Kodi playlist based on Plex playlist: %s', playlist)
@ -98,29 +96,35 @@ def delete(playlist):
db.update_playlist(playlist, delete=True)
def delete_kodi_playlists(playlist_paths):
"""
Deletes all the the playlist files passed in; WILL IGNORE THIS CHANGE ON
THE PLEX SIDE!
"""
for path in playlist_paths:
try:
path_ops.remove(path)
# Ensure we're not deleting the playlists on the Plex side later
IGNORE_KODI_PLAYLIST_CHANGE.append(path)
LOG.info('Removed playlist %s', path)
except (OSError, IOError):
LOG.warn('Could not remove playlist %s', path)
def _write_playlist_to_file(playlist, xml):
"""
Feed with playlist Playlist. Will write the playlist to a m3u file
Returns None or raises PlaylistError
"""
text = '#EXTCPlayListM3U::M3U\n'
for xml_element in xml:
text += _m3u_element(xml_element)
for element in xml:
api = API(element)
append_season_episode = False
if api.plex_type() == v.PLEX_TYPE_EPISODE:
_, _, show, season_no, episode_no = api.episode_data()
try:
season_no = int(season_no)
episode_no = int(episode_no)
except ValueError:
pass
else:
append_season_episode = True
if append_season_episode:
text += ('#EXTINF:%s,%s S%.2dE%.2d - %s\n%s\n'
% (api.runtime(), show, season_no, episode_no,
api.title(), api.path()))
else:
# Only append the TV show name
text += ('#EXTINF:%s,%s - %s\n%s\n'
% (api.runtime(), show, api.title(), api.path()))
else:
text += ('#EXTINF:%s,%s\n%s\n'
% (api.runtime(), api.title(), api.path()))
text += '\n'
text = text.encode(v.M3U_ENCODING, 'ignore')
try:
@ -131,45 +135,3 @@ def _write_playlist_to_file(playlist, xml):
LOG.error('Error message %s: %s', err.errno, err.strerror)
raise PlaylistError('Cannot write Kodi playlist to path for %s'
% playlist)
def _m3u_element(xml_element):
api = API(xml_element)
if api.plex_type == v.PLEX_TYPE_EPISODE:
if api.season_number() is not None and api.index() is not None:
return '#EXTINF:{},{} S{:2d}E{:2d} - {}\n{}\n'.format(
api.runtime(),
api.show_title(),
api.season_number(),
api.index(),
api.title(),
api.fullpath(force_addon=True)[0])
else:
# Only append the TV show name
return '#EXTINF:{},{} - {}\n{}\n'.format(
api.runtime(),
api.show_title(),
api.title(),
api.fullpath(force_addon=True)[0])
elif api.plex_type == v.PLEX_TYPE_SONG:
if api.index() is not None:
return '#EXTINF:{},{:02d}. {} - {}\n{}\n'.format(
api.runtime(),
api.index(),
api.grandparent_title(),
api.title(),
api.fullpath(force_first_media=True,
omit_check=True)[0])
else:
return '#EXTINF:{},{} - {}\n{}\n'.format(
api.runtime(),
api.grandparent_title(),
api.title(),
api.fullpath(force_first_media=True,
omit_check=True)[0])
else:
return '#EXTINF:{},{}\n{}\n'.format(
api.runtime(),
api.title(),
api.fullpath(force_first_media=True,
omit_check=True)[0])

View File

@ -6,9 +6,8 @@ Create and delete playlists on the Plex side of things
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from .common import PlaylistError
from . import pms, db
from ..exceptions import PlaylistError
###############################################################################
LOG = getLogger('PLEX.playlists.plex_pl')
# Used for updating Plex playlists due to Kodi changes - Plex playlist
@ -31,8 +30,8 @@ def create(playlist):
if not plex_ids:
LOG.warning('No Plex ids found for playlist %s', playlist)
raise PlaylistError
pms.add_items(playlist, plex_ids)
IGNORE_PLEX_PLAYLIST_CHANGE.append(playlist.plex_id)
pms.add_items(playlist, plex_ids)
db.update_playlist(playlist)
LOG.debug('Done creating Plex playlist %s', playlist)

View File

@ -7,11 +7,11 @@ manipulate playlists
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from .common import PlaylistError
from ..plex_api import API
from ..downloadutils import DownloadUtils as DU
from .. import utils, app, variables as v
from ..exceptions import PlaylistError
###############################################################################
LOG = getLogger('PLEX.playlists.pms')
@ -68,7 +68,7 @@ def initialize(playlist, plex_id):
plex_id)
raise PlaylistError('Could not initialize Plex playlist %s', plex_id)
api = API(xml[0])
playlist.plex_id = api.plex_id
playlist.plex_id = api.plex_id()
playlist.plex_updatedat = api.updated_at()
@ -121,7 +121,7 @@ def add_items(playlist, plex_ids):
raise PlaylistError('Could not add items to a new Plex playlist %s' %
playlist)
api = API(xml[0])
playlist.plex_id = api.plex_id
playlist.plex_id = api.plex_id()
playlist.plex_updatedat = api.updated_at()

View File

@ -0,0 +1,14 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly
"""
from __future__ import absolute_import, division, unicode_literals
from .common import PlaylistItem, PlaylistItemDummy, PlayqueueError, PLAYQUEUES
from .playqueue import PlayQueue
from .monitor import PlayqueueMonitor
from .functions import init_playqueues, get_playqueue_from_type, \
playqueue_from_plextype, playqueue_from_id, get_PMS_playlist, \
init_playqueue_from_plex_children, get_pms_playqueue, \
get_plextype_from_xml

View File

@ -0,0 +1,302 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from ..plex_db import PlexDB
from ..plex_api import API
from .. import plex_functions as PF, utils, kodi_db, variables as v, app
# Our PKC playqueues (3 instances of PlayQueue())
PLAYQUEUES = []
class PlayqueueError(Exception):
"""
Exception for our playqueue constructs
"""
pass
class PlaylistItem(object):
"""
Object to fill our playqueues and playlists with.
id = None [int] Plex playlist/playqueue id, e.g. playQueueItemID
plex_id = None [int] Plex unique item id, "ratingKey"
plex_type = None [str] Plex type, e.g. 'movie', 'clip'
plex_uuid = None [str] Plex librarySectionUUID
kodi_id = None [int] Kodi unique kodi id (unique only within type!)
kodi_type = None [str] Kodi type: 'movie'
file = None [str] Path to the item's file. STRING!!
uri = None [str] Weird Plex uri path involving plex_uuid. STRING!
guid = None [str] Weird Plex guid
xml = None [etree] XML from PMS, 1 lvl below <MediaContainer>
playmethod = None [str] either 'DirectPlay', 'DirectStream', 'Transcode'
playcount = None [int] how many times the item has already been played
offset = None [int] the item's view offset UPON START in Plex time
part = 0 [int] part number if Plex video consists of mult. parts
force_transcode [bool] defaults to False
PlaylistItem compare as equal, if they
- have the same plex_id
- OR: have the same kodi_id AND kodi_type
- OR: have the same file
"""
def __init__(self, plex_id=None, plex_type=None, xml_video_element=None,
kodi_id=None, kodi_type=None, kodi_item=None, grab_xml=False,
lookup_kodi=True):
"""
Pass grab_xml=True in order to get Plex metadata from the PMS while
passing a plex_id.
Pass lookup_kodi=False to NOT check the plex.db for kodi_id and
kodi_type if they're missing (won't be done for clips anyway)
"""
self.name = None
self.id = None
self.plex_id = plex_id
self.plex_type = plex_type
self.plex_uuid = None
self.kodi_id = kodi_id
self.kodi_type = kodi_type
self.file = None
if kodi_item:
self.kodi_id = utils.cast(int, kodi_item.get('id'))
self.kodi_type = kodi_item.get('type')
self.file = kodi_item.get('file')
self.uri = None
self.guid = None
self.xml = None
self.playmethod = None
self.playcount = None
self.offset = None
self.part = 0
self.force_transcode = False
# Shall we ask user to resume this item?
# None: ask user to resume
# False: do NOT resume, don't ask user
# True: do resume, don't ask user
self.resume = None
if self.plex_id is None:
self._from_plex_db()
if grab_xml and self.plex_id is not None and xml_video_element is None:
xml_video_element = PF.GetPlexMetadata(plex_id)
try:
xml_video_element = xml_video_element[0]
except (TypeError, IndexError):
xml_video_element = None
if xml_video_element is not None:
self.from_xml(xml_video_element)
if (lookup_kodi and (self.kodi_id is None or self.kodi_type is None) and
self.plex_type != v.PLEX_TYPE_CLIP):
with PlexDB(lock=False) as plexdb:
db_item = plexdb.item_by_id(self.plex_id, self.plex_type)
if db_item is not None:
self.kodi_id = db_item['kodi_id']
self.kodi_type = db_item['kodi_type']
self.plex_type = db_item['plex_type']
self.plex_uuid = db_item['section_uuid']
if (lookup_kodi and (self.kodi_id is None or self.kodi_type is None) and
self.plex_type != v.PLEX_TYPE_CLIP):
self._guess_id_from_file()
self._from_plex_db()
self._set_uri()
def __eq__(self, other):
if self.plex_id is not None and other.plex_id is not None:
return self.plex_id == other.plex_id
elif (self.kodi_id is not None and other.kodi_id is not None and
self.kodi_type and other.kodi_type):
return (self.kodi_id == other.kodi_id and
self.kodi_type == other.kodi_type)
elif self.file and other.file:
return self.file == other.file
raise RuntimeError('PlaylistItems not fully defined: %s, %s' %
(self, other))
def __ne__(self, other):
return not self == other
def __unicode__(self):
return ("{{"
"'name': '{self.name}', "
"'id': {self.id}, "
"'plex_id': {self.plex_id}, "
"'plex_type': '{self.plex_type}', "
"'kodi_id': {self.kodi_id}, "
"'kodi_type': '{self.kodi_type}', "
"'file': '{self.file}', "
"'uri': '{self.uri}', "
"'guid': '{self.guid}', "
"'playmethod': '{self.playmethod}', "
"'playcount': {self.playcount}, "
"'offset': {self.offset}, "
"'force_transcode': {self.force_transcode}, "
"'part': {self.part}"
"}}".format(self=self))
def __str__(self):
return unicode(self).encode('utf-8')
__repr__ = __str__
def _from_plex_db(self):
"""
Uses self.kodi_id and self.kodi_type to look up the item in the Plex
DB. Thus potentially sets self.plex_id, plex_type, plex_uuid
"""
if self.kodi_id is None or not self.kodi_type:
return
with PlexDB(lock=False) as plexdb:
db_item = plexdb.item_by_kodi_id(self.kodi_id, self.kodi_type)
if db_item:
self.plex_id = db_item['plex_id']
self.plex_type = db_item['plex_type']
self.plex_uuid = db_item['section_uuid']
def from_xml(self, xml_video_element):
"""
xml_video_element: etree xml piece 1 level underneath <MediaContainer>
item.id will only be set if you passed in an xml_video_element from
e.g. a playQueue
"""
api = API(xml_video_element)
self.name = api.title()
self.plex_id = api.plex_id()
self.plex_type = api.plex_type()
self.id = api.item_id()
self.guid = api.guid_html_escaped()
self.playcount = api.viewcount()
self.offset = api.resume_point()
self.xml = xml_video_element
if self.kodi_id is None or not self.kodi_type:
self._from_plex_db()
self._set_uri()
def from_kodi(self, playlist_item):
"""
playlist_item: dict contains keys 'id', 'type', 'file' (if applicable)
Will thus set the attributes kodi_id, kodi_type, file, if applicable
If kodi_id & kodi_type are provided, plex_id and plex_type will be
looked up (if not already set)
"""
self.kodi_id = utils.cast(int, playlist_item.get('id'))
self.kodi_type = playlist_item.get('type')
self.file = playlist_item.get('file')
if self.plex_id is None and self.kodi_id is not None and self.kodi_type:
self._from_plex_db()
if self.plex_id is None and self.file:
try:
query = self.file.split('?', 1)[1]
except IndexError:
query = ''
query = dict(utils.parse_qsl(query))
self.plex_id = utils.cast(int, query.get('plex_id'))
self.plex_type = query.get('itemType')
self._set_uri()
def _set_uri(self):
if self.plex_id is None and self.file is not None:
self.uri = ('library://whatever/item/%s'
% utils.quote(self.file, safe=''))
elif self.plex_id is not None and self.plex_uuid is not None:
self.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' %
(self.plex_uuid, self.plex_id))
elif self.plex_id is not None:
self.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' %
(self.plex_id, self.plex_id))
else:
self.uri = None
def _guess_id_from_file(self):
"""
If self.file is set, will try to guess kodi_id and kodi_type from the
filename and path using the Kodi video and music databases
"""
if not self.file:
return
# Special case playlist startup - got type but no id
if (not app.SYNC.direct_paths and app.SYNC.enable_music and
self.kodi_type == v.KODI_TYPE_SONG and
self.file.startswith('http')):
self.kodi_id, _ = kodi_db.kodiid_from_filename(self.file,
v.KODI_TYPE_SONG)
return
# Need more info since we don't have kodi_id nor type. Use file path.
if (self.file.startswith('plugin') or
(self.file.startswith('http') and not
self.file.startswith('http://127.0.0.1:%s' % v.WEBSERVICE_PORT))):
return
# Try the VIDEO DB first - will find both movies and episodes
self.kodi_id, self.kodi_type = kodi_db.kodiid_from_filename(
self.file, db_type='video')
if self.kodi_id is None:
# No movie or episode found - try MUSIC DB now for songs
self.kodi_id, self.kodi_type = kodi_db.kodiid_from_filename(
self.file, db_type='music')
self.kodi_type = None if self.kodi_id is None else self.kodi_type
def plex_stream_index(self, kodi_stream_index, stream_type):
"""
Pass in the kodi_stream_index [int] in order to receive the Plex stream
index.
stream_type: 'video', 'audio', 'subtitle'
Returns None if unsuccessful
"""
stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type]
count = 0
if kodi_stream_index == -1:
# Kodi telling us "it's the last one"
iterator = list(reversed(self.xml[0][self.part]))
kodi_stream_index = 0
else:
iterator = self.xml[0][self.part]
# Kodi indexes differently than Plex
for stream in iterator:
if (stream.attrib['streamType'] == stream_type and
'key' in stream.attrib):
if count == kodi_stream_index:
return stream.attrib['id']
count += 1
for stream in iterator:
if (stream.attrib['streamType'] == stream_type and
'key' not in stream.attrib):
if count == kodi_stream_index:
return stream.attrib['id']
count += 1
def kodi_stream_index(self, plex_stream_index, stream_type):
"""
Pass in the kodi_stream_index [int] in order to receive the Plex stream
index.
stream_type: 'video', 'audio', 'subtitle'
Returns None if unsuccessful
"""
stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type]
count = 0
for stream in self.xml[0][self.part]:
if (stream.attrib['streamType'] == stream_type and
'key' in stream.attrib):
if stream.attrib['id'] == plex_stream_index:
return count
count += 1
for stream in self.xml[0][self.part]:
if (stream.attrib['streamType'] == stream_type and
'key' not in stream.attrib):
if stream.attrib['id'] == plex_stream_index:
return count
count += 1
class PlaylistItemDummy(PlaylistItem):
"""
Let e.g. Kodimonitor detect that this is a dummy item
"""
def __init__(self, *args, **kwargs):
super(PlaylistItemDummy, self).__init__(*args, **kwargs)
self.name = 'PKC Dummy playqueue item'
self.id = 0
self.plex_id = 0

View File

@ -0,0 +1,164 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
import xbmc
from .common import PLAYQUEUES, PlaylistItem
from .playqueue import PlayQueue
from ..downloadutils import DownloadUtils as DU
from .. import json_rpc as js, app, variables as v, plex_functions as PF
from .. import utils
LOG = getLogger('PLEX.playqueue_functions')
def init_playqueues():
"""
Call this once on startup to initialize the PKC playqueue objects in
the list PLAYQUEUES
"""
if PLAYQUEUES:
LOG.debug('Playqueues have already been initialized')
return
# Initialize Kodi playqueues
with app.APP.lock_playqueues:
for i in (0, 1, 2):
# Just in case the Kodi response is not sorted correctly
for queue in js.get_playlists():
if queue['playlistid'] != i:
continue
playqueue = PlayQueue()
playqueue.playlistid = i
playqueue.type = queue['type']
# Initialize each Kodi playlist
if playqueue.type == v.KODI_TYPE_AUDIO:
playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
elif playqueue.type == v.KODI_TYPE_VIDEO:
playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
else:
# Currently, only video or audio playqueues available
playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
# Overwrite 'picture' with 'photo'
playqueue.type = v.KODI_TYPE_PHOTO
PLAYQUEUES.append(playqueue)
LOG.debug('Initialized the Kodi playqueues: %s', PLAYQUEUES)
def get_playqueue_from_type(kodi_playlist_type):
"""
Returns the playqueue according to the kodi_playlist_type ('video',
'audio', 'picture') passed in
"""
for playqueue in PLAYQUEUES:
if playqueue.type == kodi_playlist_type:
break
else:
raise ValueError('Wrong playlist type passed in: %s'
% kodi_playlist_type)
return playqueue
def playqueue_from_plextype(plex_type):
if plex_type in v.PLEX_VIDEOTYPES:
plex_type = v.PLEX_TYPE_VIDEO_PLAYLIST
elif plex_type in v.PLEX_AUDIOTYPES:
plex_type = v.PLEX_TYPE_AUDIO_PLAYLIST
else:
plex_type = v.PLEX_TYPE_VIDEO_PLAYLIST
for playqueue in PLAYQUEUES:
if playqueue.type == plex_type:
break
return playqueue
def playqueue_from_id(kodi_playlist_id):
for playqueue in PLAYQUEUES:
if playqueue.playlistid == kodi_playlist_id:
break
else:
raise ValueError('Wrong playlist id passed in: %s of type %s'
% (kodi_playlist_id, type(kodi_playlist_id)))
return playqueue
def init_playqueue_from_plex_children(plex_id, transient_token=None):
"""
Init a new playqueue e.g. from an album. Alexa does this
Returns the playqueue
"""
xml = PF.GetAllPlexChildren(plex_id)
try:
xml[0].attrib
except (TypeError, IndexError, AttributeError):
LOG.error('Could not download the PMS xml for %s', plex_id)
return
playqueue = get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']])
playqueue.clear()
for i, child in enumerate(xml):
playlistitem = PlaylistItem(xml_video_element=child)
playqueue.add_item(playlistitem, i)
playqueue.plex_transient_token = transient_token
LOG.debug('Firing up Kodi player')
app.APP.player.play(playqueue.kodi_pl, None, False, 0)
return playqueue
def get_PMS_playlist(playlist=None, playlist_id=None):
"""
Fetches the PMS playlist/playqueue as an XML. Pass in playlist_id if we
need to fetch a new playlist
Returns None if something went wrong
"""
playlist_id = playlist_id if playlist_id else playlist.id
if playlist and playlist.kind == 'playList':
xml = DU().downloadUrl("{server}/playlists/%s/items" % playlist_id)
else:
xml = DU().downloadUrl("{server}/playQueues/%s" % playlist_id)
try:
xml.attrib
except AttributeError:
xml = None
return xml
def get_pms_playqueue(playqueue_id):
"""
Returns the Plex playqueue as an etree XML or None if unsuccessful
"""
xml = DU().downloadUrl(
"{server}/playQueues/%s" % playqueue_id,
headerOptions={'Accept': 'application/xml'})
try:
xml.attrib
except AttributeError:
LOG.error('Could not download Plex playqueue %s', playqueue_id)
xml = None
return xml
def get_plextype_from_xml(xml):
"""
Needed if PMS returns an empty playqueue. Will get the Plex type from the
empty playlist playQueueSourceURI. Feed with (empty) etree xml
returns None if unsuccessful
"""
try:
plex_id = utils.REGEX_PLEX_ID_FROM_URL.findall(
xml.attrib['playQueueSourceURI'])[0]
except IndexError:
LOG.error('Could not get plex_id from xml: %s', xml.attrib)
return
new_xml = PF.GetPlexMetadata(plex_id)
try:
new_xml[0].attrib
except (TypeError, IndexError, AttributeError):
LOG.error('Could not get plex metadata for plex id %s', plex_id)
return
return new_xml[0].attrib.get('type').decode('utf-8')

View File

@ -7,95 +7,11 @@ from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
import copy
import xbmc
from .plex_api import API
from . import playlist_func as PL, plex_functions as PF
from . import backgroundthread, utils, json_rpc as js, app, variables as v
from . import exceptions
###############################################################################
LOG = getLogger('PLEX.playqueue')
PLUGIN = 'plugin://%s' % v.ADDON_ID
# Our PKC playqueues (3 instances of Playqueue_Object())
PLAYQUEUES = []
###############################################################################
from .common import PlayqueueError, PlaylistItem, PLAYQUEUES
from .. import backgroundthread, json_rpc as js, utils, app
def init_playqueues():
"""
Call this once on startup to initialize the PKC playqueue objects in
the list PLAYQUEUES
"""
if PLAYQUEUES:
LOG.debug('Playqueues have already been initialized')
return
# Initialize Kodi playqueues
with app.APP.lock_playqueues:
for i in (0, 1, 2):
# Just in case the Kodi response is not sorted correctly
for queue in js.get_playlists():
if queue['playlistid'] != i:
continue
playqueue = PL.Playqueue_Object()
playqueue.playlistid = i
playqueue.type = queue['type']
# Initialize each Kodi playlist
if playqueue.type == v.KODI_TYPE_AUDIO:
playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
elif playqueue.type == v.KODI_TYPE_VIDEO:
playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
else:
# Currently, only video or audio playqueues available
playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
# Overwrite 'picture' with 'photo'
playqueue.type = v.KODI_TYPE_PHOTO
PLAYQUEUES.append(playqueue)
LOG.debug('Initialized the Kodi playqueues: %s', PLAYQUEUES)
def get_playqueue_from_type(kodi_playlist_type):
"""
Returns the playqueue according to the kodi_playlist_type ('video',
'audio', 'picture') passed in
"""
for playqueue in PLAYQUEUES:
if playqueue.type == kodi_playlist_type:
break
else:
raise ValueError('Wrong playlist type passed in: %s',
kodi_playlist_type)
return playqueue
def init_playqueue_from_plex_children(plex_id, transient_token=None):
"""
Init a new playqueue e.g. from an album. Alexa does this
Returns the playqueue
"""
xml = PF.GetAllPlexChildren(plex_id)
try:
xml[0].attrib
except (TypeError, IndexError, AttributeError):
LOG.error('Could not download the PMS xml for %s', plex_id)
return
playqueue = get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']])
playqueue.clear()
for i, child in enumerate(xml):
api = API(child)
try:
PL.add_item_to_playlist(playqueue, i, plex_id=api.plex_id)
except exceptions.PlaylistError:
LOG.error('Could not add Plex item to our playlist: %s, %s',
child.tag, child.attrib)
playqueue.plex_transient_token = transient_token
LOG.debug('Firing up Kodi player')
app.APP.player.play(playqueue.kodi_pl, None, False, 0)
return playqueue
LOG = getLogger('PLEX.playqueue_monitor')
class PlayqueueMonitor(backgroundthread.KillableThread):
@ -121,7 +37,7 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
# Ignore new media added by other addons
continue
for j, old_item in enumerate(old):
if self.should_suspend() or self.should_cancel():
if self.isCanceled():
# Chances are that we got an empty Kodi playlist due to
# Kodi exit
return
@ -151,34 +67,24 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
LOG.debug('Playqueue item %s moved to position %s',
i + j, i)
try:
PL.move_playlist_item(playqueue, i + j, i)
except exceptions.PlaylistError:
playqueue.plex_move_item(i + j, i)
except PlayqueueError:
LOG.error('Could not modify playqueue positions')
LOG.error('This is likely caused by mixing audio and '
'video tracks in the Kodi playqueue')
del old[j], index[j]
break
else:
playlistitem = PlaylistItem(kodi_item=new_item)
LOG.debug('Detected new Kodi element at position %s: %s ',
i, new_item)
i, playlistitem)
try:
if playqueue.id is None:
PL.init_plex_playqueue(playqueue, kodi_item=new_item)
playqueue.init(playlistitem)
else:
PL.add_item_to_plex_playqueue(playqueue,
i,
kodi_item=new_item)
except exceptions.PlaylistError:
# Could not add the element
pass
except KeyError:
# Catches KeyError from PL.verify_kodi_item()
# Hack: Kodi already started playback of a new item and we
# started playback already using kodimonitors
# PlayBackStart(), but the Kodi playlist STILL only shows
# the old element. Hence ignore playlist difference here
LOG.debug('Detected an outdated Kodi playlist - ignoring')
return
playqueue.plex_add_item(playlistitem, i)
except PlayqueueError:
LOG.warn('Couldnt add new item to Plex: %s', playlistitem)
except IndexError:
# This is really a hack - happens when using Addon Paths
# and repeatedly starting the same element. Kodi will then
@ -190,14 +96,14 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
for j in range(i, len(index)):
index[j] += 1
for i in reversed(index):
if self.should_suspend() or self.should_cancel():
if self.isCanceled():
# Chances are that we got an empty Kodi playlist due to
# Kodi exit
return
LOG.debug('Detected deletion of playqueue element at pos %s', i)
try:
PL.delete_playlist_item_from_PMS(playqueue, i)
except exceptions.PlaylistError:
playqueue.plex_remove_item(i)
except PlayqueueError:
LOG.error('Could not delete PMS element from position %s', i)
LOG.error('This is likely caused by mixing audio and '
'video tracks in the Kodi playqueue')
@ -213,13 +119,14 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
LOG.info("----===## PlayqueueMonitor stopped ##===----")
def _run(self):
while not self.should_cancel():
if self.should_suspend():
if self.wait_while_suspended():
return
while not self.isCanceled():
if self.wait_while_suspended():
return
with app.APP.lock_playqueues:
for playqueue in PLAYQUEUES:
kodi_pl = js.playlist_get_items(playqueue.playlistid)
playqueue.old_kodi_pl = list(kodi_pl)
continue
if playqueue.old_kodi_pl != kodi_pl:
if playqueue.id is None and (not app.SYNC.direct_paths or
app.PLAYSTATE.context_menu_play):
@ -229,5 +136,4 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
else:
# compare old and new playqueue
self._compare_playqueues(playqueue, kodi_pl)
playqueue.old_kodi_pl = list(kodi_pl)
self.sleep(0.2)
app.APP.monitor.waitForAbort(0.2)

View File

@ -0,0 +1,604 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
import threading
from .common import PlaylistItem, PlaylistItemDummy, PlayqueueError
from ..downloadutils import DownloadUtils as DU
from ..plex_api import API
from ..plex_db import PlexDB
from ..kodi_db import KodiVideoDB
from ..playutils import PlayUtils
from ..windows.resume import resume_dialog
from .. import plex_functions as PF, utils, widgets, variables as v, app
from .. import json_rpc as js
LOG = getLogger('PLEX.playqueue')
class PlayQueue(object):
"""
PKC object to represent PMS playQueues and Kodi playlist for queueing
playlistid = None [int] Kodi playlist id (0, 1, 2)
type = None [str] Kodi type: 'audio', 'video', 'picture'
kodi_pl = None Kodi xbmc.PlayList object
items = [] [list] of PlaylistItem
id = None [str] Plex playQueueID, unique Plex identifier
version = None [int] Plex version of the playQueue
selectedItemID = None
[str] Plex selectedItemID, playing element in queue
selectedItemOffset = None
[str] Offset of the playing element in queue
shuffled = 0 [int] 0: not shuffled, 1: ??? 2: ???
repeat = 0 [int] 0: not repeated, 1: ??? 2: ???
If Companion playback is initiated by another user:
plex_transient_token = None
"""
kind = 'playQueue'
def __init__(self):
self.id = None
self.type = None
self.playlistid = None
self.kodi_pl = None
self.items = []
self.version = None
self.selectedItemID = None
self.selectedItemOffset = None
self.shuffled = 0
self.repeat = 0
self.plex_transient_token = None
# Need a hack for detecting swaps of elements
self.old_kodi_pl = []
# Did PKC itself just change the playqueue so the PKC playqueue monitor
# should not pick up any changes?
self.pkc_edit = False
# Workaround to avoid endless loops of detecting PL clears
self._clear_list = []
# To keep track if Kodi playback was initiated from a Kodi playlist
# There are a couple of pitfalls, unfortunately...
self.kodi_playlist_playback = False
# Playlist position/index used when initiating the playqueue
self.index = None
self.force_transcode = None
def __unicode__(self):
return ("{{"
"'playlistid': {self.playlistid}, "
"'id': {self.id}, "
"'version': {self.version}, "
"'type': '{self.type}', "
"'items': {items}, "
"'selectedItemID': {self.selectedItemID}, "
"'selectedItemOffset': {self.selectedItemOffset}, "
"'shuffled': {self.shuffled}, "
"'repeat': {self.repeat}, "
"'kodi_playlist_playback': {self.kodi_playlist_playback}, "
"'pkc_edit': {self.pkc_edit}, "
"}}").format(**{
'items': ['%s/%s: %s' % (x.plex_id, x.id, x.name)
for x in self.items],
'self': self
})
def __str__(self):
return unicode(self).encode('utf-8')
__repr__ = __str__
def is_pkc_clear(self):
"""
Returns True if PKC has cleared the Kodi playqueue just recently.
Then this clear will be ignored from now on
"""
try:
self._clear_list.pop()
except IndexError:
return False
else:
return True
def clear(self, kodi=True):
"""
Resets the playlist object to an empty playlist.
Pass kodi=False in order to NOT clear the Kodi playqueue
"""
# kodi monitor's on_clear method will only be called if there were some
# items to begin with
if kodi and self.kodi_pl.size() != 0:
self._clear_list.append(None)
self.kodi_pl.clear() # Clear Kodi playlist object
self.items = []
self.id = None
self.version = None
self.selectedItemID = None
self.selectedItemOffset = None
self.shuffled = 0
self.repeat = 0
self.plex_transient_token = None
self.old_kodi_pl = []
self.kodi_playlist_playback = False
self.index = None
self.force_transcode = None
LOG.debug('Playlist cleared: %s', self)
def init(self, playlistitem):
"""
Hit if Kodi initialized playback and we need to catch up on the PKC
and Plex side; e.g. for direct paths.
Kodi side will NOT be changed, e.g. no trailers will be added, but Kodi
playqueue taken as-is
"""
LOG.debug('Playqueue init called')
self.clear(kodi=False)
if not isinstance(playlistitem, PlaylistItem) or playlistitem.uri is None:
raise RuntimeError('Didnt receive a valid PlaylistItem but %s: %s'
% (type(playlistitem), playlistitem))
try:
params = {
'next': 0,
'type': self.type,
'uri': playlistitem.uri
}
xml = DU().downloadUrl(url="{server}/%ss" % self.kind,
action_type="POST",
parameters=params)
self.update_details_from_xml(xml)
# Need to update the details for the playlist item
playlistitem.from_xml(xml[0])
except (KeyError, IndexError, TypeError):
LOG.error('Could not init Plex playlist with %s', playlistitem)
raise PlayqueueError()
self.items.append(playlistitem)
LOG.debug('Initialized the playqueue on the Plex side: %s', self)
def play(self, plex_id, plex_type=None, startpos=None, position=None,
synched=True, force_transcode=None):
"""
Initializes the playQueue with e.g. trailers and additional file parts
Pass synched=False if you're sure that this item has not been synched
to Kodi
Or resolves webservice paths to actual paths
Hit by webservice.py
"""
LOG.debug('Play called with plex_id %s, plex_type %s, position %s, '
'synched %s, force_transcode %s, startpos %s', plex_id,
plex_type, position, synched, force_transcode, startpos)
resolve = False
try:
if plex_id == self.items[startpos].plex_id:
resolve = True
except IndexError:
pass
if resolve:
LOG.info('Resolving playback')
self._resolve(plex_id, startpos)
else:
LOG.info('Initializing playback')
self._init(plex_id,
plex_type,
startpos,
position,
synched,
force_transcode)
def _resolve(self, plex_id, startpos):
"""
The Plex playqueue has already been initialized. We resolve the path
from original webservice http://127.0.0.1 to the "correct" Plex one
"""
playlistitem = self.items[startpos]
# Add an additional item with the resolved path after the current one
self.index = startpos + 1
xml = PF.GetPlexMetadata(plex_id)
if xml in (None, 401):
raise PlayqueueError('Could not get Plex metadata %s for %s',
plex_id, self.items[startpos])
api = API(xml[0])
if playlistitem.resume is None:
# Potentially ask user to resume
resume = self._resume_playback(None, xml[0])
else:
# Do NOT ask user
resume = playlistitem.resume
# Use the original playlistitem to retain all info!
self._kodi_add_xml(xml[0],
api,
resume,
playlistitem=playlistitem)
# Add additional file parts, if any exist
self._add_additional_parts(xml)
# Note: the CURRENT playlistitem will be deleted through webservice.py
# once the path resolution has completed
def _init(self, plex_id, plex_type=None, startpos=None, position=None,
synched=True, force_transcode=None):
"""
Initializes the Plex and PKC playqueue for playback. Possibly adds
additionals trailers
"""
self.index = position
while len(self.items) < self.kodi_pl.size():
# The original item that Kodi put into the playlist, e.g.
# {
# u'title': u'',
# u'type': u'unknown',
# u'file': u'http://127.0.0.1:57578/plex/kodi/....',
# u'label': u''
# }
# We CANNOT delete that item right now - so let's add a dummy
# on the PKC side to keep all indicees lined up.
# The failing item will be deleted in webservice.py
LOG.debug('Adding a dummy item to our playqueue')
self.items.insert(0, PlaylistItemDummy())
self.force_transcode = force_transcode
if synched:
with PlexDB(lock=False) as plexdb:
db_item = plexdb.item_by_id(plex_id, plex_type)
else:
db_item = None
if db_item:
xml = None
section_uuid = db_item['section_uuid']
plex_type = db_item['plex_type']
else:
xml = PF.GetPlexMetadata(plex_id)
if xml in (None, 401):
raise PlayqueueError('Could not get Plex metadata %s', plex_id)
section_uuid = xml.get('librarySectionUUID')
api = API(xml[0])
plex_type = api.plex_type()
resume = self._resume_playback(db_item, xml)
trailers = False
if (not resume and plex_type == v.PLEX_TYPE_MOVIE and
utils.settings('enableCinema') == 'true'):
if utils.settings('askCinema') == "true":
# "Play trailers?"
trailers = utils.yesno_dialog(utils.lang(29999),
utils.lang(33016)) or False
else:
trailers = True
LOG.debug('Playing trailers: %s', trailers)
xml = PF.init_plex_playqueue(plex_id,
section_uuid,
plex_type=plex_type,
trailers=trailers)
if xml is None:
LOG.error('Could not get playqueue for plex_id %s UUID %s for %s',
plex_id, section_uuid, self)
raise PlayqueueError('Could not get playqueue')
# See that we add trailers, if they exist in the xml return
self._add_intros(xml)
# Add the main item after the trailers
# Look at the LAST item
api = API(xml[-1])
self._kodi_add_xml(xml[-1], api, resume)
# Add additional file parts, if any exist
self._add_additional_parts(xml)
self.update_details_from_xml(xml)
@staticmethod
def _resume_playback(db_item=None, xml=None):
'''
Pass in either db_item or xml
Resume item if available. Returns bool or raise a PlayqueueError if
resume was cancelled by user.
'''
resume = app.PLAYSTATE.resume_playback
app.PLAYSTATE.resume_playback = None
if app.PLAYSTATE.autoplay:
resume = False
LOG.info('Skip resume for autoplay')
elif resume is None:
if db_item:
with KodiVideoDB(lock=False) as kodidb:
resume = kodidb.get_resume(db_item['kodi_fileid'])
else:
api = API(xml)
resume = api.resume_point()
if resume:
resume = resume_dialog(resume)
LOG.info('User chose resume: %s', resume)
if resume is None:
raise PlayqueueError('User backed out of resume dialog')
app.PLAYSTATE.autoplay = True
return resume
def _add_intros(self, xml):
'''
if we have any play them when the movie/show is not being resumed.
'''
if not len(xml) > 1:
LOG.debug('No trailers returned from the PMS')
return
for i, intro in enumerate(xml):
if i + 1 == len(xml):
# The main item we're looking at - skip!
break
api = API(intro)
LOG.debug('Adding trailer: %s', api.title())
self._kodi_add_xml(intro, api, resume=False)
def _add_additional_parts(self, xml):
''' Create listitems and add them to the stack of playlist.
'''
api = API(xml[0])
for part, _ in enumerate(xml[0][0]):
if part == 0:
# The first part that we've already added
continue
api.set_part_number(part)
LOG.debug('Adding addional part for %s: %s', api.title(), part)
self._kodi_add_xml(xml[0], api, resume=False)
def _kodi_add_xml(self, xml, api, resume, playlistitem=None):
"""
Be careful what you pass as resume:
False: do not resume, do not subsequently ask user
True: do resume, do not subsequently ask user
"""
if not playlistitem:
playlistitem = PlaylistItem(xml_video_element=xml)
playlistitem.part = api.part
playlistitem.force_transcode = self.force_transcode
playlistitem.resume = resume
listitem = widgets.get_listitem(xml, resume=resume)
listitem.setSubtitles(api.cache_external_subs())
play = PlayUtils(api, playlistitem)
url = play.getPlayUrl()
listitem.setPath(url.encode('utf-8'))
self.kodi_add_item(playlistitem, self.index, listitem)
self.items.insert(self.index, playlistitem)
self.index += 1
def update_details_from_xml(self, xml):
"""
Updates the playlist details from the xml provided
"""
self.id = utils.cast(int, xml.get('%sID' % self.kind))
self.version = utils.cast(int, xml.get('%sVersion' % self.kind))
self.shuffled = utils.cast(int, xml.get('%sShuffled' % self.kind))
self.selectedItemID = utils.cast(int,
xml.get('%sSelectedItemID' % self.kind))
self.selectedItemOffset = utils.cast(int,
xml.get('%sSelectedItemOffset'
% self.kind))
LOG.debug('Updated playlist from xml: %s', self)
def add_item(self, item, pos, listitem=None):
"""
Adds a PlaylistItem to both Kodi and Plex at position pos [int]
Also changes self.items
Raises PlayqueueError
"""
self.kodi_add_item(item, pos, listitem)
self.plex_add_item(item, pos)
def kodi_add_item(self, item, pos, listitem=None):
"""
Adds a PlaylistItem to Kodi only. Will not change self.items
Raises PlayqueueError
"""
if not isinstance(item, PlaylistItem):
raise PlayqueueError('Wrong item %s of type %s received'
% (item, type(item)))
if pos > len(self.items):
raise PlayqueueError('Position %s too large for playlist length %s'
% (pos, len(self.items)))
LOG.debug('Adding item to Kodi playlist at position %s: %s', pos, item)
if listitem:
self.kodi_pl.add(url=listitem.getPath(),
listitem=listitem,
index=pos)
elif item.kodi_id is not None and item.kodi_type is not None:
# This method ensures we have full Kodi metadata, potentially
# with more artwork, for example, than Plex provides
if pos == len(self.items):
answ = js.playlist_add(self.playlistid,
{'%sid' % item.kodi_type: item.kodi_id})
else:
answ = js.playlist_insert({'playlistid': self.playlistid,
'position': pos,
'item': {'%sid' % item.kodi_type: item.kodi_id}})
if 'error' in answ:
raise PlayqueueError('Kodi did not add item to playlist: %s',
answ)
else:
if item.xml is None:
LOG.debug('Need to get metadata for item %s', item)
item.xml = PF.GetPlexMetadata(item.plex_id)
if item.xml in (None, 401):
raise PlayqueueError('Could not get metadata for %s', item)
api = API(item.xml[0])
listitem = widgets.get_listitem(item.xml, resume=True)
url = 'http://127.0.0.1:%s/plex/play/file.strm' % v.WEBSERVICE_PORT
args = {
'plex_id': item.plex_id,
'plex_type': api.plex_type()
}
if item.force_transcode:
args['transcode'] = 'true'
url = utils.extend_url(url, args)
item.file = url
listitem.setPath(url.encode('utf-8'))
self.kodi_pl.add(url=url.encode('utf-8'),
listitem=listitem,
index=pos)
def plex_add_item(self, item, pos):
"""
Adds a new PlaylistItem to the playlist at position pos [int] only on
the Plex side of things. Also changes self.items
Raises PlayqueueError
"""
if not isinstance(item, PlaylistItem) or not item.uri:
raise PlayqueueError('Wrong item %s of type %s received'
% (item, type(item)))
if pos > len(self.items):
raise PlayqueueError('Position %s too large for playlist length %s'
% (pos, len(self.items)))
LOG.debug('Adding item to Plex playlist at position %s: %s', pos, item)
url = '{server}/%ss/%s?uri=%s' % (self.kind, self.id, item.uri)
# Will usually put the new item at the end of the Plex playlist
xml = DU().downloadUrl(url, action_type='PUT')
try:
xml[0].attrib
except (TypeError, AttributeError, KeyError, IndexError):
raise PlayqueueError('Could not add item %s to playlist %s'
% (item, self))
for actual_pos, xml_video_element in enumerate(xml):
api = API(xml_video_element)
if api.plex_id() == item.plex_id:
break
else:
raise PlayqueueError('Something went wrong - Plex id not found')
item.from_xml(xml[actual_pos])
self.items.insert(actual_pos, item)
self.update_details_from_xml(xml)
if actual_pos != pos:
self.plex_move_item(actual_pos, pos)
LOG.debug('Added item %s on Plex side: %s', item, self)
def kodi_remove_item(self, pos):
"""
Only manipulates the Kodi playlist. Won't change self.items
"""
LOG.debug('Removing position %s on the Kodi side for %s', pos, self)
answ = js.playlist_remove(self.playlistid, pos)
if 'error' in answ:
raise PlayqueueError('Could not remove item: %s' % answ['error'])
def plex_remove_item(self, pos):
"""
Removes an item from Plex as well as our self.items item list
"""
LOG.debug('Deleting position %s on the Plex side for: %s', pos, self)
try:
xml = DU().downloadUrl("{server}/%ss/%s/items/%s?repeat=%s" %
(self.kind,
self.id,
self.items[pos].id,
self.repeat),
action_type="DELETE")
self.update_details_from_xml(xml)
del self.items[pos]
except IndexError:
LOG.error('Could not delete item at position %s on the Plex side',
pos)
raise PlayqueueError()
def plex_move_item(self, before, after):
"""
Moves playlist item from before [int] to after [int] for Plex only.
Will also change self.items
"""
if before > len(self.items) or after > len(self.items) or after == before:
raise PlayqueueError('Illegal original position %s and/or desired '
'position %s for playlist length %s' %
(before, after, len(self.items)))
LOG.debug('Moving item from %s to %s on the Plex side for %s',
before, after, self)
if after == 0:
url = "{server}/%ss/%s/items/%s/move?after=0" % \
(self.kind,
self.id,
self.items[before].id)
elif after > before:
url = "{server}/%ss/%s/items/%s/move?after=%s" % \
(self.kind,
self.id,
self.items[before].id,
self.items[after].id)
else:
url = "{server}/%ss/%s/items/%s/move?after=%s" % \
(self.kind,
self.id,
self.items[before].id,
self.items[after - 1].id)
xml = DU().downloadUrl(url, action_type="PUT")
try:
xml[0].attrib
except (TypeError, IndexError, AttributeError):
raise PlayqueueError('Could not move playlist item from %s to %s '
'for %s' % (before, after, self))
self.update_details_from_xml(xml)
self.items.insert(after, self.items.pop(before))
LOG.debug('Done moving items for %s', self)
def init_from_xml(self, xml, offset=None, start_plex_id=None, repeat=None,
transient_token=None):
"""
Play all items contained in the xml passed in. Called by Plex Companion.
Either supply the ratingKey of the starting Plex element. Or set
playqueue.selectedItemID
offset [float]: will seek to position offset after playback start
start_plex_id [int]: the plex_id of the element that should be
played
repeat [int]: 0: don't repear
1: repeat item
2: repeat everything
transient_token [unicode]: temporary token received from the PMS
Will stop current playback and start playback at the end
"""
LOG.debug("init_from_xml called with offset %s, start_plex_id %s",
offset, start_plex_id)
app.APP.player.stop()
self.clear()
self.update_details_from_xml(xml)
self.repeat = 0 if not repeat else repeat
self.plex_transient_token = transient_token
for pos, xml_video_element in enumerate(xml):
playlistitem = PlaylistItem(xml_video_element=xml_video_element)
self.kodi_add_item(playlistitem, pos)
self.items.append(playlistitem)
# Where do we start playback?
if start_plex_id is not None:
for startpos, item in enumerate(self.items):
if item.plex_id == start_plex_id:
break
else:
startpos = 0
else:
for startpos, item in enumerate(self.items):
if item.id == self.selectedItemID:
break
else:
startpos = 0
# Set resume for the item we should play - do NOT ask user since we
# initiated from the other Companion client
self.items[startpos].resume = True if offset else False
self.start_playback(pos=startpos, offset=offset)
def start_playback(self, pos=0, offset=0):
"""
Seek immediately after kicking off playback is not reliable.
Threaded, since we need to return BEFORE seeking
"""
LOG.info('Starting playback at %s offset %s for %s', pos, offset, self)
thread = threading.Thread(target=self._threaded_playback,
args=(self.kodi_pl, pos, offset))
thread.start()
@staticmethod
def _threaded_playback(kodi_playlist, pos, offset):
app.APP.player.play(kodi_playlist, startpos=pos, windowed=False)
if offset:
i = 0
while not app.APP.is_playing:
app.APP.monitor.waitForAbort(0.1)
i += 1
if i > 50:
LOG.warn('Could not seek to %s', offset)
return
js.seek_to(offset)

90
resources/lib/playstrm.py Normal file
View File

@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from . import app, utils, json_rpc, variables as v, playqueue as PQ
LOG = getLogger('PLEX.playstrm')
class PlayStrmException(Exception):
"""
Any Exception associated with playstrm
"""
pass
class PlayStrm(object):
'''
Workflow: Strm that calls our webservice in database. When played, the
webserivce returns a dummy file to play. Meanwhile, PlayStrm adds the real
listitems for items to play to the playlist.
'''
def __init__(self, params):
LOG.debug('Starting PlayStrm with params: %s', params)
self.plex_id = utils.cast(int, params['plex_id'])
self.plex_type = params.get('plex_type')
if params.get('synched') and params['synched'].lower() == 'false':
self.synched = False
else:
self.synched = True
self.kodi_id = utils.cast(int, params.get('kodi_id'))
self.kodi_type = params.get('kodi_type')
self.force_transcode = params.get('transcode') == 'true'
if app.PLAYSTATE.audioplaylist:
LOG.debug('Audio playlist detected')
self.playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_AUDIO)
else:
LOG.debug('Video playlist detected')
self.playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_VIDEO)
def __unicode__(self):
return ("{{"
"'plex_id': {self.plex_id}, "
"'plex_type': '{self.plex_type}', "
"'kodi_id': {self.kodi_id}, "
"'kodi_type': '{self.kodi_type}', "
"}}").format(self=self)
def __str__(self):
return unicode(self).encode('utf-8')
__repr__ = __str__
def play(self, start_position=None, delayed=True):
'''
Create and add a single listitem to the Kodi playlist, potentially
with trailers and different file-parts
'''
LOG.debug('play called with start_position %s, delayed %s',
start_position, delayed)
LOG.debug('Kodi playlist BEFORE: %s',
json_rpc.playlist_get_items(self.playqueue.playlistid))
self.playqueue.init(self.plex_id,
plex_type=self.plex_type,
position=start_position,
synched=self.synched,
force_transcode=self.force_transcode)
LOG.info('Initiating play for %s', self)
LOG.debug('Kodi playlist AFTER: %s',
json_rpc.playlist_get_items(self.playqueue.playlistid))
if not delayed:
self.playqueue.start_playback(start_position)
return self.playqueue.index
def play_folder(self, position=None):
'''
When an entire queue is requested, If requested from Kodi, kodi_type is
provided, add as Kodi would, otherwise queue playlist items using strm
links to setup playback later.
'''
start_position = position or max(self.playqueue.kodi_pl.size(), 0)
index = start_position + 1
LOG.info('Play folder plex_id %s, index: %s', self.plex_id, index)
item = PQ.PlaylistItem(plex_id=self.plex_id,
plex_type=self.plex_type,
kodi_id=self.kodi_id,
kodi_type=self.kodi_type)
self.playqueue.add_item(item, index)
index += 1
return index - 1

372
resources/lib/playutils.py Normal file
View File

@ -0,0 +1,372 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from .downloadutils import DownloadUtils as DU
from . import utils, app
from . import variables as v
###############################################################################
LOG = getLogger('PLEX.playutils')
###############################################################################
class PlayUtils():
def __init__(self, api, playlistitem):
"""
init with api (PlexAPI wrapper of the PMS xml element) and
playlistitem [PlaylistItem()]
"""
self.api = api
self.item = playlistitem
def getPlayUrl(self):
"""
Returns the playurl [unicode] for the part or returns None.
(movie might consist of several files)
"""
if self.api.mediastream_number() is None:
return
playurl = self.isDirectPlay()
if playurl is not None:
LOG.info("File is direct playing.")
self.item.playmethod = 'DirectPlay'
elif self.isDirectStream():
LOG.info("File is direct streaming.")
playurl = self.api.transcode_video_path('DirectStream')
self.item.playmethod = 'DirectStream'
else:
LOG.info("File is transcoding.")
playurl = self.api.transcode_video_path(
'Transcode',
quality={
'maxVideoBitrate': self.get_bitrate(),
'videoResolution': self.get_resolution(),
'videoQuality': '100',
'mediaBufferSize': int(
utils.settings('kodi_video_cache')) / 1024,
})
self.item.playmethod = 'Transcode'
LOG.info("The playurl is: %s", playurl)
self.item.file = playurl
return playurl
def isDirectPlay(self):
"""
Returns the path/playurl if we can direct play, None otherwise
"""
# True for e.g. plex.tv watch later
if self.api.should_stream() is True:
LOG.info("Plex item optimized for direct streaming")
return
# Check whether we have a strm file that we need to throw at Kodi 1:1
path = self.api.file_path()
if path is not None and path.endswith('.strm'):
LOG.info('.strm file detected')
playurl = self.api.validate_playurl(path,
self.api.plex_type(),
force_check=True)
return playurl
# set to either 'Direct Stream=1' or 'Transcode=2'
# and NOT to 'Direct Play=0'
if utils.settings('playType') != "0":
# User forcing to play via HTTP
LOG.info("User chose to not direct play")
return
if self.mustTranscode():
return
return self.api.validate_playurl(path,
self.api.plex_type(),
force_check=True)
def mustTranscode(self):
"""
Returns True if we need to transcode because
- codec is in h265
- 10bit video codec
- HEVC codec
- playqueue_item force_transcode is set to True
- state variable FORCE_TRANSCODE set to True
(excepting trailers etc.)
- video bitrate above specified settings bitrate
if the corresponding file settings are set to 'true'
"""
if self.api.plex_type() in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_SONG):
LOG.info('Plex clip or music track, not transcoding')
return False
videoCodec = self.api.video_codec()
LOG.info("videoCodec: %s", videoCodec)
if self.item.force_transcode is True:
LOG.info('User chose to force-transcode')
return True
codec = videoCodec['videocodec']
if codec is None:
# e.g. trailers. Avoids TypeError with "'h265' in codec"
LOG.info('No codec from PMS, not transcoding.')
return False
if ((utils.settings('transcodeHi10P') == 'true' and
videoCodec['bitDepth'] == '10') and
('h264' in codec)):
LOG.info('Option to transcode 10bit h264 video content enabled.')
return True
try:
bitrate = int(videoCodec['bitrate'])
except (TypeError, ValueError):
LOG.info('No video bitrate from PMS, not transcoding.')
return False
if bitrate > self.get_max_bitrate():
LOG.info('Video bitrate of %s is higher than the maximal video'
'bitrate of %s that the user chose. Transcoding',
bitrate, self.get_max_bitrate())
return True
try:
resolution = int(videoCodec['resolution'])
except (TypeError, ValueError):
if videoCodec['resolution'] == '4k':
resolution = 2160
else:
LOG.info('No video resolution from PMS, not transcoding.')
return False
if 'h265' in codec or 'hevc' in codec:
if resolution >= self.getH265():
LOG.info('Option to transcode h265/HEVC enabled. Resolution '
'of the media: %s, transcoding limit resolution: %s',
resolution, self.getH265())
return True
return False
def isDirectStream(self):
# Never transcode Music
if self.api.plex_type() == 'track':
return True
# set to 'Transcode=2'
if utils.settings('playType') == "2":
# User forcing to play via HTTP
LOG.info("User chose to transcode")
return False
if self.mustTranscode():
return False
return True
@staticmethod
def get_max_bitrate():
# get the addon video quality
videoQuality = utils.settings('maxVideoQualities')
bitrate = {
'0': 320,
'1': 720,
'2': 1500,
'3': 2000,
'4': 3000,
'5': 4000,
'6': 8000,
'7': 10000,
'8': 12000,
'9': 20000,
'10': 40000,
'11': 99999999 # deactivated
}
# max bit rate supported by server (max signed 32bit integer)
return bitrate.get(videoQuality, 2147483)
@staticmethod
def getH265():
"""
Returns the user settings for transcoding h265: boundary resolutions
of 480, 720 or 1080 as an int
OR 9999999 (int) if user chose not to transcode
"""
H265 = {
'0': 99999999,
'1': 480,
'2': 720,
'3': 1080
}
return H265[utils.settings('transcodeH265')]
@staticmethod
def get_bitrate():
"""
Get the desired transcoding bitrate from the settings
"""
videoQuality = utils.settings('transcoderVideoQualities')
bitrate = {
'0': 320,
'1': 720,
'2': 1500,
'3': 2000,
'4': 3000,
'5': 4000,
'6': 8000,
'7': 10000,
'8': 12000,
'9': 20000,
'10': 40000,
}
# max bit rate supported by server (max signed 32bit integer)
return bitrate.get(videoQuality, 2147483)
@staticmethod
def get_resolution():
"""
Get the desired transcoding resolutions from the settings
"""
chosen = utils.settings('transcoderVideoQualities')
res = {
'0': '420x420',
'1': '576x320',
'2': '720x480',
'3': '1024x768',
'4': '1280x720',
'5': '1280x720',
'6': '1920x1080',
'7': '1920x1080',
'8': '1920x1080',
'9': '1920x1080',
'10': '1920x1080',
}
return res[chosen]
def audio_subtitle_prefs(self, listitem):
"""
For transcoding only
Called at the very beginning of play; used to change audio and subtitle
stream by a PUT request to the PMS
"""
# Set media and part where we're at
if (self.api.mediastream is None and
self.api.mediastream_number() is None):
return
try:
mediastreams = self.api.plex_media_streams()
except (TypeError, IndexError):
LOG.error('Could not get media %s, part %s',
self.api.mediastream, self.api.part)
return
part_id = mediastreams.attrib['id']
audio_streams_list = []
audio_streams = []
subtitle_streams_list = []
# No subtitles as an option
subtitle_streams = [utils.lang(39706)]
downloadable_streams = []
download_subs = []
# selectAudioIndex = ""
select_subs_index = ""
audio_numb = 0
# Remember 'no subtitles'
sub_num = 1
default_sub = None
for stream in mediastreams:
# Since Plex returns all possible tracks together, have to sort
# them.
index = stream.attrib.get('id')
typus = stream.attrib.get('streamType')
# Audio
if typus == "2":
codec = stream.attrib.get('codec')
channellayout = stream.attrib.get('audioChannelLayout', "")
try:
track = "%s %s - %s %s" % (audio_numb + 1,
stream.attrib['language'],
codec,
channellayout)
except KeyError:
track = "%s %s - %s %s" % (audio_numb + 1,
utils.lang(39707), # unknown
codec,
channellayout)
audio_streams_list.append(index)
audio_streams.append(utils.try_encode(track))
audio_numb += 1
# Subtitles
elif typus == "3":
try:
track = "%s %s" % (sub_num + 1, stream.attrib['language'])
except KeyError:
track = "%s %s (%s)" % (sub_num + 1,
utils.lang(39707), # unknown
stream.attrib.get('codec'))
default = stream.attrib.get('default')
forced = stream.attrib.get('forced')
downloadable = stream.attrib.get('key')
if default:
track = "%s - %s" % (track, utils.lang(39708)) # Default
if forced:
track = "%s - %s" % (track, utils.lang(39709)) # Forced
if downloadable:
# We do know the language - temporarily download
if 'language' in stream.attrib:
path = self.api.download_external_subtitles(
'{server}%s' % stream.attrib['key'],
"subtitle.%s.%s" % (stream.attrib['languageCode'],
stream.attrib['codec']))
# We don't know the language - no need to download
else:
path = self.api.attach_plex_token_to_url(
"%s%s" % (app.CONN.server,
stream.attrib['key']))
downloadable_streams.append(index)
download_subs.append(utils.try_encode(path))
else:
track = "%s (%s)" % (track, utils.lang(39710)) # burn-in
if stream.attrib.get('selected') == '1' and downloadable:
# Only show subs without asking user if they can be
# turned off
default_sub = index
subtitle_streams_list.append(index)
subtitle_streams.append(utils.try_encode(track))
sub_num += 1
if audio_numb > 1:
resp = utils.dialog('select', utils.lang(33013), audio_streams)
if resp > -1:
# User selected some audio track
args = {
'audioStreamID': audio_streams_list[resp],
'allParts': 1
}
DU().downloadUrl('{server}/library/parts/%s' % part_id,
action_type='PUT',
parameters=args)
if sub_num == 1:
# No subtitles
return
select_subs_index = None
if (utils.settings('pickPlexSubtitles') == 'true' and
default_sub is not None):
LOG.info('Using default Plex subtitle: %s', default_sub)
select_subs_index = default_sub
else:
resp = utils.dialog('select', utils.lang(33014), subtitle_streams)
if resp > 0:
select_subs_index = subtitle_streams_list[resp - 1]
else:
# User selected no subtitles or backed out of dialog
select_subs_index = ''
LOG.debug('Adding external subtitles: %s', download_subs)
# Enable Kodi to switch autonomously to downloadable subtitles
if download_subs:
listitem.setSubtitles(download_subs)
# Don't additionally burn in subtitles
if select_subs_index in downloadable_streams:
select_subs_index = ''
# Now prep the PMS for our choice
args = {
'subtitleStreamID': select_subs_index,
'allParts': 1
}
DU().downloadUrl('{server}/library/parts/%s' % part_id,
action_type='PUT',
parameters=args)

1818
resources/lib/plex_api.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,32 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
plex_api interfaces with all Plex Media Server (and plex.tv) xml responses
"""
from __future__ import absolute_import, division, unicode_literals
from .base import Base
from .artwork import Artwork
from .file import File
from .media import Media
from .user import User
from .playback import Playback
from ..plex_db import PlexDB
class API(Base, Artwork, File, Media, User, Playback):
pass
def mass_api(xml):
"""
Pass in an entire XML PMS response with e.g. several movies or episodes
Will Look-up Kodi ids in the Plex.db for every element (thus speeding up
this process for several PMS items!)
"""
apis = [API(x) for x in xml]
with PlexDB(lock=False) as plexdb:
for api in apis:
api.check_db(plexdb=plexdb)
return apis

View File

@ -1,287 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from ..kodi_db import KodiVideoDB, KodiMusicDB
from ..downloadutils import DownloadUtils as DU
from .. import utils, variables as v, app
from . import fanart_lookup
LOG = getLogger('PLEX.api')
class Artwork(object):
def one_artwork(self, art_kind, aspect=None):
"""
aspect can be: 'square', '16:9', 'poster'. Defaults to 'poster'
"""
aspect = 'poster' if not aspect else aspect
if aspect == 'poster':
width = 1000
height = 1500
elif aspect == '16:9':
width = 1920
height = 1080
elif aspect == 'square':
width = 1000
height = 1000
else:
raise NotImplementedError('aspect ratio not yet implemented: %s'
% aspect)
artwork = self.xml.get(art_kind)
if not artwork or artwork.startswith('http'):
return artwork
if '/composite/' in artwork:
try:
# e.g. Plex collections where artwork already contains width and
# height. Need to upscale for better resolution
artwork, args = artwork.split('?')
args = dict(utils.parse_qsl(args))
width = int(args.get('width', 400))
height = int(args.get('height', 400))
# Adjust to 4k resolution 1920x1080
scaling = 1920.0 / float(max(width, height))
width = int(scaling * width)
height = int(scaling * height)
except ValueError:
# e.g. playlists
pass
artwork = '%s?width=%s&height=%s' % (artwork, width, height)
artwork = ('%s/photo/:/transcode?width=1920&height=1920&'
'minSize=1&upscale=0&url=%s'
% (app.CONN.server, utils.quote(artwork)))
artwork = self.attach_plex_token_to_url(artwork)
return artwork
def artwork_episode(self, full_artwork):
"""
Episodes are special, they only get the thumb, because all the other
artwork will be saved under season and show EXCEPT if you're
constructing a listitem and the item has NOT been synched to the Kodi db
"""
artworks = {}
# Item is currently NOT in the Kodi DB
art = self.one_artwork('thumb')
if art:
artworks['thumb'] = art
if not full_artwork:
# For episodes, only get the thumb. Everything else stemms from
# either the season or the show
return artworks
for kodi_artwork, plex_artwork in \
v.KODI_TO_PLEX_ARTWORK_EPISODE.iteritems():
art = self.one_artwork(plex_artwork)
if art:
artworks[kodi_artwork] = art
return artworks
def artwork(self, kodi_id=None, kodi_type=None, full_artwork=False):
"""
Gets the URLs to the Plex artwork. Dict keys will be missing if there
is no corresponding artwork.
Pass kodi_id and kodi_type to grab the artwork saved in the Kodi DB
(thus potentially more artwork, e.g. clearart, discart).
Output ('max' version)
{
'thumb'
'poster'
'banner'
'clearart'
'clearlogo'
'fanart'
}
'landscape' and 'icon' might be implemented later
Passing full_artwork=True returns ALL the artwork for the item, so not
just 'thumb' for episodes, but also season and show artwork
"""
if self.plex_type == v.PLEX_TYPE_EPISODE:
return self.artwork_episode(full_artwork)
artworks = {}
if kodi_id:
# in Kodi database, potentially with additional e.g. clearart
if self.plex_type in v.PLEX_VIDEOTYPES:
with KodiVideoDB(lock=False) as kodidb:
return kodidb.get_art(kodi_id, kodi_type)
else:
with KodiMusicDB(lock=False) as kodidb:
return kodidb.get_art(kodi_id, kodi_type)
for kodi_artwork, plex_artwork in v.KODI_TO_PLEX_ARTWORK.iteritems():
art = self.one_artwork(plex_artwork)
if art:
artworks[kodi_artwork] = art
if self.plex_type in (v.PLEX_TYPE_SONG, v.PLEX_TYPE_ALBUM):
# Get parent item artwork if the main item is missing artwork
if 'fanart' not in artworks:
art = self.one_artwork('parentArt')
if art:
artworks['fanart1'] = art
if 'poster' not in artworks:
art = self.one_artwork('parentThumb')
if art:
artworks['poster'] = art
if self.plex_type in (v.PLEX_TYPE_SONG,
v.PLEX_TYPE_ALBUM,
v.PLEX_TYPE_ARTIST):
# need to set poster also as thumb
art = self.one_artwork('thumb')
if art:
artworks['thumb'] = art
if self.plex_type == v.PLEX_TYPE_PLAYLIST:
art = self.one_artwork('composite')
if art:
artworks['thumb'] = art
return artworks
def fanart_artwork(self, artworks):
"""
Downloads additional fanart from third party sources (well, link to
fanart only).
"""
external_id = self.retrieve_external_item_id()
if external_id is not None:
artworks = self.lookup_fanart_tv(external_id[0], artworks)
return artworks
def set_artwork(self):
"""
Gets the URLs to the Plex artwork, or empty string if not found.
Only call on movies!
"""
artworks = {}
# Plex does not get much artwork - go ahead and get the rest from
# fanart tv only for movie or tv show
external_id = self.retrieve_external_item_id(collection=True)
if external_id is not None:
external_id, poster, background = external_id
if poster is not None:
artworks['poster'] = poster
if background is not None:
artworks['fanart'] = background
artworks = self.lookup_fanart_tv(external_id, artworks)
else:
LOG.info('Did not find a set/collection ID on TheMovieDB using %s.'
' Artwork will be missing.', self.title())
return artworks
def retrieve_external_item_id(self, collection=False):
"""
Returns the set
media_id [unicode]: the item's IMDB id for movies or tvdb id for
TV shows
poster [unicode]: path to the item's poster artwork
background [unicode]: path to the item's background artwork
The last two might be None if not found. Generally None is returned
if unsuccessful.
If not found in item's Plex metadata, check themovidedb.org.
"""
item = self.xml.attrib
media_type = self.plex_type
media_id = None
# Return the saved Plex id's, if applicable
# Always seek collection's ids since not provided by PMS
if collection is False:
if media_type == v.PLEX_TYPE_MOVIE:
media_id = self.guids.get('imdb')
elif media_type == v.PLEX_TYPE_SHOW:
media_id = self.guids.get('tvdb')
if media_id is not None:
return media_id, None, None
LOG.info('Plex did not provide ID for IMDB or TVDB. Start '
'lookup process')
else:
LOG.debug('Start movie set/collection lookup on themoviedb with %s',
item.get('title', ''))
return fanart_lookup.external_item_id(self.title(),
self.year(),
self.plex_type,
collection)
def lookup_fanart_tv(self, media_id, artworks):
"""
perform artwork lookup on fanart.tv
media_id: IMDB id for movies, tvdb id for TV shows
"""
api_key = utils.settings('FanArtTVAPIKey')
typus = self.plex_type
if typus == v.PLEX_TYPE_SHOW:
typus = 'tv'
if typus == v.PLEX_TYPE_MOVIE:
url = 'http://webservice.fanart.tv/v3/movies/%s?api_key=%s' \
% (media_id, api_key)
elif typus == 'tv':
url = 'http://webservice.fanart.tv/v3/tv/%s?api_key=%s' \
% (media_id, api_key)
else:
# Not supported artwork
return artworks
data = DU().downloadUrl(url,
authenticate=False,
timeout=15,
return_response=True)
if not data.ok:
LOG.debug('Could not download data from FanartTV')
return artworks
data = data.json()
fanart_tv_types = list(v.FANART_TV_TO_KODI_TYPE)
if typus == v.PLEX_TYPE_ARTIST:
fanart_tv_types.append(("thumb", "folder"))
else:
fanart_tv_types.append(("thumb", "thumb"))
prefixes = (
"hd" + typus,
"hd",
typus,
"",
)
for fanart_tv_type, kodi_type in fanart_tv_types:
# Skip the ones we already have
if kodi_type in artworks:
continue
for prefix in prefixes:
fanarttvimage = prefix + fanart_tv_type
if fanarttvimage not in data:
continue
# select image in preferred language
for entry in data[fanarttvimage]:
if entry.get("lang") == v.KODILANGUAGE:
artworks[kodi_type] = \
entry.get("url", "").replace(' ', '%20')
break
# just grab the first english OR undefinded one as fallback
# (so we're actually grabbing the more popular one)
if kodi_type not in artworks:
for entry in data[fanarttvimage]:
if entry.get("lang") in ("en", "00"):
artworks[kodi_type] = \
entry.get("url", "").replace(' ', '%20')
break
# grab extrafanarts in list
fanartcount = 1 if 'fanart' in artworks else ''
for prefix in prefixes:
fanarttvimage = prefix + 'background'
if fanarttvimage not in data:
continue
for entry in data[fanarttvimage]:
if entry.get("url") is None:
continue
artworks['fanart%s' % fanartcount] = \
entry['url'].replace(' ', '%20')
try:
fanartcount += 1
except TypeError:
fanartcount = 1
if fanartcount >= v.MAX_BACKGROUND_COUNT:
break
return artworks

View File

@ -1,697 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from re import sub
import xbmcgui
from ..utils import cast
from ..plex_db import PlexDB
from .. import utils, timing, variables as v, app, plex_functions as PF
from .. import widgets
LOG = getLogger('PLEX.api')
METADATA_PROVIDERS = (('imdb', utils.REGEX_IMDB),
('tvdb', utils.REGEX_TVDB),
('tmdb', utils.REGEX_TMDB),
('anidb', utils.REGEX_ANIDB))
class Base(object):
"""
Processes a Plex media server's XML response
xml: xml.etree.ElementTree element
"""
def __init__(self, xml):
self.xml = xml
# which media part in the XML response shall we look at if several
# media files are present for the SAME video? (e.g. a 4k and a 1080p
# version)
self.part = 0
self.mediastream = None
# Make sure we're only checking our Plex DB once
self._checked_db = False
# In order to run through the leaves of the xml only once
self._scanned_children = False
self._genres = []
self._countries = []
self._collections = []
self._people = []
self._cast = []
self._directors = []
self._writers = []
self._producers = []
self._locations = []
self._intro_markers = []
self._guids = {}
self._coll_match = None
# Plex DB attributes
self._section_id = None
self._kodi_id = None
self._last_sync = None
self._last_checksum = None
self._kodi_fileid = None
self._kodi_pathid = None
self._fanart_synced = None
@property
def tag(self):
"""
Returns the xml etree tag, e.g. 'Directory', 'Playlist', 'Hub', 'Video'
"""
return self.xml.tag
def tag_label(self):
"""
Returns the 'tag' attribute of the xml
"""
return self.xml.get('tag')
@property
def attrib(self):
"""
Returns the xml etree attrib dict
"""
return self.xml.attrib
@property
def plex_id(self):
"""
Returns the Plex ratingKey as an integer or None
"""
return cast(int, self.xml.get('ratingKey'))
@property
def fast_key(self):
"""
Returns the 'fastKey' as unicode or None
"""
return self.xml.get('fastKey')
@property
def plex_type(self):
"""
Returns the type of media, e.g. 'movie' or 'clip' for trailers as
Unicode or None.
"""
return self.xml.get('type')
@property
def section_id(self):
self.check_db()
return self._section_id
@property
def kodi_id(self):
self.check_db()
return self._kodi_id
@property
def kodi_type(self):
return v.KODITYPE_FROM_PLEXTYPE[self.plex_type]
@property
def last_sync(self):
self.check_db()
return self._last_sync
@property
def last_checksum(self):
self.check_db()
return self._last_checksum
@property
def kodi_fileid(self):
self.check_db()
return self._kodi_fileid
@property
def kodi_pathid(self):
self.check_db()
return self._kodi_pathid
@property
def fanart_synced(self):
self.check_db()
return self._fanart_synced
@property
def guids(self):
self._scan_children()
return self._guids
def check_db(self, plexdb=None):
"""
Check's whether we synched this item to Kodi. If so, then retrieve the
appropriate Kodi info like the kodi_id and kodi_fileid
Pass in a plexdb DB-connection for a faster lookup
"""
if self._checked_db:
return
self._checked_db = True
if self.plex_type == v.PLEX_TYPE_CLIP:
# Clips won't ever be synched to Kodi
return
if plexdb:
db_item = plexdb.item_by_id(self.plex_id, self.plex_type)
else:
with PlexDB(lock=False) as plexdb:
db_item = plexdb.item_by_id(self.plex_id, self.plex_type)
if not db_item:
return
self._section_id = db_item['section_id']
self._kodi_id = db_item['kodi_id']
self._last_sync = db_item['last_sync']
self._last_checksum = db_item['checksum']
if 'kodi_fileid' in db_item:
self._kodi_fileid = db_item['kodi_fileid']
if 'kodi_pathid' in db_item:
self._kodi_pathid = db_item['kodi_pathid']
if 'fanart_synced' in db_item:
self._fanart_synced = db_item['fanart_synced']
def path_and_plex_id(self):
"""
Returns the Plex key such as '/library/metadata/246922' or None
"""
return self.xml.get('key')
def item_id(self):
"""
Returns current playQueueItemID or if unsuccessful the playListItemID
as int.
If not found, None is returned
"""
return (cast(int, self.xml.get('playQueueItemID')) or
cast(int, self.xml.get('playListItemID')))
def playlist_type(self):
"""
Returns the playlist type ('video', 'audio') or None
"""
return self.xml.get('playlistType')
def library_section_id(self):
"""
Returns the id of the Plex library section (for e.g. a movies section)
as an int or None
"""
return cast(int, self.xml.get('librarySectionID'))
def guid_html_escaped(self):
"""
Returns the 'guid' attribute, e.g.
'com.plexapp.agents.thetvdb://76648/2/4?lang=en'
as an HTML-escaped string or None
"""
guid = self.xml.get('guid')
return utils.escape_html(guid) if guid else None
def date_created(self):
"""
Returns the date when this library item was created in Kodi-time as
unicode
If not found, returns 2000-01-01 10:00:00
"""
res = self.xml.get('addedAt')
return timing.plex_date_to_kodi(res) if res else '2000-01-01 10:00:00'
def updated_at(self):
"""
Returns the last time this item was updated as an int, e.g.
1524739868 or None
"""
return cast(int, self.xml.get('updatedAt'))
def checksum(self):
"""
Returns the unique int <ratingKey><updatedAt>. If updatedAt is not set,
addedAt is used.
"""
return int('%s%s' % (self.xml.get('ratingKey'),
abs(int(self.xml.get('updatedAt') or
self.xml.get('addedAt', '1541572987')))))
def title(self):
"""
Returns the title of the element as unicode or 'Missing Title'
"""
return self.xml.get('title', 'Missing Title')
def sorttitle(self):
"""
Returns an item's sorting name/title or the title itself if not found
"Missing Title" if both are not present
"""
return self.xml.get('titleSort',
self.xml.get('title', 'Missing Title'))
def plex_media_streams(self):
"""
Returns the media streams directly from the PMS xml.
Mind to set self.mediastream and self.part before calling this method!
"""
try:
return self.xml[self.mediastream][self.part]
except TypeError:
# Direct Paths when we don't set mediastream and part
return self.xml[0][0]
def part_id(self):
"""
Returns the unique id of the currently active part [int]
"""
try:
return int(self.xml[self.mediastream][self.part].attrib['id'])
except TypeError:
# Direct Paths when we don't set mediastream and part
return int(self.xml[0][0].attrib['id'])
def plot(self):
"""
Returns the plot or None.
"""
return self.xml.get('summary')
def tagline(self):
"""
Returns a shorter tagline of the plot or None
"""
return self.xml.get('tagline')
def shortplot(self):
"""
Not yet implemented - returns None
"""
pass
def premiere_date(self):
"""
Returns the "originallyAvailableAt", e.g. "2018-11-16" or None
"""
return self.xml.get('originallyAvailableAt')
def kodi_premiere_date(self):
"""
Takes Plex' originallyAvailableAt of the form "yyyy-mm-dd" and returns
Kodi's "dd.mm.yyyy" or None
"""
date = self.premiere_date()
if date is None:
return
try:
date = sub(r'(\d+)-(\d+)-(\d+)', r'\3.\2.\1', date)
except Exception:
date = None
return date
def year(self):
"""
Returns the production(?) year ("year") as Unicode or None
"""
return self.xml.get('year')
def studios(self):
"""
Returns a list of the 'studio' - currently only ever 1 entry.
Or returns an empty list
"""
return [self.xml.get('studio')] if self.xml.get('studio') else []
def content_rating(self):
"""
Get the content rating or None
"""
mpaa = self.xml.get('contentRating')
if not mpaa:
return
# Convert more complex cases
if mpaa in ('NR', 'UR'):
# Kodi seems to not like NR, but will accept Rated Not Rated
mpaa = 'Rated Not Rated'
elif mpaa.startswith('gb/'):
mpaa = mpaa.replace('gb/', 'UK:', 1)
return mpaa
def rating(self):
"""
Returns the rating [float] first from 'audienceRating', if that fails
from 'rating'.
Returns 0.0 if both are not found
"""
return cast(float, self.xml.get('audienceRating',
self.xml.get('rating'))) or 0.0
def votecount(self):
"""
Not implemented by Plex yet - returns None
"""
pass
def runtime(self):
"""
Returns the total duration of the element in seconds as int.
0 if not found
"""
runtime = cast(float, self.xml.get('duration')) or 0.0
return int(runtime * v.PLEX_TO_KODI_TIMEFACTOR)
def leave_count(self):
"""
Returns the following dict or None
{
'totalepisodes': unicode('leafCount'),
'watchedepisodes': unicode('viewedLeafCount'),
'unwatchedepisodes': unicode(totalepisodes - watchedepisodes)
}
"""
try:
total = int(self.xml.attrib['leafCount'])
watched = int(self.xml.attrib['viewedLeafCount'])
return {
'totalepisodes': unicode(total),
'watchedepisodes': unicode(watched),
'unwatchedepisodes': unicode(total - watched)
}
except (KeyError, TypeError):
pass
# Stuff having to do with parent and grandparent items
######################################################
def index(self):
"""
Returns the 'index' of the element [int]. Depicts e.g. season number of
the season or the track number of the song
"""
return cast(int, self.xml.get('index'))
def show_id(self):
"""
Returns the episode's tv show's Plex id [int] or None
"""
return self.grandparent_id()
def show_title(self):
"""
Returns the episode's tv show's name/title [unicode] or None
"""
return self.grandparent_title()
def season_id(self):
"""
Returns the episode's season's Plex id [int] or None
"""
return self.parent_id()
def season_number(self):
"""
Returns the episode's season number (e.g. season '2') as an int or None
"""
return self.parent_index()
def season_name(self):
"""
Returns the season's name/title or None
"""
return self.xml.get('title')
def artist_name(self):
"""
Returns the artist name for an album: first it attempts to return
'parentTitle', if that failes 'originalTitle'
"""
return self.xml.get('parentTitle', self.xml.get('originalTitle'))
def parent_id(self):
"""
Returns the 'parentRatingKey' as int or None
"""
return cast(int, self.xml.get('parentRatingKey'))
def parent_index(self):
"""
Returns the 'parentRatingKey' as int or None
"""
return cast(int, self.xml.get('parentIndex'))
def grandparent_id(self):
"""
Returns the ratingKey for the corresponding grandparent, e.g. a TV show
for episodes, or None
"""
return cast(int, self.xml.get('grandparentRatingKey'))
def grandparent_title(self):
"""
Returns the title for the corresponding grandparent, e.g. a TV show
name for episodes, or None
"""
return self.xml.get('grandparentTitle')
def disc_number(self):
"""
Returns the song's disc number as an int or None if not found
"""
return self.parent_index()
def _scan_children(self):
"""
Ensures that we're scanning the xml's subelements only once
"""
if self._scanned_children:
return
self._scanned_children = True
cast_order = 0
for child in self.xml:
if child.tag == 'Role':
self._cast.append((child.get('tag'),
child.get('thumb'),
child.get('role'),
cast_order))
cast_order += 1
elif child.tag == 'Genre':
self._genres.append(child.get('tag'))
elif child.tag == 'Country':
self._countries.append(child.get('tag'))
elif child.tag == 'Director':
self._directors.append(child.get('tag'))
elif child.tag == 'Writer':
self._writers.append(child.get('tag'))
elif child.tag == 'Producer':
self._producers.append(child.get('tag'))
elif child.tag == 'Location':
self._locations.append(child.get('path'))
elif child.tag == 'Collection':
self._collections.append((cast(int, child.get('id')),
child.get('tag')))
elif child.tag == 'Guid':
guid = child.get('id')
guid = guid.split('://', 1)
self._guids[guid[0]] = guid[1]
elif child.tag == 'Marker' and child.get('type') == 'intro':
intro = (cast(float, child.get('startTimeOffset')),
cast(float, child.get('endTimeOffset')))
if None in intro:
# Safety net if PMS xml is not as expected
continue
intro = (intro[0] / 1000.0, intro[1] / 1000.0)
self._intro_markers.append(intro)
# Plex Movie agent (legacy) or "normal" Plex tv show agent
if not self._guids:
guid = self.xml.get('guid')
if not guid:
return
for provider, regex in METADATA_PROVIDERS:
provider_id = regex.findall(guid)
try:
self._guids[provider] = provider_id[0]
except IndexError:
pass
else:
# There will only ever be one entry
break
def cast(self):
"""
Returns a list of tuples of the cast:
[(<name of actor [unicode]>,
<thumb url [unicode, may be None]>,
<role [unicode, may be None]>,
<order of appearance [int]>)]
"""
self._scan_children()
return self._cast
def genres(self):
"""
Returns a list of genres found
"""
self._scan_children()
return self._genres
def countries(self):
"""
Returns a list of all countries
"""
self._scan_children()
return self._countries
def directors(self):
"""
Returns a list of all directors
"""
self._scan_children()
return self._directors
def writers(self):
"""
Returns a list of all writers
"""
self._scan_children()
return self._writers
def producers(self):
"""
Returns a list of all producers
"""
self._scan_children()
return self._producers
def tv_show_path(self):
"""
Returns the direct path to the TV show, e.g. '\\NAS\tv\series'
or None
"""
self._scan_children()
if self._locations:
return self._locations[0]
def collections(self):
"""
Returns a list of tuples of the collection id and tags or an empty list
[(<collection id 1>, <collection name 1>), ...]
"""
self._scan_children()
return self._collections
def people(self):
"""
Returns a dict with lists of tuples:
{
'actor': [(<name of actor [unicode]>,
<thumb url [unicode, may be None]>,
<role [unicode, may be None]>,
<order of appearance [int]>)]
'director': [..., (<name>, ), ...],
'writer': [..., (<name>, ), ...]
}
Everything in unicode, except <cast order> which is an int.
Only <art-url> and <role> may be None if not found.
"""
self._scan_children()
return {
'actor': self._cast,
'director': [(x, ) for x in self._directors],
'writer': [(x, ) for x in self._writers]
}
def extras(self):
"""
Returns an iterator for etree elements for each extra, e.g. trailers
Returns None if no extras are found
"""
extras = self.xml.find('Extras')
if extras is None:
return
return (x for x in extras)
def trailer(self):
"""
Returns the URL for a single trailer (local trailer preferred; first
trailer found returned) or an add-on path to list all Plex extras
if the user setting showExtrasInsteadOfTrailer is set.
Returns None if nothing is found.
"""
url = None
for extras in self.xml.iterfind('Extras'):
# There will always be only 1 extras element
if (len(extras) > 0 and
app.SYNC.show_extras_instead_of_playing_trailer):
return ('plugin://%s?mode=route_to_extras&plex_id=%s'
% (v.ADDON_ID, self.plex_id))
for extra in extras:
typus = cast(int, extra.get('extraType'))
if typus != 1:
# Skip non-trailers
continue
if extra.get('guid', '').startswith('file:'):
url = extra.get('ratingKey')
# Always prefer local trailers (first one listed)
break
elif not url:
url = extra.get('ratingKey')
if url:
url = ('plugin://%s.movies/?plex_id=%s&plex_type=%s&mode=play'
% (v.ADDON_ID, url, v.PLEX_TYPE_CLIP))
return url
def listitem(self, listitem=xbmcgui.ListItem, resume=True):
"""
Returns a xbmcgui.ListItem() (or PKCListItem) for this Plex element
Pass resume=False in order to NOT set a resume point (but let Kodi
automatically handle it)
"""
item = widgets.generate_item(self)
if not resume and 'resume' in item:
del item['resume']
item = widgets.prepare_listitem(item)
return widgets.create_listitem(item, as_tuple=False, listitem=listitem)
def collections_match(self, section_id):
"""
Downloads one additional xml from the PMS in order to return a list of
tuples [(collection_id, plex_id), ...] for all collections of the
current item's Plex library sectin
Pass in the collection id of e.g. the movie's metadata
"""
if self._coll_match is None:
self._coll_match = PF.collections(section_id)
if self._coll_match is None:
LOG.error('Could not download collections for %s',
self.library_section_id())
self._coll_match = []
self._coll_match = \
[(utils.cast(int, x.get('index')),
utils.cast(int, x.get('ratingKey'))) for x in self._coll_match]
return self._coll_match
@staticmethod
def attach_plex_token_to_url(url):
"""
Returns an extended URL with the Plex token included as 'X-Plex-Token='
url may or may not already contain a '?'
"""
if not app.ACCOUNT.pms_token:
return url
if '?' not in url:
return "%s?X-Plex-Token=%s" % (url, app.ACCOUNT.pms_token)
else:
return "%s&X-Plex-Token=%s" % (url, app.ACCOUNT.pms_token)
@staticmethod
def list_to_string(input_list):
"""
Concatenates input_list (list of unicodes) with a separator ' / '
Returns None if the list was empty
"""
return ' / '.join(input_list) or None

View File

@ -1,182 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from re import sub
from string import punctuation
from ..downloadutils import DownloadUtils as DU
from .. import utils, variables as v
LOG = getLogger('PLEX.api.fanartlookup')
API_KEY = utils.settings('themoviedbAPIKey')
# How far apart can a video's airing date be (in years)
YEARS_APART = 1
# levenshtein_distance_ratio() returns a value between 0 (no match) and 1 (full
# match). What's the threshold?
LEVENSHTEIN_RATIO_THRESHOLD = 0.95
# Which character should we ignore when matching video titles?
EXCLUDE_CHARS = set(punctuation)
def external_item_id(title, year, plex_type, collection):
LOG.debug('Start identifying %s (%s, %s)', title, year, plex_type)
year = int(year) if year else None
media_type = 'tv' if plex_type == v.PLEX_TYPE_SHOW else plex_type
# if the title has the year in remove it as tmdb cannot deal with it...
# replace e.g. 'The Americans (2015)' with 'The Americans'
title = sub(r'\s*\(\d{4}\)$', '', title, count=1)
url = 'https://api.themoviedb.org/3/search/%s' % media_type
parameters = {
'api_key': API_KEY,
'language': v.KODILANGUAGE,
'query': title.encode('utf-8')
}
data = DU().downloadUrl(url,
authenticate=False,
parameters=parameters,
timeout=7)
try:
data = data['results']
except (AttributeError, KeyError, TypeError):
LOG.debug('No match found on themoviedb for %s (%s, %s)',
title, year, media_type)
return
LOG.debug('themoviedb returned results: %s', data)
# Some entries don't contain a title or id - get rid of them
data = [x for x in data if 'title' in x and 'id' in x]
# Get rid of all results that do NOT have a matching release year
if year:
data = [x for x in data if __year_almost_matches(year, x)]
if not data:
LOG.debug('Empty results returned by themoviedb for %s (%s, %s)',
title, year, media_type)
return
# Calculate how similar the titles are
title = sanitize_string(title)
for entry in data:
entry['match_score'] = levenshtein_distance_ratio(
sanitize_string(entry['title']), title)
# (one of the possibly many) best match using levenshtein distance ratio
entry = max(data, key=lambda x: x['match_score'])
if entry['match_score'] < LEVENSHTEIN_RATIO_THRESHOLD:
LOG.debug('Best themoviedb match not good enough: %s', entry)
return
# Check if we got several matches. If so, take the most popular one
best_matches = [x for x in data if
x['match_score'] == entry['match_score']
and 'popularity' in x]
entry = max(best_matches, key=lambda x: x['popularity'])
LOG.debug('Found themoviedb match: %s', entry)
# lookup external tmdb_id and perform artwork lookup on fanart.tv
tmdb_id = entry.get('id')
parameters = {'api_key': API_KEY}
if media_type == 'movie':
url = 'https://api.themoviedb.org/3/movie/%s' % tmdb_id
parameters['append_to_response'] = 'videos'
elif media_type == 'tv':
url = 'https://api.themoviedb.org/3/tv/%s' % tmdb_id
parameters['append_to_response'] = 'external_ids,videos'
media_id, poster, background = None, None, None
for language in (v.KODILANGUAGE, 'en'):
parameters['language'] = language
data = DU().downloadUrl(url,
authenticate=False,
parameters=parameters,
timeout=7)
try:
data.get('test')
except AttributeError:
LOG.warning('Could not download %s with parameters %s',
url, parameters)
continue
if collection is False:
if data.get('imdb_id'):
media_id = str(data.get('imdb_id'))
break
if (data.get('external_ids') and
data['external_ids'].get('tvdb_id')):
media_id = str(data['external_ids']['tvdb_id'])
break
else:
if not data.get('belongs_to_collection'):
continue
media_id = data.get('belongs_to_collection').get('id')
if not media_id:
continue
media_id = str(media_id)
LOG.debug('Retrieved collections tmdb id %s for %s',
media_id, title)
url = 'https://api.themoviedb.org/3/collection/%s' % media_id
data = DU().downloadUrl(url,
authenticate=False,
parameters=parameters,
timeout=7)
try:
data.get('poster_path')
except AttributeError:
LOG.debug('Could not find TheMovieDB poster paths for %s'
' in the language %s', title, language)
continue
if not poster and data.get('poster_path'):
poster = ('https://image.tmdb.org/t/p/original%s' %
data.get('poster_path'))
if not background and data.get('backdrop_path'):
background = ('https://image.tmdb.org/t/p/original%s' %
data.get('backdrop_path'))
return media_id, poster, background
def __year_almost_matches(year, entry):
try:
entry_year = int(entry['release_date'][0:4])
except (KeyError, ValueError):
return True
return abs(year - entry_year) <= YEARS_APART
def sanitize_string(s):
s = s.lower().strip()
# Get rid of chars in EXCLUDE_CHARS
s = ''.join(character for character in s if character not in EXCLUDE_CHARS)
# Get rid of multiple spaces
s = ' '.join(s.split())
return s
def levenshtein_distance_ratio(s, t):
"""
Calculates levenshtein distance ratio between two strings.
The more similar the strings, the closer the result will be to 1.
The farther disjunct the string, the closer the result to 0
https://www.datacamp.com/community/tutorials/fuzzy-string-python
"""
# Initialize matrix of zeros
rows = len(s) + 1
cols = len(t) + 1
distance = [[0 for x in range(cols)] for y in range(rows)]
# Populate matrix of zeros with the indeces of each character of both strings
for i in range(1, rows):
for k in range(1,cols):
distance[i][0] = i
distance[0][k] = k
# Iterate over the matrix to compute the cost of deletions,insertions and/or substitutions
for col in range(1, cols):
for row in range(1, rows):
if s[row-1] == t[col-1]:
cost = 0 # If the characters are the same in the two strings in a given position [i,j] then the cost is 0
else:
# In order to align the results with those of the Python Levenshtein package, if we choose to calculate the ratio
# the cost of a substitution is 2. If we calculate just distance, then the cost of a substitution is 1.
cost = 2
distance[row][col] = min(distance[row-1][col] + 1, # Cost of deletions
distance[row][col-1] + 1, # Cost of insertions
distance[row-1][col-1] + cost) # Cost of substitutions
return ((len(s)+len(t)) - distance[row][col]) / (len(s)+len(t))

View File

@ -1,207 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from .. import utils, variables as v, app
def _transcode_image_path(key, AuthToken, path, width, height):
"""
Transcode Image support
parameters:
key
AuthToken
path - source path of current XML: path[srcXML]
width
height
result:
final path to image file
"""
# external address - can we get a transcoding request for external images?
if key.startswith('http'):
path = key
elif key.startswith('/'): # internal full path.
path = 'http://127.0.0.1:32400' + key
else: # internal path, add-on
path = 'http://127.0.0.1:32400' + path + '/' + key
# This is bogus (note the extra path component) but ATV is stupid when it
# comes to caching images, it doesn't use querystrings. Fortunately PMS is
# lenient...
transcode_path = ('/photo/:/transcode/%sx%s/%s'
% (width, height, utils.quote_plus(path)))
args = {
'width': width,
'height': height,
'url': path
}
if AuthToken:
args['X-Plex-Token'] = AuthToken
return utils.extend_url(transcode_path, args)
class File(object):
def fullpath(self, force_first_media=True, force_addon=False,
direct_paths=None, omit_check=False, force_check=False):
"""
Returns a "fully qualified path" add-on paths or direct paths
depending on the current settings as the tupple
(fullpath, path, filename)
as unicode. Add-on paths are returned as a fallback. Returns None
if something went wrong.
firce_first_media=False prompts the user to choose which version of the
media should be returned, if several are present
force_addon=True will always return the add-on path
direct_path=True if you're calling from another Plex python
instance - because otherwise direct paths will
evaluate to False!
"""
direct_paths = app.SYNC.direct_paths if direct_paths is None \
else direct_paths
if (not direct_paths or force_addon or
self.plex_type == v.PLEX_TYPE_CLIP):
if self.plex_type == v.PLEX_TYPE_SONG:
return self._music_addon_paths(force_first_media)
if self.plex_type == v.PLEX_TYPE_EPISODE:
# need to include the plex show id in the path
path = ('plugin://plugin.video.plexkodiconnect.tvshows/%s/'
% self.grandparent_id())
else:
path = 'plugin://%s/' % v.ADDON_TYPE[self.plex_type]
# Filename in Kodi will end with actual filename - hopefully
# this is useful for other add-ons
filename = self.file_path(force_first_media=force_first_media)
if filename:
if '/' in filename:
filename = filename.rsplit('/', 1)[1]
else:
filename = filename.rsplit('\\', 1)[1]
entirepath = ('%s?mode=play&plex_id=%s&plex_type=%s&filename=%s'
% (path, self.plex_id, self.plex_type, filename))
else:
# E.g. clips or albums
entirepath = ('%s?mode=play&plex_id=%s&plex_type=%s'
% (path, self.plex_id, self.plex_type))
# For Kodi DB, we need to safe the ENTIRE path for filenames
filename = entirepath
else:
entirepath = self.validate_playurl(
self.file_path(force_first_media=force_first_media),
self.plex_type,
force_check=force_check,
omit_check=omit_check)
try:
if '/' in entirepath:
filename = entirepath.rsplit('/', 1)[1]
else:
filename = entirepath.rsplit('\\', 1)[1]
except (TypeError, IndexError):
# Fallback to add-on paths
return self.fullpath(force_first_media=force_first_media,
force_addon=True)
path = utils.rreplace(entirepath, filename, "", 1)
return entirepath, path, filename
def _music_addon_paths(self, force_first_media):
"""
For songs only. Normal add-on paths plugin://... don't work with the
Kodi music DB, hence use a "direct" url to the music file on the PMS.
"""
if self.mediastream is None and force_first_media is False:
if self.mediastream_number() is None:
return
streamno = 0 if force_first_media else self.mediastream
entirepath = "%s%s" % (app.CONN.server,
self.xml[streamno][self.part].get('key'))
entirepath = self.attach_plex_token_to_url(entirepath)
path, filename = entirepath.rsplit('/', 1)
return entirepath, path + '/', filename
def directory_path(self, section_id=None, plex_type=None, old_key=None,
synched=True):
key = self.xml.get('fastKey')
if not key:
key = self.xml.get('key')
if old_key:
key = '%s/%s' % (old_key, key)
elif not key.startswith('/'):
key = '/library/sections/%s/%s' % (section_id, key)
params = {
'mode': 'browseplex',
'key': key
}
if plex_type or self.plex_type:
params['plex_type'] = plex_type or self.plex_type
if not synched:
# No item to be found in the Kodi DB
params['synched'] = 'false'
if self.xml.get('prompt'):
# User input needed, e.g. search for a movie or episode
params['prompt'] = self.xml.get('prompt')
if section_id:
params['id'] = section_id
return utils.extend_url('plugin://%s/' % v.ADDON_ID, params)
def file_name(self, force_first_media=False):
"""
Returns only the filename, e.g. 'movie.mkv' as unicode or None if not
found
"""
ans = self.file_path(force_first_media=force_first_media)
if ans is None:
return
if "\\" in ans:
# Local path
filename = ans.rsplit("\\", 1)[1]
else:
try:
# Network share
filename = ans.rsplit("/", 1)[1]
except IndexError:
# E.g. certain Plex channels
filename = None
return filename
def file_path(self, force_first_media=False):
"""
Returns the direct path to this item, e.g. '\\NAS\movies\movie.mkv'
as unicode or None
force_first_media=True:
will always use 1st media stream, e.g. when several different
files are present for the same PMS item
"""
if self.mediastream is None and force_first_media is False:
if self.mediastream_number() is None:
return
try:
if force_first_media is False:
ans = self.xml[self.mediastream][self.part].attrib['file']
else:
ans = self.xml[0][self.part].attrib['file']
except (TypeError, AttributeError, IndexError, KeyError):
return
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
"""
path = self.xml[0][0].get('key')
extension = path[path.rfind('.'):].lower()
if app.SYNC.force_transcode_pix or extension not in v.KODI_SUPPORTED_IMAGES:
# Let Plex transcode
# max width/height supported by plex image transcoder is 1920x1080
path = app.CONN.server + _transcode_image_path(
path,
app.ACCOUNT.pms_token,
"%s%s" % (app.CONN.server, path),
1920,
1080)
else:
path = self.attach_plex_token_to_url('%s%s' % (app.CONN.server, path))
# Attach Plex id to url to let it be picked up by our playqueue agent
# later
return '%s&plex_id=%s' % (path, self.plex_id)

View File

@ -1,407 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from ..utils import cast
from ..downloadutils import DownloadUtils as DU
from .. import utils, variables as v, app, path_ops, clientinfo
from .. import plex_functions as PF
LOG = getLogger('PLEX.api')
class Media(object):
def optimized_for_streaming(self):
"""
Returns True if the item's 'optimizedForStreaming' is set, False other-
wise
"""
return cast(bool, self.xml[0].get('optimizedForStreaming')) or False
def _from_part_or_media(self, key):
"""
Retrieves XML data 'key' first from the active part. If unsuccessful,
tries to retrieve the data from the Media response part.
If all fails, None is returned.
"""
return self.xml[0][self.part].get(key, self.xml[0].get(key))
def intro_markers(self):
"""
Returns a list of tuples with floats (startTimeOffset, endTimeOffset)
in Koditime or an empty list.
Each entry represents an (episode) intro that Plex detected and that
can be skipped
"""
self._scan_children()
return self._intro_markers
def video_codec(self):
"""
Returns the video codec and resolution for the child and part selected.
If any data is not found on a part-level, the Media-level data is
returned.
If that also fails (e.g. for old trailers, None is returned)
Output:
{
'videocodec': xxx, e.g. 'h264'
'resolution': xxx, e.g. '720' or '1080'
'height': xxx, e.g. '816'
'width': xxx, e.g. '1920'
'aspectratio': xxx, e.g. '1.78'
'bitrate': xxx, e.g. '10642'
'container': xxx e.g. 'mkv',
'bitDepth': xxx e.g. '8', '10'
}
"""
answ = {
'videocodec': self._from_part_or_media('videoCodec'),
'resolution': self._from_part_or_media('videoResolution'),
'height': self._from_part_or_media('height'),
'width': self._from_part_or_media('width'),
'aspectratio': self._from_part_or_media('aspectratio'),
'bitrate': self._from_part_or_media('bitrate'),
'container': self._from_part_or_media('container'),
}
try:
answ['bitDepth'] = self.xml[0][self.part][self.mediastream].get('bitDepth')
except (TypeError, AttributeError, KeyError, IndexError):
answ['bitDepth'] = None
return answ
def picture_codec(self):
"""
Returns the exif metadata of pictures. This does NOT seem to be used
reliably by Kodi skins! (e.g. not at all)
"""
return {
'exif:CameraMake': self.xml[0].get('make'), # e.g. 'Canon'
'exif:CameraModel': self.xml[0].get('model'), # e.g. 'Canon XYZ'
'exif:DateTime': self.xml.get('originallyAvailableAt', '').replace('-', ':') or None, # e.g. '2017-11-05'
'exif:Height': self.xml[0].get('height'), # e.g. '2160'
'exif:Width': self.xml[0].get('width'), # e.g. '3240'
'exif:Orientation': self.xml[0][self.part].get('orientation'), # e.g. '1'
'exif:FocalLength': self.xml[0].get('focalLength'), # TO BE VALIDATED
'exif:ExposureTime': self.xml[0].get('exposure'), # e.g. '1/1000'
'exif:ApertureFNumber': self.xml[0].get('aperture'), # e.g. 'f/5.0'
'exif:ISOequivalent': self.xml[0].get('iso'), # e.g. '1600'
# missing on Kodi side: lens, e.g. "EF50mm f/1.8 II"
}
def mediastreams(self):
"""
Returns the media streams for metadata purposes
Output: each track contains a dictionaries
{
'video': videotrack-list, 'codec', 'height', 'width',
'aspect', 'video3DFormat'
'audio': audiotrack-list, 'codec', 'channels',
'language'
'subtitle': list of subtitle languages (or "Unknown")
}
"""
videotracks = []
audiotracks = []
subtitlelanguages = []
try:
# Sometimes, aspectratio is on the "toplevel"
aspect = cast(float, self.xml[0].get('aspectRatio'))
except IndexError:
# There is no stream info at all, returning empty
return {
'video': videotracks,
'audio': audiotracks,
'subtitle': subtitlelanguages
}
# Loop over parts
for child in self.xml[0]:
container = child.get('container')
# Loop over Streams
for stream in child:
media_type = int(stream.get('streamType', 999))
track = {}
if media_type == 1: # Video streams
if 'codec' in stream.attrib:
track['codec'] = stream.get('codec').lower()
if "msmpeg4" in track['codec']:
track['codec'] = "divx"
elif "mpeg4" in track['codec']:
pass
elif "h264" in track['codec']:
if container in ("mp4", "mov", "m4v"):
track['codec'] = "avc1"
track['height'] = cast(int, stream.get('height'))
track['width'] = cast(int, stream.get('width'))
# track['Video3DFormat'] = item.get('Video3DFormat')
track['aspect'] = cast(float,
stream.get('aspectRatio') or aspect)
track['duration'] = self.runtime()
track['video3DFormat'] = None
videotracks.append(track)
elif media_type == 2: # Audio streams
if 'codec' in stream.attrib:
track['codec'] = stream.get('codec').lower()
if ("dca" in track['codec'] and
"ma" in stream.get('profile', '').lower()):
track['codec'] = "dtshd_ma"
track['channels'] = cast(int, stream.get('channels'))
# 'unknown' if we cannot get language
track['language'] = stream.get('languageCode',
utils.lang(39310).lower())
audiotracks.append(track)
elif media_type == 3: # Subtitle streams
# 'unknown' if we cannot get language
subtitlelanguages.append(
stream.get('languageCode', utils.lang(39310)).lower())
return {
'video': videotracks,
'audio': audiotracks,
'subtitle': subtitlelanguages
}
def mediastream_number(self):
"""
Returns the Media stream as an int (mostly 0). Will let the user choose
if several media streams are present for a PMS item (if settings are
set accordingly)
Returns None if the user aborted selection (leaving self.mediastream at
its default of None)
"""
# How many streams do we have?
count = 0
for entry in self.xml.iterfind('./Media'):
count += 1
if (count > 1 and (
(self.plex_type != v.PLEX_TYPE_CLIP and
utils.settings('firstVideoStream') == 'false')
or
(self.plex_type == v.PLEX_TYPE_CLIP and
utils.settings('bestTrailer') == 'false'))):
# Several streams/files available.
dialoglist = []
for entry in self.xml.iterfind('./Media'):
# Get additional info (filename / languages)
if 'file' in entry[0].attrib:
option = entry[0].get('file')
option = path_ops.basename(option)
else:
option = self.title() or ''
# Languages of audio streams
languages = []
for stream in entry[0]:
if (cast(int, stream.get('streamType')) == 1 and
'language' in stream.attrib):
language = stream.get('language')
languages.append(language)
languages = ', '.join(languages)
if languages:
if option:
option = '%s (%s): ' % (option, languages)
else:
option = '%s: ' % languages
else:
option = '%s ' % option
if 'videoResolution' in entry.attrib:
res = entry.get('videoResolution')
option = '%s%sp ' % (option, res)
if 'videoCodec' in entry.attrib:
codec = entry.get('videoCodec')
option = '%s%s' % (option, codec)
option = option.strip() + ' - '
if 'audioProfile' in entry.attrib:
profile = entry.get('audioProfile')
option = '%s%s ' % (option, profile)
if 'audioCodec' in entry.attrib:
codec = entry.get('audioCodec')
option = '%s%s ' % (option, codec)
option = cast(str, option.strip())
dialoglist.append(option)
media = utils.dialog('select', 'Select stream', dialoglist)
LOG.info('User chose media stream number: %s', media)
if media == -1:
LOG.info('User cancelled media stream selection')
return
else:
media = 0
self.mediastream = media
return media
def transcode_video_path(self, action, quality=None):
"""
To be called on a VIDEO level of PMS xml response!
Transcode Video support; returns the URL to get a media started
Input:
action 'DirectPlay'
'DirectStream'
'Transcode'
quality: {
'videoResolution': e.g. '1024x768',
'videoQuality': e.g. '60',
'maxVideoBitrate': e.g. '2000' (in kbits)
}
(one or several of these options)
Output:
final URL to pull in PMS transcoder
TODO: mediaIndex
"""
if self.mediastream is None and self.mediastream_number() is None:
return
headers = clientinfo.getXArgsDeviceInfo()
if action == v.PLAYBACK_METHOD_DIRECT_PLAY:
path = self.xml[self.mediastream][self.part].get('key')
# e.g. Trailers already feature an '?'!
return utils.extend_url(app.CONN.server + path, headers)
# Direct Streaming and Transcoding
arguments = PF.transcoding_arguments(path=self.path_and_plex_id(),
media=self.mediastream,
part=self.part,
playmethod=action,
args=quality)
headers.update(arguments)
# Path/key to VIDEO item of xml PMS response is needed, not part
path = self.xml.get('key')
transcode_path = app.CONN.server + \
'/video/:/transcode/universal/start.m3u8'
return utils.extend_url(transcode_path, headers)
def cache_external_subs(self):
"""
Downloads external subtitles temporarily to Kodi and returns a list
of their paths
"""
externalsubs = []
try:
mediastreams = self.xml[0][self.part]
except (TypeError, KeyError, IndexError):
return externalsubs
for stream in mediastreams:
# Since plex returns all possible tracks together, have to pull
# only external subtitles - only for these a 'key' exists
if int(stream.get('streamType')) != 3 or 'key' not in stream.attrib:
# Not a subtitle or not not an external subtitle
continue
try:
path = self.download_external_subtitles(
'{server}%s' % stream.get('key'),
stream.get('displayTitle'),
stream.get('codec'))
except IOError:
# Catch "IOError: [Errno 22] invalid mode ('wb') or filename"
# Due to stream.get('displayTitle') returning chars that our
# OS is not supporting, e.g. "српски језик (SRT External)"
path = self.download_external_subtitles(
'{server}%s' % stream.get('key'),
stream.get('languageCode', 'Unknown'),
stream.get('codec'))
if path:
externalsubs.append(path)
LOG.info('Found external subs: %s', externalsubs)
return externalsubs
@staticmethod
def download_external_subtitles(url, filename, extension):
"""
One cannot pass the subtitle language for ListItems. Workaround; will
download the subtitle at url to the Kodi PKC directory in a temp dir
Returns the path to the downloaded subtitle or None
"""
path = path_ops.create_unique_path(v.EXTERNAL_SUBTITLE_TEMP_PATH,
filename,
extension)
response = DU().downloadUrl(url, return_response=True)
if not response.ok:
LOG.error('Could not temporarily download subtitle %s', url)
LOG.error('HTTP status: %s, message: %s',
response.status_code, response.text)
return
LOG.debug('Writing temp subtitle to %s', path)
with open(path_ops.encode_path(path), 'wb') as f:
f.write(response.content)
return path
def validate_playurl(self, path, typus, force_check=False, folder=False,
omit_check=False):
"""
Returns a valid path for Kodi, e.g. with '\' substituted to '\\' in
Unicode. Returns None if this is not possible
path : Unicode
typus : Plex type from PMS xml
force_check : Will always try to check validity of path
Will also skip confirmation dialog if path not found
folder : Set to True if path is a folder
omit_check : Will entirely omit validity check if True
"""
if path is None:
return
typus = v.REMAP_TYPE_FROM_PLEXTYPE[typus]
if app.SYNC.remap_path:
path = path.replace(getattr(app.SYNC, 'remapSMB%sOrg' % typus),
getattr(app.SYNC, 'remapSMB%sNew' % typus),
1)
# There might be backslashes left over:
path = path.replace('\\', '/')
elif app.SYNC.replace_smb_path:
if path.startswith('\\\\'):
path = 'smb:' + path.replace('\\', '/')
if app.SYNC.escape_path:
path = utils.escape_path(path, app.SYNC.escape_path_safe_chars)
if (app.SYNC.path_verified and not force_check) or omit_check:
return path
# exist() needs a / or \ at the end to work for directories
if not folder:
# files
check = path_ops.exists(path)
else:
# directories
if "\\" in path:
if not path.endswith('\\'):
# Add the missing backslash
check = path_ops.exists(path + "\\")
else:
check = path_ops.exists(path)
else:
if not path.endswith('/'):
check = path_ops.exists(path + "/")
else:
check = path_ops.exists(path)
if not check:
if force_check is False:
# Validate the path is correct with user intervention
if self.ask_to_validate(path):
app.APP.stop_threads(block=False)
path = None
app.SYNC.path_verified = True
else:
path = None
elif not force_check:
# Only set the flag if we were not force-checking the path
app.SYNC.path_verified = True
return path
@staticmethod
def ask_to_validate(url):
"""
Displays a YESNO dialog box:
Kodi can't locate file: <url>. Please verify the path.
You may need to verify your network credentials in the
add-on settings or use different Plex paths. Stop syncing?
Returns True if sync should stop, else False
"""
LOG.warn('Cannot access file: %s', url)
# Kodi cannot locate the file #s. Please verify your PKC settings. Stop
# syncing?
return utils.yesno_dialog(utils.lang(29999), utils.lang(39031) % url)

View File

@ -1,107 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from ..utils import cast
class Playback(object):
def decision_code(self):
"""
Returns the general_play_decision_code or mde_play_decision_code if
not available. Returns None if something went wrong
"""
return self.general_play_decision_code() or self.mde_play_decision_code()
def general_play_decision_code(self):
"""
Returns the 'generalDecisionCode' as an int or None
Generally, the 1xxx codes constitute a a success decision, 2xxx a
general playback error, 3xxx a direct play error, and 4xxx a transcode
error.
General decisions can include:
1000: Direct play OK.
1001: Direct play not available; Conversion OK.
2000: Neither direct play nor conversion is available.
2001: Not enough bandwidth for any playback of this item.
2002: Number of allowed streams has been reached. Stop a playback or ask
admin for more permissions.
2003: File is unplayable.
2004: Streaming Session doesnt exist or timed out.
2005: Client stopped playback.
2006: Admin Terminated Playback.
"""
return cast(int, self.xml.get('generalDecisionCode'))
def general_play_decision_text(self):
"""
Returns the text associated with the general_play_decision_code() as
text in unicode or None
"""
return self.xml.get('generalDecisionText')
def mde_play_decision_code(self):
return cast(int, self.xml.get('mdeDecisionCode'))
def mde_play_decision_text(self):
"""
Returns the text associated with the mde_play_decision_code() as
text in unicode or None
"""
return self.xml.get('mdeDecisionText')
def direct_play_decision_code(self):
return cast(int, self.xml.get('directPlayDecisionCode'))
def direct_play_decision_text(self):
"""
Returns the text associated with the mde_play_decision_code() as
text in unicode or None
"""
return self.xml.get('directPlayDecisionText')
def transcode_decision_code(self):
return cast(int, self.xml.get('directPlayDecisionCode'))
def transcode_decision_text(self):
"""
Returns the text associated with the mde_play_decision_code() as
text in unicode or None
"""
return self.xml.get('directPlayDecisionText')
def video_decision(self):
"""
Returns "copy" if PMS streaming brain decided to DirectStream, so copy
an existing video stream into a new container. Returns "transcode" if
the video stream will be transcoded.
Raises IndexError if something went wrong. Might also return None
"""
for stream in self.xml[0][0][0]:
if stream.get('streamType') == '1':
return stream.get('decision')
def audio_decision(self):
"""
Returns "copy" if PMS streaming brain decided to DirectStream, so copy
an existing audio stream into a new container. Returns "transcode" if
the audio stream will be transcoded.
Raises IndexError if something went wrong. Might also return None
"""
for stream in self.xml[0][0][0]:
if stream.get('streamType') == '2':
return stream.get('decision')
def subtitle_decision(self):
"""
Returns the PMS' decision on the subtitle stream.
Raises IndexError if something went wrong. Might also return None
"""
for stream in self.xml[0][0][0]:
if stream.get('streamType') == '3':
return stream.get('decision')

View File

@ -1,59 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from ..utils import cast
from .. import timing, variables as v, app
class User(object):
def viewcount(self):
"""
Returns the play count for the item as an int or the int 0 if not found
"""
return cast(int, self.xml.get('viewCount')) or 0
def resume_point(self):
"""
Returns the resume point of time in seconds as float. 0.0 if not found
"""
resume = cast(float, self.xml.get('viewOffset')) or 0.0
return resume * v.PLEX_TO_KODI_TIMEFACTOR
def resume_point_plex(self):
"""
Returns the resume point of time in microseconds as float.
0.0 if not found
"""
return cast(float, self.xml.get('viewOffset')) or 0.0
def userrating(self):
"""
Returns the userRating [int].
If the user chose to replace user ratings with the number of different
file versions for a specific video, that number is returned instead
(at most 10)
0 is returned if something goes wrong
"""
if (app.SYNC.indicate_media_versions is True and
self.plex_type in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_EPISODE)):
userrating = 0
for _ in self.xml.findall('./Media'):
userrating += 1
# Don't show a value of '1' - which we'll always have for normal
# Plex library items
return 0 if userrating == 1 else min(userrating, 10)
else:
return cast(int, self.xml.get('userRating')) or 0
def lastplayed(self):
"""
Returns the Kodi timestamp [unicode] for the last point of time, when
this item was played.
Returns None if this fails - item has never been played
"""
try:
return timing.plex_date_to_kodi(int(self.xml.get('lastViewedAt')))
except TypeError:
pass

View File

@ -14,14 +14,11 @@ from .plexbmchelper import listener, plexgdm, subscribers, httppersist
from .plex_api import API
from . import utils
from . import plex_functions as PF
from . import playlist_func as PL
from . import playback
from . import json_rpc as js
from . import playqueue as PQ
from . import variables as v
from . import backgroundthread
from . import app
from . import exceptions
###############################################################################
@ -34,46 +31,31 @@ def update_playqueue_from_PMS(playqueue,
playqueue_id=None,
repeat=None,
offset=None,
transient_token=None,
start_plex_id=None):
transient_token=None):
"""
Completely updates the Kodi playqueue with the new Plex playqueue. Pass
in playqueue_id if we need to fetch a new playqueue
repeat = 0, 1, 2
offset = time offset in Plextime (milliseconds)
Will (re)start playback
"""
LOG.info('New playqueue %s received from Plex companion with offset '
'%s, repeat %s, start_plex_id %s',
playqueue_id, offset, repeat, start_plex_id)
'%s, repeat %s', playqueue_id, offset, repeat)
# Safe transient token from being deleted
if transient_token is None:
transient_token = playqueue.plex_transient_token
with app.APP.lock_playqueues:
try:
xml = PL.get_PMS_playlist(playqueue, playqueue_id)
except exceptions.PlaylistError:
xml = PQ.get_PMS_playlist(playlist_id=playqueue_id)
if xml is None:
LOG.error('Could now download playqueue %s', playqueue_id)
return
if playqueue.id == playqueue_id:
# This seems to be happening ONLY if a Plex Companion device
# reconnects and Kodi is already playing something - silly, really
# For all other cases, a new playqueue is generated by Plex
LOG.debug('Update for existing playqueue detected')
return
playqueue.clear()
# Get new metadata for the playqueue first
try:
PL.get_playlist_details_from_xml(playqueue, xml)
except exceptions.PlaylistError:
LOG.error('Could not get playqueue ID %s', playqueue_id)
return
playqueue.repeat = 0 if not repeat else int(repeat)
playqueue.plex_transient_token = transient_token
playback.play_xml(playqueue,
xml,
offset=offset,
start_plex_id=start_plex_id)
raise PQ.PlayqueueError()
app.PLAYSTATE.initiated_by_plex = True
playqueue.init_from_xml(xml,
offset=offset,
repeat=0 if not repeat else int(repeat),
transient_token=transient_token)
class PlexCompanion(backgroundthread.KillableThread):
@ -93,47 +75,47 @@ class PlexCompanion(backgroundthread.KillableThread):
@staticmethod
def _process_alexa(data):
if 'key' not in data or 'containerKey' not in data:
LOG.error('Received malformed Alexa data: %s', data)
return
app.PLAYSTATE.initiated_by_plex = True
xml = PF.GetPlexMetadata(data['key'])
try:
xml[0].attrib
except (AttributeError, IndexError, TypeError):
LOG.error('Could not download Plex metadata for: %s', data)
return
raise PQ.PlayqueueError()
api = API(xml[0])
if api.plex_type == v.PLEX_TYPE_ALBUM:
if api.plex_type() == v.PLEX_TYPE_ALBUM:
LOG.debug('Plex music album detected')
PQ.init_playqueue_from_plex_children(
api.plex_id,
transient_token=data.get('token'))
xml = PF.GetAllPlexChildren(api.plex_id())
try:
xml[0].attrib
except (TypeError, IndexError, AttributeError):
LOG.error('Could not download the album xml for %s', data)
raise PQ.PlayqueueError()
playqueue = PQ.get_playqueue_from_type('audio')
playqueue.init_from_xml(xml,
transient_token=data.get('token'))
elif data['containerKey'].startswith('/playQueues/'):
_, container_key, _ = PF.ParseContainerKey(data['containerKey'])
xml = PF.DownloadChunks('{server}/playQueues/%s' % container_key)
if xml is None:
# "Play error"
utils.dialog('notification',
utils.lang(29999),
utils.lang(30128),
icon='{error}')
return
LOG.error('Could not get playqueue for %s', data)
raise PQ.PlayqueueError()
playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type])
playqueue.clear()
PL.get_playlist_details_from_xml(playqueue, xml)
playqueue.plex_transient_token = data.get('token')
if data.get('offset') != '0':
offset = float(data['offset']) / 1000.0
else:
offset = None
playback.play_xml(playqueue, xml, offset)
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()])
offset = utils.cast(float, data.get('offset')) or None
if offset:
offset = offset / 1000.0
playqueue.init_from_xml(xml,
offset=offset,
transient_token=data.get('token'))
else:
app.CONN.plex_transient_token = data.get('token')
playback.playback_triage(api.plex_id,
api.plex_type,
resolve=False,
resume=data.get('offset') not in ('0', None))
if utils.cast(float, data.get('offset')):
app.PLAYSTATE.resume_playback = True
path = ('http://127.0.0.1:%s/plex/play/file.strm?plex_id=%s'
% (v.WEBSERVICE_PORT, api.plex_id()))
path += '&plex_type=%s' % api.plex_type()
executebuiltin(('PlayMedia(%s)' % path).encode('utf-8'))
@staticmethod
def _process_node(data):
@ -151,9 +133,6 @@ class PlexCompanion(backgroundthread.KillableThread):
@staticmethod
def _process_playlist(data):
if 'containerKey' not in data:
LOG.error('Received malformed playlist data: %s', data)
return
# Get the playqueue ID
_, container_key, query = PF.ParseContainerKey(data['containerKey'])
try:
@ -167,59 +146,52 @@ class PlexCompanion(backgroundthread.KillableThread):
xml[0].attrib
except (AttributeError, IndexError, TypeError):
LOG.error('Could not download Plex metadata')
return
raise PQ.PlayqueueError()
api = API(xml[0])
playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type])
key = data.get('key')
if key:
_, key, _ = PF.ParseContainerKey(key)
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()])
update_playqueue_from_PMS(playqueue,
playqueue_id=container_key,
repeat=query.get('repeat'),
offset=utils.cast(int, data.get('offset')),
transient_token=data.get('token'),
start_plex_id=key)
offset=utils.cast(float, data.get('offset')) or None,
transient_token=data.get('token'))
@staticmethod
def _process_streams(data):
"""
Plex Companion client adjusted audio or subtitle stream
"""
if 'type' not in data:
LOG.error('Received malformed stream data: %s', data)
return
playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']])
pos = js.get_position(playqueue.playlistid)
if 'audioStreamID' in data:
index = playqueue.items[pos].kodi_stream_index(
data['audioStreamID'], 'audio')
app.APP.player.setAudioStream(index)
elif 'subtitleStreamID' in data:
if data['subtitleStreamID'] == '0':
app.APP.player.showSubtitles(False)
else:
try:
pos = js.get_position(playqueue.playlistid)
if 'audioStreamID' in data:
index = playqueue.items[pos].kodi_stream_index(
data['subtitleStreamID'], 'subtitle')
app.APP.player.setSubtitleStream(index)
else:
LOG.error('Unknown setStreams command: %s', data)
data['audioStreamID'], 'audio')
app.APP.player.setAudioStream(index)
elif 'subtitleStreamID' in data:
if data['subtitleStreamID'] == '0':
app.APP.player.showSubtitles(False)
else:
index = playqueue.items[pos].kodi_stream_index(
data['subtitleStreamID'], 'subtitle')
app.APP.player.setSubtitleStream(index)
else:
LOG.error('Unknown setStreams command: %s', data)
except KeyError:
LOG.warn('Could not process stream data: %s', data)
@staticmethod
def _process_refresh(data):
"""
example data: {'playQueueID': '8475', 'commandID': '11'}
"""
if 'playQueueID' not in data:
LOG.error('Received malformed refresh data: %s', data)
return
xml = PL.get_pms_playqueue(data['playQueueID'])
xml = PQ.get_pms_playqueue(data['playQueueID'])
if xml is None:
return
if len(xml) == 0:
LOG.debug('Empty playqueue received - clearing playqueue')
plex_type = PL.get_plextype_from_xml(xml)
plex_type = PQ.get_plextype_from_xml(xml)
if plex_type is None:
return
playqueue = PQ.get_playqueue_from_type(
@ -247,23 +219,29 @@ class PlexCompanion(backgroundthread.KillableThread):
"""
LOG.debug('Processing: %s', task)
data = task['data']
if task['action'] == 'alexa':
with app.APP.lock_playqueues:
self._process_alexa(data)
elif (task['action'] == 'playlist' and
data.get('address') == 'node.plexapp.com'):
self._process_node(data)
elif task['action'] == 'playlist':
with app.APP.lock_playqueues:
self._process_playlist(data)
elif task['action'] == 'refreshPlayQueue':
with app.APP.lock_playqueues:
self._process_refresh(data)
elif task['action'] == 'setStreams':
try:
try:
if task['action'] == 'alexa':
with app.APP.lock_playqueues:
self._process_alexa(data)
elif (task['action'] == 'playlist' and
data.get('address') == 'node.plexapp.com'):
self._process_node(data)
elif task['action'] == 'playlist':
with app.APP.lock_playqueues:
self._process_playlist(data)
elif task['action'] == 'refreshPlayQueue':
with app.APP.lock_playqueues:
self._process_refresh(data)
elif task['action'] == 'setStreams':
self._process_streams(data)
except KeyError:
pass
except PQ.PlayqueueError:
LOG.error('Could not process companion data: %s', data)
# "Play Error"
utils.dialog('notification',
utils.lang(29999),
utils.lang(30128),
icon='{error}')
app.PLAYSTATE.initiated_by_plex = False
def run(self):
"""
@ -306,7 +284,7 @@ class PlexCompanion(backgroundthread.KillableThread):
subscription_manager,
('', v.COMPANION_PORT),
listener.MyHandler)
httpd.timeout = 10.0
httpd.timeout = 0.95
break
except Exception:
LOG.error("Unable to start PlexCompanion. Traceback:")
@ -325,13 +303,12 @@ class PlexCompanion(backgroundthread.KillableThread):
if httpd:
thread = Thread(target=httpd.handle_request)
while not self.should_cancel():
while not self.isCanceled():
# If we are not authorized, sleep
# Otherwise, we trigger a download which leads to a
# re-authorizations
if self.should_suspend():
if self.wait_while_suspended():
break
if self.wait_while_suspended():
break
try:
message_count += 1
if httpd:
@ -370,6 +347,6 @@ class PlexCompanion(backgroundthread.KillableThread):
app.APP.companion_queue.task_done()
# Don't sleep
continue
self.sleep(0.05)
app.APP.monitor.waitForAbort(0.05)
subscription_manager.signal_stop()
client.stop_all()

View File

@ -12,3 +12,18 @@ from .sections import Sections
class PlexDB(PlexDBBase, TVShows, Movies, Music, Playlists, Sections):
pass
def kodi_from_plex(plex_id, plex_type=None):
"""
Returns the tuple (kodi_id, kodi_type) for plex_id. Faster, if plex_type
is provided
Returns (None, None) if unsuccessful
"""
with PlexDB(lock=False) as plexdb:
db_item = plexdb.item_by_id(plex_id, plex_type)
if db_item:
return (db_item['kodi_id'], db_item['kodi_type'])
else:
return None, None

View File

@ -3,7 +3,7 @@
from __future__ import absolute_import, division, unicode_literals
from threading import Lock
from .. import db, variables as v
from .. import utils, variables as v
PLEXDB_LOCK = Lock()
@ -20,19 +20,18 @@ SUPPORTED_KODI_TYPES = (
class PlexDBBase(object):
"""
Plex database methods used for all types of items.
Plex database methods used for all types of items
"""
def __init__(self, plexconn=None, lock=True, copy=False):
def __init__(self, plexconn=None, lock=True):
# Allows us to use this class with a cursor instead of context mgr
self.plexconn = plexconn
self.cursor = self.plexconn.cursor() if self.plexconn else None
self.lock = lock
self.copy = copy
def __enter__(self):
if self.lock:
PLEXDB_LOCK.acquire()
self.plexconn = db.connect('plex-copy' if self.copy else 'plex')
self.plexconn = utils.kodi_sql('plex')
self.cursor = self.plexconn.cursor()
return self
@ -195,9 +194,9 @@ def initialize():
plexdb.cursor.execute('''
CREATE TABLE IF NOT EXISTS sections(
section_id INTEGER PRIMARY KEY,
uuid TEXT,
section_name TEXT,
plex_type TEXT,
kodi_tagid INTEGER,
sync_to_kodi INTEGER,
last_sync INTEGER)
''')
@ -206,6 +205,7 @@ def initialize():
plex_id INTEGER PRIMARY KEY,
checksum INTEGER UNIQUE,
section_id INTEGER,
section_uuid TEXT,
kodi_id INTEGER,
kodi_fileid INTEGER,
kodi_pathid INTEGER,
@ -217,6 +217,7 @@ def initialize():
plex_id INTEGER PRIMARY KEY,
checksum INTEGER UNIQUE,
section_id INTEGER,
section_uuid TEXT,
kodi_id INTEGER,
kodi_pathid INTEGER,
fanart_synced INTEGER,
@ -227,6 +228,7 @@ def initialize():
plex_id INTEGER PRIMARY KEY,
checksum INTEGER UNIQUE,
section_id INTEGER,
section_uuid TEXT,
show_id INTEGER,
parent_id INTEGER,
kodi_id INTEGER,
@ -238,6 +240,7 @@ def initialize():
plex_id INTEGER PRIMARY KEY,
checksum INTEGER UNIQUE,
section_id INTEGER,
section_uuid TEXT,
show_id INTEGER,
grandparent_id INTEGER,
season_id INTEGER,
@ -254,6 +257,7 @@ def initialize():
plex_id INTEGER PRIMARY KEY,
checksum INTEGER UNIQUE,
section_id INTEGER,
section_uuid TEXT,
kodi_id INTEGER,
last_sync INTEGER)
''')
@ -262,6 +266,7 @@ def initialize():
plex_id INTEGER PRIMARY KEY,
checksum INTEGER UNIQUE,
section_id INTEGER,
section_uuid TEXT,
artist_id INTEGER,
parent_id INTEGER,
kodi_id INTEGER,
@ -272,6 +277,7 @@ def initialize():
plex_id INTEGER PRIMARY KEY,
checksum INTEGER UNIQUE,
section_id INTEGER,
section_uuid TEXT,
artist_id INTEGER,
grandparent_id INTEGER,
album_id INTEGER,
@ -306,22 +312,18 @@ def initialize():
'CREATE INDEX IF NOT EXISTS ix_track_1 ON track (last_sync)',
'CREATE UNIQUE INDEX IF NOT EXISTS ix_track_2 ON track (kodi_id)',
'CREATE UNIQUE INDEX IF NOT EXISTS ix_playlists_2 ON playlists (kodi_path)',
'CREATE INDEX IF NOT EXISTS ix_playlists_3 ON playlists (kodi_hash)',
'CREATE UNIQUE INDEX IF NOT EXISTS ix_playlists_3 ON playlists (kodi_hash)',
)
for cmd in commands:
plexdb.cursor.execute(cmd)
def wipe(table=None):
def wipe():
"""
Completely resets the Plex database.
If a table [unicode] name is provided, only that table will be dropped
Completely resets the Plex database
"""
with PlexDBBase() as plexdb:
if table:
tables = [table]
else:
plexdb.cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table'")
tables = [i[0] for i in plexdb.cursor.fetchall()]
plexdb.cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table'")
tables = [i[0] for i in plexdb.cursor.fetchall()]
for table in tables:
plexdb.cursor.execute('DROP table IF EXISTS %s' % table)

View File

@ -5,8 +5,8 @@ from .. import variables as v
class Movies(object):
def add_movie(self, plex_id, checksum, section_id, kodi_id, kodi_fileid,
kodi_pathid, last_sync):
def add_movie(self, plex_id, checksum, section_id, section_uuid, kodi_id,
kodi_fileid, kodi_pathid, last_sync):
"""
Appends or replaces an entry into the plex table for movies
"""
@ -15,18 +15,20 @@ class Movies(object):
plex_id,
checksum,
section_id,
section_uuid,
kodi_id,
kodi_fileid,
kodi_pathid,
fanart_synced,
last_sync)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
'''
self.cursor.execute(
query,
(plex_id,
checksum,
section_id,
section_uuid,
kodi_id,
kodi_fileid,
kodi_pathid,
@ -39,6 +41,7 @@ class Movies(object):
plex_id INTEGER PRIMARY KEY ASC,
checksum INTEGER UNIQUE,
section_id INTEGER,
section_uuid TEXT,
kodi_id INTEGER,
kodi_fileid INTEGER,
kodi_pathid INTEGER,
@ -61,9 +64,10 @@ class Movies(object):
'plex_id': entry[0],
'checksum': entry[1],
'section_id': entry[2],
'kodi_id': entry[3],
'kodi_fileid': entry[4],
'kodi_pathid': entry[5],
'fanart_synced': entry[6],
'last_sync': entry[7]
'section_uuid': entry[3],
'kodi_id': entry[4],
'kodi_fileid': entry[5],
'kodi_pathid': entry[6],
'fanart_synced': entry[7],
'last_sync': entry[8]
}

View File

@ -5,7 +5,8 @@ from .. import variables as v
class Music(object):
def add_artist(self, plex_id, checksum, section_id, kodi_id, last_sync):
def add_artist(self, plex_id, checksum, section_id, section_uuid, kodi_id,
last_sync):
"""
Appends or replaces music artist entry into the plex table
"""
@ -14,20 +15,22 @@ class Music(object):
plex_id,
checksum,
section_id,
section_uuid,
kodi_id,
last_sync)
VALUES (?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?)
'''
self.cursor.execute(
query,
(plex_id,
checksum,
section_id,
section_uuid,
kodi_id,
last_sync))
def add_album(self, plex_id, checksum, section_id, artist_id, parent_id,
kodi_id, last_sync):
def add_album(self, plex_id, checksum, section_id, section_uuid, artist_id,
parent_id, kodi_id, last_sync):
"""
Appends or replaces an entry into the plex table
"""
@ -36,24 +39,27 @@ class Music(object):
plex_id,
checksum,
section_id,
section_uuid,
artist_id,
parent_id,
kodi_id,
last_sync)
VALUES (?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
'''
self.cursor.execute(
query,
(plex_id,
checksum,
section_id,
section_uuid,
artist_id,
parent_id,
kodi_id,
last_sync))
def add_song(self, plex_id, checksum, section_id, artist_id, grandparent_id,
album_id, parent_id, kodi_id, kodi_pathid, last_sync):
def add_song(self, plex_id, checksum, section_id, section_uuid, artist_id,
grandparent_id, album_id, parent_id, kodi_id, kodi_pathid,
last_sync):
"""
Appends or replaces an entry into the plex table
"""
@ -62,6 +68,7 @@ class Music(object):
plex_id,
checksum,
section_id,
section_uuid,
artist_id,
grandparent_id,
album_id,
@ -69,13 +76,14 @@ class Music(object):
kodi_id,
kodi_pathid,
last_sync)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
'''
self.cursor.execute(
query,
(plex_id,
checksum,
section_id,
section_uuid,
artist_id,
grandparent_id,
album_id,
@ -90,6 +98,7 @@ class Music(object):
plex_id INTEGER PRIMARY KEY,
checksum INTEGER UNIQUE,
section_id INTEGER,
section_uuid TEXT,
kodi_id INTEGER,
last_sync INTEGER
"""
@ -105,6 +114,7 @@ class Music(object):
plex_id INTEGER PRIMARY KEY,
checksum INTEGER UNIQUE,
section_id INTEGER,
section_uuid TEXT,
artist_id INTEGER, # plex_id of the parent artist
parent_id INTEGER, # kodi_id of the parent artist
kodi_id INTEGER,
@ -122,6 +132,7 @@ class Music(object):
plex_id INTEGER PRIMARY KEY,
checksum INTEGER UNIQUE,
section_id INTEGER,
section_uuid TEXT,
artist_id INTEGER, # plex_id of the parent artist
grandparent_id INTEGER, # kodi_id of the parent artist
album_id INTEGER, # plex_id of the parent album
@ -146,13 +157,14 @@ class Music(object):
'plex_id': entry[0],
'checksum': entry[1],
'section_id': entry[2],
'artist_id': entry[3],
'grandparent_id': entry[4],
'album_id': entry[5],
'parent_id': entry[6],
'kodi_id': entry[7],
'kodi_pathid': entry[8],
'last_sync': entry[9]
'section_uuid': entry[3],
'artist_id': entry[4],
'grandparent_id': entry[5],
'album_id': entry[6],
'parent_id': entry[7],
'kodi_id': entry[8],
'kodi_pathid': entry[9],
'last_sync': entry[10]
}
@staticmethod
@ -165,10 +177,11 @@ class Music(object):
'plex_id': entry[0],
'checksum': entry[1],
'section_id': entry[2],
'artist_id': entry[3],
'parent_id': entry[4],
'kodi_id': entry[5],
'last_sync': entry[6]
'section_uuid': entry[3],
'artist_id': entry[4],
'parent_id': entry[5],
'kodi_id': entry[6],
'last_sync': entry[7]
}
@staticmethod
@ -181,8 +194,9 @@ class Music(object):
'plex_id': entry[0],
'checksum': entry[1],
'section_id': entry[2],
'kodi_id': entry[3],
'last_sync': entry[4]
'section_uuid': entry[3],
'kodi_id': entry[4],
'last_sync': entry[5]
}
def album_has_songs(self, plex_id):

Some files were not shown because too many files have changed in this diff Show More