Compare commits
18 commits
master
...
plex_for_k
Author | SHA1 | Date | |
---|---|---|---|
|
b988344c9b | ||
|
517f41b534 | ||
|
ae13a6f3cc | ||
|
b27a846292 | ||
|
c26e1283db | ||
|
b6dc458b54 | ||
|
9eba24485e | ||
|
a547dd790c | ||
|
3971e24b95 | ||
|
a58d284108 | ||
|
3388765b63 | ||
|
1a55301f24 | ||
|
504044b283 | ||
|
3045ecbccd | ||
|
dbe0339b71 | ||
|
5cdda0e334 | ||
|
59040f3b3e | ||
|
32927931c4 |
230 changed files with 28353 additions and 37787 deletions
|
@ -1,5 +0,0 @@
|
|||
exclude_paths:
|
||||
- 'resources/lib/watchdog/**'
|
||||
- 'resources/lib/pathtools/**'
|
||||
- 'resources/lib/pathtools/**'
|
||||
- 'resources/lib/defused_etree.py'
|
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
|
@ -1 +0,0 @@
|
|||
ko_fi: A8182EB
|
41
.github/ISSUE_TEMPLATE/bug_report.md
vendored
41
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -1,41 +0,0 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve. Please read the instructions carefully.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Help yourself
|
||||
* I did try to restart Kodi :-)
|
||||
* I checked the [PKC Frequently Asked Questions on the PKC wiki](https://github.com/croneter/PlexKodiConnect/wiki/faq)
|
||||
* I did try to reset the Kodi database by going to `PKC Settings -> Advanced -> "Reset the database and optionally reset PlexKodiConnect"` and then hitting YES, NO
|
||||
* I did check the [existing issues on Github](https://github.com/croneter/PlexKodiConnect/issues)
|
||||
|
||||
## Describe the bug
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
## To Reproduce
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
## Expected behavior
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
## You need to attach a KODI LOG FILE!
|
||||
A Kodi debug log file is needed that you recorded while you reproduced the bug. **Do clean your log of all Plex tokens (="Plex passwords")!!!** .
|
||||
1. Activate Kodi's debug logging by going to the Kodi `Settings` -> `System` -> `Logging`. Then toggle the `Enable debug logging` setting.
|
||||
2. Restart Kodi to start with a "fresh" log file.
|
||||
3. Reproduce the bug.
|
||||
4. Follow the [Kodi instructions](http://kodi.wiki/view/Log_file/Easy) to grab/share the Kodi log file. Usually only `kodi.log` is needed
|
||||
* You can [find the log file here](http://kodi.wiki/view/Log_file/Advanced#Location)
|
||||
5. **Delete all references to any of your Plex tokens** by searching for `X-Plex-Token` and `accesstoken` and replacing the strings just after that!
|
||||
* It's easiest if you copy your token, then use Search&Replace for the entire log file
|
||||
* You don't want others to have access to your Plex installation....
|
||||
6. Drop your log file here in this issue. Or use a free pasting-service like https://pastebin.com and include the link to it here
|
||||
|
||||
I am aware that I can delete Plex tokens that I accidentially posted by following the [instructions on the PKC wiki](https://github.com/croneter/PlexKodiConnect/wiki/How-to-Report-A-Bug#i-published-my-plex-token-to-some-forum-or-github-anyone-can-now-access-my-plex-server)
|
22
README.md
22
README.md
|
@ -1,14 +1,11 @@
|
|||
[![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.4.3-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.4.6-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 +36,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 +49,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 Beta 2
|
||||
- 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 +77,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!
|
||||
|
|
410
addon.xml
410
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.4.6" 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.5" />
|
||||
<import addon="plugin.video.plexkodiconnect.tvshows" version="2.0.5" />
|
||||
</requires>
|
||||
<extension point="xbmc.python.pluginsource" library="default.py">
|
||||
<provides>video audio image</provides>
|
||||
|
@ -63,9 +62,6 @@
|
|||
<summary lang="da_DK">Indbygget Integration af Plex i Kodi</summary>
|
||||
<description lang="da_DK">Tilslut Kodi til din Plex Media Server. Dette plugin forudsætter, at du administrere alle dine videoer med Plex (og ikke med Kodi). Du kan miste data som allerede er gemt i Kodi video og musik-databaser (dette plugin ændrer direkte i dem). Brug på eget ansvar!</description>
|
||||
<disclaimer lang="da_DK">Brug på eget ansvar</disclaimer>
|
||||
<summary lang="it_IT">Integrazione nativa di Plex su Kodi</summary>
|
||||
<description lang="it_IT">Connetti Kodi al tuo Plex Media Server. Questo plugin assume che tu gestisca tutti i video con Plex (e non con Kodi). Potresti perdere i dati dei film e della musica già memorizzati nel database di Kodi (questo plugin modifica direttamente il database stesso). Usa a tuo rischio e pericolo!</description>
|
||||
<disclaimer lang="it_IT">Usa a tuo rischio e pericolo</disclaimer>
|
||||
<summary lang="no_NO">Naturlig integrasjon av Plex til Kodi</summary>
|
||||
<description lang="no_NO">Koble Kodi til din Plex Media Server. Denne plugin forventer at du organiserer alle dine videor med Plex (og ingen med Kodi). Du kan miste all data allerede lagret i Kodi video- og musikkdatabasene (da denne plugin umiddelbart forandrer dem). Bruk på egen risiko!</description>
|
||||
<disclaimer lang="no_NO">Bruk på eget ansvar</disclaimer>
|
||||
|
@ -78,203 +74,243 @@
|
|||
<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.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.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.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.14.3 (beta only):
|
||||
- Implement "Reset resume position" from the Kodi context menu
|
||||
version 2.4.4 (beta only):
|
||||
- Fix rare case when playback would not start-up
|
||||
- Increase logging
|
||||
|
||||
version 2.14.2:
|
||||
- version 2.14.1 for everyone
|
||||
version 2.4.3:
|
||||
- Fix Kodi addons throwing jsonrpc errors (database reset needed)
|
||||
|
||||
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.4.2:
|
||||
- Make version 2.4.1 available for everyone
|
||||
|
||||
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.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.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.4.0:
|
||||
- Use pretty Plex dialogs for everyone!
|
||||
|
||||
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.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.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.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.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.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.12.25:
|
||||
- Update websocket client to 0.59.0. Fix threading issues and AttributeErrors
|
||||
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.12.24:
|
||||
- version 2.12.23 for everyone
|
||||
version 2.1.6:
|
||||
- Fix slow sync. Fix endless sync of corrupted PMS elements
|
||||
|
||||
version 2.12.23 (beta only):
|
||||
- Fix Alexa and RuntimeError: dictionary keys changed during iteration
|
||||
- Fix a rare AttributeError when using playlists
|
||||
version 2.1.5:
|
||||
- Fix OnDeck widget for Direct Paths
|
||||
|
||||
version 2.12.22:
|
||||
- version 2.12.20 and 2.12.21 for everyone
|
||||
version 2.1.4:
|
||||
- Fix PKC settings suddenly getting lost
|
||||
- Don't show artwork sync progress, reduce setting-writes
|
||||
|
||||
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.1.3:
|
||||
- Fix default settings string, only show in English, hopefully fixes PKC loosing its settings
|
||||
|
||||
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.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.12.19:
|
||||
- 2.12.17 and 2.12.18 for everyone
|
||||
- Rename skip intro skin file
|
||||
version 2.1.1:
|
||||
- Fix Library Sync crash on Android
|
||||
|
||||
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:
|
||||
- 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
|
||||
</news>
|
||||
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>
|
||||
|
|
715
changelog.txt
715
changelog.txt
|
@ -1,718 +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
|
||||
- Fix last-played node value to ensure a playcount greater than zero
|
||||
- 2.7.11-2.7.13 for everyone
|
||||
|
||||
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.7.12 (beta only):
|
||||
- Fix UnicodeEncodeError on playback startup for direct paths
|
||||
- Attempt to fix rare Kodi crash on PKC exit
|
||||
|
||||
version 2.7.11 (beta only):
|
||||
- Fixes to unicode
|
||||
- Cleanup code, remove some obsolet methods and functions
|
||||
- Fix FutureWarning
|
||||
|
||||
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.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.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.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.7.6:
|
||||
- Make 2.7.5 available for everyone
|
||||
|
||||
version 2.7.5 (beta only):
|
||||
- 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.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.6.3:
|
||||
- Fix PKC crashing on Xbox
|
||||
|
||||
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.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.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.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.5.22 (beta only):
|
||||
- Fix rare EOFError and PKC starting wrong video as a consequence
|
||||
|
||||
version 2.5.21 (beta only):
|
||||
- Fix KodiVideoDB object has no attribute kodiconn
|
||||
- Fix local variable 'set_api' referenced before assignment
|
||||
|
||||
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.5.19 (beta only):
|
||||
- Fix crash on startup-sync due to missing albums
|
||||
- Fix browsing to show from info dialog
|
||||
|
||||
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.5.17 (beta only):
|
||||
- Fix playback not starting for really large libraries
|
||||
|
||||
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
|
||||
|
|
|
@ -40,10 +40,9 @@ def main():
|
|||
'kodi_id': kodi_id,
|
||||
'kodi_type': kodi_type
|
||||
}
|
||||
while window.getProperty('plexkodiconnect.command'):
|
||||
while window.getProperty('plex_command'):
|
||||
sleep(20)
|
||||
window.setProperty('plexkodiconnect.command',
|
||||
'CONTEXT_menu?%s' % urlencode(args))
|
||||
window.setProperty('plex_command', 'CONTEXT_menu?%s' % urlencode(args))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
174
default.py
174
default.py
|
@ -5,18 +5,18 @@ from __future__ import absolute_import, division, unicode_literals
|
|||
import logging
|
||||
from sys import argv
|
||||
from urlparse import parse_qsl
|
||||
from xbmc import sleep, executebuiltin
|
||||
from xbmcgui import ListItem, getCurrentWindowId
|
||||
from xbmcplugin import setResolvedUrl
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcplugin
|
||||
|
||||
from resources.lib import entrypoint, utils, transfer, variables as v, loghandler
|
||||
from resources.lib import entrypoint, utils, pickler, pkc_listitem, \
|
||||
variables as v, loghandler
|
||||
from resources.lib.tools import unicode_paths
|
||||
|
||||
###############################################################################
|
||||
|
||||
loghandler.config()
|
||||
LOG = logging.getLogger('PLEX.default')
|
||||
log = logging.getLogger('PLEX.default')
|
||||
|
||||
###############################################################################
|
||||
|
||||
|
@ -27,15 +27,11 @@ class Main():
|
|||
# MAIN ENTRY POINT
|
||||
# @utils.profiling()
|
||||
def __init__(self):
|
||||
LOG.debug('Full sys.argv received: %s', argv)
|
||||
log.debug('Full sys.argv received: %s', argv)
|
||||
# Parse parameters
|
||||
params = dict(parse_qsl(argv[2][1:]))
|
||||
arguments = unicode_paths.decode(argv[2])
|
||||
path = unicode_paths.decode(argv[0])
|
||||
# Ensure unicode
|
||||
for key, value in params.iteritems():
|
||||
params[key.decode('utf-8')] = params.pop(key)
|
||||
params[key] = value.decode('utf-8')
|
||||
arguments = unicode_paths.decode(argv[2])
|
||||
params = dict(parse_qsl(arguments[1:]))
|
||||
mode = params.get('mode', '')
|
||||
itemid = params.get('id', '')
|
||||
|
||||
|
@ -45,88 +41,98 @@ class Main():
|
|||
elif mode == 'plex_node':
|
||||
self.play()
|
||||
|
||||
elif mode == 'ondeck':
|
||||
entrypoint.on_deck_episodes(itemid,
|
||||
params.get('tagname'),
|
||||
int(params.get('limit')))
|
||||
|
||||
elif mode == 'recentepisodes':
|
||||
entrypoint.recent_episodes(params.get('type'),
|
||||
params.get('tagname'),
|
||||
int(params.get('limit')))
|
||||
|
||||
elif mode == 'nextup':
|
||||
entrypoint.next_up_episodes(params['tagname'],
|
||||
int(params['limit']))
|
||||
|
||||
elif mode == 'inprogressepisodes':
|
||||
entrypoint.in_progress_episodes(params['tagname'],
|
||||
int(params['limit']))
|
||||
|
||||
elif mode == 'browseplex':
|
||||
entrypoint.browse_plex(key=params.get('key'),
|
||||
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'))
|
||||
plex_section_id=params.get('id'))
|
||||
|
||||
elif mode == 'watchlater':
|
||||
entrypoint.watchlater()
|
||||
|
||||
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'))
|
||||
entrypoint.channels()
|
||||
|
||||
elif mode == 'route_to_extras':
|
||||
# Hack so we can store this path in the Kodi DB
|
||||
handle = ('plugin://%s?mode=extras&plex_id=%s'
|
||||
% (v.ADDON_ID, params.get('plex_id')))
|
||||
if xbmcgui.getCurrentWindowId() == 10025:
|
||||
if getCurrentWindowId() == 10025:
|
||||
# Video Window
|
||||
xbmc.executebuiltin('Container.Update(\"%s\")' % handle)
|
||||
executebuiltin('Container.Update(\"%s\")' % handle)
|
||||
else:
|
||||
xbmc.executebuiltin('ActivateWindow(videos, \"%s\")' % handle)
|
||||
executebuiltin('ActivateWindow(videos, \"%s\")' % handle)
|
||||
|
||||
elif mode == 'extras':
|
||||
entrypoint.extras(plex_id=params.get('plex_id'))
|
||||
|
||||
elif mode == 'settings':
|
||||
xbmc.executebuiltin('Addon.OpenSettings(%s)' % v.ADDON_ID)
|
||||
executebuiltin('Addon.OpenSettings(%s)' % v.ADDON_ID)
|
||||
|
||||
elif mode == 'enterPMS':
|
||||
LOG.info('Request to manually enter new PMS address')
|
||||
transfer.plex_command('enter_new_pms_address')
|
||||
entrypoint.create_new_pms()
|
||||
|
||||
elif mode == 'reset':
|
||||
transfer.plex_command('RESET-PKC')
|
||||
utils.reset()
|
||||
|
||||
elif mode == 'togglePlexTV':
|
||||
LOG.info('Toggle of Plex.tv sign-in requested')
|
||||
transfer.plex_command('toggle_plex_tv_sign_in')
|
||||
entrypoint.toggle_plex_tv_sign_in()
|
||||
|
||||
elif mode == 'resetauth':
|
||||
entrypoint.reset_authorization()
|
||||
|
||||
elif mode == 'passwords':
|
||||
from resources.lib.windows import direct_path_sources
|
||||
direct_path_sources.start()
|
||||
utils.passwords_xml()
|
||||
|
||||
elif mode == 'switchuser':
|
||||
LOG.info('Plex home user switch requested')
|
||||
transfer.plex_command('switch_plex_user')
|
||||
entrypoint.switch_plex_user()
|
||||
|
||||
elif mode in ('manualsync', 'repair'):
|
||||
if mode == 'repair':
|
||||
LOG.info('Requesting repair lib sync')
|
||||
transfer.plex_command('repair-scan')
|
||||
elif mode == 'manualsync':
|
||||
LOG.info('Requesting full library scan')
|
||||
transfer.plex_command('full-scan')
|
||||
if pickler.pickl_window('plex_online') != 'true':
|
||||
# Server is not online, do not run the sync
|
||||
utils.messageDialog(utils.lang(29999), utils.lang(39205))
|
||||
log.error('Not connected to a PMS.')
|
||||
else:
|
||||
if mode == 'repair':
|
||||
log.info('Requesting repair lib sync')
|
||||
utils.plex_command('RUN_LIB_SCAN', 'repair')
|
||||
elif mode == 'manualsync':
|
||||
log.info('Requesting full library scan')
|
||||
utils.plex_command('RUN_LIB_SCAN', 'full')
|
||||
|
||||
elif mode == 'texturecache':
|
||||
LOG.info('Requesting texture caching of all textures')
|
||||
transfer.plex_command('textures-scan')
|
||||
log.info('Requesting texture caching of all textures')
|
||||
utils.plex_command('RUN_LIB_SCAN', 'textures')
|
||||
|
||||
elif mode == 'chooseServer':
|
||||
LOG.info("Choosing PMS server requested, starting")
|
||||
transfer.plex_command('choose_pms_server')
|
||||
entrypoint.choose_pms_server()
|
||||
|
||||
elif mode == 'refreshplaylist':
|
||||
log.info('Requesting playlist/nodes refresh')
|
||||
utils.plex_command('RUN_LIB_SCAN', 'views')
|
||||
|
||||
elif mode == 'deviceid':
|
||||
self.deviceid()
|
||||
|
||||
elif mode == 'fanart':
|
||||
LOG.info('User requested fanarttv refresh')
|
||||
transfer.plex_command('fanart-scan')
|
||||
log.info('User requested fanarttv refresh')
|
||||
utils.plex_command('RUN_LIB_SCAN', 'fanart')
|
||||
|
||||
elif '/extrafanart' in path:
|
||||
plexpath = arguments[1:]
|
||||
|
@ -144,15 +150,7 @@ class Main():
|
|||
entrypoint.playlists(params.get('content_type'))
|
||||
|
||||
elif mode == 'hub':
|
||||
entrypoint.hub(params.get('content_type'))
|
||||
|
||||
elif mode == 'select-libraries':
|
||||
LOG.info('User requested to select Plex libraries')
|
||||
transfer.plex_command('select-libraries')
|
||||
|
||||
elif mode == 'refreshplaylist':
|
||||
LOG.info('User requested to refresh Kodi playlists and nodes')
|
||||
transfer.plex_command('refreshplaylist')
|
||||
entrypoint.hub(params.get('type'))
|
||||
|
||||
else:
|
||||
entrypoint.show_main_menu(content_type=params.get('content_type'))
|
||||
|
@ -164,46 +162,44 @@ class Main():
|
|||
"""
|
||||
request = '%s&handle=%s' % (argv[2], HANDLE)
|
||||
# Put the request into the 'queue'
|
||||
transfer.plex_command('PLAY-%s' % request)
|
||||
utils.plex_command('PLAY', request)
|
||||
if HANDLE == -1:
|
||||
# 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:
|
||||
xbmcplugin.setResolvedUrl(HANDLE, False, xbmcgui.ListItem())
|
||||
# Tell main thread that we're done
|
||||
transfer.send(True, target='main')
|
||||
else:
|
||||
# Received a xbmcgui.ListItem()
|
||||
xbmcplugin.setResolvedUrl(HANDLE, True, result)
|
||||
# Wait for the result
|
||||
while not pickler.pickl_window('plex_result'):
|
||||
sleep(50)
|
||||
result = pickler.unpickle_me()
|
||||
if result is None:
|
||||
log.error('Error encountered, aborting')
|
||||
utils.dialog('notification',
|
||||
heading='{plex}',
|
||||
message=utils.lang(30128),
|
||||
icon='{error}',
|
||||
time=3000)
|
||||
setResolvedUrl(HANDLE, False, ListItem())
|
||||
elif result.listitem:
|
||||
listitem = pkc_listitem.convert_pkc_to_listitem(result.listitem)
|
||||
setResolvedUrl(HANDLE, True, listitem)
|
||||
|
||||
@staticmethod
|
||||
def deviceid():
|
||||
window = xbmcgui.Window(10000)
|
||||
deviceId_old = window.getProperty('plex_client_Id')
|
||||
deviceId_old = pickler.pickl_window('plex_client_Id')
|
||||
from resources.lib import clientinfo
|
||||
try:
|
||||
deviceId = clientinfo.getDeviceId(reset=True)
|
||||
except Exception as e:
|
||||
LOG.error('Failed to generate a new device Id: %s' % e)
|
||||
log.error('Failed to generate a new device Id: %s' % e)
|
||||
utils.messageDialog(utils.lang(29999), utils.lang(33032))
|
||||
else:
|
||||
LOG.info('Successfully removed old device ID: %s New deviceId:'
|
||||
log.info('Successfully removed old device ID: %s New deviceId:'
|
||||
'%s' % (deviceId_old, deviceId))
|
||||
# 'Kodi will now restart to apply the changes'
|
||||
utils.messageDialog(utils.lang(29999), utils.lang(33033))
|
||||
xbmc.executebuiltin('RestartApp')
|
||||
executebuiltin('RestartApp')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
LOG.info('%s started' % v.ADDON_ID)
|
||||
try:
|
||||
v.database_paths()
|
||||
except RuntimeError as err:
|
||||
# Database does not exists
|
||||
LOG.error('The current Kodi version is incompatible')
|
||||
LOG.error('Error: %s', err)
|
||||
else:
|
||||
Main()
|
||||
LOG.info('%s stopped' % v.ADDON_ID)
|
||||
log.info('%s started' % v.ADDON_ID)
|
||||
Main()
|
||||
log.info('%s stopped' % v.ADDON_ID)
|
||||
|
|
BIN
empty_video.mp4
Normal file
BIN
empty_video.mp4
Normal file
Binary file not shown.
|
@ -1,15 +1,14 @@
|
|||
# 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>, 2017
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PlexKodiConnect\n"
|
||||
"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"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Michal Kuncl <michal.kuncl@gmail.com>, 2017\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 +44,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: "
|
||||
|
@ -101,10 +93,10 @@ msgctxt "#30014"
|
|||
msgid "Connection"
|
||||
msgstr "Připojení"
|
||||
|
||||
# Pop-up notification if user tried to manually initiate fanart download
|
||||
# PKC settings - Artwork
|
||||
msgctxt "#30015"
|
||||
msgid "Fanart download already running"
|
||||
msgstr "Stahování obrázků již běží"
|
||||
msgid "Movie and show FanartTV lookups left to do:"
|
||||
msgstr "Filmů a seriálů k vyhledání na FanartTV:"
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Device Name"
|
||||
|
@ -120,7 +112,7 @@ msgctxt "#30018"
|
|||
msgid "Checking FanartTV for %s items"
|
||||
msgstr "Vyhledávám %s položek na FanartTV"
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
# Sync notification displayed when FanartTV lookup is completed
|
||||
msgctxt "#30019"
|
||||
msgid "FanartTV lookup completed"
|
||||
msgstr "Vyhledávání na FanartTV dokončeno"
|
||||
|
@ -163,19 +155,6 @@ msgctxt "#30027"
|
|||
msgid "Prefix in Kodi playlist name to trigger sync"
|
||||
msgstr "Předpona jména playlistu v Plexu pro synchronizaci"
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
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 +254,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í"
|
||||
|
@ -335,46 +310,6 @@ msgctxt "#30198"
|
|||
msgid "Search"
|
||||
msgstr "Hledání"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30200"
|
||||
msgid ""
|
||||
"In the following window, enter the server's hostname (or IP) where your Plex"
|
||||
" media resides. Mind the case!"
|
||||
msgstr ""
|
||||
"Do následujícího okna zadejte jméno serveru (nebo IP adresu), na kterém běží"
|
||||
" Plex Media Server. Pozor na velká písmena!"
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# for hostname
|
||||
msgctxt "#30201"
|
||||
msgid "Enter server hostname (or IP)"
|
||||
msgstr "Zadejte jméno serveru (nebo IP adresu)"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30202"
|
||||
msgid ""
|
||||
"In the following window, enter the network protocol you would like to use. "
|
||||
"This is likely 'smb'."
|
||||
msgstr ""
|
||||
"Do následujícího okna zadejte síťový protokol, který chcete použít. "
|
||||
"Pravděpodobně se jedná o 'smb'."
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# protocol
|
||||
msgctxt "#30203"
|
||||
msgid "Enter network protocol"
|
||||
msgstr "Zadejte síťový protokol"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30204"
|
||||
msgid "The hostname or IP '{0}' that you entered is not valid."
|
||||
msgstr "Jméno, nebo IP adresa hostitele '{0}' není platný."
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30205"
|
||||
msgid "The protocol '{0}' that you entered is not supported."
|
||||
msgstr "Protocol '{0}', není podporován."
|
||||
|
||||
# Video node naming for random e.g. movies
|
||||
msgctxt "#30227"
|
||||
msgid "Random"
|
||||
|
@ -488,14 +423,6 @@ msgctxt "#30416"
|
|||
msgid "Settings for the Plex Server"
|
||||
msgstr "Nastavení Plex Serveru"
|
||||
|
||||
msgctxt "#30417"
|
||||
msgid ""
|
||||
"Could not change the Kodi settings file {0}. PKC might not work correctly. "
|
||||
"Error: {1}"
|
||||
msgstr ""
|
||||
"Nemohu změnit soubor nastavení Kodi - {0}. PKC nemusí fungovat správně. "
|
||||
"Chyba: {1}"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30500"
|
||||
msgid "Verify Host SSL Certificate (more secure)"
|
||||
|
@ -506,15 +433,10 @@ msgctxt "#30501"
|
|||
msgid "Client SSL certificate"
|
||||
msgstr "Klientský SSL certifikát"
|
||||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#30502"
|
||||
msgid "Sync Plex artwork from the PMS (recommended)"
|
||||
msgstr "Synchronizovat obrázky Plexu z PMS (doporučeno)"
|
||||
|
||||
# Message shown if SSL HTTPS certificate fails
|
||||
msgctxt "#30503"
|
||||
msgid "SSL certificate failed to validate. Please check {0} for solutions."
|
||||
msgstr "Nemohu ověřit SSL certifikát. Pro řešení se podívejte do {0}."
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30505"
|
||||
msgid "[COLOR yellow]Reset login attempts[/COLOR]"
|
||||
msgstr "[COLOR yellow]Resetovat pokusy o přihlášení[/COLOR]"
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#30506"
|
||||
|
@ -558,7 +480,7 @@ msgstr "Omezit vlákna mezipaměti obrázků (doporučeno pro rpi)"
|
|||
# PKC Settings - Sync Options
|
||||
msgctxt "#30514"
|
||||
msgid "Show all Plex extras instead of immediately playing trailers"
|
||||
msgstr "Místo přehrání ukázek zobrazit všechny bonusy"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
msgctxt "#30515"
|
||||
|
@ -572,8 +494,8 @@ msgstr "Přehrávání"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30517"
|
||||
msgid "Set network credentials for Direct Paths and direct play"
|
||||
msgstr "Nastavit přihlašovací údaje pro přímé cesty a přímé přehrávání"
|
||||
msgid "[COLOR yellow]Enter network credentials[/COLOR]"
|
||||
msgstr "[COLOR yellow]Zadejte přihlašovací údaje k síťi[/COLOR]"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30518"
|
||||
|
@ -600,22 +522,6 @@ msgctxt "#30522"
|
|||
msgid "Force transcode h265/HEVC"
|
||||
msgstr "Vynutit překódování H265/HEVC"
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
msgctxt "#30523"
|
||||
msgid "Also show sync progress for playstate and user data"
|
||||
msgstr ""
|
||||
"Také zobrazovat průběh synchronizace stavu přehrávání a uživatelských dat"
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
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 +586,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 +608,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,29 +628,15 @@ 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"
|
||||
msgstr "Neplatné uživatelské jméno nebo heslo"
|
||||
|
||||
msgctxt "#33010"
|
||||
msgid "User is unauthorized for server {0}"
|
||||
msgstr "Uživatel není ověřen u serveru {0}"
|
||||
|
||||
msgctxt "#33011"
|
||||
msgid "Plex.tv did not provide us a valid list of Plex users, sorry."
|
||||
msgstr "Je nám líto, ale plex.tv neposkytl platný seznam uživatelů."
|
||||
msgid "Failed to authenticate too many times. Reset in the settings."
|
||||
msgstr ""
|
||||
"Proběhlo moc neúspěšných pokusů o ověření. Proveďte reset v nastavení."
|
||||
|
||||
# Dialog before playback
|
||||
msgctxt "#33013"
|
||||
|
@ -896,8 +773,10 @@ msgstr "Vyresetovat databázi Kodi a případně i nastavení PlexKodiConnect"
|
|||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#39020"
|
||||
msgid "Cache all images to Kodi texture cache now"
|
||||
msgstr "Nyní umístit všechny obrázky do texturové mezipaměti Kodi"
|
||||
msgid "[COLOR yellow]Cache all images to Kodi texture cache now[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow]Nyní umístit všechny obrázky do texturové mezipaměti "
|
||||
"Kodi[/COLOR]"
|
||||
|
||||
# Appended to a listed PMS if it is in the same LAN network as PKC
|
||||
msgctxt "#39022"
|
||||
|
@ -980,16 +859,6 @@ msgstr ""
|
|||
"Nahradit cesty /volume1/media nebo \\myserver\\media za vlastní cesty "
|
||||
"smb://NAS/mystuff"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
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:"
|
||||
|
@ -1065,8 +934,8 @@ msgstr "Nic nefunguje? Zkuste plný reset!"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39050"
|
||||
msgid "Choose Plex Server from a list"
|
||||
msgstr "Zvolte Plex server ze seznamu"
|
||||
msgid "[COLOR yellow]Choose Plex Server from a list[/COLOR]"
|
||||
msgstr "[COLOR yellow]Zvolte Plex server ze seznamu[/COLOR]"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39051"
|
||||
|
@ -1095,17 +964,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 +1016,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"
|
||||
|
@ -1155,8 +1030,8 @@ msgstr "Váš současný Plex Media Server:"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39068"
|
||||
msgid "Manually enter Plex Media Server address"
|
||||
msgstr "Ručně zadat adresu Plex Media Serveru"
|
||||
msgid "[COLOR yellow]Manually enter Plex Media Server address[/COLOR]"
|
||||
msgstr "[COLOR yellow]Ručně zadat adresu Plex Media Serveru[/COLOR]"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39069"
|
||||
|
@ -1173,11 +1048,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 +1058,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 +1075,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"
|
||||
|
@ -1221,68 +1091,6 @@ msgid ""
|
|||
msgstr ""
|
||||
"Plex Companion nemůže otevřít GDM port. Prosím změňte ho v nastavení PKC."
|
||||
|
||||
# Pop-up on initial sync.
|
||||
# Check that next translations for Add-on Paths and Direct Paths are
|
||||
# identical!
|
||||
msgctxt "#39080"
|
||||
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 ""
|
||||
"Použít cesty doplňku (výchozí, snadné), nebo přímé cesty? Pokud si nejste "
|
||||
"jistí, vyberte cesty doplňku. Pokud nastavíte přímé cesty špatně, tak nebude"
|
||||
" PKC fungovat!"
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39081"
|
||||
msgid "Add-on Paths"
|
||||
msgstr "Cesty doplňku"
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39082"
|
||||
msgid "Direct Paths"
|
||||
msgstr "Přímé cesty"
|
||||
|
||||
# Dialog for manually entering PMS
|
||||
msgctxt "#39083"
|
||||
msgid "Enter PMS IP or URL"
|
||||
msgstr "Zadejte IP adresu, nebo URL PMS"
|
||||
|
||||
# Dialog for manually entering PMS
|
||||
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 "
|
||||
|
@ -1291,6 +1099,10 @@ msgctxt "#39201"
|
|||
msgid "Settings"
|
||||
msgstr "Nastavení"
|
||||
|
||||
msgctxt "#39203"
|
||||
msgid "Refresh Plex playlists/nodes"
|
||||
msgstr "Obnovit playlisty Plexu"
|
||||
|
||||
msgctxt "#39204"
|
||||
msgid "Perform manual library sync"
|
||||
msgstr "Provést ruční synchronizaci knihovny"
|
||||
|
@ -1318,8 +1130,10 @@ msgstr "Nemohu resetovat PKC. Zkuste restartovat Kodi."
|
|||
|
||||
# PKC Settings - Plex
|
||||
msgctxt "#39209"
|
||||
msgid "Toggle plex.tv login (sign in or sign out)"
|
||||
msgstr "Přepnout přihlášení přes plex.tv (přihlásit nebo odhlásit)"
|
||||
msgid "[COLOR yellow]Toggle plex.tv login (sign in or sign out)[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow]Přepnout přihlášení k plex.tv login (přihlásit nebo "
|
||||
"odhlásit)[/COLOR]"
|
||||
|
||||
msgctxt "#39210"
|
||||
msgid "Not yet connected to Plex Server"
|
||||
|
@ -1329,19 +1143,21 @@ msgctxt "#39211"
|
|||
msgid "Watch later"
|
||||
msgstr "Shlédnout později"
|
||||
|
||||
# Error message pop-up if {0} cannot be contacted. {0} will be replaced by
|
||||
# e.g. the PMS' name
|
||||
# String attached at the end to get something like "PMS Name is offline"
|
||||
msgctxt "#39213"
|
||||
msgid "{0} offline"
|
||||
msgstr "{0} je offline"
|
||||
msgid "is offline"
|
||||
msgstr "je offline"
|
||||
|
||||
msgctxt "#39215"
|
||||
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 ""
|
||||
"Does your Plex Media Server support SSL connections? (https instead of "
|
||||
"http)?"
|
||||
msgstr ""
|
||||
"Podporuje Váš Plex Media Server připojení přes SSL? (https místo http)?"
|
||||
|
||||
msgctxt "#39218"
|
||||
msgid "Error contacting PMS"
|
||||
|
@ -1361,8 +1177,8 @@ msgid "plex.tv toggle successful"
|
|||
msgstr "přepnutí plex.tv úspěšné"
|
||||
|
||||
msgctxt "#39222"
|
||||
msgid "Look for missing fanart on FanartTV now"
|
||||
msgstr "Nyní vyhledat chybějící obrázky na FanartTV"
|
||||
msgid "[COLOR yellow]Look for missing fanart on FanartTV now[/COLOR]"
|
||||
msgstr "[COLOR yellow]Teď vyhledat chybějící obrázky na FanartTV[/COLOR]"
|
||||
|
||||
msgctxt "#39223"
|
||||
msgid ""
|
||||
|
@ -1390,26 +1206,11 @@ msgctxt "#39227"
|
|||
msgid "Logged in to plex.tv"
|
||||
msgstr "Přihlášeno k plex.tv"
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
# Message in the PKC settings to display the plex.tv username. Leave the colon
|
||||
# :
|
||||
msgctxt "#39228"
|
||||
msgid "Plex admin user"
|
||||
msgstr "Správce Plexu"
|
||||
|
||||
# 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 "Nezdařilo se přihlášení přes plex.tv pro uživatele"
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
msgctxt "#39230"
|
||||
msgid "Logged in Plex home user"
|
||||
msgstr "Přihlášený uživatel Plex Home"
|
||||
|
||||
# Message in the PKC settings to change the logged in Plex home user
|
||||
msgctxt "#39231"
|
||||
msgid "Change logged in Plex home user"
|
||||
msgstr "Změnit uživatele Plex Home"
|
||||
msgid "Plex user:"
|
||||
msgstr "Uživatel plexu:"
|
||||
|
||||
msgctxt "#39250"
|
||||
msgid ""
|
||||
|
@ -1481,9 +1282,11 @@ msgstr " nemusí fungovat správně dokud neprovedete reset databáze."
|
|||
|
||||
msgctxt "#39403"
|
||||
msgid ""
|
||||
"The current Kodi version is not supported by PKC. Please consult the Plex "
|
||||
"forum."
|
||||
msgstr "Tuto verzi Kodi PKC nepodporuje. Prosím projděte si fórum Plexu."
|
||||
"Cancelling the database syncing process. Current Kodi version is "
|
||||
"unsupported. Please verify your logs for more info."
|
||||
msgstr ""
|
||||
"Ruším synchronizaci databáze. Tato verze Kodi není podporována. Pro více "
|
||||
"informací se podívejte do záznamů."
|
||||
|
||||
msgctxt "#39405"
|
||||
msgid "Plex playlists/nodes refreshed"
|
||||
|
@ -1493,6 +1296,10 @@ msgctxt "#39406"
|
|||
msgid "Plex playlists/nodes refresh failed"
|
||||
msgstr "Nemohu obnovit Plex playlisty"
|
||||
|
||||
msgctxt "#39407"
|
||||
msgid "Full library sync finished"
|
||||
msgstr "Plná synchronizace knihovny dokončena"
|
||||
|
||||
msgctxt "#39408"
|
||||
msgid ""
|
||||
"Sync had to skip some items because they could not be processed. Kodi may be"
|
||||
|
@ -1524,10 +1331,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 +1389,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 +1447,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}!
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,14 +1,13 @@
|
|||
# XBMC Media Center language file
|
||||
# Translators:
|
||||
# Croneter None <croneter@gmail.com>, 2021
|
||||
#
|
||||
# Croneter None <croneter@gmail.com>, 2017
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PlexKodiConnect\n"
|
||||
"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"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Croneter None <croneter@gmail.com>, 2017\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 +43,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: "
|
||||
|
@ -103,10 +91,10 @@ msgctxt "#30014"
|
|||
msgid "Connection"
|
||||
msgstr "Verbindung"
|
||||
|
||||
# Pop-up notification if user tried to manually initiate fanart download
|
||||
# PKC settings - Artwork
|
||||
msgctxt "#30015"
|
||||
msgid "Fanart download already running"
|
||||
msgstr "Fanart-Download läuft bereits"
|
||||
msgid "Movie and show FanartTV lookups left to do:"
|
||||
msgstr "Filme und Serien noch bei FanartTV nachzuschlagen:"
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Device Name"
|
||||
|
@ -122,7 +110,7 @@ msgctxt "#30018"
|
|||
msgid "Checking FanartTV for %s items"
|
||||
msgstr "%s Nachforschungen bei FanartTV"
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
# Sync notification displayed when FanartTV lookup is completed
|
||||
msgctxt "#30019"
|
||||
msgid "FanartTV lookup completed"
|
||||
msgstr "FanartTV Nachforschungen beendet"
|
||||
|
@ -165,22 +153,6 @@ msgctxt "#30027"
|
|||
msgid "Prefix in Kodi playlist name to trigger sync"
|
||||
msgstr "Präfix der Kodi Wiedergabelisten, welche synchronisiert werden"
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
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 +252,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"
|
||||
|
@ -341,49 +308,6 @@ msgctxt "#30198"
|
|||
msgid "Search"
|
||||
msgstr "Suche"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30200"
|
||||
msgid ""
|
||||
"In the following window, enter the server's hostname (or IP) where your Plex"
|
||||
" media resides. Mind the case!"
|
||||
msgstr ""
|
||||
"Im nachfolgenden Fenster den Hostnamen (oder die IP) des Server eingeben, wo"
|
||||
" sich die Plex Medien befinden. Achtung Gross- und Kleinschreibung!"
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# for hostname
|
||||
msgctxt "#30201"
|
||||
msgid "Enter server hostname (or IP)"
|
||||
msgstr "Hostnamen (oder IP) des Servers eingeben"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30202"
|
||||
msgid ""
|
||||
"In the following window, enter the network protocol you would like to use. "
|
||||
"This is likely 'smb'."
|
||||
msgstr ""
|
||||
"Im nachfolgenden Fenster das Netzwerkprotokoll eingeben, welches genutzt "
|
||||
"werden soll. Dies ist höchstwahrscheinlich 'smb'."
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# protocol
|
||||
msgctxt "#30203"
|
||||
msgid "Enter network protocol"
|
||||
msgstr "Netzwerkprotokoll eingeben"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30204"
|
||||
msgid "The hostname or IP '{0}' that you entered is not valid."
|
||||
msgstr ""
|
||||
"Der Hostname oder die IP '{0}', welche eingegeben wurde, ist unzulässig."
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30205"
|
||||
msgid "The protocol '{0}' that you entered is not supported."
|
||||
msgstr ""
|
||||
"Das Netzwerkprotokoll '{0}', welches eingegeben wurde, wird nicht "
|
||||
"unterstützt."
|
||||
|
||||
# Video node naming for random e.g. movies
|
||||
msgctxt "#30227"
|
||||
msgid "Random"
|
||||
|
@ -497,14 +421,6 @@ msgctxt "#30416"
|
|||
msgid "Settings for the Plex Server"
|
||||
msgstr "Einstellungen für den Plex Server"
|
||||
|
||||
msgctxt "#30417"
|
||||
msgid ""
|
||||
"Could not change the Kodi settings file {0}. PKC might not work correctly. "
|
||||
"Error: {1}"
|
||||
msgstr ""
|
||||
"Die Kodi Datei {0} konnte leider nicht geändert werden. PKC wird wohl nicht "
|
||||
"richtig funktionieren. Fehler: {1}"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30500"
|
||||
msgid "Verify Host SSL Certificate (more secure)"
|
||||
|
@ -515,17 +431,10 @@ msgctxt "#30501"
|
|||
msgid "Client SSL certificate"
|
||||
msgstr "Client SSL-Zertifikat"
|
||||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#30502"
|
||||
msgid "Sync Plex artwork from the PMS (recommended)"
|
||||
msgstr "Plex Bilder vom Plex Medienserver synchronisieren (empfohlen)"
|
||||
|
||||
# Message shown if SSL HTTPS certificate fails
|
||||
msgctxt "#30503"
|
||||
msgid "SSL certificate failed to validate. Please check {0} for solutions."
|
||||
msgstr ""
|
||||
"SSL-Zertifikat konnte nicht validiert werden. Bitte besuche {0} für "
|
||||
"Lösungsvorschläge."
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30505"
|
||||
msgid "[COLOR yellow]Reset login attempts[/COLOR]"
|
||||
msgstr "[COLOR yellow]Anzahl Login-Versuche zurücksetzen[/COLOR]"
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#30506"
|
||||
|
@ -584,8 +493,8 @@ msgstr "Wiedergabe"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30517"
|
||||
msgid "Set network credentials for Direct Paths and direct play"
|
||||
msgstr "Netzwerk-Anmeldeinformationen für Direct Paths und Direct Play setzen"
|
||||
msgid "[COLOR yellow]Enter network credentials[/COLOR]"
|
||||
msgstr "[COLOR yellow]Netzwerk-Anmeldeinformationen eingeben[/COLOR]"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30518"
|
||||
|
@ -612,23 +521,6 @@ msgctxt "#30522"
|
|||
msgid "Force transcode h265/HEVC"
|
||||
msgstr "h265/HEVC Codec Transkodierung erzwingen"
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
msgctxt "#30523"
|
||||
msgid "Also show sync progress for playstate and user data"
|
||||
msgstr ""
|
||||
"Synchronisations-Fortschritt auch für Zwischenstände und Benutzerdaten "
|
||||
"anzeigen"
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
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 +584,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 +606,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,31 +626,16 @@ 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"
|
||||
msgstr "Ungültiger Benutzername oder Passwort"
|
||||
|
||||
msgctxt "#33010"
|
||||
msgid "User is unauthorized for server {0}"
|
||||
msgstr "Benutzer ist für Server {0} nicht autorisiert"
|
||||
|
||||
msgctxt "#33011"
|
||||
msgid "Plex.tv did not provide us a valid list of Plex users, sorry."
|
||||
msgid "Failed to authenticate too many times. Reset in the settings."
|
||||
msgstr ""
|
||||
"Plex.tv hat uns leider keine zulässige Liste von Plex Benutzern gesandt, "
|
||||
"sorry."
|
||||
"Plex Media Server Authorisierung ist zu häufig fehlgeschlagen. In den "
|
||||
"Einstellungen können die Anzahl erfolgloser Versuche zurückgesetzt werden."
|
||||
|
||||
# Dialog before playback
|
||||
msgctxt "#33013"
|
||||
|
@ -918,8 +779,9 @@ msgstr "Kodi Datenbank und optional PlexKodiConnect zurücksetzen"
|
|||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#39020"
|
||||
msgid "Cache all images to Kodi texture cache now"
|
||||
msgstr "Alle Plex Bilder jetzt in Kodi zwischenspeichern"
|
||||
msgid "[COLOR yellow]Cache all images to Kodi texture cache now[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow]Alle Plex Bilder jetzt in Kodi zwischenspeichern[/COLOR]"
|
||||
|
||||
# Appended to a listed PMS if it is in the same LAN network as PKC
|
||||
msgctxt "#39022"
|
||||
|
@ -1004,16 +866,6 @@ msgstr ""
|
|||
"Plex Pfade /volume1/medien oder \\\\meinServer\\medien mit "
|
||||
"benutzerdefinierten SMB Pfaden wie smb://NAS/Filme ersetzen"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
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:"
|
||||
|
@ -1089,8 +941,8 @@ msgstr "Nichts funktioniert? Setze mal alles zurück!"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39050"
|
||||
msgid "Choose Plex Server from a list"
|
||||
msgstr "Plex Server aus Liste auswählen"
|
||||
msgid "[COLOR yellow]Choose Plex Server from a list[/COLOR]"
|
||||
msgstr "[COLOR yellow]Plex Server aus Liste auswählen[/COLOR]"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39051"
|
||||
|
@ -1119,17 +971,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 +1024,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"
|
||||
|
@ -1179,8 +1038,8 @@ msgstr "Aktueller Plex Media Server:"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39068"
|
||||
msgid "Manually enter Plex Media Server address"
|
||||
msgstr "Adresse des Plex Medienservers manuell eingeben"
|
||||
msgid "[COLOR yellow]Manually enter Plex Media Server address[/COLOR]"
|
||||
msgstr "[COLOR yellow]Plex Media Server Adresse manuell eingeben[/COLOR]"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39069"
|
||||
|
@ -1197,11 +1056,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 +1066,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 +1082,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"
|
||||
|
@ -1245,68 +1099,6 @@ msgstr ""
|
|||
"Plex Companion konnte den Update Port nicht öffnen. Bitte den Port in den "
|
||||
"PKC Einstellungen ändern."
|
||||
|
||||
# Pop-up on initial sync.
|
||||
# Check that next translations for Add-on Paths and Direct Paths are
|
||||
# identical!
|
||||
msgctxt "#39080"
|
||||
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 ""
|
||||
"Add-on Paths (Standardeinstellung, einfach) oder Direct Paths? Wählen Sie "
|
||||
"Add-on Paths, wenn Sie sich nicht sicher sind. PKC wird nicht funktionieren,"
|
||||
" wenn die Einstellungen für Direct Paths nicht korrekt sind!"
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39081"
|
||||
msgid "Add-on Paths"
|
||||
msgstr "Add-on Paths"
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39082"
|
||||
msgid "Direct Paths"
|
||||
msgstr "Direct Paths"
|
||||
|
||||
# Dialog for manually entering PMS
|
||||
msgctxt "#39083"
|
||||
msgid "Enter PMS IP or URL"
|
||||
msgstr "PMS IP oder URL eingeben"
|
||||
|
||||
# Dialog for manually entering PMS
|
||||
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: "
|
||||
|
@ -1315,6 +1107,10 @@ msgctxt "#39201"
|
|||
msgid "Settings"
|
||||
msgstr "Einstellungen"
|
||||
|
||||
msgctxt "#39203"
|
||||
msgid "Refresh Plex playlists/nodes"
|
||||
msgstr "Plex Playlisten und Video Nodes aktualisieren"
|
||||
|
||||
msgctxt "#39204"
|
||||
msgid "Perform manual library sync"
|
||||
msgstr "Manuellen Scan der Plex Bibliotheken starten"
|
||||
|
@ -1346,8 +1142,8 @@ msgstr ""
|
|||
|
||||
# PKC Settings - Plex
|
||||
msgctxt "#39209"
|
||||
msgid "Toggle plex.tv login (sign in or sign out)"
|
||||
msgstr "plex.tv Login ändern (ein- resp. ausloggen)"
|
||||
msgid "[COLOR yellow]Toggle plex.tv login (sign in or sign out)[/COLOR]"
|
||||
msgstr "[COLOR yellow]plex.tv Login wechseln (ein- resp. ausloggen)[/COLOR]"
|
||||
|
||||
msgctxt "#39210"
|
||||
msgid "Not yet connected to Plex Server"
|
||||
|
@ -1357,19 +1153,22 @@ msgctxt "#39211"
|
|||
msgid "Watch later"
|
||||
msgstr "Später ansehen"
|
||||
|
||||
# Error message pop-up if {0} cannot be contacted. {0} will be replaced by
|
||||
# e.g. the PMS' name
|
||||
# String attached at the end to get something like "PMS Name is offline"
|
||||
msgctxt "#39213"
|
||||
msgid "{0} offline"
|
||||
msgstr "{0} offline"
|
||||
msgid "is offline"
|
||||
msgstr "ist offline"
|
||||
|
||||
msgctxt "#39215"
|
||||
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 ""
|
||||
"Does your Plex Media Server support SSL connections? (https instead of "
|
||||
"http)?"
|
||||
msgstr ""
|
||||
"Unterstützt der Plex Media Server sichere SSL Verbindungen (https anstelle "
|
||||
"von http)?"
|
||||
|
||||
msgctxt "#39218"
|
||||
msgid "Error contacting PMS"
|
||||
|
@ -1389,8 +1188,8 @@ msgid "plex.tv toggle successful"
|
|||
msgstr "plex.tv Wechsel OK"
|
||||
|
||||
msgctxt "#39222"
|
||||
msgid "Look for missing fanart on FanartTV now"
|
||||
msgstr "Jetzt zusätzliche Bilder auf FanartTV suchen"
|
||||
msgid "[COLOR yellow]Look for missing fanart on FanartTV now[/COLOR]"
|
||||
msgstr "[COLOR yellow]Jetzt zusätzliche Bilder auf FanartTV suchen[/COLOR]"
|
||||
|
||||
msgctxt "#39223"
|
||||
msgid ""
|
||||
|
@ -1419,26 +1218,11 @@ msgctxt "#39227"
|
|||
msgid "Logged in to plex.tv"
|
||||
msgstr "Eingeloggt bei plex.tv"
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
# Message in the PKC settings to display the plex.tv username. Leave the colon
|
||||
# :
|
||||
msgctxt "#39228"
|
||||
msgid "Plex admin user"
|
||||
msgstr "Plex Admin Benutzer"
|
||||
|
||||
# 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 "Login für plex.tv fehlgeschlagen für Benutzer"
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
msgctxt "#39230"
|
||||
msgid "Logged in Plex home user"
|
||||
msgstr "Eingeloggter Plex Home Benutzer"
|
||||
|
||||
# Message in the PKC settings to change the logged in Plex home user
|
||||
msgctxt "#39231"
|
||||
msgid "Change logged in Plex home user"
|
||||
msgstr "Eingeloggten Plex Home Benutzer wechseln"
|
||||
msgid "Plex user:"
|
||||
msgstr "Plex Benutzer:"
|
||||
|
||||
msgctxt "#39250"
|
||||
msgid ""
|
||||
|
@ -1513,11 +1297,12 @@ msgstr ""
|
|||
|
||||
msgctxt "#39403"
|
||||
msgid ""
|
||||
"The current Kodi version is not supported by PKC. Please consult the Plex "
|
||||
"forum."
|
||||
"Cancelling the database syncing process. Current Kodi version is "
|
||||
"unsupported. Please verify your logs for more info."
|
||||
msgstr ""
|
||||
"Die aktuelle Version von Kodi wird nicht von PKC unterstützt. Bitte "
|
||||
"konsultieren Sie das Plex Forum."
|
||||
"Synchronisierung der Plex Bibliotheken wird abgebrochen. Die momentane Kodi "
|
||||
"Version wird nicht unterstützt. Für weitere Informationen bitte das Kodi Log"
|
||||
" konsultieren."
|
||||
|
||||
msgctxt "#39405"
|
||||
msgid "Plex playlists/nodes refreshed"
|
||||
|
@ -1527,6 +1312,10 @@ msgctxt "#39406"
|
|||
msgid "Plex playlists/nodes refresh failed"
|
||||
msgstr "Aktualisierung der Wiedergabelisten/Video Nodes fehlgeschlagen"
|
||||
|
||||
msgctxt "#39407"
|
||||
msgid "Full library sync finished"
|
||||
msgstr "Alle Plex Bibliotheken aktualisiert"
|
||||
|
||||
msgctxt "#39408"
|
||||
msgid ""
|
||||
"Sync had to skip some items because they could not be processed. Kodi may be"
|
||||
|
@ -1558,10 +1347,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 +1407,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 +1465,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 ""
|
||||
|
@ -88,9 +84,9 @@ msgctxt "#30014"
|
|||
msgid "Connection"
|
||||
msgstr ""
|
||||
|
||||
# Pop-up notification if user tried to manually initiate fanart download
|
||||
# PKC settings - Artwork
|
||||
msgctxt "#30015"
|
||||
msgid "Fanart download already running"
|
||||
msgid "Movie and show FanartTV lookups left to do:"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30016"
|
||||
|
@ -107,7 +103,7 @@ msgctxt "#30018"
|
|||
msgid "Checking FanartTV for %s items"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
# Sync notification displayed when FanartTV lookup is completed
|
||||
msgctxt "#30019"
|
||||
msgid "FanartTV lookup completed"
|
||||
msgstr ""
|
||||
|
@ -150,11 +146,6 @@ msgctxt "#30027"
|
|||
msgid "Prefix in Kodi playlist name to trigger sync"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
msgctxt "#30028"
|
||||
msgid "PKC-only image caching completed"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Port Number"
|
||||
msgstr ""
|
||||
|
@ -254,10 +245,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 ""
|
||||
|
@ -314,36 +301,6 @@ msgctxt "#30198"
|
|||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30200"
|
||||
msgid "In the following window, enter the server's hostname (or IP) where your Plex media resides. Mind the case!"
|
||||
msgstr ""
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window for hostname
|
||||
msgctxt "#30201"
|
||||
msgid "Enter server hostname (or IP)"
|
||||
msgstr ""
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30202"
|
||||
msgid "In the following window, enter the network protocol you would like to use. This is likely 'smb'."
|
||||
msgstr ""
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window protocol
|
||||
msgctxt "#30203"
|
||||
msgid "Enter network protocol"
|
||||
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 ""
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30205"
|
||||
msgid "The protocol '{0}' that you entered is not supported."
|
||||
msgstr ""
|
||||
|
||||
# Video node naming for random e.g. movies
|
||||
msgctxt "#30227"
|
||||
msgid "Random"
|
||||
|
@ -453,10 +410,6 @@ msgctxt "#30416"
|
|||
msgid "Settings for the Plex Server"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30417"
|
||||
msgid "Could not change the Kodi settings file {0}. PKC might not work correctly. Error: {1}"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30500"
|
||||
msgid "Verify Host SSL Certificate (more secure)"
|
||||
|
@ -467,14 +420,9 @@ msgctxt "#30501"
|
|||
msgid "Client SSL certificate"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#30502"
|
||||
msgid "Sync Plex artwork from the PMS (recommended)"
|
||||
msgstr ""
|
||||
|
||||
# Message shown if SSL HTTPS certificate fails
|
||||
msgctxt "#30503"
|
||||
msgid "SSL certificate failed to validate. Please check {0} for solutions."
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30505"
|
||||
msgid "[COLOR yellow]Reset login attempts[/COLOR]"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings, category name
|
||||
|
@ -533,7 +481,7 @@ msgstr ""
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30517"
|
||||
msgid "Set network credentials for Direct Paths and direct play"
|
||||
msgid "[COLOR yellow]Enter network credentials[/COLOR]"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
|
@ -561,21 +509,6 @@ msgctxt "#30522"
|
|||
msgid "Force transcode h265/HEVC"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
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 ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -638,7 +571,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 +593,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,27 +613,13 @@ 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"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#33010"
|
||||
msgid "User is unauthorized for server {0}"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#33011"
|
||||
msgid "Plex.tv did not provide us a valid list of Plex users, sorry."
|
||||
msgid "Failed to authenticate too many times. Reset in the settings."
|
||||
msgstr ""
|
||||
|
||||
# Dialog before playback
|
||||
|
@ -840,7 +744,7 @@ msgstr ""
|
|||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#39020"
|
||||
msgid "Cache all images to Kodi texture cache now"
|
||||
msgid "[COLOR yellow]Cache all images to Kodi texture cache now[/COLOR]"
|
||||
msgstr ""
|
||||
|
||||
# Appended to a listed PMS if it is in the same LAN network as PKC
|
||||
|
@ -898,16 +802,6 @@ msgctxt "#39035"
|
|||
msgid "Replace Plex paths /volume1/media or \\\\myserver\\media with custom SMB paths smb://NAS/mystuff"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
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:"
|
||||
|
@ -975,7 +869,7 @@ msgstr ""
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39050"
|
||||
msgid "Choose Plex Server from a list"
|
||||
msgid "[COLOR yellow]Choose Plex Server from a list[/COLOR]"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Sync
|
||||
|
@ -1005,7 +899,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
|
||||
|
@ -1013,6 +907,11 @@ 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"
|
||||
|
@ -1050,7 +949,7 @@ 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 ""
|
||||
|
||||
# PKC Settings - Connection
|
||||
|
@ -1060,7 +959,7 @@ msgstr ""
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39068"
|
||||
msgid "Manually enter Plex Media Server address"
|
||||
msgid "[COLOR yellow]Manually enter Plex Media Server address[/COLOR]"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection
|
||||
|
@ -1078,11 +977,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 +987,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 +999,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
|
||||
|
@ -1129,46 +1028,6 @@ msgctxt "#39082"
|
|||
msgid "Direct Paths"
|
||||
msgstr ""
|
||||
|
||||
# Dialog for manually entering PMS
|
||||
msgctxt "#39083"
|
||||
msgid "Enter PMS IP or URL"
|
||||
msgstr ""
|
||||
|
||||
# Dialog for manually entering PMS
|
||||
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 ""
|
||||
|
@ -1177,6 +1036,10 @@ msgctxt "#39201"
|
|||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39203"
|
||||
msgid "Refresh Plex playlists/nodes"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39204"
|
||||
msgid "Perform manual library sync"
|
||||
msgstr ""
|
||||
|
@ -1200,7 +1063,7 @@ msgstr ""
|
|||
|
||||
# PKC Settings - Plex
|
||||
msgctxt "#39209"
|
||||
msgid "Toggle plex.tv login (sign in or sign out)"
|
||||
msgid "[COLOR yellow]Toggle plex.tv login (sign in or sign out)[/COLOR]"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39210"
|
||||
|
@ -1211,9 +1074,9 @@ msgctxt "#39211"
|
|||
msgid "Watch later"
|
||||
msgstr ""
|
||||
|
||||
# Error message pop-up if {0} cannot be contacted. {0} will be replaced by e.g. the PMS' name
|
||||
# String attached at the end to get something like "PMS Name is offline"
|
||||
msgctxt "#39213"
|
||||
msgid "{0} offline"
|
||||
msgid "is offline"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39215"
|
||||
|
@ -1221,7 +1084,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 "Does your Plex Media Server support SSL connections? (https instead of http)?"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39218"
|
||||
|
@ -1242,7 +1105,7 @@ msgid "plex.tv toggle successful"
|
|||
msgstr ""
|
||||
|
||||
msgctxt "#39222"
|
||||
msgid "Look for missing fanart on FanartTV now"
|
||||
msgid "[COLOR yellow]Look for missing fanart on FanartTV now[/COLOR]"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39223"
|
||||
|
@ -1267,9 +1130,9 @@ msgctxt "#39227"
|
|||
msgid "Logged in to plex.tv"
|
||||
msgstr ""
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
# Message in the PKC settings to display the plex.tv username. Leave the colon :
|
||||
msgctxt "#39228"
|
||||
msgid "Plex admin user"
|
||||
msgid "Plex user:"
|
||||
msgstr ""
|
||||
|
||||
# Error message if user could not log in; the actual user name will be appended at the end of the string
|
||||
|
@ -1277,16 +1140,6 @@ msgctxt "#39229"
|
|||
msgid "Login failed with plex.tv for user"
|
||||
msgstr ""
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
msgctxt "#39230"
|
||||
msgid "Logged in Plex home user"
|
||||
msgstr ""
|
||||
|
||||
# Message in the PKC settings to change the logged in Plex home user
|
||||
msgctxt "#39231"
|
||||
msgid "Change logged in Plex home user"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39250"
|
||||
msgid "Running the image cache process can take some time. It will happen in the background. Are you sure you want continue?"
|
||||
msgstr ""
|
||||
|
@ -1344,7 +1197,7 @@ msgid " may not work correctly until the database is reset."
|
|||
msgstr ""
|
||||
|
||||
msgctxt "#39403"
|
||||
msgid "The current Kodi version is not supported by PKC. Please consult the Plex forum."
|
||||
msgid "Cancelling the database syncing process. Current Kodi version is unsupported. Please verify your logs for more info."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39405"
|
||||
|
@ -1355,6 +1208,10 @@ msgctxt "#39406"
|
|||
msgid "Plex playlists/nodes refresh failed"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39407"
|
||||
msgid "Full library sync finished"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39408"
|
||||
msgid "Sync had to skip some items because they could not be processed. Kodi may be instable now!! Please post your Kodi logs to the Plex forum."
|
||||
msgstr ""
|
||||
|
@ -1375,10 +1232,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 +1275,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 +1322,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,14 +1,13 @@
|
|||
# XBMC Media Center language file
|
||||
# Translators:
|
||||
# Croneter None <croneter@gmail.com>, 2020
|
||||
#
|
||||
# Croneter None <croneter@gmail.com>, 2017
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PlexKodiConnect\n"
|
||||
"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"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Croneter None <croneter@gmail.com>, 2017\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 +43,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: "
|
||||
|
@ -84,7 +76,7 @@ msgstr "Progreso aproximado"
|
|||
# PKC settings - Artwork
|
||||
msgctxt "#30011"
|
||||
msgid "Artwork left to cache:"
|
||||
msgstr "Arte pendiente de caché:"
|
||||
msgstr "Arte pendiente de cache"
|
||||
|
||||
# Button text
|
||||
msgctxt "#30012"
|
||||
|
@ -100,10 +92,10 @@ msgctxt "#30014"
|
|||
msgid "Connection"
|
||||
msgstr "Conexión"
|
||||
|
||||
# Pop-up notification if user tried to manually initiate fanart download
|
||||
# PKC settings - Artwork
|
||||
msgctxt "#30015"
|
||||
msgid "Fanart download already running"
|
||||
msgstr "La descarga de fondos ya esta en marcha"
|
||||
msgid "Movie and show FanartTV lookups left to do:"
|
||||
msgstr "Búsquedas en Fanart.tv de películas y series pendientes de hacer:"
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Device Name"
|
||||
|
@ -119,7 +111,7 @@ msgctxt "#30018"
|
|||
msgid "Checking FanartTV for %s items"
|
||||
msgstr "Verificando en Fanart.tv %s ítems"
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
# Sync notification displayed when FanartTV lookup is completed
|
||||
msgctxt "#30019"
|
||||
msgid "FanartTV lookup completed"
|
||||
msgstr "Búsqueda en fanart.tv completada"
|
||||
|
@ -166,19 +158,6 @@ msgstr ""
|
|||
"Prefijo en el nombre de la lista de reproducción de Kodi para designer la "
|
||||
"sincronización"
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
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 +257,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"
|
||||
|
@ -338,46 +313,6 @@ msgctxt "#30198"
|
|||
msgid "Search"
|
||||
msgstr "Buscar"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30200"
|
||||
msgid ""
|
||||
"In the following window, enter the server's hostname (or IP) where your Plex"
|
||||
" media resides. Mind the case!"
|
||||
msgstr ""
|
||||
"En la siguiente Ventana, digite el nombre de host del servidor (o dirección "
|
||||
"IP) donde reside su media de Plex. ¡Tenga cuidado con mayúsculas!"
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# for hostname
|
||||
msgctxt "#30201"
|
||||
msgid "Enter server hostname (or IP)"
|
||||
msgstr "Digite el nombre de host del servidor (o dirección IP)"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30202"
|
||||
msgid ""
|
||||
"In the following window, enter the network protocol you would like to use. "
|
||||
"This is likely 'smb'."
|
||||
msgstr ""
|
||||
"En la siguiente Ventana, digite el protocolo de red que desea utilizer. Esto"
|
||||
" es probablemente 'smb'."
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# protocol
|
||||
msgctxt "#30203"
|
||||
msgid "Enter network protocol"
|
||||
msgstr "Digite protocolo de ted"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30204"
|
||||
msgid "The hostname or IP '{0}' that you entered is not valid."
|
||||
msgstr "El nombre de host o dirección IP '{0}' que usted digitó no es válido."
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30205"
|
||||
msgid "The protocol '{0}' that you entered is not supported."
|
||||
msgstr "El protocolo '{0}' que usted digitó no está soportado."
|
||||
|
||||
# Video node naming for random e.g. movies
|
||||
msgctxt "#30227"
|
||||
msgid "Random"
|
||||
|
@ -491,14 +426,6 @@ msgctxt "#30416"
|
|||
msgid "Settings for the Plex Server"
|
||||
msgstr "Configuración para el servidor Plex"
|
||||
|
||||
msgctxt "#30417"
|
||||
msgid ""
|
||||
"Could not change the Kodi settings file {0}. PKC might not work correctly. "
|
||||
"Error: {1}"
|
||||
msgstr ""
|
||||
"No se ha podido cambiar el archivo de ajustes de Kodi {0}. PKC puede no "
|
||||
"funcionar correctamente. Error: {1}"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30500"
|
||||
msgid "Verify Host SSL Certificate (more secure)"
|
||||
|
@ -509,16 +436,10 @@ msgctxt "#30501"
|
|||
msgid "Client SSL certificate"
|
||||
msgstr "Certificado SSL de cliente"
|
||||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#30502"
|
||||
msgid "Sync Plex artwork from the PMS (recommended)"
|
||||
msgstr "Sincronizar el arte de Plex desde PMS (recomendado)"
|
||||
|
||||
# Message shown if SSL HTTPS certificate fails
|
||||
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 - Connection
|
||||
msgctxt "#30505"
|
||||
msgid "[COLOR yellow]Reset login attempts[/COLOR]"
|
||||
msgstr "[COLOR yellow] Restablecer intentos de inicio de sesión[/COLOR]"
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#30506"
|
||||
|
@ -563,8 +484,6 @@ msgstr "Limitar hilos de caché de arte (recomendado para rpi)"
|
|||
msgctxt "#30514"
|
||||
msgid "Show all Plex extras instead of immediately playing trailers"
|
||||
msgstr ""
|
||||
"Mostrar todos los extras de Plex en vez de reproducir inmediatamente los "
|
||||
"trailers"
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
msgctxt "#30515"
|
||||
|
@ -578,8 +497,8 @@ msgstr "Reproducción"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30517"
|
||||
msgid "Set network credentials for Direct Paths and direct play"
|
||||
msgstr "Establezca credenciales de red para Direct Paths y direct play"
|
||||
msgid "[COLOR yellow]Enter network credentials[/COLOR]"
|
||||
msgstr "[COLOR yellow] Digitar credenciales de red[/COLOR]"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30518"
|
||||
|
@ -606,23 +525,6 @@ msgctxt "#30522"
|
|||
msgid "Force transcode h265/HEVC"
|
||||
msgstr "Obligar transcodificación de h265/HEVC"
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
msgctxt "#30523"
|
||||
msgid "Also show sync progress for playstate and user data"
|
||||
msgstr ""
|
||||
"Mostrar tambien el progreso de sincronizacion para el estado de reproduccion"
|
||||
" y datos del usuario"
|
||||
|
||||
# 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
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -688,8 +590,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 +612,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,29 +632,15 @@ 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"
|
||||
msgstr "Nombre de usuario o contraseña incorrecto"
|
||||
|
||||
msgctxt "#33010"
|
||||
msgid "User is unauthorized for server {0}"
|
||||
msgstr "El usuario no esta autorizado para el servidor {0}"
|
||||
|
||||
msgctxt "#33011"
|
||||
msgid "Plex.tv did not provide us a valid list of Plex users, sorry."
|
||||
msgstr "Plex.tv no proveyó una lista válida de usuarios de Plex, disculpas."
|
||||
msgid "Failed to authenticate too many times. Reset in the settings."
|
||||
msgstr ""
|
||||
"No se pudo autenticar demasiadas veces. Restablecer en la configuración."
|
||||
|
||||
# Dialog before playback
|
||||
msgctxt "#33013"
|
||||
|
@ -912,8 +785,10 @@ msgstr ""
|
|||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#39020"
|
||||
msgid "Cache all images to Kodi texture cache now"
|
||||
msgstr "Hacer caché ahora de todas las imágenes al Kodi texture cache"
|
||||
msgid "[COLOR yellow]Cache all images to Kodi texture cache now[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow] Hacer caché ahora a todas las imágenes al caché de texturas "
|
||||
"de Kodi[/COLOR]"
|
||||
|
||||
# Appended to a listed PMS if it is in the same LAN network as PKC
|
||||
msgctxt "#39022"
|
||||
|
@ -970,7 +845,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
|
||||
|
@ -996,16 +871,6 @@ msgstr ""
|
|||
"Reemplazar rutas Plex /volume1/media o \\\\myserver\\media con rutas SMB "
|
||||
"personalizadas smb://NAS/mystuff"
|
||||
|
||||
# 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"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39037"
|
||||
msgid "Original Plex MOVIE path to replace:"
|
||||
|
@ -1082,8 +947,8 @@ msgstr "¿Nada funciona? ¡Intente un reset completo!"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39050"
|
||||
msgid "Choose Plex Server from a list"
|
||||
msgstr "Elegir el Servidor Plex de una lista"
|
||||
msgid "[COLOR yellow]Choose Plex Server from a list[/COLOR]"
|
||||
msgstr "[COLOR yellow] Elegir servidor Plex de una lista[/COLOR]"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39051"
|
||||
|
@ -1112,15 +977,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 +1030,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"
|
||||
|
@ -1171,8 +1044,10 @@ msgstr "El Plex Media Server actual:"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39068"
|
||||
msgid "Manually enter Plex Media Server address"
|
||||
msgstr "Digitar manualmente la dirección del Plex Media Server"
|
||||
msgid "[COLOR yellow]Manually enter Plex Media Server address[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow] Introducir manualmente la dirección del Plex Media "
|
||||
"Server[/COLOR]"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39069"
|
||||
|
@ -1189,11 +1064,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 +1074,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 +1091,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"
|
||||
|
@ -1239,68 +1110,6 @@ msgstr ""
|
|||
"Plex Companion no pudo abrir el puerto GDM. Por favor cámbielo en los "
|
||||
"ajustes de PKC."
|
||||
|
||||
# Pop-up on initial sync.
|
||||
# Check that next translations for Add-on Paths and Direct Paths are
|
||||
# identical!
|
||||
msgctxt "#39080"
|
||||
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 ""
|
||||
"¿Utilizar Add-on Paths (por defecto, fácil) o Direct Paths? Escoja Add-on "
|
||||
"Paths si no está seguro(a). ¡PKC no funcionará si su configuración de "
|
||||
"Direct Paths está incorrecta!"
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39081"
|
||||
msgid "Add-on Paths"
|
||||
msgstr "Add-on Paths"
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39082"
|
||||
msgid "Direct Paths"
|
||||
msgstr "Direct Paths"
|
||||
|
||||
# Dialog for manually entering PMS
|
||||
msgctxt "#39083"
|
||||
msgid "Enter PMS IP or URL"
|
||||
msgstr "Introduzca la URL o IP del PMS"
|
||||
|
||||
# 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"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "Terminar sesión del usuario de Plex Home "
|
||||
|
@ -1309,6 +1118,10 @@ msgctxt "#39201"
|
|||
msgid "Settings"
|
||||
msgstr "Opciones"
|
||||
|
||||
msgctxt "#39203"
|
||||
msgid "Refresh Plex playlists/nodes"
|
||||
msgstr "Actualizar listas de reproducción/nodos de Plex"
|
||||
|
||||
msgctxt "#39204"
|
||||
msgid "Perform manual library sync"
|
||||
msgstr "Realizar sincronización manual de la bibilioteca"
|
||||
|
@ -1338,8 +1151,10 @@ msgstr "No se pudo restablecer PKC. Trate de reiniciar Kodi."
|
|||
|
||||
# PKC Settings - Plex
|
||||
msgctxt "#39209"
|
||||
msgid "Toggle plex.tv login (sign in or sign out)"
|
||||
msgstr "Alternar login de plex.tv (entrar o salir)"
|
||||
msgid "[COLOR yellow]Toggle plex.tv login (sign in or sign out)[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow] Alterne Inicio de sesión de plex.tv (ingresar o "
|
||||
"salir)[/COLOR]"
|
||||
|
||||
msgctxt "#39210"
|
||||
msgid "Not yet connected to Plex Server"
|
||||
|
@ -1349,19 +1164,21 @@ msgctxt "#39211"
|
|||
msgid "Watch later"
|
||||
msgstr "Ver Luego"
|
||||
|
||||
# Error message pop-up if {0} cannot be contacted. {0} will be replaced by
|
||||
# e.g. the PMS' name
|
||||
# String attached at the end to get something like "PMS Name is offline"
|
||||
msgctxt "#39213"
|
||||
msgid "{0} offline"
|
||||
msgstr "{0} fuera de linea"
|
||||
msgid "is offline"
|
||||
msgstr "está desconectado"
|
||||
|
||||
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 ""
|
||||
"Does your Plex Media Server support SSL connections? (https instead of "
|
||||
"http)?"
|
||||
msgstr ""
|
||||
"¿Su Plex Media Server admite conexiones SSL? ¿(https en lugar de http)?"
|
||||
|
||||
msgctxt "#39218"
|
||||
msgid "Error contacting PMS"
|
||||
|
@ -1381,8 +1198,8 @@ msgid "plex.tv toggle successful"
|
|||
msgstr "alternado de plex.tv exitoso"
|
||||
|
||||
msgctxt "#39222"
|
||||
msgid "Look for missing fanart on FanartTV now"
|
||||
msgstr "Buscar fanart faltante en fanart.tv ahora"
|
||||
msgid "[COLOR yellow]Look for missing fanart on FanartTV now[/COLOR]"
|
||||
msgstr "[COLOR yellow Buscar ahora fanart faltante en FanartTV[/COLOR]"
|
||||
|
||||
msgctxt "#39223"
|
||||
msgid ""
|
||||
|
@ -1410,26 +1227,11 @@ msgctxt "#39227"
|
|||
msgid "Logged in to plex.tv"
|
||||
msgstr "Conectado a plex.tv"
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
# Message in the PKC settings to display the plex.tv username. Leave the colon
|
||||
# :
|
||||
msgctxt "#39228"
|
||||
msgid "Plex admin user"
|
||||
msgstr "Administrador de Plex"
|
||||
|
||||
# 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 "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"
|
||||
|
||||
# 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"
|
||||
msgid "Plex user:"
|
||||
msgstr "Usuario de Plex:"
|
||||
|
||||
msgctxt "#39250"
|
||||
msgid ""
|
||||
|
@ -1502,11 +1304,12 @@ msgstr ""
|
|||
|
||||
msgctxt "#39403"
|
||||
msgid ""
|
||||
"The current Kodi version is not supported by PKC. Please consult the Plex "
|
||||
"forum."
|
||||
"Cancelling the database syncing process. Current Kodi version is "
|
||||
"unsupported. Please verify your logs for more info."
|
||||
msgstr ""
|
||||
"La versión actual de Kodi no está soportada por PKC. Por favor consultar el"
|
||||
" fórum de Plex."
|
||||
"Cancelando proceso de sincronización de la base de datos. La versión de Kodi"
|
||||
" actual no es compatible. Por favor, compruebe la bitácora para obtener más "
|
||||
"información."
|
||||
|
||||
msgctxt "#39405"
|
||||
msgid "Plex playlists/nodes refreshed"
|
||||
|
@ -1516,6 +1319,10 @@ msgctxt "#39406"
|
|||
msgid "Plex playlists/nodes refresh failed"
|
||||
msgstr "Error de actualización de listas de reproducción/nodos de Plex"
|
||||
|
||||
msgctxt "#39407"
|
||||
msgid "Full library sync finished"
|
||||
msgstr "Sincronización completa de la biblioteca terminada"
|
||||
|
||||
msgctxt "#39408"
|
||||
msgid ""
|
||||
"Sync had to skip some items because they could not be processed. Kodi may be"
|
||||
|
@ -1547,10 +1354,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 +1412,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 +1470,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 +1495,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,16 +1,13 @@
|
|||
# XBMC Media Center language file
|
||||
# Translators:
|
||||
# Dani <danichispa@gmail.com>, 2019
|
||||
# Bartolome Soriano <bsoriano@gmail.com>, 2019
|
||||
# Croneter None <croneter@gmail.com>, 2020
|
||||
#
|
||||
# Bartolome Soriano <bsoriano@gmail.com>, 2017
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PlexKodiConnect\n"
|
||||
"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"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Bartolome Soriano <bsoriano@gmail.com>, 2017\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 +43,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: "
|
||||
|
@ -86,7 +76,7 @@ msgstr "Progreso aproximado"
|
|||
# PKC settings - Artwork
|
||||
msgctxt "#30011"
|
||||
msgid "Artwork left to cache:"
|
||||
msgstr "Arte pendiente de caché:"
|
||||
msgstr "Arte pendiente de cache"
|
||||
|
||||
# Button text
|
||||
msgctxt "#30012"
|
||||
|
@ -102,10 +92,10 @@ msgctxt "#30014"
|
|||
msgid "Connection"
|
||||
msgstr "Conexión"
|
||||
|
||||
# Pop-up notification if user tried to manually initiate fanart download
|
||||
# PKC settings - Artwork
|
||||
msgctxt "#30015"
|
||||
msgid "Fanart download already running"
|
||||
msgstr "La descarga de fondos ya esta en marcha"
|
||||
msgid "Movie and show FanartTV lookups left to do:"
|
||||
msgstr "Búsquedas en Fanart.tv de películas y series pendientes de hacer:"
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Device Name"
|
||||
|
@ -121,7 +111,7 @@ msgctxt "#30018"
|
|||
msgid "Checking FanartTV for %s items"
|
||||
msgstr "Verificando en Fanart.tv %s ítems"
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
# Sync notification displayed when FanartTV lookup is completed
|
||||
msgctxt "#30019"
|
||||
msgid "FanartTV lookup completed"
|
||||
msgstr "Búsqueda en fanart.tv completada"
|
||||
|
@ -168,19 +158,6 @@ msgstr ""
|
|||
"Prefijo en el nombre de la lista de reproducción de Kodi para designer la "
|
||||
"sincronización"
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
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 +257,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"
|
||||
|
@ -340,46 +313,6 @@ msgctxt "#30198"
|
|||
msgid "Search"
|
||||
msgstr "Buscar"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30200"
|
||||
msgid ""
|
||||
"In the following window, enter the server's hostname (or IP) where your Plex"
|
||||
" media resides. Mind the case!"
|
||||
msgstr ""
|
||||
"En la siguiente Ventana, digite el nombre de host del servidor (o dirección "
|
||||
"IP) donde reside su media de Plex. ¡Tenga cuidado con mayúsculas!"
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# for hostname
|
||||
msgctxt "#30201"
|
||||
msgid "Enter server hostname (or IP)"
|
||||
msgstr "Digite el nombre de host del servidor (o dirección IP)"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30202"
|
||||
msgid ""
|
||||
"In the following window, enter the network protocol you would like to use. "
|
||||
"This is likely 'smb'."
|
||||
msgstr ""
|
||||
"En la siguiente Ventana, digite el protocolo de red que desea utilizer. Esto"
|
||||
" es probablemente 'smb'."
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# protocol
|
||||
msgctxt "#30203"
|
||||
msgid "Enter network protocol"
|
||||
msgstr "Digite protocolo de ted"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30204"
|
||||
msgid "The hostname or IP '{0}' that you entered is not valid."
|
||||
msgstr "El nombre de host o dirección IP '{0}' que usted digitó no es válido."
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30205"
|
||||
msgid "The protocol '{0}' that you entered is not supported."
|
||||
msgstr "El protocolo '{0}' que usted digitó no está soportado."
|
||||
|
||||
# Video node naming for random e.g. movies
|
||||
msgctxt "#30227"
|
||||
msgid "Random"
|
||||
|
@ -493,14 +426,6 @@ msgctxt "#30416"
|
|||
msgid "Settings for the Plex Server"
|
||||
msgstr "Configuración para el servidor Plex"
|
||||
|
||||
msgctxt "#30417"
|
||||
msgid ""
|
||||
"Could not change the Kodi settings file {0}. PKC might not work correctly. "
|
||||
"Error: {1}"
|
||||
msgstr ""
|
||||
"No se ha podido cambiar el archivo de ajustes de Kodi {0}. PKC puede no "
|
||||
"funcionar correctamente. Error: {1}"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30500"
|
||||
msgid "Verify Host SSL Certificate (more secure)"
|
||||
|
@ -511,16 +436,10 @@ msgctxt "#30501"
|
|||
msgid "Client SSL certificate"
|
||||
msgstr "Certificado SSL de cliente"
|
||||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#30502"
|
||||
msgid "Sync Plex artwork from the PMS (recommended)"
|
||||
msgstr "Sincronizar el arte de Plex desde PMS (recomendado)"
|
||||
|
||||
# Message shown if SSL HTTPS certificate fails
|
||||
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 - Connection
|
||||
msgctxt "#30505"
|
||||
msgid "[COLOR yellow]Reset login attempts[/COLOR]"
|
||||
msgstr "[COLOR yellow] Restablecer intentos de inicio de sesión[/COLOR]"
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#30506"
|
||||
|
@ -565,8 +484,6 @@ msgstr "Limitar hilos de caché de arte (recomendado para rpi)"
|
|||
msgctxt "#30514"
|
||||
msgid "Show all Plex extras instead of immediately playing trailers"
|
||||
msgstr ""
|
||||
"Mostrar todos los extras de Plex en vez de reproducir inmediatamente los "
|
||||
"trailers"
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
msgctxt "#30515"
|
||||
|
@ -580,8 +497,8 @@ msgstr "Reproducción"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30517"
|
||||
msgid "Set network credentials for Direct Paths and direct play"
|
||||
msgstr "Establezca credenciales de red para Direct Paths y direct play"
|
||||
msgid "[COLOR yellow]Enter network credentials[/COLOR]"
|
||||
msgstr "[COLOR yellow] Digitar credenciales de red[/COLOR]"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30518"
|
||||
|
@ -608,23 +525,6 @@ msgctxt "#30522"
|
|||
msgid "Force transcode h265/HEVC"
|
||||
msgstr "Obligar transcodificación de h265/HEVC"
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
msgctxt "#30523"
|
||||
msgid "Also show sync progress for playstate and user data"
|
||||
msgstr ""
|
||||
"Mostrar tambien el progreso de sincronizacion para el estado de reproduccion"
|
||||
" y datos del usuario"
|
||||
|
||||
# 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
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -690,8 +590,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 +612,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,29 +632,15 @@ 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"
|
||||
msgstr "Nombre de usuario o contraseña incorrecto"
|
||||
|
||||
msgctxt "#33010"
|
||||
msgid "User is unauthorized for server {0}"
|
||||
msgstr "El usuario no esta autorizado para el servidor {0}"
|
||||
|
||||
msgctxt "#33011"
|
||||
msgid "Plex.tv did not provide us a valid list of Plex users, sorry."
|
||||
msgstr "Plex.tv no proveyó una lista válida de usuarios de Plex, disculpas."
|
||||
msgid "Failed to authenticate too many times. Reset in the settings."
|
||||
msgstr ""
|
||||
"No se pudo autenticar demasiadas veces. Restablecer en la configuración."
|
||||
|
||||
# Dialog before playback
|
||||
msgctxt "#33013"
|
||||
|
@ -914,8 +785,10 @@ msgstr ""
|
|||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#39020"
|
||||
msgid "Cache all images to Kodi texture cache now"
|
||||
msgstr "Hacer caché ahora de todas las imágenes al Kodi texture cache"
|
||||
msgid "[COLOR yellow]Cache all images to Kodi texture cache now[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow] Hacer caché ahora a todas las imágenes al caché de texturas "
|
||||
"de Kodi[/COLOR]"
|
||||
|
||||
# Appended to a listed PMS if it is in the same LAN network as PKC
|
||||
msgctxt "#39022"
|
||||
|
@ -972,7 +845,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
|
||||
|
@ -998,16 +871,6 @@ msgstr ""
|
|||
"Reemplazar rutas Plex /volume1/media o \\\\myserver\\media con rutas SMB "
|
||||
"personalizadas smb://NAS/mystuff"
|
||||
|
||||
# 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"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39037"
|
||||
msgid "Original Plex MOVIE path to replace:"
|
||||
|
@ -1084,8 +947,8 @@ msgstr "¿Nada funciona? ¡Intente un reset completo!"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39050"
|
||||
msgid "Choose Plex Server from a list"
|
||||
msgstr "Elegir el Servidor Plex de una lista"
|
||||
msgid "[COLOR yellow]Choose Plex Server from a list[/COLOR]"
|
||||
msgstr "[COLOR yellow] Elegir servidor Plex de una lista[/COLOR]"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39051"
|
||||
|
@ -1114,15 +977,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 +1030,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"
|
||||
|
@ -1173,8 +1044,10 @@ msgstr "El Plex Media Server actual:"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39068"
|
||||
msgid "Manually enter Plex Media Server address"
|
||||
msgstr "Digitar manualmente la dirección del Plex Media Server"
|
||||
msgid "[COLOR yellow]Manually enter Plex Media Server address[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow] Introducir manualmente la dirección del Plex Media "
|
||||
"Server[/COLOR]"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39069"
|
||||
|
@ -1191,11 +1064,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 +1074,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 +1091,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"
|
||||
|
@ -1241,68 +1110,6 @@ msgstr ""
|
|||
"Plex Companion no pudo abrir el puerto GDM. Por favor cámbielo en los "
|
||||
"ajustes de PKC."
|
||||
|
||||
# Pop-up on initial sync.
|
||||
# Check that next translations for Add-on Paths and Direct Paths are
|
||||
# identical!
|
||||
msgctxt "#39080"
|
||||
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 ""
|
||||
"¿Utilizar Add-on Paths (por defecto, fácil) o Direct Paths? Escoja Add-on "
|
||||
"Paths si no está seguro(a). ¡PKC no funcionará si su configuración de "
|
||||
"Direct Paths está incorrecta!"
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39081"
|
||||
msgid "Add-on Paths"
|
||||
msgstr "Add-on Paths"
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39082"
|
||||
msgid "Direct Paths"
|
||||
msgstr "Direct Paths"
|
||||
|
||||
# Dialog for manually entering PMS
|
||||
msgctxt "#39083"
|
||||
msgid "Enter PMS IP or URL"
|
||||
msgstr "Introduzca la URL o IP del PMS"
|
||||
|
||||
# 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"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "Terminar sesión del usuario de Plex Home "
|
||||
|
@ -1311,6 +1118,10 @@ msgctxt "#39201"
|
|||
msgid "Settings"
|
||||
msgstr "Opciones"
|
||||
|
||||
msgctxt "#39203"
|
||||
msgid "Refresh Plex playlists/nodes"
|
||||
msgstr "Actualizar listas de reproducción/nodos de Plex"
|
||||
|
||||
msgctxt "#39204"
|
||||
msgid "Perform manual library sync"
|
||||
msgstr "Realizar sincronización manual de la bibilioteca"
|
||||
|
@ -1340,8 +1151,10 @@ msgstr "No se pudo restablecer PKC. Trate de reiniciar Kodi."
|
|||
|
||||
# PKC Settings - Plex
|
||||
msgctxt "#39209"
|
||||
msgid "Toggle plex.tv login (sign in or sign out)"
|
||||
msgstr "Alternar login de plex.tv (entrar o salir)"
|
||||
msgid "[COLOR yellow]Toggle plex.tv login (sign in or sign out)[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow] Alterne Inicio de sesión de plex.tv (ingresar o "
|
||||
"salir)[/COLOR]"
|
||||
|
||||
msgctxt "#39210"
|
||||
msgid "Not yet connected to Plex Server"
|
||||
|
@ -1351,19 +1164,21 @@ msgctxt "#39211"
|
|||
msgid "Watch later"
|
||||
msgstr "Ver Luego"
|
||||
|
||||
# Error message pop-up if {0} cannot be contacted. {0} will be replaced by
|
||||
# e.g. the PMS' name
|
||||
# String attached at the end to get something like "PMS Name is offline"
|
||||
msgctxt "#39213"
|
||||
msgid "{0} offline"
|
||||
msgstr "{0} fuera de linea"
|
||||
msgid "is offline"
|
||||
msgstr "está desconectado"
|
||||
|
||||
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 ""
|
||||
"Does your Plex Media Server support SSL connections? (https instead of "
|
||||
"http)?"
|
||||
msgstr ""
|
||||
"¿Su Plex Media Server admite conexiones SSL? ¿(https en lugar de http)?"
|
||||
|
||||
msgctxt "#39218"
|
||||
msgid "Error contacting PMS"
|
||||
|
@ -1383,8 +1198,8 @@ msgid "plex.tv toggle successful"
|
|||
msgstr "alternado de plex.tv exitoso"
|
||||
|
||||
msgctxt "#39222"
|
||||
msgid "Look for missing fanart on FanartTV now"
|
||||
msgstr "Buscar fanart faltante en fanart.tv ahora"
|
||||
msgid "[COLOR yellow]Look for missing fanart on FanartTV now[/COLOR]"
|
||||
msgstr "[COLOR yellow Buscar ahora fanart faltante en FanartTV[/COLOR]"
|
||||
|
||||
msgctxt "#39223"
|
||||
msgid ""
|
||||
|
@ -1412,26 +1227,11 @@ msgctxt "#39227"
|
|||
msgid "Logged in to plex.tv"
|
||||
msgstr "Conectado a plex.tv"
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
# Message in the PKC settings to display the plex.tv username. Leave the colon
|
||||
# :
|
||||
msgctxt "#39228"
|
||||
msgid "Plex admin user"
|
||||
msgstr "Administrador de Plex"
|
||||
|
||||
# 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 "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"
|
||||
|
||||
# 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"
|
||||
msgid "Plex user:"
|
||||
msgstr "Usuario de Plex:"
|
||||
|
||||
msgctxt "#39250"
|
||||
msgid ""
|
||||
|
@ -1504,11 +1304,12 @@ msgstr ""
|
|||
|
||||
msgctxt "#39403"
|
||||
msgid ""
|
||||
"The current Kodi version is not supported by PKC. Please consult the Plex "
|
||||
"forum."
|
||||
"Cancelling the database syncing process. Current Kodi version is "
|
||||
"unsupported. Please verify your logs for more info."
|
||||
msgstr ""
|
||||
"La versión actual de Kodi no está soportada por PKC. Por favor consultar el"
|
||||
" fórum de Plex."
|
||||
"Cancelando proceso de sincronización de la base de datos. La versión de Kodi"
|
||||
" actual no es compatible. Por favor, compruebe la bitácora para obtener más "
|
||||
"información."
|
||||
|
||||
msgctxt "#39405"
|
||||
msgid "Plex playlists/nodes refreshed"
|
||||
|
@ -1518,6 +1319,10 @@ msgctxt "#39406"
|
|||
msgid "Plex playlists/nodes refresh failed"
|
||||
msgstr "Error de actualización de listas de reproducción/nodos de Plex"
|
||||
|
||||
msgctxt "#39407"
|
||||
msgid "Full library sync finished"
|
||||
msgstr "Sincronización completa de la biblioteca terminada"
|
||||
|
||||
msgctxt "#39408"
|
||||
msgid ""
|
||||
"Sync had to skip some items because they could not be processed. Kodi may be"
|
||||
|
@ -1549,10 +1354,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 +1412,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 +1470,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 +1495,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,14 +1,13 @@
|
|||
# XBMC Media Center language file
|
||||
# Translators:
|
||||
# Croneter None <croneter@gmail.com>, 2020
|
||||
#
|
||||
# Croneter None <croneter@gmail.com>, 2017
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PlexKodiConnect\n"
|
||||
"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"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Croneter None <croneter@gmail.com>, 2017\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 +43,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: "
|
||||
|
@ -84,7 +76,7 @@ msgstr "Progreso aproximado"
|
|||
# PKC settings - Artwork
|
||||
msgctxt "#30011"
|
||||
msgid "Artwork left to cache:"
|
||||
msgstr "Arte pendiente de caché:"
|
||||
msgstr "Arte pendiente de cache"
|
||||
|
||||
# Button text
|
||||
msgctxt "#30012"
|
||||
|
@ -100,10 +92,10 @@ msgctxt "#30014"
|
|||
msgid "Connection"
|
||||
msgstr "Conexión"
|
||||
|
||||
# Pop-up notification if user tried to manually initiate fanart download
|
||||
# PKC settings - Artwork
|
||||
msgctxt "#30015"
|
||||
msgid "Fanart download already running"
|
||||
msgstr "La descarga de fondos ya esta en marcha"
|
||||
msgid "Movie and show FanartTV lookups left to do:"
|
||||
msgstr "Búsquedas en Fanart.tv de películas y series pendientes de hacer:"
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Device Name"
|
||||
|
@ -119,7 +111,7 @@ msgctxt "#30018"
|
|||
msgid "Checking FanartTV for %s items"
|
||||
msgstr "Verificando en Fanart.tv %s ítems"
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
# Sync notification displayed when FanartTV lookup is completed
|
||||
msgctxt "#30019"
|
||||
msgid "FanartTV lookup completed"
|
||||
msgstr "Búsqueda en fanart.tv completada"
|
||||
|
@ -166,19 +158,6 @@ msgstr ""
|
|||
"Prefijo en el nombre de la lista de reproducción de Kodi para designer la "
|
||||
"sincronización"
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
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 +257,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"
|
||||
|
@ -338,46 +313,6 @@ msgctxt "#30198"
|
|||
msgid "Search"
|
||||
msgstr "Buscar"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30200"
|
||||
msgid ""
|
||||
"In the following window, enter the server's hostname (or IP) where your Plex"
|
||||
" media resides. Mind the case!"
|
||||
msgstr ""
|
||||
"En la siguiente Ventana, digite el nombre de host del servidor (o dirección "
|
||||
"IP) donde reside su media de Plex. ¡Tenga cuidado con mayúsculas!"
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# for hostname
|
||||
msgctxt "#30201"
|
||||
msgid "Enter server hostname (or IP)"
|
||||
msgstr "Digite el nombre de host del servidor (o dirección IP)"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30202"
|
||||
msgid ""
|
||||
"In the following window, enter the network protocol you would like to use. "
|
||||
"This is likely 'smb'."
|
||||
msgstr ""
|
||||
"En la siguiente Ventana, digite el protocolo de red que desea utilizer. Esto"
|
||||
" es probablemente 'smb'."
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# protocol
|
||||
msgctxt "#30203"
|
||||
msgid "Enter network protocol"
|
||||
msgstr "Digite protocolo de ted"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30204"
|
||||
msgid "The hostname or IP '{0}' that you entered is not valid."
|
||||
msgstr "El nombre de host o dirección IP '{0}' que usted digitó no es válido."
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30205"
|
||||
msgid "The protocol '{0}' that you entered is not supported."
|
||||
msgstr "El protocolo '{0}' que usted digitó no está soportado."
|
||||
|
||||
# Video node naming for random e.g. movies
|
||||
msgctxt "#30227"
|
||||
msgid "Random"
|
||||
|
@ -491,14 +426,6 @@ msgctxt "#30416"
|
|||
msgid "Settings for the Plex Server"
|
||||
msgstr "Configuración para el servidor Plex"
|
||||
|
||||
msgctxt "#30417"
|
||||
msgid ""
|
||||
"Could not change the Kodi settings file {0}. PKC might not work correctly. "
|
||||
"Error: {1}"
|
||||
msgstr ""
|
||||
"No se ha podido cambiar el archivo de ajustes de Kodi {0}. PKC puede no "
|
||||
"funcionar correctamente. Error: {1}"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30500"
|
||||
msgid "Verify Host SSL Certificate (more secure)"
|
||||
|
@ -509,16 +436,10 @@ msgctxt "#30501"
|
|||
msgid "Client SSL certificate"
|
||||
msgstr "Certificado SSL de cliente"
|
||||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#30502"
|
||||
msgid "Sync Plex artwork from the PMS (recommended)"
|
||||
msgstr "Sincronizar el arte de Plex desde PMS (recomendado)"
|
||||
|
||||
# Message shown if SSL HTTPS certificate fails
|
||||
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 - Connection
|
||||
msgctxt "#30505"
|
||||
msgid "[COLOR yellow]Reset login attempts[/COLOR]"
|
||||
msgstr "[COLOR yellow] Restablecer intentos de inicio de sesión[/COLOR]"
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#30506"
|
||||
|
@ -563,8 +484,6 @@ msgstr "Limitar hilos de caché de arte (recomendado para rpi)"
|
|||
msgctxt "#30514"
|
||||
msgid "Show all Plex extras instead of immediately playing trailers"
|
||||
msgstr ""
|
||||
"Mostrar todos los extras de Plex en vez de reproducir inmediatamente los "
|
||||
"trailers"
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
msgctxt "#30515"
|
||||
|
@ -578,8 +497,8 @@ msgstr "Reproducción"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30517"
|
||||
msgid "Set network credentials for Direct Paths and direct play"
|
||||
msgstr "Establezca credenciales de red para Direct Paths y direct play"
|
||||
msgid "[COLOR yellow]Enter network credentials[/COLOR]"
|
||||
msgstr "[COLOR yellow] Digitar credenciales de red[/COLOR]"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30518"
|
||||
|
@ -606,23 +525,6 @@ msgctxt "#30522"
|
|||
msgid "Force transcode h265/HEVC"
|
||||
msgstr "Obligar transcodificación de h265/HEVC"
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
msgctxt "#30523"
|
||||
msgid "Also show sync progress for playstate and user data"
|
||||
msgstr ""
|
||||
"Mostrar tambien el progreso de sincronizacion para el estado de reproduccion"
|
||||
" y datos del usuario"
|
||||
|
||||
# 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
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -688,8 +590,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 +612,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,29 +632,15 @@ 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"
|
||||
msgstr "Nombre de usuario o contraseña incorrecto"
|
||||
|
||||
msgctxt "#33010"
|
||||
msgid "User is unauthorized for server {0}"
|
||||
msgstr "El usuario no esta autorizado para el servidor {0}"
|
||||
|
||||
msgctxt "#33011"
|
||||
msgid "Plex.tv did not provide us a valid list of Plex users, sorry."
|
||||
msgstr "Plex.tv no proveyó una lista válida de usuarios de Plex, disculpas."
|
||||
msgid "Failed to authenticate too many times. Reset in the settings."
|
||||
msgstr ""
|
||||
"No se pudo autenticar demasiadas veces. Restablecer en la configuración."
|
||||
|
||||
# Dialog before playback
|
||||
msgctxt "#33013"
|
||||
|
@ -912,8 +785,10 @@ msgstr ""
|
|||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#39020"
|
||||
msgid "Cache all images to Kodi texture cache now"
|
||||
msgstr "Hacer caché ahora de todas las imágenes al Kodi texture cache"
|
||||
msgid "[COLOR yellow]Cache all images to Kodi texture cache now[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow] Hacer caché ahora a todas las imágenes al caché de texturas "
|
||||
"de Kodi[/COLOR]"
|
||||
|
||||
# Appended to a listed PMS if it is in the same LAN network as PKC
|
||||
msgctxt "#39022"
|
||||
|
@ -970,7 +845,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
|
||||
|
@ -996,16 +871,6 @@ msgstr ""
|
|||
"Reemplazar rutas Plex /volume1/media o \\\\myserver\\media con rutas SMB "
|
||||
"personalizadas smb://NAS/mystuff"
|
||||
|
||||
# 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"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39037"
|
||||
msgid "Original Plex MOVIE path to replace:"
|
||||
|
@ -1082,8 +947,8 @@ msgstr "¿Nada funciona? ¡Intente un reset completo!"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39050"
|
||||
msgid "Choose Plex Server from a list"
|
||||
msgstr "Elegir el Servidor Plex de una lista"
|
||||
msgid "[COLOR yellow]Choose Plex Server from a list[/COLOR]"
|
||||
msgstr "[COLOR yellow] Elegir servidor Plex de una lista[/COLOR]"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39051"
|
||||
|
@ -1112,15 +977,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 +1030,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"
|
||||
|
@ -1171,8 +1044,10 @@ msgstr "El Plex Media Server actual:"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39068"
|
||||
msgid "Manually enter Plex Media Server address"
|
||||
msgstr "Digitar manualmente la dirección del Plex Media Server"
|
||||
msgid "[COLOR yellow]Manually enter Plex Media Server address[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow] Introducir manualmente la dirección del Plex Media "
|
||||
"Server[/COLOR]"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39069"
|
||||
|
@ -1189,11 +1064,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 +1074,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 +1091,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"
|
||||
|
@ -1239,68 +1110,6 @@ msgstr ""
|
|||
"Plex Companion no pudo abrir el puerto GDM. Por favor cámbielo en los "
|
||||
"ajustes de PKC."
|
||||
|
||||
# Pop-up on initial sync.
|
||||
# Check that next translations for Add-on Paths and Direct Paths are
|
||||
# identical!
|
||||
msgctxt "#39080"
|
||||
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 ""
|
||||
"¿Utilizar Add-on Paths (por defecto, fácil) o Direct Paths? Escoja Add-on "
|
||||
"Paths si no está seguro(a). ¡PKC no funcionará si su configuración de "
|
||||
"Direct Paths está incorrecta!"
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39081"
|
||||
msgid "Add-on Paths"
|
||||
msgstr "Add-on Paths"
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39082"
|
||||
msgid "Direct Paths"
|
||||
msgstr "Direct Paths"
|
||||
|
||||
# Dialog for manually entering PMS
|
||||
msgctxt "#39083"
|
||||
msgid "Enter PMS IP or URL"
|
||||
msgstr "Introduzca la URL o IP del PMS"
|
||||
|
||||
# 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"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "Terminar sesión del usuario de Plex Home "
|
||||
|
@ -1309,6 +1118,10 @@ msgctxt "#39201"
|
|||
msgid "Settings"
|
||||
msgstr "Opciones"
|
||||
|
||||
msgctxt "#39203"
|
||||
msgid "Refresh Plex playlists/nodes"
|
||||
msgstr "Actualizar listas de reproducción/nodos de Plex"
|
||||
|
||||
msgctxt "#39204"
|
||||
msgid "Perform manual library sync"
|
||||
msgstr "Realizar sincronización manual de la bibilioteca"
|
||||
|
@ -1338,8 +1151,10 @@ msgstr "No se pudo restablecer PKC. Trate de reiniciar Kodi."
|
|||
|
||||
# PKC Settings - Plex
|
||||
msgctxt "#39209"
|
||||
msgid "Toggle plex.tv login (sign in or sign out)"
|
||||
msgstr "Alternar login de plex.tv (entrar o salir)"
|
||||
msgid "[COLOR yellow]Toggle plex.tv login (sign in or sign out)[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow] Alterne Inicio de sesión de plex.tv (ingresar o "
|
||||
"salir)[/COLOR]"
|
||||
|
||||
msgctxt "#39210"
|
||||
msgid "Not yet connected to Plex Server"
|
||||
|
@ -1349,19 +1164,21 @@ msgctxt "#39211"
|
|||
msgid "Watch later"
|
||||
msgstr "Ver Luego"
|
||||
|
||||
# Error message pop-up if {0} cannot be contacted. {0} will be replaced by
|
||||
# e.g. the PMS' name
|
||||
# String attached at the end to get something like "PMS Name is offline"
|
||||
msgctxt "#39213"
|
||||
msgid "{0} offline"
|
||||
msgstr "{0} fuera de linea"
|
||||
msgid "is offline"
|
||||
msgstr "está desconectado"
|
||||
|
||||
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 ""
|
||||
"Does your Plex Media Server support SSL connections? (https instead of "
|
||||
"http)?"
|
||||
msgstr ""
|
||||
"¿Su Plex Media Server admite conexiones SSL? ¿(https en lugar de http)?"
|
||||
|
||||
msgctxt "#39218"
|
||||
msgid "Error contacting PMS"
|
||||
|
@ -1381,8 +1198,8 @@ msgid "plex.tv toggle successful"
|
|||
msgstr "alternado de plex.tv exitoso"
|
||||
|
||||
msgctxt "#39222"
|
||||
msgid "Look for missing fanart on FanartTV now"
|
||||
msgstr "Buscar fanart faltante en fanart.tv ahora"
|
||||
msgid "[COLOR yellow]Look for missing fanart on FanartTV now[/COLOR]"
|
||||
msgstr "[COLOR yellow Buscar ahora fanart faltante en FanartTV[/COLOR]"
|
||||
|
||||
msgctxt "#39223"
|
||||
msgid ""
|
||||
|
@ -1410,26 +1227,11 @@ msgctxt "#39227"
|
|||
msgid "Logged in to plex.tv"
|
||||
msgstr "Conectado a plex.tv"
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
# Message in the PKC settings to display the plex.tv username. Leave the colon
|
||||
# :
|
||||
msgctxt "#39228"
|
||||
msgid "Plex admin user"
|
||||
msgstr "Administrador de Plex"
|
||||
|
||||
# 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 "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"
|
||||
|
||||
# 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"
|
||||
msgid "Plex user:"
|
||||
msgstr "Usuario de Plex:"
|
||||
|
||||
msgctxt "#39250"
|
||||
msgid ""
|
||||
|
@ -1502,11 +1304,12 @@ msgstr ""
|
|||
|
||||
msgctxt "#39403"
|
||||
msgid ""
|
||||
"The current Kodi version is not supported by PKC. Please consult the Plex "
|
||||
"forum."
|
||||
"Cancelling the database syncing process. Current Kodi version is "
|
||||
"unsupported. Please verify your logs for more info."
|
||||
msgstr ""
|
||||
"La versión actual de Kodi no está soportada por PKC. Por favor consultar el"
|
||||
" fórum de Plex."
|
||||
"Cancelando proceso de sincronización de la base de datos. La versión de Kodi"
|
||||
" actual no es compatible. Por favor, compruebe la bitácora para obtener más "
|
||||
"información."
|
||||
|
||||
msgctxt "#39405"
|
||||
msgid "Plex playlists/nodes refreshed"
|
||||
|
@ -1516,6 +1319,10 @@ msgctxt "#39406"
|
|||
msgid "Plex playlists/nodes refresh failed"
|
||||
msgstr "Error de actualización de listas de reproducción/nodos de Plex"
|
||||
|
||||
msgctxt "#39407"
|
||||
msgid "Full library sync finished"
|
||||
msgstr "Sincronización completa de la biblioteca terminada"
|
||||
|
||||
msgctxt "#39408"
|
||||
msgid ""
|
||||
"Sync had to skip some items because they could not be processed. Kodi may be"
|
||||
|
@ -1547,10 +1354,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 +1412,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 +1470,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 +1495,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"
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,15 +1,13 @@
|
|||
# XBMC Media Center language file
|
||||
# Translators:
|
||||
# Croneter None <croneter@gmail.com>, 2019
|
||||
# Savage93 <savageistheking@gmail.com>, 2021
|
||||
#
|
||||
# Croneter None <croneter@gmail.com>, 2017
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PlexKodiConnect\n"
|
||||
"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"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Croneter None <croneter@gmail.com>, 2017\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 +43,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: "
|
||||
|
@ -78,9 +65,7 @@ msgstr "A képek gyorsítótárazásával kapcsolatos értesítés engedélyezé
|
|||
# PKC settings artwork: Enable image caching during Kodi playback
|
||||
msgctxt "#30009"
|
||||
msgid "Enable image caching during Kodi playback (restart Kodi!)"
|
||||
msgstr ""
|
||||
"A képek gyorsítótárazásának engedélyezése lejátszás alatt (indítsa újra a "
|
||||
"Kodi-t!)"
|
||||
msgstr "A képek gyorsítótárazásának engedélyezése lejátszás alatt (indítsa újra a Kodi-t!)"
|
||||
|
||||
# PKC settings - Artwork
|
||||
msgctxt "#30010"
|
||||
|
@ -106,10 +91,10 @@ msgctxt "#30014"
|
|||
msgid "Connection"
|
||||
msgstr "Kapcsolat"
|
||||
|
||||
# Pop-up notification if user tried to manually initiate fanart download
|
||||
# PKC settings - Artwork
|
||||
msgctxt "#30015"
|
||||
msgid "Fanart download already running"
|
||||
msgstr "Már fut a művészképek letöltése"
|
||||
msgid "Movie and show FanartTV lookups left to do:"
|
||||
msgstr "Hátralévő film és sorozat FanartTV lekérdezések:"
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Device Name"
|
||||
|
@ -125,7 +110,7 @@ msgctxt "#30018"
|
|||
msgid "Checking FanartTV for %s items"
|
||||
msgstr "%s elem keresése a FanartTV-n"
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
# Sync notification displayed when FanartTV lookup is completed
|
||||
msgctxt "#30019"
|
||||
msgid "FanartTV lookup completed"
|
||||
msgstr "A FanartTV lekérdezés befejeződött"
|
||||
|
@ -168,19 +153,6 @@ msgctxt "#30027"
|
|||
msgid "Prefix in Kodi playlist name to trigger sync"
|
||||
msgstr "Kodi lejátszási lista előtag szinkronizálásra kijelöléshez"
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
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 +252,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"
|
||||
|
@ -342,47 +308,6 @@ msgctxt "#30198"
|
|||
msgid "Search"
|
||||
msgstr "Keresés"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30200"
|
||||
msgid ""
|
||||
"In the following window, enter the server's hostname (or IP) where your Plex"
|
||||
" media resides. Mind the case!"
|
||||
msgstr ""
|
||||
"A következő ablakban adja meg a szerver kiszolgálónevét (vagy IP címét) "
|
||||
"amelyen a Plex médiatartalmak megtalálhatók. Figyeljen a kis- és "
|
||||
"nagybetűkre!"
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# for hostname
|
||||
msgctxt "#30201"
|
||||
msgid "Enter server hostname (or IP)"
|
||||
msgstr "Adja meg a szerver kiszolgálónevét (vagy IP címét)"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30202"
|
||||
msgid ""
|
||||
"In the following window, enter the network protocol you would like to use. "
|
||||
"This is likely 'smb'."
|
||||
msgstr ""
|
||||
"A következő ablakban adja meg a használni kívánt hálózati protokollt. Ez "
|
||||
"valószínűleg az 'smb'."
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# protocol
|
||||
msgctxt "#30203"
|
||||
msgid "Enter network protocol"
|
||||
msgstr "Adja meg a hálózati protokollt"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30204"
|
||||
msgid "The hostname or IP '{0}' that you entered is not valid."
|
||||
msgstr "A megadott kiszolgálónév vagy IP cím '{0}' helytelen."
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30205"
|
||||
msgid "The protocol '{0}' that you entered is not supported."
|
||||
msgstr "A megadott hálózati protokoll ('{0}') nem támogatott."
|
||||
|
||||
# Video node naming for random e.g. movies
|
||||
msgctxt "#30227"
|
||||
msgid "Random"
|
||||
|
@ -495,14 +420,6 @@ msgctxt "#30416"
|
|||
msgid "Settings for the Plex Server"
|
||||
msgstr "Plex szerver beállításai"
|
||||
|
||||
msgctxt "#30417"
|
||||
msgid ""
|
||||
"Could not change the Kodi settings file {0}. PKC might not work correctly. "
|
||||
"Error: {1}"
|
||||
msgstr ""
|
||||
"A {0} Kodi beállításfájl megváltoztatása sikertelen. Lehet, hogy a PKC nem "
|
||||
"fog megfelelően működni.Hiba: {1}"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30500"
|
||||
msgid "Verify Host SSL Certificate (more secure)"
|
||||
|
@ -513,17 +430,11 @@ msgctxt "#30501"
|
|||
msgid "Client SSL certificate"
|
||||
msgstr "Kliens SSL tanúsítvány"
|
||||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#30502"
|
||||
msgid "Sync Plex artwork from the PMS (recommended)"
|
||||
msgstr "Plex művészképek szinkronizálása a szerverről (ajánlott)"
|
||||
|
||||
# Message shown if SSL HTTPS certificate fails
|
||||
msgctxt "#30503"
|
||||
msgid "SSL certificate failed to validate. Please check {0} for solutions."
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30505"
|
||||
msgid "[COLOR yellow]Reset login attempts[/COLOR]"
|
||||
msgstr ""
|
||||
"Az SSL tanúsítvány hitelesítése sikertelen. Kérem ellenőrizze a(z) {0}-t a "
|
||||
"megoldásokért."
|
||||
"[COLOR yellow]Bejelentkezési kísérletek számának visszaállítása[/COLOR]"
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#30506"
|
||||
|
@ -583,10 +494,8 @@ msgstr "Lejátszás"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30517"
|
||||
msgid "Set network credentials for Direct Paths and direct play"
|
||||
msgstr ""
|
||||
"Hálózati hozzáférési adatok megadása a közvetlen útvonalakhoz és közvetlen "
|
||||
"lejátszáshoz"
|
||||
msgid "[COLOR yellow]Enter network credentials[/COLOR]"
|
||||
msgstr "[COLOR yellow]Hálózati hitelesítő adatok megadása[/COLOR]"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30518"
|
||||
|
@ -613,23 +522,6 @@ msgctxt "#30522"
|
|||
msgid "Force transcode h265/HEVC"
|
||||
msgstr "h265/HEVC transzkódolás kényszerítése"
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
msgctxt "#30523"
|
||||
msgid "Also show sync progress for playstate and user data"
|
||||
msgstr ""
|
||||
"Szinkronizálási folyamat állapotának mutatása a lejátszási- és felhasználói "
|
||||
"adatok esetén is"
|
||||
|
||||
# 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"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -694,10 +586,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 +608,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,29 +628,15 @@ 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"
|
||||
msgstr "Hibás felhasználónév vagy jelszó"
|
||||
|
||||
msgctxt "#33010"
|
||||
msgid "User is unauthorized for server {0}"
|
||||
msgstr "A felhasználó nem jogosult a {0} szerverhez való hozzáférésre"
|
||||
|
||||
msgctxt "#33011"
|
||||
msgid "Plex.tv did not provide us a valid list of Plex users, sorry."
|
||||
msgstr "A Plex.tv sajnos nem adta meg a jogosult Plex felhasználók listáját"
|
||||
msgid "Failed to authenticate too many times. Reset in the settings."
|
||||
msgstr ""
|
||||
"Túl sok sikertelen bejelentkezési kísérlet. Állítsa vissza a beállításoknál."
|
||||
|
||||
# Dialog before playback
|
||||
msgctxt "#33013"
|
||||
|
@ -913,14 +774,13 @@ msgstr "Kodi adatbázis javítása (minden tartalom kényszerített frissítése
|
|||
# PKC Settings - Advanced
|
||||
msgctxt "#39019"
|
||||
msgid "Reset the Kodi database and optionally reset PlexKodiConnect"
|
||||
msgstr ""
|
||||
"Kodi adatbázis visszaállítása és a PlexKodiConnect választható "
|
||||
"visszaállítása"
|
||||
msgstr "Kodi adatbázis visszaállítása és a PlexKodiConnect választható visszaállítása"
|
||||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#39020"
|
||||
msgid "Cache all images to Kodi texture cache now"
|
||||
msgstr "Minden kép gyorsítótárazása a Kodi gyorsítótárába most"
|
||||
msgid "[COLOR yellow]Cache all images to Kodi texture cache now[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow]Minden kép gyorsítótárazása a Kodi gyorsítótárába most[/COLOR]"
|
||||
|
||||
# Appended to a listed PMS if it is in the same LAN network as PKC
|
||||
msgctxt "#39022"
|
||||
|
@ -1003,17 +863,6 @@ msgstr ""
|
|||
"Lecseréli a Plex útvonalakat (/volume1/media vagy \\\\myserver\\media) "
|
||||
"egyéni SMB útvonalakra (smb://NAS/mystuff)"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39036"
|
||||
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:"
|
||||
|
@ -1089,8 +938,8 @@ msgstr "Semmi sem működik? Próbálja meg a teljes visszaállítást!"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39050"
|
||||
msgid "Choose Plex Server from a list"
|
||||
msgstr "Válasszon Plex szervert a listából"
|
||||
msgid "[COLOR yellow]Choose Plex Server from a list[/COLOR]"
|
||||
msgstr "[COLOR yellow]Válasszon Plex szervert a listából[/COLOR]"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39051"
|
||||
|
@ -1119,17 +968,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 +1022,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"
|
||||
|
@ -1179,8 +1036,8 @@ msgstr "Az Ön jelenlegi Plex médiaszervere:"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39068"
|
||||
msgid "Manually enter Plex Media Server address"
|
||||
msgstr "Plex médiaszerver címének kézi megadása"
|
||||
msgid "[COLOR yellow]Manually enter Plex Media Server address[/COLOR]"
|
||||
msgstr "[COLOR yellow]Plex médiaszerver címének kézi megadása[/COLOR]"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39069"
|
||||
|
@ -1197,11 +1054,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 +1064,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 +1082,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"
|
||||
|
@ -1246,78 +1099,18 @@ msgstr ""
|
|||
"A Plex Companion nem tudta megnyitni a GDM portot. Kérem változtassa meg a "
|
||||
"PKC beállításokban."
|
||||
|
||||
# Pop-up on initial sync.
|
||||
# Check that next translations for Add-on Paths and Direct Paths are
|
||||
# identical!
|
||||
msgctxt "#39080"
|
||||
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 ""
|
||||
"Bővítmény elérési utakat (alapértelmezett, egyszerű) vagy közvetlen elérési "
|
||||
"utakat kíván használni? Ha nem biztos benne, válassza a bővítmény elérési "
|
||||
"utakat. A PKC nem fog működni, ha a közvetlen elérési utak rosszul vannak "
|
||||
"beállítva!"
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39081"
|
||||
msgid "Add-on Paths"
|
||||
msgstr "Bővítmény elérési utak"
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39082"
|
||||
msgid "Direct Paths"
|
||||
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"
|
||||
|
||||
# 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"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "Kijelentkezés az otthoni Plex felhasználó fiókból: "
|
||||
msgstr "Kijelentkezés az otthoni Plex felhasználó fiókból"
|
||||
|
||||
msgctxt "#39201"
|
||||
msgid "Settings"
|
||||
msgstr "Beállítások"
|
||||
|
||||
msgctxt "#39203"
|
||||
msgid "Refresh Plex playlists/nodes"
|
||||
msgstr "Plex lejátszási listák/csomópontok frissítése"
|
||||
|
||||
msgctxt "#39204"
|
||||
msgid "Perform manual library sync"
|
||||
msgstr "Kézi könyvtárszinkronizáció végrehajtása"
|
||||
|
@ -1347,8 +1140,10 @@ msgstr "A PKC visszaállítása sikertelen. Kérem indítsa újra a Kodi-t."
|
|||
|
||||
# PKC Settings - Plex
|
||||
msgctxt "#39209"
|
||||
msgid "Toggle plex.tv login (sign in or sign out)"
|
||||
msgstr "Plex.tv bejelentkezés átkapcsolása (be- vagy kijelentkezés)"
|
||||
msgid "[COLOR yellow]Toggle plex.tv login (sign in or sign out)[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow]Plex.tv bejelentkezés átkapcsolása (bejelentkezés vagy "
|
||||
"kijelentkezés)[/COLOR]"
|
||||
|
||||
msgctxt "#39210"
|
||||
msgid "Not yet connected to Plex Server"
|
||||
|
@ -1358,21 +1153,22 @@ msgctxt "#39211"
|
|||
msgid "Watch later"
|
||||
msgstr "Később nézendő"
|
||||
|
||||
# Error message pop-up if {0} cannot be contacted. {0} will be replaced by
|
||||
# e.g. the PMS' name
|
||||
# String attached at the end to get something like "PMS Name is offline"
|
||||
msgctxt "#39213"
|
||||
msgid "{0} offline"
|
||||
msgstr "a(z) {0} nem elérhető"
|
||||
msgid "is offline"
|
||||
msgstr "nem elérhető"
|
||||
|
||||
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 ""
|
||||
"Does your Plex Media Server support SSL connections? (https instead of "
|
||||
"http)?"
|
||||
msgstr ""
|
||||
"Kíván HTTPS (SSL) kapcsolatokat használni? Ha nem biztos benne válassza az "
|
||||
"igent."
|
||||
"Támogatja a Plex médiaszervere az SSL csatlakozásokat (https a http "
|
||||
"helyett)?"
|
||||
|
||||
msgctxt "#39218"
|
||||
msgid "Error contacting PMS"
|
||||
|
@ -1392,8 +1188,8 @@ msgid "plex.tv toggle successful"
|
|||
msgstr "plex.tv átkapcsolás sikeres"
|
||||
|
||||
msgctxt "#39222"
|
||||
msgid "Look for missing fanart on FanartTV now"
|
||||
msgstr "Hiányzó művészképek keresése a FanArtTV-n most"
|
||||
msgid "[COLOR yellow]Look for missing fanart on FanartTV now[/COLOR]"
|
||||
msgstr "[COLOR yellow]Hiányzó művészképek keresése a FanArtTV-n most[/COLOR]"
|
||||
|
||||
msgctxt "#39223"
|
||||
msgid ""
|
||||
|
@ -1421,26 +1217,11 @@ msgctxt "#39227"
|
|||
msgid "Logged in to plex.tv"
|
||||
msgstr "Be van jelentkezve a plex.tv-be"
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
# Message in the PKC settings to display the plex.tv username. Leave the colon
|
||||
# :
|
||||
msgctxt "#39228"
|
||||
msgid "Plex admin user"
|
||||
msgstr "Plex adminisztrátor felhasználó"
|
||||
|
||||
# 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 "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ó"
|
||||
|
||||
# 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"
|
||||
msgid "Plex user:"
|
||||
msgstr "Plex felhasználó:"
|
||||
|
||||
msgctxt "#39250"
|
||||
msgid ""
|
||||
|
@ -1513,11 +1294,11 @@ msgstr " lehet nem fog megfelelően működni az adatbázis visszaállításáig
|
|||
|
||||
msgctxt "#39403"
|
||||
msgid ""
|
||||
"The current Kodi version is not supported by PKC. Please consult the Plex "
|
||||
"forum."
|
||||
"Cancelling the database syncing process. Current Kodi version is "
|
||||
"unsupported. Please verify your logs for more info."
|
||||
msgstr ""
|
||||
"A PKC nem támogatja a jelenlegi Kodi verziót. Kérjen tanácsot a Plex "
|
||||
"fórumon."
|
||||
"Az adatbázis szinkronizációs folyamat megszakítása. A jelenlegi Kodi verzió "
|
||||
"nem támogatott. Kérem tekintse meg a hibanaplót további információkért."
|
||||
|
||||
msgctxt "#39405"
|
||||
msgid "Plex playlists/nodes refreshed"
|
||||
|
@ -1527,6 +1308,10 @@ msgctxt "#39406"
|
|||
msgid "Plex playlists/nodes refresh failed"
|
||||
msgstr "a Plex lejátszási listák/csomópontok frissítése sikertelen"
|
||||
|
||||
msgctxt "#39407"
|
||||
msgid "Full library sync finished"
|
||||
msgstr "Teljes könyvtárszinkronizáció befejezve"
|
||||
|
||||
msgctxt "#39408"
|
||||
msgid ""
|
||||
"Sync had to skip some items because they could not be processed. Kodi may be"
|
||||
|
@ -1558,10 +1343,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 +1402,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 +1460,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}!
|
||||
|
|
|
@ -3,15 +3,13 @@
|
|||
# Croneter None <croneter@gmail.com>, 2017
|
||||
# Angela Calò <angycalo@libero.it>, 2018
|
||||
# Cristiano Bozzi <c.bozzi@nextworks.it>, 2018
|
||||
# Luigi Mantellini <luigi.mantellini@gmail.com>, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PlexKodiConnect\n"
|
||||
"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: Luigi Mantellini <luigi.mantellini@gmail.com>, 2019\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Cristiano Bozzi <c.bozzi@nextworks.it>, 2018\n"
|
||||
"Language-Team: Italian (Italy) (https://www.transifex.com/croneter/teams/73837/it_IT/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
@ -47,13 +45,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:"
|
||||
|
@ -104,10 +95,10 @@ msgctxt "#30014"
|
|||
msgid "Connection"
|
||||
msgstr "Connessione"
|
||||
|
||||
# Pop-up notification if user tried to manually initiate fanart download
|
||||
# PKC settings - Artwork
|
||||
msgctxt "#30015"
|
||||
msgid "Fanart download already running"
|
||||
msgstr "Scaricamento Fanart già in esecuzione"
|
||||
msgid "Movie and show FanartTV lookups left to do:"
|
||||
msgstr "Film e show FanartTV da fare:"
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Device Name"
|
||||
|
@ -123,7 +114,7 @@ msgctxt "#30018"
|
|||
msgid "Checking FanartTV for %s items"
|
||||
msgstr "Checking %s FanartTV"
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
# Sync notification displayed when FanartTV lookup is completed
|
||||
msgctxt "#30019"
|
||||
msgid "FanartTV lookup completed"
|
||||
msgstr "Ricerca FanartTV completata"
|
||||
|
@ -166,19 +157,6 @@ msgctxt "#30027"
|
|||
msgid "Prefix in Kodi playlist name to trigger sync"
|
||||
msgstr "Prefisso per sincronizzazione playlist da Kodi"
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
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 +256,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"
|
||||
|
@ -338,46 +312,6 @@ msgctxt "#30198"
|
|||
msgid "Search"
|
||||
msgstr "Cerca"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30200"
|
||||
msgid ""
|
||||
"In the following window, enter the server's hostname (or IP) where your Plex"
|
||||
" media resides. Mind the case!"
|
||||
msgstr ""
|
||||
"Nella finestra seguente, inserisci l'hostname del server (oppure l'IP) dove "
|
||||
"Plex Media Server risiede. Attenzione alle maiuscole!"
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# for hostname
|
||||
msgctxt "#30201"
|
||||
msgid "Enter server hostname (or IP)"
|
||||
msgstr "Inserisci l'hostname del server (oppure l'IP)"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30202"
|
||||
msgid ""
|
||||
"In the following window, enter the network protocol you would like to use. "
|
||||
"This is likely 'smb'."
|
||||
msgstr ""
|
||||
"Nella finestra seguente, inserisci il protocollo di rete che vorresti usare."
|
||||
" Questo è tipicamente \"smb\"."
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# protocol
|
||||
msgctxt "#30203"
|
||||
msgid "Enter network protocol"
|
||||
msgstr "Inserisci il protocollo di rete"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30204"
|
||||
msgid "The hostname or IP '{0}' that you entered is not valid."
|
||||
msgstr "L'hostname o IP '{0}' inserito non è valido."
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30205"
|
||||
msgid "The protocol '{0}' that you entered is not supported."
|
||||
msgstr "Il protocollo '{0}\" inserito non è supportato."
|
||||
|
||||
# Video node naming for random e.g. movies
|
||||
msgctxt "#30227"
|
||||
msgid "Random"
|
||||
|
@ -491,14 +425,6 @@ msgctxt "#30416"
|
|||
msgid "Settings for the Plex Server"
|
||||
msgstr "Impostazioni del server Plex"
|
||||
|
||||
msgctxt "#30417"
|
||||
msgid ""
|
||||
"Could not change the Kodi settings file {0}. PKC might not work correctly. "
|
||||
"Error: {1}"
|
||||
msgstr ""
|
||||
"Impossibile cambiare il file di configurazione di Kodi {0}. PKC potrebbe non"
|
||||
" funzionare correttamente. Errore: {1}"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30500"
|
||||
msgid "Verify Host SSL Certificate (more secure)"
|
||||
|
@ -509,16 +435,10 @@ msgctxt "#30501"
|
|||
msgid "Client SSL certificate"
|
||||
msgstr "Certificato SSL del client"
|
||||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#30502"
|
||||
msgid "Sync Plex artwork from the PMS (recommended)"
|
||||
msgstr "Sincronizza le locandine Plex da PMS (raccomandato)"
|
||||
|
||||
# Message shown if SSL HTTPS certificate fails
|
||||
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 - Connection
|
||||
msgctxt "#30505"
|
||||
msgid "[COLOR yellow]Reset login attempts[/COLOR]"
|
||||
msgstr "[COLOR yellow]Resetta i tentativi di login[/COLOR]"
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#30506"
|
||||
|
@ -563,7 +483,6 @@ msgstr "Limita numero thread di caching delle artwork (raccomandato per rpi)"
|
|||
msgctxt "#30514"
|
||||
msgid "Show all Plex extras instead of immediately playing trailers"
|
||||
msgstr ""
|
||||
"Mostra tutti i contenuti extra di Plex invece di avviare subito i trailers"
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
msgctxt "#30515"
|
||||
|
@ -577,10 +496,8 @@ msgstr "Riproduzione"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30517"
|
||||
msgid "Set network credentials for Direct Paths and direct play"
|
||||
msgstr ""
|
||||
"Inserisci le credenziali per accesso diretto ai percorsi e per la lettura "
|
||||
"diretta"
|
||||
msgid "[COLOR yellow]Enter network credentials[/COLOR]"
|
||||
msgstr "[COLOR yellow]Inserisci credenziali di rete[/COLOR]"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30518"
|
||||
|
@ -608,21 +525,6 @@ msgctxt "#30522"
|
|||
msgid "Force transcode h265/HEVC"
|
||||
msgstr "Forza transcodifica h265/HEVC"
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
msgctxt "#30523"
|
||||
msgid "Also show sync progress for playstate and user data"
|
||||
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
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -688,8 +590,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 +612,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,29 +632,16 @@ 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"
|
||||
msgstr "Username o password non validi"
|
||||
|
||||
msgctxt "#33010"
|
||||
msgid "User is unauthorized for server {0}"
|
||||
msgstr "L'utente non è autorizzato per il server {0}"
|
||||
|
||||
msgctxt "#33011"
|
||||
msgid "Plex.tv did not provide us a valid list of Plex users, sorry."
|
||||
msgstr "Spiacente ma Plex.tv non fornisce alcuna lista di utenti Plex validi."
|
||||
msgid "Failed to authenticate too many times. Reset in the settings."
|
||||
msgstr ""
|
||||
"Autenticazione fallita per troppe volte. Effettua un reset nelle "
|
||||
"impostazioni."
|
||||
|
||||
# Dialog before playback
|
||||
msgctxt "#33013"
|
||||
|
@ -909,8 +783,10 @@ msgstr "Reset database di Kodi e opzionalmente reset PlexKodiConnect"
|
|||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#39020"
|
||||
msgid "Cache all images to Kodi texture cache now"
|
||||
msgstr "Cache tutte le immagini nella cache delle texture di Kodi adesso"
|
||||
msgid "[COLOR yellow]Cache all images to Kodi texture cache now[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yelow]Sposta tutte le immagini nella cache delle texture di "
|
||||
"Kodi[/COLOR]"
|
||||
|
||||
# Appended to a listed PMS if it is in the same LAN network as PKC
|
||||
msgctxt "#39022"
|
||||
|
@ -995,18 +871,6 @@ msgstr ""
|
|||
"Sostituisci i Plex path /volume1/media o \\\\myserver\\media con path SMB "
|
||||
"custom smb://NAS/mystuff"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39036"
|
||||
msgid "Escape special characters in path (e.g. space to %20)"
|
||||
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:"
|
||||
|
@ -1083,8 +947,8 @@ msgstr "Non funziona nulla? Prova un reset completo!"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39050"
|
||||
msgid "Choose Plex Server from a list"
|
||||
msgstr "Scegli il Server Plex da una lista"
|
||||
msgid "[COLOR yellow]Choose Plex Server from a list[/COLOR]"
|
||||
msgstr "[COLOR yellow]Scegli un server Plex[/COLOR]"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39051"
|
||||
|
@ -1114,15 +978,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 +1034,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"
|
||||
|
@ -1174,8 +1048,10 @@ msgstr "Il tuo Plex Media Server corrente:"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39068"
|
||||
msgid "Manually enter Plex Media Server address"
|
||||
msgstr "Inserisci manualmente l'indirizzo del Plex Media Server"
|
||||
msgid "[COLOR yellow]Manually enter Plex Media Server address[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow]Inserisci manualmente l'indirizzo del Plex Media "
|
||||
"Server[/COLOR]"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39069"
|
||||
|
@ -1192,11 +1068,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 +1078,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 +1094,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"
|
||||
|
@ -1240,70 +1111,6 @@ msgstr ""
|
|||
"Plex Companion non può aprire la porta GDM. Si prega di modificarla nelle "
|
||||
"impostazioni di PKC."
|
||||
|
||||
# Pop-up on initial sync.
|
||||
# Check that next translations for Add-on Paths and Direct Paths are
|
||||
# identical!
|
||||
msgctxt "#39080"
|
||||
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 ""
|
||||
"Usa i Percorsi Add-on (default) oppure i Percorsi Diretti? Scegli i Percorsi"
|
||||
" Add-on se sei non sicuro. PKC non funzionerà se la configurazione dei "
|
||||
"Percorsi Diretti è errata."
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39081"
|
||||
msgid "Add-on Paths"
|
||||
msgstr "Percorsi Add-on"
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39082"
|
||||
msgid "Direct Paths"
|
||||
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"
|
||||
|
||||
# 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"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "Logout utente Plex "
|
||||
|
@ -1312,6 +1119,10 @@ msgctxt "#39201"
|
|||
msgid "Settings"
|
||||
msgstr "Impostazioni"
|
||||
|
||||
msgctxt "#39203"
|
||||
msgid "Refresh Plex playlists/nodes"
|
||||
msgstr "Aggiorna playlist/nodi Plex"
|
||||
|
||||
msgctxt "#39204"
|
||||
msgid "Perform manual library sync"
|
||||
msgstr "Eseguire sincronizzazione manuale della libreria"
|
||||
|
@ -1341,10 +1152,8 @@ msgstr "Impossibile resettare PKC. Prova a riavviare Kodi."
|
|||
|
||||
# PKC Settings - Plex
|
||||
msgctxt "#39209"
|
||||
msgid "Toggle plex.tv login (sign in or sign out)"
|
||||
msgstr ""
|
||||
"Inverti lo stato di autenticazione si plex.tv (autenticato oppure non "
|
||||
"autenticato)"
|
||||
msgid "[COLOR yellow]Toggle plex.tv login (sign in or sign out)[/COLOR]"
|
||||
msgstr "[COLOR yellow]Login/Logout plex.tv[/COLOR]"
|
||||
|
||||
msgctxt "#39210"
|
||||
msgid "Not yet connected to Plex Server"
|
||||
|
@ -1354,19 +1163,21 @@ msgctxt "#39211"
|
|||
msgid "Watch later"
|
||||
msgstr "Guarda più tardi"
|
||||
|
||||
# Error message pop-up if {0} cannot be contacted. {0} will be replaced by
|
||||
# e.g. the PMS' name
|
||||
# String attached at the end to get something like "PMS Name is offline"
|
||||
msgctxt "#39213"
|
||||
msgid "{0} offline"
|
||||
msgstr "{0} offline"
|
||||
msgid "is offline"
|
||||
msgstr "non è attivo"
|
||||
|
||||
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 ""
|
||||
"Does your Plex Media Server support SSL connections? (https instead of "
|
||||
"http)?"
|
||||
msgstr ""
|
||||
"Il tuo Plex Media Server supporta le connessioni SSL (https invece di http)?"
|
||||
|
||||
msgctxt "#39218"
|
||||
msgid "Error contacting PMS"
|
||||
|
@ -1386,8 +1197,8 @@ msgid "plex.tv toggle successful"
|
|||
msgstr "Login/Logout plex.tv eseguito"
|
||||
|
||||
msgctxt "#39222"
|
||||
msgid "Look for missing fanart on FanartTV now"
|
||||
msgstr "Cerca ora per le fanart mancanti su FanartTV"
|
||||
msgid "[COLOR yellow]Look for missing fanart on FanartTV now[/COLOR]"
|
||||
msgstr "[COLOR yellow]Cercare ora fanart mancanti su FanartTV[/COLOR]"
|
||||
|
||||
msgctxt "#39223"
|
||||
msgid ""
|
||||
|
@ -1415,26 +1226,11 @@ msgctxt "#39227"
|
|||
msgid "Logged in to plex.tv"
|
||||
msgstr "Autenticazione concessa su plex.tv"
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
# Message in the PKC settings to display the plex.tv username. Leave the colon
|
||||
# :
|
||||
msgctxt "#39228"
|
||||
msgid "Plex admin user"
|
||||
msgstr "Utente di amministrazione Plex"
|
||||
|
||||
# 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 "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"
|
||||
|
||||
# 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"
|
||||
msgid "Plex user:"
|
||||
msgstr "Utente Plex:"
|
||||
|
||||
msgctxt "#39250"
|
||||
msgid ""
|
||||
|
@ -1508,11 +1304,12 @@ msgstr ""
|
|||
|
||||
msgctxt "#39403"
|
||||
msgid ""
|
||||
"The current Kodi version is not supported by PKC. Please consult the Plex "
|
||||
"forum."
|
||||
"Cancelling the database syncing process. Current Kodi version is "
|
||||
"unsupported. Please verify your logs for more info."
|
||||
msgstr ""
|
||||
"La versione corrente di Kodi non è supportata da PKC. Per favore consulta il"
|
||||
" forum Plex."
|
||||
"Annullamento della sincronizzazione del database. La versione corrente di "
|
||||
"Kodi non è supportata. Per maggiori informazioni si prega di verificare i "
|
||||
"log."
|
||||
|
||||
msgctxt "#39405"
|
||||
msgid "Plex playlists/nodes refreshed"
|
||||
|
@ -1522,6 +1319,10 @@ msgctxt "#39406"
|
|||
msgid "Plex playlists/nodes refresh failed"
|
||||
msgstr "Aggiornamento playlist/nodi Plex fallito"
|
||||
|
||||
msgctxt "#39407"
|
||||
msgid "Full library sync finished"
|
||||
msgstr "Sincronizzazione completa libreria terminata"
|
||||
|
||||
msgctxt "#39408"
|
||||
msgid ""
|
||||
"Sync had to skip some items because they could not be processed. Kodi may be"
|
||||
|
@ -1554,10 +1355,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 +1415,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 +1473,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
File diff suppressed because it is too large
Load diff
|
@ -1,18 +1,14 @@
|
|||
# XBMC Media Center language file
|
||||
# Translators:
|
||||
# Croneter None <croneter@gmail.com>, 2017
|
||||
# Michiel van Baak <michiel@vanbaak.info>, 2019
|
||||
# Panja0 <panja0@protonmail.com>, 2019
|
||||
# Nick Corthals <corthals.nick@gmail.com>, 2019
|
||||
# Rick van Soest <r.vansoest@gmail.com>, 2019
|
||||
#
|
||||
# Michiel van Baak <michiel@vanbaak.info>, 2017
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PlexKodiConnect\n"
|
||||
"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"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Michiel van Baak <michiel@vanbaak.info>, 2017\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"
|
||||
|
@ -27,7 +23,7 @@ msgstr "PlexKodiConnect"
|
|||
|
||||
msgctxt "#30000"
|
||||
msgid "Server Address (IP)"
|
||||
msgstr "Server adres (IP)"
|
||||
msgstr "Serveradres (IP)"
|
||||
|
||||
msgctxt "#30001"
|
||||
msgid "Searching for PMS"
|
||||
|
@ -45,15 +41,8 @@ msgid ""
|
|||
"Warning: Kodi setting \"Play next video automatically\" is enabled. This "
|
||||
"could break PKC. Deactivate?"
|
||||
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 ""
|
||||
"Waarschuwing: De kodi setting 'Automatisch volgende video afspelen' is "
|
||||
"actief. Dit kan voor problemen zorgen. Setting deactiveren?"
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username: "
|
||||
|
@ -62,32 +51,32 @@ msgstr "Gebruikersnaam: "
|
|||
# Sync notification displayed if there is still artwork to be cached to Kodi
|
||||
msgctxt "#30006"
|
||||
msgid "Caching %s Plex images"
|
||||
msgstr "Caching %s Plex afbeeldingen"
|
||||
msgstr ""
|
||||
|
||||
# Sync notification displayed if syncing of major artwork is done
|
||||
msgctxt "#30007"
|
||||
msgid "Plex image caching done"
|
||||
msgstr "Cachen van Plex afbeeldingen afgerond"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings artwork: Enable notifications for artwork image sync
|
||||
msgctxt "#30008"
|
||||
msgid "Enable notifications for image caching"
|
||||
msgstr "Activeer notificaties voor afbeelding caching"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings artwork: Enable image caching during Kodi playback
|
||||
msgctxt "#30009"
|
||||
msgid "Enable image caching during Kodi playback (restart Kodi!)"
|
||||
msgstr "Afbeelding caching inschakelen tijdens Kodi weergave (herstart Kodi!)"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings - Artwork
|
||||
msgctxt "#30010"
|
||||
msgid "Approximate progress"
|
||||
msgstr "Geschatte voortgang"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings - Artwork
|
||||
msgctxt "#30011"
|
||||
msgid "Artwork left to cache:"
|
||||
msgstr "Artwork nog te cachen:"
|
||||
msgstr ""
|
||||
|
||||
# Button text
|
||||
msgctxt "#30012"
|
||||
|
@ -103,10 +92,10 @@ msgctxt "#30014"
|
|||
msgid "Connection"
|
||||
msgstr "Verbinding"
|
||||
|
||||
# Pop-up notification if user tried to manually initiate fanart download
|
||||
# PKC settings - Artwork
|
||||
msgctxt "#30015"
|
||||
msgid "Fanart download already running"
|
||||
msgstr "Het downloaden van fanart is al bezig"
|
||||
msgid "Movie and show FanartTV lookups left to do:"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Device Name"
|
||||
|
@ -120,22 +109,22 @@ msgstr "Onbevoegd voor PMS"
|
|||
# Sync notification displayed for the number of fanart.tv lookups left
|
||||
msgctxt "#30018"
|
||||
msgid "Checking FanartTV for %s items"
|
||||
msgstr "FanartTV controleren op %s items"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
# Sync notification displayed when FanartTV lookup is completed
|
||||
msgctxt "#30019"
|
||||
msgid "FanartTV lookup completed"
|
||||
msgstr "FanartTV zoek actie voltooid"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings sync options
|
||||
msgctxt "#30020"
|
||||
msgid "Sync Plex playlists (reboot Kodi!)"
|
||||
msgstr "Synchroniseer Plex afspeellijsten (herstart Kodi!)"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings sync options
|
||||
msgctxt "#30021"
|
||||
msgid "Only sync specific Plex playlists to Kodi"
|
||||
msgstr "Synchroniseer alleen specifieke Plex afspeellijsten met Kodi"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings category
|
||||
msgctxt "#30022"
|
||||
|
@ -145,7 +134,7 @@ msgstr "Geavanceerd"
|
|||
# PKC settings sync options
|
||||
msgctxt "#30023"
|
||||
msgid "Only sync specific Kodi playlists to Plex"
|
||||
msgstr "Synchroniseer alleen specifieke Kodi afspeellijsten met Plex"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30024"
|
||||
msgid "Username"
|
||||
|
@ -158,24 +147,11 @@ msgstr "Bericht weergeven als PMS offline gaat"
|
|||
# PKC settings sync options
|
||||
msgctxt "#30026"
|
||||
msgid "Prefix in Plex playlist name to trigger sync"
|
||||
msgstr "Voorvoegsel in Plex afspeellijst naam om synchronisatie te activeren"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings sync options
|
||||
msgctxt "#30027"
|
||||
msgid "Prefix in Kodi playlist name to trigger sync"
|
||||
msgstr "Voorvoegsel in Kodi afspeellijst naam om synchronisatie te activeren"
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
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"
|
||||
|
@ -277,10 +253,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"
|
||||
|
@ -337,46 +309,6 @@ msgctxt "#30198"
|
|||
msgid "Search"
|
||||
msgstr "Zoek"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30200"
|
||||
msgid ""
|
||||
"In the following window, enter the server's hostname (or IP) where your Plex"
|
||||
" media resides. Mind the case!"
|
||||
msgstr ""
|
||||
"Voer in het volgende venster de hostnaam (of IP) van de server in waar uw "
|
||||
"Plex media zich bevindt. Let op: hoofdlettergevoelig!"
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# for hostname
|
||||
msgctxt "#30201"
|
||||
msgid "Enter server hostname (or IP)"
|
||||
msgstr "Voer de hostnaam van de server in (of IP)"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30202"
|
||||
msgid ""
|
||||
"In the following window, enter the network protocol you would like to use. "
|
||||
"This is likely 'smb'."
|
||||
msgstr ""
|
||||
"Voer in het volgende venster het netwerkprotocol in dat u wilt gebruiken. "
|
||||
"Dit is waarschijnlijk 'smb'."
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# protocol
|
||||
msgctxt "#30203"
|
||||
msgid "Enter network protocol"
|
||||
msgstr "Voer het netwerkprotocol in"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30204"
|
||||
msgid "The hostname or IP '{0}' that you entered is not valid."
|
||||
msgstr "De hostnaam of IP '{0}' die u heeft ingevoerd is niet geldig."
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30205"
|
||||
msgid "The protocol '{0}' that you entered is not supported."
|
||||
msgstr "Het protocol '{0}' dat u hebt ingevoerd wordt niet ondersteund."
|
||||
|
||||
# Video node naming for random e.g. movies
|
||||
msgctxt "#30227"
|
||||
msgid "Random"
|
||||
|
@ -490,14 +422,6 @@ msgctxt "#30416"
|
|||
msgid "Settings for the Plex Server"
|
||||
msgstr "Instellingen voor de Plex Server"
|
||||
|
||||
msgctxt "#30417"
|
||||
msgid ""
|
||||
"Could not change the Kodi settings file {0}. PKC might not work correctly. "
|
||||
"Error: {1}"
|
||||
msgstr ""
|
||||
"Kon het Kodi instellingen bestand niet wijzigen {0}. PKC werkt mogelijk niet"
|
||||
" correct. Fout: {1}"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30500"
|
||||
msgid "Verify Host SSL Certificate (more secure)"
|
||||
|
@ -508,16 +432,10 @@ msgctxt "#30501"
|
|||
msgid "Client SSL certificate"
|
||||
msgstr "SSL-clientcertificaat"
|
||||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#30502"
|
||||
msgid "Sync Plex artwork from the PMS (recommended)"
|
||||
msgstr "Synchroniseer Plex artwork van de PMS (aanbevolen)"
|
||||
|
||||
# Message shown if SSL HTTPS certificate fails
|
||||
msgctxt "#30503"
|
||||
msgid "SSL certificate failed to validate. Please check {0} for solutions."
|
||||
msgstr ""
|
||||
"Validatie SSL certificaat niet gelukt. Controleer {0} voor oplossingen."
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30505"
|
||||
msgid "[COLOR yellow]Reset login attempts[/COLOR]"
|
||||
msgstr "[COLOR yellow]Reset inlog pogingen[/COLOR]"
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#30506"
|
||||
|
@ -551,7 +469,7 @@ msgstr "Afspeelmodus"
|
|||
# PKC Settings - Artwork
|
||||
msgctxt "#30512"
|
||||
msgid "Cache all artwork for a smooth Kodi experience"
|
||||
msgstr "Sla alle artwork op in de cache zodat Kodi sneller reageert"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#30513"
|
||||
|
@ -561,7 +479,7 @@ msgstr "Beperk artwork cache threads (aanbevolen voor rpi)"
|
|||
# PKC Settings - Sync Options
|
||||
msgctxt "#30514"
|
||||
msgid "Show all Plex extras instead of immediately playing trailers"
|
||||
msgstr "Toon alle Plex extra's in plaats van meteen trailers af te spelen"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
msgctxt "#30515"
|
||||
|
@ -575,8 +493,8 @@ msgstr "Afspelen"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30517"
|
||||
msgid "Set network credentials for Direct Paths and direct play"
|
||||
msgstr "Stel netwerk inloggegevens in voor directe paden en directe weergave"
|
||||
msgid "[COLOR yellow]Enter network credentials[/COLOR]"
|
||||
msgstr "[COLOR yellow]Geef netwerk authenticatie gegevens[/COLOR]"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30518"
|
||||
|
@ -604,23 +522,6 @@ msgctxt "#30522"
|
|||
msgid "Force transcode h265/HEVC"
|
||||
msgstr "Forceer transcoden h265/HEVC"
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
msgctxt "#30523"
|
||||
msgid "Also show sync progress for playstate and user data"
|
||||
msgstr ""
|
||||
"Toon ook de voortgang van de synchronisatie voor weergave status en "
|
||||
"gebruikersgegevens"
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
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"
|
||||
|
@ -654,7 +555,7 @@ msgstr "Serverberichten"
|
|||
# PKC Settings - Advanced
|
||||
msgctxt "#30535"
|
||||
msgid "Generate a new unique Plex device Id (e.g. to clone Kodi)"
|
||||
msgstr "Genereer een nieuwe unieke Plex apparaat-id (b.v. om Kodi te klonen)"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30536"
|
||||
|
@ -670,8 +571,6 @@ msgstr "BIJ AANPASSINGEN KODI HERSTARTEN"
|
|||
msgctxt "#30538"
|
||||
msgid "Manual complete reset of Kodi database necessary, see \"Advanced\""
|
||||
msgstr ""
|
||||
"Handmatige volledige reset van de Kodi database noodzakelijk, zie "
|
||||
"\"Geavanceerd\""
|
||||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#30539"
|
||||
|
@ -685,8 +584,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"
|
||||
|
@ -696,7 +595,7 @@ msgstr "Kies altijd beste kwaliteit voor trailers"
|
|||
# PKC Settings - Artwork
|
||||
msgctxt "#30543"
|
||||
msgid "Prefer Kodi artwork for collections/sets"
|
||||
msgstr "Prefereer Kodi artwork voor verzamelingen/sets"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30544"
|
||||
msgid "Artwork"
|
||||
|
@ -707,21 +606,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,30 +626,14 @@ 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"
|
||||
msgstr "Ongeldige gebruikersnaam of wachtwoord"
|
||||
|
||||
msgctxt "#33010"
|
||||
msgid "User is unauthorized for server {0}"
|
||||
msgstr "Gebruiker is niet geautoriseerd voor server {0}"
|
||||
|
||||
msgctxt "#33011"
|
||||
msgid "Plex.tv did not provide us a valid list of Plex users, sorry."
|
||||
msgstr ""
|
||||
"Plex.tv heeft ons geen geldige lijst van Plex gebruikers verstrekt, sorry."
|
||||
msgid "Failed to authenticate too many times. Reset in the settings."
|
||||
msgstr "Te vaak niet kunnen inloggen. Pas instellingen aan."
|
||||
|
||||
# Dialog before playback
|
||||
msgctxt "#33013"
|
||||
|
@ -821,7 +689,7 @@ msgstr "Ondertitel grootte"
|
|||
# PKC Settings - Sync
|
||||
msgctxt "#39003"
|
||||
msgid "Number of simultaneous download threads"
|
||||
msgstr "Aantal gelijktijdige download threads"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Plex
|
||||
msgctxt "#39004"
|
||||
|
@ -894,17 +762,18 @@ msgstr "PKC plugin instellingen aanpassen? Kodi moet hierna herstart worden!"
|
|||
# PKC Settings - Advanced
|
||||
msgctxt "#39018"
|
||||
msgid "Repair the Kodi database (force update all content)"
|
||||
msgstr "Herstel de Kodi database (forceer update alle inhoud)"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Advanced
|
||||
msgctxt "#39019"
|
||||
msgid "Reset the Kodi database and optionally reset PlexKodiConnect"
|
||||
msgstr "Reset de Kodi database en reset optioneel PlexKodiConnect"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#39020"
|
||||
msgid "Cache all images to Kodi texture cache now"
|
||||
msgstr "Cache nu alle plaatjes in Kodi texture cache"
|
||||
msgid "[COLOR yellow]Cache all images to Kodi texture cache now[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow]Alle afbeeldingen van Kodi nu aan de cache toevoegen[/COLOR]"
|
||||
|
||||
# Appended to a listed PMS if it is in the same LAN network as PKC
|
||||
msgctxt "#39022"
|
||||
|
@ -987,16 +856,6 @@ msgstr ""
|
|||
"Vervang plex paden /volume1/media of \\\\myserver\\media met aangepaste SMB "
|
||||
"paden smb://NAS/mystuff"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
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:"
|
||||
|
@ -1073,8 +932,8 @@ msgstr "Niets werkt? Probeer een volledige reset!"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39050"
|
||||
msgid "Choose Plex Server from a list"
|
||||
msgstr "Kies Plex server van een lijst"
|
||||
msgid "[COLOR yellow]Choose Plex Server from a list[/COLOR]"
|
||||
msgstr "[COLOR yellow]Kies Plex Server uit lijst[/COLOR]"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39051"
|
||||
|
@ -1103,15 +962,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 +1014,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"
|
||||
|
@ -1161,8 +1028,8 @@ msgstr "Huidige Plex Media Server:"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39068"
|
||||
msgid "Manually enter Plex Media Server address"
|
||||
msgstr "Voer handmatig het Plex Media Server adres in"
|
||||
msgid "[COLOR yellow]Manually enter Plex Media Server address[/COLOR]"
|
||||
msgstr "[COLOR yellow]Handmatig Plex Media Server adres invoeren[/COLOR]"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39069"
|
||||
|
@ -1179,11 +1046,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 +1056,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 +1073,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"
|
||||
|
@ -1228,70 +1090,6 @@ msgstr ""
|
|||
"Plex Companion kon de GDM poort niet gebruiken. Wijzig deze in de PKC "
|
||||
"instellingen."
|
||||
|
||||
# Pop-up on initial sync.
|
||||
# Check that next translations for Add-on Paths and Direct Paths are
|
||||
# identical!
|
||||
msgctxt "#39080"
|
||||
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 ""
|
||||
"Gebruik add-on paden (standaard, eenvoudig) of directe paden? Kies add-on "
|
||||
"paden als u niet zeker bent. PKC werkt niet als je instellingen voor directe"
|
||||
" paden verkeerd zijn!"
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39081"
|
||||
msgid "Add-on Paths"
|
||||
msgstr "Add-on paden"
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39082"
|
||||
msgid "Direct Paths"
|
||||
msgstr "Directe paden"
|
||||
|
||||
# Dialog for manually entering PMS
|
||||
msgctxt "#39083"
|
||||
msgid "Enter PMS IP or URL"
|
||||
msgstr "Kies PMS IP of URL"
|
||||
|
||||
# Dialog for manually entering PMS
|
||||
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 "
|
||||
|
@ -1300,6 +1098,10 @@ msgctxt "#39201"
|
|||
msgid "Settings"
|
||||
msgstr "Instellingen"
|
||||
|
||||
msgctxt "#39203"
|
||||
msgid "Refresh Plex playlists/nodes"
|
||||
msgstr "Vernieuwen van Plex afspeellijsten/nodes"
|
||||
|
||||
msgctxt "#39204"
|
||||
msgid "Perform manual library sync"
|
||||
msgstr "Handmatige sync uitvoeren"
|
||||
|
@ -1327,8 +1129,8 @@ msgstr "PKC reset niet gelukt. Herstart Kodi."
|
|||
|
||||
# PKC Settings - Plex
|
||||
msgctxt "#39209"
|
||||
msgid "Toggle plex.tv login (sign in or sign out)"
|
||||
msgstr "Pas plex.tv login aan (aanmelden of afmelden)"
|
||||
msgid "[COLOR yellow]Toggle plex.tv login (sign in or sign out)[/COLOR]"
|
||||
msgstr "[COLOR yellow]Verander plex.tv login (aanmelden of afmelden)[/COLOR]"
|
||||
|
||||
msgctxt "#39210"
|
||||
msgid "Not yet connected to Plex Server"
|
||||
|
@ -1338,19 +1140,22 @@ msgctxt "#39211"
|
|||
msgid "Watch later"
|
||||
msgstr "Later bekijken"
|
||||
|
||||
# Error message pop-up if {0} cannot be contacted. {0} will be replaced by
|
||||
# e.g. the PMS' name
|
||||
# String attached at the end to get something like "PMS Name is offline"
|
||||
msgctxt "#39213"
|
||||
msgid "{0} offline"
|
||||
msgstr "{0} offline"
|
||||
msgid "is offline"
|
||||
msgstr "is offline"
|
||||
|
||||
msgctxt "#39215"
|
||||
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 ""
|
||||
"Does your Plex Media Server support SSL connections? (https instead of "
|
||||
"http)?"
|
||||
msgstr ""
|
||||
"Ondersteunt uw Plex Media Server SSL-verbindingen? (https in plaats van "
|
||||
"http)?"
|
||||
|
||||
msgctxt "#39218"
|
||||
msgid "Error contacting PMS"
|
||||
|
@ -1370,8 +1175,8 @@ msgid "plex.tv toggle successful"
|
|||
msgstr "plex.tv aanpassing succesvol"
|
||||
|
||||
msgctxt "#39222"
|
||||
msgid "Look for missing fanart on FanartTV now"
|
||||
msgstr "Zoek nu naar missende fanart op FanartTV"
|
||||
msgid "[COLOR yellow]Look for missing fanart on FanartTV now[/COLOR]"
|
||||
msgstr "[COLOR yellow]Nu zoeken naar missende artwork op FanartTV[/COLOR]"
|
||||
|
||||
msgctxt "#39223"
|
||||
msgid ""
|
||||
|
@ -1392,33 +1197,18 @@ msgstr "Alleen ontbrekende"
|
|||
# Message in the PKC settings if user has not logged in to plex.tv
|
||||
msgctxt "#39226"
|
||||
msgid "Not logged in to plex.tv"
|
||||
msgstr "Niet ingelogd op plex.tv"
|
||||
msgstr "Niet ingelogd op plek.tv"
|
||||
|
||||
# Message in the PKC settings if user is logged in to plex.tv
|
||||
msgctxt "#39227"
|
||||
msgid "Logged in to plex.tv"
|
||||
msgstr "Ingelogd op plex.tv"
|
||||
msgstr "Ingelogd op plek.tv"
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
# Message in the PKC settings to display the plex.tv username. Leave the colon
|
||||
# :
|
||||
msgctxt "#39228"
|
||||
msgid "Plex admin user"
|
||||
msgstr "Plex admin gebruiker"
|
||||
|
||||
# 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 "Aanmelden met plex.tv mislukt voor gebruiker"
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
msgctxt "#39230"
|
||||
msgid "Logged in Plex home user"
|
||||
msgstr "Ingelogde Plex gebruiker"
|
||||
|
||||
# Message in the PKC settings to change the logged in Plex home user
|
||||
msgctxt "#39231"
|
||||
msgid "Change logged in Plex home user"
|
||||
msgstr "Wijzig ingelogde Plex gebruiker"
|
||||
msgid "Plex user:"
|
||||
msgstr "Plex gebruiker:"
|
||||
|
||||
msgctxt "#39250"
|
||||
msgid ""
|
||||
|
@ -1488,11 +1278,11 @@ msgstr ""
|
|||
|
||||
msgctxt "#39403"
|
||||
msgid ""
|
||||
"The current Kodi version is not supported by PKC. Please consult the Plex "
|
||||
"forum."
|
||||
"Cancelling the database syncing process. Current Kodi version is "
|
||||
"unsupported. Please verify your logs for more info."
|
||||
msgstr ""
|
||||
"De huidige Kodi versie wordt niet ondersteund door PKC. Raadpleeg het Plex "
|
||||
"forum."
|
||||
"Database synchroniseer proces geannuleerd. Huidige Kodi versie wordt niet "
|
||||
"ondersteund. Controleer uw logbestanden voor meer info."
|
||||
|
||||
msgctxt "#39405"
|
||||
msgid "Plex playlists/nodes refreshed"
|
||||
|
@ -1502,6 +1292,10 @@ msgctxt "#39406"
|
|||
msgid "Plex playlists/nodes refresh failed"
|
||||
msgstr "Plex afspeellijsten/nodes vernieuwen mislukt"
|
||||
|
||||
msgctxt "#39407"
|
||||
msgid "Full library sync finished"
|
||||
msgstr "Volledige bibliotheek synchronisatie voltooid"
|
||||
|
||||
msgctxt "#39408"
|
||||
msgid ""
|
||||
"Sync had to skip some items because they could not be processed. Kodi may be"
|
||||
|
@ -1531,10 +1325,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 +1384,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 +1442,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}!
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,15 +1,13 @@
|
|||
# XBMC Media Center language file
|
||||
# Translators:
|
||||
# Croneter None <croneter@gmail.com>, 2017
|
||||
# Daniel Leite <danieldefreitasleite@gmail.com>, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PlexKodiConnect\n"
|
||||
"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"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\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 +26,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 +40,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 +48,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"
|
||||
|
@ -102,10 +89,10 @@ msgctxt "#30014"
|
|||
msgid "Connection"
|
||||
msgstr "Ligação"
|
||||
|
||||
# Pop-up notification if user tried to manually initiate fanart download
|
||||
# PKC settings - Artwork
|
||||
msgctxt "#30015"
|
||||
msgid "Fanart download already running"
|
||||
msgstr "Download de Fanart já em andamento"
|
||||
msgid "Movie and show FanartTV lookups left to do:"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Device Name"
|
||||
|
@ -119,22 +106,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
|
||||
# Sync notification displayed when FanartTV lookup is completed
|
||||
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 +131,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 +144,11 @@ 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"
|
||||
|
||||
# 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 +162,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 +250,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"
|
||||
|
@ -336,42 +306,6 @@ msgctxt "#30198"
|
|||
msgid "Search"
|
||||
msgstr "Procurar"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30200"
|
||||
msgid ""
|
||||
"In the following window, enter the server's hostname (or IP) where your Plex"
|
||||
" media resides. Mind the case!"
|
||||
msgstr ""
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# for hostname
|
||||
msgctxt "#30201"
|
||||
msgid "Enter server hostname (or IP)"
|
||||
msgstr ""
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30202"
|
||||
msgid ""
|
||||
"In the following window, enter the network protocol you would like to use. "
|
||||
"This is likely 'smb'."
|
||||
msgstr ""
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# protocol
|
||||
msgctxt "#30203"
|
||||
msgid "Enter network protocol"
|
||||
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 ""
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30205"
|
||||
msgid "The protocol '{0}' that you entered is not supported."
|
||||
msgstr ""
|
||||
|
||||
# Video node naming for random e.g. movies
|
||||
msgctxt "#30227"
|
||||
msgid "Random"
|
||||
|
@ -485,12 +419,6 @@ msgctxt "#30416"
|
|||
msgid "Settings for the Plex Server"
|
||||
msgstr "Configurações do Servidor Plex"
|
||||
|
||||
msgctxt "#30417"
|
||||
msgid ""
|
||||
"Could not change the Kodi settings file {0}. PKC might not work correctly. "
|
||||
"Error: {1}"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30500"
|
||||
msgid "Verify Host SSL Certificate (more secure)"
|
||||
|
@ -501,15 +429,10 @@ msgctxt "#30501"
|
|||
msgid "Client SSL certificate"
|
||||
msgstr "Certificado SSL do cliente"
|
||||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#30502"
|
||||
msgid "Sync Plex artwork from the PMS (recommended)"
|
||||
msgstr ""
|
||||
|
||||
# Message shown if SSL HTTPS certificate fails
|
||||
msgctxt "#30503"
|
||||
msgid "SSL certificate failed to validate. Please check {0} for solutions."
|
||||
msgstr ""
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30505"
|
||||
msgid "[COLOR yellow]Reset login attempts[/COLOR]"
|
||||
msgstr "[COLOR yellow]Repor tentativas de inicio de sessão[/COLOR]"
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#30506"
|
||||
|
@ -567,8 +490,8 @@ msgstr "Reproduzir"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30517"
|
||||
msgid "Set network credentials for Direct Paths and direct play"
|
||||
msgstr ""
|
||||
msgid "[COLOR yellow]Enter network credentials[/COLOR]"
|
||||
msgstr "[COLOR yellow]Digite as credenciais da rede[/COLOR]"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30518"
|
||||
|
@ -595,21 +518,6 @@ msgctxt "#30522"
|
|||
msgid "Force transcode h265/HEVC"
|
||||
msgstr "Forçar a transcodificação dos codecs h265/HEVC"
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
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 ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -672,8 +580,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 +602,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,29 +622,14 @@ 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"
|
||||
msgstr "Utilizador ou palavra-passe inválidos"
|
||||
|
||||
msgctxt "#33010"
|
||||
msgid "User is unauthorized for server {0}"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#33011"
|
||||
msgid "Plex.tv did not provide us a valid list of Plex users, sorry."
|
||||
msgstr ""
|
||||
msgid "Failed to authenticate too many times. Reset in the settings."
|
||||
msgstr "Falha na autenticação demasiadas vezes. Repor nas definições"
|
||||
|
||||
# Dialog before playback
|
||||
msgctxt "#33013"
|
||||
|
@ -894,8 +772,9 @@ msgstr ""
|
|||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#39020"
|
||||
msgid "Cache all images to Kodi texture cache now"
|
||||
msgid "[COLOR yellow]Cache all images to Kodi texture cache now[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow]Cache todas as imagens para o cache de textura do Kodi[/COLOR]"
|
||||
|
||||
# Appended to a listed PMS if it is in the same LAN network as PKC
|
||||
msgctxt "#39022"
|
||||
|
@ -975,16 +854,6 @@ msgstr ""
|
|||
"Substituir caminhos Plex /volume1/media or \\\\myserver\\media with custom "
|
||||
"SMB paths smb://NAS/mystuff"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
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,8 +930,8 @@ msgstr "Nada funciona? Tente uma reposição completa!"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39050"
|
||||
msgid "Choose Plex Server from a list"
|
||||
msgstr ""
|
||||
msgid "[COLOR yellow]Choose Plex Server from a list[/COLOR]"
|
||||
msgstr "[COLOR yellow]Escolha o Servidor Plex de uma lista[/COLOR]"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39051"
|
||||
|
@ -1091,15 +960,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 +1014,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"
|
||||
|
@ -1150,8 +1028,10 @@ msgstr "O seu Servidor Plex Media atual:"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39068"
|
||||
msgid "Manually enter Plex Media Server address"
|
||||
msgid "[COLOR yellow]Manually enter Plex Media Server address[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow]Inserir manualmente o endereço do Servidor Plex Media "
|
||||
"Server[/COLOR]"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39069"
|
||||
|
@ -1168,11 +1048,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 +1058,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 +1074,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"
|
||||
|
@ -1216,65 +1092,6 @@ msgstr ""
|
|||
"O Acompanhante Plex não conseguiu abrir a porta GDM. Por favor mude-a nas "
|
||||
"definiçoes PKC."
|
||||
|
||||
# Pop-up on initial sync.
|
||||
# Check that next translations for Add-on Paths and Direct Paths are
|
||||
# identical!
|
||||
msgctxt "#39080"
|
||||
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 ""
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39081"
|
||||
msgid "Add-on Paths"
|
||||
msgstr ""
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39082"
|
||||
msgid "Direct Paths"
|
||||
msgstr ""
|
||||
|
||||
# Dialog for manually entering PMS
|
||||
msgctxt "#39083"
|
||||
msgid "Enter PMS IP or URL"
|
||||
msgstr ""
|
||||
|
||||
# Dialog for manually entering PMS
|
||||
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"
|
||||
|
@ -1283,6 +1100,10 @@ msgctxt "#39201"
|
|||
msgid "Settings"
|
||||
msgstr "Configurações"
|
||||
|
||||
msgctxt "#39203"
|
||||
msgid "Refresh Plex playlists/nodes"
|
||||
msgstr "Atualizar listas de reprodução/nós do Plex"
|
||||
|
||||
msgctxt "#39204"
|
||||
msgid "Perform manual library sync"
|
||||
msgstr "Executar sincronização manual da biblioteca"
|
||||
|
@ -1312,8 +1133,9 @@ msgstr "Falha na reposição do PKC. Tente reiniciar o Kodi."
|
|||
|
||||
# PKC Settings - Plex
|
||||
msgctxt "#39209"
|
||||
msgid "Toggle plex.tv login (sign in or sign out)"
|
||||
msgid "[COLOR yellow]Toggle plex.tv login (sign in or sign out)[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow] Alternar inicio de sessão plex.tv (inicio ou saida)[/COLOR]"
|
||||
|
||||
msgctxt "#39210"
|
||||
msgid "Not yet connected to Plex Server"
|
||||
|
@ -1323,19 +1145,21 @@ msgctxt "#39211"
|
|||
msgid "Watch later"
|
||||
msgstr "Ver mais tarde"
|
||||
|
||||
# Error message pop-up if {0} cannot be contacted. {0} will be replaced by
|
||||
# e.g. the PMS' name
|
||||
# String attached at the end to get something like "PMS Name is offline"
|
||||
msgctxt "#39213"
|
||||
msgid "{0} offline"
|
||||
msgstr ""
|
||||
msgid "is offline"
|
||||
msgstr "está offline"
|
||||
|
||||
msgctxt "#39215"
|
||||
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 ""
|
||||
"Does your Plex Media Server support SSL connections? (https instead of "
|
||||
"http)?"
|
||||
msgstr ""
|
||||
"O seu Servidor Plex Media suporta conexões SSL? (https em vez de http)"
|
||||
|
||||
msgctxt "#39218"
|
||||
msgid "Error contacting PMS"
|
||||
|
@ -1355,8 +1179,8 @@ msgid "plex.tv toggle successful"
|
|||
msgstr "alternação da plex.tv bem sucedida "
|
||||
|
||||
msgctxt "#39222"
|
||||
msgid "Look for missing fanart on FanartTV now"
|
||||
msgstr ""
|
||||
msgid "[COLOR yellow]Look for missing fanart on FanartTV now[/COLOR]"
|
||||
msgstr "[COLOR yellow]Procurar ilustrações em falha na FanartTV now[/COLOR]"
|
||||
|
||||
msgctxt "#39223"
|
||||
msgid ""
|
||||
|
@ -1384,26 +1208,11 @@ msgctxt "#39227"
|
|||
msgid "Logged in to plex.tv"
|
||||
msgstr "Iniciado na sessão da plex.tv"
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
# Message in the PKC settings to display the plex.tv username. Leave the colon
|
||||
# :
|
||||
msgctxt "#39228"
|
||||
msgid "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 ""
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
msgctxt "#39230"
|
||||
msgid "Logged in Plex home user"
|
||||
msgstr ""
|
||||
|
||||
# Message in the PKC settings to change the logged in Plex home user
|
||||
msgctxt "#39231"
|
||||
msgid "Change logged in Plex home user"
|
||||
msgstr ""
|
||||
msgid "Plex user:"
|
||||
msgstr "Utilizador do Plex:"
|
||||
|
||||
msgctxt "#39250"
|
||||
msgid ""
|
||||
|
@ -1475,9 +1284,12 @@ msgstr "poderá não funcionar corretamente até a base de dados ser reposta."
|
|||
|
||||
msgctxt "#39403"
|
||||
msgid ""
|
||||
"The current Kodi version is not supported by PKC. Please consult the Plex "
|
||||
"forum."
|
||||
"Cancelling the database syncing process. Current Kodi version is "
|
||||
"unsupported. Please verify your logs for more info."
|
||||
msgstr ""
|
||||
"A cancelar o processo de sincronização da base de dados. A versão corrente "
|
||||
"do Kodi não é suportada. Por favor verifique os seus registos para mais "
|
||||
"informações."
|
||||
|
||||
msgctxt "#39405"
|
||||
msgid "Plex playlists/nodes refreshed"
|
||||
|
@ -1487,6 +1299,10 @@ msgctxt "#39406"
|
|||
msgid "Plex playlists/nodes refresh failed"
|
||||
msgstr "Atualização das listas de reprodução/nós do Plex falharam"
|
||||
|
||||
msgctxt "#39407"
|
||||
msgid "Full library sync finished"
|
||||
msgstr "Sincronização completa da biblioteca terminada"
|
||||
|
||||
msgctxt "#39408"
|
||||
msgid ""
|
||||
"Sync had to skip some items because they could not be processed. Kodi may be"
|
||||
|
@ -1518,10 +1334,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 +1394,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 +1452,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,16 +1,15 @@
|
|||
# XBMC Media Center language file
|
||||
# Translators:
|
||||
# Bruno Guerreiro <american.jesus.pt@gmail.com>, 2017
|
||||
# Croneter None <croneter@gmail.com>, 2017
|
||||
# Goncalo Campos <gda_campos@hotmail.com>, 2018
|
||||
# Bruno Guerreiro <american.jesus.pt@gmail.com>, 2019
|
||||
#
|
||||
# Goncalo Campos <gda_campos@hotmail.com>, 2017
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PlexKodiConnect\n"
|
||||
"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: Bruno Guerreiro <american.jesus.pt@gmail.com>, 2019\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Goncalo Campos <gda_campos@hotmail.com>, 2017\n"
|
||||
"Language-Team: Portuguese (Portugal) (https://www.transifex.com/croneter/teams/73837/pt_PT/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
@ -29,7 +28,7 @@ msgstr "Endereço do servidor (IP)"
|
|||
|
||||
msgctxt "#30001"
|
||||
msgid "Searching for PMS"
|
||||
msgstr "A procurar por PMS"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30002"
|
||||
msgid "Preferred playback method"
|
||||
|
@ -44,13 +43,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: "
|
||||
|
@ -58,17 +50,17 @@ msgstr "Utilizador: "
|
|||
# Sync notification displayed if there is still artwork to be cached to Kodi
|
||||
msgctxt "#30006"
|
||||
msgid "Caching %s Plex images"
|
||||
msgstr "A recolher %s imagens do Plex"
|
||||
msgstr ""
|
||||
|
||||
# Sync notification displayed if syncing of major artwork is done
|
||||
msgctxt "#30007"
|
||||
msgid "Plex image caching done"
|
||||
msgstr "Recolha de imagens do Plex concluída"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings artwork: Enable notifications for artwork image sync
|
||||
msgctxt "#30008"
|
||||
msgid "Enable notifications for image caching"
|
||||
msgstr "Ativar notificações de recolha de imagens"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings artwork: Enable image caching during Kodi playback
|
||||
msgctxt "#30009"
|
||||
|
@ -78,12 +70,12 @@ msgstr ""
|
|||
# PKC settings - Artwork
|
||||
msgctxt "#30010"
|
||||
msgid "Approximate progress"
|
||||
msgstr "Progresso aproximado"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings - Artwork
|
||||
msgctxt "#30011"
|
||||
msgid "Artwork left to cache:"
|
||||
msgstr "Ilustrações para recolha em falta:"
|
||||
msgstr ""
|
||||
|
||||
# Button text
|
||||
msgctxt "#30012"
|
||||
|
@ -99,10 +91,10 @@ msgctxt "#30014"
|
|||
msgid "Connection"
|
||||
msgstr "Ligação"
|
||||
|
||||
# Pop-up notification if user tried to manually initiate fanart download
|
||||
# PKC settings - Artwork
|
||||
msgctxt "#30015"
|
||||
msgid "Fanart download already running"
|
||||
msgstr "Recolha de ilustrações já em progresso"
|
||||
msgid "Movie and show FanartTV lookups left to do:"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Device Name"
|
||||
|
@ -116,23 +108,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 "A pesquisar FanartTV por %s itens"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
# Sync notification displayed when FanartTV lookup is completed
|
||||
msgctxt "#30019"
|
||||
msgid "FanartTV lookup completed"
|
||||
msgstr "Pesquisa de FanartTV concluída"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings sync options
|
||||
msgctxt "#30020"
|
||||
msgid "Sync Plex playlists (reboot Kodi!)"
|
||||
msgstr "Sincronizar de listas de reprodução do Plex (reiniciar Kodi)"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings sync options
|
||||
msgctxt "#30021"
|
||||
msgid "Only sync specific Plex playlists to Kodi"
|
||||
msgstr ""
|
||||
"Apenas sincronizar listas de reprodução específicas do Plex para o Kodi"
|
||||
|
||||
# PKC settings category
|
||||
msgctxt "#30022"
|
||||
|
@ -143,7 +134,6 @@ msgstr "Avançado"
|
|||
msgctxt "#30023"
|
||||
msgid "Only sync specific Kodi playlists to Plex"
|
||||
msgstr ""
|
||||
"Apenas sincronizar listas de reprodução específicas do Kodi com o Plex"
|
||||
|
||||
msgctxt "#30024"
|
||||
msgid "Username"
|
||||
|
@ -157,26 +147,11 @@ msgstr "Exibir mensagem se o PMS ficar offline"
|
|||
msgctxt "#30026"
|
||||
msgid "Prefix in Plex playlist name to trigger sync"
|
||||
msgstr ""
|
||||
"Prefixo no nome da lista de reprodução do Plex para acionar a sincronização"
|
||||
|
||||
# PKC settings sync options
|
||||
msgctxt "#30027"
|
||||
msgid "Prefix in Kodi playlist name to trigger sync"
|
||||
msgstr ""
|
||||
"Prefixo no nome da lista de reprodução do Kodi para acionar a sincronização"
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
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"
|
||||
|
@ -277,10 +252,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"
|
||||
|
@ -337,44 +308,6 @@ msgctxt "#30198"
|
|||
msgid "Search"
|
||||
msgstr "Procurar"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30200"
|
||||
msgid ""
|
||||
"In the following window, enter the server's hostname (or IP) where your Plex"
|
||||
" media resides. Mind the case!"
|
||||
msgstr ""
|
||||
"Na janela seguinte, insira o nome do host (ou IP) do servidor onde reside a "
|
||||
"media do Plex. "
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# for hostname
|
||||
msgctxt "#30201"
|
||||
msgid "Enter server hostname (or IP)"
|
||||
msgstr "Intrusa o nome do host do servidor (ou IP)"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30202"
|
||||
msgid ""
|
||||
"In the following window, enter the network protocol you would like to use. "
|
||||
"This is likely 'smb'."
|
||||
msgstr ""
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# protocol
|
||||
msgctxt "#30203"
|
||||
msgid "Enter network protocol"
|
||||
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 ""
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30205"
|
||||
msgid "The protocol '{0}' that you entered is not supported."
|
||||
msgstr ""
|
||||
|
||||
# Video node naming for random e.g. movies
|
||||
msgctxt "#30227"
|
||||
msgid "Random"
|
||||
|
@ -488,12 +421,6 @@ msgctxt "#30416"
|
|||
msgid "Settings for the Plex Server"
|
||||
msgstr "Configurações do Servidor Plex"
|
||||
|
||||
msgctxt "#30417"
|
||||
msgid ""
|
||||
"Could not change the Kodi settings file {0}. PKC might not work correctly. "
|
||||
"Error: {1}"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30500"
|
||||
msgid "Verify Host SSL Certificate (more secure)"
|
||||
|
@ -504,15 +431,10 @@ msgctxt "#30501"
|
|||
msgid "Client SSL certificate"
|
||||
msgstr "Certificado SSL do cliente"
|
||||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#30502"
|
||||
msgid "Sync Plex artwork from the PMS (recommended)"
|
||||
msgstr ""
|
||||
|
||||
# Message shown if SSL HTTPS certificate fails
|
||||
msgctxt "#30503"
|
||||
msgid "SSL certificate failed to validate. Please check {0} for solutions."
|
||||
msgstr ""
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30505"
|
||||
msgid "[COLOR yellow]Reset login attempts[/COLOR]"
|
||||
msgstr "[COLOR yellow]Repor tentativas de inicio de sessão[/COLOR]"
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#30506"
|
||||
|
@ -570,8 +492,8 @@ msgstr "Reproduzir"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30517"
|
||||
msgid "Set network credentials for Direct Paths and direct play"
|
||||
msgstr ""
|
||||
msgid "[COLOR yellow]Enter network credentials[/COLOR]"
|
||||
msgstr "[COLOR yellow]Digite as credenciais da rede[/COLOR]"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30518"
|
||||
|
@ -598,21 +520,6 @@ msgctxt "#30522"
|
|||
msgid "Force transcode h265/HEVC"
|
||||
msgstr "Forçar a transcodificação dos codecs h265/HEVC"
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
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 ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -675,8 +582,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 +604,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,29 +624,14 @@ 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"
|
||||
msgstr "Utilizador ou palavra-passe inválidos"
|
||||
|
||||
msgctxt "#33010"
|
||||
msgid "User is unauthorized for server {0}"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#33011"
|
||||
msgid "Plex.tv did not provide us a valid list of Plex users, sorry."
|
||||
msgstr ""
|
||||
msgid "Failed to authenticate too many times. Reset in the settings."
|
||||
msgstr "Falha na autenticação demasiadas vezes. Repor nas definições"
|
||||
|
||||
# Dialog before playback
|
||||
msgctxt "#33013"
|
||||
|
@ -897,8 +774,9 @@ msgstr ""
|
|||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#39020"
|
||||
msgid "Cache all images to Kodi texture cache now"
|
||||
msgid "[COLOR yellow]Cache all images to Kodi texture cache now[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow]Cache todas as imagens para o cache de textura do Kodi[/COLOR]"
|
||||
|
||||
# Appended to a listed PMS if it is in the same LAN network as PKC
|
||||
msgctxt "#39022"
|
||||
|
@ -978,16 +856,6 @@ msgstr ""
|
|||
"Substituir caminhos Plex /volume1/media or \\\\myserver\\media with custom "
|
||||
"SMB paths smb://NAS/mystuff"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
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:"
|
||||
|
@ -1064,8 +932,8 @@ msgstr "Nada funciona? Tente uma reposição completa!"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39050"
|
||||
msgid "Choose Plex Server from a list"
|
||||
msgstr ""
|
||||
msgid "[COLOR yellow]Choose Plex Server from a list[/COLOR]"
|
||||
msgstr "[COLOR yellow]Escolha o Servidor Plex de uma lista[/COLOR]"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39051"
|
||||
|
@ -1094,15 +962,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 +1016,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"
|
||||
|
@ -1153,8 +1030,10 @@ msgstr "O seu Servidor Plex Media atual:"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39068"
|
||||
msgid "Manually enter Plex Media Server address"
|
||||
msgid "[COLOR yellow]Manually enter Plex Media Server address[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow]Inserir manualmente o endereço do Servidor Plex Media "
|
||||
"Server[/COLOR]"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39069"
|
||||
|
@ -1171,11 +1050,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 +1060,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 +1076,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"
|
||||
|
@ -1219,65 +1094,6 @@ msgstr ""
|
|||
"O Acompanhante Plex não conseguiu abrir a porta GDM. Por favor mude-a nas "
|
||||
"definiçoes PKC."
|
||||
|
||||
# Pop-up on initial sync.
|
||||
# Check that next translations for Add-on Paths and Direct Paths are
|
||||
# identical!
|
||||
msgctxt "#39080"
|
||||
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 ""
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39081"
|
||||
msgid "Add-on Paths"
|
||||
msgstr ""
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39082"
|
||||
msgid "Direct Paths"
|
||||
msgstr ""
|
||||
|
||||
# Dialog for manually entering PMS
|
||||
msgctxt "#39083"
|
||||
msgid "Enter PMS IP or URL"
|
||||
msgstr ""
|
||||
|
||||
# Dialog for manually entering PMS
|
||||
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"
|
||||
|
@ -1286,6 +1102,10 @@ msgctxt "#39201"
|
|||
msgid "Settings"
|
||||
msgstr "Configurações"
|
||||
|
||||
msgctxt "#39203"
|
||||
msgid "Refresh Plex playlists/nodes"
|
||||
msgstr "Atualizar listas de reprodução/nós do Plex"
|
||||
|
||||
msgctxt "#39204"
|
||||
msgid "Perform manual library sync"
|
||||
msgstr "Executar sincronização manual da biblioteca"
|
||||
|
@ -1315,8 +1135,9 @@ msgstr "Falha na reposição do PKC. Tente reiniciar o Kodi."
|
|||
|
||||
# PKC Settings - Plex
|
||||
msgctxt "#39209"
|
||||
msgid "Toggle plex.tv login (sign in or sign out)"
|
||||
msgid "[COLOR yellow]Toggle plex.tv login (sign in or sign out)[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow] Alternar inicio de sessão plex.tv (inicio ou saida)[/COLOR]"
|
||||
|
||||
msgctxt "#39210"
|
||||
msgid "Not yet connected to Plex Server"
|
||||
|
@ -1326,19 +1147,21 @@ msgctxt "#39211"
|
|||
msgid "Watch later"
|
||||
msgstr "Ver mais tarde"
|
||||
|
||||
# Error message pop-up if {0} cannot be contacted. {0} will be replaced by
|
||||
# e.g. the PMS' name
|
||||
# String attached at the end to get something like "PMS Name is offline"
|
||||
msgctxt "#39213"
|
||||
msgid "{0} offline"
|
||||
msgstr ""
|
||||
msgid "is offline"
|
||||
msgstr "está offline"
|
||||
|
||||
msgctxt "#39215"
|
||||
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 ""
|
||||
"Does your Plex Media Server support SSL connections? (https instead of "
|
||||
"http)?"
|
||||
msgstr ""
|
||||
"O seu Servidor Plex Media suporta conexões SSL? (https em vez de http)"
|
||||
|
||||
msgctxt "#39218"
|
||||
msgid "Error contacting PMS"
|
||||
|
@ -1358,8 +1181,8 @@ msgid "plex.tv toggle successful"
|
|||
msgstr "alternação da plex.tv bem sucedida "
|
||||
|
||||
msgctxt "#39222"
|
||||
msgid "Look for missing fanart on FanartTV now"
|
||||
msgstr ""
|
||||
msgid "[COLOR yellow]Look for missing fanart on FanartTV now[/COLOR]"
|
||||
msgstr "[COLOR yellow]Procurar ilustrações em falha na FanartTV now[/COLOR]"
|
||||
|
||||
msgctxt "#39223"
|
||||
msgid ""
|
||||
|
@ -1387,26 +1210,11 @@ msgctxt "#39227"
|
|||
msgid "Logged in to plex.tv"
|
||||
msgstr "Iniciado na sessão da plex.tv"
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
# Message in the PKC settings to display the plex.tv username. Leave the colon
|
||||
# :
|
||||
msgctxt "#39228"
|
||||
msgid "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 ""
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
msgctxt "#39230"
|
||||
msgid "Logged in Plex home user"
|
||||
msgstr ""
|
||||
|
||||
# Message in the PKC settings to change the logged in Plex home user
|
||||
msgctxt "#39231"
|
||||
msgid "Change logged in Plex home user"
|
||||
msgstr ""
|
||||
msgid "Plex user:"
|
||||
msgstr "Utilizador do Plex:"
|
||||
|
||||
msgctxt "#39250"
|
||||
msgid ""
|
||||
|
@ -1478,9 +1286,12 @@ msgstr "poderá não funcionar corretamente até a base de dados ser reposta."
|
|||
|
||||
msgctxt "#39403"
|
||||
msgid ""
|
||||
"The current Kodi version is not supported by PKC. Please consult the Plex "
|
||||
"forum."
|
||||
"Cancelling the database syncing process. Current Kodi version is "
|
||||
"unsupported. Please verify your logs for more info."
|
||||
msgstr ""
|
||||
"A cancelar o processo de sincronização da base de dados. A versão corrente "
|
||||
"do Kodi não é suportada. Por favor verifique os seus registos para mais "
|
||||
"informações."
|
||||
|
||||
msgctxt "#39405"
|
||||
msgid "Plex playlists/nodes refreshed"
|
||||
|
@ -1490,6 +1301,10 @@ msgctxt "#39406"
|
|||
msgid "Plex playlists/nodes refresh failed"
|
||||
msgstr "Atualização das listas de reprodução/nós do Plex falharam"
|
||||
|
||||
msgctxt "#39407"
|
||||
msgid "Full library sync finished"
|
||||
msgstr "Sincronização completa da biblioteca terminada"
|
||||
|
||||
msgctxt "#39408"
|
||||
msgid ""
|
||||
"Sync had to skip some items because they could not be processed. Kodi may be"
|
||||
|
@ -1521,10 +1336,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 +1396,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 +1454,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,19 +1,18 @@
|
|||
# XBMC Media Center language file
|
||||
# Translators:
|
||||
# Croneter None <croneter@gmail.com>, 2017
|
||||
# Alexey Korobcov <korobcoff@gmail.com>, 2017
|
||||
# Vladimir Supranenok <stark_v@mail.ru>, 2017
|
||||
# Павел Хоменко, 2017
|
||||
# Alex Freit <alex.nx@mail.ru>, 2019
|
||||
# Vladimir Supranenok <stark_v@mail.ru>, 2019
|
||||
# Vlad Anisimov <uniss@ua.fm>, 2019
|
||||
#
|
||||
# Алексей Коробцов <korobcoff@gmail.com>, 2017
|
||||
# Alex Freit <alex.nx@mail.ru>, 2017
|
||||
# Vlad Anisimov <uniss@ua.fm>, 2018
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PlexKodiConnect\n"
|
||||
"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"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Vlad Anisimov <uniss@ua.fm>, 2018\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 +48,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 "Имя пользователя: "
|
||||
|
@ -106,10 +98,10 @@ msgctxt "#30014"
|
|||
msgid "Connection"
|
||||
msgstr "Подключение"
|
||||
|
||||
# Pop-up notification if user tried to manually initiate fanart download
|
||||
# PKC settings - Artwork
|
||||
msgctxt "#30015"
|
||||
msgid "Fanart download already running"
|
||||
msgstr "Fanart уже загружаются"
|
||||
msgid "Movie and show FanartTV lookups left to do:"
|
||||
msgstr "Осталось просмотреть FanartTV для фильмов и сериалов:"
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Device Name"
|
||||
|
@ -125,7 +117,7 @@ msgctxt "#30018"
|
|||
msgid "Checking FanartTV for %s items"
|
||||
msgstr "FanartTV осталось проверить %s"
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
# Sync notification displayed when FanartTV lookup is completed
|
||||
msgctxt "#30019"
|
||||
msgid "FanartTV lookup completed"
|
||||
msgstr "Поиск FanartTV завершен"
|
||||
|
@ -133,12 +125,12 @@ msgstr "Поиск FanartTV завершен"
|
|||
# PKC settings sync options
|
||||
msgctxt "#30020"
|
||||
msgid "Sync Plex playlists (reboot Kodi!)"
|
||||
msgstr "Синхронизация плейлистов Plex (перезапустите Kodi!)"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings sync options
|
||||
msgctxt "#30021"
|
||||
msgid "Only sync specific Plex playlists to Kodi"
|
||||
msgstr "Синхронизировать только некоторые плейлисты из Plex"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings category
|
||||
msgctxt "#30022"
|
||||
|
@ -148,7 +140,7 @@ msgstr "Расширенные"
|
|||
# PKC settings sync options
|
||||
msgctxt "#30023"
|
||||
msgid "Only sync specific Kodi playlists to Plex"
|
||||
msgstr "Синхронизировать только некоторые плейлисты из Kodi"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30024"
|
||||
msgid "Username"
|
||||
|
@ -161,24 +153,11 @@ msgstr "Сообщать об отключении от сервера Plex "
|
|||
# PKC settings sync options
|
||||
msgctxt "#30026"
|
||||
msgid "Prefix in Plex playlist name to trigger sync"
|
||||
msgstr "Префикс плейлиста в Plex для синхронизации"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings sync options
|
||||
msgctxt "#30027"
|
||||
msgid "Prefix in Kodi playlist name to trigger sync"
|
||||
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"
|
||||
|
@ -280,10 +259,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 "Прямое воспроизведение"
|
||||
|
@ -340,46 +315,6 @@ msgctxt "#30198"
|
|||
msgid "Search"
|
||||
msgstr "Поиск"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30200"
|
||||
msgid ""
|
||||
"In the following window, enter the server's hostname (or IP) where your Plex"
|
||||
" media resides. Mind the case!"
|
||||
msgstr ""
|
||||
"В следующем окне введите сетевое имя сервера (или его IP-адрес) на котором "
|
||||
"находятся медиафайлы. Соблюдайте регистр!"
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# for hostname
|
||||
msgctxt "#30201"
|
||||
msgid "Enter server hostname (or IP)"
|
||||
msgstr "Введите имя сервера (или IP)"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30202"
|
||||
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 "Введите сетевой протокол"
|
||||
|
||||
# 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}\" не верно."
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30205"
|
||||
msgid "The protocol '{0}' that you entered is not supported."
|
||||
msgstr "Введенный вами протокол {0} не поддерживается."
|
||||
|
||||
# Video node naming for random e.g. movies
|
||||
msgctxt "#30227"
|
||||
msgid "Random"
|
||||
|
@ -491,14 +426,6 @@ msgctxt "#30416"
|
|||
msgid "Settings for the Plex Server"
|
||||
msgstr "Настройки для сервера Plex"
|
||||
|
||||
msgctxt "#30417"
|
||||
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"
|
||||
msgid "Verify Host SSL Certificate (more secure)"
|
||||
|
@ -509,16 +436,10 @@ msgctxt "#30501"
|
|||
msgid "Client SSL certificate"
|
||||
msgstr "SSL сертификат клиента"
|
||||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#30502"
|
||||
msgid "Sync Plex artwork from the PMS (recommended)"
|
||||
msgstr "Синхронизировать иллюстрации с Plex. (рекомендуется)"
|
||||
|
||||
# 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 - Connection
|
||||
msgctxt "#30505"
|
||||
msgid "[COLOR yellow]Reset login attempts[/COLOR]"
|
||||
msgstr "[COLOR yellow]Сбросить попытки входа[/COLOR]"
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#30506"
|
||||
|
@ -564,8 +485,6 @@ msgstr ""
|
|||
msgctxt "#30514"
|
||||
msgid "Show all Plex extras instead of immediately playing trailers"
|
||||
msgstr ""
|
||||
"Воспроизвести все дополнительные материалы Plex, вместо того, чтобы сразу "
|
||||
"воспроизводить трейлеры"
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
msgctxt "#30515"
|
||||
|
@ -579,8 +498,8 @@ msgstr "Воспроизведение"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30517"
|
||||
msgid "Set network credentials for Direct Paths and direct play"
|
||||
msgstr "Ввести сетевые учетные данные для прямых путей и Direct play"
|
||||
msgid "[COLOR yellow]Enter network credentials[/COLOR]"
|
||||
msgstr "[COLOR yellow]Введите сетевые учетные данные[/COLOR]"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30518"
|
||||
|
@ -608,23 +527,6 @@ msgctxt "#30522"
|
|||
msgid "Force transcode h265/HEVC"
|
||||
msgstr "Транскодировать h265/HEVC"
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
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
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -669,7 +571,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 +591,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"
|
||||
|
@ -700,7 +602,7 @@ msgstr "Всегда выбирать лучшее качество для тр
|
|||
# PKC Settings - Artwork
|
||||
msgctxt "#30543"
|
||||
msgid "Prefer Kodi artwork for collections/sets"
|
||||
msgstr "Предпочитать иллюстрации из Kodi для коллекций"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30544"
|
||||
msgid "Artwork"
|
||||
|
@ -711,21 +613,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,29 +633,14 @@ 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"
|
||||
msgstr "Неверное имя пользователя или пароль"
|
||||
|
||||
msgctxt "#33010"
|
||||
msgid "User is unauthorized for server {0}"
|
||||
msgstr "Пользователь не авторизован на сервере {0}"
|
||||
|
||||
msgctxt "#33011"
|
||||
msgid "Plex.tv did not provide us a valid list of Plex users, sorry."
|
||||
msgstr "Plex.tv не предоставил списка пользователей Plex, извините"
|
||||
msgid "Failed to authenticate too many times. Reset in the settings."
|
||||
msgstr "Множественная ошибка авторизации. Сбросьте настройки."
|
||||
|
||||
# Dialog before playback
|
||||
msgctxt "#33013"
|
||||
|
@ -907,8 +779,9 @@ msgstr "Сброс базы данных Kodi и возможная переза
|
|||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#39020"
|
||||
msgid "Cache all images to Kodi texture cache now"
|
||||
msgstr "Кешировать все изображения в кеш Kodi"
|
||||
msgid "[COLOR yellow]Cache all images to Kodi texture cache now[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow]Кешировать все изображения сейчас в Kodi texture cache[/COLOR]"
|
||||
|
||||
# Appended to a listed PMS if it is in the same LAN network as PKC
|
||||
msgctxt "#39022"
|
||||
|
@ -991,16 +864,6 @@ msgstr ""
|
|||
"Заменить пути Plex /volume1/media либо \\\\myserver\\media на "
|
||||
"пользовательские Samba пути smb://NAS/mystuff"
|
||||
|
||||
# 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 ""
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39037"
|
||||
msgid "Original Plex MOVIE path to replace:"
|
||||
|
@ -1076,8 +939,8 @@ msgstr "Ничего не работает? Попробуйте общий сб
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39050"
|
||||
msgid "Choose Plex Server from a list"
|
||||
msgstr "Выбрать сервер Plex из списка"
|
||||
msgid "[COLOR yellow]Choose Plex Server from a list[/COLOR]"
|
||||
msgstr "[COLOR yellow]Выберите сервер Plex из списка[/COLOR]"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39051"
|
||||
|
@ -1106,17 +969,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 +1021,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"
|
||||
|
@ -1166,8 +1035,8 @@ msgstr "Ваш текущий сервер Plex:"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39068"
|
||||
msgid "Manually enter Plex Media Server address"
|
||||
msgstr "Ввести адрес сервера Plex вручную"
|
||||
msgid "[COLOR yellow]Manually enter Plex Media Server address[/COLOR]"
|
||||
msgstr "[COLOR yellow]Вручную ввести адрес сервера Plex[/COLOR]"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39069"
|
||||
|
@ -1184,11 +1053,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 +1063,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 +1080,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"
|
||||
|
@ -1232,68 +1096,6 @@ msgid ""
|
|||
msgstr ""
|
||||
"Plex Companion не может открыть порт GDM. Смените его в настройках PKC."
|
||||
|
||||
# Pop-up on initial sync.
|
||||
# Check that next translations for Add-on Paths and Direct Paths are
|
||||
# identical!
|
||||
msgctxt "#39080"
|
||||
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 "Пути дополнения"
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39082"
|
||||
msgid "Direct Paths"
|
||||
msgstr "Прямые пути"
|
||||
|
||||
# Dialog for manually entering PMS
|
||||
msgctxt "#39083"
|
||||
msgid "Enter PMS IP or URL"
|
||||
msgstr "Введите IP или адрес PMS"
|
||||
|
||||
# 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"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "Выйти из Plex"
|
||||
|
@ -1302,6 +1104,10 @@ msgctxt "#39201"
|
|||
msgid "Settings"
|
||||
msgstr "Настройки"
|
||||
|
||||
msgctxt "#39203"
|
||||
msgid "Refresh Plex playlists/nodes"
|
||||
msgstr "Обновить плейлисты/списки Plex"
|
||||
|
||||
msgctxt "#39204"
|
||||
msgid "Perform manual library sync"
|
||||
msgstr "Синхронизировать вручную"
|
||||
|
@ -1330,8 +1136,9 @@ msgstr "Невозможно сбросить PKC. Попробуйте пере
|
|||
|
||||
# PKC Settings - Plex
|
||||
msgctxt "#39209"
|
||||
msgid "Toggle plex.tv login (sign in or sign out)"
|
||||
msgstr "Переключить статус plex.tv (войти или выйти)"
|
||||
msgid "[COLOR yellow]Toggle plex.tv login (sign in or sign out)[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow]Переключить авторизацию plex.tv(войти или выйти)[/COLOR]"
|
||||
|
||||
msgctxt "#39210"
|
||||
msgid "Not yet connected to Plex Server"
|
||||
|
@ -1341,19 +1148,20 @@ msgctxt "#39211"
|
|||
msgid "Watch later"
|
||||
msgstr "Смотреть позже"
|
||||
|
||||
# Error message pop-up if {0} cannot be contacted. {0} will be replaced by
|
||||
# e.g. the PMS' name
|
||||
# String attached at the end to get something like "PMS Name is offline"
|
||||
msgctxt "#39213"
|
||||
msgid "{0} offline"
|
||||
msgstr "{0} недоступен"
|
||||
msgid "is offline"
|
||||
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."
|
||||
msgstr ""
|
||||
msgid ""
|
||||
"Does your Plex Media Server support SSL connections? (https instead of "
|
||||
"http)?"
|
||||
msgstr "Ваш Plex-сервер поддерживает SSL соединение (https вместо http)?"
|
||||
|
||||
msgctxt "#39218"
|
||||
msgid "Error contacting PMS"
|
||||
|
@ -1373,8 +1181,8 @@ msgid "plex.tv toggle successful"
|
|||
msgstr "переключение plex.tv удачно"
|
||||
|
||||
msgctxt "#39222"
|
||||
msgid "Look for missing fanart on FanartTV now"
|
||||
msgstr "Искать отсутствующие иллюстрации на FanartTV сейчас"
|
||||
msgid "[COLOR yellow]Look for missing fanart on FanartTV now[/COLOR]"
|
||||
msgstr "[COLOR yellow]Найти отсутствующие иллюстрации на FanartTV[/COLOR]"
|
||||
|
||||
msgctxt "#39223"
|
||||
msgid ""
|
||||
|
@ -1402,26 +1210,11 @@ msgctxt "#39227"
|
|||
msgid "Logged in to plex.tv"
|
||||
msgstr "Вы вошли в plex.tv"
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
# Message in the PKC settings to display the plex.tv username. Leave the colon
|
||||
# :
|
||||
msgctxt "#39228"
|
||||
msgid "Plex admin user"
|
||||
msgstr "Администратор Plex"
|
||||
|
||||
# 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 неудачен"
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
msgctxt "#39230"
|
||||
msgid "Logged in Plex home user"
|
||||
msgstr "Учетная запись Plex home"
|
||||
|
||||
# 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"
|
||||
msgid "Plex user:"
|
||||
msgstr "Пользователь Plex:"
|
||||
|
||||
msgctxt "#39250"
|
||||
msgid ""
|
||||
|
@ -1493,11 +1286,11 @@ msgstr "может работать неправильно до сброса б
|
|||
|
||||
msgctxt "#39403"
|
||||
msgid ""
|
||||
"The current Kodi version is not supported by PKC. Please consult the Plex "
|
||||
"forum."
|
||||
"Cancelling the database syncing process. Current Kodi version is "
|
||||
"unsupported. Please verify your logs for more info."
|
||||
msgstr ""
|
||||
"Текущая версия Kodi не поддерживается PKC. Пожалуйста проконсультируйтесь на"
|
||||
" форуме Plex"
|
||||
"Синхронизация отменена, текущая версия Kodi не поддерживается. Больше "
|
||||
"информации Вы найдёте в логах."
|
||||
|
||||
msgctxt "#39405"
|
||||
msgid "Plex playlists/nodes refreshed"
|
||||
|
@ -1507,6 +1300,10 @@ msgctxt "#39406"
|
|||
msgid "Plex playlists/nodes refresh failed"
|
||||
msgstr "Обновление плейлистов/списков Plex не удалось"
|
||||
|
||||
msgctxt "#39407"
|
||||
msgid "Full library sync finished"
|
||||
msgstr "Полная синхронизация библиотеки завершена"
|
||||
|
||||
msgctxt "#39408"
|
||||
msgid ""
|
||||
"Sync had to skip some items because they could not be processed. Kodi may be"
|
||||
|
@ -1538,10 +1335,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 +1393,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 +1451,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,14 +1,13 @@
|
|||
# XBMC Media Center language file
|
||||
# Translators:
|
||||
# Vlad Anisimov <uniss@ua.fm>, 2020
|
||||
#
|
||||
# Vlad Anisimov <uniss@ua.fm>, 2017
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PlexKodiConnect\n"
|
||||
"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"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Vlad Anisimov <uniss@ua.fm>, 2017\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 +43,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 "Ім'я користувача:"
|
||||
|
@ -100,10 +92,10 @@ msgctxt "#30014"
|
|||
msgid "Connection"
|
||||
msgstr "З'єднання"
|
||||
|
||||
# Pop-up notification if user tried to manually initiate fanart download
|
||||
# PKC settings - Artwork
|
||||
msgctxt "#30015"
|
||||
msgid "Fanart download already running"
|
||||
msgstr "Завантаження фан-арту вже розпочато"
|
||||
msgid "Movie and show FanartTV lookups left to do:"
|
||||
msgstr "Залишилось оглянути FanartTV фільмів та серіалів:"
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Device Name"
|
||||
|
@ -119,7 +111,7 @@ msgctxt "#30018"
|
|||
msgid "Checking FanartTV for %s items"
|
||||
msgstr "Перевіряння у FanartTV %s елементів"
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
# Sync notification displayed when FanartTV lookup is completed
|
||||
msgctxt "#30019"
|
||||
msgid "FanartTV lookup completed"
|
||||
msgstr "Огляд FanartTV завершено"
|
||||
|
@ -162,19 +154,6 @@ msgctxt "#30027"
|
|||
msgid "Prefix in Kodi playlist name to trigger sync"
|
||||
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"
|
||||
msgid "Port Number"
|
||||
msgstr "Номер порту"
|
||||
|
@ -274,11 +253,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 "Пряме відтворення"
|
||||
|
@ -335,45 +309,6 @@ msgctxt "#30198"
|
|||
msgid "Search"
|
||||
msgstr "Пошук"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30200"
|
||||
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)"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30202"
|
||||
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 "Введіть мережевий протокол"
|
||||
|
||||
# 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}', котру ви ввели, не є правильною."
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30205"
|
||||
msgid "The protocol '{0}' that you entered is not supported."
|
||||
msgstr "Протокол '{0}', котрий ви ввели, не підтримується."
|
||||
|
||||
# Video node naming for random e.g. movies
|
||||
msgctxt "#30227"
|
||||
msgid "Random"
|
||||
|
@ -487,14 +422,6 @@ msgctxt "#30416"
|
|||
msgid "Settings for the Plex Server"
|
||||
msgstr "Налаштування для PMS"
|
||||
|
||||
msgctxt "#30417"
|
||||
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"
|
||||
msgid "Verify Host SSL Certificate (more secure)"
|
||||
|
@ -505,17 +432,10 @@ msgctxt "#30501"
|
|||
msgid "Client SSL certificate"
|
||||
msgstr "SSL сертифікат клієнта"
|
||||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#30502"
|
||||
msgid "Sync Plex artwork from the PMS (recommended)"
|
||||
msgstr "Синхронізувати арт-файли Plex із PMS (рекомендовано)"
|
||||
|
||||
# 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 - Connection
|
||||
msgctxt "#30505"
|
||||
msgid "[COLOR yellow]Reset login attempts[/COLOR]"
|
||||
msgstr "[COLOR yellow]Скидання спроб входу[/COLOR]"
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#30506"
|
||||
|
@ -561,7 +481,6 @@ msgstr ""
|
|||
msgctxt "#30514"
|
||||
msgid "Show all Plex extras instead of immediately playing trailers"
|
||||
msgstr ""
|
||||
"Показувати всі екстра-файли Plex замість негайного відтворення трейлерів"
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
msgctxt "#30515"
|
||||
|
@ -575,8 +494,8 @@ msgstr "Відтворення"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30517"
|
||||
msgid "Set network credentials for Direct Paths and direct play"
|
||||
msgstr "Встановлення мережевих даних для прямих шляхів та прямого відтворення"
|
||||
msgid "[COLOR yellow]Enter network credentials[/COLOR]"
|
||||
msgstr "[COLOR yellow]Введіть мережеві повноваження[/COLOR]"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30518"
|
||||
|
@ -603,23 +522,6 @@ msgctxt "#30522"
|
|||
msgid "Force transcode h265/HEVC"
|
||||
msgstr "Примусове перекодування h265/HEVC"
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
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
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -684,8 +586,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 +608,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,29 +628,14 @@ 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"
|
||||
msgstr "Невірне ім'я користувача або пароль"
|
||||
|
||||
msgctxt "#33010"
|
||||
msgid "User is unauthorized for server {0}"
|
||||
msgstr "Користувач не є авторизованим на сервері {0}"
|
||||
|
||||
msgctxt "#33011"
|
||||
msgid "Plex.tv did not provide us a valid list of Plex users, sorry."
|
||||
msgstr "Plex.tv не дає нам дійсний список користувачів Plex, вибачте."
|
||||
msgid "Failed to authenticate too many times. Reset in the settings."
|
||||
msgstr "Збій багаторазової автентифікації. Скиньте у налаштуваннях."
|
||||
|
||||
# Dialog before playback
|
||||
msgctxt "#33013"
|
||||
|
@ -902,8 +774,9 @@ msgstr "Скинути БД Kodi та опціонально and optionally ск
|
|||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#39020"
|
||||
msgid "Cache all images to Kodi texture cache now"
|
||||
msgstr "Кешувати зараз всі зображення до кешу текстур Kodi"
|
||||
msgid "[COLOR yellow]Cache all images to Kodi texture cache now[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow]Кешувати зараз всі зображення до кешу текстур Kodi[/COLOR]"
|
||||
|
||||
# Appended to a listed PMS if it is in the same LAN network as PKC
|
||||
msgctxt "#39022"
|
||||
|
@ -987,16 +860,6 @@ msgstr ""
|
|||
"Замінити шляхи Plex /volume1/media або \\\\myserver\\media на SMB шляхи "
|
||||
"smb://NAS/mystuff"
|
||||
|
||||
# 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"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39037"
|
||||
msgid "Original Plex MOVIE path to replace:"
|
||||
|
@ -1072,8 +935,8 @@ msgstr "Нічого не працює? Спробуйте повне скида
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39050"
|
||||
msgid "Choose Plex Server from a list"
|
||||
msgstr "Обрати сервер Plex зі списку"
|
||||
msgid "[COLOR yellow]Choose Plex Server from a list[/COLOR]"
|
||||
msgstr "[COLOR yellow]Оберіть сервер Plex зі списку[/COLOR]"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39051"
|
||||
|
@ -1102,17 +965,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 +1018,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"
|
||||
|
@ -1163,8 +1032,8 @@ msgstr "Ваш поточний Plex Media Server:"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39068"
|
||||
msgid "Manually enter Plex Media Server address"
|
||||
msgstr "Ввести вручну адресу сервера Plex"
|
||||
msgid "[COLOR yellow]Manually enter Plex Media Server address[/COLOR]"
|
||||
msgstr "[COLOR yellow]Ввести вручну адресу серверу Plex[/COLOR]"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39069"
|
||||
|
@ -1181,11 +1050,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 +1060,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 +1077,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"
|
||||
|
@ -1230,69 +1094,6 @@ msgstr ""
|
|||
"Plex Companion не може відкрити порт GDM. Будь ласка, змінить це у "
|
||||
"налаштуваннях PKC."
|
||||
|
||||
# Pop-up on initial sync.
|
||||
# Check that next translations for Add-on Paths and Direct Paths are
|
||||
# identical!
|
||||
msgctxt "#39080"
|
||||
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 "Шляхи додавань"
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39082"
|
||||
msgid "Direct Paths"
|
||||
msgstr "Прямі Шляхи"
|
||||
|
||||
# Dialog for manually entering PMS
|
||||
msgctxt "#39083"
|
||||
msgid "Enter PMS IP or URL"
|
||||
msgstr "Введіть адресу IP або посилання сервера Plex"
|
||||
|
||||
# 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"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "Вийти з профілю користувача Plex Home"
|
||||
|
@ -1301,6 +1102,10 @@ msgctxt "#39201"
|
|||
msgid "Settings"
|
||||
msgstr "Налаштування"
|
||||
|
||||
msgctxt "#39203"
|
||||
msgid "Refresh Plex playlists/nodes"
|
||||
msgstr "Оновлення плейлистів / вузлів Plex"
|
||||
|
||||
msgctxt "#39204"
|
||||
msgid "Perform manual library sync"
|
||||
msgstr "Виконати синхронізацію бібліотеки вручну"
|
||||
|
@ -1329,8 +1134,9 @@ msgstr "Збій скидання РКС. Спробуйте рестартув
|
|||
|
||||
# PKC Settings - Plex
|
||||
msgctxt "#39209"
|
||||
msgid "Toggle plex.tv login (sign in or sign out)"
|
||||
msgstr "Перемкнути логін plex.tv (увійти або вийти)"
|
||||
msgid "[COLOR yellow]Toggle plex.tv login (sign in or sign out)[/COLOR]"
|
||||
msgstr ""
|
||||
"[COLOR yellow]Переключити автентифікацію plex.tv (увійти або вийти)[/COLOR]"
|
||||
|
||||
msgctxt "#39210"
|
||||
msgid "Not yet connected to Plex Server"
|
||||
|
@ -1340,20 +1146,20 @@ msgctxt "#39211"
|
|||
msgid "Watch later"
|
||||
msgstr "Переглянути пізніше"
|
||||
|
||||
# Error message pop-up if {0} cannot be contacted. {0} will be replaced by
|
||||
# e.g. the PMS' name
|
||||
# String attached at the end to get something like "PMS Name is offline"
|
||||
msgctxt "#39213"
|
||||
msgid "{0} offline"
|
||||
msgstr "{0} не доступний"
|
||||
msgid "is offline"
|
||||
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."
|
||||
msgstr ""
|
||||
"Використовувати з'єднання HTTPS (SSL)? Відповідь, мабуть, має бути так."
|
||||
msgid ""
|
||||
"Does your Plex Media Server support SSL connections? (https instead of "
|
||||
"http)?"
|
||||
msgstr "Чи підтримує SSL з'єднання ваш сервер Plex? (https замість http)?"
|
||||
|
||||
msgctxt "#39218"
|
||||
msgid "Error contacting PMS"
|
||||
|
@ -1373,8 +1179,8 @@ msgid "plex.tv toggle successful"
|
|||
msgstr "перемикання plex.tv завершено"
|
||||
|
||||
msgctxt "#39222"
|
||||
msgid "Look for missing fanart on FanartTV now"
|
||||
msgstr "Шукати зараз відсутні фан-арти на FanartTV"
|
||||
msgid "[COLOR yellow]Look for missing fanart on FanartTV now[/COLOR]"
|
||||
msgstr "[COLOR yellow]Шукати зараз відсутній фан арт на FanartTV[/COLOR]"
|
||||
|
||||
msgctxt "#39223"
|
||||
msgid ""
|
||||
|
@ -1402,26 +1208,11 @@ msgctxt "#39227"
|
|||
msgid "Logged in to plex.tv"
|
||||
msgstr "Автентифіковано у plex.tv"
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
# Message in the PKC settings to display the plex.tv username. Leave the colon
|
||||
# :
|
||||
msgctxt "#39228"
|
||||
msgid "Plex admin user"
|
||||
msgstr "Адмініструючий користувач Plex"
|
||||
|
||||
# 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 для користувача"
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
msgctxt "#39230"
|
||||
msgid "Logged in Plex home user"
|
||||
msgstr "Залоговані у домівці користувачі Plex"
|
||||
|
||||
# Message in the PKC settings to change the logged in Plex home user
|
||||
msgctxt "#39231"
|
||||
msgid "Change logged in Plex home user"
|
||||
msgstr "Змінити залогованого користувача у домівці Plex"
|
||||
msgid "Plex user:"
|
||||
msgstr "Користувач Plex:"
|
||||
|
||||
msgctxt "#39250"
|
||||
msgid ""
|
||||
|
@ -1493,11 +1284,11 @@ msgstr "може працювати неправильно до скидання
|
|||
|
||||
msgctxt "#39403"
|
||||
msgid ""
|
||||
"The current Kodi version is not supported by PKC. Please consult the Plex "
|
||||
"forum."
|
||||
"Cancelling the database syncing process. Current Kodi version is "
|
||||
"unsupported. Please verify your logs for more info."
|
||||
msgstr ""
|
||||
"Поточна версія Kodi не підтримується PKC. Будь ласка, скористайтеся форумами"
|
||||
" Plex."
|
||||
"Синхронізація БД Kodi відмінена. Поточна версія Kodi не підтримується. Будь "
|
||||
"ласка, перевірте журнал для отримання детальної інформації."
|
||||
|
||||
msgctxt "#39405"
|
||||
msgid "Plex playlists/nodes refreshed"
|
||||
|
@ -1507,6 +1298,10 @@ msgctxt "#39406"
|
|||
msgid "Plex playlists/nodes refresh failed"
|
||||
msgstr "Збій оновлення плейлистів / вузлів Plex"
|
||||
|
||||
msgctxt "#39407"
|
||||
msgid "Full library sync finished"
|
||||
msgstr "Повна синхронізація бібліотеки завершена"
|
||||
|
||||
msgctxt "#39408"
|
||||
msgid ""
|
||||
"Sync had to skip some items because they could not be processed. Kodi may be"
|
||||
|
@ -1538,10 +1333,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 +1391,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 +1449,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}!
|
||||
|
|
|
@ -2,15 +2,13 @@
|
|||
# Translators:
|
||||
# Croneter None <croneter@gmail.com>, 2017
|
||||
# Tony Z <zj45499@gmail.com>, 2017
|
||||
# Jingen Chen <jingen.chen@gmail.com>, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PlexKodiConnect\n"
|
||||
"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: Jingen Chen <jingen.chen@gmail.com>, 2019\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Tony Z <zj45499@gmail.com>, 2017\n"
|
||||
"Language-Team: Chinese (China) (https://www.transifex.com/croneter/teams/73837/zh_CN/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
@ -29,7 +27,7 @@ msgstr "服务器地址(IP)"
|
|||
|
||||
msgctxt "#30001"
|
||||
msgid "Searching for PMS"
|
||||
msgstr "正在搜索Plex媒体服务器"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30002"
|
||||
msgid "Preferred playback method"
|
||||
|
@ -44,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 "用户名 "
|
||||
|
@ -58,22 +49,22 @@ msgstr "用户名 "
|
|||
# Sync notification displayed if there is still artwork to be cached to Kodi
|
||||
msgctxt "#30006"
|
||||
msgid "Caching %s Plex images"
|
||||
msgstr "正在缓存Plex图片:%s"
|
||||
msgstr ""
|
||||
|
||||
# Sync notification displayed if syncing of major artwork is done
|
||||
msgctxt "#30007"
|
||||
msgid "Plex image caching done"
|
||||
msgstr "Plex图片缓存完成"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings artwork: Enable notifications for artwork image sync
|
||||
msgctxt "#30008"
|
||||
msgid "Enable notifications for image caching"
|
||||
msgstr "启用图片缓存通知"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings artwork: Enable image caching during Kodi playback
|
||||
msgctxt "#30009"
|
||||
msgid "Enable image caching during Kodi playback (restart Kodi!)"
|
||||
msgstr "允许Kodi播放时缓存图片(需重启Kodi!)"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings - Artwork
|
||||
msgctxt "#30010"
|
||||
|
@ -99,9 +90,9 @@ msgctxt "#30014"
|
|||
msgid "Connection"
|
||||
msgstr "连接"
|
||||
|
||||
# Pop-up notification if user tried to manually initiate fanart download
|
||||
# PKC settings - Artwork
|
||||
msgctxt "#30015"
|
||||
msgid "Fanart download already running"
|
||||
msgid "Movie and show FanartTV lookups left to do:"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30016"
|
||||
|
@ -118,7 +109,7 @@ msgctxt "#30018"
|
|||
msgid "Checking FanartTV for %s items"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
# Sync notification displayed when FanartTV lookup is completed
|
||||
msgctxt "#30019"
|
||||
msgid "FanartTV lookup completed"
|
||||
msgstr ""
|
||||
|
@ -161,19 +152,6 @@ msgctxt "#30027"
|
|||
msgid "Prefix in Kodi playlist name to trigger sync"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
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 +251,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 "直接播放"
|
||||
|
@ -333,42 +307,6 @@ msgctxt "#30198"
|
|||
msgid "Search"
|
||||
msgstr "搜索"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30200"
|
||||
msgid ""
|
||||
"In the following window, enter the server's hostname (or IP) where your Plex"
|
||||
" media resides. Mind the case!"
|
||||
msgstr ""
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# for hostname
|
||||
msgctxt "#30201"
|
||||
msgid "Enter server hostname (or IP)"
|
||||
msgstr ""
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30202"
|
||||
msgid ""
|
||||
"In the following window, enter the network protocol you would like to use. "
|
||||
"This is likely 'smb'."
|
||||
msgstr ""
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# protocol
|
||||
msgctxt "#30203"
|
||||
msgid "Enter network protocol"
|
||||
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 ""
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30205"
|
||||
msgid "The protocol '{0}' that you entered is not supported."
|
||||
msgstr ""
|
||||
|
||||
# Video node naming for random e.g. movies
|
||||
msgctxt "#30227"
|
||||
msgid "Random"
|
||||
|
@ -480,12 +418,6 @@ msgctxt "#30416"
|
|||
msgid "Settings for the Plex Server"
|
||||
msgstr "Plex服务器配置"
|
||||
|
||||
msgctxt "#30417"
|
||||
msgid ""
|
||||
"Could not change the Kodi settings file {0}. PKC might not work correctly. "
|
||||
"Error: {1}"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30500"
|
||||
msgid "Verify Host SSL Certificate (more secure)"
|
||||
|
@ -496,15 +428,10 @@ msgctxt "#30501"
|
|||
msgid "Client SSL certificate"
|
||||
msgstr "客户端SSL证书"
|
||||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#30502"
|
||||
msgid "Sync Plex artwork from the PMS (recommended)"
|
||||
msgstr ""
|
||||
|
||||
# Message shown if SSL HTTPS certificate fails
|
||||
msgctxt "#30503"
|
||||
msgid "SSL certificate failed to validate. Please check {0} for solutions."
|
||||
msgstr ""
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30505"
|
||||
msgid "[COLOR yellow]Reset login attempts[/COLOR]"
|
||||
msgstr "[黄色]重置登录尝试[/COLOR]"
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#30506"
|
||||
|
@ -562,8 +489,8 @@ msgstr "回放"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30517"
|
||||
msgid "Set network credentials for Direct Paths and direct play"
|
||||
msgstr ""
|
||||
msgid "[COLOR yellow]Enter network credentials[/COLOR]"
|
||||
msgstr "[黄色]输入网络凭证[/COLOR]"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30518"
|
||||
|
@ -590,21 +517,6 @@ msgctxt "#30522"
|
|||
msgid "Force transcode h265/HEVC"
|
||||
msgstr "强制h265/HEVC转码"
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
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 ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -667,8 +579,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 +601,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,29 +621,14 @@ 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"
|
||||
msgstr "无效的用户名或密码"
|
||||
|
||||
msgctxt "#33010"
|
||||
msgid "User is unauthorized for server {0}"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#33011"
|
||||
msgid "Plex.tv did not provide us a valid list of Plex users, sorry."
|
||||
msgstr ""
|
||||
msgid "Failed to authenticate too many times. Reset in the settings."
|
||||
msgstr "验证失败次数过多。在设置中重置。"
|
||||
|
||||
# Dialog before playback
|
||||
msgctxt "#33013"
|
||||
|
@ -877,8 +759,8 @@ msgstr ""
|
|||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#39020"
|
||||
msgid "Cache all images to Kodi texture cache now"
|
||||
msgstr ""
|
||||
msgid "[COLOR yellow]Cache all images to Kodi texture cache now[/COLOR]"
|
||||
msgstr "[黄色]现在缓存所有图像到Kodi texture缓存[/COLOR]"
|
||||
|
||||
# Appended to a listed PMS if it is in the same LAN network as PKC
|
||||
msgctxt "#39022"
|
||||
|
@ -948,16 +830,6 @@ msgid ""
|
|||
"paths smb://NAS/mystuff"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
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:"
|
||||
|
@ -1031,8 +903,8 @@ msgstr "不起作用?试试完全重置!"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39050"
|
||||
msgid "Choose Plex Server from a list"
|
||||
msgstr ""
|
||||
msgid "[COLOR yellow]Choose Plex Server from a list[/COLOR]"
|
||||
msgstr "[黄色]从列表中选择Plex服务器[/COLOR]"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39051"
|
||||
|
@ -1061,15 +933,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 +985,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"
|
||||
|
@ -1119,8 +997,8 @@ msgstr "您当前的Plex媒体服务器:"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39068"
|
||||
msgid "Manually enter Plex Media Server address"
|
||||
msgstr ""
|
||||
msgid "[COLOR yellow]Manually enter Plex Media Server address[/COLOR]"
|
||||
msgstr "[黄色]手动输入Plex媒体服务器地址[/COLOR]"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39069"
|
||||
|
@ -1137,11 +1015,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 +1025,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 +1039,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"
|
||||
|
@ -1181,65 +1054,6 @@ msgid ""
|
|||
"settings."
|
||||
msgstr "Plex Companion无法打开GDM端口。请在PKC中更改。"
|
||||
|
||||
# Pop-up on initial sync.
|
||||
# Check that next translations for Add-on Paths and Direct Paths are
|
||||
# identical!
|
||||
msgctxt "#39080"
|
||||
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 ""
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39081"
|
||||
msgid "Add-on Paths"
|
||||
msgstr ""
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39082"
|
||||
msgid "Direct Paths"
|
||||
msgstr ""
|
||||
|
||||
# Dialog for manually entering PMS
|
||||
msgctxt "#39083"
|
||||
msgid "Enter PMS IP or URL"
|
||||
msgstr ""
|
||||
|
||||
# Dialog for manually entering PMS
|
||||
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家庭用户 "
|
||||
|
@ -1248,6 +1062,10 @@ msgctxt "#39201"
|
|||
msgid "Settings"
|
||||
msgstr "设置"
|
||||
|
||||
msgctxt "#39203"
|
||||
msgid "Refresh Plex playlists/nodes"
|
||||
msgstr "刷新Plex播放列表/节点"
|
||||
|
||||
msgctxt "#39204"
|
||||
msgid "Perform manual library sync"
|
||||
msgstr "执行手动库同步"
|
||||
|
@ -1273,8 +1091,8 @@ msgstr "重置PKC失败。尝试重启Kodi。"
|
|||
|
||||
# PKC Settings - Plex
|
||||
msgctxt "#39209"
|
||||
msgid "Toggle plex.tv login (sign in or sign out)"
|
||||
msgstr ""
|
||||
msgid "[COLOR yellow]Toggle plex.tv login (sign in or sign out)[/COLOR]"
|
||||
msgstr "[黄色]切换 plex.tv 登录 (登录或退出)[/COLOR]"
|
||||
|
||||
msgctxt "#39210"
|
||||
msgid "Not yet connected to Plex Server"
|
||||
|
@ -1284,19 +1102,20 @@ msgctxt "#39211"
|
|||
msgid "Watch later"
|
||||
msgstr "稍后观看"
|
||||
|
||||
# Error message pop-up if {0} cannot be contacted. {0} will be replaced by
|
||||
# e.g. the PMS' name
|
||||
# String attached at the end to get something like "PMS Name is offline"
|
||||
msgctxt "#39213"
|
||||
msgid "{0} offline"
|
||||
msgstr ""
|
||||
msgid "is offline"
|
||||
msgstr "处于离线状态"
|
||||
|
||||
msgctxt "#39215"
|
||||
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."
|
||||
msgstr ""
|
||||
msgid ""
|
||||
"Does your Plex Media Server support SSL connections? (https instead of "
|
||||
"http)?"
|
||||
msgstr "你Plex媒体服务器是否支持 SSL 连接?(https而不是 http)?"
|
||||
|
||||
msgctxt "#39218"
|
||||
msgid "Error contacting PMS"
|
||||
|
@ -1316,8 +1135,8 @@ msgid "plex.tv toggle successful"
|
|||
msgstr "plex.tv 切换成功"
|
||||
|
||||
msgctxt "#39222"
|
||||
msgid "Look for missing fanart on FanartTV now"
|
||||
msgstr ""
|
||||
msgid "[COLOR yellow]Look for missing fanart on FanartTV now[/COLOR]"
|
||||
msgstr "[黄色]从FanartTV 寻找缺失的fanart[/COLOR]"
|
||||
|
||||
msgctxt "#39223"
|
||||
msgid ""
|
||||
|
@ -1343,26 +1162,11 @@ msgctxt "#39227"
|
|||
msgid "Logged in to plex.tv"
|
||||
msgstr "plex.tv 登录成功"
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
# Message in the PKC settings to display the plex.tv username. Leave the colon
|
||||
# :
|
||||
msgctxt "#39228"
|
||||
msgid "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 ""
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
msgctxt "#39230"
|
||||
msgid "Logged in Plex home user"
|
||||
msgstr ""
|
||||
|
||||
# Message in the PKC settings to change the logged in Plex home user
|
||||
msgctxt "#39231"
|
||||
msgid "Change logged in Plex home user"
|
||||
msgstr ""
|
||||
msgid "Plex user:"
|
||||
msgstr "Plex 用户:"
|
||||
|
||||
msgctxt "#39250"
|
||||
msgid ""
|
||||
|
@ -1428,9 +1232,9 @@ msgstr " 重置数据库前可能无法正常运行。"
|
|||
|
||||
msgctxt "#39403"
|
||||
msgid ""
|
||||
"The current Kodi version is not supported by PKC. Please consult the Plex "
|
||||
"forum."
|
||||
msgstr ""
|
||||
"Cancelling the database syncing process. Current Kodi version is "
|
||||
"unsupported. Please verify your logs for more info."
|
||||
msgstr "正在取消数据库同步进程。不支持当前Kodi版本。请验证logs以获取更多信息。"
|
||||
|
||||
msgctxt "#39405"
|
||||
msgid "Plex playlists/nodes refreshed"
|
||||
|
@ -1440,6 +1244,10 @@ msgctxt "#39406"
|
|||
msgid "Plex playlists/nodes refresh failed"
|
||||
msgstr "Plex 播放列表/节点刷新失败"
|
||||
|
||||
msgctxt "#39407"
|
||||
msgid "Full library sync finished"
|
||||
msgstr "全库同步完成"
|
||||
|
||||
msgctxt "#39408"
|
||||
msgid ""
|
||||
"Sync had to skip some items because they could not be processed. Kodi may be"
|
||||
|
@ -1465,10 +1273,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 +1323,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 +1379,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
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
# XBMC Media Center language file
|
||||
# Translators:
|
||||
# Croneter None <croneter@gmail.com>, 2017
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PlexKodiConnect\n"
|
||||
"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"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Croneter None <croneter@gmail.com>, 2017\n"
|
||||
"Language-Team: Chinese (Taiwan) (https://www.transifex.com/croneter/teams/73837/zh_TW/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
@ -42,13 +41,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 "使用者: "
|
||||
|
@ -97,9 +89,9 @@ msgctxt "#30014"
|
|||
msgid "Connection"
|
||||
msgstr "連結"
|
||||
|
||||
# Pop-up notification if user tried to manually initiate fanart download
|
||||
# PKC settings - Artwork
|
||||
msgctxt "#30015"
|
||||
msgid "Fanart download already running"
|
||||
msgid "Movie and show FanartTV lookups left to do:"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30016"
|
||||
|
@ -116,7 +108,7 @@ msgctxt "#30018"
|
|||
msgid "Checking FanartTV for %s items"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
# Sync notification displayed when FanartTV lookup is completed
|
||||
msgctxt "#30019"
|
||||
msgid "FanartTV lookup completed"
|
||||
msgstr ""
|
||||
|
@ -159,19 +151,6 @@ msgctxt "#30027"
|
|||
msgid "Prefix in Kodi playlist name to trigger sync"
|
||||
msgstr ""
|
||||
|
||||
# PKC settings artwork options: status info
|
||||
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 +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 "直播"
|
||||
|
@ -331,42 +306,6 @@ msgctxt "#30198"
|
|||
msgid "Search"
|
||||
msgstr "搜尋"
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30200"
|
||||
msgid ""
|
||||
"In the following window, enter the server's hostname (or IP) where your Plex"
|
||||
" media resides. Mind the case!"
|
||||
msgstr ""
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# for hostname
|
||||
msgctxt "#30201"
|
||||
msgid "Enter server hostname (or IP)"
|
||||
msgstr ""
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30202"
|
||||
msgid ""
|
||||
"In the following window, enter the network protocol you would like to use. "
|
||||
"This is likely 'smb'."
|
||||
msgstr ""
|
||||
|
||||
# For setting up direct paths and adding network credentials - input window
|
||||
# protocol
|
||||
msgctxt "#30203"
|
||||
msgid "Enter network protocol"
|
||||
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 ""
|
||||
|
||||
# For setting up direct paths and adding network credentials
|
||||
msgctxt "#30205"
|
||||
msgid "The protocol '{0}' that you entered is not supported."
|
||||
msgstr ""
|
||||
|
||||
# Video node naming for random e.g. movies
|
||||
msgctxt "#30227"
|
||||
msgid "Random"
|
||||
|
@ -478,12 +417,6 @@ msgctxt "#30416"
|
|||
msgid "Settings for the Plex Server"
|
||||
msgstr "Plex 伺服器的設置"
|
||||
|
||||
msgctxt "#30417"
|
||||
msgid ""
|
||||
"Could not change the Kodi settings file {0}. PKC might not work correctly. "
|
||||
"Error: {1}"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30500"
|
||||
msgid "Verify Host SSL Certificate (more secure)"
|
||||
|
@ -494,15 +427,10 @@ msgctxt "#30501"
|
|||
msgid "Client SSL certificate"
|
||||
msgstr "用戶端 SSL 憑證"
|
||||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#30502"
|
||||
msgid "Sync Plex artwork from the PMS (recommended)"
|
||||
msgstr ""
|
||||
|
||||
# Message shown if SSL HTTPS certificate fails
|
||||
msgctxt "#30503"
|
||||
msgid "SSL certificate failed to validate. Please check {0} for solutions."
|
||||
msgstr ""
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30505"
|
||||
msgid "[COLOR yellow]Reset login attempts[/COLOR]"
|
||||
msgstr "[COLOR yellow]重置嘗試登入次數[/COLOR]"
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#30506"
|
||||
|
@ -560,8 +488,8 @@ msgstr "播放"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30517"
|
||||
msgid "Set network credentials for Direct Paths and direct play"
|
||||
msgstr ""
|
||||
msgid "[COLOR yellow]Enter network credentials[/COLOR]"
|
||||
msgstr "[COLOR yellow]輸入網路通行訊息[/COLOR]"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30518"
|
||||
|
@ -588,21 +516,6 @@ msgctxt "#30522"
|
|||
msgid "Force transcode h265/HEVC"
|
||||
msgstr "播放 h265/HEVC 時,強迫轉碼"
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
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 ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -665,8 +578,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 +600,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,29 +620,14 @@ 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"
|
||||
msgstr "無效的使用者名稱或密碼"
|
||||
|
||||
msgctxt "#33010"
|
||||
msgid "User is unauthorized for server {0}"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#33011"
|
||||
msgid "Plex.tv did not provide us a valid list of Plex users, sorry."
|
||||
msgstr ""
|
||||
msgid "Failed to authenticate too many times. Reset in the settings."
|
||||
msgstr "未能通過身份驗證的次數太多。請重置設定。"
|
||||
|
||||
# Dialog before playback
|
||||
msgctxt "#33013"
|
||||
|
@ -875,8 +758,8 @@ msgstr ""
|
|||
|
||||
# PKC Settings - Artwork
|
||||
msgctxt "#39020"
|
||||
msgid "Cache all images to Kodi texture cache now"
|
||||
msgstr ""
|
||||
msgid "[COLOR yellow]Cache all images to Kodi texture cache now[/COLOR]"
|
||||
msgstr "[COLOR yellow]立即暫存所有圖像到Kodi的圖像暫存區[/COLOR]"
|
||||
|
||||
# Appended to a listed PMS if it is in the same LAN network as PKC
|
||||
msgctxt "#39022"
|
||||
|
@ -946,16 +829,6 @@ msgid ""
|
|||
"paths smb://NAS/mystuff"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
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:"
|
||||
|
@ -1027,8 +900,8 @@ msgstr "完全掛點?試著完全重置 !"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39050"
|
||||
msgid "Choose Plex Server from a list"
|
||||
msgstr ""
|
||||
msgid "[COLOR yellow]Choose Plex Server from a list[/COLOR]"
|
||||
msgstr "[COLOR yellow]選取Plex伺服器[/COLOR]"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39051"
|
||||
|
@ -1057,15 +930,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 +982,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"
|
||||
|
@ -1115,8 +994,8 @@ msgstr "您當前的Plex媒體伺服器︰"
|
|||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39068"
|
||||
msgid "Manually enter Plex Media Server address"
|
||||
msgstr ""
|
||||
msgid "[COLOR yellow]Manually enter Plex Media Server address[/COLOR]"
|
||||
msgstr "[COLOR yellow]手動輸入Plex媒體伺服器位址[/COLOR]"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39069"
|
||||
|
@ -1133,11 +1012,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 +1022,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 +1036,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"
|
||||
|
@ -1177,65 +1051,6 @@ msgid ""
|
|||
"settings."
|
||||
msgstr "Plex Companion不能打開 GDM 埠。請在 PKC 設置中更改它。"
|
||||
|
||||
# Pop-up on initial sync.
|
||||
# Check that next translations for Add-on Paths and Direct Paths are
|
||||
# identical!
|
||||
msgctxt "#39080"
|
||||
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 ""
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39081"
|
||||
msgid "Add-on Paths"
|
||||
msgstr ""
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39082"
|
||||
msgid "Direct Paths"
|
||||
msgstr ""
|
||||
|
||||
# Dialog for manually entering PMS
|
||||
msgctxt "#39083"
|
||||
msgid "Enter PMS IP or URL"
|
||||
msgstr ""
|
||||
|
||||
# Dialog for manually entering PMS
|
||||
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用戶 "
|
||||
|
@ -1244,6 +1059,10 @@ msgctxt "#39201"
|
|||
msgid "Settings"
|
||||
msgstr "設置"
|
||||
|
||||
msgctxt "#39203"
|
||||
msgid "Refresh Plex playlists/nodes"
|
||||
msgstr "刷新Plex的播放清單/節點"
|
||||
|
||||
msgctxt "#39204"
|
||||
msgid "Perform manual library sync"
|
||||
msgstr "手動執行資料庫同步"
|
||||
|
@ -1269,8 +1088,8 @@ msgstr "重置 PKC 失敗。嘗試重開Kodi。"
|
|||
|
||||
# PKC Settings - Plex
|
||||
msgctxt "#39209"
|
||||
msgid "Toggle plex.tv login (sign in or sign out)"
|
||||
msgstr ""
|
||||
msgid "[COLOR yellow]Toggle plex.tv login (sign in or sign out)[/COLOR]"
|
||||
msgstr "[COLOR yellow]切換 plex.tv 帳號 (登錄或登出) [/COLOR]"
|
||||
|
||||
msgctxt "#39210"
|
||||
msgid "Not yet connected to Plex Server"
|
||||
|
@ -1280,19 +1099,20 @@ msgctxt "#39211"
|
|||
msgid "Watch later"
|
||||
msgstr "稍後再看"
|
||||
|
||||
# Error message pop-up if {0} cannot be contacted. {0} will be replaced by
|
||||
# e.g. the PMS' name
|
||||
# String attached at the end to get something like "PMS Name is offline"
|
||||
msgctxt "#39213"
|
||||
msgid "{0} offline"
|
||||
msgstr ""
|
||||
msgid "is offline"
|
||||
msgstr "處於離線狀態"
|
||||
|
||||
msgctxt "#39215"
|
||||
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."
|
||||
msgstr ""
|
||||
msgid ""
|
||||
"Does your Plex Media Server support SSL connections? (https instead of "
|
||||
"http)?"
|
||||
msgstr "你的Plex媒體伺服器是否支援 SSL 連線?(HTTPS 而不是 HTTP) ?"
|
||||
|
||||
msgctxt "#39218"
|
||||
msgid "Error contacting PMS"
|
||||
|
@ -1312,8 +1132,8 @@ msgid "plex.tv toggle successful"
|
|||
msgstr "plex.tv 切換成功"
|
||||
|
||||
msgctxt "#39222"
|
||||
msgid "Look for missing fanart on FanartTV now"
|
||||
msgstr ""
|
||||
msgid "[COLOR yellow]Look for missing fanart on FanartTV now[/COLOR]"
|
||||
msgstr "[COLOR yellow]由FanartTV搜尋漏失的背景海報中[/COLOR]"
|
||||
|
||||
msgctxt "#39223"
|
||||
msgid ""
|
||||
|
@ -1339,25 +1159,10 @@ msgctxt "#39227"
|
|||
msgid "Logged in to plex.tv"
|
||||
msgstr ""
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
# Message in the PKC settings to display the plex.tv username. Leave the colon
|
||||
# :
|
||||
msgctxt "#39228"
|
||||
msgid "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 ""
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
msgctxt "#39230"
|
||||
msgid "Logged in Plex home user"
|
||||
msgstr ""
|
||||
|
||||
# Message in the PKC settings to change the logged in Plex home user
|
||||
msgctxt "#39231"
|
||||
msgid "Change logged in Plex home user"
|
||||
msgid "Plex user:"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39250"
|
||||
|
@ -1424,9 +1229,9 @@ msgstr " 可能無法正常運行,直到重置資料庫。"
|
|||
|
||||
msgctxt "#39403"
|
||||
msgid ""
|
||||
"The current Kodi version is not supported by PKC. Please consult the Plex "
|
||||
"forum."
|
||||
msgstr ""
|
||||
"Cancelling the database syncing process. Current Kodi version is "
|
||||
"unsupported. Please verify your logs for more info."
|
||||
msgstr "取消資料庫同步程序。不支援當前kodi版本。請檢查您的日誌了解更多的資訊。"
|
||||
|
||||
msgctxt "#39405"
|
||||
msgid "Plex playlists/nodes refreshed"
|
||||
|
@ -1436,6 +1241,10 @@ msgctxt "#39406"
|
|||
msgid "Plex playlists/nodes refresh failed"
|
||||
msgstr "Plex 播放清單/節點刷新失敗"
|
||||
|
||||
msgctxt "#39407"
|
||||
msgid "Full library sync finished"
|
||||
msgstr "所有資料庫同步完成"
|
||||
|
||||
msgctxt "#39408"
|
||||
msgid ""
|
||||
"Sync had to skip some items because they could not be processed. Kodi may be"
|
||||
|
@ -1461,10 +1270,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 +1320,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 +1376,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
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Used to save PKC's application state and share between modules. Be careful
|
||||
if you invoke another PKC Python instance (!!) when e.g. PKC.movies is called
|
||||
"""
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from .account import Account
|
||||
from .application import App
|
||||
from .connection import Connection
|
||||
from .libsync import Sync
|
||||
from .playstate import PlayState
|
||||
|
||||
ACCOUNT = None
|
||||
APP = None
|
||||
CONN = None
|
||||
SYNC = None
|
||||
PLAYSTATE = None
|
||||
|
||||
|
||||
def init(entrypoint=False):
|
||||
"""
|
||||
entrypoint=True initiates only the bare minimum - for other PKC python
|
||||
instances
|
||||
"""
|
||||
global ACCOUNT, APP, CONN, SYNC, PLAYSTATE
|
||||
APP = App(entrypoint)
|
||||
CONN = Connection(entrypoint)
|
||||
ACCOUNT = Account(entrypoint)
|
||||
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()
|
|
@ -1,147 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from .. import utils
|
||||
|
||||
LOG = getLogger('PLEX.account')
|
||||
|
||||
|
||||
class Account(object):
|
||||
def __init__(self, entrypoint=False):
|
||||
self.plex_login = None
|
||||
self.plex_login_id = None
|
||||
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
|
||||
self.restricted_user = None
|
||||
self.force_login = None
|
||||
self._session = None
|
||||
self.authenticated = False
|
||||
if entrypoint:
|
||||
self.load_entrypoint()
|
||||
else:
|
||||
utils.window('plex_authenticated', clear=True)
|
||||
self.load()
|
||||
|
||||
def set_authenticated(self):
|
||||
self.authenticated = True
|
||||
utils.window('plex_authenticated', value='true')
|
||||
|
||||
def set_unauthenticated(self):
|
||||
self.authenticated = False
|
||||
utils.window('plex_authenticated', clear=True)
|
||||
|
||||
def reset_session(self):
|
||||
try:
|
||||
self._session.stopSession()
|
||||
except AttributeError:
|
||||
pass
|
||||
from .. import downloadutils
|
||||
self._session = downloadutils.DownloadUtils()
|
||||
self._session.startSession(reset=True)
|
||||
|
||||
def load(self):
|
||||
LOG.debug('Loading account settings')
|
||||
# User name we used to sign in to plex.tv
|
||||
self.plex_login = utils.settings('plexLogin') or None
|
||||
self.plex_login_id = utils.settings('plexid') or None
|
||||
# plex.tv username
|
||||
self.plex_username = utils.settings('username') or None
|
||||
# Plex ID of that user (e.g. for plex.tv) as a STRING
|
||||
self.plex_user_id = utils.settings('userid') or None
|
||||
# Token for that user for plex.tv
|
||||
self.plex_token = utils.settings('plexToken') or None
|
||||
# Plex token for the active PMS for the active user
|
||||
# (might be diffent to plex_token)
|
||||
self.pms_token = utils.settings('accessToken') or None
|
||||
self.avatar = utils.settings('plexAvatar') or None
|
||||
self.myplexlogin = utils.settings('myplexlogin') == 'true'
|
||||
|
||||
# Plex home user? Then "False"
|
||||
self.restricted_user = utils.settings('plex_restricteduser') == 'true'
|
||||
# Force user to enter Pin if set?
|
||||
self.force_login = utils.settings('enforceUserLogin') == 'true'
|
||||
|
||||
# Also load these settings to Kodi window variables - they'll be
|
||||
# available for other PKC Python instances
|
||||
utils.window('plex_restricteduser',
|
||||
value='true' if self.restricted_user else 'false')
|
||||
utils.window('plex_token', value=self.plex_token or '')
|
||||
utils.window('pms_token', value=self.pms_token or '')
|
||||
utils.window('plexAvatar', value=self.avatar or '')
|
||||
|
||||
# Start download session
|
||||
self.reset_session()
|
||||
|
||||
LOG.debug('Loaded user %s, %s with plex token %s... and pms token %s...',
|
||||
self.plex_username, self.plex_user_id,
|
||||
self.plex_token[:5] if self.plex_token else None,
|
||||
self.pms_token[:5] if self.pms_token else None)
|
||||
LOG.debug('User is restricted Home user: %s', self.restricted_user)
|
||||
|
||||
def load_entrypoint(self):
|
||||
self.pms_token = utils.settings('accessToken') or None
|
||||
|
||||
def log_out(self):
|
||||
LOG.debug('Logging-out user %s', self.plex_username)
|
||||
self.plex_username = None
|
||||
self.plex_user_id = None
|
||||
self.pms_token = None
|
||||
self.avatar = None
|
||||
self.restricted_user = None
|
||||
self.authenticated = False
|
||||
try:
|
||||
self._session.stopSession()
|
||||
except AttributeError:
|
||||
pass
|
||||
self._session = None
|
||||
|
||||
utils.settings('username', value='')
|
||||
utils.settings('userid', value='')
|
||||
utils.settings('plex_restricteduser', value='')
|
||||
utils.settings('accessToken', value='')
|
||||
utils.settings('plexAvatar', value='')
|
||||
|
||||
utils.window('plex_restricteduser', clear=True)
|
||||
utils.window('pms_token', clear=True)
|
||||
utils.window('plexAvatar', clear=True)
|
||||
utils.window('plex_authenticated', clear=True)
|
||||
|
||||
def clear(self):
|
||||
LOG.debug('Clearing account settings')
|
||||
self.plex_username = None
|
||||
self.plex_user_id = None
|
||||
self.plex_token = None
|
||||
self.pms_token = None
|
||||
self.avatar = None
|
||||
self.restricted_user = None
|
||||
self.authenticated = False
|
||||
self.plex_login = None
|
||||
self.plex_login_id = None
|
||||
try:
|
||||
self._session.stopSession()
|
||||
except AttributeError:
|
||||
pass
|
||||
self._session = None
|
||||
|
||||
utils.settings('username', value='')
|
||||
utils.settings('userid', value='')
|
||||
utils.settings('plex_restricteduser', value='')
|
||||
utils.settings('plexToken', value='')
|
||||
utils.settings('accessToken', value='')
|
||||
utils.settings('plexAvatar', value='')
|
||||
utils.settings('plexLogin', value='')
|
||||
utils.settings('plexid', value='')
|
||||
|
||||
utils.window('plex_restricteduser', clear=True)
|
||||
utils.window('plex_token', clear=True)
|
||||
utils.window('pms_token', clear=True)
|
||||
utils.window('plexAvatar', clear=True)
|
||||
utils.window('plex_authenticated', clear=True)
|
|
@ -1,173 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import Queue
|
||||
from threading import Lock, RLock
|
||||
|
||||
import xbmc
|
||||
|
||||
from .. import utils
|
||||
|
||||
LOG = getLogger('PLEX.app')
|
||||
|
||||
|
||||
class App(object):
|
||||
"""
|
||||
This class is used to store variables across PKC modules
|
||||
"""
|
||||
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()
|
||||
# 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
|
||||
self.lock_playqueues = RLock()
|
||||
# Necessary to temporarily hold back librarysync/websocket listener when doing
|
||||
# a full sync
|
||||
self.lock_playlists = Lock()
|
||||
|
||||
# Plex Companion Queue()
|
||||
self.companion_queue = Queue.Queue(maxsize=100)
|
||||
# Websocket_client queue to communicate with librarysync
|
||||
self.websocket_queue = Queue.Queue()
|
||||
# xbmc.Monitor() instance from kodimonitor.py
|
||||
self.monitor = None
|
||||
# xbmc.Player() instance
|
||||
self.player = None
|
||||
# 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
|
||||
|
||||
@property
|
||||
def is_playing_video(self):
|
||||
return self.player.isPlayingVideo() == 1
|
||||
|
||||
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)
|
||||
|
||||
def suspend_fanart_thread(self, block=True):
|
||||
try:
|
||||
self.fanart_thread.suspend(block=block)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def resume_fanart_thread(self):
|
||||
try:
|
||||
self.fanart_thread.resume()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def register_caching_thread(self, thread):
|
||||
self.caching_thread = thread
|
||||
self.threads.append(thread)
|
||||
|
||||
def deregister_caching_thread(self, thread):
|
||||
self.caching_thread.unblock_callers()
|
||||
self.caching_thread = None
|
||||
self.threads.remove(thread)
|
||||
|
||||
def suspend_caching_thread(self, block=True):
|
||||
try:
|
||||
self.caching_thread.suspend(block=block)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def resume_caching_thread(self):
|
||||
try:
|
||||
self.caching_thread.resume()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def register_thread(self, thread):
|
||||
"""
|
||||
Hit with thread [backgroundthread.Killablethread instance] to register
|
||||
any and all threads
|
||||
"""
|
||||
self.threads.append(thread)
|
||||
|
||||
def deregister_thread(self, thread):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Suspend all threads' activity with or without blocking.
|
||||
Returns True only if PKC shutdown requested
|
||||
"""
|
||||
LOG.debug('Suspending threads: %s', self.threads)
|
||||
for thread in self.threads:
|
||||
thread.suspend()
|
||||
if block:
|
||||
while True:
|
||||
for thread in self.threads:
|
||||
if not thread.is_suspended():
|
||||
LOG.debug('Waiting for thread to suspend: %s', thread)
|
||||
# Send suspend signal again in case self.threads
|
||||
# changed
|
||||
thread.suspend(block=True)
|
||||
else:
|
||||
break
|
||||
return self.monitor.abortRequested()
|
||||
|
||||
def resume_threads(self):
|
||||
"""
|
||||
Resume all thread activity with or without blocking.
|
||||
Returns True only if PKC shutdown requested
|
||||
"""
|
||||
LOG.debug('Resuming threads: %s', self.threads)
|
||||
for thread in self.threads:
|
||||
thread.resume()
|
||||
return self.monitor.abortRequested()
|
||||
|
||||
def stop_threads(self, block=True):
|
||||
"""
|
||||
Stop all threads. Will block until all threads are stopped
|
||||
Will NOT quit if PKC should exit!
|
||||
"""
|
||||
LOG.debug('Killing threads: %s', self.threads)
|
||||
for thread in self.threads:
|
||||
thread.cancel()
|
||||
if block:
|
||||
while self.threads:
|
||||
LOG.debug('Waiting for threads to exit: %s', self.threads)
|
||||
if xbmc.sleep(100):
|
||||
return True
|
||||
|
||||
def reload(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
|
||||
# before
|
||||
self.force_reload_skin = utils.settings('forceReloadSkinOnPlaybackStop') == 'true'
|
||||
|
||||
def load_entrypoint(self):
|
||||
self.fetch_pms_item_number = int(utils.settings('fetch_pms_item_number'))
|
|
@ -1,98 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from .. import utils, json_rpc as js, variables as v
|
||||
|
||||
LOG = getLogger('PLEX.connection')
|
||||
|
||||
|
||||
class Connection(object):
|
||||
def __init__(self, entrypoint=False):
|
||||
self.verify_ssl_cert = None
|
||||
self.ssl_cert_path = None
|
||||
self.machine_identifier = None
|
||||
self.server_name = None
|
||||
self.https = None
|
||||
self.host = None
|
||||
self.port = None
|
||||
self.server = None
|
||||
self.online = False
|
||||
self.webserver_host = None
|
||||
self.webserver_port = None
|
||||
self.webserver_username = None
|
||||
self.webserver_password = None
|
||||
|
||||
if entrypoint:
|
||||
self.load_entrypoint()
|
||||
else:
|
||||
self.load_webserver()
|
||||
self.load()
|
||||
# Token passed along, e.g. if playback initiated by Plex Companion. Might be
|
||||
# another user playing something! Token identifies user
|
||||
self.plex_transient_token = None
|
||||
|
||||
def load_webserver(self):
|
||||
"""
|
||||
PKC needs Kodi webserver to work correctly
|
||||
"""
|
||||
LOG.debug('Loading Kodi webserver details')
|
||||
# Kodi webserver details
|
||||
if js.get_setting('services.webserver') in (None, False):
|
||||
# Enable the webserver, it is disabled
|
||||
js.set_setting('services.webserver', True)
|
||||
self.webserver_host = 'localhost'
|
||||
self.webserver_port = js.get_setting('services.webserverport')
|
||||
self.webserver_username = js.get_setting('services.webserverusername')
|
||||
self.webserver_password = js.get_setting('services.webserverpassword')
|
||||
|
||||
def load(self):
|
||||
LOG.debug('Loading connection settings')
|
||||
# Shall we verify SSL certificates? "None" will leave SSL enabled
|
||||
# Ignore this setting for Kodi >= 18 as Kodi 18 is much stricter
|
||||
# with checking SSL certs
|
||||
self.verify_ssl_cert = None if v.KODIVERSION >= 18 or utils.settings('sslverify') == 'true' \
|
||||
else False
|
||||
# Do we have an ssl certificate for PKC we need to use?
|
||||
self.ssl_cert_path = utils.settings('sslcert') \
|
||||
if utils.settings('sslcert') != 'None' else None
|
||||
|
||||
self.machine_identifier = utils.settings('plex_machineIdentifier') or None
|
||||
self.server_name = utils.settings('plex_servername') or None
|
||||
self.https = utils.settings('https') == 'true'
|
||||
self.host = utils.settings('ipaddress') or None
|
||||
self.port = int(utils.settings('port')) if utils.settings('port') else None
|
||||
if not self.host:
|
||||
self.server = None
|
||||
elif self.https:
|
||||
self.server = 'https://%s:%s' % (self.host, self.port)
|
||||
else:
|
||||
self.server = 'http://%s:%s' % (self.host, self.port)
|
||||
self.online = False
|
||||
LOG.debug('Set server %s (%s) to %s',
|
||||
self.server_name, self.machine_identifier, self.server)
|
||||
|
||||
def load_entrypoint(self):
|
||||
self.verify_ssl_cert = None if v.KODIVERSION >= 18 or utils.settings('sslverify') == 'true' \
|
||||
else False
|
||||
self.ssl_cert_path = utils.settings('sslcert') \
|
||||
if utils.settings('sslcert') != 'None' else None
|
||||
self.https = utils.settings('https') == 'true'
|
||||
self.host = utils.settings('ipaddress') or None
|
||||
self.port = int(utils.settings('port')) if utils.settings('port') else None
|
||||
if not self.host:
|
||||
self.server = None
|
||||
elif self.https:
|
||||
self.server = 'https://%s:%s' % (self.host, self.port)
|
||||
else:
|
||||
self.server = 'http://%s:%s' % (self.host, self.port)
|
||||
|
||||
def clear(self):
|
||||
LOG.debug('Clearing connection settings')
|
||||
self.machine_identifier = None
|
||||
self.server_name = None
|
||||
self.https = None
|
||||
self.host = None
|
||||
self.port = None
|
||||
self.server = None
|
|
@ -1,130 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
|
||||
from .. import utils
|
||||
|
||||
|
||||
def remove_trailing_slash(path):
|
||||
"""
|
||||
Removes trailing slashes or backslashes from path [unicode], and is NOT
|
||||
dependent on os.path
|
||||
"""
|
||||
if '/' in path:
|
||||
path = path[:-1] if path.endswith('/') else path
|
||||
else:
|
||||
path = path[:-1] if path.endswith('\\') else path
|
||||
return path
|
||||
|
||||
|
||||
class Sync(object):
|
||||
def __init__(self, entrypoint=False):
|
||||
# Direct Paths (True) or Addon Paths (False)?
|
||||
self.direct_paths = None
|
||||
# Is synching of Plex music enabled?
|
||||
self.enable_music = None
|
||||
# Do we sync artwork from the PMS to Kodi?
|
||||
self.artwork = None
|
||||
# Path remapping mechanism (e.g. smb paths)
|
||||
# Do we replace \\myserver\path to smb://myserver/path?
|
||||
self.replace_smb_path = None
|
||||
# Do we generally remap?
|
||||
self.remap_path = None
|
||||
self.force_transcode_pix = None
|
||||
# Mappings for REMAP_PATH:
|
||||
self.remapSMBmovieOrg = None
|
||||
self.remapSMBmovieNew = None
|
||||
self.remapSMBtvOrg = None
|
||||
self.remapSMBtvNew = None
|
||||
self.remapSMBmusicOrg = None
|
||||
self.remapSMBmusicNew = None
|
||||
self.remapSMBphotoOrg = None
|
||||
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
|
||||
# all the Plex extras for the user to choose
|
||||
self.show_extras_instead_of_playing_trailer = None
|
||||
# Only sync specific Plex playlists to Kodi?
|
||||
self.sync_specific_plex_playlists = None
|
||||
# Only sync specific Kodi playlists to Plex?
|
||||
self.sync_specific_kodi_playlists = None
|
||||
# Shall we show Kodi dialogs when synching?
|
||||
self.sync_dialog = None
|
||||
|
||||
# How often shall we sync?
|
||||
self.full_sync_intervall = None
|
||||
# How long shall we wait with synching a new item to make sure Plex got all
|
||||
# metadata?
|
||||
self.backgroundsync_saftymargin = None
|
||||
# How many threads to download Plex metadata on sync?
|
||||
self.sync_thread_number = None
|
||||
|
||||
# Shall Kodi show dialogs for syncing/caching images? (e.g. images left
|
||||
# to sync)
|
||||
self.image_sync_notifications = None
|
||||
|
||||
# Do we need to run a special library scan?
|
||||
self.run_lib_scan = None
|
||||
# Set if user decided to cancel sync
|
||||
self.stop_sync = False
|
||||
# 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.remapSMBmovieOrg = remove_trailing_slash(utils.settings('remapSMBmovieOrg'))
|
||||
self.remapSMBmovieNew = remove_trailing_slash(utils.settings('remapSMBmovieNew'))
|
||||
self.remapSMBtvOrg = remove_trailing_slash(utils.settings('remapSMBtvOrg'))
|
||||
self.remapSMBtvNew = remove_trailing_slash(utils.settings('remapSMBtvNew'))
|
||||
self.remapSMBmusicOrg = remove_trailing_slash(utils.settings('remapSMBmusicOrg'))
|
||||
self.remapSMBmusicNew = remove_trailing_slash(utils.settings('remapSMBmusicNew'))
|
||||
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.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.backgroundsync_saftymargin = int(utils.settings('backgroundsync_saftyMargin'))
|
||||
|
||||
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'
|
|
@ -1,66 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
|
||||
|
||||
class PlayState(object):
|
||||
# "empty" dict for the PLAYER_STATES above. Use copy.deepcopy to duplicate!
|
||||
template = {
|
||||
'type': None,
|
||||
'time': {
|
||||
'hours': 0,
|
||||
'minutes': 0,
|
||||
'seconds': 0,
|
||||
'milliseconds': 0},
|
||||
'totaltime': {
|
||||
'hours': 0,
|
||||
'minutes': 0,
|
||||
'seconds': 0,
|
||||
'milliseconds': 0},
|
||||
'speed': 0,
|
||||
'shuffled': False,
|
||||
'repeat': 'off',
|
||||
'position': None,
|
||||
'playlistid': None,
|
||||
'currentvideostream': -1,
|
||||
'currentaudiostream': -1,
|
||||
'subtitleenabled': False,
|
||||
'currentsubtitle': -1,
|
||||
'file': None,
|
||||
'kodi_id': None,
|
||||
'kodi_type': None,
|
||||
'plex_id': None,
|
||||
'plex_type': None,
|
||||
'container_key': None,
|
||||
'volume': 100,
|
||||
'muted': False,
|
||||
'playmethod': None,
|
||||
'playcount': None,
|
||||
'external_player': False, # bool - xbmc.Player().isExternalPlayer()
|
||||
'intro_markers': [],
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
# Kodi player states - here, initial values are set
|
||||
self.player_states = {
|
||||
0: {},
|
||||
1: {},
|
||||
2: {}
|
||||
}
|
||||
# The LAST playstate once playback is finished
|
||||
self.old_player_states = {
|
||||
0: {},
|
||||
1: {},
|
||||
2: {}
|
||||
}
|
||||
self.played_info = {}
|
||||
|
||||
# Currently playing PKC item, a PlaylistItem()
|
||||
self.item = None
|
||||
|
||||
# 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)
|
||||
self.active_players = set()
|
|
@ -2,138 +2,328 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
from Queue import Queue, Empty
|
||||
from urllib import quote_plus, unquote
|
||||
from threading import Thread
|
||||
import requests
|
||||
|
||||
from .kodi_db import KodiVideoDB, KodiMusicDB, KodiTextureDB
|
||||
from . import app, backgroundthread, utils
|
||||
import xbmc
|
||||
|
||||
from . import path_ops
|
||||
from . import utils
|
||||
from . import state
|
||||
|
||||
###############################################################################
|
||||
LOG = getLogger('PLEX.artwork')
|
||||
|
||||
# Disable annoying requests warnings
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
ARTWORK_QUEUE = Queue()
|
||||
IMAGE_CACHING_SUSPENDS = ['SUSPEND_LIBRARY_THREAD', 'DB_SCAN', 'STOP_SYNC']
|
||||
if not utils.settings('imageSyncDuringPlayback') == 'true':
|
||||
IMAGE_CACHING_SUSPENDS.append('SUSPEND_SYNC')
|
||||
|
||||
# Potentially issues with limited number of threads Hence let Kodi wait till
|
||||
# download is successful
|
||||
TIMEOUT = (35.1, 35.1)
|
||||
BATCH_SIZE = 500
|
||||
###############################################################################
|
||||
|
||||
|
||||
def double_urlencode(text):
|
||||
return utils.quote_plus(utils.quote_plus(text))
|
||||
return quote_plus(quote_plus(text))
|
||||
|
||||
|
||||
def double_urldecode(text):
|
||||
return utils.unquote(utils.unquote(text))
|
||||
return unquote(unquote(text))
|
||||
|
||||
|
||||
class ImageCachingThread(backgroundthread.KillableThread):
|
||||
@utils.thread_methods(add_suspends=IMAGE_CACHING_SUSPENDS)
|
||||
class Image_Cache_Thread(Thread):
|
||||
sleep_between = 50
|
||||
# Potentially issues with limited number of threads
|
||||
# Hence let Kodi wait till download is successful
|
||||
timeout = (35.1, 35.1)
|
||||
|
||||
def __init__(self):
|
||||
super(ImageCachingThread, self).__init__()
|
||||
self.suspend_points = [(self, '_suspended')]
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
def _url_generator(kind, kodi_type):
|
||||
"""
|
||||
Main goal is to close DB connection between calls
|
||||
"""
|
||||
offset = 0
|
||||
i = 0
|
||||
while True:
|
||||
batch = []
|
||||
with kind(texture_db=True) as kodidb:
|
||||
texture_db = KodiTextureDB(kodiconn=kodidb.kodiconn,
|
||||
artconn=kodidb.artconn,
|
||||
lock=False)
|
||||
for i, url in enumerate(kodidb.artwork_generator(kodi_type,
|
||||
BATCH_SIZE,
|
||||
offset)):
|
||||
if texture_db.url_not_yet_cached(url):
|
||||
batch.append(url)
|
||||
if len(batch) == BATCH_SIZE:
|
||||
break
|
||||
offset += i
|
||||
for url in batch:
|
||||
yield url
|
||||
if i + 1 < BATCH_SIZE:
|
||||
break
|
||||
self.queue = ARTWORK_QUEUE
|
||||
Thread.__init__(self)
|
||||
|
||||
def run(self):
|
||||
LOG.info("---===### Starting ImageCachingThread ###===---")
|
||||
app.APP.register_caching_thread(self)
|
||||
LOG.info("---===### Starting Image_Cache_Thread ###===---")
|
||||
stopped = self.stopped
|
||||
suspended = self.suspended
|
||||
queue = self.queue
|
||||
sleep_between = self.sleep_between
|
||||
while not stopped():
|
||||
# In the event the server goes offline
|
||||
while suspended():
|
||||
# Set in service.py
|
||||
if stopped():
|
||||
# Abort was requested while waiting. We should exit
|
||||
LOG.info("---===### Stopped Image_Cache_Thread ###===---")
|
||||
return
|
||||
xbmc.sleep(1000)
|
||||
|
||||
try:
|
||||
url = queue.get(block=False)
|
||||
except Empty:
|
||||
xbmc.sleep(1000)
|
||||
continue
|
||||
if isinstance(url, ArtworkSyncMessage):
|
||||
if state.IMAGE_SYNC_NOTIFICATIONS:
|
||||
utils.dialog('notification',
|
||||
heading=utils.lang(29999),
|
||||
message=url.message,
|
||||
icon='{plex}',
|
||||
sound=False)
|
||||
queue.task_done()
|
||||
continue
|
||||
url = double_urlencode(utils.try_encode(url))
|
||||
sleeptime = 0
|
||||
while True:
|
||||
try:
|
||||
requests.head(
|
||||
url="http://%s:%s/image/image://%s"
|
||||
% (state.WEBSERVER_HOST,
|
||||
state.WEBSERVER_PORT,
|
||||
url),
|
||||
auth=(state.WEBSERVER_USERNAME,
|
||||
state.WEBSERVER_PASSWORD),
|
||||
timeout=self.timeout)
|
||||
except requests.Timeout:
|
||||
# We don't need the result, only trigger Kodi to start the
|
||||
# download. All is well
|
||||
break
|
||||
except requests.ConnectionError:
|
||||
if stopped():
|
||||
# Kodi terminated
|
||||
break
|
||||
# Server thinks its a DOS attack, ('error 10053')
|
||||
# Wait before trying again
|
||||
if sleeptime > 5:
|
||||
LOG.error('Repeatedly got ConnectionError for url %s',
|
||||
double_urldecode(url))
|
||||
break
|
||||
LOG.debug('Were trying too hard to download art, server '
|
||||
'over-loaded. Sleep %s seconds before trying '
|
||||
'again to download %s',
|
||||
2**sleeptime, double_urldecode(url))
|
||||
xbmc.sleep((2**sleeptime) * 1000)
|
||||
sleeptime += 1
|
||||
continue
|
||||
except Exception as err:
|
||||
LOG.error('Unknown exception for url %s: %s'.
|
||||
double_urldecode(url), err)
|
||||
import traceback
|
||||
LOG.error("Traceback:\n%s", traceback.format_exc())
|
||||
break
|
||||
# We did not even get a timeout
|
||||
break
|
||||
queue.task_done()
|
||||
# Sleep for a bit to reduce CPU strain
|
||||
xbmc.sleep(sleep_between)
|
||||
LOG.info("---===### Stopped Image_Cache_Thread ###===---")
|
||||
|
||||
|
||||
class Artwork():
|
||||
enableTextureCache = utils.settings('enableTextureCache') == "true"
|
||||
if enableTextureCache:
|
||||
queue = ARTWORK_QUEUE
|
||||
|
||||
def cache_major_artwork(self):
|
||||
"""
|
||||
Takes the existing Kodi library and caches posters and fanart.
|
||||
Necessary because otherwise PKC caches artwork e.g. from fanart.tv
|
||||
which basically blocks Kodi from getting needed artwork fast (e.g.
|
||||
while browsing the library)
|
||||
"""
|
||||
if not self.enableTextureCache:
|
||||
return
|
||||
artworks = list()
|
||||
# Get all posters and fanart/background for video and music
|
||||
for kind in ('video', 'music'):
|
||||
connection = utils.kodi_sql(kind)
|
||||
cursor = connection.cursor()
|
||||
for typus in ('poster', 'fanart'):
|
||||
cursor.execute('SELECT url FROM art WHERE type == ?',
|
||||
(typus, ))
|
||||
artworks.extend(cursor.fetchall())
|
||||
connection.close()
|
||||
artworks_to_cache = list()
|
||||
connection = utils.kodi_sql('texture')
|
||||
cursor = connection.cursor()
|
||||
for url in artworks:
|
||||
query = 'SELECT url FROM texture WHERE url == ? LIMIT 1'
|
||||
cursor.execute(query, (url[0], ))
|
||||
if not cursor.fetchone():
|
||||
artworks_to_cache.append(url)
|
||||
connection.close()
|
||||
if not artworks_to_cache:
|
||||
LOG.info('Caching of major images to Kodi texture cache done')
|
||||
return
|
||||
length = len(artworks_to_cache)
|
||||
LOG.info('Caching has not been completed - caching %s major images',
|
||||
length)
|
||||
# Caching %s Plex images
|
||||
self.queue.put(ArtworkSyncMessage(utils.lang(30006) % length))
|
||||
for url in artworks_to_cache:
|
||||
self.queue.put(url[0])
|
||||
# Plex image caching done
|
||||
self.queue.put(ArtworkSyncMessage(utils.lang(30007)))
|
||||
|
||||
def fullTextureCacheSync(self):
|
||||
"""
|
||||
This method will sync all Kodi artwork to textures13.db
|
||||
and cache them locally. This takes diskspace!
|
||||
"""
|
||||
if not utils.yesno_dialog("Image Texture Cache", utils.lang(39250)):
|
||||
return
|
||||
|
||||
LOG.info("Doing Image Cache Sync")
|
||||
|
||||
# ask to rest all existing or not
|
||||
if utils.yesno_dialog("Image Texture Cache", utils.lang(39251)):
|
||||
LOG.info("Resetting all cache data first")
|
||||
# Remove all existing textures first
|
||||
path = path_ops.translate_path('special://thumbnails/')
|
||||
if path_ops.exists(path):
|
||||
path_ops.rmtree(path, ignore_errors=True)
|
||||
self.restore_cache_directories()
|
||||
|
||||
# remove all existing data from texture DB
|
||||
connection = utils.kodi_sql('texture')
|
||||
cursor = connection.cursor()
|
||||
query = 'SELECT tbl_name FROM sqlite_master WHERE type=?'
|
||||
cursor.execute(query, ('table', ))
|
||||
rows = cursor.fetchall()
|
||||
for row in rows:
|
||||
tableName = row[0]
|
||||
if tableName != "version":
|
||||
cursor.execute("DELETE FROM %s" % tableName)
|
||||
connection.commit()
|
||||
connection.close()
|
||||
|
||||
# Cache all entries in video DB
|
||||
connection = utils.kodi_sql('video')
|
||||
cursor = connection.cursor()
|
||||
# dont include actors
|
||||
query = "SELECT url FROM art WHERE media_type != ?"
|
||||
cursor.execute(query, ('actor', ))
|
||||
result = cursor.fetchall()
|
||||
total = len(result)
|
||||
LOG.info("Image cache sync about to process %s video images", total)
|
||||
connection.close()
|
||||
|
||||
for url in result:
|
||||
self.cache_texture(url[0])
|
||||
# Cache all entries in music DB
|
||||
connection = utils.kodi_sql('music')
|
||||
cursor = connection.cursor()
|
||||
cursor.execute("SELECT url FROM art")
|
||||
result = cursor.fetchall()
|
||||
total = len(result)
|
||||
LOG.info("Image cache sync about to process %s music images", total)
|
||||
connection.close()
|
||||
for url in result:
|
||||
self.cache_texture(url[0])
|
||||
|
||||
def cache_texture(self, url):
|
||||
'''
|
||||
Cache a single image url to the texture cache. url: unicode
|
||||
'''
|
||||
if url and self.enableTextureCache:
|
||||
self.queue.put(url)
|
||||
|
||||
def modify_artwork(self, artworks, kodi_id, kodi_type, cursor):
|
||||
"""
|
||||
Pass in an artworks dict (see PlexAPI) to set an items artwork.
|
||||
"""
|
||||
for kodi_art, url in artworks.iteritems():
|
||||
self.modify_art(url, kodi_id, kodi_type, kodi_art, cursor)
|
||||
|
||||
def modify_art(self, url, kodi_id, kodi_type, kodi_art, cursor):
|
||||
"""
|
||||
Adds or modifies the artwork of kind kodi_art (e.g. 'poster') in the
|
||||
Kodi art table for item kodi_id/kodi_type. Will also cache everything
|
||||
except actor portraits.
|
||||
"""
|
||||
query = '''
|
||||
SELECT url FROM art
|
||||
WHERE media_id = ? AND media_type = ? AND type = ?
|
||||
LIMIT 1
|
||||
'''
|
||||
cursor.execute(query, (kodi_id, kodi_type, kodi_art,))
|
||||
try:
|
||||
self._run()
|
||||
except Exception:
|
||||
utils.ERROR()
|
||||
# Update the artwork
|
||||
old_url = cursor.fetchone()[0]
|
||||
except TypeError:
|
||||
# Add the artwork
|
||||
LOG.debug('Adding Art Link for %s kodi_id %s, kodi_type %s: %s',
|
||||
kodi_art, kodi_id, kodi_type, url)
|
||||
query = '''
|
||||
INSERT INTO art(media_id, media_type, type, url)
|
||||
VALUES (?, ?, ?, ?)
|
||||
'''
|
||||
cursor.execute(query, (kodi_id, kodi_type, kodi_art, url))
|
||||
else:
|
||||
if url == old_url:
|
||||
# Only cache artwork if it changed
|
||||
return
|
||||
self.delete_cached_artwork(old_url)
|
||||
LOG.debug("Updating Art url for %s kodi_id %s, kodi_type %s to %s",
|
||||
kodi_art, kodi_id, kodi_type, url)
|
||||
query = '''
|
||||
UPDATE art SET url = ?
|
||||
WHERE media_id = ? AND media_type = ? AND type = ?
|
||||
'''
|
||||
cursor.execute(query, (url, kodi_id, kodi_type, kodi_art))
|
||||
# Cache fanart and poster in Kodi texture cache
|
||||
if kodi_type != 'actor':
|
||||
self.cache_texture(url)
|
||||
|
||||
def delete_artwork(self, kodiId, mediaType, cursor):
|
||||
query = 'SELECT url FROM art WHERE media_id = ? AND media_type = ?'
|
||||
cursor.execute(query, (kodiId, mediaType,))
|
||||
for row in cursor.fetchall():
|
||||
self.delete_cached_artwork(row[0])
|
||||
|
||||
@staticmethod
|
||||
def delete_cached_artwork(url):
|
||||
"""
|
||||
Deleted the cached artwork with path url (if it exists)
|
||||
"""
|
||||
connection = utils.kodi_sql('texture')
|
||||
cursor = connection.cursor()
|
||||
try:
|
||||
cursor.execute("SELECT cachedurl FROM texture WHERE url=? LIMIT 1",
|
||||
(url,))
|
||||
cachedurl = cursor.fetchone()[0]
|
||||
except TypeError:
|
||||
# Could not find cached url
|
||||
pass
|
||||
else:
|
||||
# Delete thumbnail as well as the entry
|
||||
path = path_ops.translate_path("special://thumbnails/%s"
|
||||
% cachedurl)
|
||||
LOG.debug("Deleting cached thumbnail: %s", path)
|
||||
if path_ops.exists(path):
|
||||
path_ops.rmtree(path, ignore_errors=True)
|
||||
cursor.execute("DELETE FROM texture WHERE url = ?", (url,))
|
||||
connection.commit()
|
||||
finally:
|
||||
app.APP.deregister_caching_thread(self)
|
||||
LOG.info("---===### Stopped ImageCachingThread ###===---")
|
||||
connection.close()
|
||||
|
||||
def _loop(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)
|
||||
# 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
|
||||
@staticmethod
|
||||
def restore_cache_directories():
|
||||
LOG.info("Restoring cache directories...")
|
||||
paths = ("", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
||||
"a", "b", "c", "d", "e", "f",
|
||||
"Video", "plex")
|
||||
for path in paths:
|
||||
new_path = path_ops.translate_path("special://thumbnails/%s" % path)
|
||||
path_ops.makedirs(path_ops.encode_path(new_path))
|
||||
|
||||
|
||||
def cache_url(url, should_suspend=None):
|
||||
url = double_urlencode(url)
|
||||
sleeptime = 0
|
||||
while True:
|
||||
try:
|
||||
requests.head(
|
||||
url="http://%s:%s/image/image://%s"
|
||||
% (app.CONN.webserver_host,
|
||||
app.CONN.webserver_port,
|
||||
url),
|
||||
auth=(app.CONN.webserver_username,
|
||||
app.CONN.webserver_password),
|
||||
timeout=TIMEOUT)
|
||||
except requests.Timeout:
|
||||
# We don't need the result, only trigger Kodi to start the
|
||||
# download. All is well
|
||||
break
|
||||
except requests.ConnectionError:
|
||||
if app.APP.stop_pkc or (should_suspend and should_suspend()):
|
||||
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))
|
||||
break
|
||||
LOG.debug('Were trying too hard to download art, server '
|
||||
'over-loaded. Sleep %s seconds before trying '
|
||||
'again to download %s',
|
||||
2**sleeptime, double_urldecode(url))
|
||||
app.APP.monitor.waitForAbort((2**sleeptime))
|
||||
sleeptime += 1
|
||||
continue
|
||||
except Exception as err:
|
||||
LOG.error('Unknown exception for url %s: %s'.
|
||||
double_urldecode(url), err)
|
||||
import traceback
|
||||
LOG.error("Traceback:\n%s", traceback.format_exc())
|
||||
break
|
||||
# We did not even get a timeout
|
||||
break
|
||||
class ArtworkSyncMessage(object):
|
||||
"""
|
||||
Put in artwork queue to display the message as a Kodi notification
|
||||
"""
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
|
|
|
@ -2,246 +2,79 @@
|
|||
# -*- 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
|
||||
|
||||
WORKER_COUNT = 3
|
||||
LOG = getLogger('PLEX.threads')
|
||||
LOG = getLogger('PLEX.' + __name__)
|
||||
|
||||
|
||||
class KillableThread(threading.Thread):
|
||||
def __init__(self, group=None, target=None, name=None, args=(), kwargs={}):
|
||||
self._canceled = False
|
||||
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
|
||||
super(KillableThread, self).__init__(group, target, name, args, kwargs)
|
||||
pass
|
||||
'''A thread class that supports raising exception in the thread from
|
||||
another thread.
|
||||
'''
|
||||
# def _get_my_tid(self):
|
||||
# """determines this (self's) thread id
|
||||
|
||||
def should_cancel(self):
|
||||
"""
|
||||
Returns True if the thread should be stopped immediately
|
||||
"""
|
||||
return self._canceled or app.APP.stop_pkc
|
||||
# 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")
|
||||
|
||||
def cancel(self):
|
||||
"""
|
||||
Call from another thread to stop this current thread
|
||||
"""
|
||||
self._canceled = True
|
||||
# Make sure thread is running in order to exit quickly
|
||||
self._is_not_asleep.set()
|
||||
self._is_not_suspended.set()
|
||||
# return self.ident
|
||||
|
||||
def should_suspend(self):
|
||||
"""
|
||||
Returns True if the current thread should be suspended immediately
|
||||
"""
|
||||
return self._suspended
|
||||
# def _raiseExc(self, exctype):
|
||||
# """Raises the given exception type in the context of this thread.
|
||||
|
||||
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()
|
||||
# If the thread is busy in a system call (time.sleep(),
|
||||
# socket.accept(), ...), the exception is simply ignored.
|
||||
|
||||
def resume(self):
|
||||
"""
|
||||
Call from another thread to revive a suspended or asleep current thread
|
||||
back to life
|
||||
"""
|
||||
self._suspended = False
|
||||
self._is_not_asleep.set()
|
||||
self._is_not_suspended.set()
|
||||
# If you are sure that your exception should terminate the thread,
|
||||
# one way to ensure that it works is:
|
||||
|
||||
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()
|
||||
"""
|
||||
self._suspension_reached.set()
|
||||
self._is_not_suspended.wait(self.suspension_timeout)
|
||||
self._suspension_reached.clear()
|
||||
return self.should_cancel()
|
||||
# t = ThreadWithExc( ... )
|
||||
# ...
|
||||
# t.raiseExc( SomeException )
|
||||
# while t.isAlive():
|
||||
# time.sleep( 0.1 )
|
||||
# t.raiseExc( SomeException )
|
||||
|
||||
def is_suspended(self):
|
||||
"""
|
||||
Check from another thread whether the current thread is suspended
|
||||
"""
|
||||
return self._suspension_reached.is_set()
|
||||
# If the exception is to be caught by the thread, you need a way to
|
||||
# check that your thread has caught it.
|
||||
|
||||
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()
|
||||
# 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 is_asleep(self):
|
||||
"""
|
||||
Check from another thread whether the current thread is asleep
|
||||
"""
|
||||
return not self._is_not_asleep.is_set()
|
||||
def kill(self, force_and_wait=False):
|
||||
pass
|
||||
# try:
|
||||
# self._raiseExc(KillThreadException)
|
||||
|
||||
def unblock_callers(self):
|
||||
"""
|
||||
Ensures that any other thread that requested this thread's suspension
|
||||
is released
|
||||
"""
|
||||
self._suspension_reached.set()
|
||||
# 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
|
||||
|
||||
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)
|
||||
# def run(self):
|
||||
# try:
|
||||
# self._Thread__target(*self._Thread__args, **self._Thread__kwargs)
|
||||
# except KillThreadException:
|
||||
# self.onKilled()
|
||||
|
||||
|
||||
class Tasks(list):
|
||||
|
@ -260,14 +93,14 @@ class Tasks(list):
|
|||
self.pop().cancel()
|
||||
|
||||
|
||||
class Task(object):
|
||||
class Task:
|
||||
def __init__(self, priority=None):
|
||||
self.priority = priority
|
||||
self._priority = priority
|
||||
self._canceled = False
|
||||
self.finished = False
|
||||
|
||||
def __cmp__(self, other):
|
||||
return self.priority - other.priority
|
||||
return self._priority - other._priority
|
||||
|
||||
def start(self):
|
||||
BGThreader.addTask(self)
|
||||
|
@ -277,37 +110,18 @@ class Task(object):
|
|||
self.finished = True
|
||||
|
||||
def run(self):
|
||||
raise NotImplementedError
|
||||
pass
|
||||
|
||||
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
|
||||
self._callback = callback
|
||||
self._args = args
|
||||
self._kwargs = kwargs
|
||||
super(FunctionAsTask, self).__init__()
|
||||
|
||||
def run(self):
|
||||
result = self._function(*self._args, **self._kwargs)
|
||||
if self._callback:
|
||||
self._callback(result)
|
||||
|
||||
|
||||
class MutablePriorityQueue(Queue.PriorityQueue):
|
||||
def _get(self, heappop=heapq.heappop):
|
||||
self.queue.sort()
|
||||
|
@ -318,15 +132,15 @@ class MutablePriorityQueue(Queue.PriorityQueue):
|
|||
self.mutex.acquire()
|
||||
try:
|
||||
lowest = self.queue and min(self.queue) or None
|
||||
except Exception:
|
||||
except:
|
||||
lowest = None
|
||||
utils.ERROR(notify=True)
|
||||
utils.ERROR()
|
||||
finally:
|
||||
self.mutex.release()
|
||||
return lowest
|
||||
|
||||
|
||||
class BackgroundWorker(object):
|
||||
class BackgroundWorker:
|
||||
def __init__(self, queue, name=None):
|
||||
self._queue = queue
|
||||
self.name = name
|
||||
|
@ -334,21 +148,20 @@ class BackgroundWorker(object):
|
|||
self._abort = False
|
||||
self._task = None
|
||||
|
||||
@staticmethod
|
||||
def _runTask(task):
|
||||
def _runTask(self, task):
|
||||
if task._canceled:
|
||||
return
|
||||
try:
|
||||
task._run()
|
||||
except Exception:
|
||||
utils.ERROR(notify=True)
|
||||
except:
|
||||
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 +184,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)
|
||||
|
@ -386,42 +199,17 @@ class BackgroundWorker(object):
|
|||
return self._thread and self._thread.isAlive()
|
||||
|
||||
|
||||
class NonstoppingBackgroundWorker(BackgroundWorker):
|
||||
def __init__(self, queue, name=None):
|
||||
self._working = False
|
||||
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)
|
||||
|
||||
def working(self):
|
||||
return self._working
|
||||
|
||||
|
||||
class BackgroundThreader:
|
||||
def __init__(self, name=None, worker=BackgroundWorker, worker_count=6):
|
||||
def __init__(self, name=None, worker_count=8):
|
||||
self.name = name
|
||||
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._priority = -1
|
||||
self.workers = [BackgroundWorker(self._queue, 'queue.{0}:worker.{1}'.format(self.name, x)) for x in range(worker_count)]
|
||||
|
||||
def _nextPriority(self):
|
||||
self.priority += 1
|
||||
return self.priority
|
||||
self._priority += 1
|
||||
return self._priority
|
||||
|
||||
def abort(self):
|
||||
self._abort = True
|
||||
|
@ -430,22 +218,22 @@ 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()
|
||||
task._priority = self._nextPriority()
|
||||
self._queue.put(task)
|
||||
self.startWorkers()
|
||||
|
||||
def addTasks(self, tasks):
|
||||
for t in tasks:
|
||||
t.priority = self._nextPriority()
|
||||
t._priority = self._nextPriority()
|
||||
self._queue.put(t)
|
||||
|
||||
self.startWorkers()
|
||||
|
@ -457,7 +245,7 @@ class BackgroundThreader:
|
|||
|
||||
p = lowest - len(tasks)
|
||||
for t in tasks:
|
||||
t.priority = p
|
||||
t._priority = p
|
||||
self._queue.put(t)
|
||||
p += 1
|
||||
|
||||
|
@ -478,26 +266,21 @@ class BackgroundThreader:
|
|||
if not lowest:
|
||||
return None
|
||||
|
||||
return lowest.priority
|
||||
return lowest._priority
|
||||
|
||||
def moveToFront(self, qitem):
|
||||
lowest = self.getLowestPrority()
|
||||
if lowest is None:
|
||||
return
|
||||
|
||||
qitem.priority = lowest - 1
|
||||
qitem._priority = lowest - 1
|
||||
|
||||
|
||||
class ThreaderManager:
|
||||
def __init__(self,
|
||||
worker=NonstoppingBackgroundWorker,
|
||||
worker_count=WORKER_COUNT):
|
||||
def __init__(self):
|
||||
self.index = 0
|
||||
self.abandoned = []
|
||||
self._workerhandler = worker
|
||||
self.threader = BackgroundThreader(name=str(self.index),
|
||||
worker=worker,
|
||||
worker_count=worker_count)
|
||||
self.threader = BackgroundThreader(str(self.index))
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.threader, name)
|
||||
|
@ -508,13 +291,12 @@ class ThreaderManager:
|
|||
|
||||
self.index += 1
|
||||
self.abandoned.append(self.threader.abort())
|
||||
self.threader = BackgroundThreader(name=str(self.index),
|
||||
worker=self._workerhandler)
|
||||
self.threader = BackgroundThreader(str(self.index))
|
||||
|
||||
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,12 +31,13 @@ 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-Device': v.DEVICE,
|
||||
'X-Plex-Model': v.MODEL,
|
||||
# 'X-Plex-Language': 'en',
|
||||
'X-Plex-Device': v.ADDON_NAME,
|
||||
'X-Plex-Client-Platform': v.PLATFORM,
|
||||
'X-Plex-Device-Name': v.DEVICENAME,
|
||||
'X-Plex-Platform': v.PLATFORM,
|
||||
'X-Plex-Platform-Version': v.PLATFORM_VERSION,
|
||||
# 'X-Plex-Platform-Version': 'unknown',
|
||||
# 'X-Plex-Model': 'unknown',
|
||||
'X-Plex-Product': v.ADDON_NAME,
|
||||
'X-Plex-Version': v.ADDON_VERSION,
|
||||
'X-Plex-Client-Identifier': getDeviceId(),
|
||||
|
|
68
resources/lib/command_pipeline.py
Normal file
68
resources/lib/command_pipeline.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
import logging
|
||||
from threading import Thread
|
||||
from xbmc import sleep
|
||||
|
||||
from . import utils
|
||||
from . import state
|
||||
|
||||
###############################################################################
|
||||
LOG = logging.getLogger('PLEX.command_pipeline')
|
||||
###############################################################################
|
||||
|
||||
|
||||
@utils.thread_methods
|
||||
class Monitor_Window(Thread):
|
||||
"""
|
||||
Monitors window('plex_command') for new entries that we need to take care
|
||||
of, e.g. for new plays initiated on the Kodi side with addon paths.
|
||||
|
||||
Adjusts state.py accordingly
|
||||
"""
|
||||
def run(self):
|
||||
stopped = self.stopped
|
||||
queue = state.COMMAND_PIPELINE_QUEUE
|
||||
LOG.info("----===## Starting Kodi_Play_Client ##===----")
|
||||
while not stopped():
|
||||
if utils.window('plex_command'):
|
||||
value = utils.window('plex_command')
|
||||
utils.window('plex_command', clear=True)
|
||||
if value.startswith('PLAY-'):
|
||||
queue.put(value.replace('PLAY-', ''))
|
||||
elif value == 'SUSPEND_LIBRARY_THREAD-True':
|
||||
state.SUSPEND_LIBRARY_THREAD = True
|
||||
elif value == 'SUSPEND_LIBRARY_THREAD-False':
|
||||
state.SUSPEND_LIBRARY_THREAD = False
|
||||
elif value == 'STOP_SYNC-True':
|
||||
state.STOP_SYNC = True
|
||||
elif value == 'STOP_SYNC-False':
|
||||
state.STOP_SYNC = False
|
||||
elif value == 'PMS_STATUS-Auth':
|
||||
state.PMS_STATUS = 'Auth'
|
||||
elif value == 'PMS_STATUS-401':
|
||||
state.PMS_STATUS = '401'
|
||||
elif value == 'SUSPEND_USER_CLIENT-True':
|
||||
state.SUSPEND_USER_CLIENT = True
|
||||
elif value == 'SUSPEND_USER_CLIENT-False':
|
||||
state.SUSPEND_USER_CLIENT = False
|
||||
elif value.startswith('PLEX_TOKEN-'):
|
||||
state.PLEX_TOKEN = value.replace('PLEX_TOKEN-', '') or None
|
||||
elif value.startswith('PLEX_USERNAME-'):
|
||||
state.PLEX_USERNAME = \
|
||||
value.replace('PLEX_USERNAME-', '') or None
|
||||
elif value.startswith('RUN_LIB_SCAN-'):
|
||||
state.RUN_LIB_SCAN = value.replace('RUN_LIB_SCAN-', '')
|
||||
elif value.startswith('CONTEXT_menu?'):
|
||||
queue.put('dummy?mode=context_menu&%s'
|
||||
% value.replace('CONTEXT_menu?', ''))
|
||||
elif value.startswith('NAVIGATE'):
|
||||
queue.put(value.replace('NAVIGATE-', ''))
|
||||
else:
|
||||
raise NotImplementedError('%s not implemented' % value)
|
||||
else:
|
||||
sleep(50)
|
||||
# Put one last item into the queue to let playback_starter end
|
||||
queue.put(None)
|
||||
LOG.info("----===## Kodi_Play_Client stopped ##===----")
|
|
@ -7,8 +7,11 @@ from __future__ import absolute_import, division, unicode_literals
|
|||
from logging import getLogger
|
||||
from xbmc import Player
|
||||
|
||||
from . import playqueue as PQ, plex_functions as PF
|
||||
from . import json_rpc as js, variables as v, app
|
||||
from . import playqueue as PQ
|
||||
from . import plex_functions as PF
|
||||
from . import json_rpc as js
|
||||
from . import variables as v
|
||||
from . import state
|
||||
|
||||
###############################################################################
|
||||
|
||||
|
@ -49,7 +52,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]
|
||||
|
@ -65,12 +68,12 @@ def process_command(request_path, params):
|
|||
if request_path == 'player/playback/playMedia':
|
||||
# We need to tell service.py
|
||||
action = 'alexa' if params.get('deviceName') == 'Alexa' else 'playlist'
|
||||
app.APP.companion_queue.put({
|
||||
state.COMPANION_QUEUE.put({
|
||||
'action': action,
|
||||
'data': params
|
||||
})
|
||||
elif request_path == 'player/playback/refreshPlayQueue':
|
||||
app.APP.companion_queue.put({
|
||||
state.COMPANION_QUEUE.put({
|
||||
'action': 'refreshPlayQueue',
|
||||
'data': params
|
||||
})
|
||||
|
@ -112,7 +115,7 @@ def process_command(request_path, params):
|
|||
elif request_path == "player/navigation/back":
|
||||
js.input_back()
|
||||
elif request_path == "player/playback/setStreams":
|
||||
app.APP.companion_queue.put({
|
||||
state.COMPANION_QUEUE.put({
|
||||
'action': 'setStreams',
|
||||
'data': params
|
||||
})
|
||||
|
|
|
@ -43,8 +43,8 @@ class ContextMenu(xbmcgui.WindowXMLDialog):
|
|||
return self.selected_option
|
||||
|
||||
def onInit(self):
|
||||
if utils.window('plexAvatar'):
|
||||
self.getControl(USER_IMAGE).setImage(utils.window('plexAvatar'))
|
||||
if utils.window('PlexUserImage'):
|
||||
self.getControl(USER_IMAGE).setImage(utils.window('PlexUserImage'))
|
||||
height = 479 + (len(self._options) * 55)
|
||||
LOG.debug("options: %s", self._options)
|
||||
self.list_ = self.getControl(LIST)
|
||||
|
@ -60,7 +60,7 @@ class ContextMenu(xbmcgui.WindowXMLDialog):
|
|||
if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK):
|
||||
if self.getFocusId() == LIST:
|
||||
option = self.list_.getSelectedItem()
|
||||
self.selected_option = option.getLabel().decode('utf-8')
|
||||
self.selected_option = option.getLabel()
|
||||
LOG.info('option selected: %s', self.selected_option)
|
||||
self.close()
|
||||
|
||||
|
|
|
@ -5,10 +5,14 @@ from logging import getLogger
|
|||
import xbmc
|
||||
import xbmcgui
|
||||
|
||||
from . import context
|
||||
from . import plexdb_functions as plexdb
|
||||
from . import utils
|
||||
from . import plex_functions as PF
|
||||
from .plex_api import API
|
||||
from .plex_db import PlexDB
|
||||
from . import context, plex_functions as PF, playqueue as PQ
|
||||
from . import utils, variables as v, app
|
||||
from . import playqueue as PQ
|
||||
from . import variables as v
|
||||
from . import state
|
||||
|
||||
###############################################################################
|
||||
|
||||
|
@ -60,15 +64,22 @@ 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")
|
||||
xbmc.sleep(500)
|
||||
xbmc.executebuiltin('Container.Refresh')
|
||||
|
||||
@staticmethod
|
||||
def _get_plex_id(kodi_id, kodi_type):
|
||||
plex_id = xbmc.getInfoLabel('ListItem.Property(plexid)') or None
|
||||
if not plex_id and kodi_id and kodi_type:
|
||||
with PlexDB() as plexdb:
|
||||
item = plexdb.item_by_kodi_id(kodi_id, kodi_type)
|
||||
if item:
|
||||
plex_id = item['plex_id']
|
||||
with plexdb.Get_Plex_DB() as plexcursor:
|
||||
item = plexcursor.getItem_byKodiId(kodi_id, kodi_type)
|
||||
try:
|
||||
plex_id = item[0]
|
||||
except TypeError:
|
||||
LOG.info('Could not get the Plex id for context menu')
|
||||
return plex_id
|
||||
|
||||
def _select_menu(self):
|
||||
|
@ -79,7 +90,7 @@ class ContextMenu(object):
|
|||
# if user uses direct paths, give option to initiate playback via PMS
|
||||
if self.api and self.api.extras():
|
||||
options.append(OPTIONS['Extras'])
|
||||
if app.SYNC.direct_paths and self.kodi_type in v.KODI_VIDEOTYPES:
|
||||
if state.DIRECT_PATHS and self.kodi_type in v.KODI_VIDEOTYPES:
|
||||
options.append(OPTIONS['PMS_Play'])
|
||||
if self.kodi_type in v.KODI_VIDEOTYPES:
|
||||
options.append(OPTIONS['Transcode'])
|
||||
|
@ -107,7 +118,7 @@ class ContextMenu(object):
|
|||
"""
|
||||
selected = self._selected_option
|
||||
if selected == OPTIONS['Transcode']:
|
||||
app.PLAYSTATE.force_transcode = True
|
||||
state.FORCE_TRANSCODE = True
|
||||
self._PMS_play()
|
||||
elif selected == OPTIONS['PMS_Play']:
|
||||
self._PMS_play()
|
||||
|
@ -141,10 +152,9 @@ class ContextMenu(object):
|
|||
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'))
|
||||
state.CONTEXT_MENU_PLAY = True
|
||||
handle = self.api.path(force_first_media=False, force_addon=True)
|
||||
xbmc.executebuiltin('RunPlugin(%s)' % handle)
|
||||
|
||||
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
|
|
@ -1,39 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
xml.etree.ElementTree tries to encode with text.encode('ascii') - which is
|
||||
just plain BS. This etree will always return unicode, not string
|
||||
"""
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
# Originally tried faster cElementTree, but does NOT work reliably with Kodi
|
||||
from defusedxml.ElementTree import DefusedXMLParser, _generate_etree_functions
|
||||
|
||||
from xml.etree.ElementTree import TreeBuilder as _TreeBuilder
|
||||
from xml.etree.ElementTree import parse as _parse
|
||||
from xml.etree.ElementTree import iterparse as _iterparse
|
||||
from xml.etree.ElementTree import tostring
|
||||
|
||||
|
||||
class UnicodeXMLParser(DefusedXMLParser):
|
||||
"""
|
||||
PKC Hack to ensure we're always receiving unicode, not str
|
||||
"""
|
||||
@staticmethod
|
||||
def _fixtext(text):
|
||||
"""
|
||||
Do NOT try to convert every entry to str with entry.encode('ascii')!
|
||||
"""
|
||||
return text
|
||||
|
||||
|
||||
# aliases
|
||||
XMLTreeBuilder = XMLParse = UnicodeXMLParser
|
||||
|
||||
parse, iterparse, fromstring = _generate_etree_functions(UnicodeXMLParser,
|
||||
_TreeBuilder, _parse,
|
||||
_iterparse)
|
||||
XML = fromstring
|
||||
|
||||
|
||||
__all__ = ['XML', 'XMLParse', 'XMLTreeBuilder', 'fromstring', 'iterparse',
|
||||
'parse', 'tostring']
|
|
@ -2,10 +2,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import defusedxml.ElementTree as etree # etree parse unsafe
|
||||
import requests
|
||||
import requests.exceptions as exceptions
|
||||
|
||||
from . import utils, clientinfo, app
|
||||
from . import utils
|
||||
from . import clientinfo
|
||||
from . import state
|
||||
|
||||
###############################################################################
|
||||
|
||||
|
@ -13,7 +15,7 @@ from . import utils, clientinfo, app
|
|||
import requests.packages.urllib3
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
||||
LOG = getLogger('PLEX.download')
|
||||
LOG = getLogger('PLEX.downloadutils')
|
||||
|
||||
###############################################################################
|
||||
|
||||
|
@ -29,23 +31,34 @@ class DownloadUtils():
|
|||
_shared_state = {}
|
||||
|
||||
# How many failed attempts before declaring PMS dead?
|
||||
connection_attempts = 1
|
||||
count_error = 0
|
||||
connectionAttempts = 1
|
||||
# How many 401 returns before declaring unauthorized?
|
||||
unauthorized_attempts = 2
|
||||
count_unauthorized = 0
|
||||
unauthorizedAttempts = 2
|
||||
# How long should we wait for an answer from the
|
||||
timeout = 30.0
|
||||
|
||||
def __init__(self):
|
||||
self.__dict__ = self._shared_state
|
||||
|
||||
def setSSL(self):
|
||||
def setServer(self, server):
|
||||
"""
|
||||
Reserved for userclient only
|
||||
"""
|
||||
self.server = server
|
||||
LOG.debug("Set server: %s", server)
|
||||
|
||||
def setSSL(self, verifySSL=None, certificate=None):
|
||||
"""
|
||||
Reserved for userclient only
|
||||
|
||||
verifySSL must be 'true' to enable certificate validation
|
||||
|
||||
certificate must be path to certificate or 'None'
|
||||
"""
|
||||
verifySSL = app.CONN.verify_ssl_cert
|
||||
certificate = app.CONN.ssl_cert_path
|
||||
if verifySSL is None:
|
||||
verifySSL = state.VERIFY_SSL_CERT
|
||||
if certificate is None:
|
||||
certificate = state.SSL_CERT_PATH
|
||||
# Set the session's parameters
|
||||
self.s.verify = verifySSL
|
||||
if certificate:
|
||||
|
@ -55,7 +68,8 @@ class DownloadUtils():
|
|||
|
||||
def startSession(self, reset=False):
|
||||
"""
|
||||
User should be authenticated when this method is called
|
||||
User should be authenticated when this method is called (via
|
||||
userclient)
|
||||
"""
|
||||
# Start session
|
||||
self.s = requests.Session()
|
||||
|
@ -67,37 +81,40 @@ class DownloadUtils():
|
|||
# Set SSL settings
|
||||
self.setSSL()
|
||||
|
||||
# Set other stuff
|
||||
self.setServer(utils.window('pms_server'))
|
||||
|
||||
# Counters to declare PMS dead or unauthorized
|
||||
# Use window variables because start of movies will be called with a
|
||||
# new plugin instance - it's impossible to share data otherwise
|
||||
if reset is True:
|
||||
self.count_error = 0
|
||||
self.count_unauthorized = 0
|
||||
utils.window('countUnauthorized', value='0')
|
||||
utils.window('countError', value='0')
|
||||
|
||||
# Retry connections to the server
|
||||
self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1))
|
||||
self.s.mount("https://", requests.adapters.HTTPAdapter(max_retries=1))
|
||||
|
||||
LOG.debug("Requests session started on: %s", app.CONN.server)
|
||||
LOG.info("Requests session started on: %s", self.server)
|
||||
|
||||
def stopSession(self):
|
||||
try:
|
||||
self.s.close()
|
||||
except Exception:
|
||||
except:
|
||||
LOG.info("Requests session already closed")
|
||||
try:
|
||||
del self.s
|
||||
except AttributeError:
|
||||
except:
|
||||
pass
|
||||
LOG.info('Request session stopped')
|
||||
|
||||
@staticmethod
|
||||
def getHeader(options=None):
|
||||
def getHeader(self, options=None):
|
||||
header = clientinfo.getXArgsDeviceInfo()
|
||||
if options is not None:
|
||||
header.update(options)
|
||||
return header
|
||||
|
||||
@staticmethod
|
||||
def _doDownload(s, action_type, **kwargs):
|
||||
def _doDownload(self, s, action_type, **kwargs):
|
||||
if action_type == "GET":
|
||||
r = s.get(**kwargs)
|
||||
elif action_type == "POST":
|
||||
|
@ -113,7 +130,7 @@ class DownloadUtils():
|
|||
def downloadUrl(self, url, action_type="GET", postBody=None,
|
||||
parameters=None, authenticate=True, headerOptions=None,
|
||||
verifySSL=True, timeout=None, return_response=False,
|
||||
headerOverride=None, reraise=False):
|
||||
headerOverride=None):
|
||||
"""
|
||||
Override SSL check with verifySSL=False
|
||||
|
||||
|
@ -139,7 +156,7 @@ class DownloadUtils():
|
|||
self.startSession()
|
||||
s = self.s
|
||||
# Replace for the real values
|
||||
url = url.replace("{server}", app.CONN.server)
|
||||
url = url.replace("{server}", self.server)
|
||||
else:
|
||||
# User is not (yet) authenticated. Used to communicate with
|
||||
# plex.tv and to check for PMS servers
|
||||
|
@ -148,9 +165,9 @@ class DownloadUtils():
|
|||
headerOptions = self.getHeader(options=headerOptions)
|
||||
else:
|
||||
headerOptions = headerOverride
|
||||
kwargs['verify'] = app.CONN.verify_ssl_cert
|
||||
if app.CONN.ssl_cert_path:
|
||||
kwargs['cert'] = app.CONN.ssl_cert_path
|
||||
kwargs['verify'] = state.VERIFY_SSL_CERT
|
||||
if state.SSL_CERT_PATH:
|
||||
kwargs['cert'] = state.SSL_CERT_PATH
|
||||
|
||||
# Set the variables we were passed (fallback to request session
|
||||
# otherwise - faster)
|
||||
|
@ -167,68 +184,53 @@ class DownloadUtils():
|
|||
kwargs['timeout'] = timeout
|
||||
|
||||
# ACTUAL DOWNLOAD HAPPENING HERE
|
||||
success = False
|
||||
try:
|
||||
r = self._doDownload(s, action_type, **kwargs)
|
||||
|
||||
# THE EXCEPTIONS
|
||||
except exceptions.SSLError as e:
|
||||
except requests.exceptions.SSLError as e:
|
||||
LOG.warn("Invalid SSL certificate for: %s", url)
|
||||
LOG.warn(e)
|
||||
if reraise:
|
||||
raise
|
||||
except exceptions.ConnectionError as e:
|
||||
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
# Connection error
|
||||
LOG.warn("Server unreachable at: %s", url)
|
||||
LOG.warn(e)
|
||||
if reraise:
|
||||
raise
|
||||
except exceptions.Timeout as e:
|
||||
|
||||
except requests.exceptions.Timeout as e:
|
||||
LOG.warn("Server timeout at: %s", url)
|
||||
LOG.warn(e)
|
||||
if reraise:
|
||||
raise
|
||||
except exceptions.HTTPError as e:
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
LOG.warn('HTTP Error at %s', url)
|
||||
LOG.warn(e)
|
||||
if reraise:
|
||||
raise
|
||||
except exceptions.TooManyRedirects as e:
|
||||
|
||||
except requests.exceptions.TooManyRedirects as e:
|
||||
LOG.warn("Too many redirects connecting to: %s", url)
|
||||
LOG.warn(e)
|
||||
if reraise:
|
||||
raise
|
||||
except exceptions.RequestException as e:
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
LOG.warn("Unknown error connecting to: %s", url)
|
||||
LOG.warn(e)
|
||||
if reraise:
|
||||
raise
|
||||
|
||||
except SystemExit:
|
||||
LOG.info('SystemExit detected, aborting download')
|
||||
self.stopSession()
|
||||
if reraise:
|
||||
raise
|
||||
except Exception:
|
||||
|
||||
except:
|
||||
LOG.warn('Unknown error while downloading. Traceback:')
|
||||
import traceback
|
||||
LOG.warn(traceback.format_exc())
|
||||
if reraise:
|
||||
raise
|
||||
|
||||
# THE RESPONSE #####
|
||||
else:
|
||||
success = True
|
||||
# We COULD contact the PMS, hence it ain't dead
|
||||
if authenticate is True:
|
||||
self.count_error = 0
|
||||
utils.window('countError', value='0')
|
||||
if r.status_code != 401:
|
||||
self.count_unauthorized = 0
|
||||
utils.window('countUnauthorized', value='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)
|
||||
|
@ -244,16 +246,23 @@ class DownloadUtils():
|
|||
LOG.info(r.text)
|
||||
if '401 Unauthorized' in r.text:
|
||||
# Truly unauthorized
|
||||
self.count_unauthorized += 1
|
||||
if self.count_unauthorized >= self.unauthorized_attempts:
|
||||
utils.window(
|
||||
'countUnauthorized',
|
||||
value=str(int(utils.window('countUnauthorized')) + 1))
|
||||
if (int(utils.window('countUnauthorized')) >=
|
||||
self.unauthorizedAttempts):
|
||||
LOG.warn('We seem to be truly unauthorized for PMS'
|
||||
' %s ', url)
|
||||
# Unauthorized access, user no longer has access
|
||||
app.ACCOUNT.log_out()
|
||||
utils.dialog('notification',
|
||||
utils.lang(29999),
|
||||
utils.lang(30017),
|
||||
icon='{error}')
|
||||
if state.PMS_STATUS not in ('401', 'Auth'):
|
||||
# Tell userclient token has been revoked.
|
||||
LOG.debug('Setting PMS server status to '
|
||||
'unauthorized')
|
||||
state.PMS_STATUS = '401'
|
||||
utils.window('plex_serverStatus', value="401")
|
||||
utils.dialog('notification',
|
||||
utils.lang(29999),
|
||||
utils.lang(30017),
|
||||
icon='{error}')
|
||||
else:
|
||||
# there might be other 401 where e.g. PMS under strain
|
||||
LOG.info('PMS might only be under strain')
|
||||
|
@ -262,11 +271,14 @@ 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)
|
||||
r = etree.fromstring(r.content)
|
||||
return r
|
||||
except Exception:
|
||||
except:
|
||||
r.encoding = 'utf-8'
|
||||
if r.text == '':
|
||||
# Answer does not contain a body
|
||||
|
@ -275,7 +287,7 @@ class DownloadUtils():
|
|||
# UNICODE - JSON object
|
||||
r = r.json()
|
||||
return r
|
||||
except Exception:
|
||||
except:
|
||||
if '200 OK' in r.text:
|
||||
# Received fucked up OK from PMS on playstate
|
||||
# update
|
||||
|
@ -289,19 +301,24 @@ class DownloadUtils():
|
|||
elif r.status_code == 403:
|
||||
# E.g. deleting a PMS item
|
||||
LOG.warn('PMS sent 403: Forbidden error for url %s', url)
|
||||
return
|
||||
return None
|
||||
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:
|
||||
if not success and authenticate:
|
||||
# Deal with the consequences of the exceptions
|
||||
# Make the addon aware of status
|
||||
self.count_error += 1
|
||||
if self.count_error >= self.connection_attempts:
|
||||
# And now deal with the consequences of the exceptions
|
||||
if authenticate is True:
|
||||
# Make the addon aware of status
|
||||
try:
|
||||
utils.window('countError',
|
||||
value=str(int(utils.window('countError')) + 1))
|
||||
if int(utils.window('countError')) >= self.connectionAttempts:
|
||||
LOG.warn('Failed to connect to %s too many times. '
|
||||
'Declare PMS dead', url)
|
||||
app.CONN.online = False
|
||||
utils.window('plex_online', value="false")
|
||||
except ValueError:
|
||||
# 'countError' not yet set
|
||||
pass
|
||||
return None
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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
|
12
resources/lib/image.py
Normal file
12
resources/lib/image.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from . import util, path_ops
|
||||
|
||||
CACHE_PATH = path_ops.path.join(util.PROFILE, 'avatars', '')
|
||||
if not path_ops.exists(CACHE_PATH):
|
||||
path_ops.makedirs(CACHE_PATH)
|
||||
|
||||
|
||||
def getImage(url, ID):
|
||||
return url, ''
|
|
@ -2,18 +2,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
from Queue import Queue
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
from xbmc import executebuiltin
|
||||
from xbmc import executebuiltin, translatePath
|
||||
|
||||
from . import utils
|
||||
from .utils import etree
|
||||
from . import path_ops
|
||||
from . import migration
|
||||
from .downloadutils import DownloadUtils as DU, exceptions
|
||||
from .downloadutils import DownloadUtils as DU
|
||||
from . import videonodes
|
||||
from . import userclient
|
||||
from . import clientinfo
|
||||
from . import plex_functions as PF
|
||||
from . import plex_tv
|
||||
from . import json_rpc as js
|
||||
from . import app
|
||||
from . import playqueue as PQ
|
||||
from . import state
|
||||
from . import variables as v
|
||||
|
||||
###############################################################################
|
||||
|
@ -26,6 +31,113 @@ if not path_ops.exists(v.EXTERNAL_SUBTITLE_TEMP_PATH):
|
|||
path_ops.makedirs(v.EXTERNAL_SUBTITLE_TEMP_PATH)
|
||||
|
||||
|
||||
WINDOW_PROPERTIES = (
|
||||
"plex_online", "plex_serverStatus", "plex_shouldStop", "plex_dbScan",
|
||||
"plex_customplayqueue", "plex_playbackProps",
|
||||
"pms_token", "plex_token", "pms_server", "plex_machineIdentifier",
|
||||
"plex_servername", "plex_authenticated", "PlexUserImage", "useDirectPaths",
|
||||
"countError", "countUnauthorized", "plex_restricteduser",
|
||||
"plex_allows_mediaDeletion", "plex_command", "plex_result",
|
||||
"plex_force_transcode_pix"
|
||||
)
|
||||
|
||||
|
||||
def reload_pkc():
|
||||
"""
|
||||
Will reload state.py entirely and then initiate some values from the Kodi
|
||||
settings file
|
||||
"""
|
||||
LOG.info('Start (re-)loading PKC settings')
|
||||
# Reset state.py
|
||||
reload(state)
|
||||
# Reset window props
|
||||
for prop in WINDOW_PROPERTIES:
|
||||
utils.window(prop, clear=True)
|
||||
# Clear video nodes properties
|
||||
videonodes.VideoNodes().clearProperties()
|
||||
|
||||
# Initializing
|
||||
state.VERIFY_SSL_CERT = utils.settings('sslverify') == 'true'
|
||||
state.SSL_CERT_PATH = utils.settings('sslcert') \
|
||||
if utils.settings('sslcert') != 'None' else None
|
||||
state.FULL_SYNC_INTERVALL = int(utils.settings('fullSyncInterval')) * 60
|
||||
state.SYNC_THREAD_NUMBER = int(utils.settings('syncThreadNumber'))
|
||||
state.SYNC_DIALOG = utils.settings('dbSyncIndicator') == 'true'
|
||||
state.ENABLE_MUSIC = utils.settings('enableMusic') == 'true'
|
||||
state.BACKGROUND_SYNC_DISABLED = utils.settings(
|
||||
'enableBackgroundSync') == 'false'
|
||||
state.BACKGROUNDSYNC_SAFTYMARGIN = int(
|
||||
utils.settings('backgroundsync_saftyMargin'))
|
||||
state.REPLACE_SMB_PATH = utils.settings('replaceSMB') == 'true'
|
||||
state.REMAP_PATH = utils.settings('remapSMB') == 'true'
|
||||
state.KODI_PLEX_TIME_OFFSET = float(utils.settings('kodiplextimeoffset'))
|
||||
state.FETCH_PMS_ITEM_NUMBER = utils.settings('fetch_pms_item_number')
|
||||
state.FORCE_RELOAD_SKIN = \
|
||||
utils.settings('forceReloadSkinOnPlaybackStop') == 'true'
|
||||
# Init some Queues()
|
||||
state.COMMAND_PIPELINE_QUEUE = Queue()
|
||||
state.COMPANION_QUEUE = Queue(maxsize=100)
|
||||
state.WEBSOCKET_QUEUE = Queue()
|
||||
set_replace_paths()
|
||||
set_webserver()
|
||||
# To detect Kodi profile switches
|
||||
utils.window('plex_kodiProfile',
|
||||
value=utils.try_decode(translatePath("special://profile")))
|
||||
clientinfo.getDeviceId()
|
||||
# Initialize the PKC playqueues
|
||||
PQ.init_playqueues()
|
||||
LOG.info('Done (re-)loading PKC settings')
|
||||
|
||||
|
||||
def set_replace_paths():
|
||||
"""
|
||||
Sets our values for direct paths correctly (including using lower-case
|
||||
protocols like smb:// and NOT SMB://)
|
||||
"""
|
||||
for typus in v.REMAP_TYPE_FROM_PLEXTYPE.values():
|
||||
for arg in ('Org', 'New'):
|
||||
key = 'remapSMB%s%s' % (typus, arg)
|
||||
value = utils.settings(key)
|
||||
if '://' in value:
|
||||
protocol = value.split('://', 1)[0]
|
||||
value = value.replace(protocol, protocol.lower())
|
||||
setattr(state, key, value)
|
||||
|
||||
|
||||
def set_webserver():
|
||||
"""
|
||||
Set the Kodi webserver details - used to set the texture cache
|
||||
"""
|
||||
if js.get_setting('services.webserver') in (None, False):
|
||||
# Enable the webserver, it is disabled
|
||||
js.set_setting('services.webserver', True)
|
||||
# Set standard port and username
|
||||
# set_setting('services.webserverport', 8080)
|
||||
# set_setting('services.webserverusername', 'kodi')
|
||||
# Webserver already enabled
|
||||
state.WEBSERVER_PORT = js.get_setting('services.webserverport')
|
||||
state.WEBSERVER_USERNAME = js.get_setting('services.webserverusername')
|
||||
state.WEBSERVER_PASSWORD = js.get_setting('services.webserverpassword')
|
||||
|
||||
|
||||
def _write_pms_settings(url, token):
|
||||
"""
|
||||
Sets certain settings for server by asking for the PMS' settings
|
||||
Call with url: scheme://ip:port
|
||||
"""
|
||||
xml = PF.get_PMS_settings(url, token)
|
||||
try:
|
||||
xml.attrib
|
||||
except AttributeError:
|
||||
LOG.error('Could not get PMS settings for %s', url)
|
||||
return
|
||||
for entry in xml:
|
||||
if entry.attrib.get('id', '') == 'allowMediaDeletion':
|
||||
value = 'true' if entry.get('value', '1') == '1' else 'false'
|
||||
utils.settings('plex_allows_mediaDeletion', value=value)
|
||||
utils.window('plex_allows_mediaDeletion', value=value)
|
||||
|
||||
|
||||
class InitialSetup(object):
|
||||
"""
|
||||
Will load Plex PMS settings (e.g. address) and token
|
||||
|
@ -33,130 +145,33 @@ class InitialSetup(object):
|
|||
"""
|
||||
def __init__(self):
|
||||
LOG.debug('Entering initialsetup class')
|
||||
self.server = userclient.UserClient().get_server()
|
||||
self.serverid = utils.settings('plex_machineIdentifier')
|
||||
# Get Plex credentials from settings file, if they exist
|
||||
plexdict = PF.GetPlexLoginFromSettings()
|
||||
self.myplexlogin = plexdict['myplexlogin'] == 'true'
|
||||
self.plex_login = plexdict['plexLogin']
|
||||
self.plex_login_id = plexdict['plexid']
|
||||
self.plex_token = plexdict['plexToken']
|
||||
self.plexid = plexdict['plexid']
|
||||
# Token for the PMS, not plex.tv
|
||||
self.pms_token = utils.settings('accessToken')
|
||||
if self.plex_token:
|
||||
LOG.debug('Found a plex.tv token in the settings')
|
||||
|
||||
def write_credentials_to_settings(self):
|
||||
"""
|
||||
Writes Plex username, token to plex.tv and Plex id to PKC settings
|
||||
"""
|
||||
utils.settings('username', value=self.plex_login or '')
|
||||
utils.settings('userid', value=self.plex_login_id or '')
|
||||
utils.settings('plexToken', value=self.plex_token or '')
|
||||
|
||||
@staticmethod
|
||||
def save_pms_settings(url, token):
|
||||
"""
|
||||
Sets certain settings for server by asking for the PMS' settings
|
||||
Call with url: scheme://ip:port
|
||||
"""
|
||||
xml = PF.get_PMS_settings(url, token)
|
||||
try:
|
||||
xml.attrib
|
||||
except AttributeError:
|
||||
LOG.error('Could not get PMS settings for %s', url)
|
||||
return
|
||||
for entry in xml:
|
||||
if entry.attrib.get('id', '') == 'allowMediaDeletion':
|
||||
value = 'true' if entry.get('value', '1') == '1' else 'false'
|
||||
utils.settings('plex_allows_mediaDeletion', value=value)
|
||||
utils.window('plex_allows_mediaDeletion', value=value)
|
||||
|
||||
@staticmethod
|
||||
def enter_new_pms_address():
|
||||
LOG.info('Start getting manual PMS address and port')
|
||||
# "Enter your Plex Media Server's IP or URL. Examples are:"
|
||||
utils.messageDialog(utils.lang(29999),
|
||||
'%s\n%s\n%s' % (utils.lang(39215),
|
||||
'192.168.1.2',
|
||||
'plex.myServer.org'))
|
||||
# "Enter PMS IP or URL"
|
||||
address = utils.dialog('input', utils.lang(39083))
|
||||
if not address:
|
||||
return False
|
||||
port = utils.dialog('input', utils.lang(39084), '32400', type='{numeric}')
|
||||
if not port:
|
||||
return False
|
||||
url = '%s:%s' % (address, port)
|
||||
# "Use HTTPS (SSL) connections? Answer should probably be yes."
|
||||
https = utils.yesno_dialog(utils.lang(29999), utils.lang(39217))
|
||||
if https:
|
||||
url = 'https://%s' % url
|
||||
else:
|
||||
url = 'http://%s' % url
|
||||
https = 'true' if https else 'false'
|
||||
# Try to connect first
|
||||
error = False
|
||||
try:
|
||||
machine_identifier = PF.GetMachineIdentifier(url)
|
||||
except exceptions.SSLError:
|
||||
LOG.error('SSL cert error contacting %s', url)
|
||||
# "SSL certificate failed to validate. Please check {0}
|
||||
# for solutions."
|
||||
utils.messageDialog(utils.lang(29999),
|
||||
utils.lang(30503).format('github.com/croneter/PlexKodiConnect/issues'))
|
||||
return
|
||||
except Exception:
|
||||
error = True
|
||||
if error or machine_identifier is None:
|
||||
LOG.error('Could not even get a machineIdentifier for %s', url)
|
||||
# "Server is unreachable"
|
||||
utils.messageDialog(utils.lang(29999), utils.lang(33002))
|
||||
return
|
||||
# Let's use the main account's token, not managed user token
|
||||
token = utils.settings('plexToken')
|
||||
xml = PF.pms_root(url, token)
|
||||
if xml == 401:
|
||||
LOG.error('Not yet authorized for %s', url)
|
||||
# "User is unauthorized for server {0}",
|
||||
# "Please sign in to plex.tv."
|
||||
utils.messageDialog(utils.lang(29999),
|
||||
'%s. %s' % (utils.lang(33010).format(address),
|
||||
utils.lang(39014)))
|
||||
return
|
||||
try:
|
||||
xml[0].attrib
|
||||
except (IndexError, TypeError, AttributeError):
|
||||
LOG.error('Could not get PMS root directory for %s', url)
|
||||
# "Error contacting PMS"
|
||||
utils.messageDialog(utils.lang(29999), utils.lang(39218))
|
||||
return
|
||||
pms = {
|
||||
'baseURL': url,
|
||||
'ip': address,
|
||||
# Assume PMS is not local so we're not resetting verifyssl
|
||||
'local': False,
|
||||
'machineIdentifier': xml.get('machineIdentifier'),
|
||||
'name': xml.get('friendlyName'),
|
||||
# Assume that we own this PMS - no easy way to check
|
||||
'owned': True,
|
||||
'platform': xml.get('platform'),
|
||||
'port': port,
|
||||
# 'relay': True,
|
||||
'scheme': 'https' if https else 'http',
|
||||
'token': token,
|
||||
'version': xml.get('version')
|
||||
}
|
||||
return pms
|
||||
|
||||
def plex_tv_sign_in(self):
|
||||
"""
|
||||
Signs (freshly) in to plex.tv (will be saved to file settings)
|
||||
|
||||
Returns True if successful, or False if not
|
||||
"""
|
||||
user = plex_tv.sign_in_with_pin()
|
||||
try:
|
||||
user = plex_tv.sign_in_with_pin()
|
||||
except:
|
||||
utils.ERROR()
|
||||
if user:
|
||||
self.plex_login = user.username
|
||||
self.plex_token = user.authToken
|
||||
self.plex_login_id = user.id
|
||||
self.plexid = user.id
|
||||
return True
|
||||
return False
|
||||
|
||||
|
@ -196,12 +211,12 @@ class InitialSetup(object):
|
|||
LOG.error('Failed to update Plex info from plex.tv')
|
||||
else:
|
||||
utils.settings('plexLogin', value=self.plex_login)
|
||||
home = 'true' if xml.attrib.get('home') == '1' else 'false'
|
||||
utils.settings('plexAvatar', value=xml.attrib.get('thumb'))
|
||||
LOG.info('Updated Plex info from plex.tv')
|
||||
return answer
|
||||
|
||||
@staticmethod
|
||||
def check_existing_pms():
|
||||
def check_existing_pms(self):
|
||||
"""
|
||||
Check the PMS that was set in file settings.
|
||||
Will return False if we need to reconnect, because:
|
||||
|
@ -212,27 +227,26 @@ class InitialSetup(object):
|
|||
not set before
|
||||
"""
|
||||
answer = True
|
||||
chk = PF.check_connection(app.CONN.server,
|
||||
verifySSL=True if v.KODIVERSION >= 18 else False)
|
||||
chk = PF.check_connection(self.server, verifySSL=False)
|
||||
if chk is False:
|
||||
LOG.warn('Could not reach PMS %s', app.CONN.server)
|
||||
LOG.warn('Could not reach PMS %s', self.server)
|
||||
answer = False
|
||||
if answer is True and not app.CONN.machine_identifier:
|
||||
if answer is True and not self.serverid:
|
||||
LOG.info('No PMS machineIdentifier found for %s. Trying to '
|
||||
'get the PMS unique ID', app.CONN.server)
|
||||
app.CONN.machine_identifier = PF.GetMachineIdentifier(app.CONN.server)
|
||||
if app.CONN.machine_identifier is None:
|
||||
'get the PMS unique ID', self.server)
|
||||
self.serverid = PF.GetMachineIdentifier(self.server)
|
||||
if self.serverid is None:
|
||||
LOG.warn('Could not retrieve machineIdentifier')
|
||||
answer = False
|
||||
else:
|
||||
utils.settings('plex_machineIdentifier', value=app.CONN.machine_identifier)
|
||||
utils.settings('plex_machineIdentifier', value=self.serverid)
|
||||
elif answer is True:
|
||||
temp_server_id = PF.GetMachineIdentifier(app.CONN.server)
|
||||
if temp_server_id != app.CONN.machine_identifier:
|
||||
temp_server_id = PF.GetMachineIdentifier(self.server)
|
||||
if temp_server_id != self.serverid:
|
||||
LOG.warn('The current PMS %s was expected to have a '
|
||||
'unique machineIdentifier of %s. But we got '
|
||||
'%s. Pick a new server to be sure',
|
||||
app.CONN.server, app.CONN.machine_identifier, temp_server_id)
|
||||
self.server, self.serverid, temp_server_id)
|
||||
answer = False
|
||||
return answer
|
||||
|
||||
|
@ -241,20 +255,22 @@ 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']:
|
||||
# Deactive SSL verification if the server is local for Kodi 17
|
||||
verifySSL = True if v.KODIVERSION >= 18 else False
|
||||
url = ('%s://%s:%s'
|
||||
% (server['scheme'], server['ip'], server['port']))
|
||||
# Deactive SSL verification if the server is local!
|
||||
verifySSL = False
|
||||
else:
|
||||
url = server['baseURL']
|
||||
verifySSL = 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):
|
||||
def pick_pms(self, showDialog=False):
|
||||
"""
|
||||
Searches for PMS in local Lan and optionally (if self.plex_token set)
|
||||
also on plex.tv
|
||||
|
@ -287,16 +303,19 @@ 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:
|
||||
if not self.server or not self.serverid:
|
||||
showDialog = True
|
||||
if showDialog is True:
|
||||
server = self._user_pick_pms()
|
||||
else:
|
||||
server = self._auto_pick_pms(show_dialog=inform_of_search)
|
||||
server = self._auto_pick_pms()
|
||||
if server is not None:
|
||||
_write_pms_settings(server['baseURL'], server['token'])
|
||||
return server
|
||||
|
||||
def _auto_pick_pms(self, show_dialog=False):
|
||||
def _auto_pick_pms(self):
|
||||
"""
|
||||
Will try to pick PMS based on machineIdentifier saved in file settings
|
||||
but only once
|
||||
|
@ -305,47 +324,35 @@ class InitialSetup(object):
|
|||
"""
|
||||
https_updated = False
|
||||
server = None
|
||||
if show_dialog:
|
||||
# Searching for PMS
|
||||
utils.dialog('notification',
|
||||
heading='{plex}',
|
||||
message=utils.lang(30001),
|
||||
icon='{plex}',
|
||||
time=60000)
|
||||
try:
|
||||
while True:
|
||||
if https_updated is False:
|
||||
serverlist = PF.discover_pms(self.plex_token)
|
||||
for item in serverlist:
|
||||
if item.get('machineIdentifier') == app.CONN.machine_identifier:
|
||||
server = item
|
||||
if server is None:
|
||||
name = utils.settings('plex_servername')
|
||||
LOG.warn('The PMS you have used before with a unique '
|
||||
'machineIdentifier of %s and name %s is '
|
||||
'offline', app.CONN.machine_identifier, name)
|
||||
return
|
||||
chk = self._check_pms_connectivity(server)
|
||||
if chk == 504 and https_updated is False:
|
||||
# switch HTTPS to HTTP or vice-versa
|
||||
if server['scheme'] == 'https':
|
||||
server['scheme'] = 'http'
|
||||
else:
|
||||
server['scheme'] = 'https'
|
||||
https_updated = True
|
||||
continue
|
||||
# Problems connecting
|
||||
elif chk >= 400 or chk is False:
|
||||
LOG.warn('Problems connecting to server %s. chk is %s',
|
||||
server['name'], chk)
|
||||
while True:
|
||||
if https_updated is False:
|
||||
serverlist = PF.discover_pms(self.plex_token)
|
||||
for item in serverlist:
|
||||
if item.get('machineIdentifier') == self.serverid:
|
||||
server = item
|
||||
if server is None:
|
||||
name = utils.settings('plex_servername')
|
||||
LOG.warn('The PMS you have used before with a unique '
|
||||
'machineIdentifier of %s and name %s is '
|
||||
'offline', self.serverid, name)
|
||||
return
|
||||
LOG.info('We found a server to automatically connect to: %s',
|
||||
server['name'])
|
||||
return server
|
||||
finally:
|
||||
if show_dialog:
|
||||
executebuiltin("Dialog.Close(all, true)")
|
||||
|
||||
chk = self._check_pms_connectivity(server)
|
||||
if chk == 504 and https_updated is False:
|
||||
# switch HTTPS to HTTP or vice-versa
|
||||
if server['scheme'] == 'https':
|
||||
server['scheme'] = 'http'
|
||||
else:
|
||||
server['scheme'] = 'https'
|
||||
https_updated = True
|
||||
continue
|
||||
# Problems connecting
|
||||
elif chk >= 400 or chk is False:
|
||||
LOG.warn('Problems connecting to server %s. chk is %s',
|
||||
server['name'], chk)
|
||||
return
|
||||
LOG.info('We found a server to automatically connect to: %s',
|
||||
server['name'])
|
||||
return server
|
||||
|
||||
def _user_pick_pms(self):
|
||||
"""
|
||||
|
@ -359,7 +366,7 @@ class InitialSetup(object):
|
|||
heading='{plex}',
|
||||
message=utils.lang(30001),
|
||||
icon='{plex}',
|
||||
time=60000)
|
||||
time=5000)
|
||||
while True:
|
||||
if https_updated is False:
|
||||
serverlist = PF.discover_pms(self.plex_token)
|
||||
|
@ -388,8 +395,6 @@ class InitialSetup(object):
|
|||
dialoglist.append('%s (%s)'
|
||||
% (server['name'], msg))
|
||||
# Let user pick server from a list
|
||||
# Close the PKC info "Searching for PMS"
|
||||
executebuiltin("Dialog.Close(all, true)")
|
||||
resp = utils.dialog('select', utils.lang(39012), dialoglist)
|
||||
if resp == -1:
|
||||
# User cancelled
|
||||
|
@ -461,31 +466,6 @@ class InitialSetup(object):
|
|||
server['machineIdentifier'], server['ip'], server['port'],
|
||||
server['scheme'])
|
||||
|
||||
@staticmethod
|
||||
def _add_sources(root, extension):
|
||||
changed = False
|
||||
count = 2
|
||||
for source in root.findall('.//path'):
|
||||
if source.text == extension:
|
||||
count -= 1
|
||||
if count == 0:
|
||||
# sources already set
|
||||
break
|
||||
else:
|
||||
# Missing smb:// occurences, re-add.
|
||||
changed = True
|
||||
for _ in range(0, count):
|
||||
source = etree.SubElement(root, 'source')
|
||||
etree.SubElement(
|
||||
source,
|
||||
'name').text = "PlexKodiConnect Masterlock Hack"
|
||||
etree.SubElement(
|
||||
source,
|
||||
'path',
|
||||
{'pathversion': "1"}).text = extension
|
||||
etree.SubElement(source, 'allowsharing').text = "true"
|
||||
return changed
|
||||
|
||||
def setup(self):
|
||||
"""
|
||||
Initial setup. Run once upon startup.
|
||||
|
@ -504,13 +484,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'],
|
||||
|
@ -518,10 +494,9 @@ class InitialSetup(object):
|
|||
xml.set_setting(['video', 'ignoresecondsatstart'],
|
||||
value='60')
|
||||
reboot = xml.write_xml
|
||||
except utils.ParseError:
|
||||
except etree.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)
|
||||
|
@ -532,41 +507,40 @@ class InitialSetup(object):
|
|||
with utils.XmlKodiSetting('sources.xml',
|
||||
force_create=True,
|
||||
top_element='sources') as xml:
|
||||
changed = False
|
||||
for extension in ('smb://', 'nfs://'):
|
||||
root = xml.set_setting(['video'])
|
||||
changed = self._add_sources(root, extension) or changed
|
||||
if changed:
|
||||
xml.write_xml = True
|
||||
reboot = True
|
||||
except utils.ParseError:
|
||||
root = xml.set_setting(['video'])
|
||||
count = 2
|
||||
for source in root.findall('.//path'):
|
||||
if source.text == "smb://":
|
||||
count -= 1
|
||||
if count == 0:
|
||||
# sources already set
|
||||
break
|
||||
else:
|
||||
# Missing smb:// occurences, re-add.
|
||||
for _ in range(0, count):
|
||||
source = etree.SubElement(root, 'source')
|
||||
etree.SubElement(
|
||||
source,
|
||||
'name').text = "PlexKodiConnect Masterlock Hack"
|
||||
etree.SubElement(
|
||||
source,
|
||||
'path',
|
||||
attrib={'pathversion': "1"}).text = "smb://"
|
||||
etree.SubElement(source, 'allowsharing').text = "true"
|
||||
if reboot is False:
|
||||
reboot = xml.write_xml
|
||||
except etree.ParseError:
|
||||
pass
|
||||
|
||||
# Do we need to migrate stuff?
|
||||
migration.check_migration()
|
||||
# Reload the server IP cause we might've deleted it during migration
|
||||
app.CONN.load()
|
||||
self.server = userclient.UserClient().get_server()
|
||||
|
||||
# Display a warning if Kodi puts ALL movies into the queue, basically
|
||||
# breaking playback reporting for PKC
|
||||
warn = False
|
||||
settings = js.settings_getsettingvalue('videoplayer.autoplaynextitem')
|
||||
if v.KODIVERSION >= 18:
|
||||
# Answer for videoplayer.autoplaynextitem:
|
||||
# [{u'label': u'Music videos', u'value': 0},
|
||||
# {u'label': u'TV shows', u'value': 1},
|
||||
# {u'label': u'Episodes', u'value': 2},
|
||||
# {u'label': u'Movies', u'value': 3},
|
||||
# {u'label': u'Uncategorized', u'value': 4}]
|
||||
if 1 in settings or 2 in settings or 3 in settings:
|
||||
warn = True
|
||||
else:
|
||||
# Kodi Krypton: answer is boolean
|
||||
if settings:
|
||||
warn = True
|
||||
if warn:
|
||||
LOG.warn('Kodi setting videoplayer.autoplaynextitem is: %s',
|
||||
settings)
|
||||
if js.settings_getsettingvalue('videoplayer.autoplaynextitem'):
|
||||
LOG.warn('Kodi setting videoplayer.autoplaynextitem is enabled!')
|
||||
if utils.settings('warned_setting_videoplayer.autoplaynextitem') == 'false':
|
||||
# Only warn once
|
||||
utils.settings('warned_setting_videoplayer.autoplaynextitem',
|
||||
|
@ -574,65 +548,40 @@ class InitialSetup(object):
|
|||
# Warning: Kodi setting "Play next video automatically" is
|
||||
# enabled. This could break PKC. Deactivate?
|
||||
if utils.yesno_dialog(utils.lang(29999), utils.lang(30003)):
|
||||
if v.KODIVERSION >= 18:
|
||||
for i in (1, 2, 3):
|
||||
try:
|
||||
settings.remove(i)
|
||||
except ValueError:
|
||||
pass
|
||||
js.settings_setsettingvalue('videoplayer.autoplaynextitem',
|
||||
settings)
|
||||
else:
|
||||
js.settings_setsettingvalue('videoplayer.autoplaynextitem',
|
||||
False)
|
||||
js.settings_setsettingvalue('videoplayer.autoplaynextitem',
|
||||
False)
|
||||
# Set any video library updates to happen in the background in order to
|
||||
# hide "Compressing database"
|
||||
js.settings_setsettingvalue('videolibrary.backgroundupdate', True)
|
||||
|
||||
# If a Plex server IP has already been set
|
||||
# return only if the right machine identifier is found
|
||||
if app.CONN.server:
|
||||
LOG.info("PMS is already set: %s. Checking now...", app.CONN.server)
|
||||
if self.server:
|
||||
LOG.info("PMS is already set: %s. Checking now...", self.server)
|
||||
if self.check_existing_pms():
|
||||
LOG.info("Using PMS %s with machineIdentifier %s",
|
||||
app.CONN.server, app.CONN.machine_identifier)
|
||||
self.save_pms_settings(app.CONN.server, self.pms_token)
|
||||
if utils.settings('kodi_db_has_been_wiped_clean') == 'false':
|
||||
# If the user chose to go to the PKC settings on the first run
|
||||
# Will trigger a reboot
|
||||
utils.wipe_database()
|
||||
self.server, self.serverid)
|
||||
_write_pms_settings(self.server, self.pms_token)
|
||||
if reboot is True:
|
||||
utils.reboot_kodi()
|
||||
return
|
||||
else:
|
||||
LOG.info('No PMS set yet')
|
||||
|
||||
# If not already retrieved myplex info, optionally let user sign in
|
||||
# to plex.tv. This DOES get called on very first install run
|
||||
if not self.plex_token and app.ACCOUNT.myplexlogin:
|
||||
if not self.plex_token and self.myplexlogin:
|
||||
self.plex_tv_sign_in()
|
||||
|
||||
server = self.pick_pms(inform_of_search=True)
|
||||
server = self.pick_pms()
|
||||
if server is not None:
|
||||
# Write our chosen server to Kodi settings file
|
||||
self.save_pms_settings(server['baseURL'], server['token'])
|
||||
self.write_pms_to_settings(server)
|
||||
|
||||
# User already answered the installation questions
|
||||
if utils.settings('InstallQuestionsAnswered') == 'true':
|
||||
LOG.info('Installation questions already answered')
|
||||
if utils.settings('kodi_db_has_been_wiped_clean') == 'false':
|
||||
# If the user chose to go to the PKC settings on the first run
|
||||
# Will trigger a reboot
|
||||
utils.wipe_database()
|
||||
if reboot is True:
|
||||
utils.reboot_kodi()
|
||||
# Reload relevant settings
|
||||
app.CONN.load()
|
||||
app.ACCOUNT.load()
|
||||
app.SYNC.load()
|
||||
return
|
||||
LOG.info('Showing install questions')
|
||||
|
||||
# Additional settings where the user needs to choose
|
||||
# Direct paths (\\NAS\mymovie.mkv) or addon (http)?
|
||||
goto_settings = False
|
||||
|
@ -644,11 +593,7 @@ 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')
|
||||
state.DIRECT_PATHS = 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)):
|
||||
|
@ -668,8 +613,7 @@ class InitialSetup(object):
|
|||
# Go to network credentials?
|
||||
if utils.yesno_dialog(utils.lang(39029), utils.lang(39030)):
|
||||
LOG.debug("Presenting network credentials dialog.")
|
||||
from .windows import direct_path_sources
|
||||
direct_path_sources.start()
|
||||
utils.passwords_xml()
|
||||
# Disable Plex music?
|
||||
if utils.yesno_dialog(utils.lang(29999), utils.lang(39016)):
|
||||
LOG.debug("User opted to disable Plex music library.")
|
||||
|
@ -694,16 +638,17 @@ class InitialSetup(object):
|
|||
# Make sure that we only ask these questions upon first installation
|
||||
utils.settings('InstallQuestionsAnswered', value='true')
|
||||
|
||||
# New installation - make sure we start with a clean slate
|
||||
from . import kodidb_functions
|
||||
kodidb_functions.wipe_kodi_dbs()
|
||||
|
||||
if goto_settings is False:
|
||||
# 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
|
||||
state.PMS_STATUS = 'Stop'
|
||||
executebuiltin(
|
||||
'Addon.OpenSettings(plugin.video.plexkodiconnect)')
|
||||
return
|
||||
utils.reboot_kodi()
|
||||
elif reboot is True:
|
||||
utils.reboot_kodi()
|
||||
|
|
1842
resources/lib/itemtypes.py
Normal file
1842
resources/lib/itemtypes.py
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,30 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from .movies import Movie
|
||||
from .tvshows import Show, Season, Episode
|
||||
from .music import Artist, Album, Song
|
||||
from .. import variables as v
|
||||
|
||||
# Note: always use same order of URL arguments, NOT urlencode:
|
||||
# plex_id=<plex_id>&plex_type=<plex_type>&mode=play
|
||||
|
||||
ITEMTYPE_FROM_PLEXTYPE = {
|
||||
v.PLEX_TYPE_MOVIE: Movie,
|
||||
v.PLEX_TYPE_SHOW: Show,
|
||||
v.PLEX_TYPE_SEASON: Season,
|
||||
v.PLEX_TYPE_EPISODE: Episode,
|
||||
v.PLEX_TYPE_ARTIST: Artist,
|
||||
v.PLEX_TYPE_ALBUM: Album,
|
||||
v.PLEX_TYPE_SONG: Song
|
||||
}
|
||||
|
||||
ITEMTYPE_FROM_KODITYPE = {
|
||||
v.KODI_TYPE_MOVIE: Movie,
|
||||
v.KODI_TYPE_SHOW: Show,
|
||||
v.KODI_TYPE_SEASON: Season,
|
||||
v.KODI_TYPE_EPISODE: Episode,
|
||||
v.KODI_TYPE_ARTIST: Artist,
|
||||
v.KODI_TYPE_ALBUM: Album,
|
||||
v.KODI_TYPE_SONG: Song
|
||||
}
|
|
@ -1,171 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
from ntpath import dirname
|
||||
|
||||
from ..plex_db import PlexDB, PLEXDB_LOCK
|
||||
from ..kodi_db import KodiVideoDB, KODIDB_LOCK
|
||||
from .. import db, timing, app
|
||||
|
||||
LOG = getLogger('PLEX.itemtypes.common')
|
||||
|
||||
# Note: always use same order of URL arguments, NOT urlencode:
|
||||
# plex_id=<plex_id>&plex_type=<plex_type>&mode=play
|
||||
|
||||
|
||||
def process_path(playurl):
|
||||
"""
|
||||
Do NOT use os.path since we have paths that might not apply to the current
|
||||
OS!
|
||||
"""
|
||||
if '\\' in playurl:
|
||||
# Local path
|
||||
path = '%s\\' % playurl
|
||||
toplevelpath = '%s\\' % dirname(dirname(path))
|
||||
else:
|
||||
# Network path
|
||||
path = '%s/' % playurl
|
||||
toplevelpath = '%s/' % dirname(dirname(path))
|
||||
return path, toplevelpath
|
||||
|
||||
|
||||
class ItemBase(object):
|
||||
"""
|
||||
Items to be called with "with Items() as xxx:" to ensure that __enter__
|
||||
method is called (opens db connections)
|
||||
|
||||
Input:
|
||||
kodiType: optional argument; e.g. 'video' or 'music'
|
||||
"""
|
||||
def __init__(self, last_sync, plexdb=None, kodidb=None, lock=True):
|
||||
self.last_sync = last_sync
|
||||
self.lock = lock
|
||||
self.plexdb = plexdb
|
||||
self.kodidb = kodidb
|
||||
self.plexconn = plexdb.plexconn if plexdb else None
|
||||
self.plexcursor = plexdb.cursor if plexdb else None
|
||||
self.kodiconn = kodidb.kodiconn if kodidb else None
|
||||
self.kodicursor = kodidb.cursor if kodidb else None
|
||||
self.artconn = kodidb.artconn if kodidb else None
|
||||
self.artcursor = kodidb.artcursor if kodidb else None
|
||||
|
||||
def __enter__(self):
|
||||
"""
|
||||
Open DB connections and cursors
|
||||
"""
|
||||
if self.lock:
|
||||
PLEXDB_LOCK.acquire()
|
||||
KODIDB_LOCK.acquire()
|
||||
self.plexconn = db.connect('plex')
|
||||
self.plexcursor = self.plexconn.cursor()
|
||||
self.kodiconn = db.connect('video')
|
||||
self.kodicursor = self.kodiconn.cursor()
|
||||
self.artconn = db.connect('texture')
|
||||
self.artcursor = self.artconn.cursor()
|
||||
self.plexdb = PlexDB(plexconn=self.plexconn, lock=False)
|
||||
self.kodidb = KodiVideoDB(texture_db=True,
|
||||
kodiconn=self.kodiconn,
|
||||
artconn=self.artconn,
|
||||
lock=False)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""
|
||||
Make sure DB changes are committed and connection to DB is closed.
|
||||
"""
|
||||
try:
|
||||
if exc_type:
|
||||
# re-raise any exception
|
||||
return False
|
||||
self.plexconn.commit()
|
||||
self.kodiconn.commit()
|
||||
if self.artconn:
|
||||
self.artconn.commit()
|
||||
return self
|
||||
finally:
|
||||
self.plexconn.close()
|
||||
self.kodiconn.close()
|
||||
if self.artconn:
|
||||
self.artconn.close()
|
||||
if self.lock:
|
||||
PLEXDB_LOCK.release()
|
||||
KODIDB_LOCK.release()
|
||||
|
||||
def commit(self):
|
||||
self.plexconn.commit()
|
||||
self.plexconn.execute('BEGIN')
|
||||
self.kodiconn.commit()
|
||||
self.kodiconn.execute('BEGIN')
|
||||
if self.artconn:
|
||||
self.artconn.commit()
|
||||
self.artconn.execute('BEGIN')
|
||||
|
||||
def set_fanart(self, artworks, kodi_id, kodi_type):
|
||||
"""
|
||||
Writes artworks [dict containing only set artworks] to the Kodi art DB
|
||||
"""
|
||||
self.kodidb.modify_artwork(artworks,
|
||||
kodi_id,
|
||||
kodi_type)
|
||||
|
||||
def update_playstate(self, mark_played, view_count, resume, duration,
|
||||
kodi_fileid, kodi_fileid_2, lastViewedAt):
|
||||
"""
|
||||
Use with websockets, not xml
|
||||
"""
|
||||
# If the playback was stopped, check whether we need to increment the
|
||||
# playcount. PMS won't tell us the playcount via websockets
|
||||
if mark_played:
|
||||
LOG.info('Marking item as completely watched in Kodi')
|
||||
try:
|
||||
view_count += 1
|
||||
except TypeError:
|
||||
view_count = 1
|
||||
resume = 0
|
||||
# Do the actual update
|
||||
self.kodidb.set_resume(kodi_fileid,
|
||||
resume,
|
||||
duration,
|
||||
view_count,
|
||||
timing.plex_date_to_kodi(lastViewedAt))
|
||||
if kodi_fileid_2:
|
||||
# Our dirty hack for episodes
|
||||
self.kodidb.set_resume(kodi_fileid_2,
|
||||
resume,
|
||||
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
|
|
@ -1,245 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from .common import ItemBase
|
||||
from ..plex_api import API
|
||||
from .. import app, variables as v, plex_functions as PF
|
||||
|
||||
LOG = getLogger('PLEX.movies')
|
||||
|
||||
|
||||
class Movie(ItemBase):
|
||||
"""
|
||||
Used for plex library-type movies
|
||||
"""
|
||||
def add_update(self, xml, section_name=None, section_id=None,
|
||||
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())
|
||||
return
|
||||
plex_id = api.plex_id
|
||||
movie = self.plexdb.movie(plex_id)
|
||||
if movie:
|
||||
update_item = True
|
||||
kodi_id = movie['kodi_id']
|
||||
old_kodi_fileid = movie['kodi_fileid']
|
||||
kodi_pathid = movie['kodi_pathid']
|
||||
else:
|
||||
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:
|
||||
kodi_pathid = self.kodidb.get_path(path)
|
||||
|
||||
if update_item:
|
||||
LOG.info('UPDATE movie plex_id: %s - %s', plex_id, api.title())
|
||||
file_id = self.kodidb.modify_file(filename,
|
||||
kodi_pathid,
|
||||
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)
|
||||
self.kodidb.modify_people(kodi_id,
|
||||
v.KODI_TYPE_MOVIE,
|
||||
api.people())
|
||||
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())
|
||||
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)
|
||||
self.kodidb.add_people(kodi_id,
|
||||
v.KODI_TYPE_MOVIE,
|
||||
api.people())
|
||||
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(),
|
||||
api.plot(),
|
||||
api.shortplot(),
|
||||
api.tagline(),
|
||||
api.votecount(),
|
||||
rating_id,
|
||||
api.list_to_string(api.writers()),
|
||||
api.year(),
|
||||
unique_id,
|
||||
api.sorttitle(),
|
||||
api.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.trailer(),
|
||||
api.list_to_string(api.countries()),
|
||||
fullpath,
|
||||
kodi_pathid,
|
||||
api.premiere_date(),
|
||||
api.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_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_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())
|
||||
self.plexdb.add_movie(plex_id=plex_id,
|
||||
checksum=api.checksum(),
|
||||
section_id=section_id,
|
||||
kodi_id=kodi_id,
|
||||
kodi_fileid=file_id,
|
||||
kodi_pathid=kodi_pathid,
|
||||
last_sync=self.last_sync)
|
||||
|
||||
def remove(self, plex_id, plex_type=None):
|
||||
"""
|
||||
Remove a movie with all references and all orphaned associated entries
|
||||
from the Kodi DB
|
||||
"""
|
||||
movie = self.plexdb.movie(plex_id)
|
||||
try:
|
||||
kodi_id = movie['kodi_id']
|
||||
file_id = movie['kodi_fileid']
|
||||
kodi_type = v.KODI_TYPE_MOVIE
|
||||
LOG.debug('Removing movie with plex_id %s, kodi_id: %s',
|
||||
plex_id, kodi_id)
|
||||
except TypeError:
|
||||
LOG.error('Movie with plex_id %s not found - cannot delete',
|
||||
plex_id)
|
||||
return
|
||||
# Remove the plex reference
|
||||
self.plexdb.remove(plex_id, v.PLEX_TYPE_MOVIE)
|
||||
# Remove artwork
|
||||
self.kodidb.delete_artwork(kodi_id, kodi_type)
|
||||
set_id = self.kodidb.get_set_id(kodi_id)
|
||||
self.kodidb.modify_countries(kodi_id, kodi_type)
|
||||
self.kodidb.modify_people(kodi_id, kodi_type)
|
||||
self.kodidb.modify_genres(kodi_id, kodi_type)
|
||||
self.kodidb.modify_studios(kodi_id, kodi_type)
|
||||
self.kodidb.modify_tags(kodi_id, kodi_type)
|
||||
# Delete kodi movie and file
|
||||
self.kodidb.remove_file(file_id)
|
||||
self.kodidb.remove_movie(kodi_id)
|
||||
if set_id:
|
||||
self.kodidb.delete_possibly_empty_set(set_id)
|
||||
self.kodidb.remove_uniqueid(kodi_id, kodi_type)
|
||||
self.kodidb.remove_ratings(kodi_id, kodi_type)
|
||||
LOG.debug('Deleted movie %s from kodi database', plex_id)
|
||||
|
||||
def update_userdata(self, xml_element, plex_type):
|
||||
"""
|
||||
Updates the Kodi watched state of the item from PMS. Also retrieves
|
||||
Plex resume points for movies in progress.
|
||||
|
||||
Returns True if successful, False otherwise (e.g. item missing)
|
||||
"""
|
||||
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)
|
||||
if not db_item:
|
||||
LOG.info('Item not yet synced: %s', xml_element.attrib)
|
||||
return False
|
||||
# Write to Kodi DB
|
||||
self.kodidb.set_resume(db_item['kodi_fileid'],
|
||||
api.resume_point(),
|
||||
api.runtime(),
|
||||
api.viewcount(),
|
||||
api.lastplayed())
|
||||
self.kodidb.update_userrating(db_item['kodi_id'],
|
||||
db_item['kodi_type'],
|
||||
api.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')))
|
|
@ -1,635 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
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
|
||||
|
||||
LOG = getLogger('PLEX.music')
|
||||
|
||||
|
||||
class MusicMixin(object):
|
||||
def __enter__(self):
|
||||
"""
|
||||
Overwrite to use the Kodi music DB instead of the video DB
|
||||
"""
|
||||
if self.lock:
|
||||
PLEXDB_LOCK.acquire()
|
||||
KODIDB_LOCK.acquire()
|
||||
self.plexconn = db.connect('plex')
|
||||
self.plexcursor = self.plexconn.cursor()
|
||||
self.kodiconn = db.connect('music')
|
||||
self.kodicursor = self.kodiconn.cursor()
|
||||
self.artconn = db.connect('texture')
|
||||
self.artcursor = self.artconn.cursor()
|
||||
self.plexdb = PlexDB(plexconn=self.plexconn, lock=False)
|
||||
self.kodidb = KodiMusicDB(texture_db=True,
|
||||
kodiconn=self.kodiconn,
|
||||
artconn=self.artconn,
|
||||
lock=False)
|
||||
return self
|
||||
|
||||
def update_userdata(self, xml_element, plex_type):
|
||||
"""
|
||||
Updates the Kodi watched state of the item from PMS. Also retrieves
|
||||
Plex resume points for movies in progress.
|
||||
|
||||
Returns True if successful, False otherwise (e.g. item missing)
|
||||
"""
|
||||
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)
|
||||
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
|
||||
self.kodidb.update_userrating(db_item['kodi_id'],
|
||||
db_item['kodi_type'],
|
||||
api.userrating())
|
||||
if plex_type == v.PLEX_TYPE_SONG:
|
||||
self.kodidb.set_playcount(api.viewcount(),
|
||||
api.lastplayed(),
|
||||
db_item['kodi_id'],)
|
||||
return True
|
||||
|
||||
def remove(self, plex_id, plex_type=None):
|
||||
"""
|
||||
Remove the entire music object, including all associated entries from
|
||||
both Plex and Kodi DBs
|
||||
"""
|
||||
db_item = self.plexdb.item_by_id(plex_id, plex_type)
|
||||
if not db_item:
|
||||
LOG.debug('Cannot delete plex_id %s - not found in DB', plex_id)
|
||||
return
|
||||
LOG.debug('Removing %s %s with kodi_id: %s',
|
||||
db_item['plex_type'], plex_id, db_item['kodi_id'])
|
||||
|
||||
# Remove the plex reference
|
||||
self.plexdb.remove(plex_id, db_item['plex_type'])
|
||||
|
||||
# SONG #####
|
||||
if db_item['plex_type'] == v.PLEX_TYPE_SONG:
|
||||
# Delete episode, verify season and tvshow
|
||||
self.remove_song(db_item['kodi_id'], db_item['kodi_pathid'])
|
||||
# Album verification
|
||||
if not self.plexdb.album_has_songs(db_item['album_id']):
|
||||
# No songleft for this album - so delete the album
|
||||
self.remove_album(db_item['parent_id'])
|
||||
self.plexdb.remove(db_item['album_id'], v.PLEX_TYPE_ALBUM)
|
||||
# Artist verification
|
||||
if (not self.plexdb.artist_has_albums(db_item['artist_id']) and
|
||||
not self.plexdb.artist_has_songs(db_item['artist_id'])):
|
||||
self.remove_artist(db_item['grandparent_id'])
|
||||
self.plexdb.remove(db_item['artist_id'], v.PLEX_TYPE_ARTIST)
|
||||
# ALBUM #####
|
||||
elif db_item['plex_type'] == v.PLEX_TYPE_ALBUM:
|
||||
# Remove songs, album, verify artist
|
||||
songs = list(self.plexdb.song_by_album(db_item['plex_id']))
|
||||
for song in songs:
|
||||
self.remove_song(song['kodi_id'], song['kodi_pathid'])
|
||||
self.plexdb.remove(song['plex_id'], v.PLEX_TYPE_SONG)
|
||||
# Remove the album
|
||||
self.remove_album(db_item['kodi_id'])
|
||||
# Show verification
|
||||
if (not self.plexdb.artist_has_albums(db_item['kodi_id']) and
|
||||
not self.plexdb.artist_has_songs(db_item['kodi_id'])):
|
||||
# There's no other album or song left, delete the artist
|
||||
self.remove_artist(db_item['parent_id'])
|
||||
self.plexdb.remove(db_item['artist_id'], v.KODI_TYPE_ARTIST)
|
||||
# ARTIST #####
|
||||
elif db_item['plex_type'] == v.PLEX_TYPE_ARTIST:
|
||||
# Remove songs, albums and the artist himself
|
||||
songs = list(self.plexdb.song_by_artist(db_item['plex_id']))
|
||||
for song in songs:
|
||||
self.remove_song(song['kodi_id'], song['kodi_pathid'])
|
||||
self.plexdb.remove(song['plex_id'], v.PLEX_TYPE_SONG)
|
||||
albums = list(self.plexdb.album_by_artist(db_item['plex_id']))
|
||||
for album in albums:
|
||||
self.remove_album(album['kodi_id'])
|
||||
self.plexdb.remove(album['plex_id'], v.PLEX_TYPE_ALBUM)
|
||||
self.remove_artist(db_item['kodi_id'])
|
||||
|
||||
LOG.debug('Deleted %s %s from all databases',
|
||||
db_item['plex_type'], db_item['plex_id'])
|
||||
|
||||
def remove_song(self, kodi_id, path_id=None):
|
||||
"""
|
||||
Remove song, orphaned artists and orphaned paths
|
||||
"""
|
||||
if not path_id:
|
||||
path_id = self.kodidb.path_id_from_song(kodi_id)
|
||||
self.kodidb.delete_song_from_song_artist(kodi_id)
|
||||
self.kodidb.remove_song(kodi_id)
|
||||
# Check whether we have orphaned path entries
|
||||
if not self.kodidb.path_id_from_song(kodi_id):
|
||||
self.kodidb.remove_path(path_id)
|
||||
if v.KODIVERSION < 18:
|
||||
self.kodidb.remove_albuminfosong(kodi_id)
|
||||
self.kodidb.delete_artwork(kodi_id, v.KODI_TYPE_SONG)
|
||||
|
||||
def remove_album(self, kodi_id):
|
||||
'''
|
||||
Remove an album
|
||||
'''
|
||||
if v.KODIVERSION < 18:
|
||||
self.kodidb.delete_album_from_album_genre(kodi_id)
|
||||
self.kodidb.remove_album(kodi_id)
|
||||
self.kodidb.delete_artwork(kodi_id, v.KODI_TYPE_ALBUM)
|
||||
|
||||
def remove_artist(self, kodi_id):
|
||||
'''
|
||||
Remove an artist and associated songs and albums
|
||||
'''
|
||||
self.kodidb.remove_artist(kodi_id)
|
||||
self.kodidb.delete_artwork(kodi_id, v.KODI_TYPE_ARTIST)
|
||||
|
||||
|
||||
class Artist(MusicMixin, ItemBase):
|
||||
"""
|
||||
For Plex library-type artists
|
||||
"""
|
||||
def add_update(self, xml, section_name=None, section_id=None,
|
||||
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())
|
||||
return
|
||||
plex_id = api.plex_id
|
||||
artist = self.plexdb.artist(plex_id)
|
||||
if not artist:
|
||||
update_item = False
|
||||
else:
|
||||
update_item = True
|
||||
kodi_id = artist['kodi_id']
|
||||
|
||||
# Not yet implemented by Plex
|
||||
musicBrainzId = None
|
||||
|
||||
if app.SYNC.artwork:
|
||||
artworks = api.artwork()
|
||||
if 'poster' in artworks:
|
||||
thumb = "<thumb>%s</thumb>" % artworks['poster']
|
||||
else:
|
||||
thumb = None
|
||||
if 'fanart' in artworks:
|
||||
fanart = "<fanart>%s</fanart>" % artworks['fanart']
|
||||
else:
|
||||
fanart = None
|
||||
else:
|
||||
thumb, fanart = None, None
|
||||
|
||||
# UPDATE THE ARTIST #####
|
||||
if update_item:
|
||||
LOG.info("UPDATE artist plex_id: %s - Name: %s", plex_id, api.title())
|
||||
# OR ADD THE ARTIST #####
|
||||
else:
|
||||
LOG.info("ADD artist plex_id: %s - Name: %s", plex_id, api.title())
|
||||
# safety checks: It looks like plex supports the same artist
|
||||
# multiple times.
|
||||
# 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()),
|
||||
api.plot(),
|
||||
thumb,
|
||||
fanart,
|
||||
timing.unix_date_to_kodi(self.last_sync),
|
||||
kodi_id)
|
||||
if app.SYNC.artwork:
|
||||
self.kodidb.modify_artwork(artworks,
|
||||
kodi_id,
|
||||
v.KODI_TYPE_ARTIST)
|
||||
self.plexdb.add_artist(plex_id,
|
||||
api.checksum(),
|
||||
section_id,
|
||||
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):
|
||||
"""
|
||||
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())
|
||||
return
|
||||
plex_id = api.plex_id
|
||||
album = self.plexdb.album(plex_id)
|
||||
if album:
|
||||
update_item = True
|
||||
kodi_id = album['kodi_id']
|
||||
else:
|
||||
update_item = False
|
||||
|
||||
# Parent artist - should always be present
|
||||
parent_id = api.parent_id()
|
||||
artist = self.plexdb.artist(parent_id)
|
||||
if not artist:
|
||||
LOG.info('Artist %s does not yet exist in DB', parent_id)
|
||||
artist_xml = PF.GetPlexMetadata(parent_id)
|
||||
try:
|
||||
artist_xml[0].attrib
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
LOG.error('Could not get artist %s xml for %s',
|
||||
parent_id, xml.attrib)
|
||||
return
|
||||
Artist(self.last_sync,
|
||||
plexdb=self.plexdb,
|
||||
kodidb=self.kodidb).add_update(artist_xml[0],
|
||||
section_name,
|
||||
section_id)
|
||||
artist = self.plexdb.artist(parent_id)
|
||||
if not artist:
|
||||
LOG.error('Adding artist %s failed for %s',
|
||||
parent_id, xml.attrib)
|
||||
return
|
||||
artist_id = artist['kodi_id']
|
||||
# See if we have a compilation - Plex does NOT feature a compilation
|
||||
# flag for albums
|
||||
compilation = 0
|
||||
if children is None:
|
||||
LOG.info('No children songs passed, getting them')
|
||||
children = PF.GetAllPlexChildren(plex_id)
|
||||
try:
|
||||
children[0].attrib
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
LOG.error('Could not get children for Plex id %s', plex_id)
|
||||
return
|
||||
for song in children:
|
||||
if song.get('originalTitle') is not None:
|
||||
compilation = 1
|
||||
break
|
||||
name = api.title()
|
||||
# Not yet implemented by Plex, let's use unique last.fm or gracenote
|
||||
musicBrainzId = None
|
||||
genre = api.list_to_string(api.genres())
|
||||
if app.SYNC.artwork:
|
||||
artworks = api.artwork()
|
||||
if 'poster' in artworks:
|
||||
thumb = "<thumb>%s</thumb>" % artworks['poster']
|
||||
else:
|
||||
thumb = None
|
||||
else:
|
||||
thumb = None
|
||||
|
||||
# UPDATE THE ALBUM #####
|
||||
if update_item:
|
||||
LOG.info("UPDATE album plex_id: %s - Name: %s", plex_id, name)
|
||||
if v.KODIVERSION >= 18:
|
||||
self.kodidb.update_album(name,
|
||||
musicBrainzId,
|
||||
api.artist_name(),
|
||||
genre,
|
||||
api.year(),
|
||||
compilation,
|
||||
api.plot(),
|
||||
thumb,
|
||||
api.list_to_string(api.studios()),
|
||||
api.userrating(),
|
||||
timing.unix_date_to_kodi(self.last_sync),
|
||||
'album',
|
||||
kodi_id)
|
||||
else:
|
||||
self.kodidb.update_album_17(name,
|
||||
musicBrainzId,
|
||||
api.artist_name(),
|
||||
genre,
|
||||
api.year(),
|
||||
compilation,
|
||||
api.plot(),
|
||||
thumb,
|
||||
api.list_to_string(api.studios()),
|
||||
api.userrating(),
|
||||
timing.unix_date_to_kodi(self.last_sync),
|
||||
'album',
|
||||
kodi_id)
|
||||
# OR ADD THE ALBUM #####
|
||||
else:
|
||||
LOG.info("ADD album plex_id: %s - Name: %s", plex_id, name)
|
||||
kodi_id = self.kodidb.new_album_id()
|
||||
if v.KODIVERSION >= 18:
|
||||
self.kodidb.add_album(kodi_id,
|
||||
name,
|
||||
musicBrainzId,
|
||||
api.artist_name(),
|
||||
genre,
|
||||
api.year(),
|
||||
compilation,
|
||||
api.plot(),
|
||||
thumb,
|
||||
api.list_to_string(api.studios()),
|
||||
api.userrating(),
|
||||
timing.unix_date_to_kodi(self.last_sync),
|
||||
'album')
|
||||
else:
|
||||
self.kodidb.add_album_17(kodi_id,
|
||||
name,
|
||||
musicBrainzId,
|
||||
api.artist_name(),
|
||||
genre,
|
||||
api.year(),
|
||||
compilation,
|
||||
api.plot(),
|
||||
thumb,
|
||||
api.list_to_string(api.studios()),
|
||||
api.userrating(),
|
||||
timing.unix_date_to_kodi(self.last_sync),
|
||||
'album')
|
||||
self.kodidb.add_albumartist(artist_id, kodi_id, api.artist_name())
|
||||
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,
|
||||
artist_id,
|
||||
parent_id,
|
||||
kodi_id,
|
||||
self.last_sync)
|
||||
# Add all children - all tracks
|
||||
if scan_children:
|
||||
context = Song(self.last_sync,
|
||||
plexdb=self.plexdb,
|
||||
kodidb=self.kodidb)
|
||||
for song in children:
|
||||
context.add_update(song,
|
||||
section_name=section_name,
|
||||
section_id=section_id,
|
||||
album_xml=xml,
|
||||
genres=api.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):
|
||||
"""
|
||||
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())
|
||||
return
|
||||
plex_id = api.plex_id
|
||||
song = self.plexdb.song(plex_id)
|
||||
if song:
|
||||
update_item = True
|
||||
kodi_id = song['kodi_id']
|
||||
kodi_pathid = song['kodi_pathid']
|
||||
else:
|
||||
update_item = False
|
||||
kodi_id = self.kodidb.add_song_id()
|
||||
artist_id = api.grandparent_id()
|
||||
album_id = api.parent_id()
|
||||
|
||||
# The grandparent Artist - should always be present for every song!
|
||||
artist = self.plexdb.artist(artist_id)
|
||||
if not artist:
|
||||
LOG.warn('Grandparent artist %s not found in DB, adding it',
|
||||
artist_id)
|
||||
artist_xml = PF.GetPlexMetadata(artist_id)
|
||||
try:
|
||||
artist_xml[0].attrib
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
LOG.error('Grandparent tvartist %s xml download failed for %s',
|
||||
artist_id, xml.attrib)
|
||||
return
|
||||
Artist(self.last_sync,
|
||||
plexdb=self.plexdb,
|
||||
kodidb=self.kodidb).add_update(artist_xml[0],
|
||||
section_name,
|
||||
section_id)
|
||||
artist = self.plexdb.artist(artist_id)
|
||||
if not artist:
|
||||
LOG.error('Still could not find grandparent artist %s for %s',
|
||||
artist_id, xml.attrib)
|
||||
return
|
||||
grandparent_id = artist['kodi_id']
|
||||
|
||||
# The parent Album
|
||||
if not album_id:
|
||||
# No album found, create a single's album
|
||||
LOG.info('Creating singles album')
|
||||
parent_id = self.kodidb.new_album_id()
|
||||
if v.KODIVERSION >= 18:
|
||||
self.kodidb.add_album(kodi_id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
genre,
|
||||
api.year(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
timing.unix_date_to_kodi(self.last_sync),
|
||||
'single')
|
||||
else:
|
||||
self.kodidb.add_album_17(kodi_id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
genre,
|
||||
api.year(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
timing.unix_date_to_kodi(self.last_sync),
|
||||
'single')
|
||||
else:
|
||||
album = self.plexdb.album(album_id)
|
||||
if not album:
|
||||
LOG.warn('Parent album %s not found in DB, adding it', album_id)
|
||||
album_xml = PF.GetPlexMetadata(album_id)
|
||||
try:
|
||||
album_xml[0].attrib
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
LOG.error('Parent album %s xml download failed for %s',
|
||||
album_id, xml.attrib)
|
||||
return
|
||||
Album(self.last_sync,
|
||||
plexdb=self.plexdb,
|
||||
kodidb=self.kodidb).add_update(album_xml[0],
|
||||
section_name,
|
||||
section_id,
|
||||
children=[xml],
|
||||
scan_children=False)
|
||||
album = self.plexdb.album(album_id)
|
||||
if not album:
|
||||
LOG.error('Still could not find parent album %s for %s',
|
||||
album_id, xml.attrib)
|
||||
return
|
||||
parent_id = album['kodi_id']
|
||||
|
||||
title = api.title()
|
||||
# Not yet implemented by Plex
|
||||
musicBrainzId = None
|
||||
comment = None
|
||||
# Getting artists name is complicated
|
||||
if compilation is not None:
|
||||
if compilation == 0:
|
||||
artists = api.grandparent_title()
|
||||
else:
|
||||
artists = xml.get('originalTitle')
|
||||
else:
|
||||
# compilation not set
|
||||
artists = xml.get('originalTitle', api.grandparent_title())
|
||||
tracknumber = api.index() or 0
|
||||
disc = api.disc_number() or 1
|
||||
if disc == 1:
|
||||
track = tracknumber
|
||||
else:
|
||||
track = disc * 2 ** 16 + tracknumber
|
||||
year = api.year()
|
||||
if not year and album_xml is not None:
|
||||
# Plex did not pass year info - get it from the parent album
|
||||
album_api = API(album_xml)
|
||||
year = album_api.year()
|
||||
moods = []
|
||||
for entry in xml:
|
||||
if entry.tag == 'Mood':
|
||||
moods.append(entry.attrib['tag'])
|
||||
mood = api.list_to_string(moods)
|
||||
_, path, filename = api.fullpath()
|
||||
# UPDATE THE SONG #####
|
||||
if update_item:
|
||||
LOG.info("UPDATE song plex_id: %s - %s", plex_id, title)
|
||||
# Use dummy strHash '123' for Kodi
|
||||
self.kodidb.update_path(path, kodi_pathid)
|
||||
# Update the song entry
|
||||
if v.KODIVERSION >= 18:
|
||||
# Kodi Leia
|
||||
self.kodidb.update_song(parent_id,
|
||||
artists,
|
||||
genre,
|
||||
title,
|
||||
track,
|
||||
api.runtime(),
|
||||
year,
|
||||
filename,
|
||||
api.viewcount(),
|
||||
api.lastplayed(),
|
||||
api.userrating(),
|
||||
comment,
|
||||
mood,
|
||||
api.date_created(),
|
||||
kodi_id)
|
||||
else:
|
||||
self.kodidb.update_song_17(parent_id,
|
||||
artists,
|
||||
genre,
|
||||
title,
|
||||
track,
|
||||
api.runtime(),
|
||||
year,
|
||||
filename,
|
||||
api.viewcount(),
|
||||
api.lastplayed(),
|
||||
api.userrating(),
|
||||
comment,
|
||||
mood,
|
||||
api.date_created(),
|
||||
kodi_id)
|
||||
# OR ADD THE SONG #####
|
||||
else:
|
||||
LOG.info("ADD song plex_id: %s - %s", plex_id, title)
|
||||
# Add path
|
||||
kodi_pathid = self.kodidb.add_path(path)
|
||||
# Create the song entry
|
||||
if v.KODIVERSION >= 18:
|
||||
# Kodi Leia
|
||||
self.kodidb.add_song(kodi_id,
|
||||
parent_id,
|
||||
kodi_pathid,
|
||||
artists,
|
||||
genre,
|
||||
title,
|
||||
track,
|
||||
api.runtime(),
|
||||
year,
|
||||
filename,
|
||||
musicBrainzId,
|
||||
api.viewcount(),
|
||||
api.lastplayed(),
|
||||
api.userrating(),
|
||||
0,
|
||||
0,
|
||||
mood,
|
||||
api.date_created())
|
||||
else:
|
||||
self.kodidb.add_song_17(kodi_id,
|
||||
parent_id,
|
||||
kodi_pathid,
|
||||
artists,
|
||||
genre,
|
||||
title,
|
||||
track,
|
||||
api.runtime(),
|
||||
year,
|
||||
filename,
|
||||
musicBrainzId,
|
||||
api.viewcount(),
|
||||
api.lastplayed(),
|
||||
api.userrating(),
|
||||
0,
|
||||
0,
|
||||
mood,
|
||||
api.date_created())
|
||||
if v.KODIVERSION < 18:
|
||||
# Link song to album
|
||||
self.kodidb.add_albuminfosong(kodi_id,
|
||||
parent_id,
|
||||
track,
|
||||
title,
|
||||
api.runtime())
|
||||
# Link song to artists
|
||||
artist_name = api.grandparent_title()
|
||||
# Do the actual linking
|
||||
self.kodidb.add_song_artist(grandparent_id, kodi_id, artist_name)
|
||||
# Add genres
|
||||
if genres:
|
||||
self.kodidb.add_music_genres(kodi_id, genres, v.KODI_TYPE_SONG)
|
||||
if app.SYNC.artwork:
|
||||
artworks = api.artwork()
|
||||
self.kodidb.modify_artwork(artworks,
|
||||
kodi_id,
|
||||
v.KODI_TYPE_SONG)
|
||||
if xml.get('parentKey') is None:
|
||||
# Update album artwork
|
||||
self.kodidb.modify_artwork(artworks,
|
||||
parent_id,
|
||||
v.KODI_TYPE_ALBUM)
|
||||
self.plexdb.add_song(plex_id,
|
||||
api.checksum(),
|
||||
section_id,
|
||||
artist_id,
|
||||
grandparent_id,
|
||||
album_id,
|
||||
parent_id,
|
||||
kodi_id,
|
||||
kodi_pathid,
|
||||
self.last_sync)
|
|
@ -1,594 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from .common import ItemBase, process_path
|
||||
from ..plex_api import API
|
||||
from .. import plex_functions as PF, app, variables as v
|
||||
|
||||
LOG = getLogger('PLEX.tvshows')
|
||||
|
||||
|
||||
class TvShowMixin(object):
|
||||
def update_userdata(self, xml_element, plex_type):
|
||||
"""
|
||||
Updates the Kodi watched state of the item from PMS. Also retrieves
|
||||
Plex resume points for movies in progress.
|
||||
"""
|
||||
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)
|
||||
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
|
||||
self.kodidb.update_userrating(db_item['kodi_id'],
|
||||
db_item['kodi_type'],
|
||||
api.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())
|
||||
if db_item['kodi_fileid_2']:
|
||||
self.kodidb.set_resume(db_item['kodi_fileid_2'],
|
||||
api.resume_point(),
|
||||
api.runtime(),
|
||||
api.viewcount(),
|
||||
api.lastplayed())
|
||||
return True
|
||||
|
||||
def remove(self, plex_id, plex_type=None):
|
||||
"""
|
||||
Remove the entire TV shows object (show, season or episode) including
|
||||
all associated entries from the Kodi DB.
|
||||
"""
|
||||
db_item = self.plexdb.item_by_id(plex_id, plex_type)
|
||||
if not db_item:
|
||||
LOG.debug('Cannot delete plex_id %s - not found in DB', plex_id)
|
||||
return
|
||||
LOG.debug('Removing %s %s with kodi_id: %s',
|
||||
db_item['plex_type'], plex_id, db_item['kodi_id'])
|
||||
|
||||
# Remove the plex reference
|
||||
self.plexdb.remove(plex_id, db_item['plex_type'])
|
||||
|
||||
# EPISODE #####
|
||||
if db_item['plex_type'] == v.PLEX_TYPE_EPISODE:
|
||||
# Delete episode, verify season and tvshow
|
||||
self.remove_episode(db_item)
|
||||
# Season verification
|
||||
if (db_item['season_id'] and
|
||||
not self.plexdb.season_has_episodes(db_item['season_id'])):
|
||||
# No episode left for this season - so delete the season
|
||||
self.remove_season(db_item['parent_id'])
|
||||
self.plexdb.remove(db_item['season_id'], v.PLEX_TYPE_SEASON)
|
||||
# Show verification
|
||||
if (not self.plexdb.show_has_seasons(db_item['show_id']) and
|
||||
not self.plexdb.show_has_episodes(db_item['show_id'])):
|
||||
# No seasons for show left - so delete entire show
|
||||
self.remove_show(db_item['grandparent_id'])
|
||||
self.plexdb.remove(db_item['show_id'], v.PLEX_TYPE_SHOW)
|
||||
# SEASON #####
|
||||
elif db_item['plex_type'] == v.PLEX_TYPE_SEASON:
|
||||
# Remove episodes, season, verify tvshow
|
||||
episodes = list(self.plexdb.episode_by_season(db_item['plex_id']))
|
||||
for episode in episodes:
|
||||
self.remove_episode(episode)
|
||||
self.plexdb.remove(episode['plex_id'], v.PLEX_TYPE_EPISODE)
|
||||
# Remove season
|
||||
self.remove_season(db_item['kodi_id'])
|
||||
# Show verification
|
||||
if (not self.plexdb.show_has_seasons(db_item['show_id']) and
|
||||
not self.plexdb.show_has_episodes(db_item['show_id'])):
|
||||
# There's no other season or episode left, delete the show
|
||||
self.remove_show(db_item['parent_id'])
|
||||
self.plexdb.remove(db_item['show_id'], v.PLEX_TYPE_SHOW)
|
||||
# TVSHOW #####
|
||||
elif db_item['plex_type'] == v.PLEX_TYPE_SHOW:
|
||||
# Remove episodes, seasons and the tvshow itself
|
||||
seasons = list(self.plexdb.season_by_show(db_item['plex_id']))
|
||||
for season in seasons:
|
||||
self.remove_season(season['kodi_id'])
|
||||
self.plexdb.remove(season['plex_id'], v.PLEX_TYPE_SEASON)
|
||||
episodes = list(self.plexdb.episode_by_show(db_item['plex_id']))
|
||||
for episode in episodes:
|
||||
self.remove_episode(episode)
|
||||
self.plexdb.remove(episode['plex_id'], v.PLEX_TYPE_EPISODE)
|
||||
self.remove_show(db_item['kodi_id'])
|
||||
|
||||
LOG.debug('Deleted %s %s from all databases',
|
||||
db_item['plex_type'], db_item['plex_id'])
|
||||
|
||||
def remove_show(self, kodi_id):
|
||||
"""
|
||||
Remove a TV show, and only the show, no seasons or episodes
|
||||
"""
|
||||
self.kodidb.modify_genres(kodi_id, v.KODI_TYPE_SHOW)
|
||||
self.kodidb.modify_studios(kodi_id, v.KODI_TYPE_SHOW)
|
||||
self.kodidb.modify_tags(kodi_id, v.KODI_TYPE_SHOW)
|
||||
self.kodidb.delete_artwork(kodi_id, v.KODI_TYPE_SHOW)
|
||||
self.kodidb.remove_show(kodi_id)
|
||||
self.kodidb.remove_uniqueid(kodi_id, v.KODI_TYPE_SHOW)
|
||||
self.kodidb.remove_ratings(kodi_id, v.KODI_TYPE_SHOW)
|
||||
LOG.debug("Removed tvshow: %s", kodi_id)
|
||||
|
||||
def remove_season(self, kodi_id):
|
||||
"""
|
||||
Remove a season, and only a season, not the show or episodes
|
||||
"""
|
||||
self.kodidb.delete_artwork(kodi_id, v.KODI_TYPE_SEASON)
|
||||
self.kodidb.remove_season(kodi_id)
|
||||
LOG.debug("Removed season: %s", kodi_id)
|
||||
|
||||
def remove_episode(self, db_item):
|
||||
"""
|
||||
Remove an episode, and episode only from the Kodi DB (not Plex DB)
|
||||
"""
|
||||
self.kodidb.modify_people(db_item['kodi_id'], v.KODI_TYPE_EPISODE)
|
||||
self.kodidb.remove_file(db_item['kodi_fileid'])
|
||||
if db_item['kodi_fileid_2']:
|
||||
self.kodidb.remove_file(db_item['kodi_fileid_2'])
|
||||
self.kodidb.delete_artwork(db_item['kodi_id'], v.KODI_TYPE_EPISODE)
|
||||
self.kodidb.remove_episode(db_item['kodi_id'])
|
||||
self.kodidb.remove_uniqueid(db_item['kodi_id'], v.KODI_TYPE_EPISODE)
|
||||
self.kodidb.remove_ratings(db_item['kodi_id'], v.KODI_TYPE_EPISODE)
|
||||
LOG.debug("Removed episode: %s", db_item['kodi_id'])
|
||||
|
||||
|
||||
class Show(TvShowMixin, ItemBase):
|
||||
"""
|
||||
For Plex library-type TV shows
|
||||
"""
|
||||
def add_update(self, xml, section_name=None, section_id=None,
|
||||
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())
|
||||
return
|
||||
plex_id = api.plex_id
|
||||
show = self.plexdb.show(plex_id)
|
||||
if not show:
|
||||
update_item = False
|
||||
kodi_id = self.kodidb.new_show_id()
|
||||
else:
|
||||
update_item = True
|
||||
kodi_id = show['kodi_id']
|
||||
kodi_pathid = show['kodi_pathid']
|
||||
|
||||
# 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,
|
||||
folder=True)
|
||||
if playurl is None:
|
||||
return
|
||||
path, toplevelpath = process_path(playurl)
|
||||
toppathid = self.kodidb.add_path(toplevelpath,
|
||||
content='tvshows',
|
||||
scraper='metadata.local')
|
||||
else:
|
||||
# Set plugin path
|
||||
toplevelpath = "plugin://%s.tvshows/" % v.ADDON_ID
|
||||
path = "%s%s/" % (toplevelpath, plex_id)
|
||||
# Do NOT set a parent id because addon-path cannot be "stacked"
|
||||
toppathid = None
|
||||
|
||||
kodi_pathid = self.kodidb.add_path(path,
|
||||
date_added=api.date_created(),
|
||||
id_parent_path=toppathid)
|
||||
# UPDATE THE TVSHOW #####
|
||||
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))
|
||||
self.kodidb.modify_people(kodi_id,
|
||||
v.KODI_TYPE_SHOW,
|
||||
api.people())
|
||||
if app.SYNC.artwork:
|
||||
self.kodidb.modify_artwork(api.artwork(),
|
||||
kodi_id,
|
||||
v.KODI_TYPE_SHOW)
|
||||
# Update the tvshow entry
|
||||
self.kodidb.update_show(api.title(),
|
||||
api.plot(),
|
||||
rating_id,
|
||||
api.premiere_date(),
|
||||
api.list_to_string(api.genres()),
|
||||
api.title(),
|
||||
unique_id,
|
||||
api.content_rating(),
|
||||
api.list_to_string(api.studios()),
|
||||
api.sorttitle(),
|
||||
kodi_id)
|
||||
# OR ADD THE TVSHOW #####
|
||||
else:
|
||||
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))
|
||||
self.kodidb.add_people(kodi_id,
|
||||
v.KODI_TYPE_SHOW,
|
||||
api.people())
|
||||
if app.SYNC.artwork:
|
||||
self.kodidb.add_artwork(api.artwork(),
|
||||
kodi_id,
|
||||
v.KODI_TYPE_SHOW)
|
||||
# Create the tvshow entry
|
||||
self.kodidb.add_show(kodi_id,
|
||||
api.title(),
|
||||
api.plot(),
|
||||
rating_id,
|
||||
api.premiere_date(),
|
||||
api.list_to_string(api.genres()),
|
||||
api.title(),
|
||||
unique_id,
|
||||
api.content_rating(),
|
||||
api.list_to_string(api.studios()),
|
||||
api.sorttitle())
|
||||
self.kodidb.modify_genres(kodi_id, v.KODI_TYPE_SHOW, api.genres())
|
||||
# Process studios
|
||||
self.kodidb.modify_studios(kodi_id, v.KODI_TYPE_SHOW, api.studios())
|
||||
# Process tags: view, PMS collection tags
|
||||
tags = [section_name]
|
||||
tags.extend([i for _, i in api.collections()])
|
||||
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,
|
||||
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):
|
||||
"""
|
||||
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())
|
||||
return
|
||||
plex_id = api.plex_id
|
||||
season = self.plexdb.season(plex_id)
|
||||
if not season:
|
||||
update_item = False
|
||||
else:
|
||||
update_item = True
|
||||
show_id = api.parent_id()
|
||||
show = self.plexdb.show(show_id)
|
||||
if not show:
|
||||
LOG.warn('Parent 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("Parent 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(show_id)
|
||||
if not show:
|
||||
LOG.error('Still could not find parent tv show %s', show_id)
|
||||
return
|
||||
parent_id = show['kodi_id']
|
||||
if app.SYNC.artwork:
|
||||
parent_artwork = api.artwork(kodi_id=parent_id,
|
||||
kodi_type=v.KODI_TYPE_SHOW)
|
||||
artwork = api.artwork()
|
||||
# Remove all artwork that is identical for the season's show
|
||||
for key in parent_artwork:
|
||||
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())
|
||||
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)
|
||||
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,
|
||||
show_id=show_id,
|
||||
parent_id=parent_id,
|
||||
kodi_id=kodi_id,
|
||||
last_sync=self.last_sync)
|
||||
|
||||
|
||||
class Episode(TvShowMixin, ItemBase):
|
||||
def add_update(self, xml, section_name=None, section_id=None,
|
||||
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())
|
||||
return
|
||||
plex_id = api.plex_id
|
||||
episode = self.plexdb.episode(plex_id)
|
||||
if not episode:
|
||||
update_item = False
|
||||
kodi_id = self.kodidb.new_episode_id()
|
||||
else:
|
||||
update_item = True
|
||||
kodi_id = episode['kodi_id']
|
||||
old_kodi_fileid = episode['kodi_fileid']
|
||||
old_kodi_fileid_2 = episode['kodi_fileid_2']
|
||||
kodi_pathid = episode['kodi_pathid']
|
||||
|
||||
airs_before_season = "-1"
|
||||
airs_before_episode = "-1"
|
||||
|
||||
# The grandparent TV show
|
||||
show = self.plexdb.show(api.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())
|
||||
try:
|
||||
show_xml[0].attrib
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
LOG.error("Grandparent tvshow %s xml download failed", api.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())
|
||||
if not show:
|
||||
LOG.error('Still could not find grandparent tv show %s', api.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())
|
||||
try:
|
||||
season_xml[0].attrib
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
LOG.error("Parent season %s xml download failed", api.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())
|
||||
if not season:
|
||||
LOG.error('Still could not find parent season %s', api.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:
|
||||
# Root path tvshows/ already saved in Kodi DB
|
||||
kodi_pathid = self.kodidb.add_path(path)
|
||||
# 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
|
||||
# filename_2 is exactly the same as filename
|
||||
# so WITH plex show id!
|
||||
kodi_pathid_2 = self.kodidb.add_path(path_2)
|
||||
|
||||
# UPDATE THE EPISODE #####
|
||||
if update_item:
|
||||
LOG.info("UPDATE episode plex_id: %s - %s", plex_id, api.title())
|
||||
kodi_fileid = self.kodidb.modify_file(filename,
|
||||
kodi_pathid,
|
||||
api.date_created())
|
||||
if not app.SYNC.direct_paths:
|
||||
kodi_fileid_2 = self.kodidb.modify_file(filename,
|
||||
kodi_pathid_2,
|
||||
api.date_created())
|
||||
else:
|
||||
kodi_fileid_2 = None
|
||||
|
||||
if kodi_fileid != old_kodi_fileid:
|
||||
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))
|
||||
self.kodidb.modify_people(kodi_id,
|
||||
v.KODI_TYPE_EPISODE,
|
||||
api.people())
|
||||
if app.SYNC.artwork:
|
||||
self.kodidb.modify_artwork(api.artwork(),
|
||||
kodi_id,
|
||||
v.KODI_TYPE_EPISODE)
|
||||
self.kodidb.update_episode(api.title(),
|
||||
api.plot(),
|
||||
ratingid,
|
||||
api.list_to_string(api.writers()),
|
||||
api.premiere_date(),
|
||||
api.runtime(),
|
||||
api.list_to_string(api.directors()),
|
||||
api.season_number(),
|
||||
api.index(),
|
||||
api.title(),
|
||||
airs_before_season,
|
||||
airs_before_episode,
|
||||
fullpath,
|
||||
kodi_pathid,
|
||||
unique_id,
|
||||
kodi_fileid, # and NOT kodi_fileid_2
|
||||
parent_id,
|
||||
api.userrating(),
|
||||
kodi_id)
|
||||
self.kodidb.set_resume(kodi_fileid,
|
||||
api.resume_point(),
|
||||
api.runtime(),
|
||||
api.viewcount(),
|
||||
api.lastplayed())
|
||||
if not app.SYNC.direct_paths:
|
||||
self.kodidb.set_resume(kodi_fileid_2,
|
||||
api.resume_point(),
|
||||
api.runtime(),
|
||||
api.viewcount(),
|
||||
api.lastplayed())
|
||||
self.plexdb.add_episode(plex_id=plex_id,
|
||||
checksum=api.checksum(),
|
||||
section_id=section_id,
|
||||
show_id=api.show_id(),
|
||||
grandparent_id=grandparent_id,
|
||||
season_id=api.season_id(),
|
||||
parent_id=parent_id,
|
||||
kodi_id=kodi_id,
|
||||
kodi_fileid=kodi_fileid,
|
||||
kodi_fileid_2=kodi_fileid_2,
|
||||
kodi_pathid=kodi_pathid,
|
||||
last_sync=self.last_sync)
|
||||
# OR ADD THE EPISODE #####
|
||||
else:
|
||||
LOG.info("ADD episode plex_id: %s - %s", plex_id, api.title())
|
||||
kodi_fileid = self.kodidb.add_file(filename,
|
||||
kodi_pathid,
|
||||
api.date_created())
|
||||
if not app.SYNC.direct_paths:
|
||||
kodi_fileid_2 = self.kodidb.add_file(filename,
|
||||
kodi_pathid_2,
|
||||
api.date_created())
|
||||
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))
|
||||
self.kodidb.add_people(kodi_id,
|
||||
v.KODI_TYPE_EPISODE,
|
||||
api.people())
|
||||
if app.SYNC.artwork:
|
||||
self.kodidb.add_artwork(api.artwork(),
|
||||
kodi_id,
|
||||
v.KODI_TYPE_EPISODE)
|
||||
self.kodidb.add_episode(kodi_id,
|
||||
kodi_fileid, # and NOT kodi_fileid_2
|
||||
api.title(),
|
||||
api.plot(),
|
||||
rating_id,
|
||||
api.list_to_string(api.writers()),
|
||||
api.premiere_date(),
|
||||
api.runtime(),
|
||||
api.list_to_string(api.directors()),
|
||||
api.season_number(),
|
||||
api.index(),
|
||||
api.title(),
|
||||
grandparent_id,
|
||||
airs_before_season,
|
||||
airs_before_episode,
|
||||
fullpath,
|
||||
kodi_pathid,
|
||||
unique_id,
|
||||
parent_id,
|
||||
api.userrating())
|
||||
self.kodidb.set_resume(kodi_fileid,
|
||||
api.resume_point(),
|
||||
api.runtime(),
|
||||
api.viewcount(),
|
||||
api.lastplayed())
|
||||
if not app.SYNC.direct_paths:
|
||||
self.kodidb.set_resume(kodi_fileid_2,
|
||||
api.resume_point(),
|
||||
api.runtime(),
|
||||
api.viewcount(),
|
||||
api.lastplayed())
|
||||
self.plexdb.add_episode(plex_id=plex_id,
|
||||
checksum=api.checksum(),
|
||||
section_id=section_id,
|
||||
show_id=api.show_id(),
|
||||
grandparent_id=grandparent_id,
|
||||
season_id=api.season_id(),
|
||||
parent_id=parent_id,
|
||||
kodi_id=kodi_id,
|
||||
kodi_fileid=kodi_fileid,
|
||||
kodi_fileid_2=kodi_fileid_2,
|
||||
kodi_pathid=kodi_pathid,
|
||||
last_sync=self.last_sync)
|
||||
|
||||
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')))
|
|
@ -8,26 +8,7 @@ from __future__ import absolute_import, division, unicode_literals
|
|||
from json import loads, dumps
|
||||
from xbmc import executeJSONRPC
|
||||
|
||||
from . import kodi_constants, timing, variables as v
|
||||
|
||||
JSON_FROM_KODITYPE = {
|
||||
v.KODI_TYPE_MOVIE: ('VideoLibrary.GetMovieDetails',
|
||||
kodi_constants.FIELDS_MOVIES),
|
||||
v.KODI_TYPE_SHOW: ('VideoLibrary.GetTVShowDetails',
|
||||
kodi_constants.FIELDS_TVSHOWS),
|
||||
v.KODI_TYPE_SEASON: ('VideoLibrary.GetSeasonDetails',
|
||||
kodi_constants.FIELDS_SEASON),
|
||||
v.KODI_TYPE_EPISODE: ('VideoLibrary.GetEpisodeDetails',
|
||||
kodi_constants.FIELDS_EPISODES),
|
||||
v.KODI_TYPE_ARTIST: ('AudioLibrary.GetArtistDetails',
|
||||
kodi_constants.FIELDS_ARTISTS),
|
||||
v.KODI_TYPE_ALBUM: ('AudioLibrary.GetAlbumDetails',
|
||||
kodi_constants.FIELDS_ALBUMS),
|
||||
v.KODI_TYPE_SONG: ('AudioLibrary.GetSongDetails',
|
||||
kodi_constants.FIELDS_SONGS),
|
||||
v.KODI_TYPE_SET: ('VideoLibrary.GetMovieSetDetails',
|
||||
[]),
|
||||
}
|
||||
from . import utils
|
||||
|
||||
|
||||
class JsonRPC(object):
|
||||
|
@ -170,12 +151,12 @@ 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)})
|
||||
"value": utils.millis_to_kodi_time(offset)})
|
||||
|
||||
|
||||
def smallforward():
|
||||
|
@ -421,41 +402,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:
|
||||
|
@ -611,16 +557,3 @@ def settings_setsettingvalue(setting, value):
|
|||
'setting': setting,
|
||||
'value': value
|
||||
})
|
||||
|
||||
|
||||
def item_details(kodi_id, kodi_type):
|
||||
'''
|
||||
Returns the Kodi item dict for this item
|
||||
'''
|
||||
json, fields = JSON_FROM_KODITYPE[kodi_type]
|
||||
ret = JsonRPC(json).execute({'%sid' % kodi_type: kodi_id,
|
||||
'properties': fields})
|
||||
try:
|
||||
return ret['result']['%sdetails' % kodi_type]
|
||||
except (KeyError, TypeError):
|
||||
return {}
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
script.module.metadatautils
|
||||
kodi_constants.py
|
||||
Several common constants for use with Kodi json api
|
||||
'''
|
||||
FIELDS_BASE = ['dateadded', 'file', 'lastplayed', 'plot', 'title', 'art',
|
||||
'playcount']
|
||||
FIELDS_FILE = FIELDS_BASE + ['streamdetails', 'director', 'resume', 'runtime']
|
||||
FIELDS_MOVIES = FIELDS_FILE + ['plotoutline', 'sorttitle', 'cast', 'votes',
|
||||
'showlink', 'top250', 'trailer', 'year', 'country', 'studio', 'set',
|
||||
'genre', 'mpaa', 'setid', 'rating', 'tag', 'tagline', 'writer',
|
||||
'originaltitle', 'imdbnumber', 'uniqueid']
|
||||
FIELDS_TVSHOWS = FIELDS_BASE + ['sorttitle', 'mpaa', 'premiered', 'year',
|
||||
'episode', 'watchedepisodes', 'votes', 'rating', 'studio', 'season',
|
||||
'genre', 'cast', 'episodeguide', 'tag', 'originaltitle', 'imdbnumber']
|
||||
FIELDS_SEASON = ['art', 'playcount', 'season', 'showtitle', 'episode',
|
||||
'tvshowid', 'watchedepisodes', 'userrating', 'fanart', 'thumbnail']
|
||||
FIELDS_EPISODES = FIELDS_FILE + ['cast', 'productioncode', 'rating', 'votes',
|
||||
'episode', 'showtitle', 'tvshowid', 'season', 'firstaired', 'writer',
|
||||
'originaltitle']
|
||||
FIELDS_MUSICVIDEOS = FIELDS_FILE + ['genre', 'artist', 'tag', 'album', 'track',
|
||||
'studio', 'year']
|
||||
FIELDS_FILES = FIELDS_FILE + ['plotoutline', 'sorttitle', 'cast', 'votes',
|
||||
'trailer', 'year', 'country', 'studio', 'genre', 'mpaa', 'rating',
|
||||
'tagline', 'writer', 'originaltitle', 'imdbnumber', 'premiered', 'episode',
|
||||
'showtitle', 'firstaired', 'watchedepisodes', 'duration', 'season']
|
||||
FIELDS_SONGS = ['artist', 'displayartist', 'title', 'rating', 'fanart',
|
||||
'thumbnail', 'duration', 'disc', 'playcount', 'comment', 'file', 'album',
|
||||
'lastplayed', 'genre', 'musicbrainzartistid', 'track', 'dateadded']
|
||||
FIELDS_ALBUMS = ['title', 'fanart', 'thumbnail', 'genre', 'displayartist',
|
||||
'artist', 'musicbrainzalbumartistid', 'year', 'rating', 'artistid',
|
||||
'musicbrainzalbumid', 'theme', 'description', 'type', 'style', 'playcount',
|
||||
'albumlabel', 'mood', 'dateadded']
|
||||
FIELDS_ARTISTS = ['born', 'formed', 'died', 'style', 'yearsactive', 'mood',
|
||||
'fanart', 'thumbnail', 'musicbrainzartistid', 'disbanded', 'description',
|
||||
'instrument']
|
||||
FIELDS_RECORDINGS = ['art', 'channel', 'directory', 'endtime', 'file', 'genre',
|
||||
'icon', 'playcount', 'plot', 'plotoutline', 'resume', 'runtime',
|
||||
'starttime', 'streamurl', 'title']
|
||||
FIELDS_CHANNELS = ['broadcastnow', 'channeltype', 'hidden', 'locked',
|
||||
'lastplayed', 'thumbnail', 'channel']
|
||||
|
||||
FILTER_UNWATCHED = {
|
||||
'operator': 'lessthan',
|
||||
'field': 'playcount',
|
||||
'value': '1'
|
||||
}
|
||||
FILTER_WATCHED = {
|
||||
'operator': 'isnot',
|
||||
'field': 'playcount',
|
||||
'value': '0'
|
||||
}
|
||||
FILTER_RATING = {
|
||||
'operator': 'greaterthan',
|
||||
'field': 'rating',
|
||||
'value': '7'
|
||||
}
|
||||
FILTER_RATING_MUSIC = {
|
||||
'operator': 'greaterthan',
|
||||
'field': 'rating',
|
||||
'value': '3'
|
||||
}
|
||||
FILTER_INPROGRESS = {
|
||||
'operator': 'true',
|
||||
'field': 'inprogress',
|
||||
'value': ''
|
||||
}
|
||||
SORT_RATING = {
|
||||
'method': 'rating',
|
||||
'order': 'descending'
|
||||
}
|
||||
SORT_RANDOM = {
|
||||
'method': 'random',
|
||||
'order': 'descending'
|
||||
}
|
||||
SORT_TITLE = {
|
||||
'method': 'title',
|
||||
'order': 'ascending'
|
||||
}
|
||||
SORT_DATEADDED = {
|
||||
'method': 'dateadded',
|
||||
'order': 'descending'
|
||||
}
|
||||
SORT_LASTPLAYED = {
|
||||
'method': 'lastplayed',
|
||||
'order': 'descending'
|
||||
}
|
||||
SORT_EPISODE = {
|
||||
'method': 'episode'
|
||||
}
|
|
@ -1,126 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from .common import KODIDB_LOCK
|
||||
from .video import KodiVideoDB
|
||||
from .music import KodiMusicDB
|
||||
from .texture import KodiTextureDB
|
||||
|
||||
from .. import path_ops, utils, variables as v
|
||||
|
||||
LOG = getLogger('PLEX.kodi_db')
|
||||
|
||||
|
||||
def kodiid_from_filename(path, kodi_type=None, db_type=None):
|
||||
"""
|
||||
Returns kodi_id if we have an item in the Kodi video or audio database with
|
||||
said path. Feed with either koditype, e.v. 'movie', 'song' or the DB
|
||||
you want to poll ('video' or 'music')
|
||||
Returns None, <kodi_type> if not possible
|
||||
"""
|
||||
kodi_id = None
|
||||
path = utils.try_decode(path)
|
||||
# Make sure path ends in either '/' or '\'
|
||||
# We CANNOT use path_ops.path.join as this can result in \ where we need /
|
||||
try:
|
||||
filename = path.rsplit('/', 1)[1]
|
||||
path = path.rsplit('/', 1)[0] + '/'
|
||||
except IndexError:
|
||||
filename = path.rsplit('\\', 1)[1]
|
||||
path = path.rsplit('\\', 1)[0] + '\\'
|
||||
if kodi_type == v.KODI_TYPE_SONG or db_type == 'music':
|
||||
with KodiMusicDB(lock=False) as kodidb:
|
||||
try:
|
||||
kodi_id = kodidb.song_id_from_filename(filename, path)
|
||||
except TypeError:
|
||||
LOG.debug('No Kodi audio db element found for path %s', path)
|
||||
else:
|
||||
kodi_type = v.KODI_TYPE_SONG
|
||||
else:
|
||||
with KodiVideoDB(lock=False) as kodidb:
|
||||
try:
|
||||
kodi_id, kodi_type = kodidb.video_id_from_filename(filename,
|
||||
path)
|
||||
except TypeError:
|
||||
LOG.debug('No kodi video db element found for path %s file %s',
|
||||
path, filename)
|
||||
return kodi_id, kodi_type
|
||||
|
||||
|
||||
def setup_kodi_default_entries():
|
||||
"""
|
||||
Makes sure that we retain the Kodi standard databases. E.g. that there
|
||||
is a dummy artist with ID 1
|
||||
"""
|
||||
if utils.settings('enableMusic') == 'true':
|
||||
with KodiMusicDB() as kodidb:
|
||||
kodidb.setup_kodi_default_entries()
|
||||
|
||||
|
||||
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')
|
||||
path = path_ops.translate_path('special://thumbnails/')
|
||||
if path_ops.exists(path):
|
||||
path_ops.rmtree(path, ignore_errors=True)
|
||||
paths = ('', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||
'a', 'b', 'c', 'd', 'e', 'f',
|
||||
'Video', 'plex')
|
||||
for path in paths:
|
||||
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')
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
LOG.warn('Wiping Kodi databases!')
|
||||
LOG.info('Wiping Kodi video database')
|
||||
with KodiVideoDB() as kodidb:
|
||||
kodidb.wipe()
|
||||
if music:
|
||||
LOG.info('Wiping Kodi music database')
|
||||
with KodiMusicDB() as kodidb:
|
||||
kodidb.wipe()
|
||||
reset_cached_images()
|
||||
setup_kodi_default_entries()
|
||||
# Delete SQLITE wal files
|
||||
import xbmc
|
||||
# Make sure Kodi knows we wiped the databases
|
||||
xbmc.executebuiltin('UpdateLibrary(video)')
|
||||
if utils.settings('enableMusic') == '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,
|
||||
v.PLEX_TYPE_SEASON: KodiVideoDB,
|
||||
v.PLEX_TYPE_EPISODE: KodiVideoDB,
|
||||
v.PLEX_TYPE_ARTIST: KodiMusicDB,
|
||||
v.PLEX_TYPE_ALBUM: KodiMusicDB,
|
||||
v.PLEX_TYPE_SONG: KodiMusicDB
|
||||
}
|
|
@ -1,155 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from threading import Lock
|
||||
|
||||
from .. import db, path_ops
|
||||
|
||||
KODIDB_LOCK = Lock()
|
||||
# Names of tables we generally leave untouched and e.g. don't wipe
|
||||
UNTOUCHED_TABLES = ('version', 'versiontagscan')
|
||||
|
||||
|
||||
class KodiDBBase(object):
|
||||
"""
|
||||
Kodi database methods used for all types of items
|
||||
"""
|
||||
def __init__(self, texture_db=False, kodiconn=None, artconn=None,
|
||||
lock=True):
|
||||
"""
|
||||
Allows direct use with a cursor instead of context mgr
|
||||
"""
|
||||
self._texture_db = texture_db
|
||||
self.lock = lock
|
||||
self.kodiconn = kodiconn
|
||||
self.cursor = self.kodiconn.cursor() if self.kodiconn else None
|
||||
self.artconn = artconn
|
||||
self.artcursor = self.artconn.cursor() if self.artconn else None
|
||||
|
||||
def __enter__(self):
|
||||
if self.lock:
|
||||
KODIDB_LOCK.acquire()
|
||||
self.kodiconn = db.connect(self.db_kind)
|
||||
self.cursor = self.kodiconn.cursor()
|
||||
self.artconn = db.connect('texture') if self._texture_db \
|
||||
else None
|
||||
self.artcursor = self.artconn.cursor() if self._texture_db else None
|
||||
return self
|
||||
|
||||
def __exit__(self, e_typ, e_val, trcbak):
|
||||
try:
|
||||
if e_typ:
|
||||
# re-raise any exception
|
||||
return False
|
||||
self.kodiconn.commit()
|
||||
if self.artconn:
|
||||
self.artconn.commit()
|
||||
finally:
|
||||
self.kodiconn.close()
|
||||
if self.artconn:
|
||||
self.artconn.close()
|
||||
if self.lock:
|
||||
KODIDB_LOCK.release()
|
||||
|
||||
def art_urls(self, kodi_id, kodi_type):
|
||||
return (x[0] for x in
|
||||
self.cursor.execute('SELECT url FROM art WHERE media_id = ? AND media_type = ?',
|
||||
(kodi_id, kodi_type)))
|
||||
|
||||
def artwork_generator(self, kodi_type, limit, offset):
|
||||
query = 'SELECT url FROM art WHERE type == ? LIMIT ? OFFSET ?'
|
||||
return (x[0] for x in
|
||||
self.cursor.execute(query, (kodi_type, limit, offset)))
|
||||
|
||||
def add_artwork(self, artworks, kodi_id, kodi_type):
|
||||
"""
|
||||
Pass in an artworks dict (see PlexAPI) to set an items artwork.
|
||||
"""
|
||||
for kodi_art, url in artworks.iteritems():
|
||||
self.add_art(url, kodi_id, kodi_type, kodi_art)
|
||||
|
||||
@db.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
|
||||
Kodi art table for item kodi_id/kodi_type. Will also cache everything
|
||||
except actor portraits.
|
||||
"""
|
||||
self.cursor.execute('''
|
||||
INSERT INTO art(media_id, media_type, type, url)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (kodi_id, kodi_type, kodi_art, url))
|
||||
|
||||
def modify_artwork(self, artworks, kodi_id, kodi_type):
|
||||
"""
|
||||
Pass in an artworks dict (see PlexAPI) to set an items artwork.
|
||||
"""
|
||||
for kodi_art, url in artworks.iteritems():
|
||||
self.modify_art(url, kodi_id, kodi_type, kodi_art)
|
||||
|
||||
@db.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
|
||||
Kodi art table for item kodi_id/kodi_type. Will also cache everything
|
||||
except actor portraits.
|
||||
"""
|
||||
self.cursor.execute('''
|
||||
SELECT url FROM art
|
||||
WHERE media_id = ? AND media_type = ? AND type = ?
|
||||
LIMIT 1
|
||||
''', (kodi_id, kodi_type, kodi_art,))
|
||||
try:
|
||||
# Update the artwork
|
||||
old_url = self.cursor.fetchone()[0]
|
||||
except TypeError:
|
||||
# Add the artwork
|
||||
self.cursor.execute('''
|
||||
INSERT INTO art(media_id, media_type, type, url)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (kodi_id, kodi_type, kodi_art, url))
|
||||
else:
|
||||
if url == old_url:
|
||||
# Only cache artwork if it changed
|
||||
return
|
||||
self.delete_cached_artwork(old_url)
|
||||
self.cursor.execute('''
|
||||
UPDATE art SET url = ?
|
||||
WHERE media_id = ? AND media_type = ? AND type = ?
|
||||
''', (url, kodi_id, kodi_type, kodi_art))
|
||||
|
||||
def delete_artwork(self, kodi_id, kodi_type):
|
||||
self.cursor.execute('SELECT url FROM art WHERE media_id = ? AND media_type = ?',
|
||||
(kodi_id, kodi_type, ))
|
||||
for row in self.cursor.fetchall():
|
||||
self.delete_cached_artwork(row[0])
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def delete_cached_artwork(self, url):
|
||||
try:
|
||||
self.artcursor.execute("SELECT cachedurl FROM texture WHERE url = ? LIMIT 1",
|
||||
(url, ))
|
||||
cachedurl = self.artcursor.fetchone()[0]
|
||||
except TypeError:
|
||||
# Could not find cached url
|
||||
pass
|
||||
else:
|
||||
# Delete thumbnail as well as the entry
|
||||
path = path_ops.translate_path("special://thumbnails/%s"
|
||||
% cachedurl)
|
||||
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)
|
|
@ -1,628 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from . import common
|
||||
from .. import db, variables as v, app, timing
|
||||
|
||||
LOG = getLogger('PLEX.kodi_db.music')
|
||||
|
||||
|
||||
class KodiMusicDB(common.KodiDBBase):
|
||||
db_kind = 'music'
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def add_path(self, path):
|
||||
"""
|
||||
Add the path (unicode) to the music DB, if it does not exist already.
|
||||
Returns the path id
|
||||
"""
|
||||
# SQL won't return existing paths otherwise
|
||||
path = '' if path is None else path
|
||||
self.cursor.execute('SELECT idPath FROM path WHERE strPath = ?',
|
||||
(path,))
|
||||
try:
|
||||
pathid = self.cursor.fetchone()[0]
|
||||
except TypeError:
|
||||
self.cursor.execute('INSERT INTO path(strPath, strHash) VALUES (?, ?)',
|
||||
(path, '123'))
|
||||
pathid = self.cursor.lastrowid
|
||||
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
|
||||
def update_path(self, path, kodi_pathid):
|
||||
self.cursor.execute('''
|
||||
UPDATE path
|
||||
SET strPath = ?, strHash = ?
|
||||
WHERE idPath = ?
|
||||
''', (path, '123', kodi_pathid))
|
||||
|
||||
def song_id_from_filename(self, filename, path):
|
||||
"""
|
||||
Returns the Kodi song_id from the Kodi music database or None if not
|
||||
found OR something went wrong.
|
||||
"""
|
||||
self.cursor.execute('SELECT idPath FROM path WHERE strPath = ?',
|
||||
(path, ))
|
||||
path_ids = self.cursor.fetchall()
|
||||
if len(path_ids) != 1:
|
||||
LOG.debug('Found wrong number of path ids: %s for path %s, abort',
|
||||
path_ids, path)
|
||||
return
|
||||
self.cursor.execute('SELECT idSong FROM song WHERE strFileName = ? AND idPath = ?',
|
||||
(filename, path_ids[0][0]))
|
||||
song_ids = self.cursor.fetchall()
|
||||
if len(song_ids) != 1:
|
||||
LOG.info('Found wrong number of songs %s, abort', song_ids)
|
||||
return
|
||||
return song_ids[0][0]
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def delete_song_from_song_artist(self, song_id):
|
||||
"""
|
||||
Deletes son from song_artist table and possibly orphaned roles
|
||||
"""
|
||||
self.cursor.execute('''
|
||||
SELECT idArtist, idRole FROM song_artist
|
||||
WHERE idSong = ? LIMIT 1
|
||||
''', (song_id, ))
|
||||
artist = self.cursor.fetchone()
|
||||
if not artist:
|
||||
# No entry to begin with
|
||||
return
|
||||
# Delete the entry
|
||||
self.cursor.execute('DELETE FROM song_artist WHERE idSong = ?',
|
||||
(song_id, ))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def delete_song_from_song_genre(self, song_id):
|
||||
"""
|
||||
Deletes the one entry with id song_id from the song_genre table.
|
||||
Will also delete orphaned genres from genre table
|
||||
"""
|
||||
self.cursor.execute('SELECT idGenre FROM song_genre WHERE idSong = ?',
|
||||
(song_id, ))
|
||||
genres = self.cursor.fetchall()
|
||||
self.cursor.execute('DELETE FROM song_genre WHERE idSong = ?',
|
||||
(song_id, ))
|
||||
# Check for orphaned genres in both song_genre and album_genre tables
|
||||
for genre in genres:
|
||||
self.cursor.execute('SELECT idGenre FROM song_genre WHERE idGenre = ? LIMIT 1',
|
||||
(genre[0], ))
|
||||
if not self.cursor.fetchone():
|
||||
self.cursor.execute('SELECT idGenre FROM album_genre WHERE idGenre = ? LIMIT 1',
|
||||
(genre[0], ))
|
||||
if not self.cursor.fetchone():
|
||||
self.delete_genre(genre[0])
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def delete_genre(self, genre_id):
|
||||
"""
|
||||
Dedicated method in order to catch OperationalErrors correctly
|
||||
"""
|
||||
self.cursor.execute('DELETE FROM genre WHERE idGenre = ?',
|
||||
(genre_id, ))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def delete_album_from_album_genre(self, album_id):
|
||||
"""
|
||||
Deletes the one entry with id album_id from the album_genre table.
|
||||
Will also delete orphaned genres from genre table
|
||||
"""
|
||||
self.cursor.execute('SELECT idGenre FROM album_genre WHERE idAlbum = ?',
|
||||
(album_id, ))
|
||||
genres = self.cursor.fetchall()
|
||||
self.cursor.execute('DELETE FROM album_genre WHERE idAlbum = ?',
|
||||
(album_id, ))
|
||||
# Check for orphaned genres in both album_genre and song_genre tables
|
||||
for genre in genres:
|
||||
self.cursor.execute('SELECT idGenre FROM album_genre WHERE idGenre = ? LIMIT 1',
|
||||
(genre[0], ))
|
||||
if not self.cursor.fetchone():
|
||||
self.cursor.execute('SELECT idGenre FROM song_genre WHERE idGenre = ? LIMIT 1',
|
||||
(genre[0], ))
|
||||
if not self.cursor.fetchone():
|
||||
self.delete_genre(genre[0])
|
||||
|
||||
def new_album_id(self):
|
||||
self.cursor.execute('SELECT COALESCE(MAX(idAlbum), 0) FROM album')
|
||||
return self.cursor.fetchone()[0] + 1
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def add_album_17(self, *args):
|
||||
"""
|
||||
strReleaseType: 'album' or 'single'
|
||||
"""
|
||||
if app.SYNC.artwork:
|
||||
self.cursor.execute('''
|
||||
INSERT INTO album(
|
||||
idAlbum,
|
||||
strAlbum,
|
||||
strMusicBrainzAlbumID,
|
||||
strArtists,
|
||||
strGenres,
|
||||
iYear,
|
||||
bCompilation,
|
||||
strReview,
|
||||
strImage,
|
||||
strLabel,
|
||||
iUserrating,
|
||||
lastScraped,
|
||||
strReleaseType)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (args))
|
||||
else:
|
||||
args = list(args)
|
||||
del args[8]
|
||||
self.cursor.execute('''
|
||||
INSERT INTO album(
|
||||
idAlbum,
|
||||
strAlbum,
|
||||
strMusicBrainzAlbumID,
|
||||
strArtists,
|
||||
strGenres,
|
||||
iYear,
|
||||
bCompilation,
|
||||
strReview,
|
||||
strLabel,
|
||||
iUserrating,
|
||||
lastScraped,
|
||||
strReleaseType)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (args))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def update_album_17(self, *args):
|
||||
if app.SYNC.artwork:
|
||||
self.cursor.execute('''
|
||||
UPDATE album
|
||||
SET strAlbum = ?,
|
||||
strMusicBrainzAlbumID = ?,
|
||||
strArtists = ?,
|
||||
strGenres = ?,
|
||||
iYear = ?,
|
||||
bCompilation = ?,
|
||||
strReview = ?,
|
||||
strImage = ?,
|
||||
strLabel = ?,
|
||||
iUserrating = ?,
|
||||
lastScraped = ?,
|
||||
strReleaseType = ?
|
||||
WHERE idAlbum = ?
|
||||
''', (args))
|
||||
else:
|
||||
args = list(args)
|
||||
del args[7]
|
||||
self.cursor.execute('''
|
||||
UPDATE album
|
||||
SET strAlbum = ?,
|
||||
strMusicBrainzAlbumID = ?,
|
||||
strArtists = ?,
|
||||
strGenres = ?,
|
||||
iYear = ?,
|
||||
bCompilation = ?,
|
||||
strReview = ?,
|
||||
strLabel = ?,
|
||||
iUserrating = ?,
|
||||
lastScraped = ?,
|
||||
strReleaseType = ?
|
||||
WHERE idAlbum = ?
|
||||
''', (args))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def add_album(self, *args):
|
||||
"""
|
||||
strReleaseType: 'album' or 'single'
|
||||
"""
|
||||
if app.SYNC.artwork:
|
||||
self.cursor.execute('''
|
||||
INSERT INTO album(
|
||||
idAlbum,
|
||||
strAlbum,
|
||||
strMusicBrainzAlbumID,
|
||||
strArtistDisp,
|
||||
strGenres,
|
||||
iYear,
|
||||
bCompilation,
|
||||
strReview,
|
||||
strImage,
|
||||
strLabel,
|
||||
iUserrating,
|
||||
lastScraped,
|
||||
strReleaseType)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (args))
|
||||
else:
|
||||
args = list(args)
|
||||
del args[8]
|
||||
self.cursor.execute('''
|
||||
INSERT INTO album(
|
||||
idAlbum,
|
||||
strAlbum,
|
||||
strMusicBrainzAlbumID,
|
||||
strArtistDisp,
|
||||
strGenres,
|
||||
iYear,
|
||||
bCompilation,
|
||||
strReview,
|
||||
strLabel,
|
||||
iUserrating,
|
||||
lastScraped,
|
||||
strReleaseType)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (args))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def update_album(self, *args):
|
||||
if app.SYNC.artwork:
|
||||
self.cursor.execute('''
|
||||
UPDATE album
|
||||
SET strAlbum = ?,
|
||||
strMusicBrainzAlbumID = ?,
|
||||
strArtistDisp = ?,
|
||||
strGenres = ?,
|
||||
iYear = ?,
|
||||
bCompilation = ?,
|
||||
strReview = ?,
|
||||
strImage = ?,
|
||||
strLabel = ?,
|
||||
iUserrating = ?,
|
||||
lastScraped = ?,
|
||||
strReleaseType = ?
|
||||
WHERE idAlbum = ?
|
||||
''', (args))
|
||||
else:
|
||||
args = list(args)
|
||||
del args[7]
|
||||
self.cursor.execute('''
|
||||
UPDATE album
|
||||
SET strAlbum = ?,
|
||||
strMusicBrainzAlbumID = ?,
|
||||
strArtistDisp = ?,
|
||||
strGenres = ?,
|
||||
iYear = ?,
|
||||
bCompilation = ?,
|
||||
strReview = ?,
|
||||
strLabel = ?,
|
||||
iUserrating = ?,
|
||||
lastScraped = ?,
|
||||
strReleaseType = ?
|
||||
WHERE idAlbum = ?
|
||||
''', (args))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def add_albumartist(self, artist_id, kodi_id, artistname):
|
||||
self.cursor.execute('''
|
||||
INSERT OR REPLACE INTO album_artist(
|
||||
idArtist,
|
||||
idAlbum,
|
||||
strArtist)
|
||||
VALUES (?, ?, ?)
|
||||
''', (artist_id, kodi_id, artistname))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def add_music_genres(self, kodiid, genres, mediatype):
|
||||
"""
|
||||
Adds a list of genres (list of unicode) for a certain Kodi item
|
||||
"""
|
||||
if mediatype == "album":
|
||||
# Delete current genres for clean slate
|
||||
self.cursor.execute('DELETE FROM album_genre WHERE idAlbum = ?',
|
||||
(kodiid, ))
|
||||
for genre in genres:
|
||||
self.cursor.execute('SELECT idGenre FROM genre WHERE strGenre = ?',
|
||||
(genre, ))
|
||||
try:
|
||||
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('''
|
||||
INSERT OR REPLACE INTO album_genre(
|
||||
idGenre,
|
||||
idAlbum)
|
||||
VALUES (?, ?)
|
||||
''', (genreid, kodiid))
|
||||
elif mediatype == "song":
|
||||
# Delete current genres for clean slate
|
||||
self.cursor.execute('DELETE FROM song_genre WHERE idSong = ?',
|
||||
(kodiid, ))
|
||||
for genre in genres:
|
||||
self.cursor.execute('SELECT idGenre FROM genre WHERE strGenre = ?',
|
||||
(genre, ))
|
||||
try:
|
||||
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('''
|
||||
INSERT OR REPLACE INTO song_genre(
|
||||
idGenre,
|
||||
idSong,
|
||||
iOrder)
|
||||
VALUES (?, ?, ?)
|
||||
''', (genreid, kodiid, 0))
|
||||
|
||||
def add_song_id(self):
|
||||
self.cursor.execute('SELECT COALESCE(MAX(idSong),0) FROM song')
|
||||
return self.cursor.fetchone()[0] + 1
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def add_song(self, *args):
|
||||
self.cursor.execute('''
|
||||
INSERT INTO song(
|
||||
idSong,
|
||||
idAlbum,
|
||||
idPath,
|
||||
strArtistDisp,
|
||||
strGenres,
|
||||
strTitle,
|
||||
iTrack,
|
||||
iDuration,
|
||||
iYear,
|
||||
strFileName,
|
||||
strMusicBrainzTrackID,
|
||||
iTimesPlayed,
|
||||
lastplayed,
|
||||
rating,
|
||||
iStartOffset,
|
||||
iEndOffset,
|
||||
mood,
|
||||
dateAdded)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (args))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def add_song_17(self, *args):
|
||||
self.cursor.execute('''
|
||||
INSERT INTO song(
|
||||
idSong,
|
||||
idAlbum,
|
||||
idPath,
|
||||
strArtists,
|
||||
strGenres,
|
||||
strTitle,
|
||||
iTrack,
|
||||
iDuration,
|
||||
iYear,
|
||||
strFileName,
|
||||
strMusicBrainzTrackID,
|
||||
iTimesPlayed,
|
||||
lastplayed,
|
||||
rating,
|
||||
iStartOffset,
|
||||
iEndOffset,
|
||||
mood,
|
||||
dateAdded)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (args))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def update_song(self, *args):
|
||||
self.cursor.execute('''
|
||||
UPDATE song
|
||||
SET idAlbum = ?,
|
||||
strArtistDisp = ?,
|
||||
strGenres = ?,
|
||||
strTitle = ?,
|
||||
iTrack = ?,
|
||||
iDuration = ?,
|
||||
iYear = ?,
|
||||
strFilename = ?,
|
||||
iTimesPlayed = ?,
|
||||
lastplayed = ?,
|
||||
rating = ?,
|
||||
comment = ?,
|
||||
mood = ?,
|
||||
dateAdded = ?
|
||||
WHERE idSong = ?
|
||||
''', (args))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def set_playcount(self, *args):
|
||||
self.cursor.execute('''
|
||||
UPDATE song
|
||||
SET iTimesPlayed = ?,
|
||||
lastplayed = ?
|
||||
WHERE idSong = ?
|
||||
''', (args))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def update_song_17(self, *args):
|
||||
self.cursor.execute('''
|
||||
UPDATE song
|
||||
SET idAlbum = ?,
|
||||
strArtists = ?,
|
||||
strGenres = ?,
|
||||
strTitle = ?,
|
||||
iTrack = ?,
|
||||
iDuration = ?,
|
||||
iYear = ?,
|
||||
strFilename = ?,
|
||||
iTimesPlayed = ?,
|
||||
lastplayed = ?,
|
||||
rating = ?,
|
||||
comment = ?,
|
||||
mood = ?,
|
||||
dateAdded = ?
|
||||
WHERE idSong = ?
|
||||
''', (args))
|
||||
|
||||
def path_id_from_song(self, kodi_id):
|
||||
self.cursor.execute('SELECT idPath FROM song WHERE idSong = ? LIMIT 1',
|
||||
(kodi_id, ))
|
||||
try:
|
||||
return self.cursor.fetchone()[0]
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def add_artist(self, name, musicbrainz):
|
||||
"""
|
||||
Adds a single artist's name to the db
|
||||
"""
|
||||
self.cursor.execute('''
|
||||
SELECT idArtist, strArtist
|
||||
FROM artist
|
||||
WHERE strMusicBrainzArtistID = ?
|
||||
''', (musicbrainz, ))
|
||||
try:
|
||||
result = self.cursor.fetchone()
|
||||
artistid = result[0]
|
||||
artistname = result[1]
|
||||
except TypeError:
|
||||
self.cursor.execute('SELECT idArtist FROM artist WHERE strArtist = ? COLLATE NOCASE',
|
||||
(name, ))
|
||||
try:
|
||||
artistid = self.cursor.fetchone()[0]
|
||||
except TypeError:
|
||||
# Krypton has a dummy first entry idArtist: 1 strArtist:
|
||||
# [Missing Tag] strMusicBrainzArtistID: Artist Tag Missing
|
||||
self.cursor.execute('''
|
||||
INSERT INTO artist(strArtist, strMusicBrainzArtistID)
|
||||
VALUES (?, ?)
|
||||
''', (name, musicbrainz))
|
||||
artistid = self.cursor.lastrowid
|
||||
else:
|
||||
if artistname != name:
|
||||
self.cursor.execute('UPDATE artist SET strArtist = ? WHERE idArtist = ?',
|
||||
(name, artistid,))
|
||||
return artistid
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def update_artist(self, *args):
|
||||
if app.SYNC.artwork:
|
||||
self.cursor.execute('''
|
||||
UPDATE artist
|
||||
SET strGenres = ?,
|
||||
strBiography = ?,
|
||||
strImage = ?,
|
||||
strFanart = ?,
|
||||
lastScraped = ?
|
||||
WHERE idArtist = ?
|
||||
''', (args))
|
||||
else:
|
||||
args = list(args)
|
||||
del args[3], args[2]
|
||||
self.cursor.execute('''
|
||||
UPDATE artist
|
||||
SET strGenres = ?,
|
||||
strBiography = ?,
|
||||
lastScraped = ?
|
||||
WHERE idArtist = ?
|
||||
''', (args))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def remove_song(self, kodi_id):
|
||||
self.cursor.execute('DELETE FROM song WHERE idSong = ?', (kodi_id, ))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def remove_path(self, path_id):
|
||||
self.cursor.execute('DELETE FROM path WHERE idPath = ?', (path_id, ))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def add_song_artist(self, artist_id, song_id, artist_name):
|
||||
self.cursor.execute('''
|
||||
INSERT OR REPLACE INTO song_artist(
|
||||
idArtist,
|
||||
idSong,
|
||||
idRole,
|
||||
iOrder,
|
||||
strArtist)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', (artist_id, song_id, 1, 0, artist_name))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def add_albuminfosong(self, song_id, album_id, track_no, track_title,
|
||||
runtime):
|
||||
"""
|
||||
Kodi 17 only
|
||||
"""
|
||||
self.cursor.execute('''
|
||||
INSERT OR REPLACE INTO albuminfosong(
|
||||
idAlbumInfoSong,
|
||||
idAlbumInfo,
|
||||
iTrack,
|
||||
strTitle,
|
||||
iDuration)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', (song_id, album_id, track_no, track_title, runtime))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def update_userrating(self, kodi_id, kodi_type, userrating):
|
||||
"""
|
||||
Updates userrating for songs and albums
|
||||
"""
|
||||
if kodi_type == v.KODI_TYPE_SONG:
|
||||
column = 'userrating'
|
||||
identifier = 'idSong'
|
||||
elif kodi_type == v.KODI_TYPE_ALBUM:
|
||||
column = 'iUserrating'
|
||||
identifier = 'idAlbum'
|
||||
else:
|
||||
return
|
||||
self.cursor.execute('''UPDATE %s SET %s = ? WHERE ? = ?'''
|
||||
% (kodi_type, column),
|
||||
(userrating, identifier, kodi_id))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def remove_albuminfosong(self, kodi_id):
|
||||
"""
|
||||
Kodi 17 only
|
||||
"""
|
||||
self.cursor.execute('DELETE FROM albuminfosong WHERE idAlbumInfoSong = ?',
|
||||
(kodi_id, ))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def remove_album(self, kodi_id):
|
||||
if v.KODIVERSION < 18:
|
||||
self.cursor.execute('DELETE FROM albuminfosong WHERE idAlbumInfo = ?',
|
||||
(kodi_id, ))
|
||||
self.cursor.execute('DELETE FROM album_artist WHERE idAlbum = ?',
|
||||
(kodi_id, ))
|
||||
self.cursor.execute('DELETE FROM album WHERE idAlbum = ?', (kodi_id, ))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def remove_artist(self, kodi_id):
|
||||
self.cursor.execute('DELETE FROM album_artist WHERE idArtist = ?',
|
||||
(kodi_id, ))
|
||||
self.cursor.execute('DELETE FROM artist WHERE idArtist = ?',
|
||||
(kodi_id, ))
|
||||
self.cursor.execute('DELETE FROM song_artist WHERE idArtist = ?',
|
||||
(kodi_id, ))
|
|
@ -1,17 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
|
||||
from . import common
|
||||
|
||||
|
||||
class KodiTextureDB(common.KodiDBBase):
|
||||
db_kind = 'texture'
|
||||
|
||||
def url_not_yet_cached(self, url):
|
||||
"""
|
||||
Returns True if url has not yet been cached to the Kodi texture cache
|
||||
"""
|
||||
self.artcursor.execute('SELECT url FROM texture WHERE url = ? LIMIT 1',
|
||||
(url, ))
|
||||
return self.artcursor.fetchone() is None
|
File diff suppressed because it is too large
Load diff
1291
resources/lib/kodidb_functions.py
Normal file
1291
resources/lib/kodidb_functions.py
Normal file
File diff suppressed because it is too large
Load diff
105
resources/lib/kodijsonrpc.py
Normal file
105
resources/lib/kodijsonrpc.py
Normal file
|
@ -0,0 +1,105 @@
|
|||
import xbmc
|
||||
import json
|
||||
|
||||
|
||||
class JSONRPCMethod:
|
||||
|
||||
class Exception(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self):
|
||||
self.family = None
|
||||
|
||||
def __getattr__(self, method):
|
||||
def handler(**kwargs):
|
||||
command = {
|
||||
'jsonrpc': '2.0',
|
||||
'id': 1,
|
||||
'method': '{0}.{1}'.format(self.family, method)
|
||||
}
|
||||
|
||||
if kwargs:
|
||||
command['params'] = kwargs
|
||||
|
||||
# xbmc.log(json.dumps(command))
|
||||
ret = json.loads(xbmc.executeJSONRPC(json.dumps(command)))
|
||||
|
||||
if ret:
|
||||
if 'error' in ret:
|
||||
raise self.Exception(ret['error'])
|
||||
else:
|
||||
return ret['result']
|
||||
else:
|
||||
return None
|
||||
|
||||
return handler
|
||||
|
||||
def __call__(self, family):
|
||||
self.family = family
|
||||
return self
|
||||
|
||||
|
||||
class KodiJSONRPC:
|
||||
def __init__(self):
|
||||
self.methodHandler = JSONRPCMethod()
|
||||
|
||||
def __getattr__(self, family):
|
||||
return self.methodHandler(family)
|
||||
|
||||
|
||||
rpc = KodiJSONRPC()
|
||||
|
||||
|
||||
class BuiltInMethod:
|
||||
|
||||
class Exception(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self):
|
||||
self.module = None
|
||||
|
||||
def __getattr__(self, method):
|
||||
def handler(*args, **kwargs):
|
||||
args = [str(a).replace(',', '\,') for a in args]
|
||||
for k, v in kwargs.items():
|
||||
args.append('{0}={v}'.format(k, str(v).replace(',', '\,')))
|
||||
|
||||
if args:
|
||||
command = '{0}.{1}({2})'.format(self.module, method, ','.join(args))
|
||||
else:
|
||||
command = '{0}.{1}'.format(self.module, method)
|
||||
|
||||
xbmc.log(command, xbmc.LOGNOTICE)
|
||||
|
||||
xbmc.executebuiltin(command)
|
||||
|
||||
return handler
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
args = [str(a).replace(',', '\,') for a in args]
|
||||
for k, v in kwargs.items():
|
||||
args.append('{0}={v}'.format(k, str(v).replace(',', '\,')))
|
||||
|
||||
if args:
|
||||
command = '{0}({1})'.format(self.module, ','.join(args))
|
||||
else:
|
||||
command = '{0}'.format(self.module)
|
||||
|
||||
xbmc.log(command, xbmc.LOGNOTICE)
|
||||
|
||||
xbmc.executebuiltin(command)
|
||||
|
||||
def initModule(self, module):
|
||||
self.module = module
|
||||
return self
|
||||
|
||||
|
||||
class KodiBuiltin:
|
||||
def __init__(self):
|
||||
self.methodHandler = BuiltInMethod()
|
||||
|
||||
def __getattr__(self, module):
|
||||
return self.methodHandler.initModule(module)
|
||||
|
||||
|
||||
builtin = KodiBuiltin()
|
|
@ -6,37 +6,71 @@ PKC Kodi Monitoring implementation
|
|||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
from json import loads
|
||||
from threading import Thread
|
||||
import copy
|
||||
import json
|
||||
import binascii
|
||||
|
||||
import xbmc
|
||||
from xbmcgui import Window
|
||||
|
||||
from .plex_api import API
|
||||
from .plex_db import PlexDB
|
||||
from .kodi_db import KodiVideoDB
|
||||
from . import kodi_db
|
||||
from . import plexdb_functions as plexdb
|
||||
from . import kodidb_functions as kodidb
|
||||
from . import utils
|
||||
from . import plex_functions as PF
|
||||
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 playback
|
||||
from . import initialsetup
|
||||
from . import playqueue as PQ
|
||||
from . import json_rpc as js
|
||||
from . import playlist_func as PL
|
||||
from . import state
|
||||
from . import variables as v
|
||||
|
||||
###############################################################################
|
||||
|
||||
LOG = getLogger('PLEX.kodimonitor')
|
||||
|
||||
# settings: window-variable
|
||||
WINDOW_SETTINGS = {
|
||||
'plex_restricteduser': 'plex_restricteduser',
|
||||
'force_transcode_pix': 'plex_force_transcode_pix'
|
||||
}
|
||||
|
||||
# settings: state-variable (state.py)
|
||||
# Need to use getattr and setattr!
|
||||
STATE_SETTINGS = {
|
||||
'dbSyncIndicator': 'SYNC_DIALOG',
|
||||
'remapSMB': 'REMAP_PATH',
|
||||
'remapSMBmovieOrg': 'remapSMBmovieOrg',
|
||||
'remapSMBmovieNew': 'remapSMBmovieNew',
|
||||
'remapSMBtvOrg': 'remapSMBtvOrg',
|
||||
'remapSMBtvNew': 'remapSMBtvNew',
|
||||
'remapSMBmusicOrg': 'remapSMBmusicOrg',
|
||||
'remapSMBmusicNew': 'remapSMBmusicNew',
|
||||
'remapSMBphotoOrg': 'remapSMBphotoOrg',
|
||||
'remapSMBphotoNew': 'remapSMBphotoNew',
|
||||
'enableMusic': 'ENABLE_MUSIC',
|
||||
'forceReloadSkinOnPlaybackStop': 'FORCE_RELOAD_SKIN',
|
||||
'fetch_pms_item_number': 'FETCH_PMS_ITEM_NUMBER',
|
||||
'imageSyncNotifications': 'IMAGE_SYNC_NOTIFICATIONS',
|
||||
'syncSpecificPlexPlaylists': 'SYNC_SPECIFIC_PLEX_PLAYLISTS',
|
||||
'syncSpecificKodiPlaylists': 'SYNC_SPECIFIC_KODI_PLAYLISTS',
|
||||
'showExtrasInsteadOfTrailer': 'SHOW_EXTRAS_INSTEAD_OF_PLAYING_TRAILER'
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
|
||||
|
||||
class KodiMonitor(xbmc.Monitor):
|
||||
"""
|
||||
PKC implementation of the Kodi Monitor class. Invoke only once.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.xbmcplayer = xbmc.Player()
|
||||
self._already_slept = False
|
||||
self._switched_to_plex_streams = True
|
||||
self.hack_replay = None
|
||||
xbmc.Monitor.__init__(self)
|
||||
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)
|
||||
for playerid in state.PLAYER_STATES:
|
||||
state.PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE)
|
||||
state.OLD_PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE)
|
||||
LOG.info("Kodi monitor started.")
|
||||
|
||||
def onScanStarted(self, library):
|
||||
|
@ -56,6 +90,44 @@ class KodiMonitor(xbmc.Monitor):
|
|||
Monitor the PKC settings for changes made by the user
|
||||
"""
|
||||
LOG.debug('PKC settings change detected')
|
||||
changed = False
|
||||
# Reset the window variables from the settings variables
|
||||
for settings_value, window_value in WINDOW_SETTINGS.iteritems():
|
||||
if utils.window(window_value) != utils.settings(settings_value):
|
||||
changed = True
|
||||
LOG.debug('PKC window settings changed: %s is now %s',
|
||||
settings_value, utils.settings(settings_value))
|
||||
utils.window(window_value, value=utils.settings(settings_value))
|
||||
# Reset the state variables in state.py
|
||||
for settings_value, state_name in STATE_SETTINGS.iteritems():
|
||||
new = utils.settings(settings_value)
|
||||
if new == 'true':
|
||||
new = True
|
||||
elif new == 'false':
|
||||
new = False
|
||||
if getattr(state, state_name) != new:
|
||||
changed = True
|
||||
LOG.debug('PKC state settings %s changed from %s to %s',
|
||||
settings_value, getattr(state, state_name), new)
|
||||
setattr(state, state_name, new)
|
||||
if state_name == 'FETCH_PMS_ITEM_NUMBER':
|
||||
LOG.info('Requesting playlist/nodes refresh')
|
||||
utils.plex_command('RUN_LIB_SCAN', 'views')
|
||||
# Special cases, overwrite all internal settings
|
||||
initialsetup.set_replace_paths()
|
||||
state.BACKGROUND_SYNC_DISABLED = utils.settings(
|
||||
'enableBackgroundSync') == 'false'
|
||||
state.FULL_SYNC_INTERVALL = int(utils.settings('fullSyncInterval')) * 60
|
||||
state.BACKGROUNDSYNC_SAFTYMARGIN = int(
|
||||
utils.settings('backgroundsync_saftyMargin'))
|
||||
state.SYNC_THREAD_NUMBER = int(utils.settings('syncThreadNumber'))
|
||||
state.SSL_CERT_PATH = utils.settings('sslcert') \
|
||||
if utils.settings('sslcert') != 'None' else None
|
||||
if changed is True:
|
||||
# Assume that the user changed the settings so that we can now find
|
||||
# the path to all media files
|
||||
state.STOP_SYNC = False
|
||||
state.PATH_VERIFIED = False
|
||||
|
||||
def onNotification(self, sender, method, data):
|
||||
"""
|
||||
|
@ -65,50 +137,117 @@ class KodiMonitor(xbmc.Monitor):
|
|||
data = loads(data, 'utf-8')
|
||||
LOG.debug("Method: %s Data: %s", method, data)
|
||||
|
||||
# Hack
|
||||
if not method == 'Player.OnStop':
|
||||
self.hack_replay = None
|
||||
|
||||
if method == "Player.OnPlay":
|
||||
with app.APP.lock_playqueues:
|
||||
state.SUSPEND_SYNC = True
|
||||
with state.LOCK_PLAYQUEUES:
|
||||
self.PlayBackStart(data)
|
||||
elif method == 'Player.OnAVChange':
|
||||
with app.APP.lock_playqueues:
|
||||
self._on_av_change(data)
|
||||
elif method == "Player.OnStop":
|
||||
with app.APP.lock_playqueues:
|
||||
_playback_cleanup(ended=data.get('end'))
|
||||
# Should refresh our video nodes, e.g. on deck
|
||||
# xbmc.executebuiltin('ReloadSkin()')
|
||||
if (self.hack_replay and not data.get('end') and
|
||||
self.hack_replay == data['item']):
|
||||
# Hack for add-on paths
|
||||
self.hack_replay = None
|
||||
with state.LOCK_PLAYQUEUES:
|
||||
self._hack_addon_paths_replay_video()
|
||||
elif data.get('end'):
|
||||
if state.PKC_CAUSED_STOP is True:
|
||||
state.PKC_CAUSED_STOP = False
|
||||
LOG.debug('PKC caused this playback stop - ignoring')
|
||||
else:
|
||||
with state.LOCK_PLAYQUEUES:
|
||||
_playback_cleanup(ended=True)
|
||||
else:
|
||||
with state.LOCK_PLAYQUEUES:
|
||||
_playback_cleanup()
|
||||
state.PKC_CAUSED_STOP_DONE = True
|
||||
state.SUSPEND_SYNC = False
|
||||
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:
|
||||
with state.LOCK_PLAYQUEUES:
|
||||
self._playlist_onadd(data)
|
||||
elif method == 'Playlist.OnRemove':
|
||||
self._playlist_onremove(data)
|
||||
elif method == 'Playlist.OnClear':
|
||||
with app.APP.lock_playqueues:
|
||||
with state.LOCK_PLAYQUEUES:
|
||||
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:
|
||||
kodiid = item['id']
|
||||
item_type = item['type']
|
||||
except (KeyError, TypeError):
|
||||
LOG.info("Item is invalid for playstate update.")
|
||||
return
|
||||
# Send notification to the server.
|
||||
with plexdb.Get_Plex_DB() as plexcur:
|
||||
plex_dbitem = plexcur.getItem_byKodiId(kodiid, item_type)
|
||||
try:
|
||||
itemid = plex_dbitem[0]
|
||||
except TypeError:
|
||||
LOG.error("Could not find itemid in plex database for a "
|
||||
"video library update")
|
||||
else:
|
||||
# notify the server
|
||||
if playcount > 0:
|
||||
PF.scrobble(itemid, 'watched')
|
||||
else:
|
||||
PF.scrobble(itemid, 'unwatched')
|
||||
elif method == "VideoLibrary.OnRemove":
|
||||
pass
|
||||
elif method == "System.OnSleep":
|
||||
# Connection is going to sleep
|
||||
LOG.info("Marking the server as offline. SystemOnSleep activated.")
|
||||
utils.window('plex_online', value="sleep")
|
||||
elif method == "System.OnWake":
|
||||
# Allow network to wake up
|
||||
self.waitForAbort(10)
|
||||
app.CONN.online = False
|
||||
xbmc.sleep(10000)
|
||||
utils.window('plex_online', value="false")
|
||||
elif method == "GUI.OnScreensaverDeactivated":
|
||||
if utils.settings('dbSyncScreensaver') == "true":
|
||||
self.waitForAbort(5)
|
||||
app.SYNC.run_lib_scan = 'full'
|
||||
xbmc.sleep(5000)
|
||||
utils.plex_command('RUN_LIB_SCAN', 'full')
|
||||
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)
|
||||
state.STOP_PKC = True
|
||||
|
||||
@staticmethod
|
||||
def _hack_addon_paths_replay_video():
|
||||
"""
|
||||
Hack we need for RESUMABLE items because Kodi lost the path of the
|
||||
last played item that is now being replayed (see playback.py's
|
||||
Player().play()) Also see playqueue.py _compare_playqueues()
|
||||
|
||||
Needed if user re-starts the same video from the library using addon
|
||||
paths. (Video is only added to playqueue, then immediately stoppen.
|
||||
There is no playback initialized by Kodi.) Log excerpts:
|
||||
Method: Playlist.OnAdd Data:
|
||||
{u'item': {u'type': u'movie', u'id': 4},
|
||||
u'playlistid': 1,
|
||||
u'position': 0}
|
||||
Now we would hack!
|
||||
Method: Player.OnStop Data:
|
||||
{u'item': {u'type': u'movie', u'id': 4},
|
||||
u'end': False}
|
||||
(within the same micro-second!)
|
||||
"""
|
||||
LOG.info('Detected re-start of playback of last item')
|
||||
old = state.OLD_PLAYER_STATES[1]
|
||||
kwargs = {
|
||||
'plex_id': old['plex_id'],
|
||||
'plex_type': old['plex_type'],
|
||||
'path': old['file'],
|
||||
'resolve': False
|
||||
}
|
||||
thread = Thread(target=playback.playback_triage, kwargs=kwargs)
|
||||
thread.start()
|
||||
|
||||
def _playlist_onadd(self, data):
|
||||
"""
|
||||
|
@ -122,7 +261,15 @@ class KodiMonitor(xbmc.Monitor):
|
|||
}
|
||||
Will NOT be called if playback initiated by Kodi widgets
|
||||
"""
|
||||
pass
|
||||
if 'id' not in data['item']:
|
||||
return
|
||||
old = state.OLD_PLAYER_STATES[data['playlistid']]
|
||||
if (not state.DIRECT_PATHS and
|
||||
data['position'] == 0 and data['playlistid'] == 1 and
|
||||
not PQ.PLAYQUEUES[data['playlistid']].items and
|
||||
data['item']['type'] == old['kodi_type'] and
|
||||
data['item']['id'] == old['kodi_id']):
|
||||
self.hack_replay = data['item']
|
||||
|
||||
def _playlist_onremove(self, data):
|
||||
"""
|
||||
|
@ -134,8 +281,7 @@ 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:
|
||||
{
|
||||
|
@ -149,8 +295,7 @@ class KodiMonitor(xbmc.Monitor):
|
|||
else:
|
||||
LOG.debug('Detected PKC clear - ignoring')
|
||||
|
||||
@staticmethod
|
||||
def _get_ids(kodi_id, kodi_type, path):
|
||||
def _get_ids(self, kodi_id, kodi_type, path):
|
||||
"""
|
||||
Returns the tuple (plex_id, plex_type) or (None, None)
|
||||
"""
|
||||
|
@ -159,13 +304,16 @@ class KodiMonitor(xbmc.Monitor):
|
|||
plex_type = None
|
||||
# If using direct paths and starting playback from a widget
|
||||
if not kodi_id and kodi_type and path:
|
||||
kodi_id, _ = kodi_db.kodiid_from_filename(path, kodi_type)
|
||||
kodi_id, _ = kodidb.kodiid_from_filename(path, kodi_type)
|
||||
if kodi_id:
|
||||
with PlexDB() as plexdb:
|
||||
db_item = plexdb.item_by_kodi_id(kodi_id, kodi_type)
|
||||
if db_item:
|
||||
plex_id = db_item['plex_id']
|
||||
plex_type = db_item['plex_type']
|
||||
with plexdb.Get_Plex_DB() as plex_db:
|
||||
plex_dbitem = plex_db.getItem_byKodiId(kodi_id, kodi_type)
|
||||
try:
|
||||
plex_id = plex_dbitem[0]
|
||||
plex_type = plex_dbitem[2]
|
||||
except TypeError:
|
||||
# No plex id, hence item not in the library. E.g. clips
|
||||
pass
|
||||
return plex_id, plex_type
|
||||
|
||||
@staticmethod
|
||||
|
@ -183,7 +331,7 @@ class KodiMonitor(xbmc.Monitor):
|
|||
try:
|
||||
for i, item in enumerate(items):
|
||||
PL.add_item_to_plex_playqueue(playqueue, i + 1, kodi_item=item)
|
||||
except exceptions.PlaylistError:
|
||||
except PL.PlaylistError:
|
||||
LOG.info('Could not build Plex playlist for: %s', items)
|
||||
|
||||
def _json_item(self, playerid):
|
||||
|
@ -197,7 +345,7 @@ class KodiMonitor(xbmc.Monitor):
|
|||
# start as Kodi updates this info very late!! Might get previous
|
||||
# element otherwise
|
||||
self._already_slept = True
|
||||
self.waitForAbort(1)
|
||||
xbmc.sleep(1000)
|
||||
try:
|
||||
json_item = js.get_item(playerid)
|
||||
except KeyError:
|
||||
|
@ -208,18 +356,6 @@ class KodiMonitor(xbmc.Monitor):
|
|||
json_item.get('type'),
|
||||
json_item.get('file'))
|
||||
|
||||
@staticmethod
|
||||
def _start_next_episode(data):
|
||||
"""
|
||||
Used for the add-on Upnext to start playback of the next episode
|
||||
"""
|
||||
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'))
|
||||
|
||||
def PlayBackStart(self, data):
|
||||
"""
|
||||
Called whenever playback is started. Example data:
|
||||
|
@ -256,7 +392,7 @@ class KodiMonitor(xbmc.Monitor):
|
|||
return
|
||||
playerid = js.get_playlist_id(playlist_type)
|
||||
if not playerid:
|
||||
LOG.error('Coud not get playerid for data %s', data)
|
||||
LOG.error('Coud not get playerid for data', data)
|
||||
return
|
||||
playqueue = PQ.PLAYQUEUES[playerid]
|
||||
info = js.get_player_props(playerid)
|
||||
|
@ -268,10 +404,9 @@ class KodiMonitor(xbmc.Monitor):
|
|||
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]
|
||||
status = state.PLAYER_STATES[playerid]
|
||||
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)
|
||||
|
@ -289,24 +424,9 @@ class KodiMonitor(xbmc.Monitor):
|
|||
# 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
|
||||
initialize = True
|
||||
else:
|
||||
initialize = False
|
||||
if initialize:
|
||||
|
@ -316,13 +436,9 @@ class KodiMonitor(xbmc.Monitor):
|
|||
plex_id, plex_type = self._get_ids(kodi_id, kodi_type, path)
|
||||
if not plex_id:
|
||||
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')
|
||||
state.PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE)
|
||||
return
|
||||
item = PL.init_plex_playqueue(playqueue, plex_id=plex_id)
|
||||
item.file = path
|
||||
# Set the Plex container key (e.g. using the Plex playqueue)
|
||||
container_key = None
|
||||
|
@ -343,13 +459,8 @@ class KodiMonitor(xbmc.Monitor):
|
|||
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)
|
||||
state.ACTIVE_PLAYERS.add(playerid)
|
||||
status.update(info)
|
||||
LOG.debug('Set the Plex container_key to: %s', container_key)
|
||||
status['container_key'] = container_key
|
||||
|
@ -360,44 +471,38 @@ class KodiMonitor(xbmc.Monitor):
|
|||
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)
|
||||
@utils.thread_methods
|
||||
class SpecialMonitor(Thread):
|
||||
"""
|
||||
Detect the resume dialog for widgets.
|
||||
Could also be used to detect external players (see Emby implementation)
|
||||
"""
|
||||
def run(self):
|
||||
LOG.info("----====# Starting Special Monitor #====----")
|
||||
# "Start from beginning", "Play from beginning"
|
||||
strings = (utils.try_encode(utils.lang(12021)),
|
||||
utils.try_encode(utils.lang(12023)))
|
||||
while not self.stopped():
|
||||
if xbmc.getCondVisibility('Window.IsVisible(DialogContextMenu.xml)'):
|
||||
if xbmc.getInfoLabel('Control.GetLabel(1002)') in strings:
|
||||
# Remember that the item IS indeed resumable
|
||||
control = int(Window(10106).getFocusId())
|
||||
state.RESUME_PLAYBACK = True if control == 1001 else False
|
||||
else:
|
||||
# Different context menu is displayed
|
||||
state.RESUME_PLAYBACK = False
|
||||
if xbmc.getCondVisibility('Window.IsVisible(MyVideoNav.xml)'):
|
||||
path = xbmc.getInfoLabel('container.folderpath')
|
||||
if (isinstance(path, str) and
|
||||
path.startswith('special://profile/playlists')):
|
||||
pass
|
||||
# TODO: start polling PMS for playlist changes
|
||||
# Optionally: poll PMS continuously with custom intervall
|
||||
xbmc.sleep(200)
|
||||
LOG.info("#====---- Special Monitor Stopped ----====#")
|
||||
|
||||
|
||||
def _playback_cleanup(ended=False):
|
||||
|
@ -407,19 +512,16 @@ def _playback_cleanup(ended=False):
|
|||
timing data otherwise)
|
||||
"""
|
||||
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
|
||||
state.ACTIVE_PLAYERS)
|
||||
# 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
|
||||
for playerid in app.PLAYSTATE.active_players:
|
||||
status = app.PLAYSTATE.player_states[playerid]
|
||||
state.PLEX_TRANSIENT_TOKEN = None
|
||||
for playerid in state.ACTIVE_PLAYERS:
|
||||
status = state.PLAYER_STATES[playerid]
|
||||
# Remember the last played item later
|
||||
app.PLAYSTATE.old_player_states[playerid] = copy.deepcopy(status)
|
||||
state.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',
|
||||
|
@ -430,56 +532,40 @@ def _playback_cleanup(ended=False):
|
|||
# started playback via PMS
|
||||
_record_playstate(status, ended)
|
||||
# Reset the player's status
|
||||
app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
|
||||
state.PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE)
|
||||
# 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')
|
||||
state.ACTIVE_PLAYERS = set()
|
||||
LOG.info('Finished PKC playback cleanup')
|
||||
|
||||
|
||||
def _record_playstate(status, ended):
|
||||
if not status['plex_id']:
|
||||
LOG.debug('No Plex id found to record playstate for status %s', status)
|
||||
return
|
||||
if status['plex_type'] not in v.PLEX_VIDEOTYPES:
|
||||
LOG.debug('Not messing with non-video entries')
|
||||
return
|
||||
with PlexDB() as plexdb:
|
||||
db_item = plexdb.item_by_id(status['plex_id'], status['plex_type'])
|
||||
if not db_item:
|
||||
with plexdb.Get_Plex_DB() as plex_db:
|
||||
kodi_db_item = plex_db.getItem_byId(status['plex_id'])
|
||||
if kodi_db_item is None:
|
||||
# Item not (yet) in Kodi library
|
||||
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
|
||||
totaltime = float(utils.kodi_time_to_millis(status['totaltime'])) / 1000
|
||||
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(utils.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()
|
||||
last_played = utils.unix_date_to_kodi(utils.unix_timestamp())
|
||||
if playcount is None:
|
||||
LOG.debug('playcount not found, looking it up in the Kodi DB')
|
||||
with kodi_db.KodiVideoDB() as kodidb:
|
||||
playcount = kodidb.get_playcount(db_item['kodi_fileid'])
|
||||
with kodidb.GetKodiDB('video') as kodi_db:
|
||||
playcount = kodi_db.get_playcount(kodi_db_item[1])
|
||||
playcount = 0 if playcount is None else playcount
|
||||
if time < v.IGNORE_SECONDS_AT_START:
|
||||
LOG.debug('Ignoring playback less than %s seconds',
|
||||
|
@ -493,43 +579,21 @@ def _record_playstate(status, ended):
|
|||
v.MARK_PLAYED_AT)
|
||||
playcount += 1
|
||||
time = 0
|
||||
with kodi_db.KodiVideoDB() as kodidb:
|
||||
kodidb.set_resume(db_item['kodi_fileid'],
|
||||
time,
|
||||
totaltime,
|
||||
playcount,
|
||||
last_played)
|
||||
if 'kodi_fileid_2' in db_item and db_item['kodi_fileid_2']:
|
||||
# Dirty hack for our episodes
|
||||
kodidb.set_resume(db_item['kodi_fileid_2'],
|
||||
time,
|
||||
totaltime,
|
||||
playcount,
|
||||
last_played)
|
||||
with kodidb.GetKodiDB('video') as kodi_db:
|
||||
kodi_db.set_resume(kodi_db_item[1],
|
||||
time,
|
||||
totaltime,
|
||||
playcount,
|
||||
last_played,
|
||||
status['plex_type'])
|
||||
# Hack to force "in progress" widget to appear if it wasn't visible before
|
||||
if (app.APP.force_reload_skin and
|
||||
if (state.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
|
||||
thread = Thread(target=_clean_file_table)
|
||||
thread.setDaemon(True)
|
||||
thread.start()
|
||||
|
||||
|
||||
def _clean_file_table():
|
||||
|
@ -540,146 +604,16 @@ 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
|
||||
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)
|
||||
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):
|
||||
"""
|
||||
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:
|
||||
i = 0
|
||||
while i < 100 and not state.STOP_PKC:
|
||||
with kodidb.GetKodiDB('video') as kodi_db:
|
||||
files = kodi_db.obsolete_file_ids()
|
||||
if files:
|
||||
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
|
||||
|
||||
|
||||
def _complete_artwork_keys(info):
|
||||
"""
|
||||
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 _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')
|
||||
i += 1
|
||||
xbmc.sleep(50)
|
||||
with kodidb.GetKodiDB('video') as kodi_db:
|
||||
for file_id in files:
|
||||
LOG.debug('Removing obsolete Kodi file_id %s', file_id)
|
||||
kodi_db.remove_file(file_id[0], remove_orphans=False)
|
||||
LOG.debug('Done cleaning up Kodi file table')
|
||||
|
|
|
@ -1,9 +1 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
|
||||
from .full_sync import start
|
||||
from .websocket import store_websocket_message, process_websocket_messages, \
|
||||
WEBSOCKET_MESSAGES, PLAYSTATE_SESSIONS
|
||||
from .common import update_kodi_library, PLAYLIST_SYNC_ENABLED
|
||||
from .fanart import FanartThread, FanartTask
|
||||
from .sections import force_full_sync, delete_files, clear_window_vars
|
||||
# Dummy file to make this directory a package.
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
#!/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')
|
||||
|
||||
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()
|
||||
|
||||
def wait_while_suspended(self):
|
||||
"""
|
||||
Return immediately
|
||||
"""
|
||||
return self.should_cancel()
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
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
|
||||
if music:
|
||||
xbmc.executebuiltin('UpdateLibrary(music)')
|
||||
|
||||
|
||||
def tag_last(iterable):
|
||||
"""
|
||||
Given some iterable, returns (last, item), where last is only True if you
|
||||
are on the final iteration.
|
||||
"""
|
||||
iterator = iter(iterable)
|
||||
gotone = False
|
||||
try:
|
||||
lookback = next(iterator)
|
||||
gotone = True
|
||||
while True:
|
||||
cur = next(iterator)
|
||||
yield False, lookback
|
||||
lookback = cur
|
||||
except StopIteration:
|
||||
if gotone:
|
||||
yield True, lookback
|
||||
raise StopIteration()
|
|
@ -1,154 +1,141 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
from threading import Thread
|
||||
from Queue import Empty
|
||||
import xbmc
|
||||
|
||||
from ..plex_api import API
|
||||
from ..plex_db import PlexDB
|
||||
from ..kodi_db import KodiVideoDB
|
||||
from .. import backgroundthread, utils
|
||||
from .. import itemtypes, plex_functions as PF, variables as v, app
|
||||
from .. import utils, plexdb_functions as plexdb, kodidb_functions as kodidb
|
||||
from .. import itemtypes, artwork, plex_functions as PF, variables as v, state
|
||||
|
||||
###############################################################################
|
||||
|
||||
LOG = getLogger("PLEX." + __name__)
|
||||
|
||||
###############################################################################
|
||||
|
||||
|
||||
LOG = getLogger('PLEX.sync.fanart')
|
||||
|
||||
SUPPORTED_TYPES = (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)
|
||||
SYNC_FANART = (utils.settings('FanartTV') == 'true' and
|
||||
utils.settings('usePlexArtwork') == 'true')
|
||||
PREFER_KODI_COLLECTION_ART = utils.settings('PreferKodiCollectionArt') == 'false'
|
||||
BATCH_SIZE = 500
|
||||
|
||||
|
||||
class FanartThread(backgroundthread.KillableThread):
|
||||
@utils.thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD',
|
||||
'DB_SCAN',
|
||||
'STOP_SYNC',
|
||||
'SUSPEND_SYNC'])
|
||||
class ThreadedProcessFanart(Thread):
|
||||
"""
|
||||
This will potentially take hours!
|
||||
"""
|
||||
def __init__(self, callback, refresh=False):
|
||||
self.callback = callback
|
||||
self.refresh = refresh
|
||||
super(FanartThread, self).__init__()
|
||||
Threaded download of additional fanart in the background
|
||||
|
||||
def should_suspend(self):
|
||||
return self._suspended or app.APP.is_playing_video
|
||||
Input:
|
||||
queue Queue.Queue() object that you will need to fill with
|
||||
dicts of the following form:
|
||||
{
|
||||
'plex_id': the Plex id as a string
|
||||
'plex_type': the Plex media type, e.g. 'movie'
|
||||
'refresh': True/False if True, will overwrite any 3rd party
|
||||
fanart. If False, will only get missing
|
||||
}
|
||||
"""
|
||||
def __init__(self, queue):
|
||||
self.queue = queue
|
||||
Thread.__init__(self)
|
||||
|
||||
def run(self):
|
||||
LOG.info('Starting FanartThread')
|
||||
app.APP.register_fanart_thread(self)
|
||||
try:
|
||||
self._run()
|
||||
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):
|
||||
finished = False
|
||||
while not finished:
|
||||
finished = self._loop()
|
||||
if self.wait_while_suspended():
|
||||
break
|
||||
LOG.info('FanartThread finished: %s', finished)
|
||||
self.callback(finished)
|
||||
"""
|
||||
Do the work
|
||||
"""
|
||||
LOG.debug("---===### Starting FanartSync ###===---")
|
||||
while not self.stopped():
|
||||
# In the event the server goes offline
|
||||
while self.suspended():
|
||||
# Set in service.py
|
||||
if self.stopped():
|
||||
# Abort was requested while waiting. We should exit
|
||||
LOG.debug("---===### Stopped FanartSync ###===---")
|
||||
return
|
||||
xbmc.sleep(1000)
|
||||
# grabs Plex item from queue
|
||||
try:
|
||||
item = self.queue.get(block=False)
|
||||
except Empty:
|
||||
xbmc.sleep(200)
|
||||
continue
|
||||
self.queue.task_done()
|
||||
if isinstance(item, artwork.ArtworkSyncMessage):
|
||||
if state.IMAGE_SYNC_NOTIFICATIONS:
|
||||
utils.dialog('notification',
|
||||
heading=utils.lang(29999),
|
||||
message=item.message,
|
||||
icon='{plex}',
|
||||
sound=False)
|
||||
continue
|
||||
LOG.debug('Get additional fanart for Plex id %s', item['plex_id'])
|
||||
_process(item)
|
||||
LOG.debug("---===### Stopped FanartSync ###===---")
|
||||
|
||||
|
||||
class FanartTask(backgroundthread.Task):
|
||||
"""
|
||||
This task will also be executed while library sync is suspended!
|
||||
"""
|
||||
def setup(self, plex_id, plex_type, refresh=False):
|
||||
self.plex_id = plex_id
|
||||
self.plex_type = plex_type
|
||||
self.refresh = refresh
|
||||
|
||||
def run(self):
|
||||
process_fanart(self.plex_id, self.plex_type, self.refresh)
|
||||
|
||||
|
||||
def process_fanart(plex_id, plex_type, refresh=False):
|
||||
"""
|
||||
Will look for additional fanart for the plex_type item with plex_id.
|
||||
Will check if we already got all artwork and only look if some are indeed
|
||||
missing.
|
||||
Will set the fanart_synced flag in the Plex DB if successful.
|
||||
"""
|
||||
def _process(item):
|
||||
done = False
|
||||
try:
|
||||
artworks = None
|
||||
with PlexDB() as plexdb:
|
||||
db_item = plexdb.item_by_id(plex_id,
|
||||
plex_type)
|
||||
if not db_item:
|
||||
LOG.error('Could not get Kodi id for plex id %s', plex_id)
|
||||
with plexdb.Get_Plex_DB() as plex_db:
|
||||
db_item = plex_db.getItem_byId(item['plex_id'])
|
||||
try:
|
||||
kodi_id = db_item[0]
|
||||
kodi_type = db_item[4]
|
||||
except TypeError:
|
||||
LOG.error('Could not get Kodi id for plex id %s, abort getfanart',
|
||||
item['plex_id'])
|
||||
return
|
||||
if not refresh:
|
||||
with KodiVideoDB() as kodidb:
|
||||
artworks = kodidb.get_art(db_item['kodi_id'],
|
||||
db_item['kodi_type'])
|
||||
if item['refresh'] is False:
|
||||
with kodidb.GetKodiDB('video') as kodi_db:
|
||||
artworks = kodi_db.get_art(kodi_id, kodi_type)
|
||||
# Check if we even need to get additional art
|
||||
for key in v.ALL_KODI_ARTWORK:
|
||||
if key not in artworks:
|
||||
break
|
||||
else:
|
||||
LOG.debug('Already got all fanart for Plex id %s',
|
||||
item['plex_id'])
|
||||
done = True
|
||||
return
|
||||
xml = PF.GetPlexMetadata(plex_id)
|
||||
try:
|
||||
xml[0].attrib
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
LOG.warn('Could not get metadata for %s. Skipping that item '
|
||||
'for now', plex_id)
|
||||
xml = PF.GetPlexMetadata(item['plex_id'])
|
||||
if xml is None:
|
||||
LOG.error('Could not get metadata for %s. Skipping that item '
|
||||
'for now', item['plex_id'])
|
||||
return
|
||||
elif xml == 401:
|
||||
LOG.error('HTTP 401 returned by PMS. Too much strain? '
|
||||
'Cancelling sync for now')
|
||||
return
|
||||
api = API(xml[0])
|
||||
if artworks is None:
|
||||
artworks = api.artwork()
|
||||
# Get additional missing artwork from fanart artwork sites
|
||||
artworks = api.fanart_artwork(artworks)
|
||||
with itemtypes.ITEMTYPE_FROM_PLEXTYPE[plex_type](None) as context:
|
||||
context.set_fanart(artworks,
|
||||
db_item['kodi_id'],
|
||||
db_item['kodi_type'])
|
||||
with getattr(itemtypes,
|
||||
v.ITEMTYPE_FROM_PLEXTYPE[item['plex_type']])() as itm:
|
||||
itm.set_fanart(artworks, kodi_id, kodi_type)
|
||||
# Additional fanart for sets/collections
|
||||
if plex_type == v.PLEX_TYPE_MOVIE:
|
||||
for _, setname in api.collections():
|
||||
if api.plex_type() == v.PLEX_TYPE_MOVIE:
|
||||
for _, setname in api.collection_list():
|
||||
LOG.debug('Getting artwork for movie set %s', setname)
|
||||
with KodiVideoDB() as kodidb:
|
||||
setid = kodidb.create_collection(setname)
|
||||
with kodidb.GetKodiDB('video') as kodi_db:
|
||||
setid = kodi_db.create_collection(setname)
|
||||
external_set_artwork = api.set_artwork()
|
||||
if external_set_artwork and PREFER_KODI_COLLECTION_ART:
|
||||
if (external_set_artwork and
|
||||
utils.settings('PreferKodiCollectionArt') == 'false'):
|
||||
kodi_artwork = api.artwork(kodi_id=setid,
|
||||
kodi_type=v.KODI_TYPE_SET)
|
||||
for art in kodi_artwork:
|
||||
if art in external_set_artwork:
|
||||
del external_set_artwork[art]
|
||||
with itemtypes.Movie(None) as movie:
|
||||
movie.kodidb.modify_artwork(external_set_artwork,
|
||||
setid,
|
||||
v.KODI_TYPE_SET)
|
||||
with itemtypes.Movies() as movie_db:
|
||||
movie_db.artwork.modify_artwork(external_set_artwork,
|
||||
setid,
|
||||
v.KODI_TYPE_SET,
|
||||
movie_db.kodicursor)
|
||||
done = True
|
||||
finally:
|
||||
if done is True:
|
||||
with PlexDB() as plexdb:
|
||||
plexdb.set_fanart_synced(plex_id, plex_type)
|
||||
LOG.debug('Done getting fanart for Plex id %s', item['plex_id'])
|
||||
with plexdb.Get_Plex_DB() as plex_db:
|
||||
plex_db.set_fanart_synched(item['plex_id'])
|
||||
|
|
|
@ -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())
|
|
@ -1,325 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import Queue
|
||||
|
||||
import xbmcgui
|
||||
|
||||
from .get_metadata import GetMetadataThread
|
||||
from .fill_metadata_queue import FillMetadataQueue
|
||||
from .process_metadata import ProcessMetadataThread
|
||||
from . import common, sections
|
||||
from .. import utils, timing, backgroundthread as bg, variables as v, app
|
||||
from .. import plex_functions as PF, itemtypes, path_ops
|
||||
|
||||
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
|
||||
# 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):
|
||||
def __init__(self, repair, callback, show_dialog):
|
||||
"""
|
||||
repair=True: force sync EVERY item
|
||||
"""
|
||||
self.successful = True
|
||||
self.repair = repair
|
||||
self.callback = callback
|
||||
# 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.install_sync_done = utils.settings('SyncInstallRunDone') == 'true'
|
||||
super(FullSync, self).__init__()
|
||||
|
||||
def update_progressbar(self, section, title, current):
|
||||
if not self.dialog:
|
||||
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
|
||||
|
||||
@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:
|
||||
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:
|
||||
context.commit()
|
||||
except RuntimeError:
|
||||
LOG.error('Could not entirely process section %s', section)
|
||||
self.successful = False
|
||||
|
||||
def threaded_get_generators(self, kinds, section_queue, items):
|
||||
"""
|
||||
Getting iterators is costly, so let's do it in a dedicated thread
|
||||
"""
|
||||
LOG.debug('Start threaded_get_generators')
|
||||
try:
|
||||
for kind in kinds:
|
||||
for section in (x for x in app.SYNC.sections
|
||||
if x.section_type == kind[1]):
|
||||
if self.should_cancel():
|
||||
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':
|
||||
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)
|
||||
except Exception:
|
||||
utils.ERROR(notify=True)
|
||||
finally:
|
||||
# Sentinel for the section queue
|
||||
section_queue.put(None)
|
||||
LOG.debug('Exiting threaded_get_generators')
|
||||
|
||||
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)
|
||||
]
|
||||
if app.SYNC.enable_music:
|
||||
kinds.extend([
|
||||
(v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_ARTIST),
|
||||
(v.PLEX_TYPE_ALBUM, v.PLEX_TYPE_ARTIST),
|
||||
])
|
||||
|
||||
# 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)
|
||||
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
|
||||
if app.SYNC.enable_music:
|
||||
kinds.extend([
|
||||
(v.PLEX_TYPE_SONG, v.PLEX_TYPE_ARTIST),
|
||||
])
|
||||
|
||||
# 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')
|
||||
# Make sure we're not showing an item's title in the sync dialog
|
||||
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
|
||||
|
||||
# Delete movies that are not on Plex anymore
|
||||
LOG.debug('Looking for items to delete')
|
||||
kinds = [
|
||||
(v.PLEX_TYPE_MOVIE, itemtypes.Movie),
|
||||
(v.PLEX_TYPE_SHOW, itemtypes.Show),
|
||||
(v.PLEX_TYPE_SEASON, itemtypes.Season),
|
||||
(v.PLEX_TYPE_EPISODE, itemtypes.Episode)
|
||||
]
|
||||
if app.SYNC.enable_music:
|
||||
kinds.extend([
|
||||
(v.PLEX_TYPE_ARTIST, itemtypes.Artist),
|
||||
(v.PLEX_TYPE_ALBUM, itemtypes.Album),
|
||||
(v.PLEX_TYPE_SONG, itemtypes.Song)
|
||||
])
|
||||
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))
|
||||
for plex_id in plex_ids:
|
||||
if self.should_cancel():
|
||||
return
|
||||
ctx.remove(plex_id, plex_type)
|
||||
if len(plex_ids) < DELETION_BATCH_SIZE:
|
||||
break
|
||||
LOG.debug('Done looking for items to delete')
|
||||
|
||||
@utils.log_time
|
||||
def _run(self):
|
||||
try:
|
||||
# Get latest Plex libraries and build playlist and video node files
|
||||
if self.should_cancel() or not sections.sync_from_pms(self):
|
||||
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():
|
||||
# "ERROR in library sync"
|
||||
utils.dialog('notification',
|
||||
heading='{plex}',
|
||||
message=utils.lang(39410),
|
||||
icon='{error}')
|
||||
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()
|
|
@ -1,123 +1,122 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
from threading import Thread
|
||||
from Queue import Empty
|
||||
from xbmc import sleep
|
||||
|
||||
from . import common
|
||||
from ..plex_api import API
|
||||
from .. import backgroundthread, plex_functions as PF, utils, variables as v
|
||||
from .. import utils
|
||||
from .. import plex_functions as PF
|
||||
from . import sync_info
|
||||
|
||||
LOG = getLogger('PLEX.sync.get_metadata')
|
||||
LOCK = backgroundthread.threading.Lock()
|
||||
###############################################################################
|
||||
|
||||
LOG = getLogger("PLEX." + __name__)
|
||||
|
||||
###############################################################################
|
||||
|
||||
|
||||
class GetMetadataThread(common.LibrarySyncMixin,
|
||||
backgroundthread.KillableThread):
|
||||
@utils.thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD',
|
||||
'STOP_SYNC',
|
||||
'SUSPEND_SYNC'])
|
||||
class ThreadedGetMetadata(Thread):
|
||||
"""
|
||||
Threaded download of Plex XML metadata for a certain library item.
|
||||
Fills the queue with the downloaded etree XML objects
|
||||
Fills the out_queue with the downloaded etree XML objects
|
||||
|
||||
Input:
|
||||
queue Queue.Queue() object that you'll need to fill up
|
||||
with plex_ids
|
||||
out_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, out_queue):
|
||||
self.queue = queue
|
||||
self.out_queue = out_queue
|
||||
Thread.__init__(self)
|
||||
|
||||
def _collections(self, item):
|
||||
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:
|
||||
LOG.error('Could not download collections')
|
||||
return
|
||||
# Extract what we need to know
|
||||
collection_match = \
|
||||
[(utils.cast(int, x.get('index')),
|
||||
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():
|
||||
return
|
||||
if plex_set_id not in collection_xmls:
|
||||
# Get Plex metadata for collections - a pain
|
||||
for index, collection_plex_id in collection_match:
|
||||
if index == plex_set_id:
|
||||
collection_xml = PF.GetPlexMetadata(collection_plex_id)
|
||||
try:
|
||||
collection_xml[0].attrib
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
LOG.error('Could not get collection %s %s',
|
||||
collection_plex_id, set_name)
|
||||
continue
|
||||
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]
|
||||
|
||||
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 terminate_now(self):
|
||||
"""
|
||||
Needed to terminate this thread, because there might be items left in
|
||||
the queue which could cause other threads to hang
|
||||
"""
|
||||
while not self.queue.empty():
|
||||
# Still try because remaining item might have been taken
|
||||
try:
|
||||
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)
|
||||
self.queue.get(block=False)
|
||||
except Empty:
|
||||
sleep(10)
|
||||
continue
|
||||
else:
|
||||
self.queue.task_done()
|
||||
if self.stopped():
|
||||
# Shutdown from outside requested; purge out_queue as well
|
||||
while not self.out_queue.empty():
|
||||
# Still try because remaining item might have been taken
|
||||
try:
|
||||
self.out_queue.get(block=False)
|
||||
except Empty:
|
||||
sleep(10)
|
||||
continue
|
||||
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()
|
||||
else:
|
||||
self.out_queue.task_done()
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Do the work
|
||||
"""
|
||||
LOG.debug('Starting get metadata thread')
|
||||
# cache local variables because it's faster
|
||||
queue = self.queue
|
||||
out_queue = self.out_queue
|
||||
stopped = self.stopped
|
||||
while stopped() is False:
|
||||
# grabs Plex item from queue
|
||||
try:
|
||||
item = queue.get(block=False)
|
||||
# Empty queue
|
||||
except Empty:
|
||||
sleep(20)
|
||||
continue
|
||||
# Download Metadata
|
||||
xml = PF.GetPlexMetadata(item['plex_id'])
|
||||
if xml is None:
|
||||
# Did not receive a valid XML - skip that item for now
|
||||
LOG.error("Could not get metadata for %s. Skipping that item "
|
||||
"for now", item['plex_id'])
|
||||
# Increase BOTH counters - since metadata won't be processed
|
||||
with sync_info.LOCK:
|
||||
sync_info.GET_METADATA_COUNT += 1
|
||||
sync_info.PROCESS_METADATA_COUNT += 1
|
||||
queue.task_done()
|
||||
continue
|
||||
elif xml == 401:
|
||||
LOG.error('HTTP 401 returned by PMS. Too much strain? '
|
||||
'Cancelling sync for now')
|
||||
utils.window('plex_scancrashed', value='401')
|
||||
# Kill remaining items in queue (for main thread to cont.)
|
||||
queue.task_done()
|
||||
break
|
||||
|
||||
item['xml'] = xml
|
||||
if item.get('get_children') is True:
|
||||
children_xml = PF.GetAllPlexChildren(item['plex_id'])
|
||||
try:
|
||||
children_xml[0].attrib
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
LOG.error('Could not get children for Plex id %s',
|
||||
item['plex_id'])
|
||||
item['children'] = []
|
||||
else:
|
||||
item['children'] = children_xml
|
||||
|
||||
# place item into out queue
|
||||
out_queue.put(item)
|
||||
# Keep track of where we are at
|
||||
with sync_info.LOCK:
|
||||
sync_info.GET_METADATA_COUNT += 1
|
||||
# signals to queue job is done
|
||||
queue.task_done()
|
||||
# Empty queue in case PKC was shut down (main thread hangs otherwise)
|
||||
self.terminate_now()
|
||||
LOG.debug('Get metadata thread terminated')
|
||||
|
|
|
@ -1,412 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- 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
|
||||
|
||||
ICON_PATH = 'special://home/addons/plugin.video.plexkodiconnect/icon.png'
|
||||
RECOMMENDED_SCORE_LOWER_BOUND = 7
|
||||
|
||||
# Logic of the following nodes:
|
||||
# (node_type,
|
||||
# label/node name,
|
||||
# args for PKC add-on callback,
|
||||
# Kodi "content",
|
||||
# Bool: does this node's xml even point back to PKC add-on callback?
|
||||
# )
|
||||
NODE_TYPES = {
|
||||
v.PLEX_TYPE_MOVIE: (
|
||||
('plex_ondeck',
|
||||
utils.lang(39500), # "On Deck"
|
||||
{
|
||||
'mode': 'browseplex',
|
||||
'key': '/library/sections/{self.section_id}/onDeck',
|
||||
'section_id': '{self.section_id}'
|
||||
},
|
||||
v.CONTENT_TYPE_MOVIE,
|
||||
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',
|
||||
'section_id': '{self.section_id}'
|
||||
},
|
||||
v.CONTENT_TYPE_MOVIE,
|
||||
False),
|
||||
('all',
|
||||
'{self.name}', # We're using this section's name
|
||||
{
|
||||
'mode': 'browseplex',
|
||||
'key': '/library/sections/{self.section_id}/all',
|
||||
'section_id': '{self.section_id}'
|
||||
},
|
||||
v.CONTENT_TYPE_MOVIE,
|
||||
False),
|
||||
('recommended',
|
||||
utils.lang(30230), # "Recommended"
|
||||
{
|
||||
'mode': 'browseplex',
|
||||
'key': ('/library/sections/{self.section_id}&%s'
|
||||
% urllib.urlencode({'sort': 'rating:desc'})),
|
||||
'section_id': '{self.section_id}'
|
||||
},
|
||||
v.CONTENT_TYPE_MOVIE,
|
||||
False),
|
||||
('genres',
|
||||
utils.lang(135), # "Genres"
|
||||
{
|
||||
'mode': 'browseplex',
|
||||
'key': '/library/sections/{self.section_id}/genre',
|
||||
'section_id': '{self.section_id}'
|
||||
},
|
||||
v.CONTENT_TYPE_MOVIE,
|
||||
False),
|
||||
('sets',
|
||||
utils.lang(39501), # "Collections"
|
||||
{
|
||||
'mode': 'browseplex',
|
||||
'key': '/library/sections/{self.section_id}/collection',
|
||||
'section_id': '{self.section_id}'
|
||||
},
|
||||
v.CONTENT_TYPE_MOVIE,
|
||||
False),
|
||||
('random',
|
||||
utils.lang(30227), # "Random"
|
||||
{
|
||||
'mode': 'browseplex',
|
||||
'key': ('/library/sections/{self.section_id}&%s'
|
||||
% urllib.urlencode({'sort': 'random'})),
|
||||
'section_id': '{self.section_id}'
|
||||
},
|
||||
v.CONTENT_TYPE_MOVIE,
|
||||
False),
|
||||
('lastplayed',
|
||||
utils.lang(568), # "Last played"
|
||||
{
|
||||
'mode': 'browseplex',
|
||||
'key': '/library/sections/{self.section_id}/recentlyViewed',
|
||||
'section_id': '{self.section_id}'
|
||||
},
|
||||
v.CONTENT_TYPE_MOVIE,
|
||||
False),
|
||||
('browse',
|
||||
utils.lang(39702), # "Browse by folder"
|
||||
{
|
||||
'mode': 'browseplex',
|
||||
'key': '/library/sections/{self.section_id}/folder',
|
||||
'plex_type': '{self.section_type}',
|
||||
'section_id': '{self.section_id}',
|
||||
'folder': True
|
||||
},
|
||||
v.CONTENT_TYPE_MOVIE,
|
||||
True),
|
||||
('more',
|
||||
utils.lang(22082), # "More..."
|
||||
{
|
||||
'mode': 'browseplex',
|
||||
'key': '/library/sections/{self.section_id}',
|
||||
'section_id': '{self.section_id}',
|
||||
'folder': True
|
||||
},
|
||||
v.CONTENT_TYPE_FILE,
|
||||
True),
|
||||
),
|
||||
###########################################################
|
||||
v.PLEX_TYPE_SHOW: (
|
||||
('ondeck',
|
||||
utils.lang(39500), # "On Deck"
|
||||
{
|
||||
'mode': 'browseplex',
|
||||
'key': '/library/sections/{self.section_id}/onDeck',
|
||||
'section_id': '{self.section_id}'
|
||||
},
|
||||
v.CONTENT_TYPE_EPISODE,
|
||||
True),
|
||||
('recent',
|
||||
utils.lang(30174), # "Recently Added"
|
||||
{
|
||||
'mode': 'browseplex',
|
||||
'key': '/library/sections/{self.section_id}/recentlyAdded',
|
||||
'section_id': '{self.section_id}'
|
||||
},
|
||||
v.CONTENT_TYPE_EPISODE,
|
||||
False),
|
||||
('all',
|
||||
'{self.name}', # We're using this section's name
|
||||
{
|
||||
'mode': 'browseplex',
|
||||
'key': '/library/sections/{self.section_id}/all',
|
||||
'section_id': '{self.section_id}'
|
||||
},
|
||||
v.CONTENT_TYPE_SHOW,
|
||||
False),
|
||||
('recommended',
|
||||
utils.lang(30230), # "Recommended"
|
||||
{
|
||||
'mode': 'browseplex',
|
||||
'key': ('/library/sections/{self.section_id}&%s'
|
||||
% urllib.urlencode({'sort': 'rating:desc'})),
|
||||
'section_id': '{self.section_id}'
|
||||
},
|
||||
v.CONTENT_TYPE_SHOW,
|
||||
False),
|
||||
('genres',
|
||||
utils.lang(135), # "Genres"
|
||||
{
|
||||
'mode': 'browseplex',
|
||||
'key': '/library/sections/{self.section_id}/genre',
|
||||
'section_id': '{self.section_id}'
|
||||
},
|
||||
v.CONTENT_TYPE_SHOW,
|
||||
False),
|
||||
('sets',
|
||||
utils.lang(39501), # "Collections"
|
||||
{
|
||||
'mode': 'browseplex',
|
||||
'key': '/library/sections/{self.section_id}/collection',
|
||||
'section_id': '{self.section_id}'
|
||||
},
|
||||
v.CONTENT_TYPE_SHOW,
|
||||
True), # There are no sets/collections for shows with Kodi
|
||||
('random',
|
||||
utils.lang(30227), # "Random"
|
||||
{
|
||||
'mode': 'browseplex',
|
||||
'key': ('/library/sections/{self.section_id}&%s'
|
||||
% urllib.urlencode({'sort': 'random'})),
|
||||
'section_id': '{self.section_id}'
|
||||
},
|
||||
v.CONTENT_TYPE_SHOW,
|
||||
False),
|
||||
('lastplayed',
|
||||
utils.lang(568), # "Last played"
|
||||
{
|
||||
'mode': 'browseplex',
|
||||
'key': ('/library/sections/{self.section_id}/recentlyViewed&%s'
|
||||
% urllib.urlencode({'type': v.PLEX_TYPE_NUMBER_FROM_PLEX_TYPE[v.PLEX_TYPE_EPISODE]})),
|
||||
'section_id': '{self.section_id}'
|
||||
},
|
||||
v.CONTENT_TYPE_EPISODE,
|
||||
False),
|
||||
('browse',
|
||||
utils.lang(39702), # "Browse by folder"
|
||||
{
|
||||
'mode': 'browseplex',
|
||||
'key': '/library/sections/{self.section_id}/folder',
|
||||
'section_id': '{self.section_id}',
|
||||
'folder': True
|
||||
},
|
||||
v.CONTENT_TYPE_EPISODE,
|
||||
True),
|
||||
('more',
|
||||
utils.lang(22082), # "More..."
|
||||
{
|
||||
'mode': 'browseplex',
|
||||
'key': '/library/sections/{self.section_id}',
|
||||
'section_id': '{self.section_id}',
|
||||
'folder': True
|
||||
},
|
||||
v.CONTENT_TYPE_FILE,
|
||||
True),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
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'})
|
||||
etree.SubElement(xml, 'label').text = node_name
|
||||
etree.SubElement(xml, 'icon').text = ICON_PATH
|
||||
etree.SubElement(xml, 'content').text = section.content
|
||||
etree.SubElement(xml, 'path').text = section.addon_path(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),
|
||||
'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
|
||||
if ((section.section_type == v.PLEX_TYPE_SHOW and
|
||||
utils.settings('TVShowWatched') == 'false') or
|
||||
(section.section_type == v.PLEX_TYPE_MOVIE and
|
||||
utils.settings('MovieShowWatched') == 'false')):
|
||||
# Adds an additional rule if user deactivated the PKC setting
|
||||
# "Recently Added: Also show already watched episodes"
|
||||
# or
|
||||
# "Recently Added: Also show already watched episodes"
|
||||
rule = etree.SubElement(xml, 'rule', attrib={'field': 'playcount',
|
||||
'operator': 'is'})
|
||||
etree.SubElement(rule, 'value').text = '0'
|
||||
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 = 'dateadded'
|
||||
return xml
|
||||
|
||||
|
||||
def node_all(section, node_name):
|
||||
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, 'label').text = node_name
|
||||
etree.SubElement(xml, 'icon').text = ICON_PATH
|
||||
etree.SubElement(xml, 'content').text = section.content
|
||||
etree.SubElement(xml,
|
||||
'order',
|
||||
attrib={'direction':
|
||||
'ascending'}).text = 'sorttitle'
|
||||
return xml
|
||||
|
||||
|
||||
def node_recommended(section, node_name):
|
||||
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
|
||||
# rule = etree.SubElement(xml, 'rule', attrib={'field': 'rating',
|
||||
# 'operator': 'greaterthan'})
|
||||
# etree.SubElement(rule, 'value').text = unicode(RECOMMENDED_SCORE_LOWER_BOUND)
|
||||
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 = 'rating'
|
||||
return xml
|
||||
|
||||
|
||||
def node_genres(section, node_name):
|
||||
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, 'label').text = node_name
|
||||
etree.SubElement(xml, 'icon').text = ICON_PATH
|
||||
etree.SubElement(xml, 'content').text = section.content
|
||||
etree.SubElement(xml,
|
||||
'order',
|
||||
attrib={'direction':
|
||||
'ascending'}).text = 'sorttitle'
|
||||
etree.SubElement(xml, 'group').text = 'genres'
|
||||
return xml
|
||||
|
||||
|
||||
def node_sets(section, node_name):
|
||||
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
|
||||
# "Collections"
|
||||
etree.SubElement(xml, 'label').text = node_name
|
||||
etree.SubElement(xml, 'icon').text = ICON_PATH
|
||||
etree.SubElement(xml, 'content').text = section.content
|
||||
etree.SubElement(xml,
|
||||
'order',
|
||||
attrib={'direction':
|
||||
'ascending'}).text = 'sorttitle'
|
||||
etree.SubElement(xml, 'group').text = 'sets'
|
||||
return xml
|
||||
|
||||
|
||||
def node_random(section, node_name):
|
||||
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, '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':
|
||||
'ascending'}).text = 'random'
|
||||
return xml
|
||||
|
||||
|
||||
def node_lastplayed(section, node_name):
|
||||
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
|
||||
rule = etree.SubElement(xml, 'rule', attrib={'field': 'playcount',
|
||||
'operator': 'greaterthan'})
|
||||
etree.SubElement(rule, 'value').text = '0'
|
||||
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
|
|
@ -1,92 +1,88 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
from threading import Thread
|
||||
from Queue import Empty
|
||||
from xbmc import sleep
|
||||
|
||||
from . import common, sections
|
||||
from ..plex_db import PlexDB
|
||||
from .. import backgroundthread, app
|
||||
from .. import utils
|
||||
from .. import itemtypes
|
||||
from . import sync_info
|
||||
|
||||
LOG = getLogger('PLEX.sync.process_metadata')
|
||||
###############################################################################
|
||||
LOG = getLogger("PLEX." + __name__)
|
||||
|
||||
COMMIT_TO_DB_EVERY_X_ITEMS = 500
|
||||
###############################################################################
|
||||
|
||||
|
||||
class ProcessMetadataThread(common.LibrarySyncMixin,
|
||||
backgroundthread.KillableThread):
|
||||
@utils.thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD',
|
||||
'STOP_SYNC',
|
||||
'SUSPEND_SYNC'])
|
||||
class ThreadedProcessMetadata(Thread):
|
||||
"""
|
||||
Invoke once in order to process the received PMS metadata xmls
|
||||
Not yet implemented for more than 1 thread - if ever. Only to be called by
|
||||
ONE thread!
|
||||
Processes the XML metadata in the queue
|
||||
|
||||
Input:
|
||||
queue: Queue.Queue() object that you'll need to fill up with
|
||||
the downloaded XML eTree objects
|
||||
item_class: as used to call functions in itemtypes.py e.g. 'Movies' =>
|
||||
itemtypes.Movies()
|
||||
"""
|
||||
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 __init__(self, queue, item_class):
|
||||
self.queue = queue
|
||||
self.item_class = item_class
|
||||
Thread.__init__(self)
|
||||
|
||||
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 terminate_now(self):
|
||||
"""
|
||||
Needed to terminate this thread, because there might be items left in
|
||||
the queue which could cause other threads to hang
|
||||
"""
|
||||
while not self.queue.empty():
|
||||
# Still try because remaining item might have been taken
|
||||
try:
|
||||
self.queue.get(block=False)
|
||||
except Empty:
|
||||
sleep(10)
|
||||
continue
|
||||
else:
|
||||
self.queue.task_done()
|
||||
|
||||
def 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()
|
||||
def run(self):
|
||||
"""
|
||||
Do the work
|
||||
"""
|
||||
LOG.debug('Processing thread started')
|
||||
# Constructs the method name, e.g. itemtypes.Movies
|
||||
item_fct = getattr(itemtypes, self.item_class)
|
||||
# cache local variables because it's faster
|
||||
queue = self.queue
|
||||
stopped = self.stopped
|
||||
with item_fct() as item_class:
|
||||
while stopped() is False:
|
||||
# grabs item from queue
|
||||
try:
|
||||
item = queue.get(block=False)
|
||||
except Empty:
|
||||
sleep(20)
|
||||
continue
|
||||
# Do the work
|
||||
item_method = getattr(item_class, item['method'])
|
||||
if item.get('children') is not None:
|
||||
item_method(item['xml'][0],
|
||||
viewtag=item['view_name'],
|
||||
viewid=item['view_id'],
|
||||
children=item['children'])
|
||||
else:
|
||||
item_method(item['xml'][0],
|
||||
viewtag=item['view_name'],
|
||||
viewid=item['view_id'])
|
||||
# Keep track of where we are at
|
||||
with sync_info.LOCK:
|
||||
sync_info.PROCESS_METADATA_COUNT += 1
|
||||
sync_info.PROCESSING_VIEW_NAME = item['title']
|
||||
queue.task_done()
|
||||
self.terminate_now()
|
||||
LOG.debug('Processing thread terminated')
|
||||
|
|
|
@ -1,745 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import copy
|
||||
|
||||
from . import nodes
|
||||
from ..plex_db import PlexDB
|
||||
from ..plex_api import API
|
||||
from .. import kodi_db
|
||||
from .. import itemtypes, path_ops
|
||||
from .. import plex_functions as PF, music, utils, variables as v, app
|
||||
from ..utils import etree
|
||||
|
||||
LOG = getLogger('PLEX.sync.sections')
|
||||
|
||||
BATCH_SIZE = 500
|
||||
# Need a way to interrupt our synching process
|
||||
SHOULD_CANCEL = None
|
||||
|
||||
LIBRARY_PATH = path_ops.translate_path('special://profile/library/video/')
|
||||
# The video library might not yet exist for this user - create it
|
||||
if not path_ops.exists(LIBRARY_PATH):
|
||||
path_ops.copy_tree(
|
||||
src=path_ops.translate_path('special://xbmc/system/library/video'),
|
||||
dst=LIBRARY_PATH,
|
||||
preserve_mode=0) # dont copy permission bits so we have write access!
|
||||
PLAYLISTS_PATH = path_ops.translate_path("special://profile/playlists/video/")
|
||||
if not path_ops.exists(PLAYLISTS_PATH):
|
||||
path_ops.makedirs(PLAYLISTS_PATH)
|
||||
|
||||
# Windows variables we set for each node
|
||||
WINDOW_ARGS = ('index', 'title', 'id', 'path', 'type', 'content', 'artwork')
|
||||
|
||||
|
||||
class Section(object):
|
||||
"""
|
||||
Setting the attribute section_type will automatically set content and
|
||||
sync_to_kodi
|
||||
"""
|
||||
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
|
||||
# Building block for window variable
|
||||
self._node = None # unicode
|
||||
# Index of this section (as section_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
|
||||
# This section's name for the user to display
|
||||
self.name = None # unicode
|
||||
# Library type section (NOT the same as the KODI_TYPE_...)
|
||||
# E.g. 'movies', 'tvshows', 'episodes'
|
||||
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
|
||||
# Path to the Kodi userdata library FOLDER for this section
|
||||
self._path = None # unicode
|
||||
# Path to the smart playlist for this section
|
||||
self._playlist_path = None
|
||||
# "Poster" for this section
|
||||
self.icon = None # unicode
|
||||
# Background image for this section
|
||||
self.artwork = None
|
||||
# Thumbnail for this section, similar for each section type
|
||||
self.thumb = None
|
||||
# Order number in which xmls will be listed inside Kodei
|
||||
self.order = None
|
||||
# Original PMS xml for this section, including children
|
||||
self.xml = None
|
||||
# A section_type encompasses possible several plex_types! E.g. shows
|
||||
# contain shows, seasons, episodes
|
||||
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):
|
||||
return ("{{"
|
||||
"'index': {self.index}, "
|
||||
"'name': '{self.name}', "
|
||||
"'section_id': {self.section_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__
|
||||
|
||||
def __nonzero__(self):
|
||||
return (self.section_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
|
||||
self.name == section.name and
|
||||
(self.plex_type == section.plex_type if self.plex_type else
|
||||
self.section_type == section.section_type))
|
||||
|
||||
def __ne__(self, section):
|
||||
return not self == section
|
||||
|
||||
@property
|
||||
def section_id(self):
|
||||
return self._section_id
|
||||
|
||||
@section_id.setter
|
||||
def section_id(self, value):
|
||||
self._section_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)
|
||||
|
||||
@property
|
||||
def section_type(self):
|
||||
return self._section_type
|
||||
|
||||
@section_type.setter
|
||||
def section_type(self, value):
|
||||
self._section_type = value
|
||||
self.content = v.CONTENT_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
|
||||
elif not app.SYNC.enable_music and value == v.PLEX_TYPE_ARTIST:
|
||||
self.sync_to_kodi = False
|
||||
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
|
||||
|
||||
@index.setter
|
||||
def index(self, value):
|
||||
self._index = value
|
||||
self._node = 'Plex.nodes.%s' % value
|
||||
|
||||
@property
|
||||
def node(self):
|
||||
return self._node
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self._path
|
||||
|
||||
@property
|
||||
def playlist_path(self):
|
||||
return self._playlist_path
|
||||
|
||||
def from_db_element(self, section_db_element):
|
||||
self.section_id = section_db_element['section_id']
|
||||
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']
|
||||
|
||||
def from_xml(self, xml_element):
|
||||
"""
|
||||
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.name = api.title()
|
||||
self.section_type = api.plex_type
|
||||
self.icon = api.one_artwork('composite')
|
||||
self.artwork = api.one_artwork('art')
|
||||
self.thumb = api.one_artwork('thumb')
|
||||
self.xml = xml_element
|
||||
|
||||
def from_plex_db(self, section_id, plexdb=None):
|
||||
"""
|
||||
Reads section with id section_id from the plex.db
|
||||
"""
|
||||
if plexdb:
|
||||
section = plexdb.section(section_id)
|
||||
else:
|
||||
with PlexDB(lock=False) as plexdb:
|
||||
section = plexdb.section(section_id)
|
||||
if section:
|
||||
self.from_db_element(section)
|
||||
|
||||
def to_plex_db(self, plexdb=None):
|
||||
"""
|
||||
Writes this Section to the plex.db, potentially overwriting
|
||||
(INSERT OR REPLACE)
|
||||
"""
|
||||
if not self:
|
||||
raise RuntimeError('Section not clearly defined: %s' % self)
|
||||
if plexdb:
|
||||
plexdb.add_section(self.section_id,
|
||||
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,
|
||||
self.name,
|
||||
self.section_type,
|
||||
self.kodi_tagid,
|
||||
self.sync_to_kodi,
|
||||
self.last_sync)
|
||||
|
||||
def addon_path(self, args):
|
||||
"""
|
||||
Returns the plugin path pointing back to PKC for key in order to browse
|
||||
args is a dict. Its values may contain string info of the form
|
||||
{key: '{self.<Section attribute>}'}
|
||||
"""
|
||||
args = copy.deepcopy(args)
|
||||
for key, value in args.iteritems():
|
||||
args[key] = value.format(self=self)
|
||||
return utils.extend_url('plugin://%s' % v.ADDON_ID, args)
|
||||
|
||||
def to_kodi(self):
|
||||
"""
|
||||
Writes this section's nodes to the library folder in the Kodi userdata
|
||||
directory
|
||||
Won't do anything if self.sync_to_kodi is not True
|
||||
"""
|
||||
if self.index is None:
|
||||
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
|
||||
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)
|
||||
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'
|
||||
path = self.addon_path(args)
|
||||
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)
|
||||
# .path leads to all elements of this library
|
||||
if self.section_type in v.PLEX_VIDEOTYPES:
|
||||
utils.window('%s.path' % self.node,
|
||||
value='ActivateWindow(videos,%s,return)' % path)
|
||||
elif self.section_type == v.PLEX_TYPE_ARTIST:
|
||||
utils.window('%s.path' % self.node,
|
||||
value='ActivateWindow(music,%s,return)' % path)
|
||||
else:
|
||||
# Pictures
|
||||
utils.window('%s.path' % self.node,
|
||||
value='ActivateWindow(pictures,%s,return)' % path)
|
||||
utils.window('%s.id' % self.node, value=str(self.section_id))
|
||||
if not self.sync_to_kodi:
|
||||
self.remove_files_from_kodi()
|
||||
return
|
||||
if self.section_type == v.PLEX_TYPE_ARTIST:
|
||||
# Todo: Write window variables for music
|
||||
return
|
||||
if self.section_type == v.PLEX_TYPE_PHOTO:
|
||||
# Todo: Write window variables for photos
|
||||
return
|
||||
|
||||
# Create a dedicated directory for this section
|
||||
if not path_ops.exists(self.path):
|
||||
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)
|
||||
# The xmls are numbered in order of appearance
|
||||
self.order = 0
|
||||
if not path_ops.exists(path_ops.path.join(self.path, 'index.xml')):
|
||||
LOG.debug('Creating index.xml for section %s', self.name)
|
||||
xml = etree.Element('node',
|
||||
attrib={'order': unicode(self.order)})
|
||||
etree.SubElement(xml, 'label').text = self.name
|
||||
etree.SubElement(xml, 'icon').text = self.icon or nodes.ICON_PATH
|
||||
self._write_xml(xml, 'index.xml')
|
||||
self.order += 1
|
||||
# Create the one smart playlist for this section
|
||||
if not path_ops.exists(self.playlist_path):
|
||||
self._write_playlist()
|
||||
# Now build all nodes for this section - potentially creating xmls
|
||||
for node in nodes.NODE_TYPES[self.section_type]:
|
||||
self._build_node(*node)
|
||||
|
||||
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):
|
||||
# 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.order += 1
|
||||
self._window_node(path, node_name, node_type, pms_node)
|
||||
|
||||
def _write_xml(self, xml, xml_name):
|
||||
LOG.debug('Creating xml for section %s: %s', self.name, xml_name)
|
||||
utils.indent(xml)
|
||||
etree.ElementTree(xml).write(path_ops.path.join(self.path, xml_name),
|
||||
encoding='utf-8',
|
||||
xml_declaration=True)
|
||||
|
||||
def _write_playlist(self):
|
||||
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]})
|
||||
etree.SubElement(xml, 'name').text = self.name
|
||||
etree.SubElement(xml, 'match').text = 'all'
|
||||
rule = etree.SubElement(xml, 'rule', attrib={'field': 'tag',
|
||||
'operator': 'is'})
|
||||
etree.SubElement(rule, 'value').text = self.name
|
||||
utils.indent(xml)
|
||||
etree.ElementTree(xml).write(self.playlist_path, encoding='utf-8')
|
||||
|
||||
def _window_node(self, path, node_name, node_type, pms_node):
|
||||
"""
|
||||
Will save this section's node to the Kodi window variables
|
||||
|
||||
Uses the same conventions/logic as Emby for Kodi does
|
||||
"""
|
||||
if pms_node or not self.sync_to_kodi:
|
||||
# Check: elif node_type in ('browse', 'homevideos', 'photos'):
|
||||
window_path = path
|
||||
elif self.section_type == v.PLEX_TYPE_ARTIST:
|
||||
window_path = 'ActivateWindow(Music,%s,return)' % path
|
||||
else:
|
||||
window_path = 'ActivateWindow(Videos,%s,return)' % path
|
||||
# if node_type == 'all':
|
||||
# var = self.node
|
||||
# utils.window('%s.index' % var,
|
||||
# value=path.replace('%s_all.xml' % self.section_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.path' % var, value=window_path)
|
||||
utils.window('%s.type' % var, value=self.content)
|
||||
utils.window('%s.content' % var, value=path)
|
||||
utils.window('%s.artwork' % var, value=self.artwork)
|
||||
|
||||
def remove_files_from_kodi(self):
|
||||
"""
|
||||
Removes this sections from the Kodi userdata library folder (if appl.)
|
||||
Also removes the smart playlist
|
||||
"""
|
||||
if self.section_type in (v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_PHOTO):
|
||||
# No files created for these types
|
||||
return
|
||||
if path_ops.exists(self.path):
|
||||
path_ops.rmtree(self.path, ignore_errors=True)
|
||||
if path_ops.exists(self.playlist_path):
|
||||
try:
|
||||
path_ops.remove(self.playlist_path)
|
||||
except (OSError, IOError):
|
||||
LOG.warn('Could not delete smart playlist for section %s: %s',
|
||||
self.name, self.playlist_path)
|
||||
|
||||
def remove_window_vars(self):
|
||||
"""
|
||||
Removes all windows variables 'Plex.nodes.<section_id>.xxx'
|
||||
"""
|
||||
if self.index is not None:
|
||||
_clear_window_vars(self.index)
|
||||
|
||||
def remove_from_plex(self, plexdb=None):
|
||||
"""
|
||||
Removes this sections completely from the Plex DB
|
||||
"""
|
||||
if plexdb:
|
||||
plexdb.remove_section(self.section_id)
|
||||
else:
|
||||
with PlexDB(lock=False) as plexdb:
|
||||
plexdb.remove_section(self.section_id)
|
||||
|
||||
def remove(self):
|
||||
"""
|
||||
Completely and utterly removes this section from Kodi and Plex DB
|
||||
as well as from the window variables
|
||||
"""
|
||||
self.remove_files_from_kodi()
|
||||
self.remove_window_vars()
|
||||
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
|
||||
full sync (not delta)
|
||||
"""
|
||||
LOG.info('Telling PKC to do a full sync instead of a delta sync')
|
||||
with PlexDB() as plexdb:
|
||||
plexdb.force_full_sync()
|
||||
|
||||
|
||||
def _save_sections_to_plex_db(sections):
|
||||
with PlexDB() as plexdb:
|
||||
for section in sections:
|
||||
section.to_plex_db(plexdb=plexdb)
|
||||
|
||||
|
||||
def _retrieve_old_settings(sections, old_sections):
|
||||
"""
|
||||
Overwrites the PKC settings for sections, grabing them from old_sections
|
||||
if a particular section is in both sections and old_sections
|
||||
|
||||
Thus sets to the old values:
|
||||
section.last_sync
|
||||
section.kodi_tagid
|
||||
section.sync_to_kodi
|
||||
section.last_sync
|
||||
"""
|
||||
for section in 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
|
||||
|
||||
|
||||
def _delete_kodi_db_items(section):
|
||||
if section.section_type == v.PLEX_TYPE_MOVIE:
|
||||
kodi_context = kodi_db.KodiVideoDB
|
||||
types = ((v.PLEX_TYPE_MOVIE, itemtypes.Movie), )
|
||||
elif section.section_type == v.PLEX_TYPE_SHOW:
|
||||
kodi_context = kodi_db.KodiVideoDB
|
||||
types = ((v.PLEX_TYPE_SHOW, itemtypes.Show),
|
||||
(v.PLEX_TYPE_SEASON, itemtypes.Season),
|
||||
(v.PLEX_TYPE_EPISODE, itemtypes.Episode))
|
||||
elif section.section_type == v.PLEX_TYPE_ARTIST:
|
||||
kodi_context = kodi_db.KodiMusicDB
|
||||
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_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():
|
||||
return False
|
||||
typus.remove(plex_id)
|
||||
if len(plex_ids) < BATCH_SIZE:
|
||||
break
|
||||
return True
|
||||
|
||||
|
||||
def _choose_libraries(sections):
|
||||
"""
|
||||
Displays a dialog for the user to select the libraries he wants synched
|
||||
|
||||
Returns True if the user chose new sections, False if he aborted
|
||||
"""
|
||||
import xbmcgui
|
||||
selectable_sections = []
|
||||
preselected = []
|
||||
index = 0
|
||||
for section in sections:
|
||||
if not app.SYNC.enable_music and section.section_type == v.PLEX_TYPE_ARTIST:
|
||||
LOG.info('Ignoring music section: %s', section)
|
||||
continue
|
||||
elif section.section_type == v.PLEX_TYPE_PHOTO:
|
||||
# We won't ever show Photo sections
|
||||
continue
|
||||
else:
|
||||
# Offer user the new section
|
||||
selectable_sections.append(section.name)
|
||||
# Sections have been either preselected by the user or they are new
|
||||
if section.sync_to_kodi:
|
||||
preselected.append(index)
|
||||
index += 1
|
||||
# Don't ask the user again for this PMS even if user cancel the sync dialog
|
||||
utils.settings('sections_asked_for_machine_identifier',
|
||||
value=app.CONN.machine_identifier)
|
||||
# "Select Plex libraries to sync"
|
||||
selected_sections = xbmcgui.Dialog().multiselect(utils.lang(30524),
|
||||
selectable_sections,
|
||||
preselect=preselected,
|
||||
useDetails=False)
|
||||
if selected_sections is None:
|
||||
LOG.info('User chose not to select which libraries to sync')
|
||||
return False
|
||||
index = 0
|
||||
for section in sections:
|
||||
if not app.SYNC.enable_music and section.section_type == v.PLEX_TYPE_ARTIST:
|
||||
continue
|
||||
elif section.section_type == v.PLEX_TYPE_PHOTO:
|
||||
continue
|
||||
else:
|
||||
section.sync_to_kodi = index in selected_sections
|
||||
index += 1
|
||||
return True
|
||||
|
||||
|
||||
def delete_playlists():
|
||||
"""
|
||||
Clean up the playlists
|
||||
"""
|
||||
path = path_ops.translate_path('special://profile/playlists/video/')
|
||||
for root, _, files in path_ops.walk(path):
|
||||
for file in files:
|
||||
if file.startswith('Plex'):
|
||||
path_ops.remove(path_ops.path.join(root, file))
|
||||
|
||||
|
||||
def delete_nodes():
|
||||
"""
|
||||
Clean up video nodes
|
||||
"""
|
||||
path = path_ops.translate_path("special://profile/library/video/")
|
||||
for root, dirs, _ in path_ops.walk(path):
|
||||
for directory in dirs:
|
||||
if directory.startswith('Plex-'):
|
||||
path_ops.rmtree(path_ops.path.join(root, directory))
|
||||
break
|
||||
|
||||
|
||||
def delete_files():
|
||||
"""
|
||||
Deletes both all the Plex-xxx video node xmls as well as smart playlists
|
||||
"""
|
||||
delete_nodes()
|
||||
delete_playlists()
|
||||
|
||||
|
||||
def sync_from_pms(parent_self, pick_libraries=False):
|
||||
"""
|
||||
Sync the Plex library sections.
|
||||
pick_libraries=True will prompt the user the select the libraries he
|
||||
wants to sync
|
||||
"""
|
||||
global SHOULD_CANCEL
|
||||
LOG.info('Starting synching sections from the PMS')
|
||||
SHOULD_CANCEL = parent_self.should_cancel
|
||||
try:
|
||||
return _sync_from_pms(pick_libraries)
|
||||
finally:
|
||||
SHOULD_CANCEL = None
|
||||
LOG.info('Done synching sections from the PMS: %s', app.SYNC.sections)
|
||||
|
||||
|
||||
def _sync_from_pms(pick_libraries):
|
||||
# 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()
|
||||
if xml is None:
|
||||
LOG.error("Error download PMS sections, abort")
|
||||
return False
|
||||
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():
|
||||
old_sections.append(Section(section_db_element=section_db))
|
||||
# Update our latest PMS sections with info saved in the PMS DB
|
||||
_retrieve_old_settings(sections, old_sections)
|
||||
if (app.CONN.machine_identifier != utils.settings('sections_asked_for_machine_identifier') or
|
||||
pick_libraries):
|
||||
if not pick_libraries:
|
||||
LOG.info('First time connecting to this PMS, choosing libraries')
|
||||
_choose_libraries(sections)
|
||||
|
||||
# We got everything - save to Plex db in case Kodi restarts before we're
|
||||
# done here
|
||||
_save_sections_to_plex_db(sections)
|
||||
# Tweak some settings so Kodi does NOT scan the music folders
|
||||
if app.SYNC.direct_paths is True:
|
||||
# Will reboot Kodi is new library detected
|
||||
music.excludefromscan_music_folders(sections)
|
||||
|
||||
# Delete all old sections that are obsolete
|
||||
# This will also delete sections whose name (or type) have changed
|
||||
for old_section in old_sections:
|
||||
for section in sections:
|
||||
if old_section == section:
|
||||
break
|
||||
else:
|
||||
if not old_section.sync_to_kodi:
|
||||
continue
|
||||
LOG.info('Deleting entire section: %s', old_section)
|
||||
# Remove all linked items
|
||||
if not _delete_kodi_db_items(old_section):
|
||||
return False
|
||||
# Remove the section itself
|
||||
old_section.remove()
|
||||
|
||||
# Clear all existing window vars because we did NOT remove them with the
|
||||
# command section.remove()
|
||||
clear_window_vars()
|
||||
# Time to write the sections to Kodi
|
||||
for section in sections:
|
||||
section.to_kodi()
|
||||
# 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
|
||||
return True
|
||||
|
||||
|
||||
def _clear_window_vars(index):
|
||||
node = 'Plex.nodes.%s' % index
|
||||
utils.window('%s.index' % node, clear=True)
|
||||
utils.window('%s.title' % node, clear=True)
|
||||
utils.window('%s.type' % node, clear=True)
|
||||
utils.window('%s.content' % node, clear=True)
|
||||
utils.window('%s.path' % node, clear=True)
|
||||
utils.window('%s.id' % 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:
|
||||
node = 'Plex.nodes.%s.%s.%s' % (index, typus, kind)
|
||||
utils.window(node, clear=True)
|
||||
|
||||
|
||||
def clear_window_vars():
|
||||
"""
|
||||
Removes all references to sections stored in window vars 'Plex.nodes...'
|
||||
"""
|
||||
LOG.debug('Clearing all the Plex video node variables')
|
||||
number_of_nodes = int(utils.window('Plex.nodes.total') or 0)
|
||||
utils.window('Plex.nodes.total', clear=True)
|
||||
for index in range(number_of_nodes):
|
||||
_clear_window_vars(index)
|
||||
|
||||
|
||||
def delete_videonode_files():
|
||||
"""
|
||||
Removes all the PKC video node files under userdata/library/video that
|
||||
start with 'Plex-'
|
||||
"""
|
||||
for root, dirs, _ in path_ops.walk(LIBRARY_PATH):
|
||||
for directory in dirs:
|
||||
if directory.startswith('Plex-'):
|
||||
abs_path = path_ops.path.join(root, directory)
|
||||
LOG.info('Removing video node directory %s', abs_path)
|
||||
path_ops.rmtree(abs_path, ignore_errors=True)
|
||||
break
|
74
resources/lib/library_sync/sync_info.py
Normal file
74
resources/lib/library_sync/sync_info.py
Normal file
|
@ -0,0 +1,74 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
from threading import Thread, Lock
|
||||
from xbmc import sleep
|
||||
from xbmcgui import DialogProgressBG
|
||||
|
||||
from .. import utils
|
||||
|
||||
###############################################################################
|
||||
|
||||
LOG = getLogger("PLEX." + __name__)
|
||||
|
||||
GET_METADATA_COUNT = 0
|
||||
PROCESS_METADATA_COUNT = 0
|
||||
PROCESSING_VIEW_NAME = ''
|
||||
LOCK = Lock()
|
||||
|
||||
###############################################################################
|
||||
|
||||
|
||||
@utils.thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD',
|
||||
'STOP_SYNC',
|
||||
'SUSPEND_SYNC'])
|
||||
class ThreadedShowSyncInfo(Thread):
|
||||
"""
|
||||
Threaded class to show the Kodi statusbar of the metadata download.
|
||||
|
||||
Input:
|
||||
total: Total number of items to get
|
||||
item_type:
|
||||
"""
|
||||
def __init__(self, total, item_type):
|
||||
self.total = total
|
||||
self.item_type = item_type
|
||||
Thread.__init__(self)
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Do the work
|
||||
"""
|
||||
LOG.debug('Show sync info thread started')
|
||||
# cache local variables because it's faster
|
||||
total = self.total
|
||||
dialog = DialogProgressBG('dialoglogProgressBG')
|
||||
dialog.create("%s %s: %s %s"
|
||||
% (utils.lang(39714),
|
||||
self.item_type,
|
||||
unicode(total),
|
||||
utils.lang(39715)))
|
||||
|
||||
total = 2 * total
|
||||
total_progress = 0
|
||||
while not self.stopped():
|
||||
with LOCK:
|
||||
get_progress = GET_METADATA_COUNT
|
||||
process_progress = PROCESS_METADATA_COUNT
|
||||
view_name = PROCESSING_VIEW_NAME
|
||||
total_progress = get_progress + process_progress
|
||||
try:
|
||||
percentage = int(float(total_progress) / float(total) * 100.0)
|
||||
except ZeroDivisionError:
|
||||
percentage = 0
|
||||
dialog.update(percentage,
|
||||
message="%s %s. %s %s: %s"
|
||||
% (get_progress,
|
||||
utils.lang(39712),
|
||||
process_progress,
|
||||
utils.lang(39713),
|
||||
view_name))
|
||||
# Sleep for x milliseconds
|
||||
sleep(200)
|
||||
dialog.close()
|
||||
LOG.debug('Show sync info thread terminated')
|
|
@ -1,372 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from .common import update_kodi_library, PLAYLIST_SYNC_ENABLED
|
||||
from .fanart import SYNC_FANART, FanartTask
|
||||
from ..plex_api import API
|
||||
from ..plex_db import PlexDB
|
||||
from .. import kodi_db
|
||||
from .. import backgroundthread, plex_functions as PF, itemtypes
|
||||
from .. import artwork, utils, timing, variables as v, app
|
||||
|
||||
if PLAYLIST_SYNC_ENABLED:
|
||||
from .. import playlists
|
||||
|
||||
LOG = getLogger('PLEX.sync.websocket')
|
||||
|
||||
CACHING_ENALBED = utils.settings('enableTextureCache') == "true"
|
||||
|
||||
WEBSOCKET_MESSAGES = []
|
||||
# Dict to save info for Plex items currently being played somewhere
|
||||
PLAYSTATE_SESSIONS = {}
|
||||
|
||||
|
||||
def multi_delete(input_list, delete_list):
|
||||
"""
|
||||
Deletes the list items of input_list at the positions in delete_list
|
||||
(which can be in any arbitrary order)
|
||||
"""
|
||||
for index in sorted(delete_list, reverse=True):
|
||||
del input_list[index]
|
||||
return input_list
|
||||
|
||||
|
||||
def store_websocket_message(message):
|
||||
"""
|
||||
processes json.loads() messages from websocket. Triage what we need to
|
||||
do with "process_" methods
|
||||
"""
|
||||
if message['type'] == 'playing':
|
||||
process_playing(message['PlaySessionStateNotification'])
|
||||
elif message['type'] == 'timeline':
|
||||
store_timeline_message(message['TimelineEntry'])
|
||||
elif message['type'] == 'activity':
|
||||
store_activity_message(message['ActivityNotification'])
|
||||
|
||||
|
||||
def process_websocket_messages():
|
||||
"""
|
||||
Periodically called to process new/updated PMS items
|
||||
|
||||
PMS needs a while to download info from internet AFTER it
|
||||
showed up under 'timeline' websocket messages
|
||||
|
||||
data['type']:
|
||||
1: movie
|
||||
2: tv show??
|
||||
3: season??
|
||||
4: episode
|
||||
8: artist (band)
|
||||
9: album
|
||||
10: track (song)
|
||||
12: trailer, extras?
|
||||
|
||||
data['state']:
|
||||
0: 'created',
|
||||
2: 'matching',
|
||||
3: 'downloading',
|
||||
4: 'loading',
|
||||
5: 'finished',
|
||||
6: 'analyzing',
|
||||
9: 'deleted'
|
||||
"""
|
||||
global WEBSOCKET_MESSAGES
|
||||
now = timing.unix_timestamp()
|
||||
update_kodi_video_library, update_kodi_music_library = False, False
|
||||
delete_list = []
|
||||
for i, message in enumerate(WEBSOCKET_MESSAGES):
|
||||
if message['state'] == 9:
|
||||
successful, video, music = process_delete_message(message)
|
||||
elif now - message['timestamp'] < app.SYNC.backgroundsync_saftymargin:
|
||||
# We haven't waited long enough for the PMS to finish processing the
|
||||
# item. Do it later (excepting deletions)
|
||||
continue
|
||||
else:
|
||||
successful, video, music = process_new_item_message(message)
|
||||
if (successful and SYNC_FANART and
|
||||
message['plex_type'] in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)):
|
||||
task = FanartTask()
|
||||
task.setup(message['plex_id'],
|
||||
message['plex_type'],
|
||||
refresh=False)
|
||||
backgroundthread.BGThreader.addTask(task)
|
||||
if successful is True:
|
||||
delete_list.append(i)
|
||||
update_kodi_video_library = True if video else update_kodi_video_library
|
||||
update_kodi_music_library = True if music else update_kodi_music_library
|
||||
else:
|
||||
# Safety net if we can't process an item
|
||||
message['attempt'] += 1
|
||||
if message['attempt'] > 3:
|
||||
LOG.error('Repeatedly could not process message %s, abort',
|
||||
message)
|
||||
delete_list.append(i)
|
||||
|
||||
# Get rid of the items we just processed
|
||||
if delete_list:
|
||||
WEBSOCKET_MESSAGES = multi_delete(WEBSOCKET_MESSAGES, delete_list)
|
||||
# Let Kodi know of the change
|
||||
if update_kodi_video_library or update_kodi_music_library:
|
||||
update_kodi_library(video=update_kodi_video_library,
|
||||
music=update_kodi_music_library)
|
||||
|
||||
|
||||
def process_new_item_message(message):
|
||||
LOG.debug('Message: %s', message)
|
||||
xml = PF.GetPlexMetadata(message['plex_id'])
|
||||
try:
|
||||
plex_type = xml[0].attrib['type']
|
||||
except (IndexError, KeyError, TypeError):
|
||||
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'])
|
||||
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')))
|
||||
cache_artwork(message['plex_id'], plex_type)
|
||||
return True, plex_type in v.PLEX_VIDEOTYPES, plex_type in v.PLEX_AUDIOTYPES
|
||||
|
||||
|
||||
def process_delete_message(message):
|
||||
plex_type = message['plex_type']
|
||||
with itemtypes.ITEMTYPE_FROM_PLEXTYPE[plex_type](None) as typus:
|
||||
typus.remove(message['plex_id'], plex_type=plex_type)
|
||||
return True, plex_type in v.PLEX_VIDEOTYPES, plex_type in v.PLEX_AUDIOTYPES
|
||||
|
||||
|
||||
def store_timeline_message(data):
|
||||
"""
|
||||
PMS is messing with the library items, e.g. new or changed. Put in our
|
||||
"processing queue" for later
|
||||
"""
|
||||
global WEBSOCKET_MESSAGES
|
||||
for message in data:
|
||||
if 'tv.plex' in message.get('identifier', ''):
|
||||
# Ommit Plex DVR messages - the Plex IDs are not corresponding
|
||||
# (DVR ratingKeys are not unique and might correspond to a
|
||||
# movie or episode)
|
||||
continue
|
||||
try:
|
||||
typus = v.PLEX_TYPE_FROM_WEBSOCKET[int(message['type'])]
|
||||
except KeyError:
|
||||
# E.g. -1 - thanks Plex!
|
||||
LOG.info('Ignoring invalid message %s', data)
|
||||
continue
|
||||
if typus in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_SET):
|
||||
# No need to process extras or trailers
|
||||
continue
|
||||
status = int(message['state'])
|
||||
if typus == 'playlist' and PLAYLIST_SYNC_ENABLED:
|
||||
playlists.websocket(plex_id=unicode(message['itemID']),
|
||||
status=status)
|
||||
elif status == 9:
|
||||
# Immediately and always process deletions (as the PMS will
|
||||
# send additional message with other codes)
|
||||
WEBSOCKET_MESSAGES.append({
|
||||
'state': status,
|
||||
'plex_type': typus,
|
||||
'plex_id': utils.cast(int, message['itemID']),
|
||||
'timestamp': timing.unix_timestamp(),
|
||||
'attempt': 0
|
||||
})
|
||||
elif typus in (v.PLEX_TYPE_MOVIE,
|
||||
v.PLEX_TYPE_EPISODE,
|
||||
v.PLEX_TYPE_SONG) and status == 5:
|
||||
plex_id = int(message['itemID'])
|
||||
# Have we already added this element for processing?
|
||||
for existing_message in WEBSOCKET_MESSAGES:
|
||||
if existing_message['plex_id'] == plex_id:
|
||||
break
|
||||
else:
|
||||
# Haven't added this element to the queue yet
|
||||
WEBSOCKET_MESSAGES.append({
|
||||
'state': status,
|
||||
'plex_type': typus,
|
||||
'plex_id': plex_id,
|
||||
'timestamp': timing.unix_timestamp(),
|
||||
'attempt': 0
|
||||
})
|
||||
|
||||
|
||||
def store_activity_message(data):
|
||||
"""
|
||||
PMS is re-scanning an item, e.g. after having changed a movie poster.
|
||||
WATCH OUT for this if it's triggered by our PKC library scan!
|
||||
"""
|
||||
global WEBSOCKET_MESSAGES
|
||||
for message in data:
|
||||
if message['event'] != 'ended':
|
||||
# Scan still going on, so skip for now
|
||||
continue
|
||||
elif message['Activity'].get('Context') is None:
|
||||
# Not related to any Plex element, but entire library
|
||||
continue
|
||||
elif message['Activity']['type'] != 'library.refresh.items':
|
||||
# Not the type of message relevant for us
|
||||
continue
|
||||
plex_id = PF.GetPlexKeyNumber(message['Activity']['Context']['key'])[1]
|
||||
if not plex_id:
|
||||
# Likely a Plex id like /library/metadata/3/children
|
||||
continue
|
||||
# We're only looking at existing elements - have we synced yet?
|
||||
with PlexDB(lock=False) as plexdb:
|
||||
typus = plexdb.item_by_id(plex_id, plex_type=None)
|
||||
if not typus:
|
||||
LOG.debug('plex_id %s not synced yet - skipping', plex_id)
|
||||
continue
|
||||
# Have we already added this element?
|
||||
for existing_message in WEBSOCKET_MESSAGES:
|
||||
if existing_message['plex_id'] == plex_id:
|
||||
break
|
||||
else:
|
||||
# Haven't added this element to the queue yet
|
||||
WEBSOCKET_MESSAGES.append({
|
||||
'state': None, # Don't need a state here
|
||||
'plex_type': typus['plex_type'],
|
||||
'plex_id': plex_id,
|
||||
'timestamp': timing.unix_timestamp(),
|
||||
'attempt': 0
|
||||
})
|
||||
|
||||
|
||||
def process_playing(data):
|
||||
"""
|
||||
Someone (not necessarily the user signed in) is playing something some-
|
||||
where
|
||||
"""
|
||||
global PLAYSTATE_SESSIONS
|
||||
for message in data:
|
||||
status = message['state']
|
||||
if status == 'buffering' or status == 'stopped':
|
||||
# Drop buffering and stop messages immediately - no value
|
||||
continue
|
||||
plex_id = utils.cast(int, message['ratingKey'])
|
||||
skip = False
|
||||
for pid in (0, 1, 2):
|
||||
if plex_id == app.PLAYSTATE.player_states[pid]['plex_id']:
|
||||
# Kodi is playing this message - no need to set the playstate
|
||||
skip = True
|
||||
if skip:
|
||||
continue
|
||||
if 'sessionKey' not in message:
|
||||
LOG.warn('Received malformed message from the PMS: %s', message)
|
||||
continue
|
||||
session_key = message['sessionKey']
|
||||
# Do we already have a sessionKey stored?
|
||||
if session_key not in PLAYSTATE_SESSIONS:
|
||||
with PlexDB(lock=False) as plexdb:
|
||||
typus = plexdb.item_by_id(plex_id, plex_type=None)
|
||||
if not typus or 'kodi_fileid' not in typus:
|
||||
# Item not (yet) in Kodi library or not affiliated with a file
|
||||
continue
|
||||
if utils.settings('plex_serverowned') == 'false':
|
||||
# Not our PMS, we are not authorized to get the sessions
|
||||
# On the bright side, it must be us playing :-)
|
||||
PLAYSTATE_SESSIONS[session_key] = {}
|
||||
else:
|
||||
# PMS is ours - get all current sessions
|
||||
pms_sessions = PF.GetPMSStatus(app.ACCOUNT.plex_token)
|
||||
if session_key not in pms_sessions:
|
||||
LOG.info('Session key %s still unknown! Skip '
|
||||
'playstate update', session_key)
|
||||
continue
|
||||
PLAYSTATE_SESSIONS[session_key] = pms_sessions[session_key]
|
||||
LOG.debug('Updated current sessions. They are: %s',
|
||||
PLAYSTATE_SESSIONS)
|
||||
# Attach Kodi info to the session
|
||||
PLAYSTATE_SESSIONS[session_key]['kodi_fileid'] = typus['kodi_fileid']
|
||||
if typus['plex_type'] == v.PLEX_TYPE_EPISODE:
|
||||
PLAYSTATE_SESSIONS[session_key]['kodi_fileid_2'] = typus['kodi_fileid_2']
|
||||
else:
|
||||
PLAYSTATE_SESSIONS[session_key]['kodi_fileid_2'] = None
|
||||
PLAYSTATE_SESSIONS[session_key]['kodi_id'] = typus['kodi_id']
|
||||
PLAYSTATE_SESSIONS[session_key]['kodi_type'] = typus['kodi_type']
|
||||
session = PLAYSTATE_SESSIONS[session_key]
|
||||
if utils.settings('plex_serverowned') != 'false':
|
||||
# Identify the user - same one as signed on with PKC? Skip
|
||||
# update if neither session's username nor userid match
|
||||
# (Owner sometime's returns id '1', not always)
|
||||
if not app.ACCOUNT.plex_token and session['userId'] == '1':
|
||||
# PKC not signed in to plex.tv. Plus owner of PMS is
|
||||
# playing (the '1').
|
||||
# Hence must be us (since several users require plex.tv
|
||||
# token for PKC)
|
||||
pass
|
||||
elif not (session['userId'] == app.ACCOUNT.plex_user_id or
|
||||
session['username'] == app.ACCOUNT.plex_username):
|
||||
LOG.debug('Our username %s, userid %s did not match '
|
||||
'the session username %s with userid %s',
|
||||
app.ACCOUNT.plex_username,
|
||||
app.ACCOUNT.plex_user_id,
|
||||
session['username'],
|
||||
session['userId'])
|
||||
continue
|
||||
# Get an up-to-date XML from the PMS because PMS will NOT directly
|
||||
# tell us: duration of item viewCount
|
||||
if not session.get('duration'):
|
||||
xml = PF.GetPlexMetadata(plex_id)
|
||||
if xml in (None, 401):
|
||||
LOG.error('Could not get up-to-date xml for item %s',
|
||||
plex_id)
|
||||
continue
|
||||
api = API(xml[0])
|
||||
session['duration'] = api.runtime()
|
||||
session['viewCount'] = api.viewcount()
|
||||
# Sometimes, Plex tells us resume points in milliseconds and
|
||||
# not in seconds - thank you very much!
|
||||
if message['viewOffset'] > session['duration']:
|
||||
resume = message['viewOffset'] / 1000
|
||||
else:
|
||||
resume = message['viewOffset']
|
||||
if resume < v.IGNORE_SECONDS_AT_START:
|
||||
continue
|
||||
try:
|
||||
completed = float(resume) / float(session['duration'])
|
||||
except (ZeroDivisionError, TypeError):
|
||||
LOG.error('Could not mark playstate for %s and session %s',
|
||||
data, session)
|
||||
continue
|
||||
if completed >= v.MARK_PLAYED_AT:
|
||||
# Only mark completely watched ONCE
|
||||
if session.get('marked_played') is None:
|
||||
session['marked_played'] = True
|
||||
mark_played = True
|
||||
else:
|
||||
# Don't mark it as completely watched again
|
||||
continue
|
||||
else:
|
||||
mark_played = False
|
||||
LOG.debug('Update playstate for user %s for %s with plex id %s to '
|
||||
'viewCount %s, resume %s, mark_played %s for item %s',
|
||||
app.ACCOUNT.plex_username, session['kodi_type'], plex_id,
|
||||
session['viewCount'], resume, mark_played, PLAYSTATE_SESSIONS[session_key])
|
||||
func = itemtypes.ITEMTYPE_FROM_KODITYPE[session['kodi_type']]
|
||||
with func(None) as fkt:
|
||||
fkt.update_playstate(mark_played,
|
||||
session['viewCount'],
|
||||
resume,
|
||||
session['duration'],
|
||||
session['kodi_fileid'],
|
||||
session['kodi_fileid_2'],
|
||||
timing.unix_timestamp())
|
||||
|
||||
|
||||
def cache_artwork(plex_id, plex_type, kodi_id=None, kodi_type=None):
|
||||
"""
|
||||
Triggers caching of artwork (if so enabled in the PKC settings)
|
||||
"""
|
||||
if not CACHING_ENALBED:
|
||||
return
|
||||
if not kodi_id:
|
||||
with PlexDB(lock=False) as plexdb:
|
||||
item = plexdb.item_by_id(plex_id, plex_type)
|
||||
if not item:
|
||||
LOG.error('Could not retrieve Plex db info for %s', plex_id)
|
||||
return
|
||||
kodi_id, kodi_type = item['kodi_id'], item['kodi_type']
|
||||
with kodi_db.KODIDB_FROM_PLEXTYPE[plex_type]() as kodidb:
|
||||
for url in kodidb.art_urls(kodi_id, kodi_type):
|
||||
artwork.cache_url(url)
|
1715
resources/lib/librarysync.py
Normal file
1715
resources/lib/librarysync.py
Normal file
File diff suppressed because it is too large
Load diff
395
resources/lib/main.py
Normal file
395
resources/lib/main.py
Normal file
|
@ -0,0 +1,395 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
import logging
|
||||
import sys
|
||||
import threading
|
||||
import gc
|
||||
|
||||
import xbmc
|
||||
|
||||
from . import plex, util, backgroundthread
|
||||
from .plexnet import plexapp, threadutils
|
||||
from .windows import userselect
|
||||
|
||||
from . import utils
|
||||
from . import userclient
|
||||
from . import initialsetup
|
||||
from . import kodimonitor
|
||||
from . import librarysync
|
||||
from . import websocket_client
|
||||
from . import plex_companion
|
||||
from . import plex_functions as PF
|
||||
from . import command_pipeline
|
||||
from . import playback_starter
|
||||
from . import playqueue
|
||||
from . import artwork
|
||||
from . import variables as v
|
||||
from . import state
|
||||
from . import loghandler
|
||||
|
||||
###############################################################################
|
||||
loghandler.config()
|
||||
LOG = logging.getLogger("PLEX.main")
|
||||
###############################################################################
|
||||
|
||||
|
||||
class Service():
|
||||
|
||||
server_online = True
|
||||
warn_auth = True
|
||||
|
||||
user = None
|
||||
ws = None
|
||||
library = None
|
||||
plexcompanion = None
|
||||
|
||||
user_running = False
|
||||
ws_running = False
|
||||
alexa_running = False
|
||||
library_running = False
|
||||
plexcompanion_running = False
|
||||
kodimonitor_running = False
|
||||
playback_starter_running = False
|
||||
image_cache_thread_running = False
|
||||
|
||||
def __init__(self):
|
||||
# Initial logging
|
||||
LOG.info("======== START %s ========", v.ADDON_NAME)
|
||||
LOG.info("Platform: %s", v.PLATFORM)
|
||||
LOG.info("KODI Version: %s", v.KODILONGVERSION)
|
||||
LOG.info("%s Version: %s", v.ADDON_NAME, v.ADDON_VERSION)
|
||||
LOG.info("PKC Direct Paths: %s",
|
||||
utils.settings('useDirectPaths') == '1')
|
||||
LOG.info("Number of sync threads: %s",
|
||||
utils.settings('syncThreadNumber'))
|
||||
LOG.info('Playlist m3u encoding: %s', v.M3U_ENCODING)
|
||||
LOG.info("Full sys.argv received: %s", sys.argv)
|
||||
LOG.info('Sync playlists: %s', utils.settings('enablePlaylistSync'))
|
||||
LOG.info('Synching only specific Kodi playlists: %s',
|
||||
utils.settings('syncSpecificKodiPlaylists') == 'true')
|
||||
LOG.info('Kodi playlist prefix: %s',
|
||||
utils.settings('syncSpecificKodiPlaylistsPrefix'))
|
||||
LOG.info('Synching only specific Plex playlists: %s',
|
||||
utils.settings('syncSpecificPlexPlaylistsPrefix') == 'true')
|
||||
LOG.info('Play playlist prefix: %s',
|
||||
utils.settings('syncSpecificPlexPlaylistsPrefix'))
|
||||
self.monitor = xbmc.Monitor()
|
||||
# Load/Reset PKC entirely - important for user/Kodi profile switch
|
||||
initialsetup.reload_pkc()
|
||||
|
||||
def _stop_pkc(self):
|
||||
"""
|
||||
Kodi's abortRequested is really unreliable :-(
|
||||
"""
|
||||
return self.monitor.abortRequested() or state.STOP_PKC
|
||||
|
||||
def ServiceEntryPoint(self):
|
||||
# Important: Threads depending on abortRequest will not trigger
|
||||
# if profile switch happens more than once.
|
||||
_stop_pkc = self._stop_pkc
|
||||
monitor = self.monitor
|
||||
|
||||
# Server auto-detect
|
||||
initialsetup.InitialSetup().setup()
|
||||
|
||||
# Detect playback start early on
|
||||
self.command_pipeline = command_pipeline.Monitor_Window()
|
||||
self.command_pipeline.start()
|
||||
|
||||
# Initialize important threads, handing over self for callback purposes
|
||||
self.user = userclient.UserClient()
|
||||
self.ws = websocket_client.PMS_Websocket()
|
||||
self.alexa = websocket_client.Alexa_Websocket()
|
||||
self.library = librarysync.LibrarySync()
|
||||
self.plexcompanion = plex_companion.PlexCompanion()
|
||||
self.specialmonitor = kodimonitor.SpecialMonitor()
|
||||
self.playback_starter = playback_starter.PlaybackStarter()
|
||||
self.playqueue = playqueue.PlayqueueMonitor()
|
||||
if utils.settings('enableTextureCache') == "true":
|
||||
self.image_cache_thread = artwork.Image_Cache_Thread()
|
||||
|
||||
welcome_msg = True
|
||||
counter = 0
|
||||
while not _stop_pkc():
|
||||
|
||||
if utils.window('plex_kodiProfile') != v.KODI_PROFILE:
|
||||
# Profile change happened, terminate this thread and others
|
||||
LOG.info("Kodi profile was: %s and changed to: %s. "
|
||||
"Terminating old PlexKodiConnect thread.",
|
||||
v.KODI_PROFILE, utils.window('plex_kodiProfile'))
|
||||
break
|
||||
|
||||
# Before proceeding, need to make sure:
|
||||
# 1. Server is online
|
||||
# 2. User is set
|
||||
# 3. User has access to the server
|
||||
|
||||
if utils.window('plex_online') == "true":
|
||||
# Plex server is online
|
||||
# Verify if user is set and has access to the server
|
||||
if (self.user.user is not None) and self.user.has_access:
|
||||
if not self.kodimonitor_running:
|
||||
# Start up events
|
||||
self.warn_auth = True
|
||||
if welcome_msg is True:
|
||||
# Reset authentication warnings
|
||||
welcome_msg = False
|
||||
utils.dialog('notification',
|
||||
utils.lang(29999),
|
||||
"%s %s" % (utils.lang(33000),
|
||||
self.user.user),
|
||||
icon='{plex}',
|
||||
time=2000,
|
||||
sound=False)
|
||||
# Start monitoring kodi events
|
||||
self.kodimonitor_running = kodimonitor.KodiMonitor()
|
||||
self.specialmonitor.start()
|
||||
# Start the Websocket Client
|
||||
if not self.ws_running:
|
||||
self.ws_running = True
|
||||
self.ws.start()
|
||||
# Start the Alexa thread
|
||||
if (not self.alexa_running and
|
||||
utils.settings('enable_alexa') == 'true'):
|
||||
self.alexa_running = True
|
||||
self.alexa.start()
|
||||
# Start the syncing thread
|
||||
if not self.library_running:
|
||||
self.library_running = True
|
||||
self.library.start()
|
||||
# Start the Plex Companion thread
|
||||
if not self.plexcompanion_running:
|
||||
self.plexcompanion_running = True
|
||||
self.plexcompanion.start()
|
||||
if not self.playback_starter_running:
|
||||
self.playback_starter_running = True
|
||||
self.playback_starter.start()
|
||||
self.playqueue.start()
|
||||
if (not self.image_cache_thread_running and
|
||||
utils.settings('enableTextureCache') == "true"):
|
||||
self.image_cache_thread_running = True
|
||||
self.image_cache_thread.start()
|
||||
else:
|
||||
if (self.user.user is None) and self.warn_auth:
|
||||
# Alert user is not authenticated and suppress future
|
||||
# warning
|
||||
self.warn_auth = False
|
||||
LOG.warn("Not authenticated yet.")
|
||||
|
||||
# User access is restricted.
|
||||
# Keep verifying until access is granted
|
||||
# unless server goes offline or Kodi is shut down.
|
||||
while self.user.has_access is False:
|
||||
# Verify access with an API call
|
||||
self.user.check_access()
|
||||
|
||||
if utils.window('plex_online') != "true":
|
||||
# Server went offline
|
||||
break
|
||||
|
||||
if monitor.waitForAbort(3):
|
||||
# Abort was requested while waiting. We should exit
|
||||
break
|
||||
else:
|
||||
# Wait until Plex server is online
|
||||
# or Kodi is shut down.
|
||||
while not self._stop_pkc():
|
||||
server = self.user.get_server()
|
||||
if server is False:
|
||||
# No server info set in add-on settings
|
||||
pass
|
||||
elif PF.check_connection(server, verifySSL=True) is False:
|
||||
# Server is offline or cannot be reached
|
||||
# Alert the user and suppress future warning
|
||||
if self.server_online:
|
||||
self.server_online = False
|
||||
utils.window('plex_online', value="false")
|
||||
# Suspend threads
|
||||
state.SUSPEND_LIBRARY_THREAD = True
|
||||
LOG.error("Plex Media Server went offline")
|
||||
if utils.settings('show_pms_offline') == 'true':
|
||||
utils.dialog('notification',
|
||||
utils.lang(33001),
|
||||
"%s %s" % (utils.lang(29999),
|
||||
utils.lang(33002)),
|
||||
icon='{plex}',
|
||||
sound=False)
|
||||
counter += 1
|
||||
# Periodically check if the IP changed, e.g. per minute
|
||||
if counter > 20:
|
||||
counter = 0
|
||||
setup = initialsetup.InitialSetup()
|
||||
tmp = setup.pick_pms()
|
||||
if tmp is not None:
|
||||
setup.write_pms_to_settings(tmp)
|
||||
else:
|
||||
# Server is online
|
||||
counter = 0
|
||||
if not self.server_online:
|
||||
# Server was offline when Kodi started.
|
||||
# Wait for server to be fully established.
|
||||
if monitor.waitForAbort(5):
|
||||
# Abort was requested while waiting.
|
||||
break
|
||||
self.server_online = True
|
||||
# Alert the user that server is online.
|
||||
if (welcome_msg is False and
|
||||
utils.settings('show_pms_offline') == 'true'):
|
||||
utils.dialog('notification',
|
||||
utils.lang(29999),
|
||||
utils.lang(33003),
|
||||
icon='{plex}',
|
||||
time=5000,
|
||||
sound=False)
|
||||
LOG.info("Server %s is online and ready.", server)
|
||||
utils.window('plex_online', value="true")
|
||||
if state.AUTHENTICATED:
|
||||
# Server got offline when we were authenticated.
|
||||
# Hence resume threads
|
||||
state.SUSPEND_LIBRARY_THREAD = False
|
||||
|
||||
# Start the userclient thread
|
||||
if not self.user_running:
|
||||
self.user_running = True
|
||||
self.user.start()
|
||||
|
||||
break
|
||||
|
||||
if monitor.waitForAbort(3):
|
||||
# Abort was requested while waiting.
|
||||
break
|
||||
|
||||
if monitor.waitForAbort(0.05):
|
||||
# Abort was requested while waiting. We should exit
|
||||
break
|
||||
# Terminating PlexKodiConnect
|
||||
|
||||
# Tell all threads to terminate (e.g. several lib sync threads)
|
||||
state.STOP_PKC = True
|
||||
utils.window('plex_service_started', clear=True)
|
||||
LOG.info("======== STOP %s ========", v.ADDON_NAME)
|
||||
|
||||
|
||||
def waitForThreads():
|
||||
LOG.debug('Checking for any remaining threads')
|
||||
while len(threading.enumerate()) > 1:
|
||||
for t in threading.enumerate():
|
||||
if t != threading.currentThread():
|
||||
if t.isAlive():
|
||||
LOG.debug('Waiting on thread: %s', t.name)
|
||||
if isinstance(t, threading._Timer):
|
||||
t.cancel()
|
||||
t.join()
|
||||
elif isinstance(t, threadutils.KillableThread):
|
||||
t.kill(force_and_wait=True)
|
||||
else:
|
||||
t.join()
|
||||
LOG.debug('All threads done')
|
||||
|
||||
|
||||
def signout():
|
||||
util.setSetting('auth.token', '')
|
||||
LOG.info('Signing out...')
|
||||
plexapp.ACCOUNT.signOut()
|
||||
|
||||
|
||||
def main():
|
||||
LOG.info('Starting PKC %s', util.ADDON.getAddonInfo('version'))
|
||||
LOG.info('User-agent: %s', plex.defaultUserAgent())
|
||||
|
||||
server_select_shown = False
|
||||
|
||||
try:
|
||||
plex.init()
|
||||
# Check plex.tv sign-in status
|
||||
if not plexapp.ACCOUNT.authToken:
|
||||
LOG.info('Not signed in to plex.tv yet')
|
||||
if utils.settings('myplexlogin') == 'true':
|
||||
plex.authorize_user()
|
||||
# Main Loop
|
||||
while not xbmc.abortRequested:
|
||||
# Check whether a PMS server has been set
|
||||
selectedServer = plexapp.SERVERMANAGER.selectedServer
|
||||
if not selectedServer:
|
||||
if not server_select_shown:
|
||||
server_select_shown = True
|
||||
LOG.debug('No PMS set yet, presenting dialog')
|
||||
plex.select_server()
|
||||
else:
|
||||
util.MONITOR.waitForAbort(1)
|
||||
continue
|
||||
# Ping the PMS until we're sure its online
|
||||
if not selectedServer.isReachable():
|
||||
selectedServer.updateReachability(force=True,
|
||||
allowFallback=True)
|
||||
continue
|
||||
while not xbmc.abortRequested:
|
||||
|
||||
if (
|
||||
not plexapp.ACCOUNT.isOffline and not
|
||||
plexapp.ACCOUNT.isAuthenticated and
|
||||
(len(plexapp.ACCOUNT.homeUsers) > 1 or plexapp.ACCOUNT.isProtected)
|
||||
|
||||
):
|
||||
result = userselect.start()
|
||||
if not result:
|
||||
return
|
||||
elif result == 'signout':
|
||||
signout()
|
||||
break
|
||||
elif result == 'signin':
|
||||
break
|
||||
LOG.info('User selected')
|
||||
|
||||
try:
|
||||
selectedServer = plexapp.SERVERMANAGER.selectedServer
|
||||
|
||||
if not selectedServer:
|
||||
LOG.debug('Waiting for selected server...')
|
||||
for timeout, skip_preferred, skip_owned in ((10, True, False), (10, True, True)):
|
||||
plex.CallbackEvent(plexapp.APP, 'change:selectedServer', timeout=timeout).wait()
|
||||
|
||||
selectedServer = plexapp.SERVERMANAGER.checkSelectedServerSearch(skip_preferred=skip_preferred, skip_owned=skip_owned)
|
||||
if selectedServer:
|
||||
break
|
||||
else:
|
||||
LOG.debug('Finished waiting for selected server...')
|
||||
|
||||
LOG.info('Starting with server: %s', selectedServer)
|
||||
|
||||
windowutils.HOME = home.HomeWindow.open()
|
||||
util.CRON.cancelReceiver(windowutils.HOME)
|
||||
|
||||
if not windowutils.HOME.closeOption:
|
||||
return
|
||||
|
||||
closeOption = windowutils.HOME.closeOption
|
||||
|
||||
windowutils.shutdownHome()
|
||||
|
||||
if closeOption == 'signout':
|
||||
signout()
|
||||
break
|
||||
elif closeOption == 'switch':
|
||||
plexapp.ACCOUNT.isAuthenticated = False
|
||||
finally:
|
||||
windowutils.shutdownHome()
|
||||
gc.collect(2)
|
||||
except:
|
||||
util.ERROR()
|
||||
finally:
|
||||
LOG.info('SHUTTING DOWN...')
|
||||
# player.shutdown()
|
||||
plexapp.APP.preShutdown()
|
||||
if util.CRON:
|
||||
util.CRON.stop()
|
||||
backgroundthread.BGThreader.shutdown()
|
||||
plexapp.APP.shutdown()
|
||||
waitForThreads()
|
||||
LOG.info('SHUTDOWN FINISHED')
|
||||
|
||||
from .windows import kodigui
|
||||
kodigui.MONITOR = None
|
||||
util.shutdown()
|
||||
gc.collect(2)
|
|
@ -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'):
|
||||
|
@ -35,68 +31,4 @@ def check_migration():
|
|||
utils.settings('ipaddress', value='')
|
||||
utils.settings('port', value='')
|
||||
|
||||
if not utils.compare_version(last_migration, '2.7.6'):
|
||||
LOG.info('Migrating to version 2.7.5')
|
||||
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,18 +2,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import re
|
||||
from xml.etree.ElementTree import ParseError
|
||||
|
||||
from .plex_api.media import Media
|
||||
from . import utils
|
||||
from .plex_api import API
|
||||
from . import variables as v
|
||||
|
||||
###############################################################################
|
||||
LOG = getLogger('PLEX.music.py')
|
||||
LOG = getLogger('PLEX.music')
|
||||
###############################################################################
|
||||
|
||||
|
||||
def excludefromscan_music_folders(sections):
|
||||
def excludefromscan_music_folders(xml):
|
||||
"""
|
||||
Gets a complete list of paths for music libraries from the PMS. Sets them
|
||||
to be excluded in the advancedsettings.xml from being scanned by Kodi.
|
||||
|
@ -24,18 +24,17 @@ def excludefromscan_music_folders(sections):
|
|||
"""
|
||||
paths = []
|
||||
reboot = False
|
||||
api = Media()
|
||||
for section in sections:
|
||||
if section.section_type != v.PLEX_TYPE_ARTIST:
|
||||
api = API(item=None)
|
||||
for library in xml:
|
||||
if library.attrib['type'] != v.PLEX_TYPE_ARTIST:
|
||||
# Only look at music libraries
|
||||
continue
|
||||
if not section.sync_to_kodi:
|
||||
continue
|
||||
for location in section.xml.findall('Location'):
|
||||
path = api.validate_playurl(location.attrib['path'],
|
||||
typus=v.PLEX_TYPE_ARTIST,
|
||||
omit_check=True)
|
||||
paths.append(_turn_to_regex(path))
|
||||
for location in library:
|
||||
if location.tag == 'Location':
|
||||
path = api.validate_playurl(location.attrib['path'],
|
||||
typus=v.PLEX_TYPE_ARTIST,
|
||||
omit_check=True)
|
||||
paths.append(__turn_to_regex(path))
|
||||
try:
|
||||
with utils.XmlKodiSetting(
|
||||
'advancedsettings.xml',
|
||||
|
@ -67,7 +66,7 @@ def excludefromscan_music_folders(sections):
|
|||
element.text)
|
||||
parent.remove(element)
|
||||
xml_file.write_xml = True
|
||||
except (utils.ParseError, IOError):
|
||||
except (ParseError, IOError):
|
||||
LOG.error('Could not adjust advancedsettings.xml')
|
||||
if reboot is True:
|
||||
# 'New Plex music library detected. Sorry, but we need to
|
||||
|
@ -75,7 +74,7 @@ def excludefromscan_music_folders(sections):
|
|||
utils.reboot_kodi(utils.lang(39711))
|
||||
|
||||
|
||||
def _turn_to_regex(path):
|
||||
def __turn_to_regex(path):
|
||||
"""
|
||||
Turns a path into regex expression to be fed to Kodi's advancedsettings.xml
|
||||
"""
|
||||
|
@ -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
|
||||
|
|
|
@ -9,18 +9,12 @@ Unfortunatly, working with filenames and paths seems to require an encoding in
|
|||
the OS' getfilesystemencoding - it will NOT always work with unicode paths.
|
||||
However, sys.getfilesystemencoding might return None.
|
||||
Feed unicode to all the functions below and you're fine.
|
||||
|
||||
WARNING: os.path won't really work with smb paths (possibly others). For
|
||||
xbmcvfs functions to work with smb paths, they need to be both in passwords.xml
|
||||
as well as sources.xml
|
||||
"""
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
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 +22,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):
|
||||
|
@ -62,11 +55,8 @@ def translate_path(path):
|
|||
|
||||
|
||||
def exists(path):
|
||||
"""
|
||||
Returns True if the path [unicode] exists. Folders NEED a trailing slash or
|
||||
backslash!!
|
||||
"""
|
||||
return xbmcvfs.exists(path.encode(KODI_ENCODING, 'strict')) == 1
|
||||
"""Returns True if the path [unicode] exists"""
|
||||
return xbmcvfs.exists(path.encode(KODI_ENCODING, 'strict'))
|
||||
|
||||
|
||||
def rmtree(path, *args, **kwargs):
|
||||
|
@ -204,40 +194,3 @@ def copy_tree(src, dst, *args, **kwargs):
|
|||
src = encode_path(src)
|
||||
dst = encode_path(dst)
|
||||
return dir_util.copy_tree(src, dst, *args, **kwargs)
|
||||
|
||||
|
||||
def basename(path):
|
||||
"""
|
||||
Returns the filename for path [unicode] or an empty string if not possible.
|
||||
Safer than using os.path.basename, as we could be expecting \\ for / or
|
||||
vice versa
|
||||
"""
|
||||
try:
|
||||
return path.rsplit('/', 1)[1]
|
||||
except IndexError:
|
||||
try:
|
||||
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
|
||||
|
|
76
resources/lib/pickler.py
Normal file
76
resources/lib/pickler.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from cPickle import dumps, loads
|
||||
from xbmcgui import Window
|
||||
from xbmc import log, LOGDEBUG
|
||||
|
||||
###############################################################################
|
||||
WINDOW = Window(10000)
|
||||
PREFIX = 'PLEX.pickler: '
|
||||
###############################################################################
|
||||
|
||||
|
||||
def try_encode(input_str, encoding='utf-8'):
|
||||
"""
|
||||
Will try to encode input_str (in unicode) to encoding. This possibly
|
||||
fails with e.g. Android TV's Python, which does not accept arguments for
|
||||
string.encode()
|
||||
|
||||
COPY to avoid importing utils on calling default.py
|
||||
"""
|
||||
if isinstance(input_str, str):
|
||||
# already encoded
|
||||
return input_str
|
||||
try:
|
||||
input_str = input_str.encode(encoding, "ignore")
|
||||
except TypeError:
|
||||
input_str = input_str.encode()
|
||||
return input_str
|
||||
|
||||
|
||||
def pickl_window(property, value=None, clear=False):
|
||||
"""
|
||||
Get or set window property - thread safe! For use with Pickle
|
||||
Property and value must be string
|
||||
"""
|
||||
if clear:
|
||||
WINDOW.clearProperty(property)
|
||||
elif value is not None:
|
||||
WINDOW.setProperty(property, value)
|
||||
else:
|
||||
return try_encode(WINDOW.getProperty(property))
|
||||
|
||||
|
||||
def pickle_me(obj, window_var='plex_result'):
|
||||
"""
|
||||
Pickles the obj to the window variable. Use to transfer Python
|
||||
objects between different PKC python instances (e.g. if default.py is
|
||||
called and you'd want to use the service.py instance)
|
||||
|
||||
obj can be pretty much any Python object. However, classes and
|
||||
functions won't work. See the Pickle documentation
|
||||
"""
|
||||
log('%sStart pickling' % PREFIX, level=LOGDEBUG)
|
||||
pickl_window(window_var, value=dumps(obj))
|
||||
log('%sSuccessfully pickled' % PREFIX, level=LOGDEBUG)
|
||||
|
||||
|
||||
def unpickle_me(window_var='plex_result'):
|
||||
"""
|
||||
Unpickles a Python object from the window variable window_var.
|
||||
Will then clear the window variable!
|
||||
"""
|
||||
result = pickl_window(window_var)
|
||||
pickl_window(window_var, clear=True)
|
||||
log('%sStart unpickling' % PREFIX, level=LOGDEBUG)
|
||||
obj = loads(result)
|
||||
log('%sSuccessfully unpickled' % PREFIX, level=LOGDEBUG)
|
||||
return obj
|
||||
|
||||
|
||||
class Playback_Successful(object):
|
||||
"""
|
||||
Used to communicate with another PKC Python instance
|
||||
"""
|
||||
listitem = None
|
|
@ -1,144 +1,7 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Used to shovel data from separate Kodi Python instances to the main thread
|
||||
and vice versa.
|
||||
"""
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import json
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
|
||||
LOG = getLogger('PLEX.transfer')
|
||||
WINDOW = xbmcgui.Window(10000)
|
||||
WINDOW_UPSTREAM = 'plexkodiconnect.result.upstream'.encode('utf-8')
|
||||
WINDOW_DOWNSTREAM = 'plexkodiconnect.result.downstream'.encode('utf-8')
|
||||
WINDOW_COMMAND = 'plexkodiconnect.command'.encode('utf-8')
|
||||
KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
|
||||
|
||||
|
||||
def cast(func, value):
|
||||
"""
|
||||
Cast the specified value to the specified type (returned by func). Currently
|
||||
this only support int, float, bool. Should be extended if needed.
|
||||
Parameters:
|
||||
func (func): Calback function to used cast to type (int, bool, float).
|
||||
value (any): value to be cast and returned.
|
||||
|
||||
Returns None if something goes wrong
|
||||
"""
|
||||
if value is None:
|
||||
return value
|
||||
elif func == bool:
|
||||
return bool(int(value))
|
||||
elif func == unicode:
|
||||
if isinstance(value, (int, long, float)):
|
||||
return unicode(value)
|
||||
elif isinstance(value, unicode):
|
||||
return value
|
||||
else:
|
||||
return value.decode('utf-8')
|
||||
elif func == str:
|
||||
if isinstance(value, (int, long, float)):
|
||||
return str(value)
|
||||
elif isinstance(value, str):
|
||||
return value
|
||||
else:
|
||||
return value.encode('utf-8')
|
||||
elif func == int:
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
try:
|
||||
# Converting e.g. '8.0' fails; need to convert to float first
|
||||
return int(float(value))
|
||||
except ValueError:
|
||||
return
|
||||
elif func == float:
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
return
|
||||
return func(value)
|
||||
|
||||
|
||||
def kodi_window(property, value=None, clear=False):
|
||||
"""
|
||||
Get or set window property - thread safe! value must be string
|
||||
"""
|
||||
if clear:
|
||||
WINDOW.clearProperty(property)
|
||||
elif value is not None:
|
||||
WINDOW.setProperty(property, value)
|
||||
else:
|
||||
return WINDOW.getProperty(property)
|
||||
|
||||
|
||||
def plex_command(value):
|
||||
"""
|
||||
Used to funnel states between different Python instances. NOT really thread
|
||||
safe - let's hope the Kodi user can't click fast enough
|
||||
"""
|
||||
while kodi_window(WINDOW_COMMAND):
|
||||
xbmc.sleep(50)
|
||||
kodi_window(WINDOW_COMMAND, value=value)
|
||||
|
||||
|
||||
def serialize(obj):
|
||||
if isinstance(obj, PKCListItem):
|
||||
return {'type': 'PKCListItem', 'data': obj.data}
|
||||
else:
|
||||
return {'type': 'other', 'data': obj}
|
||||
return
|
||||
|
||||
|
||||
def de_serialize(answ):
|
||||
if answ['type'] == 'PKCListItem':
|
||||
result = PKCListItem()
|
||||
result.data = answ['data']
|
||||
return convert_pkc_to_listitem(result)
|
||||
elif answ['type'] == 'other':
|
||||
return answ['data']
|
||||
else:
|
||||
raise NotImplementedError('Not implemented: %s' % answ)
|
||||
|
||||
|
||||
def send(pkc_listitem, target='default'):
|
||||
"""
|
||||
Pickles the obj to the window variable. Use to transfer Python
|
||||
objects between different PKC python instances (e.g. if default.py is
|
||||
called and you'd want to use the service.py instance)
|
||||
|
||||
obj can be pretty much any Python object. However, classes and
|
||||
functions won't work. See the Pickle documentation
|
||||
|
||||
Set target='default' if you send data TO another Python default.py
|
||||
instance, 'main' if your default.py needs to send to the main thread
|
||||
"""
|
||||
window = WINDOW_DOWNSTREAM if target == 'default' else WINDOW_UPSTREAM
|
||||
LOG.debug('Sending: %s', pkc_listitem)
|
||||
kodi_window(window,
|
||||
value=json.dumps(serialize(pkc_listitem)))
|
||||
|
||||
|
||||
def wait_for_transfer(source='main'):
|
||||
"""
|
||||
Set source='default' if you wait for data FROM another Python default.py
|
||||
instance, 'main' if your default.py needs to wait for the main thread
|
||||
"""
|
||||
LOG.debug('Waiting for transfer from %s', source)
|
||||
window = WINDOW_DOWNSTREAM if source == 'main' else WINDOW_UPSTREAM
|
||||
result = ''
|
||||
while not result:
|
||||
result = kodi_window(window)
|
||||
if result:
|
||||
kodi_window(window, clear=True)
|
||||
LOG.debug('Received')
|
||||
result = json.loads(result)
|
||||
return de_serialize(result)
|
||||
xbmc.sleep(50)
|
||||
from xbmcgui import ListItem
|
||||
|
||||
|
||||
def convert_pkc_to_listitem(pkc_listitem):
|
||||
|
@ -146,15 +9,9 @@ def convert_pkc_to_listitem(pkc_listitem):
|
|||
Insert a PKCListItem() and you will receive a valid XBMC listitem
|
||||
"""
|
||||
data = pkc_listitem.data
|
||||
if KODIVERSION >= 18:
|
||||
listitem = xbmcgui.ListItem(label=data.get('label'),
|
||||
label2=data.get('label2'),
|
||||
path=data.get('path'),
|
||||
offscreen=True)
|
||||
else:
|
||||
listitem = xbmcgui.ListItem(label=data.get('label'),
|
||||
label2=data.get('label2'),
|
||||
path=data.get('path'))
|
||||
listitem = ListItem(label=data.get('label'),
|
||||
label2=data.get('label2'),
|
||||
path=data.get('path'))
|
||||
if data['info']:
|
||||
listitem.setInfo(**data['info'])
|
||||
for stream in data['stream_info']:
|
||||
|
@ -164,11 +21,9 @@ def convert_pkc_to_listitem(pkc_listitem):
|
|||
if data['art']:
|
||||
listitem.setArt(data['art'])
|
||||
for key, value in data['property'].iteritems():
|
||||
listitem.setProperty(key, cast(str, value))
|
||||
listitem.setProperty(key, value)
|
||||
if data['subtitles']:
|
||||
listitem.setSubtitles(data['subtitles'])
|
||||
if data['contextmenu']:
|
||||
listitem.addContextMenuItems(data['contextmenu'])
|
||||
return listitem
|
||||
|
||||
|
||||
|
@ -179,7 +34,7 @@ class PKCListItem(object):
|
|||
|
||||
WARNING: set/get path only via setPath and getPath! (not getProperty)
|
||||
"""
|
||||
def __init__(self, label=None, label2=None, path=None, offscreen=True):
|
||||
def __init__(self, label=None, label2=None, path=None):
|
||||
self.data = {
|
||||
'stream_info': [], # (type, values: dict { label: value })
|
||||
'art': {}, # dict
|
||||
|
@ -189,10 +44,9 @@ class PKCListItem(object):
|
|||
'path': path, # string
|
||||
'property': {}, # (key, value)
|
||||
'subtitles': [], # strings
|
||||
'contextmenu': None
|
||||
}
|
||||
|
||||
def addContextMenuItems(self, items):
|
||||
def addContextMenuItems(self, items, replaceItems):
|
||||
"""
|
||||
Adds item(s) to the context menu for media lists.
|
||||
|
||||
|
@ -210,7 +64,7 @@ class PKCListItem(object):
|
|||
|
||||
Once you use a keyword, all following arguments require the keyword.
|
||||
"""
|
||||
self.data['contextmenu'] = items
|
||||
raise NotImplementedError
|
||||
|
||||
def addStreamInfo(self, type, values):
|
||||
"""
|
|
@ -6,29 +6,31 @@ 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 xbmc import Player, sleep
|
||||
|
||||
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
|
||||
from . import plex_functions as PF
|
||||
from . import utils
|
||||
from .downloadutils import DownloadUtils as DU
|
||||
from . import plexdb_functions as plexdb
|
||||
from . import kodidb_functions as kodidb
|
||||
from . import playlist_func as PL
|
||||
from . import playqueue as PQ
|
||||
from . import json_rpc as js
|
||||
from . import pickler
|
||||
from .playutils import PlayUtils
|
||||
from .pkc_listitem import PKCListItem
|
||||
from . import variables as v
|
||||
from . import state
|
||||
|
||||
###############################################################################
|
||||
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):
|
||||
def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True):
|
||||
"""
|
||||
Hit this function for addon path playback, Plex trailers, etc.
|
||||
Will setup playback first, then on second call complete playback.
|
||||
|
@ -44,37 +46,18 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True,
|
|||
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)
|
||||
LOG.info('playback_triage called with plex_id %s, plex_type %s, path %s, '
|
||||
'resolve %s', plex_id, plex_type, path, resolve)
|
||||
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))
|
||||
RESOLVE = resolve if not state.CONTEXT_MENU_PLAY else False
|
||||
if not state.AUTHENTICATED:
|
||||
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:
|
||||
with state.LOCK_PLAYQUEUES:
|
||||
playqueue = PQ.get_playqueue_from_type(
|
||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type])
|
||||
try:
|
||||
|
@ -82,12 +65,12 @@ def _playback_triage(plex_id, plex_type, path, resolve, resume):
|
|||
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')
|
||||
LOG.info('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')
|
||||
LOG.info('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)
|
||||
|
@ -105,12 +88,12 @@ def _playback_triage(plex_id, plex_type, path, resolve, resume):
|
|||
try:
|
||||
item = items[pos]
|
||||
except IndexError:
|
||||
LOG.debug('Could not apply playlist hack! Probably Widget playback')
|
||||
LOG.info('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)
|
||||
LOG.info('Kodi playlist play detected')
|
||||
_playlist_playback(plex_id, plex_type)
|
||||
return
|
||||
|
||||
# Can return -1 (as in "no playlist")
|
||||
|
@ -124,20 +107,19 @@ def _playback_triage(plex_id, plex_type, path, resolve, resume):
|
|||
initiate = True
|
||||
else:
|
||||
if item.plex_id != plex_id:
|
||||
LOG.debug('Received new plex_id%s, expected %s',
|
||||
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)
|
||||
_playback_init(plex_id, plex_type, playqueue, pos)
|
||||
else:
|
||||
# kick off playback on second pass, resume was already set on first
|
||||
# pass (threaded_playback will seek to resume)
|
||||
# kick off playback on second pass
|
||||
_conclude_playback(playqueue, pos)
|
||||
|
||||
|
||||
def _playlist_playback(plex_id):
|
||||
def _playlist_playback(plex_id, plex_type):
|
||||
"""
|
||||
Really annoying Kodi behavior: Kodi will throw the ENTIRE playlist some-
|
||||
where, causing Playlist.onAdd to fire for each item like this:
|
||||
|
@ -154,8 +136,16 @@ def _playlist_playback(plex_id):
|
|||
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):
|
||||
xml = PF.GetPlexMetadata(plex_id)
|
||||
try:
|
||||
xml[0].attrib
|
||||
except (IndexError, TypeError, AttributeError):
|
||||
LOG.error('Could not get a PMS xml for plex id %s', plex_id)
|
||||
# "Play error"
|
||||
utils.dialog('notification',
|
||||
utils.lang(29999),
|
||||
utils.lang(30128),
|
||||
icon='{error}')
|
||||
_ensure_resolve(abort=True)
|
||||
return
|
||||
# Kodi bug: playqueue will ALWAYS be audio playqueue UNTIL playback
|
||||
|
@ -170,30 +160,28 @@ def _playlist_playback(plex_id):
|
|||
_conclude_playback(playqueue, pos=0)
|
||||
|
||||
|
||||
def _playback_init(plex_id, plex_type, playqueue, pos, resume):
|
||||
def _playback_init(plex_id, plex_type, playqueue, pos):
|
||||
"""
|
||||
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.info('Initializing PKC playback')
|
||||
xml = PF.GetPlexMetadata(plex_id)
|
||||
try:
|
||||
xml[0].attrib
|
||||
except (IndexError, TypeError, AttributeError):
|
||||
LOG.error('Could not get a PMS xml for plex id %s', plex_id)
|
||||
# "Play error"
|
||||
utils.dialog('notification',
|
||||
utils.lang(29999),
|
||||
utils.lang(30128),
|
||||
icon='{error}')
|
||||
_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:
|
||||
if playqueue.kodi_pl.size() > 1:
|
||||
# Special case - we already got a filled Kodi playqueue
|
||||
try:
|
||||
_init_existing_kodi_playlist(playqueue, pos)
|
||||
except exceptions.PlaylistError:
|
||||
except PL.PlaylistError:
|
||||
LOG.error('Playback_init for existing Kodi playlist failed')
|
||||
_ensure_resolve(abort=True)
|
||||
return
|
||||
|
@ -203,59 +191,62 @@ def _playback_init(plex_id, plex_type, playqueue, pos, resume):
|
|||
return
|
||||
# "Usual" case - consider trailers and parts and build both Kodi and Plex
|
||||
# playqueues
|
||||
# Release default.py
|
||||
# Pass dummy PKC video with 0 length so Kodi immediately stops playback
|
||||
# and we can build our own playqueue.
|
||||
_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
|
||||
if (plex_type == v.PLEX_TYPE_MOVIE and not api.resume_point() 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)
|
||||
LOG.debug('Playing trailers: %s', trailers)
|
||||
if RESOLVE:
|
||||
# Sleep a bit to let setResolvedUrl do its thing - bit ugly
|
||||
sleep_timer = 0
|
||||
while not state.PKC_CAUSED_STOP_DONE:
|
||||
sleep(50)
|
||||
sleep_timer += 1
|
||||
if sleep_timer > 100:
|
||||
break
|
||||
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'),
|
||||
xml.attrib.get('librarySectionUUID'),
|
||||
mediatype=plex_type,
|
||||
trailers=trailers)
|
||||
if xml is None:
|
||||
LOG.error('Could not get a playqueue xml for plex id %s', plex_id)
|
||||
LOG.error('Could not get a playqueue xml for plex id %s, UUID %s',
|
||||
plex_id, xml.attrib.get('librarySectionUUID'))
|
||||
# "Play error"
|
||||
utils.dialog('notification',
|
||||
utils.lang(29999),
|
||||
utils.lang(30128),
|
||||
icon='{error}')
|
||||
# Do NOT use _ensure_resolve() because we resolved above already
|
||||
state.CONTEXT_MENU_PLAY = False
|
||||
state.FORCE_TRANSCODE = False
|
||||
state.RESUME_PLAYBACK = False
|
||||
return
|
||||
PL.get_playlist_details_from_xml(playqueue, xml)
|
||||
stack = _prep_playlist_stack(xml, resume)
|
||||
stack = _prep_playlist_stack(xml)
|
||||
_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
|
||||
# Always resume if playback initiated via PMS and there IS a resume
|
||||
# point
|
||||
offset = api.resume_point() * 1000 if state.CONTEXT_MENU_PLAY else None
|
||||
# Reset some playback variables
|
||||
state.CONTEXT_MENU_PLAY = False
|
||||
state.FORCE_TRANSCODE = False
|
||||
# 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
|
||||
LOG.info('Done initializing playback, starting Kodi player at pos %s and '
|
||||
'resume point %s', pos, offset)
|
||||
# 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
|
||||
|
@ -263,6 +254,8 @@ def _playback_init(plex_id, plex_type, playqueue, pos, resume):
|
|||
# plugin://pkc will be lost; Kodi will try to startup playback for an empty
|
||||
# path: log entry is "CGUIWindowVideoBase::OnPlayMedia <missing path>"
|
||||
thread.start()
|
||||
# Ensure that PKC playqueue monitor ignores the changes we just made
|
||||
playqueue.pkc_edit = True
|
||||
|
||||
|
||||
def _ensure_resolve(abort=False):
|
||||
|
@ -275,33 +268,23 @@ def _ensure_resolve(abort=False):
|
|||
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')
|
||||
LOG.debug('Passing dummy path to Kodi')
|
||||
# if not state.CONTEXT_MENU_PLAY:
|
||||
# Because playback won't start with context menu play
|
||||
state.PKC_CAUSED_STOP = True
|
||||
state.PKC_CAUSED_STOP_DONE = False
|
||||
if not abort:
|
||||
result = pickler.Playback_Successful()
|
||||
result.listitem = PKCListItem(path=v.NULL_VIDEO)
|
||||
pickler.pickle_me(result)
|
||||
else:
|
||||
# Shows PKC error message
|
||||
pickler.pickle_me(None)
|
||||
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
|
||||
# Reset some playback variables
|
||||
state.CONTEXT_MENU_PLAY = False
|
||||
state.FORCE_TRANSCODE = False
|
||||
state.RESUME_PLAYBACK = False
|
||||
|
||||
|
||||
def _init_existing_kodi_playlist(playqueue, pos):
|
||||
|
@ -313,31 +296,27 @@ def _init_existing_kodi_playlist(playqueue, pos):
|
|||
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')
|
||||
raise PL.PlaylistError('No Kodi items returned')
|
||||
item = PL.init_plex_playqueue(playqueue, kodi_item=kodi_items[pos])
|
||||
item.force_transcode = app.PLAYSTATE.force_transcode
|
||||
item.force_transcode = state.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
|
||||
"""
|
||||
def _prep_playlist_stack(xml):
|
||||
stack = []
|
||||
for i, item in enumerate(xml):
|
||||
for item in 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 (state.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
|
||||
with plexdb.Get_Plex_DB() as plex_db:
|
||||
plex_dbitem = plex_db.getItem_byId(api.plex_id())
|
||||
kodi_id = plex_dbitem[0] if plex_dbitem else None
|
||||
kodi_type = plex_dbitem[4] if plex_dbitem 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
|
||||
|
@ -348,20 +327,12 @@ def _prep_playlist_stack(xml, resume):
|
|||
kodi_id = None
|
||||
kodi_type = None
|
||||
for part, _ in enumerate(item[0]):
|
||||
api.part = part
|
||||
api.set_part_number(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'))
|
||||
path = api.path()
|
||||
listitem = api.create_listitem()
|
||||
listitem.setPath(utils.try_encode(path))
|
||||
else:
|
||||
# Will add directly via the Kodi DB
|
||||
path = None
|
||||
|
@ -375,7 +346,6 @@ def _prep_playlist_stack(xml, resume):
|
|||
'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
|
||||
|
@ -407,27 +377,10 @@ def _process_stack(playqueue, stack):
|
|||
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']
|
||||
playlist_item.force_transcode = state.FORCE_TRANSCODE
|
||||
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).
|
||||
|
@ -443,30 +396,53 @@ def _conclude_playback(playqueue, pos):
|
|||
start playback
|
||||
return PKC listitem attached to result
|
||||
"""
|
||||
LOG.debug('Concluding playback for playqueue position %s', pos)
|
||||
LOG.info('Concluding playback for playqueue position %s', pos)
|
||||
result = pickler.Playback_Successful()
|
||||
listitem = PKCListItem()
|
||||
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:
|
||||
if item.xml is not None:
|
||||
# Got a Plex element
|
||||
api = API(item.xml)
|
||||
api.set_part_number(item.part)
|
||||
api.create_listitem(listitem)
|
||||
playutils = PlayUtils(api, item)
|
||||
playurl = playutils.getPlayUrl()
|
||||
else:
|
||||
api = None
|
||||
playurl = item.file
|
||||
if not playurl:
|
||||
LOG.info('Did not get a playurl, aborting playback silently')
|
||||
_ensure_resolve()
|
||||
state.RESUME_PLAYBACK = False
|
||||
pickler.pickle_me(result)
|
||||
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')
|
||||
listitem.setPath(utils.try_encode(playurl))
|
||||
if item.playmethod == 'DirectStream':
|
||||
listitem.setSubtitles(api.cache_external_subs())
|
||||
elif item.playmethod == 'Transcode':
|
||||
playutils.audio_subtitle_prefs(listitem)
|
||||
|
||||
if state.RESUME_PLAYBACK is True:
|
||||
state.RESUME_PLAYBACK = False
|
||||
if (item.offset is None and
|
||||
item.plex_type not in (v.PLEX_TYPE_SONG, v.PLEX_TYPE_CLIP)):
|
||||
with plexdb.Get_Plex_DB() as plex_db:
|
||||
plex_dbitem = plex_db.getItem_byId(item.plex_id)
|
||||
file_id = plex_dbitem[1] if plex_dbitem else None
|
||||
with kodidb.GetKodiDB('video') as kodi_db:
|
||||
item.offset = kodi_db.get_resume(file_id)
|
||||
LOG.info('Resuming playback at %s', item.offset)
|
||||
if v.KODIVERSION >= 18 and api:
|
||||
# Kodi 18 Alpha 3 broke StartOffset
|
||||
percent = item.offset / api.runtime() * 100.0
|
||||
LOG.debug('Resuming at %s percent', percent)
|
||||
listitem.setProperty('StartPercent', str(percent))
|
||||
else:
|
||||
listitem.setProperty('StartOffset', str(item.offset))
|
||||
listitem.setProperty('resumetime', str(item.offset))
|
||||
# Reset the resumable flag
|
||||
result.listitem = listitem
|
||||
pickler.pickle_me(result)
|
||||
LOG.info('Done concluding playback')
|
||||
|
||||
|
||||
def process_indirect(key, offset, resolve=True):
|
||||
|
@ -480,65 +456,59 @@ def process_indirect(key, offset, resolve=True):
|
|||
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)
|
||||
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
|
||||
result = pickler.Playback_Successful()
|
||||
if key.startswith('http') or key.startswith('{server}'):
|
||||
xml = PF.get_playback_xml(key, app.CONN.server_name)
|
||||
xml = DU().downloadUrl(key)
|
||||
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)
|
||||
xml = DU().downloadUrl('http://node.plexapp.com:32400%s' % key)
|
||||
else:
|
||||
xml = PF.get_playback_xml('{server}%s' % key, app.CONN.server_name)
|
||||
if xml is None:
|
||||
xml = DU().downloadUrl('{server}%s' % key)
|
||||
try:
|
||||
xml[0].attrib
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
LOG.error('Could not download PMS metadata')
|
||||
_ensure_resolve(abort=True)
|
||||
return
|
||||
|
||||
if offset != '0':
|
||||
offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset))
|
||||
# Todo: implement offset
|
||||
api = API(xml[0])
|
||||
listitem = api.listitem(listitem=transfer.PKCListItem, resume=False)
|
||||
listitem = PKCListItem()
|
||||
api.create_listitem(listitem)
|
||||
playqueue = PQ.get_playqueue_from_type(
|
||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_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
|
||||
|
||||
item = PL.Playlist_Item()
|
||||
item.xml = xml[0]
|
||||
item.offset = int(offset)
|
||||
item.plex_type = v.PLEX_TYPE_CLIP
|
||||
item.playmethod = 'DirectStream'
|
||||
# Need to get yet another xml to get the final playback url
|
||||
xml = DU().downloadUrl('http://node.plexapp.com:32400%s'
|
||||
% xml[0][0][0].attrib['key'])
|
||||
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)
|
||||
xml[0].attrib
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
LOG.error('XML malformed: %s', xml.attrib)
|
||||
xml = None
|
||||
if xml is None:
|
||||
LOG.error('Could not download last xml for playurl')
|
||||
_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
|
||||
|
||||
playurl = xml[0].attrib['key']
|
||||
item.file = playurl
|
||||
listitem.setPath(utils.try_encode(playurl))
|
||||
playqueue.items.append(item)
|
||||
if resolve is True:
|
||||
transfer.send(listitem)
|
||||
result.listitem = listitem
|
||||
pickler.pickle_me(result)
|
||||
else:
|
||||
thread = Thread(target=app.APP.player.play,
|
||||
thread = Thread(target=Player().play,
|
||||
args={'item': utils.try_encode(playurl),
|
||||
'listitem': listitem})
|
||||
thread.setDaemon(True)
|
||||
LOG.debug('Done initializing PKC playback, starting Kodi player')
|
||||
LOG.info('Done initializing PKC playback, starting Kodi player')
|
||||
thread.start()
|
||||
|
||||
|
||||
|
@ -549,67 +519,41 @@ def play_xml(playqueue, xml, offset=None, start_plex_id=None):
|
|||
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
|
||||
LOG.info("play_xml called with offset %s, start_plex_id %s",
|
||||
offset, start_plex_id)
|
||||
stack = _prep_playlist_stack(xml)
|
||||
_process_stack(playqueue, stack)
|
||||
LOG.debug('Playqueue after play_xml update: %s', playqueue)
|
||||
if start_plex_id is not None:
|
||||
for startpos, item in enumerate(playqueue.items):
|
||||
if item.plex_id == start_plex_id:
|
||||
break
|
||||
else:
|
||||
startpos = 0
|
||||
else:
|
||||
for startpos, item in enumerate(playqueue.items):
|
||||
if item.id == playqueue.selectedItemID:
|
||||
break
|
||||
else:
|
||||
startpos = 0
|
||||
thread = Thread(target=threaded_playback,
|
||||
args=(playqueue.kodi_pl, startpos, offset))
|
||||
LOG.debug('Done play_xml, starting Kodi player at position %s', startpos)
|
||||
LOG.info('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]
|
||||
Seek immediately after kicking off playback is not reliable.
|
||||
"""
|
||||
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)
|
||||
player = Player()
|
||||
player.play(kodi_playlist, None, False, startpos)
|
||||
if offset and offset != '0':
|
||||
i = 0
|
||||
while not player.isPlaying():
|
||||
sleep(100)
|
||||
i += 1
|
||||
if i > 100:
|
||||
LOG.error('Could not seek to %s', offset)
|
||||
return
|
||||
js.seek_to(int(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
|
|
@ -2,8 +2,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
from threading import Thread
|
||||
from urlparse import parse_qsl
|
||||
|
||||
from . import utils, playback, context_entry, transfer, backgroundthread
|
||||
from . import playback
|
||||
from . import context_entry
|
||||
from . import json_rpc as js
|
||||
from . import pickler
|
||||
from . import kodidb_functions as kodidb
|
||||
from . import state
|
||||
|
||||
###############################################################################
|
||||
|
||||
|
@ -12,45 +19,56 @@ LOG = getLogger('PLEX.playback_starter')
|
|||
###############################################################################
|
||||
|
||||
|
||||
class PlaybackTask(backgroundthread.Task):
|
||||
class PlaybackStarter(Thread):
|
||||
"""
|
||||
Processes new plays
|
||||
"""
|
||||
def __init__(self, command):
|
||||
self.command = command
|
||||
super(PlaybackTask, self).__init__()
|
||||
|
||||
def run(self):
|
||||
LOG.debug('Starting PlaybackTask with %s', self.command)
|
||||
item = self.command
|
||||
@staticmethod
|
||||
def _triage(item):
|
||||
try:
|
||||
_, params = item.split('?', 1)
|
||||
except ValueError:
|
||||
# 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')
|
||||
pickler.pickle_me(pickler.Playback_Successful())
|
||||
return
|
||||
params = dict(utils.parse_qsl(params))
|
||||
params = dict(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)
|
||||
resolve=resolve)
|
||||
elif mode == 'plex_node':
|
||||
playback.process_indirect(params['key'],
|
||||
params['offset'],
|
||||
resolve=resolve)
|
||||
elif mode == 'navigation':
|
||||
# e.g. when plugin://...tvshows is called for entire season
|
||||
with kodidb.GetKodiDB('video') as kodi_db:
|
||||
show_id = kodi_db.show_id_from_path(params.get('path'))
|
||||
if show_id:
|
||||
js.activate_window('videos',
|
||||
'videodb://tvshows/titles/%s' % show_id)
|
||||
else:
|
||||
LOG.error('Could not find tv show id for %s', item)
|
||||
if resolve:
|
||||
pickler.pickle_me(pickler.Playback_Successful())
|
||||
elif mode == 'context_menu':
|
||||
context_entry.ContextMenu(kodi_id=params.get('kodi_id'),
|
||||
kodi_type=params.get('kodi_type'))
|
||||
LOG.debug('Finished PlaybackTask')
|
||||
|
||||
def run(self):
|
||||
queue = state.COMMAND_PIPELINE_QUEUE
|
||||
LOG.info("----===## Starting PlaybackStarter ##===----")
|
||||
while True:
|
||||
item = queue.get()
|
||||
if item is None:
|
||||
# Need to shutdown - initiated by command_pipeline
|
||||
break
|
||||
else:
|
||||
self._triage(item)
|
||||
queue.task_done()
|
||||
LOG.info("----===## PlaybackStarter stopped ##===----")
|
||||
|
|
|
@ -5,23 +5,32 @@ Collection of functions associated with Kodi and Plex playlists and playqueues
|
|||
"""
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import urllib
|
||||
from urlparse import parse_qsl, urlsplit
|
||||
|
||||
from .plex_api import API
|
||||
from .plex_db import PlexDB
|
||||
from . import plex_functions as PF
|
||||
from .kodi_db import kodiid_from_filename
|
||||
from . import plexdb_functions as plexdb
|
||||
from . import kodidb_functions as kodidb
|
||||
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
|
||||
from . import state
|
||||
|
||||
###############################################################################
|
||||
|
||||
LOG = getLogger('PLEX.playlist_func')
|
||||
|
||||
###############################################################################
|
||||
|
||||
|
||||
class PlaylistError(Exception):
|
||||
"""
|
||||
Exception for our playlist constructs
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Playqueue_Object(object):
|
||||
"""
|
||||
|
@ -122,267 +131,116 @@ class Playqueue_Object(object):
|
|||
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):
|
||||
class Playlist_Item(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"
|
||||
id = None [str] Plex playlist/playqueue id, e.g. playQueueItemID
|
||||
plex_id = None [str] 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!)
|
||||
plex_uuid = None [str] Plex librarySectionUUID
|
||||
kodi_id = None 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
|
||||
uri = None [str] Weird Plex uri path involving plex_uuid. STRING!
|
||||
guid = None [str] Weird Plex guid
|
||||
api = None [API] API of xml 1 lvl below <MediaContainer>
|
||||
playmethod = None [str] either 'DirectPath', 'DirectStream', 'Transcode'
|
||||
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
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.id = None
|
||||
self._plex_id = None
|
||||
self.plex_id = None
|
||||
self.plex_type = None
|
||||
self.plex_uuid = None
|
||||
self.kodi_id = None
|
||||
self.kodi_type = None
|
||||
self.file = None
|
||||
self._uri = None
|
||||
self.uri = None
|
||||
self.guid = None
|
||||
self.api = None
|
||||
self.xml = 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 ("{{"
|
||||
def __repr__(self):
|
||||
answ = ("{{"
|
||||
"'id': {self.id}, "
|
||||
"'plex_id': {self.plex_id}, "
|
||||
"'plex_type': '{self.plex_type}', "
|
||||
"'plex_uuid': '{self.plex_uuid}', "
|
||||
"'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}, "
|
||||
"'resume': {self.resume},"
|
||||
"'offset': {self.offset}, "
|
||||
"'force_transcode': {self.force_transcode}, "
|
||||
"'part': {self.part}".format(self=self))
|
||||
"'part': {self.part}, ".format(self=self))
|
||||
answ = answ.encode('utf-8')
|
||||
# etree xml.__repr__() could return string, not unicode
|
||||
return answ + b"'xml': \"{self.xml}\"}}".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 __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
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].
|
||||
index.
|
||||
|
||||
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
|
||||
stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type]
|
||||
count = 0
|
||||
# Kodi indexes differently than Plex
|
||||
for stream in self.xml[0][self.part]:
|
||||
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 self.xml[0][self.part]:
|
||||
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 plex_stream_index [int] in order to receive the Kodi stream
|
||||
index [int].
|
||||
Pass in the kodi_stream_index [int] in order to receive the Plex stream
|
||||
index.
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
|
||||
def playlist_item_from_kodi(kodi_item):
|
||||
|
@ -392,24 +250,31 @@ def playlist_item_from_kodi(kodi_item):
|
|||
Supply with data['item'] as returned from Kodi JSON-RPC interface.
|
||||
kodi_item dict contains keys 'id', 'type', 'file' (if applicable)
|
||||
"""
|
||||
item = PlaylistItem()
|
||||
item = Playlist_Item()
|
||||
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']
|
||||
with plexdb.Get_Plex_DB() as plex_db:
|
||||
plex_dbitem = plex_db.getItem_byKodiId(kodi_item['id'],
|
||||
kodi_item['type'])
|
||||
try:
|
||||
item.plex_id = plex_dbitem[0]
|
||||
item.plex_type = plex_dbitem[2]
|
||||
item.plex_uuid = plex_dbitem[0] # we dont need the uuid yet :-)
|
||||
except TypeError:
|
||||
pass
|
||||
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'))
|
||||
query = dict(parse_qsl(urlsplit(item.file).query))
|
||||
item.plex_id = query.get('plex_id')
|
||||
item.plex_type = query.get('itemType')
|
||||
if item.plex_id is None and item.file is not None:
|
||||
item.uri = ('library://whatever/item/%s'
|
||||
% urllib.quote(utils.try_encode(item.file), safe=''))
|
||||
else:
|
||||
# TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER
|
||||
item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' %
|
||||
(item.plex_uuid, item.plex_id))
|
||||
LOG.debug('Made playlist item from Kodi: %s', item)
|
||||
return item
|
||||
|
||||
|
@ -423,39 +288,40 @@ def verify_kodi_item(plex_id, kodi_item):
|
|||
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
|
||||
with either 'plugin' or 'http'
|
||||
"""
|
||||
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
|
||||
if (not state.DIRECT_PATHS and state.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)
|
||||
kodi_item['id'], _ = kodidb.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.info('kodi_item %s cannot be used for Plex playback', kodi_item)
|
||||
raise PlaylistError
|
||||
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_id, _ = kodidb.kodiid_from_filename(kodi_item['file'],
|
||||
v.KODI_TYPE_MOVIE)
|
||||
kodi_item['type'] = v.KODI_TYPE_MOVIE
|
||||
if kodi_id is None:
|
||||
kodi_id, _ = kodidb.kodiid_from_filename(kodi_item['file'],
|
||||
v.KODI_TYPE_EPISODE)
|
||||
kodi_item['type'] = v.KODI_TYPE_EPISODE
|
||||
if kodi_id is None:
|
||||
kodi_id, _ = kodidb.kodiid_from_filename(kodi_item['file'],
|
||||
v.KODI_TYPE_SONG)
|
||||
kodi_item['type'] = v.KODI_TYPE_SONG
|
||||
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)
|
||||
kodi_item['type'] = None if kodi_id is None else kodi_item['type']
|
||||
LOG.debug('Research results for kodi_item: %s', kodi_item)
|
||||
return kodi_item
|
||||
|
||||
|
@ -466,16 +332,19 @@ def playlist_item_from_plex(plex_id):
|
|||
|
||||
Returns a Playlist_Item
|
||||
"""
|
||||
item = PlaylistItem()
|
||||
item = Playlist_Item()
|
||||
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:
|
||||
with plexdb.Get_Plex_DB() as plex_db:
|
||||
plex_dbitem = plex_db.getItem_byId(plex_id)
|
||||
try:
|
||||
item.plex_type = plex_dbitem[5]
|
||||
item.kodi_id = plex_dbitem[0]
|
||||
item.kodi_type = plex_dbitem[4]
|
||||
except (TypeError, IndexError):
|
||||
raise KeyError('Could not find plex_id %s in database' % plex_id)
|
||||
item.plex_uuid = plex_id
|
||||
item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' %
|
||||
(item.plex_uuid, plex_id))
|
||||
LOG.debug('Made playlist item from plex: %s', item)
|
||||
return item
|
||||
|
||||
|
@ -486,42 +355,41 @@ def playlist_item_from_xml(xml_video_element, kodi_id=None, kodi_type=None):
|
|||
|
||||
xml_video_element: etree xml piece 1 level underneath <MediaContainer>
|
||||
"""
|
||||
item = PlaylistItem()
|
||||
item = Playlist_Item()
|
||||
api = API(xml_video_element)
|
||||
item.plex_id = api.plex_id
|
||||
item.plex_type = api.plex_type
|
||||
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']
|
||||
elif item.plex_id is not None and item.plex_type != v.PLEX_TYPE_CLIP:
|
||||
with plexdb.Get_Plex_DB() as plex_db:
|
||||
db_element = plex_db.getItem_byId(item.plex_id)
|
||||
try:
|
||||
item.kodi_id, item.kodi_type = db_element[0], db_element[4]
|
||||
except TypeError:
|
||||
pass
|
||||
item.guid = api.guid_html_escaped()
|
||||
item.playcount = api.viewcount()
|
||||
item.offset = api.resume_point()
|
||||
item.api = api
|
||||
item.xml = xml_video_element
|
||||
LOG.debug('Created new playlist item from xml: %s', item)
|
||||
return item
|
||||
|
||||
|
||||
def _update_playlist_version(playlist, xml):
|
||||
def _get_playListVersion_from_xml(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
|
||||
Takes a PMS xml 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):
|
||||
playlist.version = int(xml.attrib['%sVersion' % playlist.kind])
|
||||
except (TypeError, AttributeError, KeyError):
|
||||
raise PlaylistError('Could not get new playlist Version for playlist '
|
||||
'%s' % playlist)
|
||||
|
||||
|
@ -533,15 +401,13 @@ def get_playlist_details_from_xml(playlist, xml):
|
|||
|
||||
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))
|
||||
playlist.id = xml.get('%sID' % playlist.kind).decode('utf-8')
|
||||
playlist.version = xml.get('%sVersion' % playlist.kind).decode('utf-8')
|
||||
playlist.shuffled = xml.get('%sShuffled' % playlist.kind).decode('utf-8')
|
||||
playlist.selectedItemID = xml.get(
|
||||
'%sSelectedItemID' % playlist.kind).decode('utf-8')
|
||||
playlist.selectedItemOffset = xml.get(
|
||||
'%sSelectedItemOffset' % playlist.kind).decode('utf-8')
|
||||
LOG.debug('Updated playlist from xml: %s', playlist)
|
||||
|
||||
|
||||
|
@ -551,8 +417,6 @@ def update_playlist_from_PMS(playlist, playlist_id=None, xml=None):
|
|||
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)
|
||||
|
@ -574,8 +438,8 @@ def init_plex_playqueue(playlist, plex_id=None, kodi_item=None):
|
|||
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)
|
||||
verify_kodi_item(plex_id, kodi_item)
|
||||
try:
|
||||
if plex_id:
|
||||
item = playlist_item_from_plex(plex_id)
|
||||
|
@ -584,14 +448,11 @@ def init_plex_playqueue(playlist, plex_id=None, kodi_item=None):
|
|||
params = {
|
||||
'next': 0,
|
||||
'type': playlist.type,
|
||||
'uri': item.uri,
|
||||
'includeMarkers': 1, # e.g. start + stop of intros
|
||||
'uri': item.uri
|
||||
}
|
||||
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])
|
||||
|
@ -677,22 +538,16 @@ def add_item_to_plex_playqueue(playlist, pos, plex_id=None, kodi_item=None):
|
|||
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
|
||||
}
|
||||
url = "{server}/%ss/%s?uri=%s" % (playlist.kind, playlist.id, item.uri)
|
||||
# Will always put the new item at the end of the Plex playlist
|
||||
xml = DU().downloadUrl(url,
|
||||
action_type="PUT",
|
||||
parameters=parameters)
|
||||
xml = DU().downloadUrl(url, action_type="PUT")
|
||||
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.xml = xml[-1]
|
||||
item.id = api.item_id()
|
||||
item.guid = api.guid_html_escaped()
|
||||
item.offset = api.resume_point()
|
||||
|
@ -700,7 +555,7 @@ def add_item_to_plex_playqueue(playlist, pos, plex_id=None, kodi_item=None):
|
|||
playlist.items.append(item)
|
||||
if pos == len(playlist.items) - 1:
|
||||
# Item was added at the end
|
||||
_update_playlist_version(playlist, xml)
|
||||
_get_playListVersion_from_xml(playlist, xml)
|
||||
else:
|
||||
# Move the new item to the correct position
|
||||
move_playlist_item(playlist,
|
||||
|
@ -744,7 +599,7 @@ def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None,
|
|||
{'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])
|
||||
item.xml = xml[-1]
|
||||
playlist.items.insert(pos, item)
|
||||
return item
|
||||
|
||||
|
@ -768,10 +623,9 @@ def move_playlist_item(playlist, before_pos, after_pos):
|
|||
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)
|
||||
# We need to increment the playlistVersion
|
||||
_get_playListVersion_from_xml(
|
||||
playlist, DU().downloadUrl(url, action_type="PUT"))
|
||||
# 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)
|
||||
|
@ -782,20 +636,17 @@ 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
|
||||
Returns None 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)
|
||||
xml = DU().downloadUrl("{server}/playlists/%s/items" % playlist_id)
|
||||
else:
|
||||
xml = DU().downloadUrl("{server}/playQueues/%s" % playlist_id,
|
||||
parameters=parameters)
|
||||
xml = DU().downloadUrl("{server}/playQueues/%s" % playlist_id)
|
||||
try:
|
||||
xml.attrib
|
||||
except AttributeError:
|
||||
raise PlaylistError('Did not get a valid xml')
|
||||
xml = None
|
||||
return xml
|
||||
|
||||
|
||||
|
@ -818,8 +669,8 @@ def delete_playlist_item_from_PMS(playlist, pos):
|
|||
playlist.items[pos].id,
|
||||
playlist.repeat),
|
||||
action_type="DELETE")
|
||||
_get_playListVersion_from_xml(playlist, xml)
|
||||
del playlist.items[pos]
|
||||
_update_playlist_version(playlist, xml)
|
||||
|
||||
|
||||
# Functions operating on the Kodi playlist objects ##########
|
||||
|
@ -890,10 +741,9 @@ 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'})
|
||||
xml = DU().downloadUrl(
|
||||
"{server}/playQueues/%s" % playqueue_id,
|
||||
headerOptions={'Accept': 'application/xml'})
|
||||
try:
|
||||
xml.attrib
|
||||
except AttributeError:
|
||||
|
|
|
@ -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
|
||||
from .. import utils, path_ops, variables as v, state
|
||||
|
||||
###############################################################################
|
||||
LOG = getLogger('PLEX.playlists')
|
||||
|
@ -36,13 +34,14 @@ SUPPORTED_FILETYPES = (
|
|||
# 'pls',
|
||||
# 'cue',
|
||||
)
|
||||
# Avoid endless loops. Store Plex IDs for creating, Kodi paths for deleting!
|
||||
IGNORE_KODI_PLAYLIST_CHANGE = list()
|
||||
# Used for updating Plex playlists due to Kodi changes - Plex playlist
|
||||
# will have to be deleted first. Add Plex ids!
|
||||
IGNORE_PLEX_PLAYLIST_CHANGE = list()
|
||||
###############################################################################
|
||||
|
||||
|
||||
def should_cancel():
|
||||
return app.APP.stop_pkc or app.SYNC.stop_sync
|
||||
|
||||
|
||||
def kodi_playlist_monitor():
|
||||
"""
|
||||
Monitor for the Kodi playlist folder special://profile/playlist
|
||||
|
@ -69,22 +68,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
|
||||
|
@ -109,22 +92,22 @@ def websocket(plex_id, status):
|
|||
* 9: 'deleted'
|
||||
"""
|
||||
create = False
|
||||
plex_id = int(plex_id)
|
||||
with app.APP.lock_playlists:
|
||||
with state.LOCK_PLAYLISTS:
|
||||
playlist = db.get_playlist(plex_id=plex_id)
|
||||
if plex_id in plex_pl.IGNORE_PLEX_PLAYLIST_CHANGE:
|
||||
if plex_id in IGNORE_PLEX_PLAYLIST_CHANGE:
|
||||
LOG.debug('Ignoring detected Plex playlist change for %s',
|
||||
playlist)
|
||||
plex_pl.IGNORE_PLEX_PLAYLIST_CHANGE.remove(plex_id)
|
||||
IGNORE_PLEX_PLAYLIST_CHANGE.remove(plex_id)
|
||||
return
|
||||
if playlist and status == 9:
|
||||
# Won't be able to download metadata of the deleted playlist
|
||||
if sync_plex_playlist(playlist=playlist):
|
||||
LOG.debug('Plex deletion of playlist detected: %s', playlist)
|
||||
try:
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.append(plex_id)
|
||||
kodi_pl.delete(playlist)
|
||||
except PlaylistError:
|
||||
pass
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.remove(plex_id)
|
||||
return
|
||||
xml = pms.metadata(plex_id)
|
||||
if xml is None:
|
||||
|
@ -142,6 +125,7 @@ def websocket(plex_id, status):
|
|||
else:
|
||||
LOG.debug('Change of Plex playlist detected: %s',
|
||||
playlist)
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.append(plex_id)
|
||||
kodi_pl.delete(playlist)
|
||||
create = True
|
||||
elif not playlist and not status == 9:
|
||||
|
@ -150,9 +134,10 @@ def websocket(plex_id, status):
|
|||
create = True
|
||||
# To the actual work
|
||||
if create:
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.append(plex_id)
|
||||
kodi_pl.create(plex_id)
|
||||
except PlaylistError:
|
||||
pass
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.remove(plex_id)
|
||||
|
||||
|
||||
def full_sync():
|
||||
|
@ -170,7 +155,7 @@ def full_sync():
|
|||
fetch the PMS playlists)
|
||||
"""
|
||||
LOG.info('Starting playlist full sync')
|
||||
with app.APP.lock_playlists:
|
||||
with state.LOCK_PLAYLISTS:
|
||||
# Need to lock because we're messing with playlists
|
||||
return _full_sync()
|
||||
|
||||
|
@ -184,54 +169,54 @@ 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():
|
||||
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())
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.append(api.plex_id())
|
||||
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())
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.remove(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!
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.append(playlist.kodi_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())
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.remove(playlist.kodi_path)
|
||||
else:
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.append(api.plex_id())
|
||||
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())
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.remove(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():
|
||||
return False
|
||||
playlist = db.get_playlist(plex_id=plex_id)
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.append(playlist.kodi_path)
|
||||
LOG.debug('Removing outdated Plex playlist from Kodi: %s', playlist)
|
||||
if playlist is None:
|
||||
continue
|
||||
try:
|
||||
kodi_pl.delete(playlist)
|
||||
except PlaylistError:
|
||||
LOG.debug('Skipping deletion of playlist: %s', playlist)
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.remove(playlist.kodi_path)
|
||||
# Look at all supported Kodi playlists. Check whether they are in the DB.
|
||||
old_kodi_paths = db.kodi_playlist_paths()
|
||||
for root, _, files in path_ops.walk(v.PLAYLIST_PATH):
|
||||
for f in files:
|
||||
if should_cancel():
|
||||
return False
|
||||
path = path_ops.path.join(root, f)
|
||||
try:
|
||||
old_kodi_paths.remove(path)
|
||||
|
@ -239,7 +224,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
|
||||
|
@ -254,6 +239,7 @@ def _full_sync():
|
|||
LOG.info('Skipping Kodi playlist %s', path)
|
||||
else:
|
||||
LOG.debug('Changed Kodi playlist detected: %s', path)
|
||||
IGNORE_PLEX_PLAYLIST_CHANGE.append(playlist.plex_id)
|
||||
plex_pl.delete(playlist)
|
||||
playlist.kodi_hash = kodi_hash
|
||||
try:
|
||||
|
@ -261,11 +247,7 @@ def _full_sync():
|
|||
except PlaylistError:
|
||||
LOG.info('Skipping Kodi playlist %s', path)
|
||||
for kodi_path in old_kodi_paths:
|
||||
if should_cancel():
|
||||
return False
|
||||
playlist = db.get_playlist(path=kodi_path)
|
||||
if not playlist:
|
||||
continue
|
||||
try:
|
||||
plex_pl.delete(playlist)
|
||||
except PlaylistError:
|
||||
|
@ -301,7 +283,7 @@ def sync_kodi_playlist(path):
|
|||
return False
|
||||
if extension not in SUPPORTED_FILETYPES:
|
||||
return False
|
||||
if not app.SYNC.sync_specific_kodi_playlists:
|
||||
if not state.SYNC_SPECIFIC_KODI_PLAYLISTS:
|
||||
return True
|
||||
playlist = Playlist()
|
||||
playlist.kodi_path = path
|
||||
|
@ -357,16 +339,12 @@ 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):
|
||||
if (not state.ENABLE_MUSIC and typus == v.PLEX_PLAYLIST_TYPE_AUDIO):
|
||||
LOG.debug('Not synching Plex audio playlist')
|
||||
return False
|
||||
if not app.SYNC.sync_specific_plex_playlists:
|
||||
if not state.SYNC_SPECIFIC_PLEX_PLAYLISTS:
|
||||
return True
|
||||
prefix = utils.settings('syncSpecificPlexPlaylistsPrefix').lower()
|
||||
if name and name.lower().startswith(prefix):
|
||||
|
@ -391,25 +369,31 @@ class PlaylistEventhandler(events.FileSystemEventHandler):
|
|||
"""
|
||||
path = event.dest_path if event.event_type == events.EVENT_TYPE_MOVED \
|
||||
else event.src_path
|
||||
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,
|
||||
}
|
||||
if not sync_kodi_playlist(path):
|
||||
return
|
||||
playlist = db.get_playlist(path=path)
|
||||
if playlist and playlist.plex_id in IGNORE_KODI_PLAYLIST_CHANGE:
|
||||
LOG.debug('Ignoring event %s for playlist %s', event, playlist)
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.remove(playlist.plex_id)
|
||||
return
|
||||
if not playlist and path in IGNORE_KODI_PLAYLIST_CHANGE:
|
||||
LOG.debug('Ignoring deletion event %s for playlist %s',
|
||||
event, playlist)
|
||||
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 state.LOCK_PLAYLISTS:
|
||||
_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 +412,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 +431,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,13 @@ 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
|
||||
|
||||
from .. import path_ops
|
||||
from .. import variables as v
|
||||
###############################################################################
|
||||
LOG = getLogger('PLEX.playlists.common')
|
||||
|
||||
|
@ -22,6 +19,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 +64,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 +75,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 +102,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 +122,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
|
||||
|
@ -184,7 +171,7 @@ class PlaylistObserver(Observer):
|
|||
try:
|
||||
new_event, new_watch = event_queue.get(block=False)
|
||||
except Queue.Empty:
|
||||
app.APP.monitor.waitForAbort(0.2)
|
||||
time.sleep(0.2)
|
||||
else:
|
||||
event_queue.task_done()
|
||||
start = time.time()
|
||||
|
|
|
@ -7,11 +7,11 @@ module
|
|||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from .common import Playlist
|
||||
from ..plex_db import PlexDB
|
||||
from ..kodi_db import kodiid_from_filename
|
||||
from .common import Playlist, PlaylistError
|
||||
|
||||
from .. import kodidb_functions as kodidb
|
||||
from .. import plexdb_functions as plexdb
|
||||
from .. import path_ops, utils, variables as v
|
||||
from ..exceptions import PlaylistError
|
||||
###############################################################################
|
||||
LOG = getLogger('PLEX.playlists.db')
|
||||
|
||||
|
@ -22,16 +22,16 @@ def plex_playlist_ids():
|
|||
"""
|
||||
Returns a list of all Plex ids of the playlists already in our DB
|
||||
"""
|
||||
with PlexDB() as plexdb:
|
||||
return list(plexdb.playlist_ids())
|
||||
with plexdb.Get_Plex_DB() as plex_db:
|
||||
return plex_db.plex_ids_all_playlists()
|
||||
|
||||
|
||||
def kodi_playlist_paths():
|
||||
"""
|
||||
Returns a list of all Kodi playlist paths of the playlists already synced
|
||||
"""
|
||||
with PlexDB() as plexdb:
|
||||
return list(plexdb.kodi_playlist_paths())
|
||||
with plexdb.Get_Plex_DB() as plex_db:
|
||||
return plex_db.all_kodi_playlist_paths()
|
||||
|
||||
|
||||
def update_playlist(playlist, delete=False):
|
||||
|
@ -41,40 +41,27 @@ def update_playlist(playlist, delete=False):
|
|||
|
||||
Pass delete=True to delete the playlist entry
|
||||
"""
|
||||
with PlexDB() as plexdb:
|
||||
with plexdb.Get_Plex_DB() as plex_db:
|
||||
if delete:
|
||||
plexdb.delete_playlist(playlist)
|
||||
plex_db.delete_playlist_entry(playlist)
|
||||
else:
|
||||
plexdb.add_playlist(playlist)
|
||||
plex_db.insert_playlist_entry(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)
|
||||
with plexdb.Get_Plex_DB() as plex_db:
|
||||
playlist = plex_db.retrieve_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
|
||||
|
@ -82,11 +69,7 @@ def _m3u_iterator(text):
|
|||
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):
|
||||
|
@ -108,21 +91,21 @@ def m3u_to_plex_ids(playlist):
|
|||
plex_ids.append(plex_id)
|
||||
else:
|
||||
# Add-on paths not working, try direct
|
||||
kodi_id, kodi_type = kodiid_from_filename(entry,
|
||||
db_type=playlist.kodi_type)
|
||||
kodi_id, kodi_type = kodidb.kodiid_from_filename(
|
||||
entry, db_type=playlist.kodi_type)
|
||||
if not kodi_id:
|
||||
continue
|
||||
with PlexDB() as plexdb:
|
||||
item = plexdb.item_by_kodi_id(kodi_id, kodi_type)
|
||||
if item:
|
||||
plex_ids.append(item['plex_id'])
|
||||
with plexdb.Get_Plex_DB() as plex_db:
|
||||
plex_id = plex_db.getItem_byKodiId(kodi_id, kodi_type)
|
||||
if plex_id:
|
||||
plex_ids.append(plex_id[0])
|
||||
return plex_ids
|
||||
|
||||
|
||||
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,20 +7,18 @@ 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+$''')
|
||||
# Avoid endless loops. Store the Kodi paths
|
||||
IGNORE_KODI_PLAYLIST_CHANGE = list()
|
||||
|
||||
###############################################################################
|
||||
|
||||
REGEX_FILE_NUMBERING = re.compile(r'''_(\d+)\.\w+$''')
|
||||
|
||||
|
||||
def create(plex_id):
|
||||
"""
|
||||
|
@ -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()
|
||||
|
@ -55,10 +53,6 @@ def create(plex_id):
|
|||
'%s_01.m3u' % name[:min(len(name), 248)])
|
||||
else:
|
||||
number = int(occurance.group(1)) + 1
|
||||
if number > 3:
|
||||
LOG.warn('Detected spanning tree issue, abort sync for %s',
|
||||
playlist)
|
||||
raise PlaylistError('Spanning tree warning')
|
||||
basename = re.sub(REGEX_FILE_NUMBERING, '', path)
|
||||
path = '%s_%02d.m3u' % (basename, number)
|
||||
LOG.debug('Kodi playlist path: %s', path)
|
||||
|
@ -67,13 +61,8 @@ def create(plex_id):
|
|||
if xml_playlist is None:
|
||||
LOG.error('Could not get Plex playlist %s', plex_id)
|
||||
raise PlaylistError('Could not get Plex playlist %s' % plex_id)
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.append(playlist.kodi_path)
|
||||
try:
|
||||
_write_playlist_to_file(playlist, xml_playlist)
|
||||
except Exception:
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.remove(playlist.kodi_path)
|
||||
raise
|
||||
playlist.kodi_hash = kodi_playlist_hash(path)
|
||||
_write_playlist_to_file(playlist, xml_playlist)
|
||||
playlist.kodi_hash = utils.generate_file_md5(path)
|
||||
db.update_playlist(playlist)
|
||||
LOG.debug('Created Kodi playlist based on Plex playlist: %s', playlist)
|
||||
|
||||
|
@ -86,41 +75,45 @@ def delete(playlist):
|
|||
Returns None or raises PlaylistError
|
||||
"""
|
||||
if path_ops.exists(playlist.kodi_path):
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.append(playlist.kodi_path)
|
||||
try:
|
||||
path_ops.remove(playlist.kodi_path)
|
||||
LOG.debug('Deleted Kodi playlist: %s', playlist)
|
||||
except (OSError, IOError) as err:
|
||||
LOG.error('Could not delete Kodi playlist file %s. Error:\n%s: %s',
|
||||
playlist, err.errno, err.strerror)
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.remove(playlist.kodi_path)
|
||||
raise PlaylistError('Could not delete %s' % playlist.kodi_path)
|
||||
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_id, episode_id = api.episode_data()
|
||||
try:
|
||||
season_id = int(season_id)
|
||||
episode_id = int(episode_id)
|
||||
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_id, episode_id,
|
||||
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 +124,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,14 +6,11 @@ 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
|
||||
# will have to be deleted first. Add Plex ids!
|
||||
IGNORE_PLEX_PLAYLIST_CHANGE = list()
|
||||
|
||||
###############################################################################
|
||||
|
||||
|
||||
|
@ -31,8 +28,14 @@ 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)
|
||||
for pos, plex_id in enumerate(plex_ids):
|
||||
try:
|
||||
if pos == 0 or not playlist.plex_id:
|
||||
pms.initialize(playlist, plex_id)
|
||||
else:
|
||||
pms.add_item(playlist, plex_id=plex_id)
|
||||
except PlaylistError:
|
||||
continue
|
||||
db.update_playlist(playlist)
|
||||
LOG.debug('Done creating Plex playlist %s', playlist)
|
||||
|
||||
|
@ -44,6 +47,5 @@ def delete(playlist):
|
|||
Returns None or raises PlaylistError
|
||||
"""
|
||||
LOG.debug('Deleting playlist from PMS: %s', playlist)
|
||||
IGNORE_PLEX_PLAYLIST_CHANGE.append(playlist.plex_id)
|
||||
pms.delete(playlist)
|
||||
db.update_playlist(playlist, delete=True)
|
||||
|
|
|
@ -6,12 +6,13 @@ manipulate playlists
|
|||
"""
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import urllib
|
||||
|
||||
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
|
||||
|
||||
from .. import variables as v
|
||||
###############################################################################
|
||||
LOG = getLogger('PLEX.playlists.pms')
|
||||
|
||||
|
@ -55,8 +56,8 @@ def initialize(playlist, plex_id):
|
|||
'type': v.PLEX_PLAYLIST_TYPE_FROM_KODI[playlist.kodi_type],
|
||||
'title': playlist.plex_name,
|
||||
'smart': 0,
|
||||
'uri': ('library://None/item/%s' % (utils.quote('/library/metadata/%s'
|
||||
% plex_id, safe='')))
|
||||
'uri': ('library://None/item/%s' % (urllib.quote('/library/metadata/%s'
|
||||
% plex_id, safe='')))
|
||||
}
|
||||
xml = DU().downloadUrl(url='{server}/playlists',
|
||||
action_type='POST',
|
||||
|
@ -68,7 +69,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()
|
||||
|
||||
|
||||
|
@ -79,8 +80,8 @@ def add_item(playlist, plex_id):
|
|||
Raises PlaylistError if that did not work out.
|
||||
"""
|
||||
params = {
|
||||
'uri': ('library://None/item/%s' % (utils.quote('/library/metadata/%s'
|
||||
% plex_id, safe='')))
|
||||
'uri': ('library://None/item/%s' % (urllib.quote('/library/metadata/%s'
|
||||
% plex_id, safe='')))
|
||||
}
|
||||
xml = DU().downloadUrl(url='{server}/playlists/%s/items' % playlist.plex_id,
|
||||
action_type='PUT',
|
||||
|
@ -96,35 +97,6 @@ def add_item(playlist, plex_id):
|
|||
playlist.plex_updatedat = api.updated_at()
|
||||
|
||||
|
||||
def add_items(playlist, plex_ids):
|
||||
"""
|
||||
Adds all plex_ids (a list of ints) to a new Plex playlist.
|
||||
Will set playlist.plex_updatedat
|
||||
Raises PlaylistError if that did not work out.
|
||||
"""
|
||||
params = {
|
||||
'type': v.PLEX_PLAYLIST_TYPE_FROM_KODI[playlist.kodi_type],
|
||||
'title': playlist.plex_name,
|
||||
'smart': 0,
|
||||
'uri': ('server://%s/com.plexapp.plugins.library/library/metadata/%s'
|
||||
% (app.CONN.machine_identifier,
|
||||
','.join(unicode(x) for x in plex_ids)))
|
||||
}
|
||||
xml = DU().downloadUrl(url='{server}/playlists/',
|
||||
action_type='POST',
|
||||
parameters=params)
|
||||
try:
|
||||
xml[0].attrib
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
LOG.error('Could not add items to a new playlist %s on Plex side',
|
||||
playlist)
|
||||
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_updatedat = api.updated_at()
|
||||
|
||||
|
||||
def metadata(plex_id):
|
||||
"""
|
||||
Returns an xml with the entire metadata like updatedAt.
|
||||
|
|
|
@ -5,14 +5,16 @@ Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly
|
|||
"""
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import copy
|
||||
|
||||
from threading import Thread
|
||||
import xbmc
|
||||
|
||||
from . import utils
|
||||
from . import playlist_func as PL
|
||||
from . import plex_functions as PF
|
||||
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
|
||||
from . import json_rpc as js
|
||||
from . import variables as v
|
||||
from . import state
|
||||
|
||||
###############################################################################
|
||||
LOG = getLogger('PLEX.playqueue')
|
||||
|
@ -33,7 +35,7 @@ def init_playqueues():
|
|||
LOG.debug('Playqueues have already been initialized')
|
||||
return
|
||||
# Initialize Kodi playqueues
|
||||
with app.APP.lock_playqueues:
|
||||
with state.LOCK_PLAYQUEUES:
|
||||
for i in (0, 1, 2):
|
||||
# Just in case the Kodi response is not sorted correctly
|
||||
for queue in js.get_playlists():
|
||||
|
@ -87,31 +89,25 @@ def init_playqueue_from_plex_children(plex_id, transient_token=None):
|
|||
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)
|
||||
PL.add_item_to_playlist(playqueue, i, plex_id=api.plex_id())
|
||||
playqueue.plex_transient_token = transient_token
|
||||
LOG.debug('Firing up Kodi player')
|
||||
app.APP.player.play(playqueue.kodi_pl, None, False, 0)
|
||||
xbmc.Player().play(playqueue.kodi_pl, None, False, 0)
|
||||
return playqueue
|
||||
|
||||
|
||||
class PlayqueueMonitor(backgroundthread.KillableThread):
|
||||
@utils.thread_methods(add_suspends=['PMS_STATUS'])
|
||||
class PlayqueueMonitor(Thread):
|
||||
"""
|
||||
Unfortunately, Kodi does not tell if items within a Kodi playqueue
|
||||
(playlist) are swapped. This is what this monitor is for. Don't replace
|
||||
this mechanism till Kodi's implementation of playlists has improved
|
||||
"""
|
||||
def _compare_playqueues(self, playqueue, new_kodi_playqueue):
|
||||
def _compare_playqueues(self, playqueue, new):
|
||||
"""
|
||||
Used to poll the Kodi playqueue and update the Plex playqueue if needed
|
||||
"""
|
||||
old = list(playqueue.items)
|
||||
# We might append to new_kodi_playqueue but will need the original
|
||||
# still back in the main loop
|
||||
new = copy.deepcopy(new_kodi_playqueue)
|
||||
index = list(range(0, len(old)))
|
||||
LOG.debug('Comparing new Kodi playqueue %s with our play queue %s',
|
||||
new, old)
|
||||
|
@ -121,7 +117,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.stopped():
|
||||
# Chances are that we got an empty Kodi playlist due to
|
||||
# Kodi exit
|
||||
return
|
||||
|
@ -138,7 +134,7 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
|
|||
old_item.kodi_type == new_item['type'])
|
||||
else:
|
||||
try:
|
||||
plex_id = int(utils.REGEX_PLEX_ID.findall(new_item['file'])[0])
|
||||
plex_id = utils.REGEX_PLEX_ID.findall(new_item['file'])[0]
|
||||
except IndexError:
|
||||
LOG.debug('Comparing paths directly as a fallback')
|
||||
identical = old_item.file == new_item['file']
|
||||
|
@ -152,7 +148,7 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
|
|||
i + j, i)
|
||||
try:
|
||||
PL.move_playlist_item(playqueue, i + j, i)
|
||||
except exceptions.PlaylistError:
|
||||
except PL.PlaylistError:
|
||||
LOG.error('Could not modify playqueue positions')
|
||||
LOG.error('This is likely caused by mixing audio and '
|
||||
'video tracks in the Kodi playqueue')
|
||||
|
@ -168,17 +164,9 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
|
|||
PL.add_item_to_plex_playqueue(playqueue,
|
||||
i,
|
||||
kodi_item=new_item)
|
||||
except exceptions.PlaylistError:
|
||||
except PL.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
|
||||
except IndexError:
|
||||
# This is really a hack - happens when using Addon Paths
|
||||
# and repeatedly starting the same element. Kodi will then
|
||||
|
@ -190,39 +178,34 @@ 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.stopped():
|
||||
# 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:
|
||||
except PL.PlaylistError:
|
||||
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')
|
||||
LOG.debug('Done comparing playqueues')
|
||||
|
||||
def run(self):
|
||||
stopped = self.stopped
|
||||
suspended = self.suspended
|
||||
LOG.info("----===## Starting PlayqueueMonitor ##===----")
|
||||
app.APP.register_thread(self)
|
||||
try:
|
||||
self._run()
|
||||
finally:
|
||||
app.APP.deregister_thread(self)
|
||||
LOG.info("----===## PlayqueueMonitor stopped ##===----")
|
||||
|
||||
def _run(self):
|
||||
while not self.should_cancel():
|
||||
if self.should_suspend():
|
||||
if self.wait_while_suspended():
|
||||
return
|
||||
with app.APP.lock_playqueues:
|
||||
while not stopped():
|
||||
while suspended():
|
||||
if stopped():
|
||||
break
|
||||
xbmc.sleep(1000)
|
||||
with state.LOCK_PLAYQUEUES:
|
||||
for playqueue in PLAYQUEUES:
|
||||
kodi_pl = js.playlist_get_items(playqueue.playlistid)
|
||||
if playqueue.old_kodi_pl != kodi_pl:
|
||||
if playqueue.id is None and (not app.SYNC.direct_paths or
|
||||
app.PLAYSTATE.context_menu_play):
|
||||
if playqueue.id is None and (not state.DIRECT_PATHS or
|
||||
state.CONTEXT_MENU_PLAY):
|
||||
# Only initialize if directly fired up using direct
|
||||
# paths. Otherwise let default.py do its magic
|
||||
LOG.debug('Not yet initiating playback')
|
||||
|
@ -230,4 +213,5 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
|
|||
# compare old and new playqueue
|
||||
self._compare_playqueues(playqueue, kodi_pl)
|
||||
playqueue.old_kodi_pl = list(kodi_pl)
|
||||
self.sleep(0.2)
|
||||
xbmc.sleep(200)
|
||||
LOG.info("----===## PlayqueueMonitor stopped ##===----")
|
||||
|
|
365
resources/lib/playutils.py
Normal file
365
resources/lib/playutils.py
Normal file
|
@ -0,0 +1,365 @@
|
|||
#!/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
|
||||
from . import variables as v
|
||||
|
||||
###############################################################################
|
||||
LOG = getLogger('PLEX.playutils')
|
||||
###############################################################################
|
||||
|
||||
|
||||
class PlayUtils():
|
||||
|
||||
def __init__(self, api, playqueue_item):
|
||||
"""
|
||||
init with api (PlexAPI wrapper of the PMS xml element) and
|
||||
playqueue_item (Playlist_Item())
|
||||
"""
|
||||
self.api = api
|
||||
self.item = playqueue_item
|
||||
|
||||
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):
|
||||
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
|
||||
|
||||
def get_max_bitrate(self):
|
||||
# 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)
|
||||
|
||||
def getH265(self):
|
||||
"""
|
||||
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')]
|
||||
|
||||
def get_bitrate(self):
|
||||
"""
|
||||
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)
|
||||
|
||||
def get_resolution(self):
|
||||
"""
|
||||
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" % (utils.window('pms_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)
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue