Compare commits
74 commits
master
...
webserver-
Author | SHA1 | Date | |
---|---|---|---|
|
6cba4a1d01 | ||
|
48d288ac53 | ||
|
27c4c6ac38 | ||
|
e204ef9849 | ||
|
7e676eb043 | ||
|
a650c42cfd | ||
|
0f9e754815 | ||
|
bb2fff5909 | ||
|
725416751f | ||
|
6e692d22c2 | ||
|
fb21bc7d71 | ||
|
d3752e1958 | ||
|
3d4bde878e | ||
|
9d517c2c3d | ||
|
d397fb5b20 | ||
|
f7237d7033 | ||
|
9d79f78190 | ||
|
7725af5a6f | ||
|
0ce29dc0ce | ||
|
ea4a062aac | ||
|
a1f4960bca | ||
|
1d01f4794e | ||
|
cef07c3598 | ||
|
7616d6dc26 | ||
|
b586ac09c4 | ||
|
dc56c2a6a2 | ||
|
1123a2ee3c | ||
|
8ad6d1bcce | ||
|
45fc9fa8be | ||
|
353cb04532 | ||
|
48cda467c3 | ||
|
6bd98fcefd | ||
|
1218cde0a2 | ||
|
578ced789f | ||
|
0d8b3b3ba7 | ||
|
8660b12d15 | ||
|
bbd8e18002 | ||
|
7753903c05 | ||
|
4fa1f48b43 | ||
|
130ec674e5 | ||
|
2dac26ffc4 | ||
|
3aa5c87ca0 | ||
|
c63d9ad4d6 | ||
|
95b37b51f5 | ||
|
d380aa8ac3 | ||
|
885e8dd581 | ||
|
ac285467c4 | ||
|
61ff2b72f3 | ||
|
0acf470343 | ||
|
9a9bc9f0eb | ||
|
643e6171c4 | ||
|
ad6c160524 | ||
|
b11ca48294 | ||
|
fe52efd88e | ||
|
8c614f3e47 | ||
|
0d36a2a3b9 | ||
|
f4c3674bc2 | ||
|
5428dafe59 | ||
|
4ed17f1a5b | ||
|
484b03482e | ||
|
797a58a3d5 | ||
|
439857a9ce | ||
|
12befecc4a | ||
|
20bffc1b41 | ||
|
d7541b7f74 | ||
|
dfcfa0edab | ||
|
20c1c6e502 | ||
|
875d704e5a | ||
|
c0035c84a6 | ||
|
4a3b38f5b6 | ||
|
16423e18ec | ||
|
059ed7a5f0 | ||
|
7c6fdad770 | ||
|
9b4584e7df |
157 changed files with 9993 additions and 22973 deletions
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
|
@ -1 +0,0 @@
|
|||
ko_fi: A8182EB
|
21
README.md
21
README.md
|
@ -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
697
addon.xml
|
@ -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>
|
||||
|
|
409
changelog.txt
409
changelog.txt
|
@ -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
|
||||
|
|
44
default.py
44
default.py
|
@ -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)
|
||||
|
|
|
@ -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}!
|
||||
|
|
|
@ -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}!
|
||||
|
|
|
@ -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
|
@ -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}!
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 d’extension de plex"
|
||||
msgstr "Paramètres d’addon 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 d’internet ?"
|
||||
msgstr "Problèmes de connexion à plex.tv. Problème de réseau ou d’internet ?"
|
||||
|
||||
# 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 "S’il 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é "
|
||||
"d’utiliser Plex musique seulement avec des chemins d’accès directs pour les "
|
||||
"grandes bibliothèques musicale. Sinon Kodi peut s’interrompre)"
|
||||
|
||||
|
@ -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 d’effectuer l'authentification. Êtes-vous connectez à plex.tv?"
|
||||
"Impossible d’effectuer 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 d’utiliser des"
|
||||
" chemins d’accè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 d’identification réseau pour permettre d’accé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 l’analyse 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 d’un type, par exemple "
|
||||
"«Films enfants» et «Films parents», assurez-vous de consulter le Wiki: "
|
||||
"Si vous utilisez plusieurs bibliothèques de Plex d’un 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, l’extension n’est pas connecté à un"
|
||||
" serveur Plex."
|
||||
"Impossible de lancer la synchronisation, l’Add-on n’est 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 n’est "
|
||||
"généralement pas recommandée et inutiles!)"
|
||||
"Réinitialiser tous les paramètres de PlexKodiConnect Addon ? (ceci n’est "
|
||||
"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"
|
||||
|
|
|
@ -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 d’extension de plex"
|
||||
msgstr "Paramètres d’addon 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 d’internet ?"
|
||||
msgstr "Problèmes de connexion à plex.tv. Problème de réseau ou d’internet ?"
|
||||
|
||||
# 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 "S’il 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é "
|
||||
"d’utiliser Plex musique seulement avec des chemins d’accès directs pour les "
|
||||
"grandes bibliothèques musicale. Sinon Kodi peut s’interrompre)"
|
||||
|
||||
|
@ -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 d’effectuer l'authentification. Êtes-vous connectez à plex.tv?"
|
||||
"Impossible d’effectuer 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 d’utiliser des"
|
||||
" chemins d’accè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 d’identification réseau pour permettre d’accé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 l’analyse 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 d’un type, par exemple "
|
||||
"«Films enfants» et «Films parents», assurez-vous de consulter le Wiki: "
|
||||
"Si vous utilisez plusieurs bibliothèques de Plex d’un 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, l’extension n’est pas connecté à un"
|
||||
" serveur Plex."
|
||||
"Impossible de lancer la synchronisation, l’Add-on n’est 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 n’est "
|
||||
"généralement pas recommandée et inutiles!)"
|
||||
"Réinitialiser tous les paramètres de PlexKodiConnect Addon ? (ceci n’est "
|
||||
"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"
|
||||
|
|
|
@ -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}!
|
||||
|
|
|
@ -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
|
@ -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}!
|
||||
|
|
|
@ -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}!
|
||||
|
|
|
@ -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
|
@ -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}!
|
||||
|
|
|
@ -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}!
|
||||
|
|
|
@ -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
|
@ -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}!
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')))
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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')))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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, ))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)')
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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()
|
|
@ -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:
|
||||
|
|
104
resources/lib/library_sync/time.py
Normal file
104
resources/lib/library_sync/time.py
Normal 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
|
|
@ -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']:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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')
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
14
resources/lib/playqueue/__init__.py
Normal file
14
resources/lib/playqueue/__init__.py
Normal 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
|
302
resources/lib/playqueue/common.py
Normal file
302
resources/lib/playqueue/common.py
Normal 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
|
164
resources/lib/playqueue/functions.py
Normal file
164
resources/lib/playqueue/functions.py
Normal 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')
|
|
@ -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)
|
604
resources/lib/playqueue/playqueue.py
Normal file
604
resources/lib/playqueue/playqueue.py
Normal 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
90
resources/lib/playstrm.py
Normal 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
372
resources/lib/playutils.py
Normal 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
1818
resources/lib/plex_api.py
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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))
|
|
@ -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)
|
|
@ -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)
|
|
@ -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 doesn’t 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')
|
|
@ -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
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue