Compare commits
219 commits
py3-add-mo
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
ee1eb14476 | ||
|
9e54f59fd4 | ||
|
4cfcc4c1f8 | ||
|
5a6623a1dc | ||
|
e5fa5de670 | ||
|
191a3131e3 | ||
|
f96c246244 | ||
|
a4bf3d061a | ||
|
24c1ada5b1 | ||
|
61114e0d2e | ||
|
bdc98d0352 | ||
|
436d2e4391 | ||
|
2bc98f9ff1 | ||
|
a4fba553f3 | ||
|
097fd4cfa2 | ||
|
d80d3525b3 | ||
|
d54307ffd5 | ||
|
53e3258517 | ||
|
dae123acee | ||
|
f4a0789fc0 | ||
|
887f659b2f | ||
|
2bd692e173 | ||
|
176fa07e80 | ||
|
9da61a059f | ||
|
11d06d909e | ||
|
e96df700c1 | ||
|
057921b05e | ||
|
63bd85d5c8 | ||
|
9d6bae3957 | ||
|
2432ce5ee6 | ||
|
5a009b7ea0 | ||
|
26073e5dac | ||
|
47cd15baa0 | ||
|
560fc5b9c8 | ||
|
9495e1e27d | ||
|
5720811a7e | ||
|
45afba1840 | ||
|
cb1a3e74e0 | ||
|
76c4fba8e6 | ||
|
516a09ce56 | ||
|
41855882ab | ||
|
289266bb81 | ||
|
0490ce766e | ||
|
c182b8f5f8 | ||
|
e6a0af4621 | ||
|
ea877b55d5 | ||
|
7c2478a568 | ||
|
c99db1edff | ||
|
2f25ba2eae | ||
|
7602f02bcd | ||
|
de9c935a40 | ||
|
e049f37da9 | ||
|
ce6ab2c258 | ||
|
0f7410e0e3 | ||
|
4de0920bf5 | ||
|
2b9594dd90 | ||
|
4f75502a8a | ||
|
6bf41116cb | ||
|
6201a04513 | ||
|
74ec9eff97 | ||
|
295f403c64 | ||
|
cb8dc30c7c | ||
|
262315c3e7 | ||
|
a18b971564 | ||
|
1bd1da9f5a | ||
|
2fd91ff9d6 | ||
|
1001df5e30 | ||
|
0f2fd110db | ||
|
ada337c2c4 | ||
|
1066f857a2 | ||
|
858a33f816 | ||
|
fce964cc7b | ||
|
7c903d0c94 | ||
|
3ff97d0669 | ||
|
7553061945 | ||
|
6105a571c8 | ||
|
2484cf10ac | ||
|
f171785602 | ||
|
3e9c8c6361 | ||
|
cac32cc66a | ||
|
c4d14c02e2 | ||
|
c6056b4efc | ||
|
2c979fba57 | ||
|
f877c37e76 | ||
|
038960c538 | ||
|
cdf1514215 | ||
|
f15ef8886a | ||
|
7f8339a753 | ||
|
0cf35b7b87 | ||
|
09b0c61f11 | ||
|
675a8150cc | ||
|
a2194a5ce8 | ||
|
166b94c4cd | ||
|
46f99901cc | ||
|
36befcf46a | ||
|
abd8b04ff9 | ||
|
dbf2117a30 | ||
|
a2e08a30ec | ||
|
d38fe789b3 | ||
|
f262fba18a | ||
|
29822db781 | ||
|
7c12b7aa36 | ||
|
019bd1aeae | ||
|
c29be48cac | ||
|
4916bbb46e | ||
|
f7ae807167 | ||
|
966cf6f526 | ||
|
ce14d394d4 | ||
|
46f115de68 | ||
|
e98aca1f00 | ||
|
cb6ba50904 | ||
|
2d02f4af07 | ||
|
1493ac0c58 | ||
|
7c57dca0ec | ||
|
04e2d09835 | ||
|
fbfcffbb0c | ||
|
6b6464dac3 | ||
|
34045c0136 | ||
|
1c4b15e357 | ||
|
3d139b0929 | ||
|
4c0634bc13 | ||
|
060880e754 | ||
|
95758b5dc8 | ||
|
3d7d2d0993 | ||
|
808136bff8 | ||
|
98b6b681fd | ||
|
0a1edcd24a | ||
|
c8caf2f11b | ||
|
4bef20da32 | ||
|
886d2e5df7 | ||
|
f6c2a7c08f | ||
|
0fd7d11631 | ||
|
c69d131084 | ||
|
dc5402abcc | ||
|
9d7d33c0d0 | ||
|
1885d3fc94 | ||
|
bb7b2de44b | ||
|
f134266efc | ||
|
66771c53a2 | ||
|
16cbe430af | ||
|
8aa5890e67 | ||
|
12587a985c | ||
|
9150e168f6 | ||
|
a12e07da6a | ||
|
fad755745a | ||
|
07ed0d1105 | ||
|
f524018160 | ||
|
cf6a301d70 | ||
|
09d4ed597b | ||
|
faf8575537 | ||
|
c4cfdddb91 | ||
|
f469627d33 | ||
|
8bccff05b6 | ||
|
08bbf38128 | ||
|
474e4ac5d1 | ||
|
10326882bd | ||
|
e980de05a8 | ||
|
0051ed316e | ||
|
c79938e08b | ||
|
3e1f52802f | ||
|
06a20a8358 | ||
|
31549a1ffb | ||
|
a3d654c65c | ||
|
dad8d58824 | ||
|
e5585aec44 | ||
|
a7ffceb631 | ||
|
538832bed5 | ||
|
94e474513c | ||
|
1b56f5cef9 | ||
|
f192c0912c | ||
|
269dedf398 | ||
|
63144ba070 | ||
|
625d4c91b4 | ||
|
011d20473e | ||
|
2884054fd4 | ||
|
01fb1d5da6 | ||
|
f187111411 | ||
|
281c7d1599 | ||
|
89afd46b56 | ||
|
94a86b43c1 | ||
|
7393023fcc | ||
|
f544c4065f | ||
|
8ae2fdc10a | ||
|
b9c1aaac20 | ||
|
e887e7162b | ||
|
b2139ce150 | ||
|
22efe274a1 | ||
|
828a580031 | ||
|
151e3a5eef | ||
|
939cdd4615 | ||
|
a867acb0f8 | ||
|
acf446dcc0 | ||
|
70e6e4350e | ||
|
86dab2ab66 | ||
|
4bae675181 | ||
|
250859d3a7 | ||
|
3cc939f320 | ||
|
aac16f38b3 | ||
|
5014a0fafa | ||
|
7cf8cb59f1 | ||
|
a648d8941a | ||
|
d1fdf5d25f | ||
|
5eb1c2aacd | ||
|
9e0ac64bb9 | ||
|
5816235062 | ||
|
0982c3bae2 | ||
|
17d84c1f29 | ||
|
d096854b14 | ||
|
ddf8637bb6 | ||
|
089294681e | ||
|
a0280fdbd3 | ||
|
e2ebe98fde | ||
|
e60816c022 | ||
|
fb53ba3a0a | ||
|
27202d2ab2 | ||
|
ba6c46afac | ||
|
941ac4ef3b | ||
|
493ac7f49a | ||
|
d8dc959879 |
105 changed files with 12658 additions and 2504 deletions
18
README.md
18
README.md
|
@ -1,5 +1,7 @@
|
||||||
[![stable version](https://img.shields.io/badge/stable_version-2.12.3-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip)
|
[![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)
|
||||||
[![beta version](https://img.shields.io/badge/beta_version-2.12.5-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.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)
|
||||||
|
|
||||||
[![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation)
|
[![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)
|
[![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq)
|
||||||
|
@ -37,11 +39,7 @@ Unfortunately, the PKC Kodi repository had to move because it stopped working (t
|
||||||
|
|
||||||
### Download and Installation
|
### Download and Installation
|
||||||
|
|
||||||
Install PKC via the PlexKodiConnect Kodi repository download button just below (do NOT use the standard GitHub download!). Alternatively, add [https://croneter.github.io/pkc-source](https://croneter.github.io/pkc-source) as a new Kodi `Web server directory (HTTPS)` source. 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.
|
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.
|
||||||
|
|
||||||
| 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
|
### Warning
|
||||||
Use at your own risk! This plugin assumes that you manage all your videos with Plex (and none with Kodi). You might lose data already stored in the Kodi video and music databases as this plugin directly changes them. Don't worry if you want Plex to manage all your media (like you should ;-)).
|
Use at your own risk! This plugin assumes that you manage all your videos with Plex (and none with Kodi). You might lose data already stored in the Kodi video and music databases as this plugin directly changes them. Don't worry if you want Plex to manage all your media (like you should ;-)).
|
||||||
|
@ -50,8 +48,9 @@ Some people argue that PKC is 'hacky' because of the way it directly accesses th
|
||||||
|
|
||||||
### PKC Features
|
### PKC Features
|
||||||
|
|
||||||
- Kodi 19 Matrix is not yet supported (PKC is written in Python 2)
|
- Support for Kodi 18 Leia and Kodi 19 Matrix
|
||||||
- Support for Kodi 18 Leia
|
- 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/)
|
||||||
- [Amazon Alexa voice recognition](https://www.plex.tv/apps/streaming-devices/amazon-alexa)
|
- [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/)
|
- [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-)
|
- [Plex Watch Later / Plex It!](https://support.plex.tv/hc/en-us/sections/200211783-Plex-It-)
|
||||||
|
@ -79,6 +78,7 @@ Some people argue that PKC is 'hacky' because of the way it directly accesses th
|
||||||
+ Hungarian, thanks @savage93
|
+ Hungarian, thanks @savage93
|
||||||
+ Ukrainian, thanks @uniss
|
+ Ukrainian, thanks @uniss
|
||||||
+ Lithuanian, thanks @egidusm
|
+ Lithuanian, thanks @egidusm
|
||||||
|
+ Korean, thanks @so-o-bima
|
||||||
|
|
||||||
### Additional Artwork
|
### Additional Artwork
|
||||||
PKC uses additional artwork for free from [TheMovieDB](https://www.themoviedb.org). Many thanks for lettings us use the API, guys!
|
PKC uses additional artwork for free from [TheMovieDB](https://www.themoviedb.org). Many thanks for lettings us use the API, guys!
|
||||||
|
|
901
addon.xml
901
addon.xml
|
@ -1,9 +1,10 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.12.5" provider-name="croneter">
|
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.15.0" provider-name="croneter">
|
||||||
<requires>
|
<requires>
|
||||||
<import addon="xbmc.python" version="2.1.0"/>
|
<import addon="xbmc.python" version="2.1.0"/>
|
||||||
<import addon="script.module.requests" version="2.9.1" />
|
<import addon="script.module.requests" version="2.9.1" />
|
||||||
<import addon="script.module.defusedxml" version="0.5.0"/>
|
<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.movies" version="2.1.3" />
|
||||||
<import addon="plugin.video.plexkodiconnect.tvshows" version="2.1.3" />
|
<import addon="plugin.video.plexkodiconnect.tvshows" version="2.1.3" />
|
||||||
</requires>
|
</requires>
|
||||||
|
@ -77,13 +78,142 @@
|
||||||
<summary lang="uk_UA">Нативна інтеграція Plex в Kodi</summary>
|
<summary lang="uk_UA">Нативна інтеграція Plex в Kodi</summary>
|
||||||
<description lang="uk_UA">Підключає Kodi до серверу Plex. Цей плагін передбачає, що ви керуєте всіма своїми відео за допомогою Plex (і ніяк не Kodi). Ви можете втратити дані, які вже зберігаються у відео та музичних БД Kodi (оскільки цей плагін безпосередньо їх змінює). Використовуйте на свій страх і ризик!</description>
|
<description lang="uk_UA">Підключає Kodi до серверу Plex. Цей плагін передбачає, що ви керуєте всіма своїми відео за допомогою Plex (і ніяк не Kodi). Ви можете втратити дані, які вже зберігаються у відео та музичних БД Kodi (оскільки цей плагін безпосередньо їх змінює). Використовуйте на свій страх і ризик!</description>
|
||||||
<disclaimer lang="uk_UA">Використовуйте на свій ризик</disclaimer>
|
<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>
|
<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>
|
<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>
|
<disclaimer lang="sv_SE">Använd på egen risk</disclaimer>
|
||||||
<summary lang="lt_LT">Natūralioji „Plex“ integracija į „Kodi“</summary>
|
<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>
|
<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>
|
<disclaimer lang="lt_LT">Naudokite savo pačių rizika</disclaimer>
|
||||||
<news>version 2.12.5 (beta only):
|
<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]
|
||||||
|
|
||||||
|
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:
|
||||||
|
- 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
|
- 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 high transcoding resolutions not being available for Win10
|
||||||
- Fix rare playback progress report failing and KeyError: u'containerKey'
|
- Fix rare playback progress report failing and KeyError: u'containerKey'
|
||||||
|
@ -145,771 +275,6 @@ version 2.11.0 (beta only):
|
||||||
- Ensure that our only video transcoding target is h264
|
- Ensure that our only video transcoding target is h264
|
||||||
- Fix adjusted subtitle size not working when burning in subtitles
|
- 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
|
- Fix regression: burn-in subtitles picking up the last user setting instead of the current one
|
||||||
|
</news>
|
||||||
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:
|
|
||||||
- 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
|
|
||||||
- Make sure we retain a dummy first music artist entry
|
|
||||||
- Increase logging
|
|
||||||
|
|
||||||
version 2.4.5 (beta only):
|
|
||||||
- Fix playback not starting up at all
|
|
||||||
- Rewire Kodi library refreshs
|
|
||||||
- Wipe Kodi database on first PKC run to more reliably install PKC
|
|
||||||
|
|
||||||
version 2.4.4 (beta only):
|
|
||||||
- Fix rare case when playback would not start-up
|
|
||||||
- Increase logging
|
|
||||||
|
|
||||||
version 2.4.3:
|
|
||||||
- Fix Kodi addons throwing jsonrpc errors (database reset needed)
|
|
||||||
|
|
||||||
version 2.4.2:
|
|
||||||
- Make version 2.4.1 available for everyone
|
|
||||||
|
|
||||||
version 2.4.1 (beta only):
|
|
||||||
- Hopefully fix endless playlist sync loops
|
|
||||||
- Ensure shows are deleted before seasons before episodes
|
|
||||||
- Fix library sync crash on deleting episode with missing season
|
|
||||||
- Fix numbering of already existing playlist files
|
|
||||||
- Optimize logging
|
|
||||||
|
|
||||||
version 2.4.0:
|
|
||||||
- Use pretty Plex dialogs for everyone!
|
|
||||||
|
|
||||||
version 2.3.14 (beta only):
|
|
||||||
- Fix AttributeError on forcing texture caching
|
|
||||||
- Switch to Plex style dialogs
|
|
||||||
- Include PKC info in plex.tv dialogs
|
|
||||||
- Include PKC info in user selection dialog
|
|
||||||
|
|
||||||
version 2.3.13 (beta only):
|
|
||||||
- Pretty Plex dialogs for plex.tv sign-in and user selection
|
|
||||||
- Fix UnicodeDecodeError for PMS with non ASCII chars on local LAN discovery
|
|
||||||
- Fix add-on settings not opening on installation
|
|
||||||
- Greatly speed up deleting of items on the Kodi side
|
|
||||||
- Safely parse XMLs using defusedxml
|
|
||||||
- Fix PKC trying to sync audio playlists even when audio sync disabled
|
|
||||||
- Some code cleanup
|
|
||||||
|
|
||||||
version 2.3.12:
|
|
||||||
- Fix Kodi hanging if media stream selection is aborted
|
|
||||||
- Fix potential sync crash
|
|
||||||
- Revert "Fix Kodi crash by committing to DB frequently"
|
|
||||||
|
|
||||||
version 2.3.11 (beta only):
|
|
||||||
- Fix Kodi crash by committing to DB frequently
|
|
||||||
|
|
||||||
version 2.3.10:
|
|
||||||
- Compatibility with Kodi 18 Leia Beta 1
|
|
||||||
- Update translations
|
|
||||||
- Make version 2.3.9 available for everyone
|
|
||||||
|
|
||||||
version 2.3.9 (beta only):
|
|
||||||
- Fix playback not resuming (Kodi 18 ignores listitem "StartOffset")
|
|
||||||
- Fix playerid not being retrieved for Kodi 18
|
|
||||||
- Prefer local trailers; new setting to list extras instead of playing trailer
|
|
||||||
|
|
||||||
version 2.3.8:
|
|
||||||
- Fix typo
|
|
||||||
- Make version 2.3.4-2.3.7 available for everyone
|
|
||||||
|
|
||||||
version 2.3.7 (beta only):
|
|
||||||
- Fix library sync crash due to exotic playlist characters
|
|
||||||
- Force-deactivate playlist sync for Microsoft UWP for Kodi 18
|
|
||||||
|
|
||||||
version 2.3.6 (beta only):
|
|
||||||
- Fix PKC not starting by decoupling watchdog/subprocess modules
|
|
||||||
|
|
||||||
version 2.3.5 (beta only):
|
|
||||||
- Fix PKC not starting by importing playlist module only when sync enabled
|
|
||||||
|
|
||||||
version 2.3.4 (beta only):
|
|
||||||
- Fix playback sometimes not starting and UnicodeEncodeError for logging
|
|
||||||
|
|
||||||
version 2.3.3:
|
|
||||||
- Choose trailer if several are present (DB reset required)
|
|
||||||
|
|
||||||
version 2.3.2:
|
|
||||||
- Fix casting to PKC failing
|
|
||||||
|
|
||||||
version 2.3.1:
|
|
||||||
- Fix library sync crashing due to Plex photo albums
|
|
||||||
|
|
||||||
version 2.3.0:
|
|
||||||
Major stable version bump. Highlights:
|
|
||||||
- Sync Plex playlists to Kodi and Kodi playlists to Plex!
|
|
||||||
- Support for Plex collection/set artwork
|
|
||||||
- Many bug fixes, especially Plex Companion
|
|
||||||
- Tons of code improvements in the hope that someone else will help with developing PKC
|
|
||||||
Warning: the 2 helper add-ons for movies and tv shows also received an upgrade from 2.0.4 to 2.0.5. If you want to downgrade PKC, be sure to downgrade these add-ons as well!
|
|
||||||
|
|
||||||
version 2.2.18 (beta only):
|
|
||||||
- Fix PKC tv show node "all"
|
|
||||||
- Move PKC playlist shortcut
|
|
||||||
|
|
||||||
version 2.2.17 (beta only):
|
|
||||||
- Access Plex Hubs. Listing will be different depending on Kodi section!
|
|
||||||
- Fix year for songs missing
|
|
||||||
- Fix Plex extras not playing
|
|
||||||
- Fix rare library sync crash
|
|
||||||
|
|
||||||
version 2.2.16 (beta only):
|
|
||||||
- Enable Kodi libraries for Plex Music libraries
|
|
||||||
- New Playlists menu item for video libraries
|
|
||||||
- Only show Plex libraries in the applicable Kodi media category
|
|
||||||
- Optimize code
|
|
||||||
|
|
||||||
version 2.2.15 (beta only):
|
|
||||||
- Fix ImportError on first PKC run
|
|
||||||
|
|
||||||
version 2.2.14 (beta only):
|
|
||||||
- Hopefully fix playlist sync loops
|
|
||||||
|
|
||||||
version 2.2.13 (beta only):
|
|
||||||
- Fix library sync crash
|
|
||||||
- Fix switching to __future__ module
|
|
||||||
- Fix "Prefer Kodi Artwork" toggle doing the exact opposite
|
|
||||||
- Fix "Prefer Kodi artwork" setting not being visible
|
|
||||||
|
|
||||||
version 2.2.12 (beta only):
|
|
||||||
- Fix slow sync. Fix endless sync of corrupted PMS elements
|
|
||||||
- Refactor playlist code
|
|
||||||
- Fix FutureWarning
|
|
||||||
|
|
||||||
version 2.2.11 (beta only):
|
|
||||||
- Fix OnDeck widget for Direct Paths
|
|
||||||
- Fix Plex Companion crashing when connected to Plex Web
|
|
||||||
- Fix Plex Companion crash when connected to Plex Web playing playlist music
|
|
||||||
- Improve Plex playback report when playing music playlist
|
|
||||||
- Improve reliability in Kodi song playback
|
|
||||||
- Catch some errors if user mixes audio and video in Kodi playqueue
|
|
||||||
|
|
||||||
version 2.2.10 (beta only):
|
|
||||||
- Fix playlists getting recreated and deleted in an endless loop
|
|
||||||
- Add some safety nets for playlist sync
|
|
||||||
- Fix FutureWarning
|
|
||||||
- Fix playlist sync settings not disappearing
|
|
||||||
- Optimize code
|
|
||||||
|
|
||||||
version 2.2.9 (beta only):
|
|
||||||
- Hopefully fix Kodi and Plex playlists getting out of sync
|
|
||||||
- Fix and optimize startup of playlist sync
|
|
||||||
- Hide certain playlist settings under certain conditions
|
|
||||||
- Fix errors in Kodi log
|
|
||||||
|
|
||||||
version 2.2.8 (beta only):
|
|
||||||
- Support for Plex collection artwork (PKC settings toggle under Artwork)
|
|
||||||
- Fix hard PKC reset not working (OSError: no such file)
|
|
||||||
- Deduplication
|
|
||||||
- Catch exception
|
|
||||||
- Update translations
|
|
||||||
- Extend Kodi metadata
|
|
||||||
- Update readme
|
|
||||||
|
|
||||||
version 2.2.7 (beta only):
|
|
||||||
- Allow to only sync specific Plex or Kodi playlists
|
|
||||||
- Don't show artwork sync progress, reduce setting-writes
|
|
||||||
- Fix playback sometimes not starting up
|
|
||||||
- Use __future__ for contextmenu.py
|
|
||||||
- Fix imports
|
|
||||||
|
|
||||||
version 2.2.6 (beta only):
|
|
||||||
- Fix default settings string, only show in English, hopefully fixes PKC loosing its settings
|
|
||||||
|
|
||||||
version 2.2.5 (beta only):
|
|
||||||
- Fix AttributeError and add_update has crashed
|
|
||||||
|
|
||||||
version 2.2.4 (beta only):
|
|
||||||
- Fix LibrarySync crashing due to Plex Companion messages
|
|
||||||
|
|
||||||
version 2.2.3 (beta only):
|
|
||||||
- Compatibility with Kodi Krypton Alpha 2
|
|
||||||
- Append tv show and SxxExx to episode playlist entries
|
|
||||||
|
|
||||||
version 2.2.2 (beta only):
|
|
||||||
- Fixes to locking mechanisms which resulted in weird behavior in some cases
|
|
||||||
- Switch to Python __future__ unicode_literals and absolute paths
|
|
||||||
- Fix UnboundLocalError for playlists
|
|
||||||
- Check all Kodi database versions before starting PKC
|
|
||||||
- Fix KeyError on non-PKC playback startup
|
|
||||||
- Speed up subtitle download to Kodi
|
|
||||||
- Update translations
|
|
||||||
- PEP-8 stuff
|
|
||||||
|
|
||||||
version 2.2.1 (beta only):
|
|
||||||
- Fix library sync crash due to PMS sending string, not unicode
|
|
||||||
- Fix playback from playlists for add-on paths
|
|
||||||
- Detect playback from a Kodi playlist for add-on paths - because we need some hacks due to Kodi bugs
|
|
||||||
- Fix add-on paths playstate and Plex Companion for playlists
|
|
||||||
- Fix Kodi telling Plex companion false playqueue position
|
|
||||||
- Don't try to get a Kodi library items for Plex clips
|
|
||||||
- Update translations
|
|
||||||
|
|
||||||
version 2.2.0 (beta only):
|
|
||||||
- Support for syncing Plex playlists to Kodi and vice-versa! (Kodi mixed music and video playlists cannot be supported as Plex does not support them)
|
|
||||||
|
|
||||||
version 2.1.6:
|
|
||||||
- Fix slow sync. Fix endless sync of corrupted PMS elements
|
|
||||||
|
|
||||||
version 2.1.5:
|
|
||||||
- Fix OnDeck widget for Direct Paths
|
|
||||||
|
|
||||||
version 2.1.4:
|
|
||||||
- Fix PKC settings suddenly getting lost
|
|
||||||
- Don't show artwork sync progress, reduce setting-writes
|
|
||||||
|
|
||||||
version 2.1.3:
|
|
||||||
- Fix default settings string, only show in English, hopefully fixes PKC loosing its settings
|
|
||||||
|
|
||||||
version 2.1.2:
|
|
||||||
- Compatibility with Kodi Krypton Alpha 2
|
|
||||||
- Check all Kodi database versions before starting PKC
|
|
||||||
- Fix KeyError on non-PKC playback startup
|
|
||||||
- PEP-8 stuff
|
|
||||||
|
|
||||||
version 2.1.1:
|
|
||||||
- Fix Library Sync crash on Android
|
|
||||||
|
|
||||||
version 2.1.0:
|
|
||||||
Finally a new update for the stable version. You will need to reconnect to your PMS and reset the Kodi database once. Highlights of v2 include:
|
|
||||||
- Support for Plex extras
|
|
||||||
- Huge improvements to Plex Companion
|
|
||||||
- Fixes to Alexa voice control
|
|
||||||
- Kodi 18 Leia Alpha 1 support
|
|
||||||
- Improvements to playback start-up
|
|
||||||
- Improvements to the syncing mechanism, which should get rid of a ton of small bugs
|
|
||||||
- Fixes to widgets and resuming playback
|
|
||||||
- Use of plex.direct paths instead of local IP addresses to ensure the SSL certificates shown by the PMS are deemed valid
|
|
||||||
- Fix Kodi screensaver
|
|
||||||
- Faster PKC startup
|
|
||||||
- And tons of other stuff...</news>
|
|
||||||
</extension>
|
</extension>
|
||||||
</addon>
|
</addon>
|
||||||
|
|
125
changelog.txt
125
changelog.txt
|
@ -1,3 +1,128 @@
|
||||||
|
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):
|
version 2.12.5 (beta only):
|
||||||
- Greatly improve matching logic for The Movie Database if Plex does not provide an appropriate id
|
- 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 high transcoding resolutions not being available for Win10
|
||||||
|
|
|
@ -45,6 +45,13 @@ msgstr ""
|
||||||
"Varování: Máte v Kodi zapnuté nastavení \"Automaticky přehrát další video\"."
|
"Varování: Máte v Kodi zapnuté nastavení \"Automaticky přehrát další video\"."
|
||||||
" Toto může narušit funkčnost PKC. Deaktivovat?"
|
" 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"
|
msgctxt "#30005"
|
||||||
msgid "Username: "
|
msgid "Username: "
|
||||||
msgstr "Uživ. jméno: "
|
msgstr "Uživ. jméno: "
|
||||||
|
@ -161,6 +168,14 @@ msgctxt "#30028"
|
||||||
msgid "PKC-only image caching completed"
|
msgid "PKC-only image caching completed"
|
||||||
msgstr "Stahování obrázků PKC dokončeno"
|
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"
|
msgctxt "#30030"
|
||||||
msgid "Port Number"
|
msgid "Port Number"
|
||||||
msgstr "Číslo portu"
|
msgstr "Číslo portu"
|
||||||
|
@ -596,6 +611,11 @@ msgctxt "#30524"
|
||||||
msgid "Select Plex libraries to sync"
|
msgid "Select Plex libraries to sync"
|
||||||
msgstr "Zvolte knihovny Plexu k synchronizaci"
|
msgstr "Zvolte knihovny Plexu k synchronizaci"
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30525"
|
||||||
|
msgid "Skip intro"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30527"
|
msgctxt "#30527"
|
||||||
msgid "Ignore specials in next episodes"
|
msgid "Ignore specials in next episodes"
|
||||||
|
@ -660,8 +680,8 @@ msgstr "Stahovat obrázky filmových kolekcí z FanArtTV"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30541"
|
msgctxt "#30541"
|
||||||
msgid "Don't ask to pick a certain stream/quality"
|
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||||
msgstr "Nepožadovat výběr proudu nebo kvality"
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30542"
|
msgctxt "#30542"
|
||||||
|
@ -682,6 +702,21 @@ msgctxt "#30545"
|
||||||
msgid "Force transcode pictures"
|
msgid "Force transcode pictures"
|
||||||
msgstr "Vynutit překódování obrázků"
|
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
|
# Welcome to Plex notification
|
||||||
msgctxt "#33000"
|
msgctxt "#33000"
|
||||||
msgid "Welcome"
|
msgid "Welcome"
|
||||||
|
@ -950,6 +985,11 @@ msgctxt "#39036"
|
||||||
msgid "Escape special characters in path (e.g. space to %20)"
|
msgid "Escape special characters in path (e.g. space to %20)"
|
||||||
msgstr "Nahrazovat speciální znaky v cestě (např. z mezery na %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
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39037"
|
msgctxt "#39037"
|
||||||
msgid "Original Plex MOVIE path to replace:"
|
msgid "Original Plex MOVIE path to replace:"
|
||||||
|
@ -1133,6 +1173,11 @@ msgctxt "#39071"
|
||||||
msgid "Current plex.tv status:"
|
msgid "Current plex.tv status:"
|
||||||
msgstr "Současný stav plex.tv:"
|
msgstr "Současný stav plex.tv:"
|
||||||
|
|
||||||
|
# PKC Settings - Connection
|
||||||
|
msgctxt "#39072"
|
||||||
|
msgid "Background sync connection:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings, category name
|
# PKC Settings, category name
|
||||||
msgctxt "#39073"
|
msgctxt "#39073"
|
||||||
msgid "Appearance Tweaks"
|
msgid "Appearance Tweaks"
|
||||||
|
@ -1143,6 +1188,11 @@ msgctxt "#39074"
|
||||||
msgid "TV Shows"
|
msgid "TV Shows"
|
||||||
msgstr "Seriály"
|
msgstr "Seriály"
|
||||||
|
|
||||||
|
# PKC Settings - Sync
|
||||||
|
msgctxt "#39075"
|
||||||
|
msgid "Verify access to media files while synching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Pop-up during initial sync
|
# Pop-up during initial sync
|
||||||
msgctxt "#39076"
|
msgctxt "#39076"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1208,6 +1258,31 @@ msgctxt "#39085"
|
||||||
msgid "Reload Kodi node files to apply all the settings below"
|
msgid "Reload Kodi node files to apply all the settings below"
|
||||||
msgstr "Znovu načíst Kodi pro aplikování nastavení níže"
|
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"
|
msgctxt "#39200"
|
||||||
msgid "Log-out Plex Home User "
|
msgid "Log-out Plex Home User "
|
||||||
msgstr "Odhlásit uživatele Plex Home "
|
msgstr "Odhlásit uživatele Plex Home "
|
||||||
|
|
|
@ -46,6 +46,13 @@ msgstr ""
|
||||||
"Advarsel: Kodi indstillingen \"Afspil næste video automatisk\" er aktiveret."
|
"Advarsel: Kodi indstillingen \"Afspil næste video automatisk\" er aktiveret."
|
||||||
" Dette kan ødelægge PKC funktionalitet. Deaktiver? "
|
" Dette kan ødelægge PKC funktionalitet. Deaktiver? "
|
||||||
|
|
||||||
|
msgctxt "#30004"
|
||||||
|
msgid ""
|
||||||
|
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||||
|
"random password automatically if you haven't done so already. Please confirm"
|
||||||
|
" the next dialog that you want to enable the webserver now with Yes."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30005"
|
msgctxt "#30005"
|
||||||
msgid "Username: "
|
msgid "Username: "
|
||||||
msgstr "Brugernavn: "
|
msgstr "Brugernavn: "
|
||||||
|
@ -161,6 +168,14 @@ msgctxt "#30028"
|
||||||
msgid "PKC-only image caching completed"
|
msgid "PKC-only image caching completed"
|
||||||
msgstr "PKC billede caching er færdiggjort"
|
msgstr "PKC billede caching er færdiggjort"
|
||||||
|
|
||||||
|
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||||
|
msgctxt "#30029"
|
||||||
|
msgid ""
|
||||||
|
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
|
||||||
|
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
|
||||||
|
"database resets. Continue?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30030"
|
msgctxt "#30030"
|
||||||
msgid "Port Number"
|
msgid "Port Number"
|
||||||
msgstr "Portnummer"
|
msgstr "Portnummer"
|
||||||
|
@ -596,6 +611,11 @@ msgctxt "#30524"
|
||||||
msgid "Select Plex libraries to sync"
|
msgid "Select Plex libraries to sync"
|
||||||
msgstr "Vælg Plex biblioteker der skal synkroniseres"
|
msgstr "Vælg Plex biblioteker der skal synkroniseres"
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30525"
|
||||||
|
msgid "Skip intro"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30527"
|
msgctxt "#30527"
|
||||||
msgid "Ignore specials in next episodes"
|
msgid "Ignore specials in next episodes"
|
||||||
|
@ -660,8 +680,8 @@ msgstr "Download film sæt/samling info fra FanArtTV"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30541"
|
msgctxt "#30541"
|
||||||
msgid "Don't ask to pick a certain stream/quality"
|
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||||
msgstr "Spørg ikke at vælge en bestemt stream/kvalitet"
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30542"
|
msgctxt "#30542"
|
||||||
|
@ -682,6 +702,21 @@ msgctxt "#30545"
|
||||||
msgid "Force transcode pictures"
|
msgid "Force transcode pictures"
|
||||||
msgstr "Transcode billeder"
|
msgstr "Transcode billeder"
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30546"
|
||||||
|
msgid "Pick the first video if several versions are present"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30547"
|
||||||
|
msgid "Who picks the audio stream on playback start?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30548"
|
||||||
|
msgid "Who picks subtitles on playback start?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Welcome to Plex notification
|
# Welcome to Plex notification
|
||||||
msgctxt "#33000"
|
msgctxt "#33000"
|
||||||
msgid "Welcome"
|
msgid "Welcome"
|
||||||
|
@ -954,6 +989,11 @@ msgctxt "#39036"
|
||||||
msgid "Escape special characters in path (e.g. space to %20)"
|
msgid "Escape special characters in path (e.g. space to %20)"
|
||||||
msgstr "Escape special characters in path (e.g. space to %20)"
|
msgstr "Escape special characters in path (e.g. space to %20)"
|
||||||
|
|
||||||
|
# PKC Settings - Customize Paths
|
||||||
|
msgctxt "#39090"
|
||||||
|
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Customize Paths
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39037"
|
msgctxt "#39037"
|
||||||
msgid "Original Plex MOVIE path to replace:"
|
msgid "Original Plex MOVIE path to replace:"
|
||||||
|
@ -1136,6 +1176,11 @@ msgctxt "#39071"
|
||||||
msgid "Current plex.tv status:"
|
msgid "Current plex.tv status:"
|
||||||
msgstr "Nuværende plex.tv status:"
|
msgstr "Nuværende plex.tv status:"
|
||||||
|
|
||||||
|
# PKC Settings - Connection
|
||||||
|
msgctxt "#39072"
|
||||||
|
msgid "Background sync connection:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings, category name
|
# PKC Settings, category name
|
||||||
msgctxt "#39073"
|
msgctxt "#39073"
|
||||||
msgid "Appearance Tweaks"
|
msgid "Appearance Tweaks"
|
||||||
|
@ -1146,6 +1191,11 @@ msgctxt "#39074"
|
||||||
msgid "TV Shows"
|
msgid "TV Shows"
|
||||||
msgstr "TV-udsendelser"
|
msgstr "TV-udsendelser"
|
||||||
|
|
||||||
|
# PKC Settings - Sync
|
||||||
|
msgctxt "#39075"
|
||||||
|
msgid "Verify access to media files while synching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Pop-up during initial sync
|
# Pop-up during initial sync
|
||||||
msgctxt "#39076"
|
msgctxt "#39076"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1213,6 +1263,31 @@ msgstr ""
|
||||||
"Reload Kodi node filer for alle indstillinger\n"
|
"Reload Kodi node filer for alle indstillinger\n"
|
||||||
"nedeunder"
|
"nedeunder"
|
||||||
|
|
||||||
|
# PKC Settings - Connection - Background sync connection status
|
||||||
|
msgctxt "#39089"
|
||||||
|
msgid "Alexa connection status:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Connection - Background sync connection status
|
||||||
|
msgctxt "#39091"
|
||||||
|
msgid "Timeout - not connected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Connection - Background sync connection status
|
||||||
|
msgctxt "#39092"
|
||||||
|
msgid "IOError - not connected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Connection - Background sync connection status
|
||||||
|
msgctxt "#39093"
|
||||||
|
msgid "Suspended - not connected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Connection - Background sync connection status
|
||||||
|
msgctxt "#39094"
|
||||||
|
msgid "Managed Plex User - not connected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#39200"
|
msgctxt "#39200"
|
||||||
msgid "Log-out Plex Home User "
|
msgid "Log-out Plex Home User "
|
||||||
msgstr "Log ud Plex hjemme bruger "
|
msgstr "Log ud Plex hjemme bruger "
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# XBMC Media Center language file
|
# XBMC Media Center language file
|
||||||
# Translators:
|
# Translators:
|
||||||
# Croneter None <croneter@gmail.com>, 2020
|
# Croneter None <croneter@gmail.com>, 2021
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -8,7 +8,7 @@ msgstr ""
|
||||||
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
|
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
|
||||||
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
|
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
|
||||||
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
|
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
|
||||||
"Last-Translator: Croneter None <croneter@gmail.com>, 2020\n"
|
"Last-Translator: Croneter None <croneter@gmail.com>, 2021\n"
|
||||||
"Language-Team: German (Germany) (https://www.transifex.com/croneter/teams/73837/de_DE/)\n"
|
"Language-Team: German (Germany) (https://www.transifex.com/croneter/teams/73837/de_DE/)\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
@ -44,6 +44,17 @@ msgstr ""
|
||||||
"Achtung: Kodi Einstellung \"Nächsten Video automatisch abspielen\" ist "
|
"Achtung: Kodi Einstellung \"Nächsten Video automatisch abspielen\" ist "
|
||||||
"aktiviert. Dies kann PKC stören. Deaktivieren?"
|
"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"
|
msgctxt "#30005"
|
||||||
msgid "Username: "
|
msgid "Username: "
|
||||||
msgstr "Benutzername: "
|
msgstr "Benutzername: "
|
||||||
|
@ -159,6 +170,17 @@ msgctxt "#30028"
|
||||||
msgid "PKC-only image caching completed"
|
msgid "PKC-only image caching completed"
|
||||||
msgstr "PKC Bilder-Caching beendet"
|
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"
|
msgctxt "#30030"
|
||||||
msgid "Port Number"
|
msgid "Port Number"
|
||||||
msgstr "Portnummer"
|
msgstr "Portnummer"
|
||||||
|
@ -602,6 +624,11 @@ msgctxt "#30524"
|
||||||
msgid "Select Plex libraries to sync"
|
msgid "Select Plex libraries to sync"
|
||||||
msgstr "Zu synchronisierende Plex Bibliotheken auswählen"
|
msgstr "Zu synchronisierende Plex Bibliotheken auswählen"
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30525"
|
||||||
|
msgid "Skip intro"
|
||||||
|
msgstr "Intro überspringen"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30527"
|
msgctxt "#30527"
|
||||||
msgid "Ignore specials in next episodes"
|
msgid "Ignore specials in next episodes"
|
||||||
|
@ -665,8 +692,9 @@ msgstr "FanArtTV Bilder für Film-Sets/Collections herunterladen"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30541"
|
msgctxt "#30541"
|
||||||
msgid "Don't ask to pick a certain stream/quality"
|
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||||
msgstr "Nicht nachfragen, welcher Stream oder Qualität gespielt werden soll"
|
msgstr ""
|
||||||
|
"Transkodierung: Plex-Standards für Audio- und Untertitel-Streams verwenden"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30542"
|
msgctxt "#30542"
|
||||||
|
@ -687,6 +715,21 @@ msgctxt "#30545"
|
||||||
msgid "Force transcode pictures"
|
msgid "Force transcode pictures"
|
||||||
msgstr "Bilder immer transkodieren"
|
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
|
# Welcome to Plex notification
|
||||||
msgctxt "#33000"
|
msgctxt "#33000"
|
||||||
msgid "Welcome"
|
msgid "Welcome"
|
||||||
|
@ -966,6 +1009,11 @@ msgctxt "#39036"
|
||||||
msgid "Escape special characters in path (e.g. space to %20)"
|
msgid "Escape special characters in path (e.g. space to %20)"
|
||||||
msgstr "Sonderzeichen im Pfad escapen (z.B. Leerzeichen zu %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
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39037"
|
msgctxt "#39037"
|
||||||
msgid "Original Plex MOVIE path to replace:"
|
msgid "Original Plex MOVIE path to replace:"
|
||||||
|
@ -1149,6 +1197,11 @@ msgctxt "#39071"
|
||||||
msgid "Current plex.tv status:"
|
msgid "Current plex.tv status:"
|
||||||
msgstr "Aktueller 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
|
# PKC Settings, category name
|
||||||
msgctxt "#39073"
|
msgctxt "#39073"
|
||||||
msgid "Appearance Tweaks"
|
msgid "Appearance Tweaks"
|
||||||
|
@ -1159,6 +1212,11 @@ msgctxt "#39074"
|
||||||
msgid "TV Shows"
|
msgid "TV Shows"
|
||||||
msgstr "Serien"
|
msgstr "Serien"
|
||||||
|
|
||||||
|
# PKC Settings - Sync
|
||||||
|
msgctxt "#39075"
|
||||||
|
msgid "Verify access to media files while synching"
|
||||||
|
msgstr "Zugriff auf Mediendateien während der Synchronisierung überprüfen"
|
||||||
|
|
||||||
# Pop-up during initial sync
|
# Pop-up during initial sync
|
||||||
msgctxt "#39076"
|
msgctxt "#39076"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1224,6 +1282,31 @@ msgctxt "#39085"
|
||||||
msgid "Reload Kodi node files to apply all the settings below"
|
msgid "Reload Kodi node files to apply all the settings below"
|
||||||
msgstr "Kodi neu laden um Einstellungen unten zu übernehmen"
|
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"
|
msgctxt "#39200"
|
||||||
msgid "Log-out Plex Home User "
|
msgid "Log-out Plex Home User "
|
||||||
msgstr "Plex Home Benutzer abmelden: "
|
msgstr "Plex Home Benutzer abmelden: "
|
||||||
|
|
1603
resources/language/resource.language.el_GR/strings.po
Normal file
1603
resources/language/resource.language.el_GR/strings.po
Normal file
File diff suppressed because it is too large
Load diff
|
@ -36,6 +36,10 @@ msgctxt "#30003"
|
||||||
msgid "Warning: Kodi setting \"Play next video automatically\" is enabled. This could break PKC. Deactivate?"
|
msgid "Warning: Kodi setting \"Play next video automatically\" is enabled. This could break PKC. Deactivate?"
|
||||||
msgstr ""
|
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"
|
msgctxt "#30005"
|
||||||
msgid "Username: "
|
msgid "Username: "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -567,6 +571,10 @@ msgctxt "#30524"
|
||||||
msgid "Select Plex libraries to sync"
|
msgid "Select Plex libraries to sync"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30525"
|
||||||
|
msgid "Skip intro"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30527"
|
msgctxt "#30527"
|
||||||
|
@ -630,7 +638,7 @@ msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30541"
|
msgctxt "#30541"
|
||||||
msgid "Don't ask to pick a certain stream/quality"
|
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
|
@ -652,6 +660,21 @@ msgctxt "#30545"
|
||||||
msgid "Force transcode pictures"
|
msgid "Force transcode pictures"
|
||||||
msgstr ""
|
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
|
# Welcome to Plex notification
|
||||||
msgctxt "#33000"
|
msgctxt "#33000"
|
||||||
msgid "Welcome"
|
msgid "Welcome"
|
||||||
|
@ -1055,6 +1078,11 @@ msgctxt "#39071"
|
||||||
msgid "Current plex.tv status:"
|
msgid "Current plex.tv status:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Connection
|
||||||
|
msgctxt "#39072"
|
||||||
|
msgid "Background sync connection:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings, category name
|
# PKC Settings, category name
|
||||||
msgctxt "#39073"
|
msgctxt "#39073"
|
||||||
msgid "Appearance Tweaks"
|
msgid "Appearance Tweaks"
|
||||||
|
@ -1116,6 +1144,31 @@ msgctxt "#39085"
|
||||||
msgid "Reload Kodi node files to apply all the settings below"
|
msgid "Reload Kodi node files to apply all the settings below"
|
||||||
msgstr ""
|
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"
|
msgctxt "#39200"
|
||||||
msgid "Log-out Plex Home User "
|
msgid "Log-out Plex Home User "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# XBMC Media Center language file
|
# XBMC Media Center language file
|
||||||
# Translators:
|
# Translators:
|
||||||
# Croneter None <croneter@gmail.com>, 2019
|
# Croneter None <croneter@gmail.com>, 2020
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -8,7 +8,7 @@ msgstr ""
|
||||||
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
|
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
|
||||||
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
|
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
|
||||||
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
|
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
|
||||||
"Last-Translator: Croneter None <croneter@gmail.com>, 2019\n"
|
"Last-Translator: Croneter None <croneter@gmail.com>, 2020\n"
|
||||||
"Language-Team: Spanish (Argentina) (https://www.transifex.com/croneter/teams/73837/es_AR/)\n"
|
"Language-Team: Spanish (Argentina) (https://www.transifex.com/croneter/teams/73837/es_AR/)\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
@ -44,6 +44,13 @@ msgstr ""
|
||||||
"Advertencia: El ajuste de Kodi \"Reproducir el siguiente video "
|
"Advertencia: El ajuste de Kodi \"Reproducir el siguiente video "
|
||||||
"automáticamente\" está activado. Esto puede dañar PKC. ¿Desactivar?"
|
"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"
|
msgctxt "#30005"
|
||||||
msgid "Username: "
|
msgid "Username: "
|
||||||
msgstr "Usuario: "
|
msgstr "Usuario: "
|
||||||
|
@ -164,6 +171,14 @@ msgctxt "#30028"
|
||||||
msgid "PKC-only image caching completed"
|
msgid "PKC-only image caching completed"
|
||||||
msgstr "El caché de imágenes solo-PKC fue completado"
|
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"
|
msgctxt "#30030"
|
||||||
msgid "Port Number"
|
msgid "Port Number"
|
||||||
msgstr "Número de puerto"
|
msgstr "Número de puerto"
|
||||||
|
@ -603,6 +618,11 @@ msgctxt "#30524"
|
||||||
msgid "Select Plex libraries to sync"
|
msgid "Select Plex libraries to sync"
|
||||||
msgstr "Seleccionar librerias de Plex para sincronizar"
|
msgstr "Seleccionar librerias de Plex para sincronizar"
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30525"
|
||||||
|
msgid "Skip intro"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30527"
|
msgctxt "#30527"
|
||||||
msgid "Ignore specials in next episodes"
|
msgid "Ignore specials in next episodes"
|
||||||
|
@ -668,8 +688,8 @@ msgstr "Descargar arte de sagas de FanArtTV"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30541"
|
msgctxt "#30541"
|
||||||
msgid "Don't ask to pick a certain stream/quality"
|
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||||
msgstr "No solicitar elegir un stream o una calidad en particular"
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30542"
|
msgctxt "#30542"
|
||||||
|
@ -690,6 +710,21 @@ msgctxt "#30545"
|
||||||
msgid "Force transcode pictures"
|
msgid "Force transcode pictures"
|
||||||
msgstr "Obligar transcodificar fotografías"
|
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
|
# Welcome to Plex notification
|
||||||
msgctxt "#33000"
|
msgctxt "#33000"
|
||||||
msgid "Welcome"
|
msgid "Welcome"
|
||||||
|
@ -935,7 +970,7 @@ msgid ""
|
||||||
"Kodi cannot locate the file %s. Please verify your PKC settings. Stop "
|
"Kodi cannot locate the file %s. Please verify your PKC settings. Stop "
|
||||||
"syncing?"
|
"syncing?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Kodi no puede localizer el archive %s. Por favoer verificar sus ajustes de "
|
"Kodi no puede localizer el archivo %s. Por favor verificar sus ajustes de "
|
||||||
"PKC. ¿Detener la sincronización?"
|
"PKC. ¿Detener la sincronización?"
|
||||||
|
|
||||||
# Pop-up on initial sync
|
# Pop-up on initial sync
|
||||||
|
@ -964,7 +999,12 @@ msgstr ""
|
||||||
# PKC Settings - Customize Paths
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39036"
|
msgctxt "#39036"
|
||||||
msgid "Escape special characters in path (e.g. space to %20)"
|
msgid "Escape special characters in path (e.g. space to %20)"
|
||||||
msgstr "Escapar caracteres especiales en la ruta (i.e. espacio a %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
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39037"
|
msgctxt "#39037"
|
||||||
|
@ -1149,6 +1189,11 @@ msgctxt "#39071"
|
||||||
msgid "Current plex.tv status:"
|
msgid "Current plex.tv status:"
|
||||||
msgstr "Estado actual de plex.tv:"
|
msgstr "Estado actual de plex.tv:"
|
||||||
|
|
||||||
|
# PKC Settings - Connection
|
||||||
|
msgctxt "#39072"
|
||||||
|
msgid "Background sync connection:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings, category name
|
# PKC Settings, category name
|
||||||
msgctxt "#39073"
|
msgctxt "#39073"
|
||||||
msgid "Appearance Tweaks"
|
msgid "Appearance Tweaks"
|
||||||
|
@ -1159,6 +1204,11 @@ msgctxt "#39074"
|
||||||
msgid "TV Shows"
|
msgid "TV Shows"
|
||||||
msgstr "Series"
|
msgstr "Series"
|
||||||
|
|
||||||
|
# PKC Settings - Sync
|
||||||
|
msgctxt "#39075"
|
||||||
|
msgid "Verify access to media files while synching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Pop-up during initial sync
|
# Pop-up during initial sync
|
||||||
msgctxt "#39076"
|
msgctxt "#39076"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1226,6 +1276,31 @@ msgctxt "#39085"
|
||||||
msgid "Reload Kodi node files to apply all the settings below"
|
msgid "Reload Kodi node files to apply all the settings below"
|
||||||
msgstr "Recargar Kodi para aplicar todos los ajustes."
|
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"
|
msgctxt "#39200"
|
||||||
msgid "Log-out Plex Home User "
|
msgid "Log-out Plex Home User "
|
||||||
msgstr "Terminar sesión del usuario de Plex Home "
|
msgstr "Terminar sesión del usuario de Plex Home "
|
||||||
|
@ -1430,7 +1505,7 @@ msgid ""
|
||||||
"The current Kodi version is not supported by PKC. Please consult the Plex "
|
"The current Kodi version is not supported by PKC. Please consult the Plex "
|
||||||
"forum."
|
"forum."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"La version actual de Kodi no está soportada por PKC. Por favor consultar el"
|
"La versión actual de Kodi no está soportada por PKC. Por favor consultar el"
|
||||||
" fórum de Plex."
|
" fórum de Plex."
|
||||||
|
|
||||||
msgctxt "#39405"
|
msgctxt "#39405"
|
||||||
|
@ -1474,7 +1549,7 @@ msgstr "Sagas"
|
||||||
|
|
||||||
msgctxt "#39502"
|
msgctxt "#39502"
|
||||||
msgid "PKC On Deck (faster)"
|
msgid "PKC On Deck (faster)"
|
||||||
msgstr "Tablero de PKC (mas rapido)"
|
msgstr "On Deck de PKC (más rápido)"
|
||||||
|
|
||||||
msgctxt "#39600"
|
msgctxt "#39600"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1616,8 +1691,8 @@ msgid ""
|
||||||
"Do you want to replace your custom user ratings with an indicator of how "
|
"Do you want to replace your custom user ratings with an indicator of how "
|
||||||
"many versions of a media item you posses?"
|
"many versions of a media item you posses?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"¿Quiere reemplazar su valoración personalizada con cuántas versione posee de"
|
"¿Quiere reemplazar su valoración personalizada por cuántas versiones posee "
|
||||||
" un elemento de medios?"
|
"de un elemento de medios?"
|
||||||
|
|
||||||
# In PKC Settings under Sync
|
# In PKC Settings under Sync
|
||||||
msgctxt "#39719"
|
msgctxt "#39719"
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
# Translators:
|
# Translators:
|
||||||
# Dani <danichispa@gmail.com>, 2019
|
# Dani <danichispa@gmail.com>, 2019
|
||||||
# Bartolome Soriano <bsoriano@gmail.com>, 2019
|
# Bartolome Soriano <bsoriano@gmail.com>, 2019
|
||||||
|
# Croneter None <croneter@gmail.com>, 2020
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -9,7 +10,7 @@ msgstr ""
|
||||||
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
|
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
|
||||||
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
|
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
|
||||||
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
|
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
|
||||||
"Last-Translator: Bartolome Soriano <bsoriano@gmail.com>, 2019\n"
|
"Last-Translator: Croneter None <croneter@gmail.com>, 2020\n"
|
||||||
"Language-Team: Spanish (Spain) (https://www.transifex.com/croneter/teams/73837/es_ES/)\n"
|
"Language-Team: Spanish (Spain) (https://www.transifex.com/croneter/teams/73837/es_ES/)\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
@ -45,6 +46,13 @@ msgstr ""
|
||||||
"Advertencia: El ajuste de Kodi \"Reproducir el siguiente video "
|
"Advertencia: El ajuste de Kodi \"Reproducir el siguiente video "
|
||||||
"automáticamente\" está activado. Esto puede dañar PKC. ¿Desactivar?"
|
"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"
|
msgctxt "#30005"
|
||||||
msgid "Username: "
|
msgid "Username: "
|
||||||
msgstr "Usuario: "
|
msgstr "Usuario: "
|
||||||
|
@ -165,6 +173,14 @@ msgctxt "#30028"
|
||||||
msgid "PKC-only image caching completed"
|
msgid "PKC-only image caching completed"
|
||||||
msgstr "El caché de imágenes solo-PKC fue completado"
|
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"
|
msgctxt "#30030"
|
||||||
msgid "Port Number"
|
msgid "Port Number"
|
||||||
msgstr "Número de puerto"
|
msgstr "Número de puerto"
|
||||||
|
@ -604,6 +620,11 @@ msgctxt "#30524"
|
||||||
msgid "Select Plex libraries to sync"
|
msgid "Select Plex libraries to sync"
|
||||||
msgstr "Seleccionar librerias de Plex para sincronizar"
|
msgstr "Seleccionar librerias de Plex para sincronizar"
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30525"
|
||||||
|
msgid "Skip intro"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30527"
|
msgctxt "#30527"
|
||||||
msgid "Ignore specials in next episodes"
|
msgid "Ignore specials in next episodes"
|
||||||
|
@ -669,8 +690,8 @@ msgstr "Descargar arte de sagas de FanArtTV"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30541"
|
msgctxt "#30541"
|
||||||
msgid "Don't ask to pick a certain stream/quality"
|
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||||
msgstr "No solicitar elegir un stream o una calidad en particular"
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30542"
|
msgctxt "#30542"
|
||||||
|
@ -691,6 +712,21 @@ msgctxt "#30545"
|
||||||
msgid "Force transcode pictures"
|
msgid "Force transcode pictures"
|
||||||
msgstr "Obligar transcodificar fotografías"
|
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
|
# Welcome to Plex notification
|
||||||
msgctxt "#33000"
|
msgctxt "#33000"
|
||||||
msgid "Welcome"
|
msgid "Welcome"
|
||||||
|
@ -1155,6 +1191,11 @@ msgctxt "#39071"
|
||||||
msgid "Current plex.tv status:"
|
msgid "Current plex.tv status:"
|
||||||
msgstr "Estado actual de plex.tv:"
|
msgstr "Estado actual de plex.tv:"
|
||||||
|
|
||||||
|
# PKC Settings - Connection
|
||||||
|
msgctxt "#39072"
|
||||||
|
msgid "Background sync connection:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings, category name
|
# PKC Settings, category name
|
||||||
msgctxt "#39073"
|
msgctxt "#39073"
|
||||||
msgid "Appearance Tweaks"
|
msgid "Appearance Tweaks"
|
||||||
|
@ -1165,6 +1206,11 @@ msgctxt "#39074"
|
||||||
msgid "TV Shows"
|
msgid "TV Shows"
|
||||||
msgstr "Series"
|
msgstr "Series"
|
||||||
|
|
||||||
|
# PKC Settings - Sync
|
||||||
|
msgctxt "#39075"
|
||||||
|
msgid "Verify access to media files while synching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Pop-up during initial sync
|
# Pop-up during initial sync
|
||||||
msgctxt "#39076"
|
msgctxt "#39076"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1232,6 +1278,31 @@ msgctxt "#39085"
|
||||||
msgid "Reload Kodi node files to apply all the settings below"
|
msgid "Reload Kodi node files to apply all the settings below"
|
||||||
msgstr "Recargar Kodi para aplicar todos los ajustes."
|
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"
|
msgctxt "#39200"
|
||||||
msgid "Log-out Plex Home User "
|
msgid "Log-out Plex Home User "
|
||||||
msgstr "Terminar sesión del usuario de Plex Home "
|
msgstr "Terminar sesión del usuario de Plex Home "
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# XBMC Media Center language file
|
# XBMC Media Center language file
|
||||||
# Translators:
|
# Translators:
|
||||||
# Croneter None <croneter@gmail.com>, 2019
|
# Croneter None <croneter@gmail.com>, 2020
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -8,7 +8,7 @@ msgstr ""
|
||||||
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
|
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
|
||||||
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
|
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
|
||||||
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
|
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
|
||||||
"Last-Translator: Croneter None <croneter@gmail.com>, 2019\n"
|
"Last-Translator: Croneter None <croneter@gmail.com>, 2020\n"
|
||||||
"Language-Team: Spanish (Mexico) (https://www.transifex.com/croneter/teams/73837/es_MX/)\n"
|
"Language-Team: Spanish (Mexico) (https://www.transifex.com/croneter/teams/73837/es_MX/)\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
@ -44,6 +44,13 @@ msgstr ""
|
||||||
"Advertencia: El ajuste de Kodi \"Reproducir el siguiente video "
|
"Advertencia: El ajuste de Kodi \"Reproducir el siguiente video "
|
||||||
"automáticamente\" está activado. Esto puede dañar PKC. ¿Desactivar?"
|
"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"
|
msgctxt "#30005"
|
||||||
msgid "Username: "
|
msgid "Username: "
|
||||||
msgstr "Usuario: "
|
msgstr "Usuario: "
|
||||||
|
@ -164,6 +171,14 @@ msgctxt "#30028"
|
||||||
msgid "PKC-only image caching completed"
|
msgid "PKC-only image caching completed"
|
||||||
msgstr "El caché de imágenes solo-PKC fue completado"
|
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"
|
msgctxt "#30030"
|
||||||
msgid "Port Number"
|
msgid "Port Number"
|
||||||
msgstr "Número de puerto"
|
msgstr "Número de puerto"
|
||||||
|
@ -603,6 +618,11 @@ msgctxt "#30524"
|
||||||
msgid "Select Plex libraries to sync"
|
msgid "Select Plex libraries to sync"
|
||||||
msgstr "Seleccionar librerias de Plex para sincronizar"
|
msgstr "Seleccionar librerias de Plex para sincronizar"
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30525"
|
||||||
|
msgid "Skip intro"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30527"
|
msgctxt "#30527"
|
||||||
msgid "Ignore specials in next episodes"
|
msgid "Ignore specials in next episodes"
|
||||||
|
@ -668,8 +688,8 @@ msgstr "Descargar arte de sagas de FanArtTV"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30541"
|
msgctxt "#30541"
|
||||||
msgid "Don't ask to pick a certain stream/quality"
|
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||||
msgstr "No solicitar elegir un stream o una calidad en particular"
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30542"
|
msgctxt "#30542"
|
||||||
|
@ -690,6 +710,21 @@ msgctxt "#30545"
|
||||||
msgid "Force transcode pictures"
|
msgid "Force transcode pictures"
|
||||||
msgstr "Obligar transcodificar fotografías"
|
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
|
# Welcome to Plex notification
|
||||||
msgctxt "#33000"
|
msgctxt "#33000"
|
||||||
msgid "Welcome"
|
msgid "Welcome"
|
||||||
|
@ -935,7 +970,7 @@ msgid ""
|
||||||
"Kodi cannot locate the file %s. Please verify your PKC settings. Stop "
|
"Kodi cannot locate the file %s. Please verify your PKC settings. Stop "
|
||||||
"syncing?"
|
"syncing?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Kodi no puede localizer el archive %s. Por favoer verificar sus ajustes de "
|
"Kodi no puede localizer el archivo %s. Por favor verificar sus ajustes de "
|
||||||
"PKC. ¿Detener la sincronización?"
|
"PKC. ¿Detener la sincronización?"
|
||||||
|
|
||||||
# Pop-up on initial sync
|
# Pop-up on initial sync
|
||||||
|
@ -964,7 +999,12 @@ msgstr ""
|
||||||
# PKC Settings - Customize Paths
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39036"
|
msgctxt "#39036"
|
||||||
msgid "Escape special characters in path (e.g. space to %20)"
|
msgid "Escape special characters in path (e.g. space to %20)"
|
||||||
msgstr "Escapar caracteres especiales en la ruta (i.e. espacio a %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
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39037"
|
msgctxt "#39037"
|
||||||
|
@ -1149,6 +1189,11 @@ msgctxt "#39071"
|
||||||
msgid "Current plex.tv status:"
|
msgid "Current plex.tv status:"
|
||||||
msgstr "Estado actual de plex.tv:"
|
msgstr "Estado actual de plex.tv:"
|
||||||
|
|
||||||
|
# PKC Settings - Connection
|
||||||
|
msgctxt "#39072"
|
||||||
|
msgid "Background sync connection:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings, category name
|
# PKC Settings, category name
|
||||||
msgctxt "#39073"
|
msgctxt "#39073"
|
||||||
msgid "Appearance Tweaks"
|
msgid "Appearance Tweaks"
|
||||||
|
@ -1159,6 +1204,11 @@ msgctxt "#39074"
|
||||||
msgid "TV Shows"
|
msgid "TV Shows"
|
||||||
msgstr "Series"
|
msgstr "Series"
|
||||||
|
|
||||||
|
# PKC Settings - Sync
|
||||||
|
msgctxt "#39075"
|
||||||
|
msgid "Verify access to media files while synching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Pop-up during initial sync
|
# Pop-up during initial sync
|
||||||
msgctxt "#39076"
|
msgctxt "#39076"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1226,6 +1276,31 @@ msgctxt "#39085"
|
||||||
msgid "Reload Kodi node files to apply all the settings below"
|
msgid "Reload Kodi node files to apply all the settings below"
|
||||||
msgstr "Recargar Kodi para aplicar todos los ajustes."
|
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"
|
msgctxt "#39200"
|
||||||
msgid "Log-out Plex Home User "
|
msgid "Log-out Plex Home User "
|
||||||
msgstr "Terminar sesión del usuario de Plex Home "
|
msgstr "Terminar sesión del usuario de Plex Home "
|
||||||
|
@ -1430,7 +1505,7 @@ msgid ""
|
||||||
"The current Kodi version is not supported by PKC. Please consult the Plex "
|
"The current Kodi version is not supported by PKC. Please consult the Plex "
|
||||||
"forum."
|
"forum."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"La version actual de Kodi no está soportada por PKC. Por favor consultar el"
|
"La versión actual de Kodi no está soportada por PKC. Por favor consultar el"
|
||||||
" fórum de Plex."
|
" fórum de Plex."
|
||||||
|
|
||||||
msgctxt "#39405"
|
msgctxt "#39405"
|
||||||
|
@ -1474,7 +1549,7 @@ msgstr "Sagas"
|
||||||
|
|
||||||
msgctxt "#39502"
|
msgctxt "#39502"
|
||||||
msgid "PKC On Deck (faster)"
|
msgid "PKC On Deck (faster)"
|
||||||
msgstr "Tablero de PKC (mas rapido)"
|
msgstr "On Deck de PKC (más rápido)"
|
||||||
|
|
||||||
msgctxt "#39600"
|
msgctxt "#39600"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1616,8 +1691,8 @@ msgid ""
|
||||||
"Do you want to replace your custom user ratings with an indicator of how "
|
"Do you want to replace your custom user ratings with an indicator of how "
|
||||||
"many versions of a media item you posses?"
|
"many versions of a media item you posses?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"¿Quiere reemplazar su valoración personalizada con cuántas versione posee de"
|
"¿Quiere reemplazar su valoración personalizada por cuántas versiones posee "
|
||||||
" un elemento de medios?"
|
"de un elemento de medios?"
|
||||||
|
|
||||||
# In PKC Settings under Sync
|
# In PKC Settings under Sync
|
||||||
msgctxt "#39719"
|
msgctxt "#39719"
|
||||||
|
|
|
@ -46,6 +46,13 @@ msgstr ""
|
||||||
"Avertissement : Le paramètre Kodi \"Lecture automatique de la vidéo "
|
"Avertissement : Le paramètre Kodi \"Lecture automatique de la vidéo "
|
||||||
"suivante\" est activé. Cela pourrait casser PKC. Désactiver ?"
|
"suivante\" est activé. Cela pourrait casser PKC. Désactiver ?"
|
||||||
|
|
||||||
|
msgctxt "#30004"
|
||||||
|
msgid ""
|
||||||
|
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||||
|
"random password automatically if you haven't done so already. Please confirm"
|
||||||
|
" the next dialog that you want to enable the webserver now with Yes."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30005"
|
msgctxt "#30005"
|
||||||
msgid "Username: "
|
msgid "Username: "
|
||||||
msgstr "Identifiant : "
|
msgstr "Identifiant : "
|
||||||
|
@ -165,6 +172,14 @@ msgctxt "#30028"
|
||||||
msgid "PKC-only image caching completed"
|
msgid "PKC-only image caching completed"
|
||||||
msgstr "Mise en cache de images pour PKC-seulement terminée"
|
msgstr "Mise en cache de images pour PKC-seulement terminée"
|
||||||
|
|
||||||
|
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||||
|
msgctxt "#30029"
|
||||||
|
msgid ""
|
||||||
|
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
|
||||||
|
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
|
||||||
|
"database resets. Continue?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30030"
|
msgctxt "#30030"
|
||||||
msgid "Port Number"
|
msgid "Port Number"
|
||||||
msgstr "Numéro de port"
|
msgstr "Numéro de port"
|
||||||
|
@ -608,6 +623,11 @@ msgctxt "#30524"
|
||||||
msgid "Select Plex libraries to sync"
|
msgid "Select Plex libraries to sync"
|
||||||
msgstr "Sélectionner les bibliothèques Plex à synchroniser"
|
msgstr "Sélectionner les bibliothèques Plex à synchroniser"
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30525"
|
||||||
|
msgid "Skip intro"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30527"
|
msgctxt "#30527"
|
||||||
msgid "Ignore specials in next episodes"
|
msgid "Ignore specials in next episodes"
|
||||||
|
@ -673,8 +693,8 @@ msgstr "Télécharger les affiches des sagas sur FanArtTV"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30541"
|
msgctxt "#30541"
|
||||||
msgid "Don't ask to pick a certain stream/quality"
|
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||||
msgstr "Ne pas demander de choisir un certain flux/qualité"
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30542"
|
msgctxt "#30542"
|
||||||
|
@ -695,6 +715,21 @@ msgctxt "#30545"
|
||||||
msgid "Force transcode pictures"
|
msgid "Force transcode pictures"
|
||||||
msgstr "Forcer le transcodage des images"
|
msgstr "Forcer le transcodage des images"
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30546"
|
||||||
|
msgid "Pick the first video if several versions are present"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30547"
|
||||||
|
msgid "Who picks the audio stream on playback start?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30548"
|
||||||
|
msgid "Who picks subtitles on playback start?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Welcome to Plex notification
|
# Welcome to Plex notification
|
||||||
msgctxt "#33000"
|
msgctxt "#33000"
|
||||||
msgid "Welcome"
|
msgid "Welcome"
|
||||||
|
@ -975,6 +1010,11 @@ msgstr ""
|
||||||
"Echapper les caractères spéciaux dans le chemin (ex: %20 au lieu des "
|
"Echapper les caractères spéciaux dans le chemin (ex: %20 au lieu des "
|
||||||
"espaces)"
|
"espaces)"
|
||||||
|
|
||||||
|
# PKC Settings - Customize Paths
|
||||||
|
msgctxt "#39090"
|
||||||
|
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||||
|
msgstr "Caractères sûrs pour les urls http(s), dav(s) et (s)ftp"
|
||||||
|
|
||||||
# PKC Settings - Customize Paths
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39037"
|
msgctxt "#39037"
|
||||||
msgid "Original Plex MOVIE path to replace:"
|
msgid "Original Plex MOVIE path to replace:"
|
||||||
|
@ -1163,6 +1203,11 @@ msgctxt "#39071"
|
||||||
msgid "Current plex.tv status:"
|
msgid "Current plex.tv status:"
|
||||||
msgstr "État actuel de plex.tv: "
|
msgstr "État actuel de plex.tv: "
|
||||||
|
|
||||||
|
# PKC Settings - Connection
|
||||||
|
msgctxt "#39072"
|
||||||
|
msgid "Background sync connection:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings, category name
|
# PKC Settings, category name
|
||||||
msgctxt "#39073"
|
msgctxt "#39073"
|
||||||
msgid "Appearance Tweaks"
|
msgid "Appearance Tweaks"
|
||||||
|
@ -1173,6 +1218,11 @@ msgctxt "#39074"
|
||||||
msgid "TV Shows"
|
msgid "TV Shows"
|
||||||
msgstr "Séries TV"
|
msgstr "Séries TV"
|
||||||
|
|
||||||
|
# PKC Settings - Sync
|
||||||
|
msgctxt "#39075"
|
||||||
|
msgid "Verify access to media files while synching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Pop-up during initial sync
|
# Pop-up during initial sync
|
||||||
msgctxt "#39076"
|
msgctxt "#39076"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1240,6 +1290,31 @@ msgid "Reload Kodi node files to apply all the settings below"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Recharger les fichiers de Kodi pour appliquer tous les paramètres ci-dessous"
|
"Recharger les fichiers de Kodi pour appliquer tous les paramètres ci-dessous"
|
||||||
|
|
||||||
|
# PKC Settings - Connection - Background sync connection status
|
||||||
|
msgctxt "#39089"
|
||||||
|
msgid "Alexa connection status:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Connection - Background sync connection status
|
||||||
|
msgctxt "#39091"
|
||||||
|
msgid "Timeout - not connected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Connection - Background sync connection status
|
||||||
|
msgctxt "#39092"
|
||||||
|
msgid "IOError - not connected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Connection - Background sync connection status
|
||||||
|
msgctxt "#39093"
|
||||||
|
msgid "Suspended - not connected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Connection - Background sync connection status
|
||||||
|
msgctxt "#39094"
|
||||||
|
msgid "Managed Plex User - not connected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#39200"
|
msgctxt "#39200"
|
||||||
msgid "Log-out Plex Home User "
|
msgid "Log-out Plex Home User "
|
||||||
msgstr "Log-out Plex Home User "
|
msgstr "Log-out Plex Home User "
|
||||||
|
|
|
@ -50,6 +50,13 @@ msgstr ""
|
||||||
"Avertissement : Le paramètre Kodi \"Lecture automatique de la vidéo "
|
"Avertissement : Le paramètre Kodi \"Lecture automatique de la vidéo "
|
||||||
"suivante\" est activé. Cela pourrait casser PKC. Désactiver ?"
|
"suivante\" est activé. Cela pourrait casser PKC. Désactiver ?"
|
||||||
|
|
||||||
|
msgctxt "#30004"
|
||||||
|
msgid ""
|
||||||
|
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||||
|
"random password automatically if you haven't done so already. Please confirm"
|
||||||
|
" the next dialog that you want to enable the webserver now with Yes."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30005"
|
msgctxt "#30005"
|
||||||
msgid "Username: "
|
msgid "Username: "
|
||||||
msgstr "Identifiant : "
|
msgstr "Identifiant : "
|
||||||
|
@ -169,6 +176,14 @@ msgctxt "#30028"
|
||||||
msgid "PKC-only image caching completed"
|
msgid "PKC-only image caching completed"
|
||||||
msgstr "Mise en cache de images pour PKC-seulement terminée"
|
msgstr "Mise en cache de images pour PKC-seulement terminée"
|
||||||
|
|
||||||
|
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||||
|
msgctxt "#30029"
|
||||||
|
msgid ""
|
||||||
|
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
|
||||||
|
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
|
||||||
|
"database resets. Continue?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30030"
|
msgctxt "#30030"
|
||||||
msgid "Port Number"
|
msgid "Port Number"
|
||||||
msgstr "Numéro de port"
|
msgstr "Numéro de port"
|
||||||
|
@ -612,6 +627,11 @@ msgctxt "#30524"
|
||||||
msgid "Select Plex libraries to sync"
|
msgid "Select Plex libraries to sync"
|
||||||
msgstr "Sélectionner les bibliothèques Plex à synchroniser"
|
msgstr "Sélectionner les bibliothèques Plex à synchroniser"
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30525"
|
||||||
|
msgid "Skip intro"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30527"
|
msgctxt "#30527"
|
||||||
msgid "Ignore specials in next episodes"
|
msgid "Ignore specials in next episodes"
|
||||||
|
@ -677,8 +697,8 @@ msgstr "Télécharger les affiches des sagas sur FanArtTV"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30541"
|
msgctxt "#30541"
|
||||||
msgid "Don't ask to pick a certain stream/quality"
|
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||||
msgstr "Ne pas demander de choisir un certain flux/qualité"
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30542"
|
msgctxt "#30542"
|
||||||
|
@ -699,6 +719,21 @@ msgctxt "#30545"
|
||||||
msgid "Force transcode pictures"
|
msgid "Force transcode pictures"
|
||||||
msgstr "Forcer le transcodage des images"
|
msgstr "Forcer le transcodage des images"
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30546"
|
||||||
|
msgid "Pick the first video if several versions are present"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30547"
|
||||||
|
msgid "Who picks the audio stream on playback start?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30548"
|
||||||
|
msgid "Who picks subtitles on playback start?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Welcome to Plex notification
|
# Welcome to Plex notification
|
||||||
msgctxt "#33000"
|
msgctxt "#33000"
|
||||||
msgid "Welcome"
|
msgid "Welcome"
|
||||||
|
@ -979,6 +1014,11 @@ msgstr ""
|
||||||
"Echapper les caractères spéciaux dans le chemin (ex: %20 au lieu des "
|
"Echapper les caractères spéciaux dans le chemin (ex: %20 au lieu des "
|
||||||
"espaces)"
|
"espaces)"
|
||||||
|
|
||||||
|
# PKC Settings - Customize Paths
|
||||||
|
msgctxt "#39090"
|
||||||
|
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||||
|
msgstr "Caractères sûrs pour les urls http(s), dav(s) et (s)ftp"
|
||||||
|
|
||||||
# PKC Settings - Customize Paths
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39037"
|
msgctxt "#39037"
|
||||||
msgid "Original Plex MOVIE path to replace:"
|
msgid "Original Plex MOVIE path to replace:"
|
||||||
|
@ -1167,6 +1207,11 @@ msgctxt "#39071"
|
||||||
msgid "Current plex.tv status:"
|
msgid "Current plex.tv status:"
|
||||||
msgstr "État actuel de plex.tv: "
|
msgstr "État actuel de plex.tv: "
|
||||||
|
|
||||||
|
# PKC Settings - Connection
|
||||||
|
msgctxt "#39072"
|
||||||
|
msgid "Background sync connection:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings, category name
|
# PKC Settings, category name
|
||||||
msgctxt "#39073"
|
msgctxt "#39073"
|
||||||
msgid "Appearance Tweaks"
|
msgid "Appearance Tweaks"
|
||||||
|
@ -1177,6 +1222,11 @@ msgctxt "#39074"
|
||||||
msgid "TV Shows"
|
msgid "TV Shows"
|
||||||
msgstr "Séries TV"
|
msgstr "Séries TV"
|
||||||
|
|
||||||
|
# PKC Settings - Sync
|
||||||
|
msgctxt "#39075"
|
||||||
|
msgid "Verify access to media files while synching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Pop-up during initial sync
|
# Pop-up during initial sync
|
||||||
msgctxt "#39076"
|
msgctxt "#39076"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1244,6 +1294,31 @@ msgid "Reload Kodi node files to apply all the settings below"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Recharger les fichiers de Kodi pour appliquer tous les paramètres ci-dessous"
|
"Recharger les fichiers de Kodi pour appliquer tous les paramètres ci-dessous"
|
||||||
|
|
||||||
|
# PKC Settings - Connection - Background sync connection status
|
||||||
|
msgctxt "#39089"
|
||||||
|
msgid "Alexa connection status:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Connection - Background sync connection status
|
||||||
|
msgctxt "#39091"
|
||||||
|
msgid "Timeout - not connected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Connection - Background sync connection status
|
||||||
|
msgctxt "#39092"
|
||||||
|
msgid "IOError - not connected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Connection - Background sync connection status
|
||||||
|
msgctxt "#39093"
|
||||||
|
msgid "Suspended - not connected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Connection - Background sync connection status
|
||||||
|
msgctxt "#39094"
|
||||||
|
msgid "Managed Plex User - not connected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#39200"
|
msgctxt "#39200"
|
||||||
msgid "Log-out Plex Home User "
|
msgid "Log-out Plex Home User "
|
||||||
msgstr "Log-out Plex Home User "
|
msgstr "Log-out Plex Home User "
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# XBMC Media Center language file
|
# XBMC Media Center language file
|
||||||
# Translators:
|
# Translators:
|
||||||
# Croneter None <croneter@gmail.com>, 2019
|
# Croneter None <croneter@gmail.com>, 2019
|
||||||
# Savage93 <savageistheking@gmail.com>, 2020
|
# Savage93 <savageistheking@gmail.com>, 2021
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -9,7 +9,7 @@ msgstr ""
|
||||||
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
|
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
|
||||||
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
|
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
|
||||||
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
|
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
|
||||||
"Last-Translator: Savage93 <savageistheking@gmail.com>, 2020\n"
|
"Last-Translator: Savage93 <savageistheking@gmail.com>, 2021\n"
|
||||||
"Language-Team: Hungarian (Hungary) (https://www.transifex.com/croneter/teams/73837/hu_HU/)\n"
|
"Language-Team: Hungarian (Hungary) (https://www.transifex.com/croneter/teams/73837/hu_HU/)\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
@ -45,6 +45,17 @@ msgstr ""
|
||||||
"Figyelem: \"A következő videó automatikus lejátszása\" be van kapcsolva. Ez "
|
"Figyelem: \"A következő videó automatikus lejátszása\" be van kapcsolva. Ez "
|
||||||
"megakadályozhatja a PKC megfelelő működését. Kikapcsolja?"
|
"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"
|
msgctxt "#30005"
|
||||||
msgid "Username: "
|
msgid "Username: "
|
||||||
msgstr "Felhasználónév: "
|
msgstr "Felhasználónév: "
|
||||||
|
@ -162,6 +173,14 @@ msgctxt "#30028"
|
||||||
msgid "PKC-only image caching completed"
|
msgid "PKC-only image caching completed"
|
||||||
msgstr "PKC képek gyorsítótárazása befejeződött"
|
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"
|
msgctxt "#30030"
|
||||||
msgid "Port Number"
|
msgid "Port Number"
|
||||||
msgstr "Portszám"
|
msgstr "Portszám"
|
||||||
|
@ -606,6 +625,11 @@ msgctxt "#30524"
|
||||||
msgid "Select Plex libraries to sync"
|
msgid "Select Plex libraries to sync"
|
||||||
msgstr "Szinkronizálni kívánt Plex könyvtárak kiválasztása"
|
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
|
# PKC Settings - Playback
|
||||||
msgctxt "#30527"
|
msgctxt "#30527"
|
||||||
msgid "Ignore specials in next episodes"
|
msgid "Ignore specials in next episodes"
|
||||||
|
@ -670,8 +694,10 @@ msgstr "Film-szett/kollekció képek letöltése a FanArtTV-ről"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30541"
|
msgctxt "#30541"
|
||||||
msgid "Don't ask to pick a certain stream/quality"
|
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||||
msgstr "Ne kérdezze meg melyik stream/minőség kerüljön lejátszásra"
|
msgstr ""
|
||||||
|
"Transzkódolás: hang- és feliratsávok automatikus kiválasztása a Plex "
|
||||||
|
"alapértelmezések alapján"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30542"
|
msgctxt "#30542"
|
||||||
|
@ -692,6 +718,21 @@ msgctxt "#30545"
|
||||||
msgid "Force transcode pictures"
|
msgid "Force transcode pictures"
|
||||||
msgstr "Képek transzkódolásának kényszerítése"
|
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
|
# Welcome to Plex notification
|
||||||
msgctxt "#33000"
|
msgctxt "#33000"
|
||||||
msgid "Welcome"
|
msgid "Welcome"
|
||||||
|
@ -968,6 +1009,11 @@ msgid "Escape special characters in path (e.g. space to %20)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"A speciális karakterek feloldása az elérési útban (pl. szóköz helyett %20)"
|
"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
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39037"
|
msgctxt "#39037"
|
||||||
msgid "Original Plex MOVIE path to replace:"
|
msgid "Original Plex MOVIE path to replace:"
|
||||||
|
@ -1151,6 +1197,11 @@ msgctxt "#39071"
|
||||||
msgid "Current plex.tv status:"
|
msgid "Current plex.tv status:"
|
||||||
msgstr "Jelenlegi plex.tv állapot:"
|
msgstr "Jelenlegi plex.tv állapot:"
|
||||||
|
|
||||||
|
# PKC Settings - Connection
|
||||||
|
msgctxt "#39072"
|
||||||
|
msgid "Background sync connection:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings, category name
|
# PKC Settings, category name
|
||||||
msgctxt "#39073"
|
msgctxt "#39073"
|
||||||
msgid "Appearance Tweaks"
|
msgid "Appearance Tweaks"
|
||||||
|
@ -1161,6 +1212,11 @@ msgctxt "#39074"
|
||||||
msgid "TV Shows"
|
msgid "TV Shows"
|
||||||
msgstr "TV sorozatok"
|
msgstr "TV sorozatok"
|
||||||
|
|
||||||
|
# PKC Settings - Sync
|
||||||
|
msgctxt "#39075"
|
||||||
|
msgid "Verify access to media files while synching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Pop-up during initial sync
|
# Pop-up during initial sync
|
||||||
msgctxt "#39076"
|
msgctxt "#39076"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1229,6 +1285,31 @@ msgid "Reload Kodi node files to apply all the settings below"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Kodi csomópont fájlok újratöltése az alábbi beállítások alkalmazásához"
|
"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"
|
msgctxt "#39200"
|
||||||
msgid "Log-out Plex Home User "
|
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: "
|
||||||
|
|
|
@ -47,6 +47,13 @@ msgstr ""
|
||||||
"Attenzione: l'impostazione Kodi \"Avvia il video successivo "
|
"Attenzione: l'impostazione Kodi \"Avvia il video successivo "
|
||||||
"automaticamente\" è attivata. Questo può interrompere PKC. Disattivare?"
|
"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"
|
msgctxt "#30005"
|
||||||
msgid "Username: "
|
msgid "Username: "
|
||||||
msgstr "Nome utente:"
|
msgstr "Nome utente:"
|
||||||
|
@ -164,6 +171,14 @@ msgctxt "#30028"
|
||||||
msgid "PKC-only image caching completed"
|
msgid "PKC-only image caching completed"
|
||||||
msgstr "Cache delle immagini di PKC completato"
|
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"
|
msgctxt "#30030"
|
||||||
msgid "Port Number"
|
msgid "Port Number"
|
||||||
msgstr "Porta"
|
msgstr "Porta"
|
||||||
|
@ -603,6 +618,11 @@ msgctxt "#30524"
|
||||||
msgid "Select Plex libraries to sync"
|
msgid "Select Plex libraries to sync"
|
||||||
msgstr "Seleziona le librerie Plex da sincronizzazare"
|
msgstr "Seleziona le librerie Plex da sincronizzazare"
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30525"
|
||||||
|
msgid "Skip intro"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30527"
|
msgctxt "#30527"
|
||||||
msgid "Ignore specials in next episodes"
|
msgid "Ignore specials in next episodes"
|
||||||
|
@ -668,8 +688,8 @@ msgstr "Scarica collezioni/cofanetti film da FanartTV"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30541"
|
msgctxt "#30541"
|
||||||
msgid "Don't ask to pick a certain stream/quality"
|
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||||
msgstr "Non chiedere di scegliere la qualità dello stream"
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30542"
|
msgctxt "#30542"
|
||||||
|
@ -690,6 +710,21 @@ msgctxt "#30545"
|
||||||
msgid "Force transcode pictures"
|
msgid "Force transcode pictures"
|
||||||
msgstr "Forza transcodifica immagini"
|
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
|
# Welcome to Plex notification
|
||||||
msgctxt "#33000"
|
msgctxt "#33000"
|
||||||
msgid "Welcome"
|
msgid "Welcome"
|
||||||
|
@ -967,6 +1002,11 @@ msgstr ""
|
||||||
"Esegui l'escape dei caratteri speciali nel percorso (es. \"spazio\" "
|
"Esegui l'escape dei caratteri speciali nel percorso (es. \"spazio\" "
|
||||||
"trasformato in \"%20\")"
|
"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
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39037"
|
msgctxt "#39037"
|
||||||
msgid "Original Plex MOVIE path to replace:"
|
msgid "Original Plex MOVIE path to replace:"
|
||||||
|
@ -1152,6 +1192,11 @@ msgctxt "#39071"
|
||||||
msgid "Current plex.tv status:"
|
msgid "Current plex.tv status:"
|
||||||
msgstr "Stato attuale di plex.tv:"
|
msgstr "Stato attuale di plex.tv:"
|
||||||
|
|
||||||
|
# PKC Settings - Connection
|
||||||
|
msgctxt "#39072"
|
||||||
|
msgid "Background sync connection:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings, category name
|
# PKC Settings, category name
|
||||||
msgctxt "#39073"
|
msgctxt "#39073"
|
||||||
msgid "Appearance Tweaks"
|
msgid "Appearance Tweaks"
|
||||||
|
@ -1162,6 +1207,11 @@ msgctxt "#39074"
|
||||||
msgid "TV Shows"
|
msgid "TV Shows"
|
||||||
msgstr "Serie TV"
|
msgstr "Serie TV"
|
||||||
|
|
||||||
|
# PKC Settings - Sync
|
||||||
|
msgctxt "#39075"
|
||||||
|
msgid "Verify access to media files while synching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Pop-up during initial sync
|
# Pop-up during initial sync
|
||||||
msgctxt "#39076"
|
msgctxt "#39076"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1229,6 +1279,31 @@ msgstr ""
|
||||||
"Ricarica i nodi di file di Kodi per applicare tutte le impostazioni di "
|
"Ricarica i nodi di file di Kodi per applicare tutte le impostazioni di "
|
||||||
"sotto"
|
"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"
|
msgctxt "#39200"
|
||||||
msgid "Log-out Plex Home User "
|
msgid "Log-out Plex Home User "
|
||||||
msgstr "Logout utente Plex "
|
msgstr "Logout utente Plex "
|
||||||
|
|
1634
resources/language/resource.language.ko_KR/strings.po
Normal file
1634
resources/language/resource.language.ko_KR/strings.po
Normal file
File diff suppressed because it is too large
Load diff
|
@ -45,6 +45,13 @@ msgstr ""
|
||||||
"Įspėjimas: „Kodi“ nustatymas „Leisti kitą vaizdo įrašą automatiškai“ yra "
|
"Įspėjimas: „Kodi“ nustatymas „Leisti kitą vaizdo įrašą automatiškai“ yra "
|
||||||
"įjungtas. Tai gali pažeisti „PKC“. Išjungti?"
|
"įjungtas. Tai gali pažeisti „PKC“. Išjungti?"
|
||||||
|
|
||||||
|
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"
|
msgctxt "#30005"
|
||||||
msgid "Username: "
|
msgid "Username: "
|
||||||
msgstr "Vartotojo vardas:"
|
msgstr "Vartotojo vardas:"
|
||||||
|
@ -164,6 +171,14 @@ msgctxt "#30028"
|
||||||
msgid "PKC-only image caching completed"
|
msgid "PKC-only image caching completed"
|
||||||
msgstr "„PKC“-baigtas tik atvaizdžių podėliavimas"
|
msgstr "„PKC“-baigtas tik atvaizdžių podėliavimas"
|
||||||
|
|
||||||
|
# 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"
|
msgctxt "#30030"
|
||||||
msgid "Port Number"
|
msgid "Port Number"
|
||||||
msgstr "Prievado numeris"
|
msgstr "Prievado numeris"
|
||||||
|
@ -600,6 +615,11 @@ msgctxt "#30524"
|
||||||
msgid "Select Plex libraries to sync"
|
msgid "Select Plex libraries to sync"
|
||||||
msgstr "Pasirinkti sinchronizuojamas „Plex“ bibliotekas"
|
msgstr "Pasirinkti sinchronizuojamas „Plex“ bibliotekas"
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30525"
|
||||||
|
msgid "Skip intro"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30527"
|
msgctxt "#30527"
|
||||||
msgid "Ignore specials in next episodes"
|
msgid "Ignore specials in next episodes"
|
||||||
|
@ -664,8 +684,8 @@ msgstr "Atsisiųskite filmų komplekto / rinkinio iliustraciją iš „FanArtTV
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30541"
|
msgctxt "#30541"
|
||||||
msgid "Don't ask to pick a certain stream/quality"
|
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||||
msgstr "Neprašykite pasirinkti tam tikro srauto / kokybės"
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30542"
|
msgctxt "#30542"
|
||||||
|
@ -686,6 +706,21 @@ msgctxt "#30545"
|
||||||
msgid "Force transcode pictures"
|
msgid "Force transcode pictures"
|
||||||
msgstr "Priverstinai perkoduoti nuotraukas"
|
msgstr "Priverstinai perkoduoti nuotraukas"
|
||||||
|
|
||||||
|
# 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
|
# Welcome to Plex notification
|
||||||
msgctxt "#33000"
|
msgctxt "#33000"
|
||||||
msgid "Welcome"
|
msgid "Welcome"
|
||||||
|
@ -963,6 +998,11 @@ msgctxt "#39036"
|
||||||
msgid "Escape special characters in path (e.g. space to %20)"
|
msgid "Escape special characters in path (e.g. space to %20)"
|
||||||
msgstr "Kaita specialių simbolių kelyje (pvz., tarpas %20)"
|
msgstr "Kaita specialių simbolių kelyje (pvz., tarpas %20)"
|
||||||
|
|
||||||
|
# PKC Settings - Customize Paths
|
||||||
|
msgctxt "#39090"
|
||||||
|
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Customize Paths
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39037"
|
msgctxt "#39037"
|
||||||
msgid "Original Plex MOVIE path to replace:"
|
msgid "Original Plex MOVIE path to replace:"
|
||||||
|
@ -1145,6 +1185,11 @@ msgctxt "#39071"
|
||||||
msgid "Current plex.tv status:"
|
msgid "Current plex.tv status:"
|
||||||
msgstr "Dabartinė „plex.tv“ būsena:"
|
msgstr "Dabartinė „plex.tv“ būsena:"
|
||||||
|
|
||||||
|
# PKC Settings - Connection
|
||||||
|
msgctxt "#39072"
|
||||||
|
msgid "Background sync connection:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings, category name
|
# PKC Settings, category name
|
||||||
msgctxt "#39073"
|
msgctxt "#39073"
|
||||||
msgid "Appearance Tweaks"
|
msgid "Appearance Tweaks"
|
||||||
|
@ -1155,6 +1200,11 @@ msgctxt "#39074"
|
||||||
msgid "TV Shows"
|
msgid "TV Shows"
|
||||||
msgstr "TV Laidos"
|
msgstr "TV Laidos"
|
||||||
|
|
||||||
|
# PKC Settings - Sync
|
||||||
|
msgctxt "#39075"
|
||||||
|
msgid "Verify access to media files while synching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Pop-up during initial sync
|
# Pop-up during initial sync
|
||||||
msgctxt "#39076"
|
msgctxt "#39076"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1222,6 +1272,31 @@ msgstr ""
|
||||||
"Atnaujinkite „Kodi“ mazgų failus, kad galėtumėte taikyti visus toliau "
|
"Atnaujinkite „Kodi“ mazgų failus, kad galėtumėte taikyti visus toliau "
|
||||||
"pateiktus nustatymus"
|
"pateiktus nustatymus"
|
||||||
|
|
||||||
|
# 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"
|
msgctxt "#39200"
|
||||||
msgid "Log-out Plex Home User "
|
msgid "Log-out Plex Home User "
|
||||||
msgstr "Atjungti „Plex“ namų vartotoją"
|
msgstr "Atjungti „Plex“ namų vartotoją"
|
||||||
|
|
|
@ -44,6 +44,13 @@ msgstr ""
|
||||||
"Brīdinājums: Kodi iestatījums \"Atskaņot nākamo video automātiski\" ir "
|
"Brīdinājums: Kodi iestatījums \"Atskaņot nākamo video automātiski\" ir "
|
||||||
"ieslēgts. Tas var salauzt PKC. Izslēgt?"
|
"ieslēgts. Tas var salauzt PKC. Izslēgt?"
|
||||||
|
|
||||||
|
msgctxt "#30004"
|
||||||
|
msgid ""
|
||||||
|
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||||
|
"random password automatically if you haven't done so already. Please confirm"
|
||||||
|
" the next dialog that you want to enable the webserver now with Yes."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30005"
|
msgctxt "#30005"
|
||||||
msgid "Username: "
|
msgid "Username: "
|
||||||
msgstr "Lietotājvārds:"
|
msgstr "Lietotājvārds:"
|
||||||
|
@ -159,6 +166,14 @@ msgctxt "#30028"
|
||||||
msgid "PKC-only image caching completed"
|
msgid "PKC-only image caching completed"
|
||||||
msgstr "Tikai-PKC attēlu kešošana pabeigta"
|
msgstr "Tikai-PKC attēlu kešošana pabeigta"
|
||||||
|
|
||||||
|
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||||
|
msgctxt "#30029"
|
||||||
|
msgid ""
|
||||||
|
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
|
||||||
|
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
|
||||||
|
"database resets. Continue?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30030"
|
msgctxt "#30030"
|
||||||
msgid "Port Number"
|
msgid "Port Number"
|
||||||
msgstr "Porta Numurs"
|
msgstr "Porta Numurs"
|
||||||
|
@ -593,6 +608,11 @@ msgctxt "#30524"
|
||||||
msgid "Select Plex libraries to sync"
|
msgid "Select Plex libraries to sync"
|
||||||
msgstr "Izvēlies kuras Plex bibliotēkas sinhronizēt"
|
msgstr "Izvēlies kuras Plex bibliotēkas sinhronizēt"
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30525"
|
||||||
|
msgid "Skip intro"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30527"
|
msgctxt "#30527"
|
||||||
msgid "Ignore specials in next episodes"
|
msgid "Ignore specials in next episodes"
|
||||||
|
@ -657,8 +677,8 @@ msgstr "Lejupielādēt filmu komplektu/kolekciju attēlus no FanArtTV"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30541"
|
msgctxt "#30541"
|
||||||
msgid "Don't ask to pick a certain stream/quality"
|
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||||
msgstr "Nejautāt par konkrētas kvalitātes/straumes izvēli"
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30542"
|
msgctxt "#30542"
|
||||||
|
@ -679,6 +699,21 @@ msgctxt "#30545"
|
||||||
msgid "Force transcode pictures"
|
msgid "Force transcode pictures"
|
||||||
msgstr "Uzspiest attēlu pārkodēšanu"
|
msgstr "Uzspiest attēlu pārkodēšanu"
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30546"
|
||||||
|
msgid "Pick the first video if several versions are present"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30547"
|
||||||
|
msgid "Who picks the audio stream on playback start?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30548"
|
||||||
|
msgid "Who picks subtitles on playback start?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Welcome to Plex notification
|
# Welcome to Plex notification
|
||||||
msgctxt "#33000"
|
msgctxt "#33000"
|
||||||
msgid "Welcome"
|
msgid "Welcome"
|
||||||
|
@ -946,6 +981,11 @@ msgctxt "#39036"
|
||||||
msgid "Escape special characters in path (e.g. space to %20)"
|
msgid "Escape special characters in path (e.g. space to %20)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Customize Paths
|
||||||
|
msgctxt "#39090"
|
||||||
|
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Customize Paths
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39037"
|
msgctxt "#39037"
|
||||||
msgid "Original Plex MOVIE path to replace:"
|
msgid "Original Plex MOVIE path to replace:"
|
||||||
|
@ -1100,7 +1140,7 @@ msgstr "Uzspiest atjaunošanu Kodi ādiņai apturot atskaņošanu"
|
||||||
# PKC Settings - Appearance Tweaks
|
# PKC Settings - Appearance Tweaks
|
||||||
msgctxt "#39066"
|
msgctxt "#39066"
|
||||||
msgid "Recently Added: Also show already watched movies"
|
msgid "Recently Added: Also show already watched movies"
|
||||||
msgstr ""
|
msgstr "Nesen Pievienots: Rādīt arī jau skatītas filmas"
|
||||||
|
|
||||||
# PKC Settings - Connection
|
# PKC Settings - Connection
|
||||||
msgctxt "#39067"
|
msgctxt "#39067"
|
||||||
|
@ -1110,7 +1150,7 @@ msgstr "Tavs pašreizējais Plex Media Serveris:"
|
||||||
# PKC Settings - Connection
|
# PKC Settings - Connection
|
||||||
msgctxt "#39068"
|
msgctxt "#39068"
|
||||||
msgid "Manually enter Plex Media Server address"
|
msgid "Manually enter Plex Media Server address"
|
||||||
msgstr ""
|
msgstr "Pats ievadi Plex Media Server adresi"
|
||||||
|
|
||||||
# PKC Settings - Connection
|
# PKC Settings - Connection
|
||||||
msgctxt "#39069"
|
msgctxt "#39069"
|
||||||
|
@ -1127,6 +1167,11 @@ msgctxt "#39071"
|
||||||
msgid "Current plex.tv status:"
|
msgid "Current plex.tv status:"
|
||||||
msgstr "Pašreizējais plex.tv statuss:"
|
msgstr "Pašreizējais plex.tv statuss:"
|
||||||
|
|
||||||
|
# PKC Settings - Connection
|
||||||
|
msgctxt "#39072"
|
||||||
|
msgid "Background sync connection:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings, category name
|
# PKC Settings, category name
|
||||||
msgctxt "#39073"
|
msgctxt "#39073"
|
||||||
msgid "Appearance Tweaks"
|
msgid "Appearance Tweaks"
|
||||||
|
@ -1137,6 +1182,11 @@ msgctxt "#39074"
|
||||||
msgid "TV Shows"
|
msgid "TV Shows"
|
||||||
msgstr "Seriāli"
|
msgstr "Seriāli"
|
||||||
|
|
||||||
|
# PKC Settings - Sync
|
||||||
|
msgctxt "#39075"
|
||||||
|
msgid "Verify access to media files while synching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Pop-up during initial sync
|
# Pop-up during initial sync
|
||||||
msgctxt "#39076"
|
msgctxt "#39076"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1173,28 +1223,53 @@ msgstr ""
|
||||||
# Button text for choosing PKC mode
|
# Button text for choosing PKC mode
|
||||||
msgctxt "#39081"
|
msgctxt "#39081"
|
||||||
msgid "Add-on Paths"
|
msgid "Add-on Paths"
|
||||||
msgstr ""
|
msgstr "Spraudņu Ceļš"
|
||||||
|
|
||||||
# Button text for choosing PKC mode
|
# Button text for choosing PKC mode
|
||||||
msgctxt "#39082"
|
msgctxt "#39082"
|
||||||
msgid "Direct Paths"
|
msgid "Direct Paths"
|
||||||
msgstr ""
|
msgstr "Tiešie Ceļi"
|
||||||
|
|
||||||
# Dialog for manually entering PMS
|
# Dialog for manually entering PMS
|
||||||
msgctxt "#39083"
|
msgctxt "#39083"
|
||||||
msgid "Enter PMS IP or URL"
|
msgid "Enter PMS IP or URL"
|
||||||
msgstr ""
|
msgstr "Ievadi PMS IP vai URL"
|
||||||
|
|
||||||
# Dialog for manually entering PMS
|
# Dialog for manually entering PMS
|
||||||
msgctxt "#39084"
|
msgctxt "#39084"
|
||||||
msgid "Enter PMS port"
|
msgid "Enter PMS port"
|
||||||
msgstr ""
|
msgstr "Ievadi PMS portu"
|
||||||
|
|
||||||
# PKC settings - Appearance Tweaks
|
# PKC settings - Appearance Tweaks
|
||||||
msgctxt "#39085"
|
msgctxt "#39085"
|
||||||
msgid "Reload Kodi node files to apply all the settings below"
|
msgid "Reload Kodi node files to apply all the settings below"
|
||||||
msgstr ""
|
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"
|
msgctxt "#39200"
|
||||||
msgid "Log-out Plex Home User "
|
msgid "Log-out Plex Home User "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -1291,23 +1366,23 @@ msgstr "Tikai trūkstošo"
|
||||||
# Message in the PKC settings if user has not logged in to plex.tv
|
# Message in the PKC settings if user has not logged in to plex.tv
|
||||||
msgctxt "#39226"
|
msgctxt "#39226"
|
||||||
msgid "Not logged in to plex.tv"
|
msgid "Not logged in to plex.tv"
|
||||||
msgstr ""
|
msgstr "Nav pieteicies plex.tv"
|
||||||
|
|
||||||
# Message in the PKC settings if user is logged in to plex.tv
|
# Message in the PKC settings if user is logged in to plex.tv
|
||||||
msgctxt "#39227"
|
msgctxt "#39227"
|
||||||
msgid "Logged in to plex.tv"
|
msgid "Logged in to plex.tv"
|
||||||
msgstr ""
|
msgstr "Pieteicies plex.tv"
|
||||||
|
|
||||||
# Message in the PKC settings to display the plex.tv username
|
# Message in the PKC settings to display the plex.tv username
|
||||||
msgctxt "#39228"
|
msgctxt "#39228"
|
||||||
msgid "Plex admin user"
|
msgid "Plex admin user"
|
||||||
msgstr ""
|
msgstr "Plex admin user"
|
||||||
|
|
||||||
# Error message if user could not log in; the actual user name will be
|
# Error message if user could not log in; the actual user name will be
|
||||||
# appended at the end of the string
|
# appended at the end of the string
|
||||||
msgctxt "#39229"
|
msgctxt "#39229"
|
||||||
msgid "Login failed with plex.tv for user"
|
msgid "Login failed with plex.tv for user"
|
||||||
msgstr ""
|
msgstr "Lietotāja pieteikšanās plex.tv neizdevās"
|
||||||
|
|
||||||
# Message in the PKC settings to display the plex.tv username
|
# Message in the PKC settings to display the plex.tv username
|
||||||
msgctxt "#39230"
|
msgctxt "#39230"
|
||||||
|
@ -1472,7 +1547,7 @@ msgstr ""
|
||||||
# Addon Disclaimer
|
# Addon Disclaimer
|
||||||
msgctxt "#39705"
|
msgctxt "#39705"
|
||||||
msgid "Use at your own risk"
|
msgid "Use at your own risk"
|
||||||
msgstr ""
|
msgstr "Lieto uz savu atbildību"
|
||||||
|
|
||||||
# If user gets prompted to choose between several subtitles to burn in
|
# If user gets prompted to choose between several subtitles to burn in
|
||||||
msgctxt "#39706"
|
msgctxt "#39706"
|
||||||
|
@ -1530,7 +1605,7 @@ msgstr "Sinhronizēt"
|
||||||
# Shown during sync process
|
# Shown during sync process
|
||||||
msgctxt "#39715"
|
msgctxt "#39715"
|
||||||
msgid "Synching playlists"
|
msgid "Synching playlists"
|
||||||
msgstr ""
|
msgstr "Sinhronizē spēļsarakstus"
|
||||||
|
|
||||||
# Error message if an xml, e.g. advancedsettings.xml cannot be parsed (xml is
|
# 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}!
|
# screwed up; formated the wrong way). Do NOT replace {0} and {1}!
|
||||||
|
|
|
@ -48,6 +48,13 @@ msgstr ""
|
||||||
"Waarschuwing: De kodi instelling 'Automatisch volgende video afspelen' is "
|
"Waarschuwing: De kodi instelling 'Automatisch volgende video afspelen' is "
|
||||||
"actief. Dit kan voor problemen zorgen. Instelling deactiveren?"
|
"actief. Dit kan voor problemen zorgen. Instelling deactiveren?"
|
||||||
|
|
||||||
|
msgctxt "#30004"
|
||||||
|
msgid ""
|
||||||
|
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||||
|
"random password automatically if you haven't done so already. Please confirm"
|
||||||
|
" the next dialog that you want to enable the webserver now with Yes."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30005"
|
msgctxt "#30005"
|
||||||
msgid "Username: "
|
msgid "Username: "
|
||||||
msgstr "Gebruikersnaam: "
|
msgstr "Gebruikersnaam: "
|
||||||
|
@ -163,6 +170,14 @@ msgctxt "#30028"
|
||||||
msgid "PKC-only image caching completed"
|
msgid "PKC-only image caching completed"
|
||||||
msgstr "PKC afbeelding caching voltooid"
|
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"
|
msgctxt "#30030"
|
||||||
msgid "Port Number"
|
msgid "Port Number"
|
||||||
msgstr "Poortnummer"
|
msgstr "Poortnummer"
|
||||||
|
@ -601,6 +616,11 @@ msgctxt "#30524"
|
||||||
msgid "Select Plex libraries to sync"
|
msgid "Select Plex libraries to sync"
|
||||||
msgstr "Selecteer Plex-bibliotheken om te synchroniseren"
|
msgstr "Selecteer Plex-bibliotheken om te synchroniseren"
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30525"
|
||||||
|
msgid "Skip intro"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30527"
|
msgctxt "#30527"
|
||||||
msgid "Ignore specials in next episodes"
|
msgid "Ignore specials in next episodes"
|
||||||
|
@ -665,8 +685,8 @@ msgstr "Download film set/collectie artwork van FanArtTV"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30541"
|
msgctxt "#30541"
|
||||||
msgid "Don't ask to pick a certain stream/quality"
|
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||||
msgstr "Niet vragen om een bepaalde stream/kwaliteit te kiezen"
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30542"
|
msgctxt "#30542"
|
||||||
|
@ -687,6 +707,21 @@ msgctxt "#30545"
|
||||||
msgid "Force transcode pictures"
|
msgid "Force transcode pictures"
|
||||||
msgstr "Forceer transcoden van foto's"
|
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
|
# Welcome to Plex notification
|
||||||
msgctxt "#33000"
|
msgctxt "#33000"
|
||||||
msgid "Welcome"
|
msgid "Welcome"
|
||||||
|
@ -957,6 +992,11 @@ msgctxt "#39036"
|
||||||
msgid "Escape special characters in path (e.g. space to %20)"
|
msgid "Escape special characters in path (e.g. space to %20)"
|
||||||
msgstr "Pas speciale tekens aan in pad (b.v. spatie naar %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
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39037"
|
msgctxt "#39037"
|
||||||
msgid "Original Plex MOVIE path to replace:"
|
msgid "Original Plex MOVIE path to replace:"
|
||||||
|
@ -1139,6 +1179,11 @@ msgctxt "#39071"
|
||||||
msgid "Current plex.tv status:"
|
msgid "Current plex.tv status:"
|
||||||
msgstr "Huidige status van de plex.tv:"
|
msgstr "Huidige status van de plex.tv:"
|
||||||
|
|
||||||
|
# PKC Settings - Connection
|
||||||
|
msgctxt "#39072"
|
||||||
|
msgid "Background sync connection:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings, category name
|
# PKC Settings, category name
|
||||||
msgctxt "#39073"
|
msgctxt "#39073"
|
||||||
msgid "Appearance Tweaks"
|
msgid "Appearance Tweaks"
|
||||||
|
@ -1149,6 +1194,11 @@ msgctxt "#39074"
|
||||||
msgid "TV Shows"
|
msgid "TV Shows"
|
||||||
msgstr "TV series"
|
msgstr "TV series"
|
||||||
|
|
||||||
|
# PKC Settings - Sync
|
||||||
|
msgctxt "#39075"
|
||||||
|
msgid "Verify access to media files while synching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Pop-up during initial sync
|
# Pop-up during initial sync
|
||||||
msgctxt "#39076"
|
msgctxt "#39076"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1217,6 +1267,31 @@ msgstr ""
|
||||||
"Herlaad de Kodi node bestanden om alles onderstaande instellingen door te "
|
"Herlaad de Kodi node bestanden om alles onderstaande instellingen door te "
|
||||||
"voeren"
|
"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"
|
msgctxt "#39200"
|
||||||
msgid "Log-out Plex Home User "
|
msgid "Log-out Plex Home User "
|
||||||
msgstr "Log-out Plex Home gebruiker "
|
msgstr "Log-out Plex Home gebruiker "
|
||||||
|
|
|
@ -46,6 +46,13 @@ msgstr ""
|
||||||
"Advarsel: Kodi instilling \"Automatisk avspilling av neste video\" er "
|
"Advarsel: Kodi instilling \"Automatisk avspilling av neste video\" er "
|
||||||
"aktivert. Det kan medføre problemer med PKC. Ønsker du å deaktivere?"
|
"aktivert. Det kan medføre problemer med PKC. Ønsker du å deaktivere?"
|
||||||
|
|
||||||
|
msgctxt "#30004"
|
||||||
|
msgid ""
|
||||||
|
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||||
|
"random password automatically if you haven't done so already. Please confirm"
|
||||||
|
" the next dialog that you want to enable the webserver now with Yes."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30005"
|
msgctxt "#30005"
|
||||||
msgid "Username: "
|
msgid "Username: "
|
||||||
msgstr "Brukernavn:"
|
msgstr "Brukernavn:"
|
||||||
|
@ -165,6 +172,14 @@ msgctxt "#30028"
|
||||||
msgid "PKC-only image caching completed"
|
msgid "PKC-only image caching completed"
|
||||||
msgstr "PKC mellomlagring av bilder gjennomført"
|
msgstr "PKC mellomlagring av bilder gjennomført"
|
||||||
|
|
||||||
|
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||||
|
msgctxt "#30029"
|
||||||
|
msgid ""
|
||||||
|
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
|
||||||
|
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
|
||||||
|
"database resets. Continue?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30030"
|
msgctxt "#30030"
|
||||||
msgid "Port Number"
|
msgid "Port Number"
|
||||||
msgstr "Portnummer"
|
msgstr "Portnummer"
|
||||||
|
@ -600,6 +615,11 @@ msgctxt "#30524"
|
||||||
msgid "Select Plex libraries to sync"
|
msgid "Select Plex libraries to sync"
|
||||||
msgstr "Velg Plex bibliotek som skal synkroniseres"
|
msgstr "Velg Plex bibliotek som skal synkroniseres"
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30525"
|
||||||
|
msgid "Skip intro"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30527"
|
msgctxt "#30527"
|
||||||
msgid "Ignore specials in next episodes"
|
msgid "Ignore specials in next episodes"
|
||||||
|
@ -662,8 +682,8 @@ msgstr "Last ned filmsamling-kunst fra FanArtTV"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30541"
|
msgctxt "#30541"
|
||||||
msgid "Don't ask to pick a certain stream/quality"
|
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||||
msgstr "Ikke spør om å velge en utvalgt strøm/kvalitet"
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30542"
|
msgctxt "#30542"
|
||||||
|
@ -684,6 +704,21 @@ msgctxt "#30545"
|
||||||
msgid "Force transcode pictures"
|
msgid "Force transcode pictures"
|
||||||
msgstr "Tving transkoding av bilde"
|
msgstr "Tving transkoding av bilde"
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30546"
|
||||||
|
msgid "Pick the first video if several versions are present"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30547"
|
||||||
|
msgid "Who picks the audio stream on playback start?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30548"
|
||||||
|
msgid "Who picks subtitles on playback start?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Welcome to Plex notification
|
# Welcome to Plex notification
|
||||||
msgctxt "#33000"
|
msgctxt "#33000"
|
||||||
msgid "Welcome"
|
msgid "Welcome"
|
||||||
|
@ -951,6 +986,11 @@ msgctxt "#39036"
|
||||||
msgid "Escape special characters in path (e.g. space to %20)"
|
msgid "Escape special characters in path (e.g. space to %20)"
|
||||||
msgstr "Unngå spesielle tegn i stier (eksempel mellomrom til %20)"
|
msgstr "Unngå spesielle tegn i stier (eksempel mellomrom til %20)"
|
||||||
|
|
||||||
|
# PKC Settings - Customize Paths
|
||||||
|
msgctxt "#39090"
|
||||||
|
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Customize Paths
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39037"
|
msgctxt "#39037"
|
||||||
msgid "Original Plex MOVIE path to replace:"
|
msgid "Original Plex MOVIE path to replace:"
|
||||||
|
@ -1132,6 +1172,11 @@ msgctxt "#39071"
|
||||||
msgid "Current plex.tv status:"
|
msgid "Current plex.tv status:"
|
||||||
msgstr "Aktuell plex.tv statys:"
|
msgstr "Aktuell plex.tv statys:"
|
||||||
|
|
||||||
|
# PKC Settings - Connection
|
||||||
|
msgctxt "#39072"
|
||||||
|
msgid "Background sync connection:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings, category name
|
# PKC Settings, category name
|
||||||
msgctxt "#39073"
|
msgctxt "#39073"
|
||||||
msgid "Appearance Tweaks"
|
msgid "Appearance Tweaks"
|
||||||
|
@ -1142,6 +1187,11 @@ msgctxt "#39074"
|
||||||
msgid "TV Shows"
|
msgid "TV Shows"
|
||||||
msgstr "TV-show"
|
msgstr "TV-show"
|
||||||
|
|
||||||
|
# PKC Settings - Sync
|
||||||
|
msgctxt "#39075"
|
||||||
|
msgid "Verify access to media files while synching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Pop-up during initial sync
|
# Pop-up during initial sync
|
||||||
msgctxt "#39076"
|
msgctxt "#39076"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1207,6 +1257,31 @@ msgctxt "#39085"
|
||||||
msgid "Reload Kodi node files to apply all the settings below"
|
msgid "Reload Kodi node files to apply all the settings below"
|
||||||
msgstr ""
|
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"
|
msgctxt "#39200"
|
||||||
msgid "Log-out Plex Home User "
|
msgid "Log-out Plex Home User "
|
||||||
msgstr "Logg av Plex Home User"
|
msgstr "Logg av Plex Home User"
|
||||||
|
|
1608
resources/language/resource.language.pl_PL/strings.po
Normal file
1608
resources/language/resource.language.pl_PL/strings.po
Normal file
File diff suppressed because it is too large
Load diff
|
@ -45,6 +45,13 @@ msgstr ""
|
||||||
"Atenção: Configuração \"Iniciar próximo vídeo automaticamente\" está ativada"
|
"Atenção: Configuração \"Iniciar próximo vídeo automaticamente\" está ativada"
|
||||||
" no Kodi. Isto pode travar o PKC. Desativar?"
|
" 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"
|
msgctxt "#30005"
|
||||||
msgid "Username: "
|
msgid "Username: "
|
||||||
msgstr "Utilizador: "
|
msgstr "Utilizador: "
|
||||||
|
@ -162,6 +169,14 @@ msgctxt "#30028"
|
||||||
msgid "PKC-only image caching completed"
|
msgid "PKC-only image caching completed"
|
||||||
msgstr "Armazenamento PKC somente imagens finalizado"
|
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"
|
msgctxt "#30030"
|
||||||
msgid "Port Number"
|
msgid "Port Number"
|
||||||
msgstr "Número da Porta"
|
msgstr "Número da Porta"
|
||||||
|
@ -590,6 +605,11 @@ msgctxt "#30524"
|
||||||
msgid "Select Plex libraries to sync"
|
msgid "Select Plex libraries to sync"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30525"
|
||||||
|
msgid "Skip intro"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30527"
|
msgctxt "#30527"
|
||||||
msgid "Ignore specials in next episodes"
|
msgid "Ignore specials in next episodes"
|
||||||
|
@ -652,8 +672,8 @@ msgstr "Descarregar arte para o conjunto/coleção de filmes da FanArtTV "
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30541"
|
msgctxt "#30541"
|
||||||
msgid "Don't ask to pick a certain stream/quality"
|
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||||
msgstr "Não perguntar para escolher uma certa qualidade/transmissão"
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30542"
|
msgctxt "#30542"
|
||||||
|
@ -674,6 +694,21 @@ msgctxt "#30545"
|
||||||
msgid "Force transcode pictures"
|
msgid "Force transcode pictures"
|
||||||
msgstr "Forçar transcodificação de imagens"
|
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
|
# Welcome to Plex notification
|
||||||
msgctxt "#33000"
|
msgctxt "#33000"
|
||||||
msgid "Welcome"
|
msgid "Welcome"
|
||||||
|
@ -945,6 +980,11 @@ msgctxt "#39036"
|
||||||
msgid "Escape special characters in path (e.g. space to %20)"
|
msgid "Escape special characters in path (e.g. space to %20)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Customize Paths
|
||||||
|
msgctxt "#39090"
|
||||||
|
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Customize Paths
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39037"
|
msgctxt "#39037"
|
||||||
msgid "Original Plex MOVIE path to replace:"
|
msgid "Original Plex MOVIE path to replace:"
|
||||||
|
@ -1128,6 +1168,11 @@ msgctxt "#39071"
|
||||||
msgid "Current plex.tv status:"
|
msgid "Current plex.tv status:"
|
||||||
msgstr "Estado atual da plex.tv:"
|
msgstr "Estado atual da plex.tv:"
|
||||||
|
|
||||||
|
# PKC Settings - Connection
|
||||||
|
msgctxt "#39072"
|
||||||
|
msgid "Background sync connection:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings, category name
|
# PKC Settings, category name
|
||||||
msgctxt "#39073"
|
msgctxt "#39073"
|
||||||
msgid "Appearance Tweaks"
|
msgid "Appearance Tweaks"
|
||||||
|
@ -1138,6 +1183,11 @@ msgctxt "#39074"
|
||||||
msgid "TV Shows"
|
msgid "TV Shows"
|
||||||
msgstr "Programas de TV"
|
msgstr "Programas de TV"
|
||||||
|
|
||||||
|
# PKC Settings - Sync
|
||||||
|
msgctxt "#39075"
|
||||||
|
msgid "Verify access to media files while synching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Pop-up during initial sync
|
# Pop-up during initial sync
|
||||||
msgctxt "#39076"
|
msgctxt "#39076"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1200,6 +1250,31 @@ msgctxt "#39085"
|
||||||
msgid "Reload Kodi node files to apply all the settings below"
|
msgid "Reload Kodi node files to apply all the settings below"
|
||||||
msgstr ""
|
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"
|
msgctxt "#39200"
|
||||||
msgid "Log-out Plex Home User "
|
msgid "Log-out Plex Home User "
|
||||||
msgstr "Sair da sessão do Utilizador Caseiro Plex"
|
msgstr "Sair da sessão do Utilizador Caseiro Plex"
|
||||||
|
|
|
@ -44,6 +44,13 @@ msgid ""
|
||||||
"could break PKC. Deactivate?"
|
"could break PKC. Deactivate?"
|
||||||
msgstr ""
|
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"
|
msgctxt "#30005"
|
||||||
msgid "Username: "
|
msgid "Username: "
|
||||||
msgstr "Utilizador: "
|
msgstr "Utilizador: "
|
||||||
|
@ -163,6 +170,14 @@ msgctxt "#30028"
|
||||||
msgid "PKC-only image caching completed"
|
msgid "PKC-only image caching completed"
|
||||||
msgstr ""
|
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"
|
msgctxt "#30030"
|
||||||
msgid "Port Number"
|
msgid "Port Number"
|
||||||
msgstr "Número da Porta"
|
msgstr "Número da Porta"
|
||||||
|
@ -593,6 +608,11 @@ msgctxt "#30524"
|
||||||
msgid "Select Plex libraries to sync"
|
msgid "Select Plex libraries to sync"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30525"
|
||||||
|
msgid "Skip intro"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30527"
|
msgctxt "#30527"
|
||||||
msgid "Ignore specials in next episodes"
|
msgid "Ignore specials in next episodes"
|
||||||
|
@ -655,8 +675,8 @@ msgstr "Descarregar arte para o conjunto/coleção de filmes da FanArtTV "
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30541"
|
msgctxt "#30541"
|
||||||
msgid "Don't ask to pick a certain stream/quality"
|
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||||
msgstr "Não perguntar para escolher uma certa qualidade/transmissão"
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30542"
|
msgctxt "#30542"
|
||||||
|
@ -677,6 +697,21 @@ msgctxt "#30545"
|
||||||
msgid "Force transcode pictures"
|
msgid "Force transcode pictures"
|
||||||
msgstr "Forçar transcodificação de imagens"
|
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
|
# Welcome to Plex notification
|
||||||
msgctxt "#33000"
|
msgctxt "#33000"
|
||||||
msgid "Welcome"
|
msgid "Welcome"
|
||||||
|
@ -948,6 +983,11 @@ msgctxt "#39036"
|
||||||
msgid "Escape special characters in path (e.g. space to %20)"
|
msgid "Escape special characters in path (e.g. space to %20)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Customize Paths
|
||||||
|
msgctxt "#39090"
|
||||||
|
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Customize Paths
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39037"
|
msgctxt "#39037"
|
||||||
msgid "Original Plex MOVIE path to replace:"
|
msgid "Original Plex MOVIE path to replace:"
|
||||||
|
@ -1131,6 +1171,11 @@ msgctxt "#39071"
|
||||||
msgid "Current plex.tv status:"
|
msgid "Current plex.tv status:"
|
||||||
msgstr "Estado atual da plex.tv:"
|
msgstr "Estado atual da plex.tv:"
|
||||||
|
|
||||||
|
# PKC Settings - Connection
|
||||||
|
msgctxt "#39072"
|
||||||
|
msgid "Background sync connection:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings, category name
|
# PKC Settings, category name
|
||||||
msgctxt "#39073"
|
msgctxt "#39073"
|
||||||
msgid "Appearance Tweaks"
|
msgid "Appearance Tweaks"
|
||||||
|
@ -1141,6 +1186,11 @@ msgctxt "#39074"
|
||||||
msgid "TV Shows"
|
msgid "TV Shows"
|
||||||
msgstr "Programas de TV"
|
msgstr "Programas de TV"
|
||||||
|
|
||||||
|
# PKC Settings - Sync
|
||||||
|
msgctxt "#39075"
|
||||||
|
msgid "Verify access to media files while synching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Pop-up during initial sync
|
# Pop-up during initial sync
|
||||||
msgctxt "#39076"
|
msgctxt "#39076"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1203,6 +1253,31 @@ msgctxt "#39085"
|
||||||
msgid "Reload Kodi node files to apply all the settings below"
|
msgid "Reload Kodi node files to apply all the settings below"
|
||||||
msgstr ""
|
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"
|
msgctxt "#39200"
|
||||||
msgid "Log-out Plex Home User "
|
msgid "Log-out Plex Home User "
|
||||||
msgstr "Sair da sessão do Utilizador Caseiro Plex"
|
msgstr "Sair da sessão do Utilizador Caseiro Plex"
|
||||||
|
|
|
@ -49,6 +49,13 @@ msgstr ""
|
||||||
"Предупреждение: включена настройка Kodi «Воспроизвести следующее видео "
|
"Предупреждение: включена настройка Kodi «Воспроизвести следующее видео "
|
||||||
"автоматически». Это может сломать PKC. Деактивировать?"
|
"автоматически». Это может сломать 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"
|
msgctxt "#30005"
|
||||||
msgid "Username: "
|
msgid "Username: "
|
||||||
msgstr "Имя пользователя: "
|
msgstr "Имя пользователя: "
|
||||||
|
@ -166,6 +173,14 @@ msgctxt "#30028"
|
||||||
msgid "PKC-only image caching completed"
|
msgid "PKC-only image caching completed"
|
||||||
msgstr "Кеширование изображений PKC завершено"
|
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"
|
msgctxt "#30030"
|
||||||
msgid "Port Number"
|
msgid "Port Number"
|
||||||
msgstr "Порт"
|
msgstr "Порт"
|
||||||
|
@ -605,6 +620,11 @@ msgctxt "#30524"
|
||||||
msgid "Select Plex libraries to sync"
|
msgid "Select Plex libraries to sync"
|
||||||
msgstr "Выбор библиотек Plex для синхронизации"
|
msgstr "Выбор библиотек Plex для синхронизации"
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30525"
|
||||||
|
msgid "Skip intro"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30527"
|
msgctxt "#30527"
|
||||||
msgid "Ignore specials in next episodes"
|
msgid "Ignore specials in next episodes"
|
||||||
|
@ -669,8 +689,8 @@ msgstr "Загружать иллюстрации сборников с FanArtTV
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30541"
|
msgctxt "#30541"
|
||||||
msgid "Don't ask to pick a certain stream/quality"
|
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||||
msgstr "Не просить выбрать качество потока"
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30542"
|
msgctxt "#30542"
|
||||||
|
@ -691,6 +711,21 @@ msgctxt "#30545"
|
||||||
msgid "Force transcode pictures"
|
msgid "Force transcode pictures"
|
||||||
msgstr "Принудительно транскодировать изображения"
|
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
|
# Welcome to Plex notification
|
||||||
msgctxt "#33000"
|
msgctxt "#33000"
|
||||||
msgid "Welcome"
|
msgid "Welcome"
|
||||||
|
@ -961,6 +996,11 @@ msgctxt "#39036"
|
||||||
msgid "Escape special characters in path (e.g. space to %20)"
|
msgid "Escape special characters in path (e.g. space to %20)"
|
||||||
msgstr "Преобразуйте специальные символы в пути. (например пробел в %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
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39037"
|
msgctxt "#39037"
|
||||||
msgid "Original Plex MOVIE path to replace:"
|
msgid "Original Plex MOVIE path to replace:"
|
||||||
|
@ -1144,6 +1184,11 @@ msgctxt "#39071"
|
||||||
msgid "Current plex.tv status:"
|
msgid "Current plex.tv status:"
|
||||||
msgstr "Текущий статус на plex.tv:"
|
msgstr "Текущий статус на plex.tv:"
|
||||||
|
|
||||||
|
# PKC Settings - Connection
|
||||||
|
msgctxt "#39072"
|
||||||
|
msgid "Background sync connection:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings, category name
|
# PKC Settings, category name
|
||||||
msgctxt "#39073"
|
msgctxt "#39073"
|
||||||
msgid "Appearance Tweaks"
|
msgid "Appearance Tweaks"
|
||||||
|
@ -1154,6 +1199,11 @@ msgctxt "#39074"
|
||||||
msgid "TV Shows"
|
msgid "TV Shows"
|
||||||
msgstr "Сериалы"
|
msgstr "Сериалы"
|
||||||
|
|
||||||
|
# PKC Settings - Sync
|
||||||
|
msgctxt "#39075"
|
||||||
|
msgid "Verify access to media files while synching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Pop-up during initial sync
|
# Pop-up during initial sync
|
||||||
msgctxt "#39076"
|
msgctxt "#39076"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1219,6 +1269,31 @@ msgctxt "#39085"
|
||||||
msgid "Reload Kodi node files to apply all the settings below"
|
msgid "Reload Kodi node files to apply all the settings below"
|
||||||
msgstr "Перезаписать узлы БД Kodi, чтобы применить следующие настройки"
|
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"
|
msgctxt "#39200"
|
||||||
msgid "Log-out Plex Home User "
|
msgid "Log-out Plex Home User "
|
||||||
msgstr "Выйти из Plex"
|
msgstr "Выйти из Plex"
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
# Samuel Linde <samuel@linde.im>, 2018
|
# Samuel Linde <samuel@linde.im>, 2018
|
||||||
# Nisse Karlsson <transifex@xcorp.at>, 2019
|
# Nisse Karlsson <transifex@xcorp.at>, 2019
|
||||||
# Ludwig Johnson <public@ludwigjohnson.se>, 2019
|
# Ludwig Johnson <public@ludwigjohnson.se>, 2019
|
||||||
|
# namob <boman.d@gmail.com>, 2021
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -13,7 +14,7 @@ msgstr ""
|
||||||
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
|
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
|
||||||
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
|
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
|
||||||
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
|
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
|
||||||
"Last-Translator: Ludwig Johnson <public@ludwigjohnson.se>, 2019\n"
|
"Last-Translator: namob <boman.d@gmail.com>, 2021\n"
|
||||||
"Language-Team: Swedish (Sweden) (https://www.transifex.com/croneter/teams/73837/sv_SE/)\n"
|
"Language-Team: Swedish (Sweden) (https://www.transifex.com/croneter/teams/73837/sv_SE/)\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
@ -49,6 +50,13 @@ msgstr ""
|
||||||
"Varning: Kodi-inställningen \"Spela nästa video automatiskt\" är aktiverad. "
|
"Varning: Kodi-inställningen \"Spela nästa video automatiskt\" är aktiverad. "
|
||||||
"Detta kan orsaka problem med PKC. Vill du avaktivera?"
|
"Detta kan orsaka problem med PKC. Vill du avaktivera?"
|
||||||
|
|
||||||
|
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"
|
msgctxt "#30005"
|
||||||
msgid "Username: "
|
msgid "Username: "
|
||||||
msgstr "Användarnamn:"
|
msgstr "Användarnamn:"
|
||||||
|
@ -165,6 +173,14 @@ msgctxt "#30028"
|
||||||
msgid "PKC-only image caching completed"
|
msgid "PKC-only image caching completed"
|
||||||
msgstr "Cachelagring av PKC-bilder färdig"
|
msgstr "Cachelagring av PKC-bilder färdig"
|
||||||
|
|
||||||
|
# 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"
|
msgctxt "#30030"
|
||||||
msgid "Port Number"
|
msgid "Port Number"
|
||||||
msgstr "Portnummer"
|
msgstr "Portnummer"
|
||||||
|
@ -266,7 +282,7 @@ msgstr "Videokvalitet då omkodning krävs"
|
||||||
|
|
||||||
msgctxt "#30161"
|
msgctxt "#30161"
|
||||||
msgid "Auto-adjust transcoding quality (deactivate for Chromecast)"
|
msgid "Auto-adjust transcoding quality (deactivate for Chromecast)"
|
||||||
msgstr ""
|
msgstr "Justera automatiskt omkodningskvaliteten (inaktivera för Chromecast)"
|
||||||
|
|
||||||
msgctxt "#30165"
|
msgctxt "#30165"
|
||||||
msgid "Direct Play"
|
msgid "Direct Play"
|
||||||
|
@ -553,12 +569,12 @@ msgstr ""
|
||||||
# PKC Settings - Sync Options
|
# PKC Settings - Sync Options
|
||||||
msgctxt "#30515"
|
msgctxt "#30515"
|
||||||
msgid "Maximum items to request from the server at once"
|
msgid "Maximum items to request from the server at once"
|
||||||
msgstr "max antal föremåls begäran till server"
|
msgstr "Max antal föremål att fråga efter på en och samma gång"
|
||||||
|
|
||||||
# PKC Settings, category name
|
# PKC Settings, category name
|
||||||
msgctxt "#30516"
|
msgctxt "#30516"
|
||||||
msgid "Playback"
|
msgid "Playback"
|
||||||
msgstr "uppspelning"
|
msgstr "Uppspelning"
|
||||||
|
|
||||||
# PKC Settings - Connection
|
# PKC Settings - Connection
|
||||||
msgctxt "#30517"
|
msgctxt "#30517"
|
||||||
|
@ -578,17 +594,17 @@ msgstr "Fråga om uppspelning av trailers."
|
||||||
# PKC Settings - Plex
|
# PKC Settings - Plex
|
||||||
msgctxt "#30520"
|
msgctxt "#30520"
|
||||||
msgid "Skip PMS delete confirmation (use at your own risk)"
|
msgid "Skip PMS delete confirmation (use at your own risk)"
|
||||||
msgstr "Skippa PMS radera konfirmations meddelande (avnänd på egen risk)"
|
msgstr "Hoppa över PMS bekräftelse på att radera data (använd på egen risk)"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30521"
|
msgctxt "#30521"
|
||||||
msgid "Jump back on resume (in seconds)"
|
msgid "Jump back on resume (in seconds)"
|
||||||
msgstr "Spola tillbaka vid återuppta(i sekunder)"
|
msgstr "Spola tillbaka vid återuppta (i sekunder)"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30522"
|
msgctxt "#30522"
|
||||||
msgid "Force transcode h265/HEVC"
|
msgid "Force transcode h265/HEVC"
|
||||||
msgstr "Tvinga omkodning (trancoding) av h265/hevc"
|
msgstr "Tvinga omkodning av H.265/HEVC"
|
||||||
|
|
||||||
# PKC Settings - Sync Options
|
# PKC Settings - Sync Options
|
||||||
msgctxt "#30523"
|
msgctxt "#30523"
|
||||||
|
@ -600,46 +616,51 @@ msgctxt "#30524"
|
||||||
msgid "Select Plex libraries to sync"
|
msgid "Select Plex libraries to sync"
|
||||||
msgstr "Välj Plex-bibliotek att synkronisera "
|
msgstr "Välj Plex-bibliotek att synkronisera "
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30525"
|
||||||
|
msgid "Skip intro"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30527"
|
msgctxt "#30527"
|
||||||
msgid "Ignore specials in next episodes"
|
msgid "Ignore specials in next episodes"
|
||||||
msgstr "ignorera specialer av nästa episoder"
|
msgstr "ignorera specialer i nästa episoder"
|
||||||
|
|
||||||
msgctxt "#30528"
|
msgctxt "#30528"
|
||||||
msgid "Permanent users to add to the session"
|
msgid "Permanent users to add to the session"
|
||||||
msgstr "Permanenta användare tillägs till denna session"
|
msgstr "Permanenta användare att lägga till i sessionen"
|
||||||
|
|
||||||
# PKC Settings - Advanced
|
# PKC Settings - Advanced
|
||||||
msgctxt "#30529"
|
msgctxt "#30529"
|
||||||
msgid "Startup delay (in seconds)"
|
msgid "Startup delay (in seconds)"
|
||||||
msgstr "Uppstartnings dröjsmål (i sekunder)"
|
msgstr "Fördröjning vid uppstart (i sekunder)"
|
||||||
|
|
||||||
msgctxt "#30531"
|
msgctxt "#30531"
|
||||||
msgid "Enable new content notification"
|
msgid "Enable new content notification"
|
||||||
msgstr "Aktivera nytt innehål notifiering"
|
msgstr "Aktivera notifiering vid nytt innehåll"
|
||||||
|
|
||||||
msgctxt "#30532"
|
msgctxt "#30532"
|
||||||
msgid "Duration of the video library pop up (in seconds)"
|
msgid "Duration of the video library pop up (in seconds)"
|
||||||
msgstr "varaktighet av video biblioteks pop up(i sekunder)"
|
msgstr "Varaktighet av videobibliotekspopup (i sekunder)"
|
||||||
|
|
||||||
msgctxt "#30533"
|
msgctxt "#30533"
|
||||||
msgid "Duration of the music library pop up (in seconds)"
|
msgid "Duration of the music library pop up (in seconds)"
|
||||||
msgstr "varaktighet av musik biblioteks pop up(i sekunder)"
|
msgstr "Varaktighet av musikbibliotekspopup (i sekunder)"
|
||||||
|
|
||||||
msgctxt "#30534"
|
msgctxt "#30534"
|
||||||
msgid "Server messages"
|
msgid "Server messages"
|
||||||
msgstr "Server meddelanden"
|
msgstr "Servermeddelanden"
|
||||||
|
|
||||||
# PKC Settings - Advanced
|
# PKC Settings - Advanced
|
||||||
msgctxt "#30535"
|
msgctxt "#30535"
|
||||||
msgid "Generate a new unique Plex device Id (e.g. to clone Kodi)"
|
msgid "Generate a new unique Plex device Id (e.g. to clone Kodi)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Generera ett nytt unikt Plex enhets id (för att exempelvis klona Kodi)"
|
"Generera ett nytt unikt Plex enhets-id (för att exempelvis klona Kodi)"
|
||||||
|
|
||||||
# PKC Settings - Connection
|
# PKC Settings - Connection
|
||||||
msgctxt "#30536"
|
msgctxt "#30536"
|
||||||
msgid "Users must log in every time Kodi restarts"
|
msgid "Users must log in every time Kodi restarts"
|
||||||
msgstr "användare måste logga in varje gång kodi startas om"
|
msgstr "Användare måste logga in varje gång Kodi startas om"
|
||||||
|
|
||||||
# PKC Settings warning
|
# PKC Settings warning
|
||||||
msgctxt "#30537"
|
msgctxt "#30537"
|
||||||
|
@ -654,7 +675,7 @@ msgstr "Fullständig återställning av Kodi-databasen krävs, se \"Avancerad\""
|
||||||
# PKC Settings - Artwork
|
# PKC Settings - Artwork
|
||||||
msgctxt "#30539"
|
msgctxt "#30539"
|
||||||
msgid "Download additional art from FanArtTV"
|
msgid "Download additional art from FanArtTV"
|
||||||
msgstr "ladda ner extra affischer från FanArtTV"
|
msgstr "Ladda ner extra affischer från FanArtTV"
|
||||||
|
|
||||||
# PKC Settings - Artwork
|
# PKC Settings - Artwork
|
||||||
msgctxt "#30540"
|
msgctxt "#30540"
|
||||||
|
@ -663,8 +684,8 @@ msgstr "Ladda ner film set affischer från FanArtTV"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30541"
|
msgctxt "#30541"
|
||||||
msgid "Don't ask to pick a certain stream/quality"
|
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||||
msgstr "Fråga inte om välja stream kvalitet"
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30542"
|
msgctxt "#30542"
|
||||||
|
@ -678,12 +699,27 @@ msgstr "Föredra Kodi-bilder för kollektioner"
|
||||||
|
|
||||||
msgctxt "#30544"
|
msgctxt "#30544"
|
||||||
msgid "Artwork"
|
msgid "Artwork"
|
||||||
msgstr "affischer"
|
msgstr "Affischer"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30545"
|
msgctxt "#30545"
|
||||||
msgid "Force transcode pictures"
|
msgid "Force transcode pictures"
|
||||||
msgstr "tvinga omkodning(transcoding) av bilder"
|
msgstr "Tvinga omkodning av bilder"
|
||||||
|
|
||||||
|
# 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
|
# Welcome to Plex notification
|
||||||
msgctxt "#33000"
|
msgctxt "#33000"
|
||||||
|
@ -708,18 +744,18 @@ msgstr "Server är online"
|
||||||
# Plex notification when we need to transcode
|
# Plex notification when we need to transcode
|
||||||
msgctxt "#33004"
|
msgctxt "#33004"
|
||||||
msgid "PMS enforced transcoding"
|
msgid "PMS enforced transcoding"
|
||||||
msgstr ""
|
msgstr "PMS-tvingad omkodning"
|
||||||
|
|
||||||
# Plex notification when we need to use direct streaming (instead of
|
# Plex notification when we need to use direct streaming (instead of
|
||||||
# transcoding)
|
# transcoding)
|
||||||
msgctxt "#33005"
|
msgctxt "#33005"
|
||||||
msgid "PMS enforced direct streaming"
|
msgid "PMS enforced direct streaming"
|
||||||
msgstr ""
|
msgstr "PMS-tvingad direkt ström"
|
||||||
|
|
||||||
# Error notification
|
# Error notification
|
||||||
msgctxt "#33009"
|
msgctxt "#33009"
|
||||||
msgid "Invalid username or password"
|
msgid "Invalid username or password"
|
||||||
msgstr "fel användarnamn eller lösenord"
|
msgstr "Fel användarnamn eller lösenord"
|
||||||
|
|
||||||
msgctxt "#33010"
|
msgctxt "#33010"
|
||||||
msgid "User is unauthorized for server {0}"
|
msgid "User is unauthorized for server {0}"
|
||||||
|
@ -732,7 +768,7 @@ msgstr "Plex.tv skickade inte en lista över giltiga Plex-användare."
|
||||||
# Dialog before playback
|
# Dialog before playback
|
||||||
msgctxt "#33013"
|
msgctxt "#33013"
|
||||||
msgid "Choose the audio stream"
|
msgid "Choose the audio stream"
|
||||||
msgstr "välj ljudfil"
|
msgstr "Välj ljudström"
|
||||||
|
|
||||||
# Dialog before playback
|
# Dialog before playback
|
||||||
msgctxt "#33014"
|
msgctxt "#33014"
|
||||||
|
@ -742,19 +778,20 @@ msgstr "Välj undertext"
|
||||||
# Dialog before playback
|
# Dialog before playback
|
||||||
msgctxt "#33016"
|
msgctxt "#33016"
|
||||||
msgid "Play trailers?"
|
msgid "Play trailers?"
|
||||||
msgstr "spela upp trailer?"
|
msgstr "Spela upp trailer?"
|
||||||
|
|
||||||
# Error message
|
# Error message
|
||||||
msgctxt "#33032"
|
msgctxt "#33032"
|
||||||
msgid ""
|
msgid ""
|
||||||
"Failed to generate a new device Id. See your logs for more information."
|
"Failed to generate a new device Id. See your logs for more information."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"misslyckades med att generera nytt enhets id. kolla logs för mer information"
|
"Misslyckades med att generera nytt enhets-id. Kontrollera loggar för mer "
|
||||||
|
"information."
|
||||||
|
|
||||||
# Pop-up informing about Kodi restart
|
# Pop-up informing about Kodi restart
|
||||||
msgctxt "#33033"
|
msgctxt "#33033"
|
||||||
msgid "Kodi will now restart to apply the changes."
|
msgid "Kodi will now restart to apply the changes."
|
||||||
msgstr "Kodi kommer startas om för att applicera inställningar"
|
msgstr "Kodi kommer startas om för att applicera förändringarna"
|
||||||
|
|
||||||
# Confirmation dialog before item gets deleted from the PMS
|
# Confirmation dialog before item gets deleted from the PMS
|
||||||
msgctxt "#33041"
|
msgctxt "#33041"
|
||||||
|
@ -762,22 +799,22 @@ msgid ""
|
||||||
"Delete file(s) from Plex Server? This will also delete the file(s) from "
|
"Delete file(s) from Plex Server? This will also delete the file(s) from "
|
||||||
"disk!"
|
"disk!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"radera filer från plex server?filer kommer också raderas från hårddisk"
|
"Radera fil(er) från Plexserver? Fil(er) kommer också raderas från hårddisk!"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#39000"
|
msgctxt "#39000"
|
||||||
msgid "- Number of trailers to play before a movie"
|
msgid "- Number of trailers to play before a movie"
|
||||||
msgstr "-antal trailers att spela för filmen"
|
msgstr "- Antal trailers att spela innan en film"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#39001"
|
msgctxt "#39001"
|
||||||
msgid "Boost audio when transcoding"
|
msgid "Boost audio when transcoding"
|
||||||
msgstr "öka ljudet när det omkodas(transcoding)"
|
msgstr "Öka ljudet när det omkodas"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#39002"
|
msgctxt "#39002"
|
||||||
msgid "Burnt-in subtitle size"
|
msgid "Burnt-in subtitle size"
|
||||||
msgstr "inbränd undertext storlek"
|
msgstr "Storlek på inbränd undertext"
|
||||||
|
|
||||||
# PKC Settings - Sync
|
# PKC Settings - Sync
|
||||||
msgctxt "#39003"
|
msgctxt "#39003"
|
||||||
|
@ -787,47 +824,47 @@ msgstr "Antal samtidiga nedladdningstrådar"
|
||||||
# PKC Settings - Plex
|
# PKC Settings - Plex
|
||||||
msgctxt "#39004"
|
msgctxt "#39004"
|
||||||
msgid "Enable Plex Companion (restart Kodi!)"
|
msgid "Enable Plex Companion (restart Kodi!)"
|
||||||
msgstr "aktivera Plex Companion (restart kodi)"
|
msgstr "Aktivera Plex Companion (kräver omstart av Kodi!)"
|
||||||
|
|
||||||
# PKC Settings - Plex
|
# PKC Settings - Plex
|
||||||
msgctxt "#39005"
|
msgctxt "#39005"
|
||||||
msgid "Plex Companion Port (change only if needed)"
|
msgid "Plex Companion Port (change only if needed)"
|
||||||
msgstr "Plex Companion Port(ändra bara om det är nödvändigt)"
|
msgstr "Plex Companion Port (ändra bara om det är nödvändigt)"
|
||||||
|
|
||||||
# PKC Settings - Plex
|
# PKC Settings - Plex
|
||||||
msgctxt "#39008"
|
msgctxt "#39008"
|
||||||
msgid "Plex Companion: Allows flinging media to Kodi through Plex"
|
msgid "Plex Companion: Allows flinging media to Kodi through Plex"
|
||||||
msgstr "Plex companion: tillåt strömmning av media till kodi från plex."
|
msgstr "Plex Companion: tillåt strömmning av media till Kodi från Plex."
|
||||||
|
|
||||||
# Error message
|
# Error message
|
||||||
msgctxt "#39009"
|
msgctxt "#39009"
|
||||||
msgid "Could not login to plex.tv. Please try signing in again."
|
msgid "Could not login to plex.tv. Please try signing in again."
|
||||||
msgstr "kunde inte logga in tillplex.tv. Försök logga in igen."
|
msgstr "Kunde inte logga in till plex.tv. Försök logga in igen."
|
||||||
|
|
||||||
# Error message
|
# Error message
|
||||||
msgctxt "#39010"
|
msgctxt "#39010"
|
||||||
msgid "Problems connecting to plex.tv. Network or internet issue?"
|
msgid "Problems connecting to plex.tv. Network or internet issue?"
|
||||||
msgstr "problem att ansluta till plex.tv. nätverk eller interna fel."
|
msgstr "Problem att ansluta till plex.tv. Nätverks- eller internetproblem?"
|
||||||
|
|
||||||
# Error message
|
# Error message
|
||||||
msgctxt "#39011"
|
msgctxt "#39011"
|
||||||
msgid "Could not find any Plex server in the network. Aborting..."
|
msgid "Could not find any Plex server in the network. Aborting..."
|
||||||
msgstr "kunde inte hitta plex server på nätverket. avbryter."
|
msgstr "Kunde inte hitta Plex-server på nätverket. Avbryter..."
|
||||||
|
|
||||||
# Dialog text for choosing PMS
|
# Dialog text for choosing PMS
|
||||||
msgctxt "#39012"
|
msgctxt "#39012"
|
||||||
msgid "Choose your Plex server"
|
msgid "Choose your Plex server"
|
||||||
msgstr "välj din plex server."
|
msgstr "Välj din Plex-server"
|
||||||
|
|
||||||
# Error message
|
# Error message
|
||||||
msgctxt "#39013"
|
msgctxt "#39013"
|
||||||
msgid "Not yet authorized for Plex server "
|
msgid "Not yet authorized for Plex server "
|
||||||
msgstr "inte authoriserad ännu"
|
msgstr "Ännu inte auktoriserad för Plex-servern"
|
||||||
|
|
||||||
# Error message
|
# Error message
|
||||||
msgctxt "#39014"
|
msgctxt "#39014"
|
||||||
msgid "Please sign in to plex.tv."
|
msgid "Please sign in to plex.tv."
|
||||||
msgstr "logga in på plex.tv"
|
msgstr "Vänligen logga in mot plex.tv"
|
||||||
|
|
||||||
# Error message
|
# Error message
|
||||||
msgctxt "#39015"
|
msgctxt "#39015"
|
||||||
|
@ -840,8 +877,9 @@ msgid ""
|
||||||
"Disable Plex music library? (It is HIGHLY recommended to use Plex music only"
|
"Disable Plex music library? (It is HIGHLY recommended to use Plex music only"
|
||||||
" with direct paths for large music libraries. Kodi might crash otherwise)"
|
" with direct paths for large music libraries. Kodi might crash otherwise)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"avaktivera Plex music bibliotek (rekommenderat att endast använda plex musik"
|
"Inaktivera Plex musikbibliotek? (Det är STARKT rekommenderat att endast "
|
||||||
" med direkt paths till stora musik bibliotek.)"
|
"använda Direct Path tillsammans med stora musikbibliotek, Kodi kan krasha "
|
||||||
|
"annars)"
|
||||||
|
|
||||||
# Pop-up on initial sync
|
# Pop-up on initial sync
|
||||||
msgctxt "#39017"
|
msgctxt "#39017"
|
||||||
|
@ -875,17 +913,17 @@ msgstr "lokal"
|
||||||
# Error message
|
# Error message
|
||||||
msgctxt "#39023"
|
msgctxt "#39023"
|
||||||
msgid "Failed to authenticate. Did you login to plex.tv?"
|
msgid "Failed to authenticate. Did you login to plex.tv?"
|
||||||
msgstr "misslyckade att authentisera. Har du loggat in på plex.tv"
|
msgstr "Misslyckades med autentisering. Har du loggat in på plex.tv?"
|
||||||
|
|
||||||
# PKC Settings - Plex
|
# PKC Settings - Plex
|
||||||
msgctxt "#39025"
|
msgctxt "#39025"
|
||||||
msgid "Automatically log into plex.tv on startup"
|
msgid "Automatically log into plex.tv on startup"
|
||||||
msgstr "automatiskt logga in på plex.tv vid start"
|
msgstr "Logga in automatiskt på plex.tv vid start"
|
||||||
|
|
||||||
# PKC Settings - Sync
|
# PKC Settings - Sync
|
||||||
msgctxt "#39026"
|
msgctxt "#39026"
|
||||||
msgid "Enable constant background sync"
|
msgid "Enable constant background sync"
|
||||||
msgstr "aktivera konstant bakgrunds synkronisering"
|
msgstr "Aktivera konstant bakgrundssynkronisering"
|
||||||
|
|
||||||
# Pop-up on initial sync
|
# Pop-up on initial sync
|
||||||
msgctxt "#39028"
|
msgctxt "#39028"
|
||||||
|
@ -895,14 +933,15 @@ msgid ""
|
||||||
"shares need to use direct paths (e.g. smb://myNAS/mymovie.mkv or "
|
"shares need to use direct paths (e.g. smb://myNAS/mymovie.mkv or "
|
||||||
"\\\\myNAS/mymovie.mkv)!"
|
"\\\\myNAS/mymovie.mkv)!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"VARNING! om du väljer native läge, kanske du förlorar tillgång till vissa plex funktioner som t.ex.\n"
|
"VARNING! Om du väljer \"Nativt\" läge kan du förlora tillgång till vissa "
|
||||||
"plex trailer och omkodning(transcoding) alternativ. alla plex shares behöver använda direct paths\n"
|
"Plex-funktioner såsom Plextrailer och omkodningsalternativ. ALLA "
|
||||||
"(t.ex. smb://myNAS/mymovie.mkv or \\\\myNAS/mymovie.mkv)!"
|
"Plexutdelningar måste använda Direct Path (t.ex smb://myNAS/mymovie.mkv "
|
||||||
|
"eller \\\\myNAS\\mymovie.mkv)!"
|
||||||
|
|
||||||
# Pop-up on initial sync
|
# Pop-up on initial sync
|
||||||
msgctxt "#39029"
|
msgctxt "#39029"
|
||||||
msgid "Network credentials"
|
msgid "Network credentials"
|
||||||
msgstr "nätverks inloggningsuppgifter"
|
msgstr "Inloggningsuppgifter för nätverk"
|
||||||
|
|
||||||
# Pop-up on initial sync
|
# Pop-up on initial sync
|
||||||
msgctxt "#39030"
|
msgctxt "#39030"
|
||||||
|
@ -921,7 +960,7 @@ msgid ""
|
||||||
"Kodi cannot locate the file %s. Please verify your PKC settings. Stop "
|
"Kodi cannot locate the file %s. Please verify your PKC settings. Stop "
|
||||||
"syncing?"
|
"syncing?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Kodi kan inte hitta filen 1%s. verifiera pkc inställningar. sluta synka?"
|
"Kodi kan inte hitta filen %s. Verifiera PKC-inställningar. Avsluta synk?"
|
||||||
|
|
||||||
# Pop-up on initial sync
|
# Pop-up on initial sync
|
||||||
msgctxt "#39033"
|
msgctxt "#39033"
|
||||||
|
@ -929,13 +968,13 @@ msgid ""
|
||||||
"Transform Plex UNC library paths \\\\myNas\\mymovie.mkv automatically to smb"
|
"Transform Plex UNC library paths \\\\myNas\\mymovie.mkv automatically to smb"
|
||||||
" paths, smb://myNas/mymovie.mkv? (recommended)"
|
" paths, smb://myNas/mymovie.mkv? (recommended)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"omvandla plex unc biblioteks paths \\\\myNas\\mymovie.mkv automatiskt till smb delningar.\n"
|
"Omvandla Plex UNC-sökvägar \\\\myNas\\mymovie.mkv automatiskt till SMB-sökvägar \n"
|
||||||
"smb://myNas/mymovie.mkv? (rekommenderas)"
|
"smb://myNas/mymovie.mkv? (rekommenderas)"
|
||||||
|
|
||||||
# PKC Settings - Customize Paths
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39034"
|
msgctxt "#39034"
|
||||||
msgid "Replace Plex UNC paths \\\\myNas with smb://myNas"
|
msgid "Replace Plex UNC paths \\\\myNas with smb://myNas"
|
||||||
msgstr "Ersätt Plex UNC sökväg \\\\myNas med smb://myNas"
|
msgstr "Ersätt Plex UNC-sökväg \\\\myNas med smb://myNas"
|
||||||
|
|
||||||
# PKC Settings - Customize Paths
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39035"
|
msgctxt "#39035"
|
||||||
|
@ -943,18 +982,23 @@ msgid ""
|
||||||
"Replace Plex paths /volume1/media or \\\\myserver\\media with custom SMB "
|
"Replace Plex paths /volume1/media or \\\\myserver\\media with custom SMB "
|
||||||
"paths smb://NAS/mystuff"
|
"paths smb://NAS/mystuff"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Ersätt Plex sökväg /volume1/media eller \\\\myserver\\media med anpassade "
|
"Ersätt Plex-sökväg /volume1/media eller \\\\myserver\\media med anpassade "
|
||||||
"SMB sökvägar smb://NAS/mystuff"
|
"SMB-sökvägar smb://NAS/mystuff"
|
||||||
|
|
||||||
# PKC Settings - Customize Paths
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39036"
|
msgctxt "#39036"
|
||||||
msgid "Escape special characters in path (e.g. space to %20)"
|
msgid "Escape special characters in path (e.g. space to %20)"
|
||||||
msgstr "Omkoda specialtecken i sökväg (exempelvis mellanslag som %20)"
|
msgstr "Omkoda specialtecken i sökväg (exempelvis mellanslag som %20)"
|
||||||
|
|
||||||
|
# PKC Settings - Customize Paths
|
||||||
|
msgctxt "#39090"
|
||||||
|
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||||
|
msgstr "Säkra karaktärer för http(s), dav(s) och (s)ftp URLer"
|
||||||
|
|
||||||
# PKC Settings - Customize Paths
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39037"
|
msgctxt "#39037"
|
||||||
msgid "Original Plex MOVIE path to replace:"
|
msgid "Original Plex MOVIE path to replace:"
|
||||||
msgstr "Ursprunglig Plex MOVIE sökväg att ersätta."
|
msgstr "Ursprunglig Plex MOVIE-sökväg att ersätta:"
|
||||||
|
|
||||||
# PKC Settings - Customize Paths
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39038"
|
msgctxt "#39038"
|
||||||
|
@ -964,7 +1008,7 @@ msgstr "Ersätt Plex MOVIE med:"
|
||||||
# PKC Settings - Customize Paths
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39039"
|
msgctxt "#39039"
|
||||||
msgid "Original Plex TV SHOWS path to replace:"
|
msgid "Original Plex TV SHOWS path to replace:"
|
||||||
msgstr "Ursprunglig Plex TV SHOWS sökväg att ersätta."
|
msgstr "Ursprunglig Plex TV SHOWS-sökväg att ersätta:"
|
||||||
|
|
||||||
# PKC Settings - Customize Paths
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39040"
|
msgctxt "#39040"
|
||||||
|
@ -974,7 +1018,7 @@ msgstr "Ersätt Plex TV SHOWS med:"
|
||||||
# PKC Settings - Customize Paths
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39041"
|
msgctxt "#39041"
|
||||||
msgid "Original Plex MUSIC path to replace:"
|
msgid "Original Plex MUSIC path to replace:"
|
||||||
msgstr "Ursprunglig Plex MUSIC sökväg att ersätta."
|
msgstr "Ursprunglig Plex MUSIC-sökväg att ersätta:"
|
||||||
|
|
||||||
# PKC Settings - Customize Paths
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39042"
|
msgctxt "#39042"
|
||||||
|
@ -997,8 +1041,8 @@ msgid ""
|
||||||
"Please enter your custom smb paths in the settings under \"Sync Options\" "
|
"Please enter your custom smb paths in the settings under \"Sync Options\" "
|
||||||
"and then restart Kodi"
|
"and then restart Kodi"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Ange din anpassade smb-sökväg i inställningarna under \"Synkroniserings "
|
"Ange din anpassade SMB-sökväg i inställningarna under "
|
||||||
"inställningar\" och starta sedan om Kodi"
|
"\"Synkroniseringsinställningar\" och starta om Kodi"
|
||||||
|
|
||||||
# PKC Settings - Customize Paths
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39045"
|
msgctxt "#39045"
|
||||||
|
@ -1033,12 +1077,12 @@ msgstr "Välj en Plex Server från en lista"
|
||||||
# PKC Settings - Sync
|
# PKC Settings - Sync
|
||||||
msgctxt "#39051"
|
msgctxt "#39051"
|
||||||
msgid "Wait before sync new/changed PMS item [s]"
|
msgid "Wait before sync new/changed PMS item [s]"
|
||||||
msgstr "Vänta före synkronisering av nya/ändrade PMS objekt"
|
msgstr "Vänta före synkronisering av nya/ändrade PMS-objekt"
|
||||||
|
|
||||||
# PKC Settings - Sync
|
# PKC Settings - Sync
|
||||||
msgctxt "#39052"
|
msgctxt "#39052"
|
||||||
msgid "Background Sync"
|
msgid "Background Sync"
|
||||||
msgstr "Bakgrundssynkning."
|
msgstr "Bakgrundssynkronisering."
|
||||||
|
|
||||||
# PKC Settings - Sync
|
# PKC Settings - Sync
|
||||||
msgctxt "#39053"
|
msgctxt "#39053"
|
||||||
|
@ -1060,6 +1104,8 @@ msgctxt "#39056"
|
||||||
msgid ""
|
msgid ""
|
||||||
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
|
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Används av sync samt vid användning av Direct Paths. Starta om Kodi vid "
|
||||||
|
"förändringar!"
|
||||||
|
|
||||||
# PKC Settings, category name
|
# PKC Settings, category name
|
||||||
msgctxt "#39057"
|
msgctxt "#39057"
|
||||||
|
@ -1133,6 +1179,11 @@ msgctxt "#39071"
|
||||||
msgid "Current plex.tv status:"
|
msgid "Current plex.tv status:"
|
||||||
msgstr "Nuvarande plex.tv status:"
|
msgstr "Nuvarande plex.tv status:"
|
||||||
|
|
||||||
|
# PKC Settings - Connection
|
||||||
|
msgctxt "#39072"
|
||||||
|
msgid "Background sync connection:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings, category name
|
# PKC Settings, category name
|
||||||
msgctxt "#39073"
|
msgctxt "#39073"
|
||||||
msgid "Appearance Tweaks"
|
msgid "Appearance Tweaks"
|
||||||
|
@ -1143,6 +1194,11 @@ msgctxt "#39074"
|
||||||
msgid "TV Shows"
|
msgid "TV Shows"
|
||||||
msgstr "TV-Serier"
|
msgstr "TV-Serier"
|
||||||
|
|
||||||
|
# PKC Settings - Sync
|
||||||
|
msgctxt "#39075"
|
||||||
|
msgid "Verify access to media files while synching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Pop-up during initial sync
|
# Pop-up during initial sync
|
||||||
msgctxt "#39076"
|
msgctxt "#39076"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1160,7 +1216,7 @@ msgstr "Maximalt antal filmer att visa i widgets"
|
||||||
# PKC Settings - Plex
|
# PKC Settings - Plex
|
||||||
msgctxt "#39078"
|
msgctxt "#39078"
|
||||||
msgid "Plex Companion Update Port (change only if needed)"
|
msgid "Plex Companion Update Port (change only if needed)"
|
||||||
msgstr "Plex Companion Update port (ändra bara vid behov)"
|
msgstr "Plex Companion Update-port (ändra bara vid behov)"
|
||||||
|
|
||||||
# Error message
|
# Error message
|
||||||
msgctxt "#39079"
|
msgctxt "#39079"
|
||||||
|
@ -1168,7 +1224,7 @@ msgid ""
|
||||||
"Plex Companion could not open the GDM port. Please change it in the PKC "
|
"Plex Companion could not open the GDM port. Please change it in the PKC "
|
||||||
"settings."
|
"settings."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Plex Companion kunde inte öppna GDM porten. Ändra den i PKC inställningarna."
|
"Plex Companion kunde inte öppna GDM-porten. Ändra den i PKC-inställningarna."
|
||||||
|
|
||||||
# Pop-up on initial sync.
|
# Pop-up on initial sync.
|
||||||
# Check that next translations for Add-on Paths and Direct Paths are
|
# Check that next translations for Add-on Paths and Direct Paths are
|
||||||
|
@ -1205,7 +1261,32 @@ msgstr "Ange PMS port"
|
||||||
# PKC settings - Appearance Tweaks
|
# PKC settings - Appearance Tweaks
|
||||||
msgctxt "#39085"
|
msgctxt "#39085"
|
||||||
msgid "Reload Kodi node files to apply all the settings below"
|
msgid "Reload Kodi node files to apply all the settings below"
|
||||||
msgstr "Ladda om Kodi nodfiler för att applicera alla inställningar nedan"
|
msgstr "Ladda om Kodi-nodfiler för att applicera alla inställningar nedan"
|
||||||
|
|
||||||
|
# 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"
|
msgctxt "#39200"
|
||||||
msgid "Log-out Plex Home User "
|
msgid "Log-out Plex Home User "
|
||||||
|
@ -1223,8 +1304,8 @@ msgstr "Utför manuell bibliotekssynkronisering"
|
||||||
msgctxt "#39205"
|
msgctxt "#39205"
|
||||||
msgid "Unable to run the sync, the add-on is not connected to a Plex server."
|
msgid "Unable to run the sync, the add-on is not connected to a Plex server."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Kunde inte köra synkronisering, tillägget är inte ansluten till en Plex "
|
"Kunde inte köra synkronisering, tillägget är inte ansluten till en "
|
||||||
"server."
|
"Plexserver."
|
||||||
|
|
||||||
msgctxt "#39206"
|
msgctxt "#39206"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1267,7 +1348,7 @@ msgstr "Ange din Plex Media Server IP eller URL, exempelvis:"
|
||||||
|
|
||||||
msgctxt "#39217"
|
msgctxt "#39217"
|
||||||
msgid "Use HTTPS (SSL) connections? Answer should probably be yes."
|
msgid "Use HTTPS (SSL) connections? Answer should probably be yes."
|
||||||
msgstr ""
|
msgstr "Används HTTPS(SSL)-anslutningar? Svaret bör nog vara ja."
|
||||||
|
|
||||||
msgctxt "#39218"
|
msgctxt "#39218"
|
||||||
msgid "Error contacting PMS"
|
msgid "Error contacting PMS"
|
||||||
|
@ -1403,7 +1484,7 @@ msgstr ""
|
||||||
|
|
||||||
msgctxt "#39402"
|
msgctxt "#39402"
|
||||||
msgid " may not work correctly until the database is reset."
|
msgid " may not work correctly until the database is reset."
|
||||||
msgstr "fungerar kanske inte fören databasen är återställd. "
|
msgstr "fungerar kanske inte förrän databasen är återställd. "
|
||||||
|
|
||||||
msgctxt "#39403"
|
msgctxt "#39403"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1516,7 +1597,7 @@ msgstr "Använd på egen risk"
|
||||||
# If user gets prompted to choose between several subtitles to burn in
|
# If user gets prompted to choose between several subtitles to burn in
|
||||||
msgctxt "#39706"
|
msgctxt "#39706"
|
||||||
msgid "Don't burn-in any subtitle"
|
msgid "Don't burn-in any subtitle"
|
||||||
msgstr ""
|
msgstr "Använd inte några inbrända undertexter"
|
||||||
|
|
||||||
# If user gets prompted to choose between several audio/subtitle tracks and
|
# If user gets prompted to choose between several audio/subtitle tracks and
|
||||||
# language is unknown
|
# language is unknown
|
||||||
|
@ -1556,7 +1637,7 @@ msgstr ""
|
||||||
# Shown during sync process
|
# Shown during sync process
|
||||||
msgctxt "#39712"
|
msgctxt "#39712"
|
||||||
msgid "downloaded"
|
msgid "downloaded"
|
||||||
msgstr "Nedladdade"
|
msgstr "nedladdade"
|
||||||
|
|
||||||
# Shown during sync process
|
# Shown during sync process
|
||||||
msgctxt "#39713"
|
msgctxt "#39713"
|
||||||
|
|
|
@ -44,6 +44,13 @@ msgstr ""
|
||||||
"Попередження: налаштування Kodi \"відтворювати наступне відео автоматично\" "
|
"Попередження: налаштування Kodi \"відтворювати наступне відео автоматично\" "
|
||||||
"включено. Це може перервати роботу PKC. Вимкнути?"
|
"включено. Це може перервати роботу 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"
|
msgctxt "#30005"
|
||||||
msgid "Username: "
|
msgid "Username: "
|
||||||
msgstr "Ім'я користувача:"
|
msgstr "Ім'я користувача:"
|
||||||
|
@ -160,6 +167,14 @@ msgctxt "#30028"
|
||||||
msgid "PKC-only image caching completed"
|
msgid "PKC-only image caching completed"
|
||||||
msgstr "Кешування зображень PKC завершено"
|
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"
|
msgctxt "#30030"
|
||||||
msgid "Port Number"
|
msgid "Port Number"
|
||||||
msgstr "Номер порту"
|
msgstr "Номер порту"
|
||||||
|
@ -600,6 +615,11 @@ msgctxt "#30524"
|
||||||
msgid "Select Plex libraries to sync"
|
msgid "Select Plex libraries to sync"
|
||||||
msgstr "Обрати бібліотеки Plex для синхронізації"
|
msgstr "Обрати бібліотеки Plex для синхронізації"
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30525"
|
||||||
|
msgid "Skip intro"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30527"
|
msgctxt "#30527"
|
||||||
msgid "Ignore specials in next episodes"
|
msgid "Ignore specials in next episodes"
|
||||||
|
@ -664,8 +684,8 @@ msgstr "Завантажувати матеріали набору фільмі
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30541"
|
msgctxt "#30541"
|
||||||
msgid "Don't ask to pick a certain stream/quality"
|
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||||
msgstr "Не запитувати обирання певного потоку або якості"
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30542"
|
msgctxt "#30542"
|
||||||
|
@ -686,6 +706,21 @@ msgctxt "#30545"
|
||||||
msgid "Force transcode pictures"
|
msgid "Force transcode pictures"
|
||||||
msgstr "Примусове перекодування зображень"
|
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
|
# Welcome to Plex notification
|
||||||
msgctxt "#33000"
|
msgctxt "#33000"
|
||||||
msgid "Welcome"
|
msgid "Welcome"
|
||||||
|
@ -957,6 +992,11 @@ msgctxt "#39036"
|
||||||
msgid "Escape special characters in path (e.g. space to %20)"
|
msgid "Escape special characters in path (e.g. space to %20)"
|
||||||
msgstr "Замінювати спеціальні символи у шляхах (наприклад, пробіл у %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
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39037"
|
msgctxt "#39037"
|
||||||
msgid "Original Plex MOVIE path to replace:"
|
msgid "Original Plex MOVIE path to replace:"
|
||||||
|
@ -1141,6 +1181,11 @@ msgctxt "#39071"
|
||||||
msgid "Current plex.tv status:"
|
msgid "Current plex.tv status:"
|
||||||
msgstr "Поточний plex.tv статус:"
|
msgstr "Поточний plex.tv статус:"
|
||||||
|
|
||||||
|
# PKC Settings - Connection
|
||||||
|
msgctxt "#39072"
|
||||||
|
msgid "Background sync connection:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings, category name
|
# PKC Settings, category name
|
||||||
msgctxt "#39073"
|
msgctxt "#39073"
|
||||||
msgid "Appearance Tweaks"
|
msgid "Appearance Tweaks"
|
||||||
|
@ -1151,6 +1196,11 @@ msgctxt "#39074"
|
||||||
msgid "TV Shows"
|
msgid "TV Shows"
|
||||||
msgstr "Серіали"
|
msgstr "Серіали"
|
||||||
|
|
||||||
|
# PKC Settings - Sync
|
||||||
|
msgctxt "#39075"
|
||||||
|
msgid "Verify access to media files while synching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Pop-up during initial sync
|
# Pop-up during initial sync
|
||||||
msgctxt "#39076"
|
msgctxt "#39076"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1218,6 +1268,31 @@ msgid "Reload Kodi node files to apply all the settings below"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Перезавантажити файли вузла Kodi для застосування всіх наступних налаштувань"
|
"Перезавантажити файли вузла 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"
|
msgctxt "#39200"
|
||||||
msgid "Log-out Plex Home User "
|
msgid "Log-out Plex Home User "
|
||||||
msgstr "Вийти з профілю користувача Plex Home"
|
msgstr "Вийти з профілю користувача Plex Home"
|
||||||
|
@ -1528,7 +1603,7 @@ msgstr "Використовуйте на свій ризик"
|
||||||
# If user gets prompted to choose between several subtitles to burn in
|
# If user gets prompted to choose between several subtitles to burn in
|
||||||
msgctxt "#39706"
|
msgctxt "#39706"
|
||||||
msgid "Don't burn-in any subtitle"
|
msgid "Don't burn-in any subtitle"
|
||||||
msgstr ""
|
msgstr "Не виводити жодних субтитрів"
|
||||||
|
|
||||||
# If user gets prompted to choose between several audio/subtitle tracks and
|
# If user gets prompted to choose between several audio/subtitle tracks and
|
||||||
# language is unknown
|
# language is unknown
|
||||||
|
|
|
@ -44,6 +44,13 @@ msgid ""
|
||||||
"could break PKC. Deactivate?"
|
"could break PKC. Deactivate?"
|
||||||
msgstr ""
|
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"
|
msgctxt "#30005"
|
||||||
msgid "Username: "
|
msgid "Username: "
|
||||||
msgstr "用户名 "
|
msgstr "用户名 "
|
||||||
|
@ -159,6 +166,14 @@ msgctxt "#30028"
|
||||||
msgid "PKC-only image caching completed"
|
msgid "PKC-only image caching completed"
|
||||||
msgstr ""
|
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"
|
msgctxt "#30030"
|
||||||
msgid "Port Number"
|
msgid "Port Number"
|
||||||
msgstr "端口号"
|
msgstr "端口号"
|
||||||
|
@ -585,6 +600,11 @@ msgctxt "#30524"
|
||||||
msgid "Select Plex libraries to sync"
|
msgid "Select Plex libraries to sync"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30525"
|
||||||
|
msgid "Skip intro"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30527"
|
msgctxt "#30527"
|
||||||
msgid "Ignore specials in next episodes"
|
msgid "Ignore specials in next episodes"
|
||||||
|
@ -647,8 +667,8 @@ msgstr "从FanArtTV下载额外的电影集/收藏art"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30541"
|
msgctxt "#30541"
|
||||||
msgid "Don't ask to pick a certain stream/quality"
|
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||||
msgstr "无需询问挑选特定的串流/质量"
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30542"
|
msgctxt "#30542"
|
||||||
|
@ -669,6 +689,21 @@ msgctxt "#30545"
|
||||||
msgid "Force transcode pictures"
|
msgid "Force transcode pictures"
|
||||||
msgstr "强制图片转码"
|
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
|
# Welcome to Plex notification
|
||||||
msgctxt "#33000"
|
msgctxt "#33000"
|
||||||
msgid "Welcome"
|
msgid "Welcome"
|
||||||
|
@ -918,6 +953,11 @@ msgctxt "#39036"
|
||||||
msgid "Escape special characters in path (e.g. space to %20)"
|
msgid "Escape special characters in path (e.g. space to %20)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Customize Paths
|
||||||
|
msgctxt "#39090"
|
||||||
|
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Customize Paths
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39037"
|
msgctxt "#39037"
|
||||||
msgid "Original Plex MOVIE path to replace:"
|
msgid "Original Plex MOVIE path to replace:"
|
||||||
|
@ -1097,6 +1137,11 @@ msgctxt "#39071"
|
||||||
msgid "Current plex.tv status:"
|
msgid "Current plex.tv status:"
|
||||||
msgstr "当前plex.tv状态:"
|
msgstr "当前plex.tv状态:"
|
||||||
|
|
||||||
|
# PKC Settings - Connection
|
||||||
|
msgctxt "#39072"
|
||||||
|
msgid "Background sync connection:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings, category name
|
# PKC Settings, category name
|
||||||
msgctxt "#39073"
|
msgctxt "#39073"
|
||||||
msgid "Appearance Tweaks"
|
msgid "Appearance Tweaks"
|
||||||
|
@ -1107,6 +1152,11 @@ msgctxt "#39074"
|
||||||
msgid "TV Shows"
|
msgid "TV Shows"
|
||||||
msgstr "电视节目"
|
msgstr "电视节目"
|
||||||
|
|
||||||
|
# PKC Settings - Sync
|
||||||
|
msgctxt "#39075"
|
||||||
|
msgid "Verify access to media files while synching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Pop-up during initial sync
|
# Pop-up during initial sync
|
||||||
msgctxt "#39076"
|
msgctxt "#39076"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1165,6 +1215,31 @@ msgctxt "#39085"
|
||||||
msgid "Reload Kodi node files to apply all the settings below"
|
msgid "Reload Kodi node files to apply all the settings below"
|
||||||
msgstr ""
|
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"
|
msgctxt "#39200"
|
||||||
msgid "Log-out Plex Home User "
|
msgid "Log-out Plex Home User "
|
||||||
msgstr "退出Plex家庭用户 "
|
msgstr "退出Plex家庭用户 "
|
||||||
|
|
|
@ -42,6 +42,13 @@ msgid ""
|
||||||
"could break PKC. Deactivate?"
|
"could break PKC. Deactivate?"
|
||||||
msgstr ""
|
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"
|
msgctxt "#30005"
|
||||||
msgid "Username: "
|
msgid "Username: "
|
||||||
msgstr "使用者: "
|
msgstr "使用者: "
|
||||||
|
@ -157,6 +164,14 @@ msgctxt "#30028"
|
||||||
msgid "PKC-only image caching completed"
|
msgid "PKC-only image caching completed"
|
||||||
msgstr ""
|
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"
|
msgctxt "#30030"
|
||||||
msgid "Port Number"
|
msgid "Port Number"
|
||||||
msgstr "埠號"
|
msgstr "埠號"
|
||||||
|
@ -583,6 +598,11 @@ msgctxt "#30524"
|
||||||
msgid "Select Plex libraries to sync"
|
msgid "Select Plex libraries to sync"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Playback
|
||||||
|
msgctxt "#30525"
|
||||||
|
msgid "Skip intro"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30527"
|
msgctxt "#30527"
|
||||||
msgid "Ignore specials in next episodes"
|
msgid "Ignore specials in next episodes"
|
||||||
|
@ -645,8 +665,8 @@ msgstr "從 FanArtTV 下載電影合輯海報"
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30541"
|
msgctxt "#30541"
|
||||||
msgid "Don't ask to pick a certain stream/quality"
|
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||||
msgstr "不要要求挑選特定的 串流/品質"
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30542"
|
msgctxt "#30542"
|
||||||
|
@ -667,6 +687,21 @@ msgctxt "#30545"
|
||||||
msgid "Force transcode pictures"
|
msgid "Force transcode pictures"
|
||||||
msgstr "強制圖片轉碼"
|
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
|
# Welcome to Plex notification
|
||||||
msgctxt "#33000"
|
msgctxt "#33000"
|
||||||
msgid "Welcome"
|
msgid "Welcome"
|
||||||
|
@ -916,6 +951,11 @@ msgctxt "#39036"
|
||||||
msgid "Escape special characters in path (e.g. space to %20)"
|
msgid "Escape special characters in path (e.g. space to %20)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Customize Paths
|
||||||
|
msgctxt "#39090"
|
||||||
|
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings - Customize Paths
|
# PKC Settings - Customize Paths
|
||||||
msgctxt "#39037"
|
msgctxt "#39037"
|
||||||
msgid "Original Plex MOVIE path to replace:"
|
msgid "Original Plex MOVIE path to replace:"
|
||||||
|
@ -1093,6 +1133,11 @@ msgctxt "#39071"
|
||||||
msgid "Current plex.tv status:"
|
msgid "Current plex.tv status:"
|
||||||
msgstr "plex.tv 狀態︰"
|
msgstr "plex.tv 狀態︰"
|
||||||
|
|
||||||
|
# PKC Settings - Connection
|
||||||
|
msgctxt "#39072"
|
||||||
|
msgid "Background sync connection:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# PKC Settings, category name
|
# PKC Settings, category name
|
||||||
msgctxt "#39073"
|
msgctxt "#39073"
|
||||||
msgid "Appearance Tweaks"
|
msgid "Appearance Tweaks"
|
||||||
|
@ -1103,6 +1148,11 @@ msgctxt "#39074"
|
||||||
msgid "TV Shows"
|
msgid "TV Shows"
|
||||||
msgstr "電視節目"
|
msgstr "電視節目"
|
||||||
|
|
||||||
|
# PKC Settings - Sync
|
||||||
|
msgctxt "#39075"
|
||||||
|
msgid "Verify access to media files while synching"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
# Pop-up during initial sync
|
# Pop-up during initial sync
|
||||||
msgctxt "#39076"
|
msgctxt "#39076"
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1161,6 +1211,31 @@ msgctxt "#39085"
|
||||||
msgid "Reload Kodi node files to apply all the settings below"
|
msgid "Reload Kodi node files to apply all the settings below"
|
||||||
msgstr ""
|
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"
|
msgctxt "#39200"
|
||||||
msgid "Log-out Plex Home User "
|
msgid "Log-out Plex Home User "
|
||||||
msgstr "登出Plex Home用戶 "
|
msgstr "登出Plex Home用戶 "
|
||||||
|
|
|
@ -15,6 +15,8 @@ class Account(object):
|
||||||
self.plex_username = None
|
self.plex_username = None
|
||||||
self.plex_user_id = None
|
self.plex_user_id = None
|
||||||
self.plex_token = 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.pms_token = None
|
||||||
self.avatar = None
|
self.avatar = None
|
||||||
self.myplexlogin = None
|
self.myplexlogin = None
|
||||||
|
|
|
@ -19,6 +19,8 @@ class App(object):
|
||||||
def __init__(self, entrypoint=False):
|
def __init__(self, entrypoint=False):
|
||||||
self.fetch_pms_item_number = None
|
self.fetch_pms_item_number = None
|
||||||
self.force_reload_skin = None
|
self.force_reload_skin = None
|
||||||
|
# All thread instances
|
||||||
|
self.threads = []
|
||||||
if entrypoint:
|
if entrypoint:
|
||||||
self.load_entrypoint()
|
self.load_entrypoint()
|
||||||
else:
|
else:
|
||||||
|
@ -45,12 +47,12 @@ class App(object):
|
||||||
self.monitor = None
|
self.monitor = None
|
||||||
# xbmc.Player() instance
|
# xbmc.Player() instance
|
||||||
self.player = None
|
self.player = None
|
||||||
# All thread instances
|
|
||||||
self.threads = []
|
|
||||||
# Instance of FanartThread()
|
# Instance of FanartThread()
|
||||||
self.fanart_thread = None
|
self.fanart_thread = None
|
||||||
# Instance of ImageCachingThread()
|
# Instance of ImageCachingThread()
|
||||||
self.caching_thread = None
|
self.caching_thread = None
|
||||||
|
# Dialog to skip intro
|
||||||
|
self.skip_intro_dialog = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_playing(self):
|
def is_playing(self):
|
||||||
|
|
|
@ -57,8 +57,6 @@ class Sync(object):
|
||||||
|
|
||||||
# How often shall we sync?
|
# How often shall we sync?
|
||||||
self.full_sync_intervall = None
|
self.full_sync_intervall = None
|
||||||
# Background Sync disabled?
|
|
||||||
self.background_sync_disabled = None
|
|
||||||
# How long shall we wait with synching a new item to make sure Plex got all
|
# How long shall we wait with synching a new item to make sure Plex got all
|
||||||
# metadata?
|
# metadata?
|
||||||
self.backgroundsync_saftymargin = None
|
self.backgroundsync_saftymargin = None
|
||||||
|
@ -81,7 +79,6 @@ class Sync(object):
|
||||||
# List of section_ids we're synching to Kodi - will be automatically
|
# List of section_ids we're synching to Kodi - will be automatically
|
||||||
# re-built if sections are set a-new
|
# re-built if sections are set a-new
|
||||||
self.section_ids = set()
|
self.section_ids = set()
|
||||||
self.enable_alexa = None
|
|
||||||
|
|
||||||
self.load()
|
self.load()
|
||||||
|
|
||||||
|
@ -122,8 +119,6 @@ class Sync(object):
|
||||||
Any settings unrelated to syncs to the Kodi database - can thus be
|
Any settings unrelated to syncs to the Kodi database - can thus be
|
||||||
safely reset without a Kodi reboot
|
safely reset without a Kodi reboot
|
||||||
"""
|
"""
|
||||||
self.background_sync_disabled = utils.settings('enableBackgroundSync') == 'false'
|
|
||||||
self.enable_alexa = utils.settings('enable_alexa') == 'true'
|
|
||||||
self.sync_dialog = utils.settings('dbSyncIndicator') == 'true'
|
self.sync_dialog = utils.settings('dbSyncIndicator') == 'true'
|
||||||
self.full_sync_intervall = int(utils.settings('fullSyncInterval')) * 60
|
self.full_sync_intervall = int(utils.settings('fullSyncInterval')) * 60
|
||||||
self.backgroundsync_saftymargin = int(utils.settings('backgroundsync_saftyMargin'))
|
self.backgroundsync_saftymargin = int(utils.settings('backgroundsync_saftyMargin'))
|
||||||
|
|
|
@ -36,7 +36,8 @@ class PlayState(object):
|
||||||
'muted': False,
|
'muted': False,
|
||||||
'playmethod': None,
|
'playmethod': None,
|
||||||
'playcount': None,
|
'playcount': None,
|
||||||
'external_player': False # bool - xbmc.Player().isExternalPlayer()
|
'external_player': False, # bool - xbmc.Player().isExternalPlayer()
|
||||||
|
'intro_markers': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
|
@ -82,7 +82,7 @@ class ImageCachingThread(backgroundthread.KillableThread):
|
||||||
for url in self._url_generator(kind, kodi_type):
|
for url in self._url_generator(kind, kodi_type):
|
||||||
if self.should_suspend() or self.should_cancel():
|
if self.should_suspend() or self.should_cancel():
|
||||||
return False
|
return False
|
||||||
cache_url(url)
|
cache_url(url, self.should_suspend)
|
||||||
# Toggles Image caching completed to Yes
|
# Toggles Image caching completed to Yes
|
||||||
utils.settings('plex_status_image_caching', value=utils.lang(107))
|
utils.settings('plex_status_image_caching', value=utils.lang(107))
|
||||||
return True
|
return True
|
||||||
|
@ -95,7 +95,7 @@ class ImageCachingThread(backgroundthread.KillableThread):
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def cache_url(url):
|
def cache_url(url, should_suspend=None):
|
||||||
url = double_urlencode(url)
|
url = double_urlencode(url)
|
||||||
sleeptime = 0
|
sleeptime = 0
|
||||||
while True:
|
while True:
|
||||||
|
@ -113,11 +113,11 @@ def cache_url(url):
|
||||||
# download. All is well
|
# download. All is well
|
||||||
break
|
break
|
||||||
except requests.ConnectionError:
|
except requests.ConnectionError:
|
||||||
if app.APP.stop_pkc:
|
if app.APP.stop_pkc or (should_suspend and should_suspend()):
|
||||||
# Kodi terminated
|
|
||||||
break
|
break
|
||||||
# Server thinks its a DOS attack, ('error 10053')
|
# Server thinks its a DOS attack, ('error 10053')
|
||||||
# Wait before trying again
|
# Wait before trying again
|
||||||
|
# OR: Kodi refuses Webserver connection (no password set)
|
||||||
if sleeptime > 5:
|
if sleeptime > 5:
|
||||||
LOG.error('Repeatedly got ConnectionError for url %s',
|
LOG.error('Repeatedly got ConnectionError for url %s',
|
||||||
double_urldecode(url))
|
double_urldecode(url))
|
||||||
|
|
|
@ -135,38 +135,6 @@ class ProcessingQueue(Queue.Queue, object):
|
||||||
def _qsize(self):
|
def _qsize(self):
|
||||||
return self._current_queue._qsize() if self._current_queue else 0
|
return self._current_queue._qsize() if self._current_queue else 0
|
||||||
|
|
||||||
def _total_qsize(self):
|
|
||||||
return sum(q._qsize() for q in self._queues) if self._queues else 0
|
|
||||||
|
|
||||||
def put(self, item, block=True, timeout=None):
|
|
||||||
"""
|
|
||||||
PKC customization of Queue.put. item needs to be the tuple
|
|
||||||
(count [int], {'section': [Section], 'xml': [etree xml]})
|
|
||||||
"""
|
|
||||||
self.not_full.acquire()
|
|
||||||
try:
|
|
||||||
if self.maxsize > 0:
|
|
||||||
if not block:
|
|
||||||
if self._total_qsize() == self.maxsize:
|
|
||||||
raise Queue.Full
|
|
||||||
elif timeout is None:
|
|
||||||
while self._total_qsize() == self.maxsize:
|
|
||||||
self.not_full.wait()
|
|
||||||
elif timeout < 0:
|
|
||||||
raise ValueError("'timeout' must be a non-negative number")
|
|
||||||
else:
|
|
||||||
endtime = _time() + timeout
|
|
||||||
while self._total_qsize() == self.maxsize:
|
|
||||||
remaining = endtime - _time()
|
|
||||||
if remaining <= 0.0:
|
|
||||||
raise Queue.Full
|
|
||||||
self.not_full.wait(remaining)
|
|
||||||
self._put(item)
|
|
||||||
self.unfinished_tasks += 1
|
|
||||||
self.not_empty.notify()
|
|
||||||
finally:
|
|
||||||
self.not_full.release()
|
|
||||||
|
|
||||||
def _put(self, item):
|
def _put(self, item):
|
||||||
for i, section in enumerate(self._sections):
|
for i, section in enumerate(self._sections):
|
||||||
if item[1]['section'] == section:
|
if item[1]['section'] == section:
|
||||||
|
@ -183,16 +151,13 @@ class ProcessingQueue(Queue.Queue, object):
|
||||||
Once the get()-method returns None, you've received the sentinel and
|
Once the get()-method returns None, you've received the sentinel and
|
||||||
you've thus exhausted the queue
|
you've thus exhausted the queue
|
||||||
"""
|
"""
|
||||||
self.not_full.acquire()
|
with self.not_full:
|
||||||
try:
|
|
||||||
section.number_of_items = 1
|
section.number_of_items = 1
|
||||||
self._add_section(section)
|
self._add_section(section)
|
||||||
# Add the actual sentinel to the queue we just added
|
# Add the actual sentinel to the queue we just added
|
||||||
self._queues[-1]._put((None, None))
|
self._queues[-1]._put((None, None))
|
||||||
self.unfinished_tasks += 1
|
self.unfinished_tasks += 1
|
||||||
self.not_empty.notify()
|
self.not_empty.notify()
|
||||||
finally:
|
|
||||||
self.not_full.release()
|
|
||||||
|
|
||||||
def add_section(self, section):
|
def add_section(self, section):
|
||||||
"""
|
"""
|
||||||
|
@ -202,11 +167,26 @@ class ProcessingQueue(Queue.Queue, object):
|
||||||
Be sure to set section.number_of_items correctly as it will signal
|
Be sure to set section.number_of_items correctly as it will signal
|
||||||
when processing is completely done for a specific section!
|
when processing is completely done for a specific section!
|
||||||
"""
|
"""
|
||||||
self.mutex.acquire()
|
with self.mutex:
|
||||||
try:
|
|
||||||
self._add_section(section)
|
self._add_section(section)
|
||||||
finally:
|
|
||||||
self.mutex.release()
|
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):
|
def _add_section(self, section):
|
||||||
self._sections.append(section)
|
self._sections.append(section)
|
||||||
|
|
|
@ -49,7 +49,7 @@ def convert_alexa_to_companion(dictionary):
|
||||||
"""
|
"""
|
||||||
The params passed by Alexa must first be converted to Companion talk
|
The params passed by Alexa must first be converted to Companion talk
|
||||||
"""
|
"""
|
||||||
for key in dictionary:
|
for key in list(dictionary):
|
||||||
if key in v.ALEXA_TO_COMPANION:
|
if key in v.ALEXA_TO_COMPANION:
|
||||||
dictionary[v.ALEXA_TO_COMPANION[key]] = dictionary[key]
|
dictionary[v.ALEXA_TO_COMPANION[key]] = dictionary[key]
|
||||||
del dictionary[key]
|
del dictionary[key]
|
||||||
|
|
|
@ -4,18 +4,13 @@ import sqlite3
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from . import variables as v, app
|
from . import variables as v, app
|
||||||
|
from .exceptions import LockedDatabase
|
||||||
|
|
||||||
DB_WRITE_ATTEMPTS = 100
|
DB_WRITE_ATTEMPTS = 100
|
||||||
|
DB_WRITE_ATTEMPTS_TIMEOUT = 1 # in seconds
|
||||||
DB_CONNECTION_TIMEOUT = 10
|
DB_CONNECTION_TIMEOUT = 10
|
||||||
|
|
||||||
|
|
||||||
class LockedDatabase(Exception):
|
|
||||||
"""
|
|
||||||
Dedicated class to make sure we're not silently catching locked DBs.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def catch_operationalerrors(method):
|
def catch_operationalerrors(method):
|
||||||
"""
|
"""
|
||||||
sqlite.OperationalError is raised immediately if another DB connection
|
sqlite.OperationalError is raised immediately if another DB connection
|
||||||
|
@ -43,7 +38,7 @@ def catch_operationalerrors(method):
|
||||||
self.kodiconn.commit()
|
self.kodiconn.commit()
|
||||||
if self.artconn:
|
if self.artconn:
|
||||||
self.artconn.commit()
|
self.artconn.commit()
|
||||||
if app.APP.monitor.waitForAbort(0.1):
|
if app.APP.monitor.waitForAbort(DB_WRITE_ATTEMPTS_TIMEOUT):
|
||||||
# PKC needs to quit
|
# PKC needs to quit
|
||||||
return
|
return
|
||||||
# Start new transactions
|
# Start new transactions
|
||||||
|
|
|
@ -224,7 +224,11 @@ class DownloadUtils():
|
||||||
if r.status_code != 401:
|
if r.status_code != 401:
|
||||||
self.count_unauthorized = 0
|
self.count_unauthorized = 0
|
||||||
|
|
||||||
if r.status_code == 204:
|
if return_response is True:
|
||||||
|
# return the entire response object
|
||||||
|
return r
|
||||||
|
|
||||||
|
elif r.status_code == 204:
|
||||||
# No body in the response
|
# No body in the response
|
||||||
# But read (empty) content to release connection back to pool
|
# But read (empty) content to release connection back to pool
|
||||||
# (see requests: keep-alive documentation)
|
# (see requests: keep-alive documentation)
|
||||||
|
@ -258,9 +262,6 @@ class DownloadUtils():
|
||||||
elif r.status_code in (200, 201):
|
elif r.status_code in (200, 201):
|
||||||
# 200: OK
|
# 200: OK
|
||||||
# 201: Created
|
# 201: Created
|
||||||
if return_response is True:
|
|
||||||
# return the entire response object
|
|
||||||
return r
|
|
||||||
try:
|
try:
|
||||||
# xml response
|
# xml response
|
||||||
r = utils.defused_etree.fromstring(r.content)
|
r = utils.defused_etree.fromstring(r.content)
|
||||||
|
|
|
@ -7,6 +7,7 @@ e.g. plugin://... calls. Hence be careful to only rely on window variables.
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
import sys
|
import sys
|
||||||
|
import copy
|
||||||
|
|
||||||
import xbmc
|
import xbmc
|
||||||
import xbmcplugin
|
import xbmcplugin
|
||||||
|
@ -423,6 +424,7 @@ def hub(content_type):
|
||||||
# We need to make sure that only entries that WORK are displayed
|
# We need to make sure that only entries that WORK are displayed
|
||||||
# WARNING: using xml.remove(child) in for-loop requires traversing from
|
# WARNING: using xml.remove(child) in for-loop requires traversing from
|
||||||
# the end!
|
# the end!
|
||||||
|
pkc_cont_watching = None
|
||||||
for entry in reversed(xml):
|
for entry in reversed(xml):
|
||||||
api = API(entry)
|
api = API(entry)
|
||||||
append = False
|
append = False
|
||||||
|
@ -439,6 +441,21 @@ def hub(content_type):
|
||||||
append = True
|
append = True
|
||||||
if not append:
|
if not append:
|
||||||
xml.remove(entry)
|
xml.remove(entry)
|
||||||
|
|
||||||
|
# HACK ##################
|
||||||
|
# Merge Plex's "Continue watching" with "On deck"
|
||||||
|
if entry.get('key') == '/hubs/home/continueWatching':
|
||||||
|
pkc_cont_watching = copy.deepcopy(entry)
|
||||||
|
pkc_cont_watching.set('key', '/hubs/continueWatching')
|
||||||
|
title = pkc_cont_watching.get('title') or 'Continue Watching'
|
||||||
|
pkc_cont_watching.set('title', 'PKC %s' % title)
|
||||||
|
if pkc_cont_watching:
|
||||||
|
for i, entry in enumerate(xml):
|
||||||
|
if entry.get('key') == '/hubs/home/continueWatching':
|
||||||
|
xml.insert(i + 1, pkc_cont_watching)
|
||||||
|
break
|
||||||
|
# END HACK ##################
|
||||||
|
|
||||||
show_listing(xml)
|
show_listing(xml)
|
||||||
|
|
||||||
|
|
||||||
|
|
32
resources/lib/exceptions.py
Normal file
32
resources/lib/exceptions.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
#!/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
|
|
@ -287,7 +287,6 @@ class InitialSetup(object):
|
||||||
}
|
}
|
||||||
or None if unsuccessful
|
or None if unsuccessful
|
||||||
"""
|
"""
|
||||||
server = None
|
|
||||||
# If no server is set, let user choose one
|
# If no server is set, let user choose one
|
||||||
if not app.CONN.server or not app.CONN.machine_identifier:
|
if not app.CONN.server or not app.CONN.machine_identifier:
|
||||||
showDialog = True
|
showDialog = True
|
||||||
|
@ -435,7 +434,6 @@ class InitialSetup(object):
|
||||||
utils.settings('plex_servername', server['name'])
|
utils.settings('plex_servername', server['name'])
|
||||||
utils.settings('plex_serverowned',
|
utils.settings('plex_serverowned',
|
||||||
'true' if server['owned'] else 'false')
|
'true' if server['owned'] else 'false')
|
||||||
utils.settings('accessToken', server['token'])
|
|
||||||
# Careful to distinguish local from remote PMS
|
# Careful to distinguish local from remote PMS
|
||||||
if server['local']:
|
if server['local']:
|
||||||
scheme = server['scheme']
|
scheme = server['scheme']
|
||||||
|
|
|
@ -195,9 +195,10 @@ class Movie(ItemBase):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _process_collections(self, api, tags, kodi_id, section_id, children):
|
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():
|
for plex_set_id, set_name in api.collections():
|
||||||
set_api = None
|
set_api = None
|
||||||
tags.append(set_name)
|
|
||||||
# Add any sets from Plex collection tags
|
# Add any sets from Plex collection tags
|
||||||
kodi_set_id = self.kodidb.create_collection(set_name)
|
kodi_set_id = self.kodidb.create_collection(set_name)
|
||||||
self.kodidb.assign_collection(kodi_set_id, kodi_id)
|
self.kodidb.assign_collection(kodi_set_id, kodi_id)
|
||||||
|
|
|
@ -135,7 +135,6 @@ class MusicMixin(object):
|
||||||
'''
|
'''
|
||||||
Remove an album
|
Remove an album
|
||||||
'''
|
'''
|
||||||
self.kodidb.delete_album_from_discography(kodi_id)
|
|
||||||
if v.KODIVERSION < 18:
|
if v.KODIVERSION < 18:
|
||||||
self.kodidb.delete_album_from_album_genre(kodi_id)
|
self.kodidb.delete_album_from_album_genre(kodi_id)
|
||||||
self.kodidb.remove_album(kodi_id)
|
self.kodidb.remove_album(kodi_id)
|
||||||
|
@ -353,11 +352,6 @@ class Album(MusicMixin, ItemBase):
|
||||||
timing.unix_date_to_kodi(self.last_sync),
|
timing.unix_date_to_kodi(self.last_sync),
|
||||||
'album')
|
'album')
|
||||||
self.kodidb.add_albumartist(artist_id, kodi_id, api.artist_name())
|
self.kodidb.add_albumartist(artist_id, kodi_id, api.artist_name())
|
||||||
if v.KODIVERSION < 18:
|
|
||||||
self.kodidb.add_discography(artist_id, name, api.year())
|
|
||||||
self.kodidb.add_music_genres(kodi_id,
|
|
||||||
api.genres(),
|
|
||||||
v.KODI_TYPE_ALBUM)
|
|
||||||
if app.SYNC.artwork:
|
if app.SYNC.artwork:
|
||||||
self.kodidb.modify_artwork(artworks,
|
self.kodidb.modify_artwork(artworks,
|
||||||
kodi_id,
|
kodi_id,
|
||||||
|
|
|
@ -270,6 +270,7 @@ class Show(TvShowMixin, ItemBase):
|
||||||
unique_ids.get('imdb',
|
unique_ids.get('imdb',
|
||||||
unique_ids.get('tmdb')))
|
unique_ids.get('tmdb')))
|
||||||
|
|
||||||
|
|
||||||
class Season(TvShowMixin, ItemBase):
|
class Season(TvShowMixin, ItemBase):
|
||||||
def add_update(self, xml, section_name=None, section_id=None,
|
def add_update(self, xml, section_name=None, section_id=None,
|
||||||
children=None):
|
children=None):
|
||||||
|
@ -279,7 +280,7 @@ class Season(TvShowMixin, ItemBase):
|
||||||
api = API(xml)
|
api = API(xml)
|
||||||
if not self.sync_this_item(section_id or api.library_section_id()):
|
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 '
|
LOG.debug('Skipping sync of %s %s: %s - section %s not synched to '
|
||||||
'Kodi', api.plex_type, api.plex_id, api.title(),
|
'Kodi', api.plex_type, api.plex_id, api.season_name(),
|
||||||
section_id or api.library_section_id())
|
section_id or api.library_section_id())
|
||||||
return
|
return
|
||||||
plex_id = api.plex_id
|
plex_id = api.plex_id
|
||||||
|
@ -317,15 +318,24 @@ class Season(TvShowMixin, ItemBase):
|
||||||
if key in artwork and artwork[key] == parent_artwork[key]:
|
if key in artwork and artwork[key] == parent_artwork[key]:
|
||||||
del artwork[key]
|
del artwork[key]
|
||||||
if update_item:
|
if update_item:
|
||||||
LOG.info('UPDATE season plex_id %s - %s', plex_id, api.title())
|
LOG.info('UPDATE season plex_id %s - %s',
|
||||||
|
plex_id, api.season_name())
|
||||||
kodi_id = season['kodi_id']
|
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:
|
if app.SYNC.artwork:
|
||||||
self.kodidb.modify_artwork(artwork,
|
self.kodidb.modify_artwork(artwork,
|
||||||
kodi_id,
|
kodi_id,
|
||||||
v.KODI_TYPE_SEASON)
|
v.KODI_TYPE_SEASON)
|
||||||
else:
|
else:
|
||||||
LOG.info('ADD season plex_id %s - %s', plex_id, api.title())
|
LOG.info('ADD season plex_id %s - %s', plex_id, api.season_name())
|
||||||
kodi_id = self.kodidb.add_season(parent_id, api.index())
|
kodi_id = self.kodidb.add_season(parent_id,
|
||||||
|
api.index(),
|
||||||
|
api.season_name(),
|
||||||
|
api.userrating() or None)
|
||||||
if app.SYNC.artwork:
|
if app.SYNC.artwork:
|
||||||
self.kodidb.add_artwork(artwork,
|
self.kodidb.add_artwork(artwork,
|
||||||
kodi_id,
|
kodi_id,
|
||||||
|
|
|
@ -421,6 +421,41 @@ def get_item(playerid):
|
||||||
'properties': ['title', 'file']})['result']['item']
|
'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):
|
def get_player_props(playerid):
|
||||||
"""
|
"""
|
||||||
Returns a dict for the active Kodi player with the following values:
|
Returns a dict for the active Kodi player with the following values:
|
||||||
|
|
|
@ -106,26 +106,6 @@ class KodiMusicDB(common.KodiDBBase):
|
||||||
self.cursor.execute('DELETE FROM song_artist WHERE idSong = ?',
|
self.cursor.execute('DELETE FROM song_artist WHERE idSong = ?',
|
||||||
(song_id, ))
|
(song_id, ))
|
||||||
|
|
||||||
@db.catch_operationalerrors
|
|
||||||
def delete_album_from_discography(self, album_id):
|
|
||||||
"""
|
|
||||||
Removes the album with id album_id from the table discography
|
|
||||||
"""
|
|
||||||
# Need to get the album name as a string first!
|
|
||||||
self.cursor.execute('SELECT strAlbum, iYear FROM album WHERE idAlbum = ? LIMIT 1',
|
|
||||||
(album_id, ))
|
|
||||||
try:
|
|
||||||
name, year = self.cursor.fetchone()
|
|
||||||
except TypeError:
|
|
||||||
return
|
|
||||||
self.cursor.execute('SELECT idArtist FROM album_artist WHERE idAlbum = ? LIMIT 1',
|
|
||||||
(album_id, ))
|
|
||||||
artist = self.cursor.fetchone()
|
|
||||||
if not artist:
|
|
||||||
return
|
|
||||||
self.cursor.execute('DELETE FROM discography WHERE idArtist = ? AND strAlbum = ? AND strYear = ?',
|
|
||||||
(artist[0], name, year))
|
|
||||||
|
|
||||||
@db.catch_operationalerrors
|
@db.catch_operationalerrors
|
||||||
def delete_song_from_song_genre(self, song_id):
|
def delete_song_from_song_genre(self, song_id):
|
||||||
"""
|
"""
|
||||||
|
@ -352,16 +332,6 @@ class KodiMusicDB(common.KodiDBBase):
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
''', (artist_id, kodi_id, artistname))
|
''', (artist_id, kodi_id, artistname))
|
||||||
|
|
||||||
@db.catch_operationalerrors
|
|
||||||
def add_discography(self, artist_id, albumname, year):
|
|
||||||
self.cursor.execute('''
|
|
||||||
INSERT OR REPLACE INTO discography(
|
|
||||||
idArtist,
|
|
||||||
strAlbum,
|
|
||||||
strYear)
|
|
||||||
VALUES (?, ?, ?)
|
|
||||||
''', (artist_id, albumname, year))
|
|
||||||
|
|
||||||
@db.catch_operationalerrors
|
@db.catch_operationalerrors
|
||||||
def add_music_genres(self, kodiid, genres, mediatype):
|
def add_music_genres(self, kodiid, genres, mediatype):
|
||||||
"""
|
"""
|
||||||
|
@ -656,5 +626,3 @@ class KodiMusicDB(common.KodiDBBase):
|
||||||
(kodi_id, ))
|
(kodi_id, ))
|
||||||
self.cursor.execute('DELETE FROM song_artist WHERE idArtist = ?',
|
self.cursor.execute('DELETE FROM song_artist WHERE idArtist = ?',
|
||||||
(kodi_id, ))
|
(kodi_id, ))
|
||||||
self.cursor.execute('DELETE FROM discography WHERE idArtist = ?',
|
|
||||||
(kodi_id, ))
|
|
||||||
|
|
|
@ -575,6 +575,22 @@ class KodiVideoDB(common.KodiDBBase):
|
||||||
return
|
return
|
||||||
return movie_id, typus
|
return movie_id, typus
|
||||||
|
|
||||||
|
def file_id_from_id(self, kodi_id, kodi_type):
|
||||||
|
"""
|
||||||
|
Returns the Kodi file_id for the item with kodi_id and kodi_type or
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
if kodi_type == v.KODI_TYPE_MOVIE:
|
||||||
|
identifier = 'idMovie'
|
||||||
|
elif kodi_type == v.KODI_TYPE_EPISODE:
|
||||||
|
identifier = 'idEpisode'
|
||||||
|
self.cursor.execute('SELECT idFile FROM %s WHERE %s = ? LIMIT 1'
|
||||||
|
% (kodi_type, identifier), (kodi_id, ))
|
||||||
|
try:
|
||||||
|
return self.cursor.fetchone()[0]
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
|
||||||
def get_resume(self, file_id):
|
def get_resume(self, file_id):
|
||||||
"""
|
"""
|
||||||
Returns the first resume point in seconds (int) if found, else None for
|
Returns the first resume point in seconds (int) if found, else None for
|
||||||
|
@ -718,15 +734,32 @@ class KodiVideoDB(common.KodiDBBase):
|
||||||
self.cursor.execute('DELETE FROM sets WHERE idSet = ?', (set_id,))
|
self.cursor.execute('DELETE FROM sets WHERE idSet = ?', (set_id,))
|
||||||
|
|
||||||
@db.catch_operationalerrors
|
@db.catch_operationalerrors
|
||||||
def add_season(self, showid, seasonnumber):
|
def add_season(self, showid, seasonnumber, name, userrating):
|
||||||
"""
|
"""
|
||||||
Adds a TV show season to the Kodi video DB or simply returns the ID,
|
Adds a TV show season to the Kodi video DB or simply returns the ID,
|
||||||
if there already is an entry in the DB
|
if there already is an entry in the DB
|
||||||
"""
|
"""
|
||||||
self.cursor.execute('INSERT INTO seasons(idShow, season) VALUES (?, ?)',
|
self.cursor.execute('''
|
||||||
(showid, seasonnumber))
|
INSERT INTO seasons(
|
||||||
|
idShow, season, name, userrating)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
''', (showid, seasonnumber, name, userrating))
|
||||||
return self.cursor.lastrowid
|
return self.cursor.lastrowid
|
||||||
|
|
||||||
|
@db.catch_operationalerrors
|
||||||
|
def update_season(self, seasonid, showid, seasonnumber, name, userrating):
|
||||||
|
"""
|
||||||
|
Updates a TV show season with a certain seasonid
|
||||||
|
"""
|
||||||
|
self.cursor.execute('''
|
||||||
|
UPDATE seasons
|
||||||
|
SET idShow = ?,
|
||||||
|
season = ?,
|
||||||
|
name = ?,
|
||||||
|
userrating = ?
|
||||||
|
WHERE idSeason = ?
|
||||||
|
''', (showid, seasonnumber, name, userrating, seasonid))
|
||||||
|
|
||||||
@db.catch_operationalerrors
|
@db.catch_operationalerrors
|
||||||
def add_uniqueid(self, *args):
|
def add_uniqueid(self, *args):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -14,11 +14,13 @@ import xbmc
|
||||||
|
|
||||||
from .plex_api import API
|
from .plex_api import API
|
||||||
from .plex_db import PlexDB
|
from .plex_db import PlexDB
|
||||||
|
from .kodi_db import KodiVideoDB
|
||||||
from . import kodi_db
|
from . import kodi_db
|
||||||
from .downloadutils import DownloadUtils as DU
|
from .downloadutils import DownloadUtils as DU
|
||||||
from . import utils, timing, plex_functions as PF
|
from . import utils, timing, plex_functions as PF
|
||||||
from . import json_rpc as js, playqueue as PQ, playlist_func as PL
|
from . import json_rpc as js, playqueue as PQ, playlist_func as PL
|
||||||
from . import backgroundthread, app, variables as v
|
from . import backgroundthread, app, variables as v
|
||||||
|
from . import exceptions
|
||||||
|
|
||||||
LOG = getLogger('PLEX.kodimonitor')
|
LOG = getLogger('PLEX.kodimonitor')
|
||||||
|
|
||||||
|
@ -27,8 +29,10 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
"""
|
"""
|
||||||
PKC implementation of the Kodi Monitor class. Invoke only once.
|
PKC implementation of the Kodi Monitor class. Invoke only once.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._already_slept = False
|
self._already_slept = False
|
||||||
|
self._switched_to_plex_streams = True
|
||||||
xbmc.Monitor.__init__(self)
|
xbmc.Monitor.__init__(self)
|
||||||
for playerid in app.PLAYSTATE.player_states:
|
for playerid in app.PLAYSTATE.player_states:
|
||||||
app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
|
app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
|
||||||
|
@ -64,6 +68,9 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
if method == "Player.OnPlay":
|
if method == "Player.OnPlay":
|
||||||
with app.APP.lock_playqueues:
|
with app.APP.lock_playqueues:
|
||||||
self.PlayBackStart(data)
|
self.PlayBackStart(data)
|
||||||
|
elif method == 'Player.OnAVChange':
|
||||||
|
with app.APP.lock_playqueues:
|
||||||
|
self._on_av_change(data)
|
||||||
elif method == "Player.OnStop":
|
elif method == "Player.OnStop":
|
||||||
with app.APP.lock_playqueues:
|
with app.APP.lock_playqueues:
|
||||||
_playback_cleanup(ended=data.get('end'))
|
_playback_cleanup(ended=data.get('end'))
|
||||||
|
@ -82,7 +89,8 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
with app.APP.lock_playqueues:
|
with app.APP.lock_playqueues:
|
||||||
self._playlist_onclear(data)
|
self._playlist_onclear(data)
|
||||||
elif method == "VideoLibrary.OnUpdate":
|
elif method == "VideoLibrary.OnUpdate":
|
||||||
_videolibrary_onupdate(data)
|
with app.APP.lock_playqueues:
|
||||||
|
_videolibrary_onupdate(data)
|
||||||
elif method == "VideoLibrary.OnRemove":
|
elif method == "VideoLibrary.OnRemove":
|
||||||
pass
|
pass
|
||||||
elif method == "System.OnSleep":
|
elif method == "System.OnSleep":
|
||||||
|
@ -175,7 +183,7 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
try:
|
try:
|
||||||
for i, item in enumerate(items):
|
for i, item in enumerate(items):
|
||||||
PL.add_item_to_plex_playqueue(playqueue, i + 1, kodi_item=item)
|
PL.add_item_to_plex_playqueue(playqueue, i + 1, kodi_item=item)
|
||||||
except PL.PlaylistError:
|
except exceptions.PlaylistError:
|
||||||
LOG.info('Could not build Plex playlist for: %s', items)
|
LOG.info('Could not build Plex playlist for: %s', items)
|
||||||
|
|
||||||
def _json_item(self, playerid):
|
def _json_item(self, playerid):
|
||||||
|
@ -289,7 +297,7 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
LOG.debug('Detected different path')
|
LOG.debug('Detected different path')
|
||||||
try:
|
try:
|
||||||
tmp_plex_id = int(utils.REGEX_PLEX_ID.findall(path)[0])
|
tmp_plex_id = int(utils.REGEX_PLEX_ID.findall(path)[0])
|
||||||
except IndexError:
|
except (IndexError, TypeError):
|
||||||
LOG.debug('No Plex id in path, need to init playqueue')
|
LOG.debug('No Plex id in path, need to init playqueue')
|
||||||
initialize = True
|
initialize = True
|
||||||
else:
|
else:
|
||||||
|
@ -312,7 +320,7 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
item = PL.init_plex_playqueue(playqueue, plex_id=plex_id)
|
item = PL.init_plex_playqueue(playqueue, plex_id=plex_id)
|
||||||
except PL.PlaylistError:
|
except exceptions.PlaylistError:
|
||||||
LOG.info('Could not initialize the Plex playlist')
|
LOG.info('Could not initialize the Plex playlist')
|
||||||
return
|
return
|
||||||
item.file = path
|
item.file = path
|
||||||
|
@ -335,6 +343,9 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
container_key = '/playQueues/%s' % playqueue.id
|
container_key = '/playQueues/%s' % playqueue.id
|
||||||
else:
|
else:
|
||||||
container_key = '/library/metadata/%s' % plex_id
|
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
|
# Remember the currently playing item
|
||||||
app.PLAYSTATE.item = item
|
app.PLAYSTATE.item = item
|
||||||
# Remember that this player has been active
|
# Remember that this player has been active
|
||||||
|
@ -349,14 +360,44 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
status['plex_type'] = plex_type
|
status['plex_type'] = plex_type
|
||||||
status['playmethod'] = item.playmethod
|
status['playmethod'] = item.playmethod
|
||||||
status['playcount'] = item.playcount
|
status['playcount'] = item.playcount
|
||||||
try:
|
status['external_player'] = app.APP.player.isExternalPlayer() == 1
|
||||||
status['external_player'] = app.APP.player.isExternalPlayer() == 1
|
|
||||||
except AttributeError:
|
|
||||||
# Kodi version < 17
|
|
||||||
pass
|
|
||||||
LOG.debug('Set the player state: %s', status)
|
LOG.debug('Set the player state: %s', status)
|
||||||
|
|
||||||
|
# Workaround for the Kodi add-on Up Next
|
||||||
if not app.SYNC.direct_paths:
|
if not app.SYNC.direct_paths:
|
||||||
_notify_upnext(item)
|
_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)
|
||||||
|
|
||||||
|
|
||||||
def _playback_cleanup(ended=False):
|
def _playback_cleanup(ended=False):
|
||||||
|
@ -367,6 +408,9 @@ def _playback_cleanup(ended=False):
|
||||||
"""
|
"""
|
||||||
LOG.debug('playback_cleanup called. Active players: %s',
|
LOG.debug('playback_cleanup called. Active players: %s',
|
||||||
app.PLAYSTATE.active_players)
|
app.PLAYSTATE.active_players)
|
||||||
|
if app.APP.skip_intro_dialog:
|
||||||
|
app.APP.skip_intro_dialog.close()
|
||||||
|
app.APP.skip_intro_dialog = None
|
||||||
# We might have saved a transient token from a user flinging media via
|
# 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)
|
# Companion (if we could not use the playqueue to store the token)
|
||||||
app.CONN.plex_transient_token = None
|
app.CONN.plex_transient_token = None
|
||||||
|
@ -529,11 +573,10 @@ def _next_episode(current_api):
|
||||||
current_api.grandparent_title())
|
current_api.grandparent_title())
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
next_api = API(xml[counter + 1])
|
return API(xml[counter + 1])
|
||||||
except IndexError:
|
except IndexError:
|
||||||
# Was the last episode
|
# Was the last episode
|
||||||
return
|
pass
|
||||||
return next_api
|
|
||||||
|
|
||||||
|
|
||||||
def _complete_artwork_keys(info):
|
def _complete_artwork_keys(info):
|
||||||
|
@ -559,7 +602,7 @@ def _notify_upnext(item):
|
||||||
"""
|
"""
|
||||||
if not item.plex_type == v.PLEX_TYPE_EPISODE:
|
if not item.plex_type == v.PLEX_TYPE_EPISODE:
|
||||||
return
|
return
|
||||||
this_api = API(item.xml)
|
this_api = item.api
|
||||||
next_api = _next_episode(this_api)
|
next_api = _next_episode(this_api)
|
||||||
if next_api is None:
|
if next_api is None:
|
||||||
return
|
return
|
||||||
|
@ -595,17 +638,34 @@ def _videolibrary_onupdate(data):
|
||||||
A specific Kodi library item has been updated. This seems to happen if the
|
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
|
user marks an item as watched/unwatched or if playback of the item just
|
||||||
stopped
|
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}
|
||||||
"""
|
"""
|
||||||
playcount = data.get('playcount')
|
item = data.get('item') if 'item' in data else data
|
||||||
item = data.get('item')
|
|
||||||
if playcount is None or item is None:
|
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
kodi_id = item['id']
|
kodi_id = item['id']
|
||||||
kodi_type = item['type']
|
kodi_type = item['type']
|
||||||
except (KeyError, TypeError):
|
except (KeyError, TypeError):
|
||||||
LOG.info("Item is invalid for playstate update.")
|
LOG.debug("Item is invalid for a Plex playstate update")
|
||||||
return
|
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 \
|
if app.PLAYSTATE.item and kodi_id == app.PLAYSTATE.item.kodi_id and \
|
||||||
kodi_type == app.PLAYSTATE.item.kodi_type:
|
kodi_type == app.PLAYSTATE.item.kodi_type:
|
||||||
# Kodi updates an item immediately after playback. Hence we do NOT
|
# Kodi updates an item immediately after playback. Hence we do NOT
|
||||||
|
|
|
@ -41,11 +41,15 @@ class FillMetadataQueue(common.LibrarySyncMixin,
|
||||||
plex_id = int(xml.get('ratingKey'))
|
plex_id = int(xml.get('ratingKey'))
|
||||||
checksum = int('{}{}'.format(
|
checksum = int('{}{}'.format(
|
||||||
plex_id,
|
plex_id,
|
||||||
xml.get('updatedAt',
|
abs(int(xml.get('updatedAt',
|
||||||
xml.get('addedAt', '1541572987')).replace('-', '')))
|
xml.get('addedAt', '1541572987'))))))
|
||||||
if (not self.repair and
|
if (not self.repair and
|
||||||
plexdb.checksum(plex_id, section.plex_type) == checksum):
|
plexdb.checksum(plex_id, section.plex_type) == checksum):
|
||||||
continue
|
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:
|
try:
|
||||||
self.get_metadata_queue.put((count, plex_id, section),
|
self.get_metadata_queue.put((count, plex_id, section),
|
||||||
timeout=QUEUE_TIMEOUT)
|
timeout=QUEUE_TIMEOUT)
|
||||||
|
@ -54,16 +58,14 @@ class FillMetadataQueue(common.LibrarySyncMixin,
|
||||||
'aborting sync now', plex_id)
|
'aborting sync now', plex_id)
|
||||||
section.sync_successful = False
|
section.sync_successful = False
|
||||||
break
|
break
|
||||||
count += 1
|
else:
|
||||||
if not do_process_section:
|
count += 1
|
||||||
do_process_section = True
|
|
||||||
self.processing_queue.add_section(section)
|
|
||||||
LOG.debug('Put section in queue with %s items: %s',
|
|
||||||
section.number_of_items, section)
|
|
||||||
# We might have received LESS items from the PMS than anticipated.
|
# We might have received LESS items from the PMS than anticipated.
|
||||||
# Ensures that our queues finish
|
# Ensures that our queues finish
|
||||||
LOG.debug('%s items to process for section %s', count, section)
|
self.processing_queue.change_section_number_of_items(section,
|
||||||
section.number_of_items = count
|
count)
|
||||||
|
LOG.debug('%s items to process for section %s',
|
||||||
|
section.number_of_items, section)
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
while not self.should_cancel():
|
while not self.should_cancel():
|
||||||
|
|
|
@ -137,7 +137,7 @@ class FullSync(common.LibrarySyncMixin, bg.KillableThread):
|
||||||
LOG.error('Could not entirely process section %s', section)
|
LOG.error('Could not entirely process section %s', section)
|
||||||
self.successful = False
|
self.successful = False
|
||||||
|
|
||||||
def threaded_get_generators(self, kinds, section_queue, all_items):
|
def threaded_get_generators(self, kinds, section_queue, items):
|
||||||
"""
|
"""
|
||||||
Getting iterators is costly, so let's do it in a dedicated thread
|
Getting iterators is costly, so let's do it in a dedicated thread
|
||||||
"""
|
"""
|
||||||
|
@ -154,17 +154,28 @@ class FullSync(common.LibrarySyncMixin, bg.KillableThread):
|
||||||
continue
|
continue
|
||||||
section = sections.get_sync_section(section,
|
section = sections.get_sync_section(section,
|
||||||
plex_type=kind[0])
|
plex_type=kind[0])
|
||||||
if self.repair or all_items:
|
timestamp = section.last_sync - UPDATED_AT_SAFETY \
|
||||||
|
if section.last_sync else None
|
||||||
|
if items == 'all':
|
||||||
updated_at = None
|
updated_at = None
|
||||||
else:
|
last_viewed_at = None
|
||||||
updated_at = section.last_sync - UPDATED_AT_SAFETY \
|
elif items == 'watched':
|
||||||
if section.last_sync else None
|
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:
|
try:
|
||||||
section.iterator = PF.get_section_iterator(
|
section.iterator = PF.get_section_iterator(
|
||||||
section.section_id,
|
section.section_id,
|
||||||
plex_type=section.plex_type,
|
plex_type=section.plex_type,
|
||||||
updated_at=updated_at,
|
updated_at=updated_at,
|
||||||
last_viewed_at=None)
|
last_viewed_at=last_viewed_at)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
LOG.error('Sync at least partially unsuccessful!')
|
LOG.error('Sync at least partially unsuccessful!')
|
||||||
LOG.error('Error getting section iterator %s', section)
|
LOG.error('Error getting section iterator %s', section)
|
||||||
|
@ -195,19 +206,42 @@ class FullSync(common.LibrarySyncMixin, bg.KillableThread):
|
||||||
(v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_ARTIST),
|
(v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_ARTIST),
|
||||||
(v.PLEX_TYPE_ALBUM, v.PLEX_TYPE_ARTIST),
|
(v.PLEX_TYPE_ALBUM, v.PLEX_TYPE_ARTIST),
|
||||||
])
|
])
|
||||||
|
|
||||||
# ADD NEW ITEMS
|
# ADD NEW ITEMS
|
||||||
# We need to enforce syncing e.g. show before season before episode
|
# We need to enforce syncing e.g. show before season before episode
|
||||||
bg.FunctionAsTask(self.threaded_get_generators,
|
bg.FunctionAsTask(self.threaded_get_generators,
|
||||||
None,
|
None,
|
||||||
kinds, section_queue, False).start()
|
kinds,
|
||||||
|
section_queue,
|
||||||
|
items='all' if self.repair else 'updated').start()
|
||||||
# Do the heavy lifting
|
# Do the heavy lifting
|
||||||
self.process_new_and_changed_items(section_queue, processing_queue)
|
self.process_new_and_changed_items(section_queue, processing_queue)
|
||||||
common.update_kodi_library(video=True, music=True)
|
common.update_kodi_library(video=True, music=True)
|
||||||
if self.should_cancel() or not self.successful:
|
if self.should_cancel() or not self.successful:
|
||||||
return
|
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
|
# Sync Plex playlists to Kodi and vice-versa
|
||||||
if common.PLAYLIST_SYNC_ENABLED:
|
if common.PLAYLIST_SYNC_ENABLED:
|
||||||
|
LOG.debug('Start playlist sync')
|
||||||
if self.show_dialog:
|
if self.show_dialog:
|
||||||
if self.dialog:
|
if self.dialog:
|
||||||
self.dialog.close()
|
self.dialog.close()
|
||||||
|
@ -218,14 +252,9 @@ class FullSync(common.LibrarySyncMixin, bg.KillableThread):
|
||||||
return
|
return
|
||||||
|
|
||||||
# SYNC PLAYSTATE of ALL items (otherwise we won't pick up on items that
|
# SYNC PLAYSTATE of ALL items (otherwise we won't pick up on items that
|
||||||
# were set to unwatched). Also mark all items on the PMS to be able
|
# were set to unwatched or changed user ratings). Also mark all items on
|
||||||
# to delete the ones still in Kodi
|
# the PMS to be able to delete the ones still in Kodi
|
||||||
LOG.debug('Start synching playstate and userdata for every item')
|
LOG.debug('Start synching playstate and userdata for every item')
|
||||||
if app.SYNC.enable_music:
|
|
||||||
# In order to not delete all your songs again
|
|
||||||
kinds.extend([
|
|
||||||
(v.PLEX_TYPE_SONG, v.PLEX_TYPE_ARTIST),
|
|
||||||
])
|
|
||||||
# Make sure we're not showing an item's title in the sync dialog
|
# Make sure we're not showing an item's title in the sync dialog
|
||||||
if not self.show_dialog_userdata and self.dialog:
|
if not self.show_dialog_userdata and self.dialog:
|
||||||
# Close the progress indicator dialog
|
# Close the progress indicator dialog
|
||||||
|
@ -233,7 +262,9 @@ class FullSync(common.LibrarySyncMixin, bg.KillableThread):
|
||||||
self.dialog = None
|
self.dialog = None
|
||||||
bg.FunctionAsTask(self.threaded_get_generators,
|
bg.FunctionAsTask(self.threaded_get_generators,
|
||||||
None,
|
None,
|
||||||
kinds, section_queue, True).start()
|
kinds,
|
||||||
|
section_queue,
|
||||||
|
items='all').start()
|
||||||
self.processing_loop_playstates(section_queue)
|
self.processing_loop_playstates(section_queue)
|
||||||
if self.should_cancel() or not self.successful:
|
if self.should_cancel() or not self.successful:
|
||||||
return
|
return
|
||||||
|
|
|
@ -93,6 +93,7 @@ class Section(object):
|
||||||
"'name': '{self.name}', "
|
"'name': '{self.name}', "
|
||||||
"'section_id': {self.section_id}, "
|
"'section_id': {self.section_id}, "
|
||||||
"'section_type': '{self.section_type}', "
|
"'section_type': '{self.section_type}', "
|
||||||
|
"'plex_type': '{self.plex_type}', "
|
||||||
"'sync_to_kodi': {self.sync_to_kodi}, "
|
"'sync_to_kodi': {self.sync_to_kodi}, "
|
||||||
"'last_sync': {self.last_sync}"
|
"'last_sync': {self.last_sync}"
|
||||||
"}}").format(self=self).encode('utf-8')
|
"}}").format(self=self).encode('utf-8')
|
||||||
|
@ -108,6 +109,8 @@ class Section(object):
|
||||||
Sections compare equal if their section_id, name and plex_type (first
|
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
|
prio) OR section_type (if there is no plex_type is set) compare equal
|
||||||
"""
|
"""
|
||||||
|
if not isinstance(section, Section):
|
||||||
|
return False
|
||||||
return (self.section_id == section.section_id and
|
return (self.section_id == section.section_id and
|
||||||
self.name == section.name and
|
self.name == section.name and
|
||||||
(self.plex_type == section.plex_type if self.plex_type else
|
(self.plex_type == section.plex_type if self.plex_type else
|
||||||
|
@ -650,6 +653,9 @@ def _sync_from_pms(pick_libraries):
|
||||||
sections = []
|
sections = []
|
||||||
old_sections = []
|
old_sections = []
|
||||||
for i, xml_element in enumerate(xml.findall('Directory')):
|
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))
|
sections.append(Section(index=i, xml_element=xml_element))
|
||||||
with PlexDB() as plexdb:
|
with PlexDB() as plexdb:
|
||||||
for section_db in plexdb.all_sections():
|
for section_db in plexdb.all_sections():
|
||||||
|
|
|
@ -16,6 +16,7 @@ from .kodi_db import KodiVideoDB
|
||||||
from . import plex_functions as PF, playlist_func as PL, playqueue as PQ
|
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 json_rpc as js, variables as v, utils, transfer
|
||||||
from . import playback_decision, app
|
from . import playback_decision, app
|
||||||
|
from . import exceptions
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
LOG = getLogger('PLEX.playback')
|
LOG = getLogger('PLEX.playback')
|
||||||
|
@ -192,7 +193,7 @@ def _playback_init(plex_id, plex_type, playqueue, pos, resume):
|
||||||
# Special case - we already got a filled Kodi playqueue
|
# Special case - we already got a filled Kodi playqueue
|
||||||
try:
|
try:
|
||||||
_init_existing_kodi_playlist(playqueue, pos)
|
_init_existing_kodi_playlist(playqueue, pos)
|
||||||
except PL.PlaylistError:
|
except exceptions.PlaylistError:
|
||||||
LOG.error('Playback_init for existing Kodi playlist failed')
|
LOG.error('Playback_init for existing Kodi playlist failed')
|
||||||
_ensure_resolve(abort=True)
|
_ensure_resolve(abort=True)
|
||||||
return
|
return
|
||||||
|
@ -312,7 +313,7 @@ def _init_existing_kodi_playlist(playqueue, pos):
|
||||||
kodi_items = js.playlist_get_items(playqueue.playlistid)
|
kodi_items = js.playlist_get_items(playqueue.playlistid)
|
||||||
if not kodi_items:
|
if not kodi_items:
|
||||||
LOG.error('No Kodi items returned')
|
LOG.error('No Kodi items returned')
|
||||||
raise PL.PlaylistError('No Kodi items returned')
|
raise exceptions.PlaylistError('No Kodi items returned')
|
||||||
item = PL.init_plex_playqueue(playqueue, kodi_item=kodi_items[pos])
|
item = PL.init_plex_playqueue(playqueue, kodi_item=kodi_items[pos])
|
||||||
item.force_transcode = app.PLAYSTATE.force_transcode
|
item.force_transcode = app.PLAYSTATE.force_transcode
|
||||||
# playqueue.py will add the rest - this will likely put the PMS under
|
# playqueue.py will add the rest - this will likely put the PMS under
|
||||||
|
@ -444,27 +445,26 @@ def _conclude_playback(playqueue, pos):
|
||||||
"""
|
"""
|
||||||
LOG.debug('Concluding playback for playqueue position %s', pos)
|
LOG.debug('Concluding playback for playqueue position %s', pos)
|
||||||
item = playqueue.items[pos]
|
item = playqueue.items[pos]
|
||||||
api = API(item.xml)
|
if item.api.mediastream_number() is None:
|
||||||
if api.mediastream_number() is None:
|
|
||||||
# E.g. user could choose between several media streams and cancelled
|
# E.g. user could choose between several media streams and cancelled
|
||||||
LOG.debug('Did not get a mediastream_number')
|
LOG.debug('Did not get a mediastream_number')
|
||||||
_ensure_resolve()
|
_ensure_resolve()
|
||||||
return
|
return
|
||||||
api.part = item.part or 0
|
item.api.part = item.part or 0
|
||||||
playback_decision.set_pkc_playmethod(api, item)
|
playback_decision.set_pkc_playmethod(item.api, item)
|
||||||
if not playback_decision.audio_subtitle_prefs(api, item):
|
if not playback_decision.audio_subtitle_prefs(item.api, item):
|
||||||
LOG.info('Did not set audio subtitle prefs, aborting silently')
|
LOG.info('Did not set audio subtitle prefs, aborting silently')
|
||||||
_ensure_resolve()
|
_ensure_resolve()
|
||||||
return
|
return
|
||||||
playback_decision.set_playurl(api, item)
|
playback_decision.set_playurl(item.api, item)
|
||||||
if not item.file:
|
if not item.file:
|
||||||
LOG.info('Did not get a playurl, aborting playback silently')
|
LOG.info('Did not get a playurl, aborting playback silently')
|
||||||
_ensure_resolve()
|
_ensure_resolve()
|
||||||
return
|
return
|
||||||
listitem = api.listitem(listitem=transfer.PKCListItem, resume=False)
|
listitem = item.api.listitem(listitem=transfer.PKCListItem, resume=False)
|
||||||
listitem.setPath(item.file.encode('utf-8'))
|
listitem.setPath(item.file.encode('utf-8'))
|
||||||
if item.playmethod != v.PLAYBACK_METHOD_DIRECT_PATH:
|
if item.playmethod != v.PLAYBACK_METHOD_DIRECT_PATH:
|
||||||
listitem.setSubtitles(api.cache_external_subs())
|
listitem.setSubtitles(item.api.cache_external_subs())
|
||||||
transfer.send(listitem)
|
transfer.send(listitem)
|
||||||
LOG.debug('Done concluding playback')
|
LOG.debug('Done concluding playback')
|
||||||
|
|
||||||
|
|
|
@ -329,28 +329,19 @@ def audio_subtitle_prefs(api, item):
|
||||||
Returns None if user cancelled or we need to abort, True otherwise
|
Returns None if user cancelled or we need to abort, True otherwise
|
||||||
"""
|
"""
|
||||||
# Set media and part where we're at
|
# Set media and part where we're at
|
||||||
if (api.mediastream is None and
|
if api.mediastream is None and api.mediastream_number() is None:
|
||||||
api.mediastream_number() is None):
|
|
||||||
return
|
return
|
||||||
try:
|
|
||||||
mediastreams = api.plex_media_streams()
|
|
||||||
except (TypeError, IndexError):
|
|
||||||
LOG.error('Could not get media %s, part %s',
|
|
||||||
api.mediastream, api.part)
|
|
||||||
return
|
|
||||||
part_id = mediastreams.attrib['id']
|
|
||||||
if item.playmethod != v.PLAYBACK_METHOD_TRANSCODE:
|
if item.playmethod != v.PLAYBACK_METHOD_TRANSCODE:
|
||||||
LOG.debug('Telling PMS we are not burning in any subtitles')
|
|
||||||
args = {
|
|
||||||
'subtitleStreamID': 0,
|
|
||||||
'allParts': 1
|
|
||||||
}
|
|
||||||
DU().downloadUrl('{server}/library/parts/%s' % part_id,
|
|
||||||
action_type='PUT',
|
|
||||||
parameters=args)
|
|
||||||
return True
|
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_list = []
|
||||||
audio_streams = []
|
audio_streams = []
|
||||||
|
audio_default = None
|
||||||
|
subtitle_default = None
|
||||||
subtitle_streams_list = []
|
subtitle_streams_list = []
|
||||||
# "Don't burn-in any subtitle"
|
# "Don't burn-in any subtitle"
|
||||||
subtitle_streams = ['1 %s' % utils.lang(39706)]
|
subtitle_streams = ['1 %s' % utils.lang(39706)]
|
||||||
|
@ -379,6 +370,8 @@ def audio_subtitle_prefs(api, item):
|
||||||
utils.lang(39707), # unknown
|
utils.lang(39707), # unknown
|
||||||
codec,
|
codec,
|
||||||
channellayout)
|
channellayout)
|
||||||
|
if stream.get('default'):
|
||||||
|
audio_default = audio_numb
|
||||||
audio_streams_list.append(index)
|
audio_streams_list.append(index)
|
||||||
audio_streams.append(track.encode('utf-8'))
|
audio_streams.append(track.encode('utf-8'))
|
||||||
audio_numb += 1
|
audio_numb += 1
|
||||||
|
@ -391,7 +384,6 @@ def audio_subtitle_prefs(api, item):
|
||||||
continue
|
continue
|
||||||
# Subtitle is available within the video file
|
# Subtitle is available within the video file
|
||||||
# Burn in the subtitle, if user chooses to do so
|
# Burn in the subtitle, if user chooses to do so
|
||||||
default = stream.get('default')
|
|
||||||
forced = stream.get('forced')
|
forced = stream.get('forced')
|
||||||
try:
|
try:
|
||||||
track = '{} {}'.format(sub_num + 1,
|
track = '{} {}'.format(sub_num + 1,
|
||||||
|
@ -400,7 +392,8 @@ def audio_subtitle_prefs(api, item):
|
||||||
track = '{} {} ({})'.format(sub_num + 1,
|
track = '{} {} ({})'.format(sub_num + 1,
|
||||||
utils.lang(39707), # unknown
|
utils.lang(39707), # unknown
|
||||||
stream.get('codec'))
|
stream.get('codec'))
|
||||||
if default:
|
if stream.get('default'):
|
||||||
|
subtitle_default = sub_num
|
||||||
track = "%s - %s" % (track, utils.lang(39708)) # Default
|
track = "%s - %s" % (track, utils.lang(39708)) # Default
|
||||||
if forced:
|
if forced:
|
||||||
track = "%s - %s" % (track, utils.lang(39709)) # Forced
|
track = "%s - %s" % (track, utils.lang(39709)) # Forced
|
||||||
|
@ -410,10 +403,14 @@ def audio_subtitle_prefs(api, item):
|
||||||
sub_num += 1
|
sub_num += 1
|
||||||
|
|
||||||
if audio_numb > 1:
|
if audio_numb > 1:
|
||||||
resp = utils.dialog('select', utils.lang(33013), audio_streams)
|
# "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||||
if resp == -1:
|
if utils.settings('bestQuality') == 'true' and audio_default is not None:
|
||||||
LOG.info('User aborted dialog to select audio stream')
|
resp = audio_default
|
||||||
return
|
else:
|
||||||
|
resp = utils.dialog('select', utils.lang(33013), audio_streams)
|
||||||
|
if resp == -1:
|
||||||
|
LOG.info('User aborted dialog to select audio stream')
|
||||||
|
return
|
||||||
args = {
|
args = {
|
||||||
'audioStreamID': audio_streams_list[resp],
|
'audioStreamID': audio_streams_list[resp],
|
||||||
'allParts': 1
|
'allParts': 1
|
||||||
|
@ -422,30 +419,29 @@ def audio_subtitle_prefs(api, item):
|
||||||
action_type='PUT',
|
action_type='PUT',
|
||||||
parameters=args)
|
parameters=args)
|
||||||
|
|
||||||
select_subs_index = ''
|
# Zero telling the PMS to deactivate subs altogether
|
||||||
|
select_subs_index = 0
|
||||||
if sub_num == 1:
|
if sub_num == 1:
|
||||||
# Note: we DO need to tell the PMS that we DONT want any sub
|
# Note: we DO need to tell the PMS that we DONT want any sub
|
||||||
# Otherwise, the PMS might pick-up the last one
|
# Otherwise, the PMS might pick-up the last one
|
||||||
LOG.info('No subtitles to burn-in')
|
LOG.info('No subtitles to burn-in')
|
||||||
else:
|
else:
|
||||||
resp = utils.dialog('select', utils.lang(33014), subtitle_streams)
|
# "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||||
if resp == -1:
|
if utils.settings('bestQuality') == 'true' and subtitle_default is not None:
|
||||||
LOG.info('User aborted dialog to select subtitle stream')
|
resp = subtitle_default
|
||||||
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:
|
else:
|
||||||
LOG.info('User chose to burn-in subtitle %s: %s',
|
resp = utils.dialog('select', utils.lang(33014), subtitle_streams)
|
||||||
select_subs_index,
|
if resp == -1:
|
||||||
subtitle_streams[resp].decode('utf-8'))
|
LOG.info('User aborted dialog to select subtitle stream')
|
||||||
select_subs_index = subtitle_streams_list[resp - 1]
|
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
|
# Now prep the PMS for our choice
|
||||||
args = {
|
PF.change_subtitle(select_subs_index, part_id)
|
||||||
'subtitleStreamID': select_subs_index,
|
|
||||||
'allParts': 1
|
|
||||||
}
|
|
||||||
DU().downloadUrl('{server}/library/parts/%s' % part_id,
|
|
||||||
action_type='PUT',
|
|
||||||
parameters=args)
|
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -12,23 +12,16 @@ from . import plex_functions as PF
|
||||||
from .kodi_db import kodiid_from_filename
|
from .kodi_db import kodiid_from_filename
|
||||||
from .downloadutils import DownloadUtils as DU
|
from .downloadutils import DownloadUtils as DU
|
||||||
from . import utils
|
from . import utils
|
||||||
|
from .utils import cast
|
||||||
from . import json_rpc as js
|
from . import json_rpc as js
|
||||||
from . import variables as v
|
from . import variables as v
|
||||||
from . import app
|
from . import app
|
||||||
|
from .exceptions import PlaylistError
|
||||||
|
from .subtitles import accessible_plex_subtitles
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
LOG = getLogger('PLEX.playlist_func')
|
LOG = getLogger('PLEX.playlist_func')
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
|
|
||||||
class PlaylistError(Exception):
|
|
||||||
"""
|
|
||||||
Exception for our playlist constructs
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Playqueue_Object(object):
|
class Playqueue_Object(object):
|
||||||
"""
|
"""
|
||||||
|
@ -157,13 +150,14 @@ class PlaylistItem(object):
|
||||||
file = None [str] Path to the item's file. STRING!!
|
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] PMS path to item; will be auto-set with plex_id
|
||||||
guid = None [str] Weird Plex guid
|
guid = None [str] Weird Plex guid
|
||||||
xml = None [etree] XML from PMS, 1 lvl below <MediaContainer>
|
api = None [API] API of xml 1 lvl below <MediaContainer>
|
||||||
playmethod = None [str] either 'DirectPath', 'DirectStream', 'Transcode'
|
playmethod = None [str] either 'DirectPath', 'DirectStream', 'Transcode'
|
||||||
playcount = None [int] how many times the item has already been played
|
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
|
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
|
part = 0 [int] part number if Plex video consists of mult. parts
|
||||||
force_transcode [bool] defaults to False
|
force_transcode [bool] defaults to False
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.id = None
|
self.id = None
|
||||||
self._plex_id = None
|
self._plex_id = None
|
||||||
|
@ -173,7 +167,7 @@ class PlaylistItem(object):
|
||||||
self.file = None
|
self.file = None
|
||||||
self._uri = None
|
self._uri = None
|
||||||
self.guid = None
|
self.guid = None
|
||||||
self.xml = None
|
self.api = None
|
||||||
self.playmethod = None
|
self.playmethod = None
|
||||||
self.playcount = None
|
self.playcount = None
|
||||||
self.offset = None
|
self.offset = None
|
||||||
|
@ -187,6 +181,16 @@ class PlaylistItem(object):
|
||||||
# False: do NOT resume, don't ask user
|
# False: do NOT resume, don't ask user
|
||||||
# True: do resume, don't ask user
|
# True: do resume, don't ask user
|
||||||
self.resume = None
|
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
|
@property
|
||||||
def plex_id(self):
|
def plex_id(self):
|
||||||
|
@ -202,6 +206,18 @@ class PlaylistItem(object):
|
||||||
def uri(self):
|
def uri(self):
|
||||||
return self._uri
|
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):
|
def __unicode__(self):
|
||||||
return ("{{"
|
return ("{{"
|
||||||
"'id': {self.id}, "
|
"'id': {self.id}, "
|
||||||
|
@ -221,60 +237,152 @@ class PlaylistItem(object):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.__unicode__().encode('utf-8')
|
return self.__unicode__().encode('utf-8')
|
||||||
|
|
||||||
|
def _process_streams(self):
|
||||||
|
"""
|
||||||
|
Builds audio and subtitle streams and enables matching between Plex
|
||||||
|
and Kodi using self.audio_streams and self.subtitle_streams
|
||||||
|
"""
|
||||||
|
# The playqueue response from the PMS does not contain a stream filename
|
||||||
|
# thanks Plex
|
||||||
|
self._subtitle_streams = accessible_plex_subtitles(
|
||||||
|
self.playmethod,
|
||||||
|
self.file,
|
||||||
|
self.api.plex_media_streams())
|
||||||
|
# Audio streams are much easier - they're always available and sorted
|
||||||
|
# the same in Kodi and Plex
|
||||||
|
self._audio_streams = [x for x in self.api.plex_media_streams()
|
||||||
|
if x.get('streamType') == '2']
|
||||||
|
self._streams_have_been_processed = True
|
||||||
|
|
||||||
|
def _get_iterator(self, stream_type):
|
||||||
|
if stream_type == 'audio':
|
||||||
|
return self.audio_streams
|
||||||
|
elif stream_type == 'subtitle':
|
||||||
|
return self.subtitle_streams
|
||||||
|
|
||||||
def plex_stream_index(self, kodi_stream_index, stream_type):
|
def plex_stream_index(self, kodi_stream_index, stream_type):
|
||||||
"""
|
"""
|
||||||
Pass in the kodi_stream_index [int] in order to receive the Plex stream
|
Pass in the kodi_stream_index [int] in order to receive the Plex stream
|
||||||
index.
|
index [int].
|
||||||
|
|
||||||
stream_type: 'video', 'audio', 'subtitle'
|
stream_type: 'video', 'audio', 'subtitle'
|
||||||
|
|
||||||
Returns None if unsuccessful
|
Returns None if unsuccessful
|
||||||
"""
|
"""
|
||||||
stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type]
|
if stream_type == 'audio':
|
||||||
count = 0
|
return int(self.audio_streams[kodi_stream_index].get('id'))
|
||||||
if kodi_stream_index == -1:
|
elif stream_type == 'subtitle':
|
||||||
# Kodi telling us "it's the last one"
|
try:
|
||||||
iterator = list(reversed(self.xml[0][self.part]))
|
return int(self.subtitle_streams[kodi_stream_index].get('id'))
|
||||||
kodi_stream_index = 0
|
except (IndexError, TypeError):
|
||||||
else:
|
pass
|
||||||
iterator = self.xml[0][self.part]
|
|
||||||
# Kodi indexes differently than Plex
|
|
||||||
for stream in iterator:
|
|
||||||
if (stream.attrib['streamType'] == stream_type and
|
|
||||||
'key' in stream.attrib):
|
|
||||||
if count == kodi_stream_index:
|
|
||||||
return stream.attrib['id']
|
|
||||||
count += 1
|
|
||||||
for stream in iterator:
|
|
||||||
if (stream.attrib['streamType'] == stream_type and
|
|
||||||
'key' not in stream.attrib):
|
|
||||||
if count == kodi_stream_index:
|
|
||||||
return stream.attrib['id']
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
def kodi_stream_index(self, plex_stream_index, stream_type):
|
def kodi_stream_index(self, plex_stream_index, stream_type):
|
||||||
"""
|
"""
|
||||||
Pass in the kodi_stream_index [int] in order to receive the Plex stream
|
Pass in the plex_stream_index [int] in order to receive the Kodi stream
|
||||||
index.
|
index [int].
|
||||||
|
|
||||||
stream_type: 'video', 'audio', 'subtitle'
|
stream_type: 'video', 'audio', 'subtitle'
|
||||||
|
|
||||||
Returns None if unsuccessful
|
Returns None if unsuccessful
|
||||||
"""
|
"""
|
||||||
stream_type = v.PLEX_STREAM_TYPE_FROM_STREAM_TYPE[stream_type]
|
if plex_stream_index is None:
|
||||||
count = 0
|
return
|
||||||
for stream in self.xml[0][self.part]:
|
for i, stream in enumerate(self._get_iterator(stream_type)):
|
||||||
if (stream.attrib['streamType'] == stream_type and
|
if cast(int, stream.get('id')) == plex_stream_index:
|
||||||
'key' in stream.attrib):
|
return i
|
||||||
if stream.attrib['id'] == plex_stream_index:
|
|
||||||
return count
|
def active_plex_stream_index(self, stream_type):
|
||||||
count += 1
|
"""
|
||||||
for stream in self.xml[0][self.part]:
|
Returns the following tuple for the active stream on the Plex side:
|
||||||
if (stream.attrib['streamType'] == stream_type and
|
(Plex stream id [int], languageTag [str] or None)
|
||||||
'key' not in stream.attrib):
|
Returns None if no stream has been selected
|
||||||
if stream.attrib['id'] == plex_stream_index:
|
"""
|
||||||
return count
|
for i, stream in enumerate(self._get_iterator(stream_type)):
|
||||||
count += 1
|
if stream.get('selected') == '1':
|
||||||
|
return (int(stream.get('id')), stream.get('languageTag'))
|
||||||
|
|
||||||
|
def on_kodi_subtitle_stream_change(self, kodi_stream_index, subs_enabled):
|
||||||
|
"""
|
||||||
|
Call this method if Kodi changed its subtitle and you want Plex to
|
||||||
|
know.
|
||||||
|
"""
|
||||||
|
if subs_enabled:
|
||||||
|
try:
|
||||||
|
plex_stream_index = int(self.subtitle_streams[kodi_stream_index].get('id'))
|
||||||
|
except (IndexError, TypeError):
|
||||||
|
LOG.debug('Kodi subtitle change detected to a sub %s that is '
|
||||||
|
'NOT available on the Plex side', kodi_stream_index)
|
||||||
|
self.current_kodi_sub_stream = None
|
||||||
|
return
|
||||||
|
LOG.debug('Kodi subtitle change detected: telling Plex about '
|
||||||
|
'switch to index %s, Plex stream id %s',
|
||||||
|
kodi_stream_index, plex_stream_index)
|
||||||
|
self.current_kodi_sub_stream = kodi_stream_index
|
||||||
|
else:
|
||||||
|
plex_stream_index = 0
|
||||||
|
LOG.debug('Kodi subtitle has been deactivated, telling Plex')
|
||||||
|
self.current_kodi_sub_stream = False
|
||||||
|
PF.change_subtitle(plex_stream_index, self.api.part_id())
|
||||||
|
|
||||||
|
def on_kodi_audio_stream_change(self, kodi_stream_index):
|
||||||
|
"""
|
||||||
|
Call this method if Kodi changed its audio stream and you want Plex to
|
||||||
|
know. kodi_stream_index [int]
|
||||||
|
"""
|
||||||
|
plex_stream_index = int(self.audio_streams[kodi_stream_index].get('id'))
|
||||||
|
LOG.debug('Changing Plex audio stream to %s, Kodi index %s',
|
||||||
|
plex_stream_index, kodi_stream_index)
|
||||||
|
PF.change_audio_stream(plex_stream_index, self.api.part_id())
|
||||||
|
self.current_kodi_audio_stream = kodi_stream_index
|
||||||
|
|
||||||
|
def switch_to_plex_streams(self):
|
||||||
|
self.switch_to_plex_stream('audio')
|
||||||
|
self.switch_to_plex_stream('subtitle')
|
||||||
|
|
||||||
|
def switch_to_plex_stream(self, typus):
|
||||||
|
try:
|
||||||
|
plex_index, language_tag = self.active_plex_stream_index(typus)
|
||||||
|
except TypeError:
|
||||||
|
LOG.debug('Deactivating Kodi subtitles because the PMS '
|
||||||
|
'told us to not show any subtitles')
|
||||||
|
app.APP.player.showSubtitles(False)
|
||||||
|
self.current_kodi_sub_stream = False
|
||||||
|
return
|
||||||
|
LOG.debug('The PMS wants to display %s stream with Plex id %s and '
|
||||||
|
'languageTag %s', typus, plex_index, language_tag)
|
||||||
|
kodi_index = self.kodi_stream_index(plex_index, typus)
|
||||||
|
if kodi_index is None:
|
||||||
|
LOG.debug('Leaving Kodi %s stream settings untouched since we '
|
||||||
|
'could not parse Plex %s stream with id %s to a Kodi'
|
||||||
|
' index', typus, typus, plex_index)
|
||||||
|
else:
|
||||||
|
LOG.debug('Switching to Kodi %s stream number %s because the '
|
||||||
|
'PMS told us to show stream with Plex id %s',
|
||||||
|
typus, kodi_index, plex_index)
|
||||||
|
# If we're choosing an "illegal" index, this function does
|
||||||
|
# need seem to fail nor log any errors
|
||||||
|
if typus == 'audio':
|
||||||
|
app.APP.player.setAudioStream(kodi_index)
|
||||||
|
else:
|
||||||
|
app.APP.player.setSubtitleStream(kodi_index)
|
||||||
|
app.APP.player.showSubtitles(True)
|
||||||
|
if typus == 'audio':
|
||||||
|
self.current_kodi_audio_stream = kodi_index
|
||||||
|
else:
|
||||||
|
self.current_kodi_sub_stream = kodi_index
|
||||||
|
|
||||||
|
def on_av_change(self, playerid):
|
||||||
|
kodi_audio_stream = js.get_current_audio_stream_index(playerid)
|
||||||
|
sub_enabled = js.get_subtitle_enabled(playerid)
|
||||||
|
kodi_sub_stream = js.get_current_subtitle_stream_index(playerid)
|
||||||
|
# Audio
|
||||||
|
if kodi_audio_stream != self.current_kodi_audio_stream:
|
||||||
|
self.on_kodi_audio_stream_change(kodi_audio_stream)
|
||||||
|
# Subtitles - CURRENTLY BROKEN ON THE KODI SIDE!
|
||||||
|
# current_kodi_sub_stream may also be zero
|
||||||
|
subs_off = (None, False)
|
||||||
|
if ((sub_enabled and self.current_kodi_sub_stream in subs_off)
|
||||||
|
or (not sub_enabled and self.current_kodi_sub_stream not in subs_off)
|
||||||
|
or (kodi_sub_stream is not None
|
||||||
|
and kodi_sub_stream != self.current_kodi_sub_stream)):
|
||||||
|
self.on_kodi_subtitle_stream_change(kodi_sub_stream, sub_enabled)
|
||||||
|
|
||||||
|
|
||||||
def playlist_item_from_kodi(kodi_item):
|
def playlist_item_from_kodi(kodi_item):
|
||||||
|
@ -300,7 +408,7 @@ def playlist_item_from_kodi(kodi_item):
|
||||||
except IndexError:
|
except IndexError:
|
||||||
query = ''
|
query = ''
|
||||||
query = dict(utils.parse_qsl(query))
|
query = dict(utils.parse_qsl(query))
|
||||||
item.plex_id = utils.cast(int, query.get('plex_id'))
|
item.plex_id = cast(int, query.get('plex_id'))
|
||||||
item.plex_type = query.get('itemType')
|
item.plex_type = query.get('itemType')
|
||||||
LOG.debug('Made playlist item from Kodi: %s', item)
|
LOG.debug('Made playlist item from Kodi: %s', item)
|
||||||
return item
|
return item
|
||||||
|
@ -398,21 +506,22 @@ def playlist_item_from_xml(xml_video_element, kodi_id=None, kodi_type=None):
|
||||||
item.guid = api.guid_html_escaped()
|
item.guid = api.guid_html_escaped()
|
||||||
item.playcount = api.viewcount()
|
item.playcount = api.viewcount()
|
||||||
item.offset = api.resume_point()
|
item.offset = api.resume_point()
|
||||||
item.xml = xml_video_element
|
item.api = api
|
||||||
LOG.debug('Created new playlist item from xml: %s', item)
|
LOG.debug('Created new playlist item from xml: %s', item)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
def _get_playListVersion_from_xml(playlist, xml):
|
def _update_playlist_version(playlist, xml):
|
||||||
"""
|
"""
|
||||||
Takes a PMS xml as input to overwrite the playlist version (e.g. Plex
|
Takes a PMS xml (one level above the xml-depth where we're usually applying
|
||||||
|
API()) as input to overwrite the playlist version (e.g. Plex
|
||||||
playQueueVersion).
|
playQueueVersion).
|
||||||
|
|
||||||
Raises PlaylistError if unsuccessful
|
Raises PlaylistError if unsuccessful
|
||||||
"""
|
"""
|
||||||
playlist.version = utils.cast(int,
|
try:
|
||||||
xml.get('%sVersion' % playlist.kind))
|
playlist.version = int(xml.get('%sVersion' % playlist.kind))
|
||||||
if playlist.version is None:
|
except (AttributeError, TypeError):
|
||||||
raise PlaylistError('Could not get new playlist Version for playlist '
|
raise PlaylistError('Could not get new playlist Version for playlist '
|
||||||
'%s' % playlist)
|
'%s' % playlist)
|
||||||
|
|
||||||
|
@ -424,18 +533,15 @@ def get_playlist_details_from_xml(playlist, xml):
|
||||||
|
|
||||||
Raises PlaylistError if something went wrong.
|
Raises PlaylistError if something went wrong.
|
||||||
"""
|
"""
|
||||||
playlist.id = utils.cast(int,
|
if xml is None:
|
||||||
xml.get('%sID' % playlist.kind))
|
raise PlaylistError('No playlist received for playlist %s' % playlist)
|
||||||
playlist.version = utils.cast(int,
|
playlist.id = cast(int, xml.get('%sID' % playlist.kind))
|
||||||
xml.get('%sVersion' % playlist.kind))
|
playlist.version = cast(int, xml.get('%sVersion' % playlist.kind))
|
||||||
playlist.shuffled = utils.cast(int,
|
playlist.shuffled = cast(int, xml.get('%sShuffled' % playlist.kind))
|
||||||
xml.get('%sShuffled' % playlist.kind))
|
playlist.selectedItemID = cast(int, xml.get('%sSelectedItemID'
|
||||||
playlist.selectedItemID = utils.cast(int,
|
% playlist.kind))
|
||||||
xml.get('%sSelectedItemID'
|
playlist.selectedItemOffset = cast(int, xml.get('%sSelectedItemOffset'
|
||||||
% playlist.kind))
|
% playlist.kind))
|
||||||
playlist.selectedItemOffset = utils.cast(int,
|
|
||||||
xml.get('%sSelectedItemOffset'
|
|
||||||
% playlist.kind))
|
|
||||||
LOG.debug('Updated playlist from xml: %s', playlist)
|
LOG.debug('Updated playlist from xml: %s', playlist)
|
||||||
|
|
||||||
|
|
||||||
|
@ -478,7 +584,8 @@ def init_plex_playqueue(playlist, plex_id=None, kodi_item=None):
|
||||||
params = {
|
params = {
|
||||||
'next': 0,
|
'next': 0,
|
||||||
'type': playlist.type,
|
'type': playlist.type,
|
||||||
'uri': item.uri
|
'uri': item.uri,
|
||||||
|
'includeMarkers': 1, # e.g. start + stop of intros
|
||||||
}
|
}
|
||||||
xml = DU().downloadUrl(url="{server}/%ss" % playlist.kind,
|
xml = DU().downloadUrl(url="{server}/%ss" % playlist.kind,
|
||||||
action_type="POST",
|
action_type="POST",
|
||||||
|
@ -570,16 +677,22 @@ def add_item_to_plex_playqueue(playlist, pos, plex_id=None, kodi_item=None):
|
||||||
item = playlist_item_from_plex(plex_id)
|
item = playlist_item_from_plex(plex_id)
|
||||||
else:
|
else:
|
||||||
item = playlist_item_from_kodi(kodi_item)
|
item = playlist_item_from_kodi(kodi_item)
|
||||||
url = "{server}/%ss/%s?uri=%s" % (playlist.kind, playlist.id, item.uri)
|
url = "{server}/%ss/%s" % (playlist.kind, playlist.id)
|
||||||
|
parameters = {
|
||||||
|
'uri': item.uri,
|
||||||
|
'includeMarkers': 1, # e.g. start + stop of intros
|
||||||
|
}
|
||||||
# Will always put the new item at the end of the Plex playlist
|
# Will always put the new item at the end of the Plex playlist
|
||||||
xml = DU().downloadUrl(url, action_type="PUT")
|
xml = DU().downloadUrl(url,
|
||||||
|
action_type="PUT",
|
||||||
|
parameters=parameters)
|
||||||
try:
|
try:
|
||||||
xml[-1].attrib
|
xml[-1].attrib
|
||||||
except (TypeError, AttributeError, KeyError, IndexError):
|
except (TypeError, AttributeError, KeyError, IndexError):
|
||||||
raise PlaylistError('Could not add item %s to playlist %s'
|
raise PlaylistError('Could not add item %s to playlist %s'
|
||||||
% (kodi_item, playlist))
|
% (kodi_item, playlist))
|
||||||
api = API(xml[-1])
|
api = API(xml[-1])
|
||||||
item.xml = xml[-1]
|
item.api = api
|
||||||
item.id = api.item_id()
|
item.id = api.item_id()
|
||||||
item.guid = api.guid_html_escaped()
|
item.guid = api.guid_html_escaped()
|
||||||
item.offset = api.resume_point()
|
item.offset = api.resume_point()
|
||||||
|
@ -587,7 +700,7 @@ def add_item_to_plex_playqueue(playlist, pos, plex_id=None, kodi_item=None):
|
||||||
playlist.items.append(item)
|
playlist.items.append(item)
|
||||||
if pos == len(playlist.items) - 1:
|
if pos == len(playlist.items) - 1:
|
||||||
# Item was added at the end
|
# Item was added at the end
|
||||||
_get_playListVersion_from_xml(playlist, xml)
|
_update_playlist_version(playlist, xml)
|
||||||
else:
|
else:
|
||||||
# Move the new item to the correct position
|
# Move the new item to the correct position
|
||||||
move_playlist_item(playlist,
|
move_playlist_item(playlist,
|
||||||
|
@ -631,7 +744,7 @@ def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None,
|
||||||
{'id': kodi_id, 'type': kodi_type, 'file': file})
|
{'id': kodi_id, 'type': kodi_type, 'file': file})
|
||||||
if item.plex_id is not None:
|
if item.plex_id is not None:
|
||||||
xml = PF.GetPlexMetadata(item.plex_id)
|
xml = PF.GetPlexMetadata(item.plex_id)
|
||||||
item.xml = xml[-1]
|
item.api = API(xml[-1])
|
||||||
playlist.items.insert(pos, item)
|
playlist.items.insert(pos, item)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
@ -655,9 +768,10 @@ def move_playlist_item(playlist, before_pos, after_pos):
|
||||||
playlist.id,
|
playlist.id,
|
||||||
playlist.items[before_pos].id,
|
playlist.items[before_pos].id,
|
||||||
playlist.items[after_pos - 1].id)
|
playlist.items[after_pos - 1].id)
|
||||||
# We need to increment the playlistVersion
|
# Tell the PMS that we're moving items around
|
||||||
_get_playListVersion_from_xml(
|
xml = DU().downloadUrl(url, action_type="PUT")
|
||||||
playlist, DU().downloadUrl(url, action_type="PUT"))
|
# We need to increment the playlist version for communicating with the PMS
|
||||||
|
_update_playlist_version(playlist, xml)
|
||||||
# Move our item's position in our internal playlist
|
# Move our item's position in our internal playlist
|
||||||
playlist.items.insert(after_pos, playlist.items.pop(before_pos))
|
playlist.items.insert(after_pos, playlist.items.pop(before_pos))
|
||||||
LOG.debug('Done moving for %s', playlist)
|
LOG.debug('Done moving for %s', playlist)
|
||||||
|
@ -671,10 +785,13 @@ def get_PMS_playlist(playlist, playlist_id=None):
|
||||||
Raises PlaylistError if something went wrong
|
Raises PlaylistError if something went wrong
|
||||||
"""
|
"""
|
||||||
playlist_id = playlist_id if playlist_id else playlist.id
|
playlist_id = playlist_id if playlist_id else playlist.id
|
||||||
|
parameters = {'includeMarkers': 1}
|
||||||
if playlist.kind == 'playList':
|
if playlist.kind == 'playList':
|
||||||
xml = DU().downloadUrl("{server}/playlists/%s/items" % playlist_id)
|
xml = DU().downloadUrl("{server}/playlists/%s/items" % playlist_id,
|
||||||
|
parameters=parameters)
|
||||||
else:
|
else:
|
||||||
xml = DU().downloadUrl("{server}/playQueues/%s" % playlist_id)
|
xml = DU().downloadUrl("{server}/playQueues/%s" % playlist_id,
|
||||||
|
parameters=parameters)
|
||||||
try:
|
try:
|
||||||
xml.attrib
|
xml.attrib
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
@ -701,8 +818,8 @@ def delete_playlist_item_from_PMS(playlist, pos):
|
||||||
playlist.items[pos].id,
|
playlist.items[pos].id,
|
||||||
playlist.repeat),
|
playlist.repeat),
|
||||||
action_type="DELETE")
|
action_type="DELETE")
|
||||||
_get_playListVersion_from_xml(playlist, xml)
|
|
||||||
del playlist.items[pos]
|
del playlist.items[pos]
|
||||||
|
_update_playlist_version(playlist, xml)
|
||||||
|
|
||||||
|
|
||||||
# Functions operating on the Kodi playlist objects ##########
|
# Functions operating on the Kodi playlist objects ##########
|
||||||
|
@ -773,9 +890,10 @@ def get_pms_playqueue(playqueue_id):
|
||||||
"""
|
"""
|
||||||
Returns the Plex playqueue as an etree XML or None if unsuccessful
|
Returns the Plex playqueue as an etree XML or None if unsuccessful
|
||||||
"""
|
"""
|
||||||
xml = DU().downloadUrl(
|
parameters = {'includeMarkers': 1}
|
||||||
"{server}/playQueues/%s" % playqueue_id,
|
xml = DU().downloadUrl("{server}/playQueues/%s" % playqueue_id,
|
||||||
headerOptions={'Accept': 'application/xml'})
|
parameters=parameters,
|
||||||
|
headerOptions={'Accept': 'application/xml'})
|
||||||
try:
|
try:
|
||||||
xml.attrib
|
xml.attrib
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
|
|
@ -15,13 +15,13 @@ from __future__ import absolute_import, division, unicode_literals
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from sqlite3 import OperationalError
|
from sqlite3 import OperationalError
|
||||||
|
|
||||||
from .common import Playlist, PlaylistError, PlaylistObserver, \
|
from .common import Playlist, PlaylistObserver, kodi_playlist_hash
|
||||||
kodi_playlist_hash
|
|
||||||
from . import pms, db, kodi_pl, plex_pl
|
from . import pms, db, kodi_pl, plex_pl
|
||||||
|
|
||||||
from ..watchdog import events
|
from ..watchdog import events
|
||||||
from ..plex_api import API
|
from ..plex_api import API
|
||||||
from .. import utils, path_ops, variables as v, app
|
from .. import utils, path_ops, variables as v, app
|
||||||
|
from ..exceptions import PlaylistError
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
LOG = getLogger('PLEX.playlists')
|
LOG = getLogger('PLEX.playlists')
|
||||||
|
|
|
@ -12,6 +12,8 @@ from ..watchdog.observers import Observer
|
||||||
from ..watchdog.utils.bricks import OrderedSetQueue
|
from ..watchdog.utils.bricks import OrderedSetQueue
|
||||||
|
|
||||||
from .. import path_ops, variables as v, app
|
from .. import path_ops, variables as v, app
|
||||||
|
from ..exceptions import PlaylistError
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
LOG = getLogger('PLEX.playlists.common')
|
LOG = getLogger('PLEX.playlists.common')
|
||||||
|
|
||||||
|
@ -20,13 +22,6 @@ 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 Playlist(object):
|
||||||
"""
|
"""
|
||||||
Class representing a synced Playlist with info for both Kodi and Plex.
|
Class representing a synced Playlist with info for both Kodi and Plex.
|
||||||
|
|
|
@ -7,10 +7,11 @@ module
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from .common import Playlist, PlaylistError
|
from .common import Playlist
|
||||||
from ..plex_db import PlexDB
|
from ..plex_db import PlexDB
|
||||||
from ..kodi_db import kodiid_from_filename
|
from ..kodi_db import kodiid_from_filename
|
||||||
from .. import path_ops, utils, variables as v
|
from .. import path_ops, utils, variables as v
|
||||||
|
from ..exceptions import PlaylistError
|
||||||
###############################################################################
|
###############################################################################
|
||||||
LOG = getLogger('PLEX.playlists.db')
|
LOG = getLogger('PLEX.playlists.db')
|
||||||
|
|
||||||
|
@ -121,7 +122,7 @@ def m3u_to_plex_ids(playlist):
|
||||||
def playlist_file_to_plex_ids(playlist):
|
def playlist_file_to_plex_ids(playlist):
|
||||||
"""
|
"""
|
||||||
Takes the playlist file located at path [unicode] and parses it.
|
Takes the playlist file located at path [unicode] and parses it.
|
||||||
Returns a list of plex_ids (str) or raises PL.PlaylistError if a single
|
Returns a list of plex_ids (str) or raises PlaylistError if a single
|
||||||
item cannot be parsed from Kodi to Plex.
|
item cannot be parsed from Kodi to Plex.
|
||||||
"""
|
"""
|
||||||
if playlist.kodi_extension == 'm3u':
|
if playlist.kodi_extension == 'm3u':
|
||||||
|
|
|
@ -7,11 +7,13 @@ from __future__ import absolute_import, division, unicode_literals
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import Playlist, PlaylistError, kodi_playlist_hash
|
from .common import Playlist, kodi_playlist_hash
|
||||||
from . import db, pms
|
from . import db, pms
|
||||||
|
|
||||||
from ..plex_api import API
|
from ..plex_api import API
|
||||||
from .. import utils, path_ops, variables as v
|
from .. import utils, path_ops, variables as v
|
||||||
|
from ..exceptions import PlaylistError
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
LOG = getLogger('PLEX.playlists.kodi_pl')
|
LOG = getLogger('PLEX.playlists.kodi_pl')
|
||||||
REGEX_FILE_NUMBERING = re.compile(r'''_(\d\d)\.\w+$''')
|
REGEX_FILE_NUMBERING = re.compile(r'''_(\d\d)\.\w+$''')
|
||||||
|
|
|
@ -6,8 +6,9 @@ Create and delete playlists on the Plex side of things
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from .common import PlaylistError
|
|
||||||
from . import pms, db
|
from . import pms, db
|
||||||
|
from ..exceptions import PlaylistError
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
LOG = getLogger('PLEX.playlists.plex_pl')
|
LOG = getLogger('PLEX.playlists.plex_pl')
|
||||||
# Used for updating Plex playlists due to Kodi changes - Plex playlist
|
# Used for updating Plex playlists due to Kodi changes - Plex playlist
|
||||||
|
|
|
@ -7,11 +7,11 @@ manipulate playlists
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from .common import PlaylistError
|
|
||||||
|
|
||||||
from ..plex_api import API
|
from ..plex_api import API
|
||||||
from ..downloadutils import DownloadUtils as DU
|
from ..downloadutils import DownloadUtils as DU
|
||||||
from .. import utils, app, variables as v
|
from .. import utils, app, variables as v
|
||||||
|
from ..exceptions import PlaylistError
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
LOG = getLogger('PLEX.playlists.pms')
|
LOG = getLogger('PLEX.playlists.pms')
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import xbmc
|
||||||
from .plex_api import API
|
from .plex_api import API
|
||||||
from . import playlist_func as PL, plex_functions as PF
|
from . import playlist_func as PL, plex_functions as PF
|
||||||
from . import backgroundthread, utils, json_rpc as js, app, variables as v
|
from . import backgroundthread, utils, json_rpc as js, app, variables as v
|
||||||
|
from . import exceptions
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
LOG = getLogger('PLEX.playqueue')
|
LOG = getLogger('PLEX.playqueue')
|
||||||
|
@ -88,7 +89,7 @@ def init_playqueue_from_plex_children(plex_id, transient_token=None):
|
||||||
api = API(child)
|
api = API(child)
|
||||||
try:
|
try:
|
||||||
PL.add_item_to_playlist(playqueue, i, plex_id=api.plex_id)
|
PL.add_item_to_playlist(playqueue, i, plex_id=api.plex_id)
|
||||||
except PL.PlaylistError:
|
except exceptions.PlaylistError:
|
||||||
LOG.error('Could not add Plex item to our playlist: %s, %s',
|
LOG.error('Could not add Plex item to our playlist: %s, %s',
|
||||||
child.tag, child.attrib)
|
child.tag, child.attrib)
|
||||||
playqueue.plex_transient_token = transient_token
|
playqueue.plex_transient_token = transient_token
|
||||||
|
@ -151,7 +152,7 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
|
||||||
i + j, i)
|
i + j, i)
|
||||||
try:
|
try:
|
||||||
PL.move_playlist_item(playqueue, i + j, i)
|
PL.move_playlist_item(playqueue, i + j, i)
|
||||||
except PL.PlaylistError:
|
except exceptions.PlaylistError:
|
||||||
LOG.error('Could not modify playqueue positions')
|
LOG.error('Could not modify playqueue positions')
|
||||||
LOG.error('This is likely caused by mixing audio and '
|
LOG.error('This is likely caused by mixing audio and '
|
||||||
'video tracks in the Kodi playqueue')
|
'video tracks in the Kodi playqueue')
|
||||||
|
@ -167,7 +168,7 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
|
||||||
PL.add_item_to_plex_playqueue(playqueue,
|
PL.add_item_to_plex_playqueue(playqueue,
|
||||||
i,
|
i,
|
||||||
kodi_item=new_item)
|
kodi_item=new_item)
|
||||||
except PL.PlaylistError:
|
except exceptions.PlaylistError:
|
||||||
# Could not add the element
|
# Could not add the element
|
||||||
pass
|
pass
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -196,7 +197,7 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
|
||||||
LOG.debug('Detected deletion of playqueue element at pos %s', i)
|
LOG.debug('Detected deletion of playqueue element at pos %s', i)
|
||||||
try:
|
try:
|
||||||
PL.delete_playlist_item_from_PMS(playqueue, i)
|
PL.delete_playlist_item_from_PMS(playqueue, i)
|
||||||
except PL.PlaylistError:
|
except exceptions.PlaylistError:
|
||||||
LOG.error('Could not delete PMS element from position %s', i)
|
LOG.error('Could not delete PMS element from position %s', i)
|
||||||
LOG.error('This is likely caused by mixing audio and '
|
LOG.error('This is likely caused by mixing audio and '
|
||||||
'video tracks in the Kodi playqueue')
|
'video tracks in the Kodi playqueue')
|
||||||
|
|
|
@ -222,12 +222,14 @@ class Artwork(object):
|
||||||
else:
|
else:
|
||||||
# Not supported artwork
|
# Not supported artwork
|
||||||
return artworks
|
return artworks
|
||||||
data = DU().downloadUrl(url, authenticate=False, timeout=15)
|
data = DU().downloadUrl(url,
|
||||||
try:
|
authenticate=False,
|
||||||
data.get('test')
|
timeout=15,
|
||||||
except AttributeError:
|
return_response=True)
|
||||||
LOG.error('Could not download data from FanartTV')
|
if not data.ok:
|
||||||
|
LOG.debug('Could not download data from FanartTV')
|
||||||
return artworks
|
return artworks
|
||||||
|
data = data.json()
|
||||||
|
|
||||||
fanart_tv_types = list(v.FANART_TV_TO_KODI_TYPE)
|
fanart_tv_types = list(v.FANART_TV_TO_KODI_TYPE)
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,9 @@ LOG = getLogger('PLEX.api')
|
||||||
|
|
||||||
METADATA_PROVIDERS = (('imdb', utils.REGEX_IMDB),
|
METADATA_PROVIDERS = (('imdb', utils.REGEX_IMDB),
|
||||||
('tvdb', utils.REGEX_TVDB),
|
('tvdb', utils.REGEX_TVDB),
|
||||||
('tmdb', utils.REGEX_TMDB))
|
('tmdb', utils.REGEX_TMDB),
|
||||||
|
('anidb', utils.REGEX_ANIDB))
|
||||||
|
|
||||||
|
|
||||||
class Base(object):
|
class Base(object):
|
||||||
"""
|
"""
|
||||||
|
@ -23,6 +25,7 @@ class Base(object):
|
||||||
|
|
||||||
xml: xml.etree.ElementTree element
|
xml: xml.etree.ElementTree element
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, xml):
|
def __init__(self, xml):
|
||||||
self.xml = xml
|
self.xml = xml
|
||||||
# which media part in the XML response shall we look at if several
|
# which media part in the XML response shall we look at if several
|
||||||
|
@ -43,6 +46,7 @@ class Base(object):
|
||||||
self._writers = []
|
self._writers = []
|
||||||
self._producers = []
|
self._producers = []
|
||||||
self._locations = []
|
self._locations = []
|
||||||
|
self._intro_markers = []
|
||||||
self._guids = {}
|
self._guids = {}
|
||||||
self._coll_match = None
|
self._coll_match = None
|
||||||
# Plex DB attributes
|
# Plex DB attributes
|
||||||
|
@ -231,8 +235,8 @@ class Base(object):
|
||||||
addedAt is used.
|
addedAt is used.
|
||||||
"""
|
"""
|
||||||
return int('%s%s' % (self.xml.get('ratingKey'),
|
return int('%s%s' % (self.xml.get('ratingKey'),
|
||||||
self.xml.get('updatedAt') or
|
abs(int(self.xml.get('updatedAt') or
|
||||||
self.xml.get('addedAt', '1541572987')))
|
self.xml.get('addedAt', '1541572987')))))
|
||||||
|
|
||||||
def title(self):
|
def title(self):
|
||||||
"""
|
"""
|
||||||
|
@ -253,7 +257,21 @@ class Base(object):
|
||||||
Returns the media streams directly from the PMS xml.
|
Returns the media streams directly from the PMS xml.
|
||||||
Mind to set self.mediastream and self.part before calling this method!
|
Mind to set self.mediastream and self.part before calling this method!
|
||||||
"""
|
"""
|
||||||
return self.xml[self.mediastream][self.part]
|
try:
|
||||||
|
return self.xml[self.mediastream][self.part]
|
||||||
|
except TypeError:
|
||||||
|
# Direct Paths when we don't set mediastream and part
|
||||||
|
return self.xml[0][0]
|
||||||
|
|
||||||
|
def part_id(self):
|
||||||
|
"""
|
||||||
|
Returns the unique id of the currently active part [int]
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return int(self.xml[self.mediastream][self.part].attrib['id'])
|
||||||
|
except TypeError:
|
||||||
|
# Direct Paths when we don't set mediastream and part
|
||||||
|
return int(self.xml[0][0].attrib['id'])
|
||||||
|
|
||||||
def plot(self):
|
def plot(self):
|
||||||
"""
|
"""
|
||||||
|
@ -397,6 +415,12 @@ class Base(object):
|
||||||
"""
|
"""
|
||||||
return self.parent_index()
|
return self.parent_index()
|
||||||
|
|
||||||
|
def season_name(self):
|
||||||
|
"""
|
||||||
|
Returns the season's name/title or None
|
||||||
|
"""
|
||||||
|
return self.xml.get('title')
|
||||||
|
|
||||||
def artist_name(self):
|
def artist_name(self):
|
||||||
"""
|
"""
|
||||||
Returns the artist name for an album: first it attempts to return
|
Returns the artist name for an album: first it attempts to return
|
||||||
|
@ -470,6 +494,14 @@ class Base(object):
|
||||||
guid = child.get('id')
|
guid = child.get('id')
|
||||||
guid = guid.split('://', 1)
|
guid = guid.split('://', 1)
|
||||||
self._guids[guid[0]] = guid[1]
|
self._guids[guid[0]] = guid[1]
|
||||||
|
elif child.tag == 'Marker' and child.get('type') == 'intro':
|
||||||
|
intro = (cast(float, child.get('startTimeOffset')),
|
||||||
|
cast(float, child.get('endTimeOffset')))
|
||||||
|
if None in intro:
|
||||||
|
# Safety net if PMS xml is not as expected
|
||||||
|
continue
|
||||||
|
intro = (intro[0] / 1000.0, intro[1] / 1000.0)
|
||||||
|
self._intro_markers.append(intro)
|
||||||
# Plex Movie agent (legacy) or "normal" Plex tv show agent
|
# Plex Movie agent (legacy) or "normal" Plex tv show agent
|
||||||
if not self._guids:
|
if not self._guids:
|
||||||
guid = self.xml.get('guid')
|
guid = self.xml.get('guid')
|
||||||
|
|
|
@ -28,6 +28,16 @@ class Media(object):
|
||||||
"""
|
"""
|
||||||
return self.xml[0][self.part].get(key, self.xml[0].get(key))
|
return self.xml[0][self.part].get(key, self.xml[0].get(key))
|
||||||
|
|
||||||
|
def intro_markers(self):
|
||||||
|
"""
|
||||||
|
Returns a list of tuples with floats (startTimeOffset, endTimeOffset)
|
||||||
|
in Koditime or an empty list.
|
||||||
|
Each entry represents an (episode) intro that Plex detected and that
|
||||||
|
can be skipped
|
||||||
|
"""
|
||||||
|
self._scan_children()
|
||||||
|
return self._intro_markers
|
||||||
|
|
||||||
def video_codec(self):
|
def video_codec(self):
|
||||||
"""
|
"""
|
||||||
Returns the video codec and resolution for the child and part selected.
|
Returns the video codec and resolution for the child and part selected.
|
||||||
|
@ -62,6 +72,25 @@ class Media(object):
|
||||||
answ['bitDepth'] = None
|
answ['bitDepth'] = None
|
||||||
return answ
|
return answ
|
||||||
|
|
||||||
|
def picture_codec(self):
|
||||||
|
"""
|
||||||
|
Returns the exif metadata of pictures. This does NOT seem to be used
|
||||||
|
reliably by Kodi skins! (e.g. not at all)
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'exif:CameraMake': self.xml[0].get('make'), # e.g. 'Canon'
|
||||||
|
'exif:CameraModel': self.xml[0].get('model'), # e.g. 'Canon XYZ'
|
||||||
|
'exif:DateTime': self.xml.get('originallyAvailableAt', '').replace('-', ':') or None, # e.g. '2017-11-05'
|
||||||
|
'exif:Height': self.xml[0].get('height'), # e.g. '2160'
|
||||||
|
'exif:Width': self.xml[0].get('width'), # e.g. '3240'
|
||||||
|
'exif:Orientation': self.xml[0][self.part].get('orientation'), # e.g. '1'
|
||||||
|
'exif:FocalLength': self.xml[0].get('focalLength'), # TO BE VALIDATED
|
||||||
|
'exif:ExposureTime': self.xml[0].get('exposure'), # e.g. '1/1000'
|
||||||
|
'exif:ApertureFNumber': self.xml[0].get('aperture'), # e.g. 'f/5.0'
|
||||||
|
'exif:ISOequivalent': self.xml[0].get('iso'), # e.g. '1600'
|
||||||
|
# missing on Kodi side: lens, e.g. "EF50mm f/1.8 II"
|
||||||
|
}
|
||||||
|
|
||||||
def mediastreams(self):
|
def mediastreams(self):
|
||||||
"""
|
"""
|
||||||
Returns the media streams for metadata purposes
|
Returns the media streams for metadata purposes
|
||||||
|
@ -149,7 +178,7 @@ class Media(object):
|
||||||
count += 1
|
count += 1
|
||||||
if (count > 1 and (
|
if (count > 1 and (
|
||||||
(self.plex_type != v.PLEX_TYPE_CLIP and
|
(self.plex_type != v.PLEX_TYPE_CLIP and
|
||||||
utils.settings('bestQuality') == 'false')
|
utils.settings('firstVideoStream') == 'false')
|
||||||
or
|
or
|
||||||
(self.plex_type == v.PLEX_TYPE_CLIP and
|
(self.plex_type == v.PLEX_TYPE_CLIP and
|
||||||
utils.settings('bestTrailer') == 'false'))):
|
utils.settings('bestTrailer') == 'false'))):
|
||||||
|
@ -291,16 +320,15 @@ class Media(object):
|
||||||
filename,
|
filename,
|
||||||
extension)
|
extension)
|
||||||
response = DU().downloadUrl(url, return_response=True)
|
response = DU().downloadUrl(url, return_response=True)
|
||||||
try:
|
if not response.ok:
|
||||||
response.status_code
|
|
||||||
except AttributeError:
|
|
||||||
LOG.error('Could not temporarily download subtitle %s', url)
|
LOG.error('Could not temporarily download subtitle %s', url)
|
||||||
|
LOG.error('HTTP status: %s, message: %s',
|
||||||
|
response.status_code, response.text)
|
||||||
return
|
return
|
||||||
else:
|
LOG.debug('Writing temp subtitle to %s', path)
|
||||||
LOG.debug('Writing temp subtitle to %s', path)
|
with open(path_ops.encode_path(path), 'wb') as f:
|
||||||
with open(path_ops.encode_path(path), 'wb') as f:
|
f.write(response.content)
|
||||||
f.write(response.content)
|
return path
|
||||||
return path
|
|
||||||
|
|
||||||
def validate_playurl(self, path, typus, force_check=False, folder=False,
|
def validate_playurl(self, path, typus, force_check=False, folder=False,
|
||||||
omit_check=False):
|
omit_check=False):
|
||||||
|
|
|
@ -21,6 +21,7 @@ from . import playqueue as PQ
|
||||||
from . import variables as v
|
from . import variables as v
|
||||||
from . import backgroundthread
|
from . import backgroundthread
|
||||||
from . import app
|
from . import app
|
||||||
|
from . import exceptions
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -51,7 +52,7 @@ def update_playqueue_from_PMS(playqueue,
|
||||||
with app.APP.lock_playqueues:
|
with app.APP.lock_playqueues:
|
||||||
try:
|
try:
|
||||||
xml = PL.get_PMS_playlist(playqueue, playqueue_id)
|
xml = PL.get_PMS_playlist(playqueue, playqueue_id)
|
||||||
except PL.PlaylistError:
|
except exceptions.PlaylistError:
|
||||||
LOG.error('Could now download playqueue %s', playqueue_id)
|
LOG.error('Could now download playqueue %s', playqueue_id)
|
||||||
return
|
return
|
||||||
if playqueue.id == playqueue_id:
|
if playqueue.id == playqueue_id:
|
||||||
|
@ -64,7 +65,7 @@ def update_playqueue_from_PMS(playqueue,
|
||||||
# Get new metadata for the playqueue first
|
# Get new metadata for the playqueue first
|
||||||
try:
|
try:
|
||||||
PL.get_playlist_details_from_xml(playqueue, xml)
|
PL.get_playlist_details_from_xml(playqueue, xml)
|
||||||
except PL.PlaylistError:
|
except exceptions.PlaylistError:
|
||||||
LOG.error('Could not get playqueue ID %s', playqueue_id)
|
LOG.error('Could not get playqueue ID %s', playqueue_id)
|
||||||
return
|
return
|
||||||
playqueue.repeat = 0 if not repeat else int(repeat)
|
playqueue.repeat = 0 if not repeat else int(repeat)
|
||||||
|
|
|
@ -164,7 +164,7 @@ def check_connection(url, token=None, verifySSL=None):
|
||||||
def discover_pms(token=None):
|
def discover_pms(token=None):
|
||||||
"""
|
"""
|
||||||
Optional parameter:
|
Optional parameter:
|
||||||
token token for plex.tv
|
token token for plex.tv - WARNING: for the main Plex user only!
|
||||||
|
|
||||||
Returns a list of available PMS to connect to, one entry is the dict:
|
Returns a list of available PMS to connect to, one entry is the dict:
|
||||||
{
|
{
|
||||||
|
@ -479,6 +479,7 @@ def GetPlexMetadata(key, reraise=False):
|
||||||
'includeReviews': 1,
|
'includeReviews': 1,
|
||||||
'includeRelated': 0, # Similar movies => Video -> Related
|
'includeRelated': 0, # Similar movies => Video -> Related
|
||||||
'skipRefresh': 1,
|
'skipRefresh': 1,
|
||||||
|
'includeMarkers': 1, # e.g. start + stop of intros
|
||||||
# 'includeRelatedCount': 0,
|
# 'includeRelatedCount': 0,
|
||||||
# 'includeOnDeck': 1,
|
# 'includeOnDeck': 1,
|
||||||
# 'includeChapters': 1,
|
# 'includeChapters': 1,
|
||||||
|
@ -518,7 +519,9 @@ def get_playback_xml(url, server_name, authenticate=True, token=None):
|
||||||
"""
|
"""
|
||||||
Returns None if something went wrong
|
Returns None if something went wrong
|
||||||
"""
|
"""
|
||||||
header_options = {'X-Plex-Token': token} if not authenticate else None
|
header_options = {'includeMarkers': 1}
|
||||||
|
if not authenticate:
|
||||||
|
header_options['X-Plex-Token'] = token
|
||||||
try:
|
try:
|
||||||
xml = DU().downloadUrl(url,
|
xml = DU().downloadUrl(url,
|
||||||
authenticate=authenticate,
|
authenticate=authenticate,
|
||||||
|
@ -806,7 +809,8 @@ def init_plex_playqueue(plex_id, plex_type, section_uuid, trailers=False):
|
||||||
(app.CONN.machine_identifier, plex_id)),
|
(app.CONN.machine_identifier, plex_id)),
|
||||||
'includeChapters': '1',
|
'includeChapters': '1',
|
||||||
'shuffle': '0',
|
'shuffle': '0',
|
||||||
'repeat': '0'
|
'repeat': '0',
|
||||||
|
'includeMarkers': 1, # e.g. start + stop of intros
|
||||||
}
|
}
|
||||||
if trailers is True:
|
if trailers is True:
|
||||||
args['extrasPrefixCount'] = utils.settings('trailerNumber')
|
args['extrasPrefixCount'] = utils.settings('trailerNumber')
|
||||||
|
@ -1113,3 +1117,35 @@ def playback_decision(path, media, part, playmethod, video=True, args=None):
|
||||||
return DU().downloadUrl(utils.extend_url(url, arguments),
|
return DU().downloadUrl(utils.extend_url(url, arguments),
|
||||||
headerOptions=v.STREAMING_HEADERS,
|
headerOptions=v.STREAMING_HEADERS,
|
||||||
reraise=True)
|
reraise=True)
|
||||||
|
|
||||||
|
|
||||||
|
def change_subtitle(plex_stream_id, part_id):
|
||||||
|
"""
|
||||||
|
Tell the PMS to display/burn-in the subtitle stream with id plex_stream_id
|
||||||
|
for the Part (PMS XML etree tag "Part") with unique id part_id.
|
||||||
|
- plex_stream_id = 0 will deactivate the subtitle
|
||||||
|
- We always do this for ALL parts of a video
|
||||||
|
"""
|
||||||
|
arguments = {
|
||||||
|
'subtitleStreamID': plex_stream_id,
|
||||||
|
'allParts': 1
|
||||||
|
}
|
||||||
|
url = '{server}/library/parts/%s' % part_id
|
||||||
|
return DU().downloadUrl(utils.extend_url(url, arguments),
|
||||||
|
action_type='PUT')
|
||||||
|
|
||||||
|
|
||||||
|
def change_audio_stream(plex_stream_id, part_id):
|
||||||
|
"""
|
||||||
|
Tell the PMS to display/burn-in the subtitle stream with id plex_stream_id
|
||||||
|
for the Part (PMS XML etree tag "Part") with unique id part_id.
|
||||||
|
- plex_stream_id = 0 will deactivate the subtitle
|
||||||
|
- We always do this for ALL parts of a video
|
||||||
|
"""
|
||||||
|
arguments = {
|
||||||
|
'audioStreamID': plex_stream_id,
|
||||||
|
'allParts': 1
|
||||||
|
}
|
||||||
|
url = '{server}/library/parts/%s' % part_id
|
||||||
|
return DU().downloadUrl(utils.extend_url(url, arguments),
|
||||||
|
action_type='PUT')
|
||||||
|
|
|
@ -35,18 +35,6 @@ class HomeUser(utils.AttributeDict):
|
||||||
return self.restricted == '1'
|
return self.restricted == '1'
|
||||||
|
|
||||||
|
|
||||||
def homeuser_to_settings(user):
|
|
||||||
"""
|
|
||||||
Writes one HomeUser to the Kodi settings file
|
|
||||||
"""
|
|
||||||
utils.settings('myplexlogin', 'true')
|
|
||||||
utils.settings('plexLogin', user.title)
|
|
||||||
utils.settings('plexToken', user.authToken)
|
|
||||||
utils.settings('plexid', user.id)
|
|
||||||
utils.settings('plexAvatar', user.thumb)
|
|
||||||
utils.settings('plex_status', value=utils.lang(39227))
|
|
||||||
|
|
||||||
|
|
||||||
def switch_home_user(userid, pin, token, machine_identifier):
|
def switch_home_user(userid, pin, token, machine_identifier):
|
||||||
"""
|
"""
|
||||||
Retrieves Plex home token for a Plex home user. Returns None if this fails
|
Retrieves Plex home token for a Plex home user. Returns None if this fails
|
||||||
|
@ -59,8 +47,6 @@ def switch_home_user(userid, pin, token, machine_identifier):
|
||||||
Output:
|
Output:
|
||||||
usertoken Might be empty strings if no token found
|
usertoken Might be empty strings if no token found
|
||||||
for the machine_identifier that was chosen
|
for the machine_identifier that was chosen
|
||||||
|
|
||||||
utils.settings('userid') and utils.settings('username') with new plex token
|
|
||||||
"""
|
"""
|
||||||
LOG.info('Switching to user %s', userid)
|
LOG.info('Switching to user %s', userid)
|
||||||
url = 'https://plex.tv/api/home/users/%s/switch' % userid
|
url = 'https://plex.tv/api/home/users/%s/switch' % userid
|
||||||
|
@ -79,16 +65,6 @@ def switch_home_user(userid, pin, token, machine_identifier):
|
||||||
username = xml.get('title', '')
|
username = xml.get('title', '')
|
||||||
token = xml.get('authenticationToken', '')
|
token = xml.get('authenticationToken', '')
|
||||||
|
|
||||||
# Write to settings file
|
|
||||||
utils.settings('username', username)
|
|
||||||
utils.settings('accessToken', token)
|
|
||||||
utils.settings('userid', xml.get('id', ''))
|
|
||||||
utils.settings('plex_restricteduser',
|
|
||||||
'true' if xml.get('restricted', '0') == '1'
|
|
||||||
else 'false')
|
|
||||||
app.CONN.restricted_user = True if \
|
|
||||||
xml.get('restricted', '0') == '1' else False
|
|
||||||
|
|
||||||
# Get final token to the PMS we've chosen
|
# Get final token to the PMS we've chosen
|
||||||
url = 'https://plex.tv/api/resources?includeHttps=1'
|
url = 'https://plex.tv/api/resources?includeHttps=1'
|
||||||
xml = DU().downloadUrl(url,
|
xml = DU().downloadUrl(url,
|
||||||
|
@ -101,15 +77,14 @@ def switch_home_user(userid, pin, token, machine_identifier):
|
||||||
# Set to empty iterable list for loop
|
# Set to empty iterable list for loop
|
||||||
xml = []
|
xml = []
|
||||||
|
|
||||||
found = 0
|
|
||||||
LOG.debug('Our machine_identifier is %s', machine_identifier)
|
LOG.debug('Our machine_identifier is %s', machine_identifier)
|
||||||
for device in xml:
|
for device in xml:
|
||||||
identifier = device.attrib.get('clientIdentifier')
|
identifier = device.attrib.get('clientIdentifier')
|
||||||
LOG.debug('Found the Plex clientIdentifier: %s', identifier)
|
LOG.debug('Found the Plex clientIdentifier: %s', identifier)
|
||||||
if identifier == machine_identifier:
|
if identifier == machine_identifier:
|
||||||
found += 1
|
|
||||||
token = device.attrib.get('accessToken')
|
token = device.attrib.get('accessToken')
|
||||||
if found == 0:
|
break
|
||||||
|
else:
|
||||||
LOG.info('No tokens found for your server! Using empty string')
|
LOG.info('No tokens found for your server! Using empty string')
|
||||||
token = ''
|
token = ''
|
||||||
LOG.info('Plex.tv switch HomeUser change successfull for user %s',
|
LOG.info('Plex.tv switch HomeUser change successfull for user %s',
|
||||||
|
@ -232,7 +207,11 @@ def sign_in_with_pin():
|
||||||
if xml is None:
|
if xml is None:
|
||||||
return
|
return
|
||||||
user = HomeUser(xml.attrib)
|
user = HomeUser(xml.attrib)
|
||||||
homeuser_to_settings(user)
|
utils.settings('myplexlogin', 'true')
|
||||||
|
utils.settings('plex_status', value=utils.lang(39227))
|
||||||
|
utils.settings('plexLogin', user.title)
|
||||||
|
utils.settings('plexid', user.id)
|
||||||
|
utils.settings('plexToken', user.authToken)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ from . import variables as v
|
||||||
from . import app
|
from . import app
|
||||||
from . import loghandler
|
from . import loghandler
|
||||||
from . import backgroundthread
|
from . import backgroundthread
|
||||||
|
from . import skip_plex_intro
|
||||||
from .windows import userselect
|
from .windows import userselect
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
@ -97,7 +98,8 @@ class Service(object):
|
||||||
self.welcome_msg = True
|
self.welcome_msg = True
|
||||||
self.connection_check_counter = 0
|
self.connection_check_counter = 0
|
||||||
self.setup = None
|
self.setup = None
|
||||||
self.alexa = None
|
self.pms_ws = None
|
||||||
|
self.alexa_ws = None
|
||||||
self.playqueue = None
|
self.playqueue = None
|
||||||
# Flags for other threads
|
# Flags for other threads
|
||||||
self.connection_check_running = False
|
self.connection_check_running = False
|
||||||
|
@ -361,6 +363,7 @@ class Service(object):
|
||||||
app.ACCOUNT.set_authenticated()
|
app.ACCOUNT.set_authenticated()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
user = None
|
||||||
while True:
|
while True:
|
||||||
# Could not use settings - try to get Plex user list from plex.tv
|
# Could not use settings - try to get Plex user list from plex.tv
|
||||||
if app.ACCOUNT.plex_token:
|
if app.ACCOUNT.plex_token:
|
||||||
|
@ -416,6 +419,13 @@ class Service(object):
|
||||||
utils.settings('username', value=username)
|
utils.settings('username', value=username)
|
||||||
utils.settings('userid', value=user_id)
|
utils.settings('userid', value=user_id)
|
||||||
utils.settings('accessToken', value=token)
|
utils.settings('accessToken', value=token)
|
||||||
|
if user:
|
||||||
|
utils.settings('plex_restricteduser',
|
||||||
|
'true' if user.isManaged else 'false')
|
||||||
|
app.CONN.restricted_user = user.isManaged
|
||||||
|
else:
|
||||||
|
utils.settings('plex_restricteduser', 'false')
|
||||||
|
app.CONN.restricted_user = False
|
||||||
app.ACCOUNT.load()
|
app.ACCOUNT.load()
|
||||||
app.ACCOUNT.set_authenticated()
|
app.ACCOUNT.set_authenticated()
|
||||||
return True
|
return True
|
||||||
|
@ -437,8 +447,8 @@ class Service(object):
|
||||||
self.setup.setup()
|
self.setup.setup()
|
||||||
|
|
||||||
# Initialize important threads
|
# Initialize important threads
|
||||||
self.ws = websocket_client.PMS_Websocket()
|
self.pms_ws = websocket_client.get_pms_websocketapp()
|
||||||
self.alexa = websocket_client.Alexa_Websocket()
|
self.alexa_ws = websocket_client.get_alexa_websocketapp()
|
||||||
self.sync = sync.Sync()
|
self.sync = sync.Sync()
|
||||||
self.plexcompanion = plex_companion.PlexCompanion()
|
self.plexcompanion = plex_companion.PlexCompanion()
|
||||||
self.playqueue = playqueue.PlayqueueMonitor()
|
self.playqueue = playqueue.PlayqueueMonitor()
|
||||||
|
@ -538,13 +548,16 @@ class Service(object):
|
||||||
continue
|
continue
|
||||||
elif not self.startup_completed:
|
elif not self.startup_completed:
|
||||||
self.startup_completed = True
|
self.startup_completed = True
|
||||||
self.ws.start()
|
self.pms_ws.start()
|
||||||
self.sync.start()
|
self.sync.start()
|
||||||
self.plexcompanion.start()
|
self.plexcompanion.start()
|
||||||
self.playqueue.start()
|
self.playqueue.start()
|
||||||
self.alexa.start()
|
self.alexa_ws.start()
|
||||||
|
|
||||||
xbmc.sleep(100)
|
elif app.APP.is_playing:
|
||||||
|
skip_plex_intro.check()
|
||||||
|
|
||||||
|
xbmc.sleep(200)
|
||||||
|
|
||||||
# EXITING PKC
|
# EXITING PKC
|
||||||
# Tell all threads to terminate (e.g. several lib sync threads)
|
# Tell all threads to terminate (e.g. several lib sync threads)
|
||||||
|
|
38
resources/lib/skip_plex_intro.py
Normal file
38
resources/lib/skip_plex_intro.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
from .windows.skip_intro import SkipIntroDialog
|
||||||
|
from . import app, variables as v
|
||||||
|
|
||||||
|
|
||||||
|
def skip_intro(intros):
|
||||||
|
try:
|
||||||
|
progress = app.APP.player.getTime()
|
||||||
|
except RuntimeError:
|
||||||
|
# XBMC is not playing any media file yet
|
||||||
|
return
|
||||||
|
in_intro = False
|
||||||
|
for start, end in intros:
|
||||||
|
if start <= progress < end:
|
||||||
|
in_intro = True
|
||||||
|
if in_intro and app.APP.skip_intro_dialog is None:
|
||||||
|
app.APP.skip_intro_dialog = SkipIntroDialog('script-plex-skip_intro.xml',
|
||||||
|
v.ADDON_PATH,
|
||||||
|
'default',
|
||||||
|
'1080i',
|
||||||
|
intro_end=end)
|
||||||
|
app.APP.skip_intro_dialog.show()
|
||||||
|
elif not in_intro and app.APP.skip_intro_dialog is not None:
|
||||||
|
app.APP.skip_intro_dialog.close()
|
||||||
|
app.APP.skip_intro_dialog = None
|
||||||
|
|
||||||
|
|
||||||
|
def check():
|
||||||
|
with app.APP.lock_playqueues:
|
||||||
|
if len(app.PLAYSTATE.active_players) != 1:
|
||||||
|
return
|
||||||
|
playerid = list(app.PLAYSTATE.active_players)[0]
|
||||||
|
intros = app.PLAYSTATE.player_states[playerid]['intro_markers']
|
||||||
|
if not intros:
|
||||||
|
return
|
||||||
|
skip_intro(intros)
|
470
resources/lib/subtitles.py
Normal file
470
resources/lib/subtitles.py
Normal file
|
@ -0,0 +1,470 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
from logging import getLogger
|
||||||
|
import re
|
||||||
|
from os import path
|
||||||
|
import xml.etree.ElementTree as etree
|
||||||
|
|
||||||
|
from . import app
|
||||||
|
from . import path_ops
|
||||||
|
from . import variables as v
|
||||||
|
from .exceptions import SubtitleError
|
||||||
|
|
||||||
|
LOG = getLogger('PLEX.subtitles')
|
||||||
|
|
||||||
|
# See https://kodi.wiki/view/Subtitles
|
||||||
|
SUBTITLE_LANGUAGE = re.compile(r'''(?i)[\. -]*(.*?)([\. -]forced)?$''')
|
||||||
|
|
||||||
|
# Plex support for external subtitles: srt, smi, ssa, aas, vtt
|
||||||
|
# https://support.plex.tv/articles/200471133-adding-local-subtitles-to-your-media/
|
||||||
|
|
||||||
|
# Which subtitles files are picked up by Kodi, what extensions do they need?
|
||||||
|
KODI_SUBTITLE_EXTENSIONS = ('srt', 'ssa', 'ass', 'usf', 'cdg', 'idx', 'sub',
|
||||||
|
'utf', 'aqt', 'jss', 'psb', 'rt', 'smi', 'txt',
|
||||||
|
'smil', 'stl', 'dks', 'pjs', 'mpl2', 'mks')
|
||||||
|
|
||||||
|
# Official language designations. Tuples consist of
|
||||||
|
# (ISO language name, ISO 639-1, ISO 639-2, ISO 639-2/B)
|
||||||
|
# source: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
|
||||||
|
LANGUAGE_ISO_CODES = (
|
||||||
|
('abkhazian', 'ab', 'abk', 'abk'),
|
||||||
|
('afar', 'aa', 'aar', 'aar'),
|
||||||
|
('afrikaans', 'af', 'afr', 'afr'),
|
||||||
|
('akan', 'ak', 'aka', 'aka'),
|
||||||
|
('albanian', 'sq', 'sqi', 'alb'),
|
||||||
|
('amharic', 'am', 'amh', 'amh'),
|
||||||
|
('arabic', 'ar', 'ara', 'ara'),
|
||||||
|
('aragonese', 'an', 'arg', 'arg'),
|
||||||
|
('armenian', 'hy', 'hye', 'arm'),
|
||||||
|
('assamese', 'as', 'asm', 'asm'),
|
||||||
|
('avaric', 'av', 'ava', 'ava'),
|
||||||
|
('avestan', 'ae', 'ave', 'ave'),
|
||||||
|
('aymara', 'ay', 'aym', 'aym'),
|
||||||
|
('azerbaijani', 'az', 'aze', 'aze'),
|
||||||
|
('bambara', 'bm', 'bam', 'bam'),
|
||||||
|
('bashkir', 'ba', 'bak', 'bak'),
|
||||||
|
('basque', 'eu', 'eus', 'baq'),
|
||||||
|
('belarusian', 'be', 'bel', 'bel'),
|
||||||
|
('bengali', 'bn', 'ben', 'ben'),
|
||||||
|
('bislama', 'bi', 'bis', 'bis'),
|
||||||
|
('bosnian', 'bs', 'bos', 'bos'),
|
||||||
|
('breton', 'br', 'bre', 'bre'),
|
||||||
|
('bulgarian', 'bg', 'bul', 'bul'),
|
||||||
|
('burmese', 'my', 'mya', 'bur'),
|
||||||
|
('catalan', 'ca', 'cat', 'cat'),
|
||||||
|
('chamorro', 'ch', 'cha', 'cha'),
|
||||||
|
('chechen', 'ce', 'che', 'che'),
|
||||||
|
('chichewa', 'ny', 'nya', 'nya'),
|
||||||
|
('chinese', 'zh', 'zho', 'chi'),
|
||||||
|
('chuvash', 'cv', 'chv', 'chv'),
|
||||||
|
('cornish', 'kw', 'cor', 'cor'),
|
||||||
|
('corsican', 'co', 'cos', 'cos'),
|
||||||
|
('cree', 'cr', 'cre', 'cre'),
|
||||||
|
('croatian', 'hr', 'hrv', 'hrv'),
|
||||||
|
('czech', 'cs', 'ces', 'cze'),
|
||||||
|
('danish', 'da', 'dan', 'dan'),
|
||||||
|
('divehi', 'dv', 'div', 'div'),
|
||||||
|
('dutch', 'nl', 'nld', 'dut'),
|
||||||
|
('dzongkha', 'dz', 'dzo', 'dzo'),
|
||||||
|
('english', 'en', 'eng', 'eng'),
|
||||||
|
('esperanto', 'eo', 'epo', 'epo'),
|
||||||
|
('estonian', 'et', 'est', 'est'),
|
||||||
|
('ewe', 'ee', 'ewe', 'ewe'),
|
||||||
|
('faroese', 'fo', 'fao', 'fao'),
|
||||||
|
('fijian', 'fj', 'fij', 'fij'),
|
||||||
|
('finnish', 'fi', 'fin', 'fin'),
|
||||||
|
('french', 'fr', 'fra', 'fre'),
|
||||||
|
('fulah', 'ff', 'ful', 'ful'),
|
||||||
|
('galician', 'gl', 'glg', 'glg'),
|
||||||
|
('georgian', 'ka', 'kat', 'geo'),
|
||||||
|
('german', 'de', 'deu', 'ger'),
|
||||||
|
('greek', 'el', 'ell', 'gre'),
|
||||||
|
('guarani', 'gn', 'grn', 'grn'),
|
||||||
|
('gujarati', 'gu', 'guj', 'guj'),
|
||||||
|
('haitian', 'ht', 'hat', 'hat'),
|
||||||
|
('hausa', 'ha', 'hau', 'hau'),
|
||||||
|
('hebrew', 'he', 'heb', 'heb'),
|
||||||
|
('herero', 'hz', 'her', 'her'),
|
||||||
|
('hindi', 'hi', 'hin', 'hin'),
|
||||||
|
('hiri motu', 'ho', 'hmo', 'hmo'),
|
||||||
|
('hungarian', 'hu', 'hun', 'hun'),
|
||||||
|
('interlingua', 'ia', 'ina', 'ina'),
|
||||||
|
('indonesian', 'id', 'ind', 'ind'),
|
||||||
|
('interlingue', 'ie', 'ile', 'ile'),
|
||||||
|
('irish', 'ga', 'gle', 'gle'),
|
||||||
|
('igbo', 'ig', 'ibo', 'ibo'),
|
||||||
|
('inupiaq', 'ik', 'ipk', 'ipk'),
|
||||||
|
('ido', 'io', 'ido', 'ido'),
|
||||||
|
('icelandic', 'is', 'isl', 'ice'),
|
||||||
|
('italian', 'it', 'ita', 'ita'),
|
||||||
|
('inuktitut', 'iu', 'iku', 'iku'),
|
||||||
|
('japanese', 'ja', 'jpn', 'jpn'),
|
||||||
|
('javanese', 'jv', 'jav', 'jav'),
|
||||||
|
('kalaallisut', 'kl', 'kal', 'kal'),
|
||||||
|
('kannada', 'kn', 'kan', 'kan'),
|
||||||
|
('kanuri', 'kr', 'kau', 'kau'),
|
||||||
|
('kashmiri', 'ks', 'kas', 'kas'),
|
||||||
|
('kazakh', 'kk', 'kaz', 'kaz'),
|
||||||
|
('central khmer', 'km', 'khm', 'khm'),
|
||||||
|
('kikuyu', 'ki', 'kik', 'kik'),
|
||||||
|
('kinyarwanda', 'rw', 'kin', 'kin'),
|
||||||
|
('kirghiz', 'ky', 'kir', 'kir'),
|
||||||
|
('komi', 'kv', 'kom', 'kom'),
|
||||||
|
('kongo', 'kg', 'kon', 'kon'),
|
||||||
|
('korean', 'ko', 'kor', 'kor'),
|
||||||
|
('kurdish', 'ku', 'kur', 'kur'),
|
||||||
|
('kuanyama', 'kj', 'kua', 'kua'),
|
||||||
|
('latin', 'la', 'lat', 'lat'),
|
||||||
|
('luxembourgish', 'lb', 'ltz', 'ltz'),
|
||||||
|
('ganda', 'lg', 'lug', 'lug'),
|
||||||
|
('limburgan', 'li', 'lim', 'lim'),
|
||||||
|
('lingala', 'ln', 'lin', 'lin'),
|
||||||
|
('lao', 'lo', 'lao', 'lao'),
|
||||||
|
('lithuanian', 'lt', 'lit', 'lit'),
|
||||||
|
('luba-katanga', 'lu', 'lub', 'lub'),
|
||||||
|
('latvian', 'lv', 'lav', 'lav'),
|
||||||
|
('manx', 'gv', 'glv', 'glv'),
|
||||||
|
('macedonian', 'mk', 'mkd', 'mac'),
|
||||||
|
('malagasy', 'mg', 'mlg', 'mlg'),
|
||||||
|
('malay', 'ms', 'msa', 'may'),
|
||||||
|
('malayalam', 'ml', 'mal', 'mal'),
|
||||||
|
('maltese', 'mt', 'mlt', 'mlt'),
|
||||||
|
('maori', 'mi', 'mri', 'mao'),
|
||||||
|
('marathi', 'mr', 'mar', 'mar'),
|
||||||
|
('marshallese', 'mh', 'mah', 'mah'),
|
||||||
|
('mongolian', 'mn', 'mon', 'mon'),
|
||||||
|
('nauru', 'na', 'nau', 'nau'),
|
||||||
|
('navajo', 'nv', 'nav', 'nav'),
|
||||||
|
('north ndebele', 'nd', 'nde', 'nde'),
|
||||||
|
('nepali', 'ne', 'nep', 'nep'),
|
||||||
|
('ndonga', 'ng', 'ndo', 'ndo'),
|
||||||
|
('norwegian bokmål', 'nb', 'nob', 'nob'),
|
||||||
|
('norwegian nynorsk', 'nn', 'nno', 'nno'),
|
||||||
|
('norwegian', 'no', 'nor', 'nor'),
|
||||||
|
('sichuan yi', 'ii', 'iii', 'iii'),
|
||||||
|
('south ndebele', 'nr', 'nbl', 'nbl'),
|
||||||
|
('occitan', 'oc', 'oci', 'oci'),
|
||||||
|
('ojibwa', 'oj', 'oji', 'oji'),
|
||||||
|
('church slavic', 'cu', 'chu', 'chu'),
|
||||||
|
('oromo', 'om', 'orm', 'orm'),
|
||||||
|
('oriya', 'or', 'ori', 'ori'),
|
||||||
|
('ossetian', 'os', 'oss', 'oss'),
|
||||||
|
('punjabi', 'pa', 'pan', 'pan'),
|
||||||
|
('pali', 'pi', 'pli', 'pli'),
|
||||||
|
('persian', 'fa', 'fas', 'per'),
|
||||||
|
('polish', 'pl', 'pol', 'pol'),
|
||||||
|
('pashto', 'ps', 'pus', 'pus'),
|
||||||
|
('portuguese', 'pt', 'por', 'por'),
|
||||||
|
('quechua', 'qu', 'que', 'que'),
|
||||||
|
('romansh', 'rm', 'roh', 'roh'),
|
||||||
|
('rundi', 'rn', 'run', 'run'),
|
||||||
|
('romanian', 'ro', 'ron', 'rum'),
|
||||||
|
('russian', 'ru', 'rus', 'rus'),
|
||||||
|
('sanskrit', 'sa', 'san', 'san'),
|
||||||
|
('sardinian', 'sc', 'srd', 'srd'),
|
||||||
|
('sindhi', 'sd', 'snd', 'snd'),
|
||||||
|
('northern sami', 'se', 'sme', 'sme'),
|
||||||
|
('samoan', 'sm', 'smo', 'smo'),
|
||||||
|
('sango', 'sg', 'sag', 'sag'),
|
||||||
|
('serbian', 'sr', 'srp', 'srp'),
|
||||||
|
('gaelic', 'gd', 'gla', 'gla'),
|
||||||
|
('shona', 'sn', 'sna', 'sna'),
|
||||||
|
('sinhala', 'si', 'sin', 'sin'),
|
||||||
|
('slovak', 'sk', 'slk', 'slo'),
|
||||||
|
('slovenian', 'sl', 'slv', 'slv'),
|
||||||
|
('somali', 'so', 'som', 'som'),
|
||||||
|
('southern sotho', 'st', 'sot', 'sot'),
|
||||||
|
('spanish', 'es', 'spa', 'spa'),
|
||||||
|
('sundanese', 'su', 'sun', 'sun'),
|
||||||
|
('swahili', 'sw', 'swa', 'swa'),
|
||||||
|
('swati', 'ss', 'ssw', 'ssw'),
|
||||||
|
('swedish', 'sv', 'swe', 'swe'),
|
||||||
|
('tamil', 'ta', 'tam', 'tam'),
|
||||||
|
('telugu', 'te', 'tel', 'tel'),
|
||||||
|
('tajik', 'tg', 'tgk', 'tgk'),
|
||||||
|
('thai', 'th', 'tha', 'tha'),
|
||||||
|
('tigrinya', 'ti', 'tir', 'tir'),
|
||||||
|
('tibetan', 'bo', 'bod', 'tib'),
|
||||||
|
('turkmen', 'tk', 'tuk', 'tuk'),
|
||||||
|
('tagalog', 'tl', 'tgl', 'tgl'),
|
||||||
|
('tswana', 'tn', 'tsn', 'tsn'),
|
||||||
|
('tonga', 'to', 'ton', 'ton'),
|
||||||
|
('turkish', 'tr', 'tur', 'tur'),
|
||||||
|
('tsonga', 'ts', 'tso', 'tso'),
|
||||||
|
('tatar', 'tt', 'tat', 'tat'),
|
||||||
|
('twi', 'tw', 'twi', 'twi'),
|
||||||
|
('tahitian', 'ty', 'tah', 'tah'),
|
||||||
|
('uighur', 'ug', 'uig', 'uig'),
|
||||||
|
('ukrainian', 'uk', 'ukr', 'ukr'),
|
||||||
|
('urdu', 'ur', 'urd', 'urd'),
|
||||||
|
('uzbek', 'uz', 'uzb', 'uzb'),
|
||||||
|
('venda', 've', 'ven', 'ven'),
|
||||||
|
('vietnamese', 'vi', 'vie', 'vie'),
|
||||||
|
('volapük', 'vo', 'vol', 'vol'),
|
||||||
|
('walloon', 'wa', 'wln', 'wln'),
|
||||||
|
('welsh', 'cy', 'cym', 'wel'),
|
||||||
|
('wolof', 'wo', 'wol', 'wol'),
|
||||||
|
('western frisian', 'fy', 'fry', 'fry'),
|
||||||
|
('xhosa', 'xh', 'xho', 'xho'),
|
||||||
|
('yiddish', 'yi', 'yid', 'yid'),
|
||||||
|
('yoruba', 'yo', 'yor', 'yor'),
|
||||||
|
('zhuang', 'za', 'zha', 'zha'),
|
||||||
|
('zulu', 'zu', 'zul', 'zul'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def accessible_plex_subtitles(playmethod, playing_file, xml_streams):
|
||||||
|
if not playmethod == v.PLAYBACK_METHOD_DIRECT_PATH:
|
||||||
|
# We can access all subtitles because we're downloading additional
|
||||||
|
# external ones into the Kodi PKC add-on directory
|
||||||
|
streams = []
|
||||||
|
# Kodi ennumerates EXTERNAL subtitles first, then internal ones
|
||||||
|
for stream in xml_streams:
|
||||||
|
if stream.get('streamType') == '3' and 'key' in stream.attrib:
|
||||||
|
streams.append(stream)
|
||||||
|
for stream in xml_streams:
|
||||||
|
if stream.get('streamType') == '3' and 'key' not in stream.attrib:
|
||||||
|
streams.append(stream)
|
||||||
|
if streams:
|
||||||
|
LOG.debug('Working with the following Plex subtitle streams:')
|
||||||
|
log_plex_streams(streams)
|
||||||
|
return streams
|
||||||
|
kodi_subs = kodi_subs_from_player()
|
||||||
|
plex_streams_int, plex_streams_ext = accessible_plex_sub_streams(xml_streams)
|
||||||
|
# Kodi appends internal streams at the end of its list
|
||||||
|
kodi_subs_ext = kodi_subs[:len(kodi_subs) - len(plex_streams_int)]
|
||||||
|
LOG.debug('Kodi list of external subs: %s', kodi_subs_ext)
|
||||||
|
LOG.debug('Kodi has %s external subs, Plex %s, trying to match them',
|
||||||
|
len(kodi_subs_ext), len(plex_streams_ext))
|
||||||
|
dirname, basename = path.split(playing_file)
|
||||||
|
filename, _ = path.splitext(basename)
|
||||||
|
try:
|
||||||
|
kodi_subs_file = kodi_external_subs(dirname, filename, kodi_subs_ext)
|
||||||
|
reordered_plex_streams_ext = reorder_plex_streams(plex_streams_ext,
|
||||||
|
kodi_subs_file)
|
||||||
|
except SubtitleError:
|
||||||
|
# Add dummy subtitles so we won't match against Plex subtitles that
|
||||||
|
# are in an incorrect order - keeps Kodi order of subs intact
|
||||||
|
reordered_plex_streams_ext = [DummySub()
|
||||||
|
for _ in range(len(kodi_subs_ext))]
|
||||||
|
reordered_plex_streams_ext.extend(plex_streams_int)
|
||||||
|
return reordered_plex_streams_ext
|
||||||
|
|
||||||
|
|
||||||
|
def reorder_plex_streams(plex_streams_ext, kodi_subs_file):
|
||||||
|
"""
|
||||||
|
Returns the Plex streams in a "best-guess" order as indicated by the
|
||||||
|
Kodi external subtitles kodi_subs_file
|
||||||
|
"""
|
||||||
|
order = [None for i in range(len(kodi_subs_file))]
|
||||||
|
# Pick subtitles with known language, extension and "forced" True first
|
||||||
|
for i, kodi_sub in enumerate(kodi_subs_file):
|
||||||
|
if not kodi_sub['iso'] or not kodi_sub['forced']:
|
||||||
|
continue
|
||||||
|
for plex_stream in plex_streams_ext:
|
||||||
|
if not plex_stream.get('forced'):
|
||||||
|
continue
|
||||||
|
elif not kodi_sub['iso'][1] == plex_stream.get('languageTag'):
|
||||||
|
continue
|
||||||
|
elif not kodi_sub['codec'] == plex_stream.get('codec').lower():
|
||||||
|
continue
|
||||||
|
# Pick the first matching result - even though it's a best guess
|
||||||
|
order[i] = plex_stream
|
||||||
|
plex_streams_ext.remove(plex_stream)
|
||||||
|
break
|
||||||
|
# Pick non-forced
|
||||||
|
for i, kodi_sub in enumerate(kodi_subs_file):
|
||||||
|
if order[i] is not None or not kodi_sub['iso']:
|
||||||
|
continue
|
||||||
|
for plex_stream in plex_streams_ext:
|
||||||
|
if not kodi_sub['iso'][1] == plex_stream.get('languageTag'):
|
||||||
|
continue
|
||||||
|
elif not kodi_sub['codec'] == plex_stream.get('codec').lower():
|
||||||
|
continue
|
||||||
|
elif not (kodi_sub['forced'] is (plex_stream.get('forced') == '1')):
|
||||||
|
continue
|
||||||
|
# Pick the first matching result - even though it's a best guess
|
||||||
|
order[i] = plex_stream
|
||||||
|
plex_streams_ext.remove(plex_stream)
|
||||||
|
break
|
||||||
|
# Pick subs irrelevant of forced flag
|
||||||
|
for i, kodi_sub in enumerate(kodi_subs_file):
|
||||||
|
if order[i] is not None or not kodi_sub['iso']:
|
||||||
|
continue
|
||||||
|
for plex_stream in plex_streams_ext:
|
||||||
|
if not kodi_sub['iso'][1] == plex_stream.get('languageTag'):
|
||||||
|
continue
|
||||||
|
elif not kodi_sub['codec'] == plex_stream.get('codec').lower():
|
||||||
|
continue
|
||||||
|
# Pick the first matching result - even though it's a best guess
|
||||||
|
order[i] = plex_stream
|
||||||
|
plex_streams_ext.remove(plex_stream)
|
||||||
|
break
|
||||||
|
# Pick subs based on codec (Plex does not detect "English" as en). Forced
|
||||||
|
# ones first
|
||||||
|
for i, kodi_sub in enumerate(kodi_subs_file):
|
||||||
|
if order[i] is not None or not kodi_sub['forced']:
|
||||||
|
continue
|
||||||
|
for plex_stream in plex_streams_ext:
|
||||||
|
if not kodi_sub['codec'] == plex_stream.get('codec').lower():
|
||||||
|
continue
|
||||||
|
elif not plex_stream.get('forced'):
|
||||||
|
continue
|
||||||
|
elif plex_stream.get('languageTag') and kodi_sub['iso'] \
|
||||||
|
and not plex_stream.get('languageTag') == kodi_sub['iso'][1]:
|
||||||
|
continue
|
||||||
|
# Pick the first matching result - even though it's a best guess
|
||||||
|
order[i] = plex_stream
|
||||||
|
plex_streams_ext.remove(plex_stream)
|
||||||
|
break
|
||||||
|
# Pick subs based on codec alone (Plex does not detect "English" as en).
|
||||||
|
# Non-forced
|
||||||
|
for i, kodi_sub in enumerate(kodi_subs_file):
|
||||||
|
if order[i] is not None:
|
||||||
|
continue
|
||||||
|
for plex_stream in plex_streams_ext:
|
||||||
|
if not kodi_sub['codec'] == plex_stream.get('codec').lower():
|
||||||
|
continue
|
||||||
|
elif not (kodi_sub['forced'] is (plex_stream.get('forced') == '1')):
|
||||||
|
continue
|
||||||
|
elif plex_stream.get('languageTag') and kodi_sub['iso'] \
|
||||||
|
and not plex_stream.get('languageTag') == kodi_sub['iso'][1]:
|
||||||
|
continue
|
||||||
|
# Pick the first matching result - even though it's a best guess
|
||||||
|
order[i] = plex_stream
|
||||||
|
plex_streams_ext.remove(plex_stream)
|
||||||
|
break
|
||||||
|
# Pick subs based on codec alone (Plex does not detect "English" as en).
|
||||||
|
# Even with miss-matching forced flag
|
||||||
|
for i, kodi_sub in enumerate(kodi_subs_file):
|
||||||
|
if order[i] is not None:
|
||||||
|
continue
|
||||||
|
for plex_stream in plex_streams_ext:
|
||||||
|
if not kodi_sub['codec'] == plex_stream.get('codec').lower():
|
||||||
|
continue
|
||||||
|
elif plex_stream.get('languageTag') and kodi_sub['iso'] \
|
||||||
|
and not plex_stream.get('languageTag') == kodi_sub['iso'][1]:
|
||||||
|
continue
|
||||||
|
# Pick the first matching result - even though it's a best guess
|
||||||
|
order[i] = plex_stream
|
||||||
|
plex_streams_ext.remove(plex_stream)
|
||||||
|
break
|
||||||
|
# Now lets add dummies for Kodi subs we could not match
|
||||||
|
for i, kodi_sub in enumerate(kodi_subs_file):
|
||||||
|
if order[i] is not None:
|
||||||
|
continue
|
||||||
|
LOG.debug('Could not match Kodi sub number %s %s, adding a dummy',
|
||||||
|
i, kodi_sub)
|
||||||
|
order[i] = DummySub()
|
||||||
|
if plex_streams_ext:
|
||||||
|
LOG.debug('We could not match the following Plex subtitles:')
|
||||||
|
log_plex_streams(plex_streams_ext)
|
||||||
|
if order:
|
||||||
|
LOG.debug('Derived order of external subtitle streams:')
|
||||||
|
log_plex_streams(order)
|
||||||
|
return order
|
||||||
|
|
||||||
|
|
||||||
|
def log_plex_streams(plex_streams):
|
||||||
|
for i, stream in enumerate(plex_streams):
|
||||||
|
LOG.debug('Number %s: %s: %s', i, stream.tag, stream.attrib)
|
||||||
|
|
||||||
|
|
||||||
|
def accessible_plex_sub_streams(xml):
|
||||||
|
# Any additionally downloaded subtitles are not accessible for Kodi
|
||||||
|
# We're identifying them by the additional key 'providerTitle'
|
||||||
|
plex_streams = [stream for stream in xml
|
||||||
|
if stream.get('streamType') == '3'
|
||||||
|
and not stream.get('providerTitle')]
|
||||||
|
LOG.debug('Available Plex subtitle streams for currently playing item:')
|
||||||
|
log_plex_streams(plex_streams)
|
||||||
|
# Kodi can display internal subtitle streams for sure
|
||||||
|
plex_streams_int = [x for x in plex_streams if 'key' not in x.attrib]
|
||||||
|
# We need to check external ones
|
||||||
|
# If the movie name is 'The Dark Knight (2008).mkv', Kodi finds
|
||||||
|
# subtitles 'The Dark Knight (2008)*.*.<ext>'
|
||||||
|
plex_streams_ext = [x for x in plex_streams if 'key' in x.attrib]
|
||||||
|
return plex_streams_int, plex_streams_ext
|
||||||
|
|
||||||
|
|
||||||
|
def kodi_subs_from_player():
|
||||||
|
"""
|
||||||
|
Kodi can only play subtitles that it pickes up itself: They lie in the
|
||||||
|
same folder as the video file and are named similarly
|
||||||
|
"""
|
||||||
|
kodi_subs = app.APP.player.getAvailableSubtitleStreams()
|
||||||
|
LOG.debug('Kodi list of available subtitles: %s', kodi_subs)
|
||||||
|
return kodi_subs
|
||||||
|
|
||||||
|
|
||||||
|
def kodi_external_subs(dirname, filename, kodi_subs_ext):
|
||||||
|
file_subs = external_subs_from_filesystem(dirname, filename)
|
||||||
|
if len(file_subs) != len(kodi_subs_ext):
|
||||||
|
LOG.warn('Unexpected missmatch of number of Kodi subtitles')
|
||||||
|
LOG.warn('Kodi subs: %s', kodi_subs_ext)
|
||||||
|
LOG.warn('Subs from the filesystem: %s', file_subs)
|
||||||
|
raise SubtitleError()
|
||||||
|
for i, sub in enumerate(file_subs):
|
||||||
|
if sub['iso'] and kodi_subs_ext[i].lower() not in sub['iso']:
|
||||||
|
LOG.warn('Unexpected Kodi external subtitle language combo')
|
||||||
|
LOG.warn('Kodi subs: %s', kodi_subs_ext)
|
||||||
|
LOG.warn('Subs from the filesystem: %s', file_subs)
|
||||||
|
raise SubtitleError()
|
||||||
|
return file_subs
|
||||||
|
|
||||||
|
|
||||||
|
def external_subs_from_filesystem(dirname, filename):
|
||||||
|
"""
|
||||||
|
Returns a list of dicts of subtitles lying within the directory dirname:
|
||||||
|
{'iso': tuple of detected ISO language (see LANGUAGE_ISO_CODES) or None,
|
||||||
|
'language': language string that Kodi might show in its GUI,
|
||||||
|
'forced': has '[. -]forced' been appended to the filename?
|
||||||
|
'file': subtitle file name}
|
||||||
|
Supply with the currently playing filename as Kodi uses that to search
|
||||||
|
for subtitles. See https://kodi.wiki/view/Subtitles
|
||||||
|
"""
|
||||||
|
file_subs = []
|
||||||
|
for root, dirs, files in path_ops.walk(dirname):
|
||||||
|
for file in files:
|
||||||
|
name, extension = path.splitext(file)
|
||||||
|
# Get rid of the dot and force lowercase
|
||||||
|
extension = extension[1:].lower()
|
||||||
|
if extension not in KODI_SUBTITLE_EXTENSIONS:
|
||||||
|
# Not an extension Kodi supports
|
||||||
|
continue
|
||||||
|
elif not name.startswith(filename):
|
||||||
|
# Naming not up to standards, Kodi won't pick up this file
|
||||||
|
# (but Plex might!!)
|
||||||
|
continue
|
||||||
|
regex = SUBTITLE_LANGUAGE.search(name.replace(filename, '', 1))
|
||||||
|
language = (regex.group(1) if regex.group(1) else '').lower()
|
||||||
|
forced = True if regex.group(2) else False
|
||||||
|
iso = None
|
||||||
|
if len(language) == 2:
|
||||||
|
language_searchgrid = (1, )
|
||||||
|
elif len(language) == 3:
|
||||||
|
language_searchgrid = (2, 3)
|
||||||
|
else:
|
||||||
|
language_searchgrid = (0, )
|
||||||
|
for lang in LANGUAGE_ISO_CODES:
|
||||||
|
for i in language_searchgrid:
|
||||||
|
if lang[i] == language:
|
||||||
|
iso = lang
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
file_subs.append({'iso': iso,
|
||||||
|
'language': language,
|
||||||
|
'forced': forced,
|
||||||
|
'codec': extension,
|
||||||
|
'file': '%s.%s' % (name, extension)})
|
||||||
|
LOG.debug('Detected these external subtitles while scanning the file '
|
||||||
|
'system: %s', file_subs)
|
||||||
|
return file_subs
|
||||||
|
|
||||||
|
|
||||||
|
class DummySub(etree.Element):
|
||||||
|
def __init__(self):
|
||||||
|
super(DummySub, self).__init__('Stream-subtitle-dummy')
|
|
@ -227,7 +227,7 @@ class Sync(backgroundthread.KillableThread):
|
||||||
not app.APP.is_playing_video):
|
not app.APP.is_playing_video):
|
||||||
LOG.info('Doing scheduled full library scan')
|
LOG.info('Doing scheduled full library scan')
|
||||||
self.start_library_sync()
|
self.start_library_sync()
|
||||||
elif not app.SYNC.background_sync_disabled:
|
else:
|
||||||
# Check back whether we should process something Only do
|
# Check back whether we should process something Only do
|
||||||
# this once a while (otherwise, potentially many screen
|
# this once a while (otherwise, potentially many screen
|
||||||
# refreshes lead to flickering)
|
# refreshes lead to flickering)
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
from logging import getLogger
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from time import localtime, strftime
|
from time import localtime, strftime
|
||||||
|
|
||||||
|
LOG = getLogger('PLEX.timing')
|
||||||
|
|
||||||
EPOCH = datetime.utcfromtimestamp(0)
|
EPOCH = datetime.utcfromtimestamp(0)
|
||||||
|
|
||||||
|
|
||||||
|
@ -29,7 +32,13 @@ def unix_date_to_kodi(unix_kodi_time):
|
||||||
|
|
||||||
Output: Y-m-d h:m:s = 2009-04-05 23:16:04
|
Output: Y-m-d h:m:s = 2009-04-05 23:16:04
|
||||||
"""
|
"""
|
||||||
return strftime('%Y-%m-%d %H:%M:%S', localtime(float(unix_kodi_time)))
|
try:
|
||||||
|
return strftime('%Y-%m-%d %H:%M:%S', localtime(float(unix_kodi_time)))
|
||||||
|
except:
|
||||||
|
LOG.error('Received an illegal timestamp from Plex: %s. '
|
||||||
|
'Using 1970-01-01 12:00:00',
|
||||||
|
unix_kodi_time)
|
||||||
|
return '1970-01-01 12:00:00'
|
||||||
|
|
||||||
|
|
||||||
def plex_date_to_kodi(plex_timestamp):
|
def plex_date_to_kodi(plex_timestamp):
|
||||||
|
|
|
@ -48,8 +48,9 @@ REGEX_END_DIGITS = re.compile(r'''/(.+)/(\d+)$''')
|
||||||
REGEX_PLEX_DIRECT = re.compile(r'''\.plex\.direct:\d+$''')
|
REGEX_PLEX_DIRECT = re.compile(r'''\.plex\.direct:\d+$''')
|
||||||
# Plex API
|
# Plex API
|
||||||
REGEX_IMDB = re.compile(r'''/(tt\d+)''')
|
REGEX_IMDB = re.compile(r'''/(tt\d+)''')
|
||||||
REGEX_TVDB = re.compile(r'''thetvdb:\/\/(.+?)\?''')
|
REGEX_TVDB = re.compile(r'''(?:the)?tvdb(?::\/\/|[2-5]?-)(\d+?)\?''')
|
||||||
REGEX_TMDB = re.compile(r'''themoviedb:\/\/(.+?)\?''')
|
REGEX_TMDB = re.compile(r'''themoviedb:\/\/(.+?)\?''')
|
||||||
|
REGEX_ANIDB = re.compile(r'''anidb[2-4]?-(\d+?)\?''')
|
||||||
# Plex music
|
# Plex music
|
||||||
REGEX_MUSICPATH = re.compile(r'''^\^(.+)\$$''')
|
REGEX_MUSICPATH = re.compile(r'''^\^(.+)\$$''')
|
||||||
# Grab Plex id from an URL-encoded string
|
# Grab Plex id from an URL-encoded string
|
||||||
|
@ -118,10 +119,10 @@ def settings(setting, value=None):
|
||||||
"""
|
"""
|
||||||
# We need to instantiate every single time to read changed variables!
|
# We need to instantiate every single time to read changed variables!
|
||||||
with SETTINGS_LOCK:
|
with SETTINGS_LOCK:
|
||||||
addon = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
|
addon = xbmcaddon.Addon()
|
||||||
if value is not None:
|
if value is not None:
|
||||||
# Takes string or unicode by default!
|
# Takes string or unicode by default!
|
||||||
addon.setSetting(try_encode(setting), try_encode(value))
|
addon.setSetting(try_encode(setting), try_encode(value))
|
||||||
else:
|
else:
|
||||||
# Should return unicode by default, but just in case
|
# Should return unicode by default, but just in case
|
||||||
return try_decode(addon.getSetting(setting))
|
return try_decode(addon.getSetting(setting))
|
||||||
|
|
|
@ -45,6 +45,10 @@ ADDON_PATH = try_decode(_ADDON.getAddonInfo('path'))
|
||||||
ADDON_FOLDER = try_decode(xbmc.translatePath('special://home'))
|
ADDON_FOLDER = try_decode(xbmc.translatePath('special://home'))
|
||||||
ADDON_PROFILE = try_decode(xbmc.translatePath(_ADDON.getAddonInfo('profile')))
|
ADDON_PROFILE = try_decode(xbmc.translatePath(_ADDON.getAddonInfo('profile')))
|
||||||
|
|
||||||
|
# Used e.g. for json_rpc
|
||||||
|
KODI_VIDEO_PLAYER_ID = 1
|
||||||
|
KODI_AUDIO_PLAYER_ID = 0
|
||||||
|
|
||||||
KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1)
|
KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1)
|
||||||
KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
|
KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
|
||||||
KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion')
|
KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion')
|
||||||
|
@ -189,9 +193,15 @@ PLEX_TYPE_PHOTO = 'photo'
|
||||||
PLEX_TYPE_PLAYLIST = 'playlist'
|
PLEX_TYPE_PLAYLIST = 'playlist'
|
||||||
PLEX_TYPE_CHANNEL = 'channel'
|
PLEX_TYPE_CHANNEL = 'channel'
|
||||||
|
|
||||||
|
PLEX_TYPE_GAME = 'game'
|
||||||
|
|
||||||
# E.g. PMS answer when hitting the PMS endpoint /hubs/search
|
# E.g. PMS answer when hitting the PMS endpoint /hubs/search
|
||||||
PLEX_TYPE_TAG = 'tag'
|
PLEX_TYPE_TAG = 'tag'
|
||||||
|
|
||||||
|
# PlexKodiConnect does not support all (content) types
|
||||||
|
# e.g. Plex Arcade games
|
||||||
|
UNSUPPORTED_PLEX_TYPES = (PLEX_TYPE_GAME, )
|
||||||
|
|
||||||
# Used for /:/timeline XML messages
|
# Used for /:/timeline XML messages
|
||||||
PLEX_PLAYLIST_TYPE_VIDEO = 'video'
|
PLEX_PLAYLIST_TYPE_VIDEO = 'video'
|
||||||
PLEX_PLAYLIST_TYPE_AUDIO = 'music'
|
PLEX_PLAYLIST_TYPE_AUDIO = 'music'
|
||||||
|
@ -536,7 +546,8 @@ ALL_KODI_ARTWORK = (
|
||||||
'clearart',
|
'clearart',
|
||||||
'clearlogo',
|
'clearlogo',
|
||||||
'fanart',
|
'fanart',
|
||||||
'discart'
|
'discart',
|
||||||
|
'landscape'
|
||||||
)
|
)
|
||||||
|
|
||||||
# we need to use a little mapping between fanart.tv arttypes and kodi artttypes
|
# we need to use a little mapping between fanart.tv arttypes and kodi artttypes
|
||||||
|
@ -550,7 +561,8 @@ FANART_TV_TO_KODI_TYPE = [
|
||||||
('clearlogo', 'clearlogo'),
|
('clearlogo', 'clearlogo'),
|
||||||
('background', 'fanart'),
|
('background', 'fanart'),
|
||||||
('showbackground', 'fanart'),
|
('showbackground', 'fanart'),
|
||||||
('characterart', 'characterart')
|
('characterart', 'characterart'),
|
||||||
|
('thumb', 'landscape')
|
||||||
]
|
]
|
||||||
# How many different backgrounds do we want to load from fanart.tv?
|
# How many different backgrounds do we want to load from fanart.tv?
|
||||||
MAX_BACKGROUND_COUNT = 10
|
MAX_BACKGROUND_COUNT = 10
|
||||||
|
|
|
@ -1,917 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
websocket - WebSocket client library for Python
|
|
||||||
|
|
||||||
Copyright (C) 2010 Hiroki Ohtani(liris)
|
|
||||||
|
|
||||||
This library is free software; you can redistribute it and/or
|
|
||||||
modify it under the terms of the GNU Lesser General Public
|
|
||||||
License as published by the Free Software Foundation; either
|
|
||||||
version 2.1 of the License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This library is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
Lesser General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Lesser General Public
|
|
||||||
License along with this library; if not, write to the Free Software
|
|
||||||
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
||||||
|
|
||||||
"""
|
|
||||||
import socket
|
|
||||||
|
|
||||||
try:
|
|
||||||
import ssl
|
|
||||||
from ssl import SSLError
|
|
||||||
HAVE_SSL = True
|
|
||||||
except ImportError:
|
|
||||||
class SSLError(Exception):
|
|
||||||
"""
|
|
||||||
Dummy class of SSLError for ssl none-support environment.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
HAVE_SSL = False
|
|
||||||
|
|
||||||
from urlparse import urlparse
|
|
||||||
import os
|
|
||||||
import array
|
|
||||||
import struct
|
|
||||||
import uuid
|
|
||||||
import hashlib
|
|
||||||
import base64
|
|
||||||
import threading
|
|
||||||
import logging
|
|
||||||
import traceback
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from . import utils, app
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
LOG = logging.getLogger('PLEX.websocket')
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
"""
|
|
||||||
websocket python client.
|
|
||||||
=========================
|
|
||||||
|
|
||||||
This version support only hybi-13.
|
|
||||||
Please see http://tools.ietf.org/html/rfc6455 for protocol.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
# websocket supported version.
|
|
||||||
VERSION = 13
|
|
||||||
|
|
||||||
# closing frame status codes.
|
|
||||||
STATUS_NORMAL = 1000
|
|
||||||
STATUS_GOING_AWAY = 1001
|
|
||||||
STATUS_PROTOCOL_ERROR = 1002
|
|
||||||
STATUS_UNSUPPORTED_DATA_TYPE = 1003
|
|
||||||
STATUS_STATUS_NOT_AVAILABLE = 1005
|
|
||||||
STATUS_ABNORMAL_CLOSED = 1006
|
|
||||||
STATUS_INVALID_PAYLOAD = 1007
|
|
||||||
STATUS_POLICY_VIOLATION = 1008
|
|
||||||
STATUS_MESSAGE_TOO_BIG = 1009
|
|
||||||
STATUS_INVALID_EXTENSION = 1010
|
|
||||||
STATUS_UNEXPECTED_CONDITION = 1011
|
|
||||||
STATUS_TLS_HANDSHAKE_ERROR = 1015
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketException(Exception):
|
|
||||||
"""
|
|
||||||
websocket exeception class.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketConnectionClosedException(WebSocketException):
|
|
||||||
"""
|
|
||||||
If remote host closed the connection or some network error happened,
|
|
||||||
this exception will be raised.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketTimeoutException(WebSocketException):
|
|
||||||
"""
|
|
||||||
WebSocketTimeoutException will be raised at socket timeout during read and
|
|
||||||
write data.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class WebsocketRedirect(WebSocketException):
|
|
||||||
"""
|
|
||||||
WebsocketRedirect will be raised if a status code 301 is returned
|
|
||||||
The Exception will be instantiated with a dict containing all response
|
|
||||||
headers; which should contain the redirect address under the key 'location'
|
|
||||||
|
|
||||||
Access the headers via the attribute headers
|
|
||||||
"""
|
|
||||||
def __init__(self, headers):
|
|
||||||
self.headers = headers
|
|
||||||
super(WebsocketRedirect, self).__init__()
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_TIMEOUT = None
|
|
||||||
TRACE_ENABLED = False
|
|
||||||
|
|
||||||
|
|
||||||
def enable_trace(tracable):
|
|
||||||
"""
|
|
||||||
turn on/off the tracability.
|
|
||||||
|
|
||||||
tracable: boolean value. if set True, tracability is enabled.
|
|
||||||
"""
|
|
||||||
global TRACE_ENABLED
|
|
||||||
TRACE_ENABLED = tracable
|
|
||||||
if tracable:
|
|
||||||
if not LOG.handlers:
|
|
||||||
LOG.addHandler(logging.StreamHandler())
|
|
||||||
LOG.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
def setdefaulttimeout(timeout):
|
|
||||||
"""
|
|
||||||
Set the global timeout setting to connect.
|
|
||||||
|
|
||||||
timeout: default socket timeout time. This value is second.
|
|
||||||
"""
|
|
||||||
global DEFAULT_TIMEOUT
|
|
||||||
DEFAULT_TIMEOUT = timeout
|
|
||||||
|
|
||||||
|
|
||||||
def getdefaulttimeout():
|
|
||||||
"""
|
|
||||||
Return the global timeout setting(second) to connect.
|
|
||||||
"""
|
|
||||||
return DEFAULT_TIMEOUT
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_url(url):
|
|
||||||
"""
|
|
||||||
parse url and the result is tuple of
|
|
||||||
(hostname, port, resource path and the flag of secure mode)
|
|
||||||
|
|
||||||
url: url string.
|
|
||||||
"""
|
|
||||||
if ":" not in url:
|
|
||||||
raise ValueError("url is invalid")
|
|
||||||
|
|
||||||
scheme, url = url.split(":", 1)
|
|
||||||
|
|
||||||
parsed = urlparse(url, scheme="http")
|
|
||||||
if parsed.hostname:
|
|
||||||
hostname = parsed.hostname
|
|
||||||
else:
|
|
||||||
raise ValueError("hostname is invalid")
|
|
||||||
port = 0
|
|
||||||
if parsed.port:
|
|
||||||
port = parsed.port
|
|
||||||
|
|
||||||
is_secure = False
|
|
||||||
if scheme == "ws" or scheme == 'http':
|
|
||||||
if not port:
|
|
||||||
port = 80
|
|
||||||
elif scheme == "wss" or scheme == 'https':
|
|
||||||
is_secure = True
|
|
||||||
if not port:
|
|
||||||
port = 443
|
|
||||||
else:
|
|
||||||
raise ValueError("scheme %s is invalid" % scheme)
|
|
||||||
|
|
||||||
if parsed.path:
|
|
||||||
resource = parsed.path
|
|
||||||
else:
|
|
||||||
resource = "/"
|
|
||||||
|
|
||||||
if parsed.query:
|
|
||||||
resource += "?" + parsed.query
|
|
||||||
|
|
||||||
return (hostname, port, resource, is_secure)
|
|
||||||
|
|
||||||
|
|
||||||
def create_connection(url, timeout=None, **options):
|
|
||||||
"""
|
|
||||||
connect to url and return websocket object.
|
|
||||||
|
|
||||||
Connect to url and return the WebSocket object.
|
|
||||||
Passing optional timeout parameter will set the timeout on the socket.
|
|
||||||
If no timeout is supplied, the global default timeout setting returned by
|
|
||||||
getdefauttimeout() is used.
|
|
||||||
You can customize using 'options'.
|
|
||||||
If you set "header" list object, you can set your own custom header.
|
|
||||||
|
|
||||||
>>> conn = create_connection("ws://echo.websocket.org/",
|
|
||||||
... header=["User-Agent: MyProgram",
|
|
||||||
... "x-custom: header"])
|
|
||||||
|
|
||||||
|
|
||||||
timeout: socket timeout time. This value is integer.
|
|
||||||
if you set None for this value, it means "use DEFAULT_TIMEOUT
|
|
||||||
value"
|
|
||||||
|
|
||||||
options: current support option is only "header".
|
|
||||||
if you set header as dict value, the custom HTTP headers are added
|
|
||||||
"""
|
|
||||||
sockopt = options.get("sockopt", [])
|
|
||||||
sslopt = options.get("sslopt", {})
|
|
||||||
websock = WebSocket(sockopt=sockopt, sslopt=sslopt)
|
|
||||||
websock.settimeout(timeout if timeout is not None else DEFAULT_TIMEOUT)
|
|
||||||
websock.connect(url, **options)
|
|
||||||
return websock
|
|
||||||
|
|
||||||
|
|
||||||
_MAX_INTEGER = (1 << 32) - 1
|
|
||||||
_AVAILABLE_KEY_CHARS = range(0x21, 0x2f + 1) + range(0x3a, 0x7e + 1)
|
|
||||||
_MAX_CHAR_BYTE = (1 << 8) - 1
|
|
||||||
|
|
||||||
# ref. Websocket gets an update, and it breaks stuff.
|
|
||||||
# http://axod.blogspot.com/2010/06/websocket-gets-update-and-it-breaks.html
|
|
||||||
|
|
||||||
|
|
||||||
def _create_sec_websocket_key():
|
|
||||||
uid = uuid.uuid4()
|
|
||||||
return base64.encodestring(uid.bytes).strip()
|
|
||||||
|
|
||||||
|
|
||||||
_HEADERS_TO_CHECK = {"upgrade": "websocket", "connection": "upgrade"}
|
|
||||||
|
|
||||||
|
|
||||||
class ABNF(object):
|
|
||||||
"""
|
|
||||||
ABNF frame class.
|
|
||||||
see http://tools.ietf.org/html/rfc5234
|
|
||||||
and http://tools.ietf.org/html/rfc6455#section-5.2
|
|
||||||
"""
|
|
||||||
|
|
||||||
# operation code values.
|
|
||||||
OPCODE_CONT = 0x0
|
|
||||||
OPCODE_TEXT = 0x1
|
|
||||||
OPCODE_BINARY = 0x2
|
|
||||||
OPCODE_CLOSE = 0x8
|
|
||||||
OPCODE_PING = 0x9
|
|
||||||
OPCODE_PONG = 0xa
|
|
||||||
|
|
||||||
# available operation code value tuple
|
|
||||||
OPCODES = (OPCODE_CONT, OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE,
|
|
||||||
OPCODE_PING, OPCODE_PONG)
|
|
||||||
|
|
||||||
# opcode human readable string
|
|
||||||
OPCODE_MAP = {
|
|
||||||
OPCODE_CONT: "cont",
|
|
||||||
OPCODE_TEXT: "text",
|
|
||||||
OPCODE_BINARY: "binary",
|
|
||||||
OPCODE_CLOSE: "close",
|
|
||||||
OPCODE_PING: "ping",
|
|
||||||
OPCODE_PONG: "pong"
|
|
||||||
}
|
|
||||||
|
|
||||||
# data length threashold.
|
|
||||||
LENGTH_7 = 0x7d
|
|
||||||
LENGTH_16 = 1 << 16
|
|
||||||
LENGTH_63 = 1 << 63
|
|
||||||
|
|
||||||
def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0,
|
|
||||||
opcode=OPCODE_TEXT, mask=1, data=""):
|
|
||||||
"""
|
|
||||||
Constructor for ABNF.
|
|
||||||
please check RFC for arguments.
|
|
||||||
"""
|
|
||||||
self.fin = fin
|
|
||||||
self.rsv1 = rsv1
|
|
||||||
self.rsv2 = rsv2
|
|
||||||
self.rsv3 = rsv3
|
|
||||||
self.opcode = opcode
|
|
||||||
self.mask = mask
|
|
||||||
self.data = data
|
|
||||||
self.get_mask_key = os.urandom
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "fin=" + str(self.fin) \
|
|
||||||
+ " opcode=" + str(self.opcode) \
|
|
||||||
+ " data=" + str(self.data)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def create_frame(data, opcode):
|
|
||||||
"""
|
|
||||||
create frame to send text, binary and other data.
|
|
||||||
|
|
||||||
data: data to send. This is string value(byte array).
|
|
||||||
if opcode is OPCODE_TEXT and this value is uniocde,
|
|
||||||
data value is conveted into unicode string, automatically.
|
|
||||||
|
|
||||||
opcode: operation code. please see OPCODE_XXX.
|
|
||||||
"""
|
|
||||||
if opcode == ABNF.OPCODE_TEXT and isinstance(data, unicode):
|
|
||||||
data = utils.try_encode(data)
|
|
||||||
# mask must be set if send data from client
|
|
||||||
return ABNF(1, 0, 0, 0, opcode, 1, data)
|
|
||||||
|
|
||||||
def format(self):
|
|
||||||
"""
|
|
||||||
format this object to string(byte array) to send data to server.
|
|
||||||
"""
|
|
||||||
if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]):
|
|
||||||
raise ValueError("not 0 or 1")
|
|
||||||
if self.opcode not in ABNF.OPCODES:
|
|
||||||
raise ValueError("Invalid OPCODE")
|
|
||||||
length = len(self.data)
|
|
||||||
if length >= ABNF.LENGTH_63:
|
|
||||||
raise ValueError("data is too long")
|
|
||||||
|
|
||||||
frame_header = chr(self.fin << 7 |
|
|
||||||
self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4 |
|
|
||||||
self.opcode)
|
|
||||||
if length < ABNF.LENGTH_7:
|
|
||||||
frame_header += chr(self.mask << 7 | length)
|
|
||||||
elif length < ABNF.LENGTH_16:
|
|
||||||
frame_header += chr(self.mask << 7 | 0x7e)
|
|
||||||
frame_header += struct.pack("!H", length)
|
|
||||||
else:
|
|
||||||
frame_header += chr(self.mask << 7 | 0x7f)
|
|
||||||
frame_header += struct.pack("!Q", length)
|
|
||||||
|
|
||||||
if not self.mask:
|
|
||||||
return frame_header + self.data
|
|
||||||
else:
|
|
||||||
mask_key = self.get_mask_key(4)
|
|
||||||
return frame_header + self._get_masked(mask_key)
|
|
||||||
|
|
||||||
def _get_masked(self, mask_key):
|
|
||||||
s = ABNF.mask(mask_key, self.data)
|
|
||||||
return mask_key + "".join(s)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def mask(mask_key, data):
|
|
||||||
"""
|
|
||||||
mask or unmask data. Just do xor for each byte
|
|
||||||
|
|
||||||
mask_key: 4 byte string(byte).
|
|
||||||
|
|
||||||
data: data to mask/unmask.
|
|
||||||
"""
|
|
||||||
_m = array.array("B", mask_key)
|
|
||||||
_d = array.array("B", data)
|
|
||||||
for i in xrange(len(_d)):
|
|
||||||
_d[i] ^= _m[i % 4]
|
|
||||||
return _d.tostring()
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocket(object):
|
|
||||||
"""
|
|
||||||
Low level WebSocket interface.
|
|
||||||
This class is based on
|
|
||||||
The WebSocket protocol draft-hixie-thewebsocketprotocol-76
|
|
||||||
http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
|
|
||||||
|
|
||||||
We can connect to the websocket server and send/recieve data.
|
|
||||||
The following example is a echo client.
|
|
||||||
|
|
||||||
>>> import websocket
|
|
||||||
>>> ws = websocket.WebSocket()
|
|
||||||
>>> ws.connect("ws://echo.websocket.org")
|
|
||||||
>>> ws.send("Hello, Server")
|
|
||||||
>>> ws.recv()
|
|
||||||
'Hello, Server'
|
|
||||||
>>> ws.close()
|
|
||||||
|
|
||||||
get_mask_key: a callable to produce new mask keys, see the set_mask_key
|
|
||||||
function's docstring for more details
|
|
||||||
sockopt: values for socket.setsockopt.
|
|
||||||
sockopt must be tuple and each element is argument of sock.setscokopt.
|
|
||||||
sslopt: dict object for ssl socket option.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, get_mask_key=None, sockopt=None, sslopt=None):
|
|
||||||
"""
|
|
||||||
Initalize WebSocket object.
|
|
||||||
"""
|
|
||||||
if sockopt is None:
|
|
||||||
sockopt = []
|
|
||||||
if sslopt is None:
|
|
||||||
sslopt = {}
|
|
||||||
self.connected = False
|
|
||||||
self.sock = socket.socket()
|
|
||||||
for opts in sockopt:
|
|
||||||
self.sock.setsockopt(*opts)
|
|
||||||
self.sslopt = sslopt
|
|
||||||
self.get_mask_key = get_mask_key
|
|
||||||
# Buffers over the packets from the layer beneath until desired amount
|
|
||||||
# bytes of bytes are received.
|
|
||||||
self._recv_buffer = []
|
|
||||||
# These buffer over the build-up of a single frame.
|
|
||||||
self._frame_header = None
|
|
||||||
self._frame_length = None
|
|
||||||
self._frame_mask = None
|
|
||||||
self._cont_data = None
|
|
||||||
|
|
||||||
def fileno(self):
|
|
||||||
"""
|
|
||||||
Returns sock.fileno()
|
|
||||||
"""
|
|
||||||
return self.sock.fileno()
|
|
||||||
|
|
||||||
def set_mask_key(self, func):
|
|
||||||
"""
|
|
||||||
set function to create musk key. You can custumize mask key generator.
|
|
||||||
Mainly, this is for testing purpose.
|
|
||||||
|
|
||||||
func: callable object. the fuct must 1 argument as integer.
|
|
||||||
The argument means length of mask key.
|
|
||||||
This func must be return string(byte array),
|
|
||||||
which length is argument specified.
|
|
||||||
"""
|
|
||||||
self.get_mask_key = func
|
|
||||||
|
|
||||||
def gettimeout(self):
|
|
||||||
"""
|
|
||||||
Get the websocket timeout(second).
|
|
||||||
"""
|
|
||||||
return self.sock.gettimeout()
|
|
||||||
|
|
||||||
def settimeout(self, timeout):
|
|
||||||
"""
|
|
||||||
Set the timeout to the websocket.
|
|
||||||
|
|
||||||
timeout: timeout time(second).
|
|
||||||
"""
|
|
||||||
self.sock.settimeout(timeout)
|
|
||||||
|
|
||||||
timeout = property(gettimeout, settimeout)
|
|
||||||
|
|
||||||
def connect(self, url, **options):
|
|
||||||
"""
|
|
||||||
Connect to url. url is websocket url scheme. ie. ws://host:port/resource
|
|
||||||
You can customize using 'options'.
|
|
||||||
If you set "header" dict object, you can set your own custom header.
|
|
||||||
|
|
||||||
>>> ws = WebSocket()
|
|
||||||
>>> ws.connect("ws://echo.websocket.org/",
|
|
||||||
... header={"User-Agent: MyProgram",
|
|
||||||
... "x-custom: header"})
|
|
||||||
|
|
||||||
timeout: socket timeout time. This value is integer.
|
|
||||||
if you set None for this value,
|
|
||||||
it means "use DEFAULT_TIMEOUT value"
|
|
||||||
|
|
||||||
options: current support option is only "header".
|
|
||||||
if you set header as dict value,
|
|
||||||
the custom HTTP headers are added.
|
|
||||||
|
|
||||||
"""
|
|
||||||
hostname, port, resource, is_secure = _parse_url(url)
|
|
||||||
# TODO: we need to support proxy
|
|
||||||
self.sock.connect((hostname, port))
|
|
||||||
if is_secure:
|
|
||||||
if HAVE_SSL:
|
|
||||||
if self.sslopt is None:
|
|
||||||
sslopt = {}
|
|
||||||
else:
|
|
||||||
sslopt = self.sslopt
|
|
||||||
self.sock = ssl.wrap_socket(self.sock, **sslopt)
|
|
||||||
else:
|
|
||||||
raise WebSocketException("SSL not available.")
|
|
||||||
|
|
||||||
self._handshake(hostname, port, resource, **options)
|
|
||||||
|
|
||||||
def _handshake(self, host, port, resource, **options):
|
|
||||||
headers = []
|
|
||||||
headers.append("GET %s HTTP/1.1" % resource)
|
|
||||||
headers.append("Upgrade: websocket")
|
|
||||||
headers.append("Connection: Upgrade")
|
|
||||||
if port == 80:
|
|
||||||
hostport = host
|
|
||||||
else:
|
|
||||||
hostport = "%s:%d" % (host, port)
|
|
||||||
headers.append("Host: %s" % hostport)
|
|
||||||
|
|
||||||
if "origin" in options:
|
|
||||||
headers.append("Origin: %s" % options["origin"])
|
|
||||||
else:
|
|
||||||
headers.append("Origin: http://%s" % hostport)
|
|
||||||
|
|
||||||
key = _create_sec_websocket_key()
|
|
||||||
headers.append("Sec-WebSocket-Key: %s" % key)
|
|
||||||
headers.append("Sec-WebSocket-Version: %s" % VERSION)
|
|
||||||
if "header" in options:
|
|
||||||
headers.extend(options["header"])
|
|
||||||
|
|
||||||
headers.append("")
|
|
||||||
headers.append("")
|
|
||||||
|
|
||||||
header_str = "\r\n".join(headers)
|
|
||||||
self._send(header_str)
|
|
||||||
if TRACE_ENABLED:
|
|
||||||
LOG.debug("--- request header ---")
|
|
||||||
LOG.debug(header_str)
|
|
||||||
LOG.debug("-----------------------")
|
|
||||||
|
|
||||||
status, resp_headers = self._read_headers()
|
|
||||||
if status == 301:
|
|
||||||
# Redirect
|
|
||||||
raise WebsocketRedirect(resp_headers)
|
|
||||||
if status != 101:
|
|
||||||
self.close()
|
|
||||||
raise WebSocketException("Handshake Status %d" % status)
|
|
||||||
|
|
||||||
success = self._validate_header(resp_headers, key)
|
|
||||||
if not success:
|
|
||||||
self.close()
|
|
||||||
raise WebSocketException("Invalid WebSocket Header")
|
|
||||||
|
|
||||||
self.connected = True
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _validate_header(headers, key):
|
|
||||||
for k, v in _HEADERS_TO_CHECK.iteritems():
|
|
||||||
r = headers.get(k, None)
|
|
||||||
if not r:
|
|
||||||
return False
|
|
||||||
r = r.lower()
|
|
||||||
if v != r:
|
|
||||||
return False
|
|
||||||
|
|
||||||
result = headers.get("sec-websocket-accept", None)
|
|
||||||
if not result:
|
|
||||||
return False
|
|
||||||
result = result.lower()
|
|
||||||
|
|
||||||
value = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
||||||
hashed = base64.encodestring(hashlib.sha1(value).digest()).strip().lower()
|
|
||||||
return hashed == result
|
|
||||||
|
|
||||||
def _read_headers(self):
|
|
||||||
status = None
|
|
||||||
headers = {}
|
|
||||||
if TRACE_ENABLED:
|
|
||||||
LOG.debug("--- response header ---")
|
|
||||||
|
|
||||||
while True:
|
|
||||||
line = self._recv_line()
|
|
||||||
if line == "\r\n":
|
|
||||||
break
|
|
||||||
line = line.strip()
|
|
||||||
if TRACE_ENABLED:
|
|
||||||
LOG.debug(line)
|
|
||||||
if not status:
|
|
||||||
status_info = line.split(" ", 2)
|
|
||||||
status = int(status_info[1])
|
|
||||||
else:
|
|
||||||
kv = line.split(":", 1)
|
|
||||||
if len(kv) == 2:
|
|
||||||
key, value = kv
|
|
||||||
headers[key.lower()] = value.strip().lower()
|
|
||||||
else:
|
|
||||||
raise WebSocketException("Invalid header")
|
|
||||||
|
|
||||||
if TRACE_ENABLED:
|
|
||||||
LOG.debug("-----------------------")
|
|
||||||
|
|
||||||
return status, headers
|
|
||||||
|
|
||||||
def send(self, payload, opcode=ABNF.OPCODE_TEXT):
|
|
||||||
"""
|
|
||||||
Send the data as string.
|
|
||||||
|
|
||||||
payload: Payload must be utf-8 string or unicoce,
|
|
||||||
if the opcode is OPCODE_TEXT.
|
|
||||||
Otherwise, it must be string(byte array)
|
|
||||||
|
|
||||||
opcode: operation code to send. Please see OPCODE_XXX.
|
|
||||||
"""
|
|
||||||
frame = ABNF.create_frame(payload, opcode)
|
|
||||||
if self.get_mask_key:
|
|
||||||
frame.get_mask_key = self.get_mask_key
|
|
||||||
data = frame.format()
|
|
||||||
length = len(data)
|
|
||||||
if TRACE_ENABLED:
|
|
||||||
LOG.debug("send: %s", repr(data))
|
|
||||||
while data:
|
|
||||||
l = self._send(data)
|
|
||||||
data = data[l:]
|
|
||||||
return length
|
|
||||||
|
|
||||||
def send_binary(self, payload):
|
|
||||||
"""
|
|
||||||
send the payload
|
|
||||||
"""
|
|
||||||
return self.send(payload, ABNF.OPCODE_BINARY)
|
|
||||||
|
|
||||||
def ping(self, payload=""):
|
|
||||||
"""
|
|
||||||
send ping data.
|
|
||||||
|
|
||||||
payload: data payload to send server.
|
|
||||||
"""
|
|
||||||
self.send(payload, ABNF.OPCODE_PING)
|
|
||||||
|
|
||||||
def pong(self, payload):
|
|
||||||
"""
|
|
||||||
send pong data.
|
|
||||||
|
|
||||||
payload: data payload to send server.
|
|
||||||
"""
|
|
||||||
self.send(payload, ABNF.OPCODE_PONG)
|
|
||||||
|
|
||||||
def recv(self):
|
|
||||||
"""
|
|
||||||
Receive string data(byte array) from the server.
|
|
||||||
|
|
||||||
return value: string(byte array) value.
|
|
||||||
"""
|
|
||||||
_, data = self.recv_data()
|
|
||||||
return data
|
|
||||||
|
|
||||||
def recv_data(self):
|
|
||||||
"""
|
|
||||||
Recieve data with operation code.
|
|
||||||
|
|
||||||
return value: tuple of operation code and string(byte array) value.
|
|
||||||
"""
|
|
||||||
while True:
|
|
||||||
frame = self.recv_frame()
|
|
||||||
if not frame:
|
|
||||||
# handle error:
|
|
||||||
# 'NoneType' object has no attribute 'opcode'
|
|
||||||
raise WebSocketException("Not a valid frame %s" % frame)
|
|
||||||
elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY, ABNF.OPCODE_CONT):
|
|
||||||
if frame.opcode == ABNF.OPCODE_CONT and not self._cont_data:
|
|
||||||
raise WebSocketException("Illegal frame")
|
|
||||||
if self._cont_data:
|
|
||||||
self._cont_data[1] += frame.data
|
|
||||||
else:
|
|
||||||
self._cont_data = [frame.opcode, frame.data]
|
|
||||||
if frame.fin:
|
|
||||||
data = self._cont_data
|
|
||||||
self._cont_data = None
|
|
||||||
return data
|
|
||||||
elif frame.opcode == ABNF.OPCODE_CLOSE:
|
|
||||||
self.send_close()
|
|
||||||
return (frame.opcode, None)
|
|
||||||
elif frame.opcode == ABNF.OPCODE_PING:
|
|
||||||
self.pong(frame.data)
|
|
||||||
|
|
||||||
def recv_frame(self):
|
|
||||||
"""
|
|
||||||
recieve data as frame from server.
|
|
||||||
|
|
||||||
return value: ABNF frame object.
|
|
||||||
"""
|
|
||||||
# Header
|
|
||||||
if self._frame_header is None:
|
|
||||||
self._frame_header = self._recv_strict(2)
|
|
||||||
b1 = ord(self._frame_header[0])
|
|
||||||
fin = b1 >> 7 & 1
|
|
||||||
rsv1 = b1 >> 6 & 1
|
|
||||||
rsv2 = b1 >> 5 & 1
|
|
||||||
rsv3 = b1 >> 4 & 1
|
|
||||||
opcode = b1 & 0xf
|
|
||||||
b2 = ord(self._frame_header[1])
|
|
||||||
has_mask = b2 >> 7 & 1
|
|
||||||
# Frame length
|
|
||||||
if self._frame_length is None:
|
|
||||||
length_bits = b2 & 0x7f
|
|
||||||
if length_bits == 0x7e:
|
|
||||||
length_data = self._recv_strict(2)
|
|
||||||
self._frame_length = struct.unpack("!H", length_data)[0]
|
|
||||||
elif length_bits == 0x7f:
|
|
||||||
length_data = self._recv_strict(8)
|
|
||||||
self._frame_length = struct.unpack("!Q", length_data)[0]
|
|
||||||
else:
|
|
||||||
self._frame_length = length_bits
|
|
||||||
# Mask
|
|
||||||
if self._frame_mask is None:
|
|
||||||
self._frame_mask = self._recv_strict(4) if has_mask else ""
|
|
||||||
# Payload
|
|
||||||
payload = self._recv_strict(self._frame_length)
|
|
||||||
if has_mask:
|
|
||||||
payload = ABNF.mask(self._frame_mask, payload)
|
|
||||||
# Reset for next frame
|
|
||||||
self._frame_header = None
|
|
||||||
self._frame_length = None
|
|
||||||
self._frame_mask = None
|
|
||||||
return ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload)
|
|
||||||
|
|
||||||
|
|
||||||
def send_close(self, status=STATUS_NORMAL, reason=""):
|
|
||||||
"""
|
|
||||||
send close data to the server.
|
|
||||||
|
|
||||||
status: status code to send. see STATUS_XXX.
|
|
||||||
|
|
||||||
reason: the reason to close. This must be string.
|
|
||||||
"""
|
|
||||||
if status < 0 or status >= ABNF.LENGTH_16:
|
|
||||||
raise ValueError("code is invalid range")
|
|
||||||
self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE)
|
|
||||||
|
|
||||||
def close(self, status=STATUS_NORMAL, reason=""):
|
|
||||||
"""
|
|
||||||
Close Websocket object
|
|
||||||
|
|
||||||
status: status code to send. see STATUS_XXX.
|
|
||||||
|
|
||||||
reason: the reason to close. This must be string.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self.sock.shutdown(socket.SHUT_RDWR)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
self._closeInternal()
|
|
||||||
|
|
||||||
def _closeInternal(self):
|
|
||||||
self.connected = False
|
|
||||||
self.sock.close()
|
|
||||||
|
|
||||||
def _send(self, data):
|
|
||||||
try:
|
|
||||||
return self.sock.send(data)
|
|
||||||
except socket.timeout as e:
|
|
||||||
raise WebSocketTimeoutException(e.args[0])
|
|
||||||
except Exception as e:
|
|
||||||
if "timed out" in e.args[0]:
|
|
||||||
raise WebSocketTimeoutException(e.args[0])
|
|
||||||
else:
|
|
||||||
raise e
|
|
||||||
|
|
||||||
def _recv(self, bufsize):
|
|
||||||
try:
|
|
||||||
bytes_ = self.sock.recv(bufsize)
|
|
||||||
except socket.timeout as e:
|
|
||||||
raise WebSocketTimeoutException(e.args[0])
|
|
||||||
except SSLError as e:
|
|
||||||
if e.args[0] == "The read operation timed out":
|
|
||||||
raise WebSocketTimeoutException(e.args[0])
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
if not bytes_:
|
|
||||||
raise WebSocketConnectionClosedException()
|
|
||||||
return bytes_
|
|
||||||
|
|
||||||
def _recv_strict(self, bufsize):
|
|
||||||
shortage = bufsize - sum(len(x) for x in self._recv_buffer)
|
|
||||||
while shortage > 0:
|
|
||||||
bytes_ = self._recv(shortage)
|
|
||||||
self._recv_buffer.append(bytes_)
|
|
||||||
shortage -= len(bytes_)
|
|
||||||
unified = "".join(self._recv_buffer)
|
|
||||||
if shortage == 0:
|
|
||||||
self._recv_buffer = []
|
|
||||||
return unified
|
|
||||||
else:
|
|
||||||
self._recv_buffer = [unified[bufsize:]]
|
|
||||||
return unified[:bufsize]
|
|
||||||
|
|
||||||
def _recv_line(self):
|
|
||||||
line = []
|
|
||||||
while True:
|
|
||||||
c = self._recv(1)
|
|
||||||
line.append(c)
|
|
||||||
if c == "\n":
|
|
||||||
break
|
|
||||||
return "".join(line)
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketApp(object):
|
|
||||||
"""
|
|
||||||
Higher level of APIs are provided.
|
|
||||||
The interface is like JavaScript WebSocket object.
|
|
||||||
"""
|
|
||||||
def __init__(self, url, header=None,
|
|
||||||
on_open=None, on_message=None, on_error=None,
|
|
||||||
on_close=None, keep_running=True, get_mask_key=None):
|
|
||||||
"""
|
|
||||||
url: websocket url.
|
|
||||||
header: custom header for websocket handshake.
|
|
||||||
on_open: callable object which is called at opening websocket.
|
|
||||||
this function has one argument. The arugment is this class object.
|
|
||||||
on_message: callbale object which is called when recieved data.
|
|
||||||
on_message has 2 arguments.
|
|
||||||
The 1st arugment is this class object.
|
|
||||||
The passing 2nd arugment is utf-8 string which we get from the server.
|
|
||||||
on_error: callable object which is called when we get error.
|
|
||||||
on_error has 2 arguments.
|
|
||||||
The 1st arugment is this class object.
|
|
||||||
The passing 2nd arugment is exception object.
|
|
||||||
on_close: callable object which is called when closed the connection.
|
|
||||||
this function has one argument. The arugment is this class object.
|
|
||||||
keep_running: a boolean flag indicating whether the app's main loop should
|
|
||||||
keep running, defaults to True
|
|
||||||
get_mask_key: a callable to produce new mask keys, see the WebSocket.set_mask_key's
|
|
||||||
docstring for more information
|
|
||||||
"""
|
|
||||||
self.url = url
|
|
||||||
self.header = [] if header is None else header
|
|
||||||
self.on_open = on_open
|
|
||||||
self.on_message = on_message
|
|
||||||
self.on_error = on_error
|
|
||||||
self.on_close = on_close
|
|
||||||
self.keep_running = keep_running
|
|
||||||
self.get_mask_key = get_mask_key
|
|
||||||
self.sock = None
|
|
||||||
|
|
||||||
def send(self, data, opcode=ABNF.OPCODE_TEXT):
|
|
||||||
"""
|
|
||||||
send message.
|
|
||||||
data: message to send. If you set opcode to OPCODE_TEXT, data must be utf-8 string or unicode.
|
|
||||||
opcode: operation code of data. default is OPCODE_TEXT.
|
|
||||||
"""
|
|
||||||
if self.sock.send(data, opcode) == 0:
|
|
||||||
raise WebSocketConnectionClosedException()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""
|
|
||||||
close websocket connection.
|
|
||||||
"""
|
|
||||||
self.keep_running = False
|
|
||||||
if self.sock != None:
|
|
||||||
self.sock.close()
|
|
||||||
|
|
||||||
def _send_ping(self, interval):
|
|
||||||
while True:
|
|
||||||
for _ in range(interval):
|
|
||||||
app.APP.monitor.waitForAbort(1)
|
|
||||||
if not self.keep_running:
|
|
||||||
return
|
|
||||||
self.sock.ping()
|
|
||||||
|
|
||||||
def run_forever(self, sockopt=None, sslopt=None, ping_interval=0):
|
|
||||||
"""
|
|
||||||
run event loop for WebSocket framework.
|
|
||||||
This loop is infinite loop and is alive during websocket is available.
|
|
||||||
sockopt: values for socket.setsockopt.
|
|
||||||
sockopt must be tuple and each element is argument of
|
|
||||||
sock.setscokopt.
|
|
||||||
sslopt: ssl socket optional dict.
|
|
||||||
ping_interval: automatically send "ping" command every specified
|
|
||||||
period(second)
|
|
||||||
if set to 0, not send automatically.
|
|
||||||
"""
|
|
||||||
if sockopt is None:
|
|
||||||
sockopt = []
|
|
||||||
if sslopt is None:
|
|
||||||
sslopt = {}
|
|
||||||
if self.sock:
|
|
||||||
raise WebSocketException("socket is already opened")
|
|
||||||
thread = None
|
|
||||||
self.keep_running = True
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.sock = WebSocket(self.get_mask_key,
|
|
||||||
sockopt=sockopt,
|
|
||||||
sslopt=sslopt)
|
|
||||||
self.sock.settimeout(DEFAULT_TIMEOUT)
|
|
||||||
self.sock.connect(self.url, header=self.header)
|
|
||||||
self._callback(self.on_open)
|
|
||||||
|
|
||||||
if ping_interval:
|
|
||||||
thread = threading.Thread(target=self._send_ping,
|
|
||||||
args=(ping_interval,))
|
|
||||||
thread.setDaemon(True)
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
while self.keep_running:
|
|
||||||
try:
|
|
||||||
data = self.sock.recv()
|
|
||||||
if data is None or self.keep_running is False:
|
|
||||||
break
|
|
||||||
self._callback(self.on_message, data)
|
|
||||||
except Exception, e:
|
|
||||||
if "timed out" not in e.args[0]:
|
|
||||||
raise e
|
|
||||||
|
|
||||||
except Exception, e:
|
|
||||||
self._callback(self.on_error, e)
|
|
||||||
finally:
|
|
||||||
if thread:
|
|
||||||
self.keep_running = False
|
|
||||||
self.sock.close()
|
|
||||||
self._callback(self.on_close)
|
|
||||||
self.sock = None
|
|
||||||
|
|
||||||
def _callback(self, callback, *args):
|
|
||||||
if callback:
|
|
||||||
try:
|
|
||||||
callback(self, *args)
|
|
||||||
except Exception, e:
|
|
||||||
LOG.error(e)
|
|
||||||
_, _, tb = sys.exc_info()
|
|
||||||
traceback.print_tb(tb)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
enable_trace(True)
|
|
||||||
WEBSOCKET = create_connection("ws://echo.websocket.org/")
|
|
||||||
LOG.info("Sending 'Hello, World'...")
|
|
||||||
WEBSOCKET.send("Hello, World")
|
|
||||||
LOG.info("Sent")
|
|
||||||
LOG.info("Receiving...")
|
|
||||||
RESULT = WEBSOCKET.recv()
|
|
||||||
LOG.info("Received '%s'", RESULT)
|
|
||||||
WEBSOCKET.close()
|
|
28
resources/lib/websocket/__init__.py
Normal file
28
resources/lib/websocket/__init__.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
from ._abnf import *
|
||||||
|
from ._app import WebSocketApp
|
||||||
|
from ._core import *
|
||||||
|
from ._exceptions import *
|
||||||
|
from ._logging import *
|
||||||
|
from ._socket import *
|
||||||
|
|
||||||
|
__version__ = "0.59.0"
|
458
resources/lib/websocket/_abnf.py
Normal file
458
resources/lib/websocket/_abnf.py
Normal file
|
@ -0,0 +1,458 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
import array
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from ._exceptions import *
|
||||||
|
from ._utils import validate_utf8
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
|
try:
|
||||||
|
if six.PY3:
|
||||||
|
import numpy
|
||||||
|
else:
|
||||||
|
numpy = None
|
||||||
|
except ImportError:
|
||||||
|
numpy = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# If wsaccel is available we use compiled routines to mask data.
|
||||||
|
if not numpy:
|
||||||
|
from wsaccel.xormask import XorMaskerSimple
|
||||||
|
|
||||||
|
def _mask(_m, _d):
|
||||||
|
return XorMaskerSimple(_m).process(_d)
|
||||||
|
except ImportError:
|
||||||
|
# wsaccel is not available, we rely on python implementations.
|
||||||
|
def _mask(_m, _d):
|
||||||
|
for i in range(len(_d)):
|
||||||
|
_d[i] ^= _m[i % 4]
|
||||||
|
|
||||||
|
if six.PY3:
|
||||||
|
return _d.tobytes()
|
||||||
|
else:
|
||||||
|
return _d.tostring()
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'ABNF', 'continuous_frame', 'frame_buffer',
|
||||||
|
'STATUS_NORMAL',
|
||||||
|
'STATUS_GOING_AWAY',
|
||||||
|
'STATUS_PROTOCOL_ERROR',
|
||||||
|
'STATUS_UNSUPPORTED_DATA_TYPE',
|
||||||
|
'STATUS_STATUS_NOT_AVAILABLE',
|
||||||
|
'STATUS_ABNORMAL_CLOSED',
|
||||||
|
'STATUS_INVALID_PAYLOAD',
|
||||||
|
'STATUS_POLICY_VIOLATION',
|
||||||
|
'STATUS_MESSAGE_TOO_BIG',
|
||||||
|
'STATUS_INVALID_EXTENSION',
|
||||||
|
'STATUS_UNEXPECTED_CONDITION',
|
||||||
|
'STATUS_BAD_GATEWAY',
|
||||||
|
'STATUS_TLS_HANDSHAKE_ERROR',
|
||||||
|
]
|
||||||
|
|
||||||
|
# closing frame status codes.
|
||||||
|
STATUS_NORMAL = 1000
|
||||||
|
STATUS_GOING_AWAY = 1001
|
||||||
|
STATUS_PROTOCOL_ERROR = 1002
|
||||||
|
STATUS_UNSUPPORTED_DATA_TYPE = 1003
|
||||||
|
STATUS_STATUS_NOT_AVAILABLE = 1005
|
||||||
|
STATUS_ABNORMAL_CLOSED = 1006
|
||||||
|
STATUS_INVALID_PAYLOAD = 1007
|
||||||
|
STATUS_POLICY_VIOLATION = 1008
|
||||||
|
STATUS_MESSAGE_TOO_BIG = 1009
|
||||||
|
STATUS_INVALID_EXTENSION = 1010
|
||||||
|
STATUS_UNEXPECTED_CONDITION = 1011
|
||||||
|
STATUS_BAD_GATEWAY = 1014
|
||||||
|
STATUS_TLS_HANDSHAKE_ERROR = 1015
|
||||||
|
|
||||||
|
VALID_CLOSE_STATUS = (
|
||||||
|
STATUS_NORMAL,
|
||||||
|
STATUS_GOING_AWAY,
|
||||||
|
STATUS_PROTOCOL_ERROR,
|
||||||
|
STATUS_UNSUPPORTED_DATA_TYPE,
|
||||||
|
STATUS_INVALID_PAYLOAD,
|
||||||
|
STATUS_POLICY_VIOLATION,
|
||||||
|
STATUS_MESSAGE_TOO_BIG,
|
||||||
|
STATUS_INVALID_EXTENSION,
|
||||||
|
STATUS_UNEXPECTED_CONDITION,
|
||||||
|
STATUS_BAD_GATEWAY,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ABNF(object):
|
||||||
|
"""
|
||||||
|
ABNF frame class.
|
||||||
|
See http://tools.ietf.org/html/rfc5234
|
||||||
|
and http://tools.ietf.org/html/rfc6455#section-5.2
|
||||||
|
"""
|
||||||
|
|
||||||
|
# operation code values.
|
||||||
|
OPCODE_CONT = 0x0
|
||||||
|
OPCODE_TEXT = 0x1
|
||||||
|
OPCODE_BINARY = 0x2
|
||||||
|
OPCODE_CLOSE = 0x8
|
||||||
|
OPCODE_PING = 0x9
|
||||||
|
OPCODE_PONG = 0xa
|
||||||
|
|
||||||
|
# available operation code value tuple
|
||||||
|
OPCODES = (OPCODE_CONT, OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE,
|
||||||
|
OPCODE_PING, OPCODE_PONG)
|
||||||
|
|
||||||
|
# opcode human readable string
|
||||||
|
OPCODE_MAP = {
|
||||||
|
OPCODE_CONT: "cont",
|
||||||
|
OPCODE_TEXT: "text",
|
||||||
|
OPCODE_BINARY: "binary",
|
||||||
|
OPCODE_CLOSE: "close",
|
||||||
|
OPCODE_PING: "ping",
|
||||||
|
OPCODE_PONG: "pong"
|
||||||
|
}
|
||||||
|
|
||||||
|
# data length threshold.
|
||||||
|
LENGTH_7 = 0x7e
|
||||||
|
LENGTH_16 = 1 << 16
|
||||||
|
LENGTH_63 = 1 << 63
|
||||||
|
|
||||||
|
def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0,
|
||||||
|
opcode=OPCODE_TEXT, mask=1, data=""):
|
||||||
|
"""
|
||||||
|
Constructor for ABNF. Please check RFC for arguments.
|
||||||
|
"""
|
||||||
|
self.fin = fin
|
||||||
|
self.rsv1 = rsv1
|
||||||
|
self.rsv2 = rsv2
|
||||||
|
self.rsv3 = rsv3
|
||||||
|
self.opcode = opcode
|
||||||
|
self.mask = mask
|
||||||
|
if data is None:
|
||||||
|
data = ""
|
||||||
|
self.data = data
|
||||||
|
self.get_mask_key = os.urandom
|
||||||
|
|
||||||
|
def validate(self, skip_utf8_validation=False):
|
||||||
|
"""
|
||||||
|
Validate the ABNF frame.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
skip_utf8_validation: skip utf8 validation.
|
||||||
|
"""
|
||||||
|
if self.rsv1 or self.rsv2 or self.rsv3:
|
||||||
|
raise WebSocketProtocolException("rsv is not implemented, yet")
|
||||||
|
|
||||||
|
if self.opcode not in ABNF.OPCODES:
|
||||||
|
raise WebSocketProtocolException("Invalid opcode %r", self.opcode)
|
||||||
|
|
||||||
|
if self.opcode == ABNF.OPCODE_PING and not self.fin:
|
||||||
|
raise WebSocketProtocolException("Invalid ping frame.")
|
||||||
|
|
||||||
|
if self.opcode == ABNF.OPCODE_CLOSE:
|
||||||
|
l = len(self.data)
|
||||||
|
if not l:
|
||||||
|
return
|
||||||
|
if l == 1 or l >= 126:
|
||||||
|
raise WebSocketProtocolException("Invalid close frame.")
|
||||||
|
if l > 2 and not skip_utf8_validation and not validate_utf8(self.data[2:]):
|
||||||
|
raise WebSocketProtocolException("Invalid close frame.")
|
||||||
|
|
||||||
|
code = 256 * \
|
||||||
|
six.byte2int(self.data[0:1]) + six.byte2int(self.data[1:2])
|
||||||
|
if not self._is_valid_close_status(code):
|
||||||
|
raise WebSocketProtocolException("Invalid close opcode.")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_valid_close_status(code):
|
||||||
|
return code in VALID_CLOSE_STATUS or (3000 <= code < 5000)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "fin=" + str(self.fin) \
|
||||||
|
+ " opcode=" + str(self.opcode) \
|
||||||
|
+ " data=" + str(self.data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_frame(data, opcode, fin=1):
|
||||||
|
"""
|
||||||
|
Create frame to send text, binary and other data.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data: <type>
|
||||||
|
data to send. This is string value(byte array).
|
||||||
|
If opcode is OPCODE_TEXT and this value is unicode,
|
||||||
|
data value is converted into unicode string, automatically.
|
||||||
|
opcode: <type>
|
||||||
|
operation code. please see OPCODE_XXX.
|
||||||
|
fin: <type>
|
||||||
|
fin flag. if set to 0, create continue fragmentation.
|
||||||
|
"""
|
||||||
|
if opcode == ABNF.OPCODE_TEXT and isinstance(data, six.text_type):
|
||||||
|
data = data.encode("utf-8")
|
||||||
|
# mask must be set if send data from client
|
||||||
|
return ABNF(fin, 0, 0, 0, opcode, 1, data)
|
||||||
|
|
||||||
|
def format(self):
|
||||||
|
"""
|
||||||
|
Format this object to string(byte array) to send data to server.
|
||||||
|
"""
|
||||||
|
if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]):
|
||||||
|
raise ValueError("not 0 or 1")
|
||||||
|
if self.opcode not in ABNF.OPCODES:
|
||||||
|
raise ValueError("Invalid OPCODE")
|
||||||
|
length = len(self.data)
|
||||||
|
if length >= ABNF.LENGTH_63:
|
||||||
|
raise ValueError("data is too long")
|
||||||
|
|
||||||
|
frame_header = chr(self.fin << 7 |
|
||||||
|
self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4 |
|
||||||
|
self.opcode)
|
||||||
|
if length < ABNF.LENGTH_7:
|
||||||
|
frame_header += chr(self.mask << 7 | length)
|
||||||
|
frame_header = six.b(frame_header)
|
||||||
|
elif length < ABNF.LENGTH_16:
|
||||||
|
frame_header += chr(self.mask << 7 | 0x7e)
|
||||||
|
frame_header = six.b(frame_header)
|
||||||
|
frame_header += struct.pack("!H", length)
|
||||||
|
else:
|
||||||
|
frame_header += chr(self.mask << 7 | 0x7f)
|
||||||
|
frame_header = six.b(frame_header)
|
||||||
|
frame_header += struct.pack("!Q", length)
|
||||||
|
|
||||||
|
if not self.mask:
|
||||||
|
return frame_header + self.data
|
||||||
|
else:
|
||||||
|
mask_key = self.get_mask_key(4)
|
||||||
|
return frame_header + self._get_masked(mask_key)
|
||||||
|
|
||||||
|
def _get_masked(self, mask_key):
|
||||||
|
s = ABNF.mask(mask_key, self.data)
|
||||||
|
|
||||||
|
if isinstance(mask_key, six.text_type):
|
||||||
|
mask_key = mask_key.encode('utf-8')
|
||||||
|
|
||||||
|
return mask_key + s
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mask(mask_key, data):
|
||||||
|
"""
|
||||||
|
Mask or unmask data. Just do xor for each byte
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
mask_key: <type>
|
||||||
|
4 byte string(byte).
|
||||||
|
data: <type>
|
||||||
|
data to mask/unmask.
|
||||||
|
"""
|
||||||
|
if data is None:
|
||||||
|
data = ""
|
||||||
|
|
||||||
|
if isinstance(mask_key, six.text_type):
|
||||||
|
mask_key = six.b(mask_key)
|
||||||
|
|
||||||
|
if isinstance(data, six.text_type):
|
||||||
|
data = six.b(data)
|
||||||
|
|
||||||
|
if numpy:
|
||||||
|
origlen = len(data)
|
||||||
|
_mask_key = mask_key[3] << 24 | mask_key[2] << 16 | mask_key[1] << 8 | mask_key[0]
|
||||||
|
|
||||||
|
# We need data to be a multiple of four...
|
||||||
|
data += bytes(" " * (4 - (len(data) % 4)), "us-ascii")
|
||||||
|
a = numpy.frombuffer(data, dtype="uint32")
|
||||||
|
masked = numpy.bitwise_xor(a, [_mask_key]).astype("uint32")
|
||||||
|
if len(data) > origlen:
|
||||||
|
return masked.tobytes()[:origlen]
|
||||||
|
return masked.tobytes()
|
||||||
|
else:
|
||||||
|
_m = array.array("B", mask_key)
|
||||||
|
_d = array.array("B", data)
|
||||||
|
return _mask(_m, _d)
|
||||||
|
|
||||||
|
|
||||||
|
class frame_buffer(object):
|
||||||
|
_HEADER_MASK_INDEX = 5
|
||||||
|
_HEADER_LENGTH_INDEX = 6
|
||||||
|
|
||||||
|
def __init__(self, recv_fn, skip_utf8_validation):
|
||||||
|
self.recv = recv_fn
|
||||||
|
self.skip_utf8_validation = skip_utf8_validation
|
||||||
|
# Buffers over the packets from the layer beneath until desired amount
|
||||||
|
# bytes of bytes are received.
|
||||||
|
self.recv_buffer = []
|
||||||
|
self.clear()
|
||||||
|
self.lock = Lock()
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.header = None
|
||||||
|
self.length = None
|
||||||
|
self.mask = None
|
||||||
|
|
||||||
|
def has_received_header(self):
|
||||||
|
return self.header is None
|
||||||
|
|
||||||
|
def recv_header(self):
|
||||||
|
header = self.recv_strict(2)
|
||||||
|
b1 = header[0]
|
||||||
|
|
||||||
|
if six.PY2:
|
||||||
|
b1 = ord(b1)
|
||||||
|
|
||||||
|
fin = b1 >> 7 & 1
|
||||||
|
rsv1 = b1 >> 6 & 1
|
||||||
|
rsv2 = b1 >> 5 & 1
|
||||||
|
rsv3 = b1 >> 4 & 1
|
||||||
|
opcode = b1 & 0xf
|
||||||
|
b2 = header[1]
|
||||||
|
|
||||||
|
if six.PY2:
|
||||||
|
b2 = ord(b2)
|
||||||
|
|
||||||
|
has_mask = b2 >> 7 & 1
|
||||||
|
length_bits = b2 & 0x7f
|
||||||
|
|
||||||
|
self.header = (fin, rsv1, rsv2, rsv3, opcode, has_mask, length_bits)
|
||||||
|
|
||||||
|
def has_mask(self):
|
||||||
|
if not self.header:
|
||||||
|
return False
|
||||||
|
return self.header[frame_buffer._HEADER_MASK_INDEX]
|
||||||
|
|
||||||
|
def has_received_length(self):
|
||||||
|
return self.length is None
|
||||||
|
|
||||||
|
def recv_length(self):
|
||||||
|
bits = self.header[frame_buffer._HEADER_LENGTH_INDEX]
|
||||||
|
length_bits = bits & 0x7f
|
||||||
|
if length_bits == 0x7e:
|
||||||
|
v = self.recv_strict(2)
|
||||||
|
self.length = struct.unpack("!H", v)[0]
|
||||||
|
elif length_bits == 0x7f:
|
||||||
|
v = self.recv_strict(8)
|
||||||
|
self.length = struct.unpack("!Q", v)[0]
|
||||||
|
else:
|
||||||
|
self.length = length_bits
|
||||||
|
|
||||||
|
def has_received_mask(self):
|
||||||
|
return self.mask is None
|
||||||
|
|
||||||
|
def recv_mask(self):
|
||||||
|
self.mask = self.recv_strict(4) if self.has_mask() else ""
|
||||||
|
|
||||||
|
def recv_frame(self):
|
||||||
|
|
||||||
|
with self.lock:
|
||||||
|
# Header
|
||||||
|
if self.has_received_header():
|
||||||
|
self.recv_header()
|
||||||
|
(fin, rsv1, rsv2, rsv3, opcode, has_mask, _) = self.header
|
||||||
|
|
||||||
|
# Frame length
|
||||||
|
if self.has_received_length():
|
||||||
|
self.recv_length()
|
||||||
|
length = self.length
|
||||||
|
|
||||||
|
# Mask
|
||||||
|
if self.has_received_mask():
|
||||||
|
self.recv_mask()
|
||||||
|
mask = self.mask
|
||||||
|
|
||||||
|
# Payload
|
||||||
|
payload = self.recv_strict(length)
|
||||||
|
if has_mask:
|
||||||
|
payload = ABNF.mask(mask, payload)
|
||||||
|
|
||||||
|
# Reset for next frame
|
||||||
|
self.clear()
|
||||||
|
|
||||||
|
frame = ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload)
|
||||||
|
frame.validate(self.skip_utf8_validation)
|
||||||
|
|
||||||
|
return frame
|
||||||
|
|
||||||
|
def recv_strict(self, bufsize):
|
||||||
|
shortage = bufsize - sum(len(x) for x in self.recv_buffer)
|
||||||
|
while shortage > 0:
|
||||||
|
# Limit buffer size that we pass to socket.recv() to avoid
|
||||||
|
# fragmenting the heap -- the number of bytes recv() actually
|
||||||
|
# reads is limited by socket buffer and is relatively small,
|
||||||
|
# yet passing large numbers repeatedly causes lots of large
|
||||||
|
# buffers allocated and then shrunk, which results in
|
||||||
|
# fragmentation.
|
||||||
|
bytes_ = self.recv(min(16384, shortage))
|
||||||
|
self.recv_buffer.append(bytes_)
|
||||||
|
shortage -= len(bytes_)
|
||||||
|
|
||||||
|
unified = six.b("").join(self.recv_buffer)
|
||||||
|
|
||||||
|
if shortage == 0:
|
||||||
|
self.recv_buffer = []
|
||||||
|
return unified
|
||||||
|
else:
|
||||||
|
self.recv_buffer = [unified[bufsize:]]
|
||||||
|
return unified[:bufsize]
|
||||||
|
|
||||||
|
|
||||||
|
class continuous_frame(object):
|
||||||
|
|
||||||
|
def __init__(self, fire_cont_frame, skip_utf8_validation):
|
||||||
|
self.fire_cont_frame = fire_cont_frame
|
||||||
|
self.skip_utf8_validation = skip_utf8_validation
|
||||||
|
self.cont_data = None
|
||||||
|
self.recving_frames = None
|
||||||
|
|
||||||
|
def validate(self, frame):
|
||||||
|
if not self.recving_frames and frame.opcode == ABNF.OPCODE_CONT:
|
||||||
|
raise WebSocketProtocolException("Illegal frame")
|
||||||
|
if self.recving_frames and \
|
||||||
|
frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY):
|
||||||
|
raise WebSocketProtocolException("Illegal frame")
|
||||||
|
|
||||||
|
def add(self, frame):
|
||||||
|
if self.cont_data:
|
||||||
|
self.cont_data[1] += frame.data
|
||||||
|
else:
|
||||||
|
if frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY):
|
||||||
|
self.recving_frames = frame.opcode
|
||||||
|
self.cont_data = [frame.opcode, frame.data]
|
||||||
|
|
||||||
|
if frame.fin:
|
||||||
|
self.recving_frames = None
|
||||||
|
|
||||||
|
def is_fire(self, frame):
|
||||||
|
return frame.fin or self.fire_cont_frame
|
||||||
|
|
||||||
|
def extract(self, frame):
|
||||||
|
data = self.cont_data
|
||||||
|
self.cont_data = None
|
||||||
|
frame.data = data[1]
|
||||||
|
if not self.fire_cont_frame and data[0] == ABNF.OPCODE_TEXT and not self.skip_utf8_validation and not validate_utf8(frame.data):
|
||||||
|
raise WebSocketPayloadException(
|
||||||
|
"cannot decode: " + repr(frame.data))
|
||||||
|
|
||||||
|
return [data[0], frame]
|
400
resources/lib/websocket/_app.py
Normal file
400
resources/lib/websocket/_app.py
Normal file
|
@ -0,0 +1,400 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
import inspect
|
||||||
|
import select
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from ._abnf import ABNF
|
||||||
|
from ._core import WebSocket, getdefaulttimeout
|
||||||
|
from ._exceptions import *
|
||||||
|
from . import _logging
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["WebSocketApp"]
|
||||||
|
|
||||||
|
|
||||||
|
class Dispatcher:
|
||||||
|
"""
|
||||||
|
Dispatcher
|
||||||
|
"""
|
||||||
|
def __init__(self, app, ping_timeout):
|
||||||
|
self.app = app
|
||||||
|
self.ping_timeout = ping_timeout
|
||||||
|
|
||||||
|
def read(self, sock, read_callback, check_callback):
|
||||||
|
while self.app.keep_running:
|
||||||
|
r, w, e = select.select(
|
||||||
|
(self.app.sock.sock, ), (), (), self.ping_timeout)
|
||||||
|
if r:
|
||||||
|
if not read_callback():
|
||||||
|
break
|
||||||
|
check_callback()
|
||||||
|
|
||||||
|
|
||||||
|
class SSLDispatcher:
|
||||||
|
"""
|
||||||
|
SSLDispatcher
|
||||||
|
"""
|
||||||
|
def __init__(self, app, ping_timeout):
|
||||||
|
self.app = app
|
||||||
|
self.ping_timeout = ping_timeout
|
||||||
|
|
||||||
|
def read(self, sock, read_callback, check_callback):
|
||||||
|
while self.app.keep_running:
|
||||||
|
r = self.select()
|
||||||
|
if r:
|
||||||
|
if not read_callback():
|
||||||
|
break
|
||||||
|
check_callback()
|
||||||
|
|
||||||
|
def select(self):
|
||||||
|
sock = self.app.sock.sock
|
||||||
|
if sock.pending():
|
||||||
|
return [sock,]
|
||||||
|
|
||||||
|
r, w, e = select.select((sock, ), (), (), self.ping_timeout)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketApp(object):
|
||||||
|
"""
|
||||||
|
Higher level of APIs are provided. The interface is like JavaScript WebSocket object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, url, header=None,
|
||||||
|
on_open=None, on_message=None, on_error=None,
|
||||||
|
on_close=None, on_ping=None, on_pong=None,
|
||||||
|
on_cont_message=None,
|
||||||
|
keep_running=True, get_mask_key=None, cookie=None,
|
||||||
|
subprotocols=None,
|
||||||
|
on_data=None):
|
||||||
|
"""
|
||||||
|
WebSocketApp initialization
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
url: <type>
|
||||||
|
websocket url.
|
||||||
|
header: list or dict
|
||||||
|
custom header for websocket handshake.
|
||||||
|
on_open: <type>
|
||||||
|
callable object which is called at opening websocket.
|
||||||
|
this function has one argument. The argument is this class object.
|
||||||
|
on_message: <type>
|
||||||
|
callable object which is called when received data.
|
||||||
|
on_message has 2 arguments.
|
||||||
|
The 1st argument is this class object.
|
||||||
|
The 2nd argument is utf-8 string which we get from the server.
|
||||||
|
on_error: <type>
|
||||||
|
callable object which is called when we get error.
|
||||||
|
on_error has 2 arguments.
|
||||||
|
The 1st argument is this class object.
|
||||||
|
The 2nd argument is exception object.
|
||||||
|
on_close: <type>
|
||||||
|
callable object which is called when closed the connection.
|
||||||
|
this function has one argument. The argument is this class object.
|
||||||
|
on_cont_message: <type>
|
||||||
|
callback object which is called when receive continued
|
||||||
|
frame data.
|
||||||
|
on_cont_message has 3 arguments.
|
||||||
|
The 1st argument is this class object.
|
||||||
|
The 2nd argument is utf-8 string which we get from the server.
|
||||||
|
The 3rd argument is continue flag. if 0, the data continue
|
||||||
|
to next frame data
|
||||||
|
on_data: <type>
|
||||||
|
callback object which is called when a message received.
|
||||||
|
This is called before on_message or on_cont_message,
|
||||||
|
and then on_message or on_cont_message is called.
|
||||||
|
on_data has 4 argument.
|
||||||
|
The 1st argument is this class object.
|
||||||
|
The 2nd argument is utf-8 string which we get from the server.
|
||||||
|
The 3rd argument is data type. ABNF.OPCODE_TEXT or ABNF.OPCODE_BINARY will be came.
|
||||||
|
The 4th argument is continue flag. if 0, the data continue
|
||||||
|
keep_running: <type>
|
||||||
|
this parameter is obsolete and ignored.
|
||||||
|
get_mask_key: func
|
||||||
|
a callable to produce new mask keys,
|
||||||
|
see the WebSocket.set_mask_key's docstring for more information
|
||||||
|
cookie: str
|
||||||
|
cookie value.
|
||||||
|
subprotocols: <type>
|
||||||
|
array of available sub protocols. default is None.
|
||||||
|
"""
|
||||||
|
self.url = url
|
||||||
|
self.header = header if header is not None else []
|
||||||
|
self.cookie = cookie
|
||||||
|
|
||||||
|
self.on_open = on_open
|
||||||
|
self.on_message = on_message
|
||||||
|
self.on_data = on_data
|
||||||
|
self.on_error = on_error
|
||||||
|
self.on_close = on_close
|
||||||
|
self.on_ping = on_ping
|
||||||
|
self.on_pong = on_pong
|
||||||
|
self.on_cont_message = on_cont_message
|
||||||
|
self.keep_running = False
|
||||||
|
self.get_mask_key = get_mask_key
|
||||||
|
self.sock = None
|
||||||
|
self.last_ping_tm = 0
|
||||||
|
self.last_pong_tm = 0
|
||||||
|
self.subprotocols = subprotocols
|
||||||
|
|
||||||
|
def send(self, data, opcode=ABNF.OPCODE_TEXT):
|
||||||
|
"""
|
||||||
|
send message
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data: <type>
|
||||||
|
Message to send. If you set opcode to OPCODE_TEXT,
|
||||||
|
data must be utf-8 string or unicode.
|
||||||
|
opcode: <type>
|
||||||
|
Operation code of data. default is OPCODE_TEXT.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.sock or self.sock.send(data, opcode) == 0:
|
||||||
|
raise WebSocketConnectionClosedException(
|
||||||
|
"Connection is already closed.")
|
||||||
|
|
||||||
|
def close(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Close websocket connection.
|
||||||
|
"""
|
||||||
|
self.keep_running = False
|
||||||
|
if self.sock:
|
||||||
|
self.sock.close(**kwargs)
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
def _send_ping(self, interval, event, payload):
|
||||||
|
while not event.wait(interval):
|
||||||
|
self.last_ping_tm = time.time()
|
||||||
|
if self.sock:
|
||||||
|
try:
|
||||||
|
self.sock.ping(payload)
|
||||||
|
except Exception as ex:
|
||||||
|
_logging.warning("send_ping routine terminated: {}".format(ex))
|
||||||
|
break
|
||||||
|
|
||||||
|
def run_forever(self, sockopt=None, sslopt=None,
|
||||||
|
ping_interval=0, ping_timeout=None,
|
||||||
|
ping_payload="",
|
||||||
|
http_proxy_host=None, http_proxy_port=None,
|
||||||
|
http_no_proxy=None, http_proxy_auth=None,
|
||||||
|
skip_utf8_validation=False,
|
||||||
|
host=None, origin=None, dispatcher=None,
|
||||||
|
suppress_origin=False, proxy_type=None,
|
||||||
|
enable_multithread=True):
|
||||||
|
"""
|
||||||
|
Run event loop for WebSocket framework.
|
||||||
|
|
||||||
|
This loop is an infinite loop and is alive while websocket is available.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
sockopt: tuple
|
||||||
|
values for socket.setsockopt.
|
||||||
|
sockopt must be tuple
|
||||||
|
and each element is argument of sock.setsockopt.
|
||||||
|
sslopt: dict
|
||||||
|
optional dict object for ssl socket option.
|
||||||
|
ping_interval: int or float
|
||||||
|
automatically send "ping" command
|
||||||
|
every specified period (in seconds)
|
||||||
|
if set to 0, not send automatically.
|
||||||
|
ping_timeout: int or float
|
||||||
|
timeout (in seconds) if the pong message is not received.
|
||||||
|
ping_payload: str
|
||||||
|
payload message to send with each ping.
|
||||||
|
http_proxy_host: <type>
|
||||||
|
http proxy host name.
|
||||||
|
http_proxy_port: <type>
|
||||||
|
http proxy port. If not set, set to 80.
|
||||||
|
http_no_proxy: <type>
|
||||||
|
host names, which doesn't use proxy.
|
||||||
|
skip_utf8_validation: bool
|
||||||
|
skip utf8 validation.
|
||||||
|
host: str
|
||||||
|
update host header.
|
||||||
|
origin: str
|
||||||
|
update origin header.
|
||||||
|
dispatcher: <type>
|
||||||
|
customize reading data from socket.
|
||||||
|
suppress_origin: bool
|
||||||
|
suppress outputting origin header.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
teardown: bool
|
||||||
|
False if caught KeyboardInterrupt, True if other exception was raised during a loop
|
||||||
|
"""
|
||||||
|
|
||||||
|
if ping_timeout is not None and ping_timeout <= 0:
|
||||||
|
ping_timeout = None
|
||||||
|
if ping_timeout and ping_interval and ping_interval <= ping_timeout:
|
||||||
|
raise WebSocketException("Ensure ping_interval > ping_timeout")
|
||||||
|
if not sockopt:
|
||||||
|
sockopt = []
|
||||||
|
if not sslopt:
|
||||||
|
sslopt = {}
|
||||||
|
if self.sock:
|
||||||
|
raise WebSocketException("socket is already opened")
|
||||||
|
thread = None
|
||||||
|
self.keep_running = True
|
||||||
|
self.last_ping_tm = 0
|
||||||
|
self.last_pong_tm = 0
|
||||||
|
|
||||||
|
def teardown(close_frame=None):
|
||||||
|
"""
|
||||||
|
Tears down the connection.
|
||||||
|
|
||||||
|
If close_frame is set, we will invoke the on_close handler with the
|
||||||
|
statusCode and reason from there.
|
||||||
|
"""
|
||||||
|
if thread and thread.is_alive():
|
||||||
|
event.set()
|
||||||
|
thread.join()
|
||||||
|
self.keep_running = False
|
||||||
|
if self.sock:
|
||||||
|
self.sock.close()
|
||||||
|
close_args = self._get_close_args(
|
||||||
|
close_frame.data if close_frame else None)
|
||||||
|
self._callback(self.on_close, *close_args)
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.sock = WebSocket(
|
||||||
|
self.get_mask_key, sockopt=sockopt, sslopt=sslopt,
|
||||||
|
fire_cont_frame=self.on_cont_message is not None,
|
||||||
|
skip_utf8_validation=skip_utf8_validation,
|
||||||
|
enable_multithread=enable_multithread)
|
||||||
|
self.sock.settimeout(getdefaulttimeout())
|
||||||
|
self.sock.connect(
|
||||||
|
self.url, header=self.header, cookie=self.cookie,
|
||||||
|
http_proxy_host=http_proxy_host,
|
||||||
|
http_proxy_port=http_proxy_port, http_no_proxy=http_no_proxy,
|
||||||
|
http_proxy_auth=http_proxy_auth, subprotocols=self.subprotocols,
|
||||||
|
host=host, origin=origin, suppress_origin=suppress_origin,
|
||||||
|
proxy_type=proxy_type)
|
||||||
|
if not dispatcher:
|
||||||
|
dispatcher = self.create_dispatcher(ping_timeout)
|
||||||
|
|
||||||
|
self._callback(self.on_open)
|
||||||
|
|
||||||
|
if ping_interval:
|
||||||
|
event = threading.Event()
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=self._send_ping, args=(ping_interval, event, ping_payload))
|
||||||
|
thread.daemon = True
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def read():
|
||||||
|
if not self.keep_running:
|
||||||
|
return teardown()
|
||||||
|
|
||||||
|
op_code, frame = self.sock.recv_data_frame(True)
|
||||||
|
if op_code == ABNF.OPCODE_CLOSE:
|
||||||
|
return teardown(frame)
|
||||||
|
elif op_code == ABNF.OPCODE_PING:
|
||||||
|
self._callback(self.on_ping, frame.data)
|
||||||
|
elif op_code == ABNF.OPCODE_PONG:
|
||||||
|
self.last_pong_tm = time.time()
|
||||||
|
self._callback(self.on_pong, frame.data)
|
||||||
|
elif op_code == ABNF.OPCODE_CONT and self.on_cont_message:
|
||||||
|
self._callback(self.on_data, frame.data,
|
||||||
|
frame.opcode, frame.fin)
|
||||||
|
self._callback(self.on_cont_message,
|
||||||
|
frame.data, frame.fin)
|
||||||
|
else:
|
||||||
|
data = frame.data
|
||||||
|
if six.PY3 and op_code == ABNF.OPCODE_TEXT:
|
||||||
|
data = data.decode("utf-8")
|
||||||
|
self._callback(self.on_data, data, frame.opcode, True)
|
||||||
|
self._callback(self.on_message, data)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check():
|
||||||
|
if (ping_timeout):
|
||||||
|
has_timeout_expired = time.time() - self.last_ping_tm > ping_timeout
|
||||||
|
has_pong_not_arrived_after_last_ping = self.last_pong_tm - self.last_ping_tm < 0
|
||||||
|
has_pong_arrived_too_late = self.last_pong_tm - self.last_ping_tm > ping_timeout
|
||||||
|
|
||||||
|
if (self.last_ping_tm and
|
||||||
|
has_timeout_expired and
|
||||||
|
(has_pong_not_arrived_after_last_ping or has_pong_arrived_too_late)):
|
||||||
|
raise WebSocketTimeoutException("ping/pong timed out")
|
||||||
|
return True
|
||||||
|
|
||||||
|
dispatcher.read(self.sock.sock, read, check)
|
||||||
|
except (Exception, KeyboardInterrupt, SystemExit) as e:
|
||||||
|
self._callback(self.on_error, e)
|
||||||
|
if isinstance(e, SystemExit):
|
||||||
|
# propagate SystemExit further
|
||||||
|
raise
|
||||||
|
teardown()
|
||||||
|
return not isinstance(e, KeyboardInterrupt)
|
||||||
|
|
||||||
|
def create_dispatcher(self, ping_timeout):
|
||||||
|
timeout = ping_timeout or 10
|
||||||
|
if self.sock.is_ssl():
|
||||||
|
return SSLDispatcher(self, timeout)
|
||||||
|
|
||||||
|
return Dispatcher(self, timeout)
|
||||||
|
|
||||||
|
def _get_close_args(self, data):
|
||||||
|
"""
|
||||||
|
_get_close_args extracts the code, reason from the close body
|
||||||
|
if they exists, and if the self.on_close except three arguments
|
||||||
|
"""
|
||||||
|
# if the on_close callback is "old", just return empty list
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
if not self.on_close or len(inspect.getargspec(self.on_close).args) != 3:
|
||||||
|
return []
|
||||||
|
else:
|
||||||
|
if not self.on_close or len(inspect.getfullargspec(self.on_close).args) != 3:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if data and len(data) >= 2:
|
||||||
|
code = 256 * six.byte2int(data[0:1]) + six.byte2int(data[1:2])
|
||||||
|
reason = data[2:].decode('utf-8')
|
||||||
|
return [code, reason]
|
||||||
|
|
||||||
|
return [None, None]
|
||||||
|
|
||||||
|
def _callback(self, callback, *args):
|
||||||
|
if callback:
|
||||||
|
try:
|
||||||
|
callback(self, *args)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
_logging.error("error from callback {}: {}".format(callback, e))
|
||||||
|
if _logging.isEnabledForDebug():
|
||||||
|
_, _, tb = sys.exc_info()
|
||||||
|
traceback.print_tb(tb)
|
78
resources/lib/websocket/_cookiejar.py
Normal file
78
resources/lib/websocket/_cookiejar.py
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import Cookie
|
||||||
|
except:
|
||||||
|
import http.cookies as Cookie
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleCookieJar(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.jar = dict()
|
||||||
|
|
||||||
|
def add(self, set_cookie):
|
||||||
|
if set_cookie:
|
||||||
|
try:
|
||||||
|
simpleCookie = Cookie.SimpleCookie(set_cookie)
|
||||||
|
except:
|
||||||
|
simpleCookie = Cookie.SimpleCookie(set_cookie.encode('ascii', 'ignore'))
|
||||||
|
|
||||||
|
for k, v in simpleCookie.items():
|
||||||
|
domain = v.get("domain")
|
||||||
|
if domain:
|
||||||
|
if not domain.startswith("."):
|
||||||
|
domain = "." + domain
|
||||||
|
cookie = self.jar.get(domain) if self.jar.get(domain) else Cookie.SimpleCookie()
|
||||||
|
cookie.update(simpleCookie)
|
||||||
|
self.jar[domain.lower()] = cookie
|
||||||
|
|
||||||
|
def set(self, set_cookie):
|
||||||
|
if set_cookie:
|
||||||
|
try:
|
||||||
|
simpleCookie = Cookie.SimpleCookie(set_cookie)
|
||||||
|
except:
|
||||||
|
simpleCookie = Cookie.SimpleCookie(set_cookie.encode('ascii', 'ignore'))
|
||||||
|
|
||||||
|
for k, v in simpleCookie.items():
|
||||||
|
domain = v.get("domain")
|
||||||
|
if domain:
|
||||||
|
if not domain.startswith("."):
|
||||||
|
domain = "." + domain
|
||||||
|
self.jar[domain.lower()] = simpleCookie
|
||||||
|
|
||||||
|
def get(self, host):
|
||||||
|
if not host:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
cookies = []
|
||||||
|
for domain, simpleCookie in self.jar.items():
|
||||||
|
host = host.lower()
|
||||||
|
if host.endswith(domain) or host == domain[1:]:
|
||||||
|
cookies.append(self.jar.get(domain))
|
||||||
|
|
||||||
|
return "; ".join(filter(
|
||||||
|
None, sorted(
|
||||||
|
["%s=%s" % (k, v.value) for cookie in filter(None, cookies) for k, v in cookie.items()]
|
||||||
|
)))
|
598
resources/lib/websocket/_core.py
Normal file
598
resources/lib/websocket/_core.py
Normal file
|
@ -0,0 +1,598 @@
|
||||||
|
from __future__ import print_function
|
||||||
|
"""
|
||||||
|
_core.py
|
||||||
|
====================================
|
||||||
|
WebSocket Python client
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
# websocket modules
|
||||||
|
from ._abnf import *
|
||||||
|
from ._exceptions import *
|
||||||
|
from ._handshake import *
|
||||||
|
from ._http import *
|
||||||
|
from ._logging import *
|
||||||
|
from ._socket import *
|
||||||
|
from ._ssl_compat import *
|
||||||
|
from ._utils import *
|
||||||
|
|
||||||
|
__all__ = ['WebSocket', 'create_connection']
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocket(object):
|
||||||
|
"""
|
||||||
|
Low level WebSocket interface.
|
||||||
|
|
||||||
|
This class is based on the WebSocket protocol `draft-hixie-thewebsocketprotocol-76 <http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76>`_
|
||||||
|
|
||||||
|
We can connect to the websocket server and send/receive data.
|
||||||
|
The following example is an echo client.
|
||||||
|
|
||||||
|
>>> import websocket
|
||||||
|
>>> ws = websocket.WebSocket()
|
||||||
|
>>> ws.connect("ws://echo.websocket.org")
|
||||||
|
>>> ws.send("Hello, Server")
|
||||||
|
>>> ws.recv()
|
||||||
|
'Hello, Server'
|
||||||
|
>>> ws.close()
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
get_mask_key: func
|
||||||
|
a callable to produce new mask keys, see the set_mask_key
|
||||||
|
function's docstring for more details
|
||||||
|
sockopt: tuple
|
||||||
|
values for socket.setsockopt.
|
||||||
|
sockopt must be tuple and each element is argument of sock.setsockopt.
|
||||||
|
sslopt: dict
|
||||||
|
optional dict object for ssl socket option.
|
||||||
|
fire_cont_frame: bool
|
||||||
|
fire recv event for each cont frame. default is False
|
||||||
|
enable_multithread: bool
|
||||||
|
if set to True, lock send method.
|
||||||
|
skip_utf8_validation: bool
|
||||||
|
skip utf8 validation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, get_mask_key=None, sockopt=None, sslopt=None,
|
||||||
|
fire_cont_frame=False, enable_multithread=False,
|
||||||
|
skip_utf8_validation=False, **_):
|
||||||
|
"""
|
||||||
|
Initialize WebSocket object.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
sslopt: specify ssl certification verification options
|
||||||
|
"""
|
||||||
|
self.sock_opt = sock_opt(sockopt, sslopt)
|
||||||
|
self.handshake_response = None
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
self.connected = False
|
||||||
|
self.get_mask_key = get_mask_key
|
||||||
|
# These buffer over the build-up of a single frame.
|
||||||
|
self.frame_buffer = frame_buffer(self._recv, skip_utf8_validation)
|
||||||
|
self.cont_frame = continuous_frame(
|
||||||
|
fire_cont_frame, skip_utf8_validation)
|
||||||
|
|
||||||
|
if enable_multithread:
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
self.readlock = threading.Lock()
|
||||||
|
else:
|
||||||
|
self.lock = NoLock()
|
||||||
|
self.readlock = NoLock()
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
"""
|
||||||
|
Allow iteration over websocket, implying sequential `recv` executions.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
yield self.recv()
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
return self.recv()
|
||||||
|
|
||||||
|
def next(self):
|
||||||
|
return self.__next__()
|
||||||
|
|
||||||
|
def fileno(self):
|
||||||
|
return self.sock.fileno()
|
||||||
|
|
||||||
|
def set_mask_key(self, func):
|
||||||
|
"""
|
||||||
|
Set function to create mask key. You can customize mask key generator.
|
||||||
|
Mainly, this is for testing purpose.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
func: func
|
||||||
|
callable object. the func takes 1 argument as integer.
|
||||||
|
The argument means length of mask key.
|
||||||
|
This func must return string(byte array),
|
||||||
|
which length is argument specified.
|
||||||
|
"""
|
||||||
|
self.get_mask_key = func
|
||||||
|
|
||||||
|
def gettimeout(self):
|
||||||
|
"""
|
||||||
|
Get the websocket timeout (in seconds) as an int or float
|
||||||
|
|
||||||
|
Returns
|
||||||
|
----------
|
||||||
|
timeout: int or float
|
||||||
|
returns timeout value (in seconds). This value could be either float/integer.
|
||||||
|
"""
|
||||||
|
return self.sock_opt.timeout
|
||||||
|
|
||||||
|
def settimeout(self, timeout):
|
||||||
|
"""
|
||||||
|
Set the timeout to the websocket.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
timeout: int or float
|
||||||
|
timeout time (in seconds). This value could be either float/integer.
|
||||||
|
"""
|
||||||
|
self.sock_opt.timeout = timeout
|
||||||
|
if self.sock:
|
||||||
|
self.sock.settimeout(timeout)
|
||||||
|
|
||||||
|
timeout = property(gettimeout, settimeout)
|
||||||
|
|
||||||
|
def getsubprotocol(self):
|
||||||
|
"""
|
||||||
|
Get subprotocol
|
||||||
|
"""
|
||||||
|
if self.handshake_response:
|
||||||
|
return self.handshake_response.subprotocol
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
subprotocol = property(getsubprotocol)
|
||||||
|
|
||||||
|
def getstatus(self):
|
||||||
|
"""
|
||||||
|
Get handshake status
|
||||||
|
"""
|
||||||
|
if self.handshake_response:
|
||||||
|
return self.handshake_response.status
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
status = property(getstatus)
|
||||||
|
|
||||||
|
def getheaders(self):
|
||||||
|
"""
|
||||||
|
Get handshake response header
|
||||||
|
"""
|
||||||
|
if self.handshake_response:
|
||||||
|
return self.handshake_response.headers
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def is_ssl(self):
|
||||||
|
try:
|
||||||
|
return isinstance(self.sock, ssl.SSLSocket)
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
headers = property(getheaders)
|
||||||
|
|
||||||
|
def connect(self, url, **options):
|
||||||
|
"""
|
||||||
|
Connect to url. url is websocket url scheme.
|
||||||
|
ie. ws://host:port/resource
|
||||||
|
You can customize using 'options'.
|
||||||
|
If you set "header" list object, you can set your own custom header.
|
||||||
|
|
||||||
|
>>> ws = WebSocket()
|
||||||
|
>>> ws.connect("ws://echo.websocket.org/",
|
||||||
|
... header=["User-Agent: MyProgram",
|
||||||
|
... "x-custom: header"])
|
||||||
|
|
||||||
|
timeout: <type>
|
||||||
|
socket timeout time. This value is an integer or float.
|
||||||
|
if you set None for this value, it means "use default_timeout value"
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
options:
|
||||||
|
- header: list or dict
|
||||||
|
custom http header list or dict.
|
||||||
|
- cookie: str
|
||||||
|
cookie value.
|
||||||
|
- origin: str
|
||||||
|
custom origin url.
|
||||||
|
- suppress_origin: bool
|
||||||
|
suppress outputting origin header.
|
||||||
|
- host: str
|
||||||
|
custom host header string.
|
||||||
|
- http_proxy_host: <type>
|
||||||
|
http proxy host name.
|
||||||
|
- http_proxy_port: <type>
|
||||||
|
http proxy port. If not set, set to 80.
|
||||||
|
- http_no_proxy: <type>
|
||||||
|
host names, which doesn't use proxy.
|
||||||
|
- http_proxy_auth: <type>
|
||||||
|
http proxy auth information. tuple of username and password. default is None
|
||||||
|
- redirect_limit: <type>
|
||||||
|
number of redirects to follow.
|
||||||
|
- subprotocols: <type>
|
||||||
|
array of available sub protocols. default is None.
|
||||||
|
- socket: <type>
|
||||||
|
pre-initialized stream socket.
|
||||||
|
"""
|
||||||
|
self.sock_opt.timeout = options.get('timeout', self.sock_opt.timeout)
|
||||||
|
self.sock, addrs = connect(url, self.sock_opt, proxy_info(**options),
|
||||||
|
options.pop('socket', None))
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.handshake_response = handshake(self.sock, *addrs, **options)
|
||||||
|
for attempt in range(options.pop('redirect_limit', 3)):
|
||||||
|
if self.handshake_response.status in SUPPORTED_REDIRECT_STATUSES:
|
||||||
|
url = self.handshake_response.headers['location']
|
||||||
|
self.sock.close()
|
||||||
|
self.sock, addrs = connect(url, self.sock_opt, proxy_info(**options),
|
||||||
|
options.pop('socket', None))
|
||||||
|
self.handshake_response = handshake(self.sock, *addrs, **options)
|
||||||
|
self.connected = True
|
||||||
|
except:
|
||||||
|
if self.sock:
|
||||||
|
self.sock.close()
|
||||||
|
self.sock = None
|
||||||
|
raise
|
||||||
|
|
||||||
|
def send(self, payload, opcode=ABNF.OPCODE_TEXT):
|
||||||
|
"""
|
||||||
|
Send the data as string.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
payload: <type>
|
||||||
|
Payload must be utf-8 string or unicode,
|
||||||
|
if the opcode is OPCODE_TEXT.
|
||||||
|
Otherwise, it must be string(byte array)
|
||||||
|
opcode: <type>
|
||||||
|
operation code to send. Please see OPCODE_XXX.
|
||||||
|
"""
|
||||||
|
|
||||||
|
frame = ABNF.create_frame(payload, opcode)
|
||||||
|
return self.send_frame(frame)
|
||||||
|
|
||||||
|
def send_frame(self, frame):
|
||||||
|
"""
|
||||||
|
Send the data frame.
|
||||||
|
|
||||||
|
>>> ws = create_connection("ws://echo.websocket.org/")
|
||||||
|
>>> frame = ABNF.create_frame("Hello", ABNF.OPCODE_TEXT)
|
||||||
|
>>> ws.send_frame(frame)
|
||||||
|
>>> cont_frame = ABNF.create_frame("My name is ", ABNF.OPCODE_CONT, 0)
|
||||||
|
>>> ws.send_frame(frame)
|
||||||
|
>>> cont_frame = ABNF.create_frame("Foo Bar", ABNF.OPCODE_CONT, 1)
|
||||||
|
>>> ws.send_frame(frame)
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
frame: <type>
|
||||||
|
frame data created by ABNF.create_frame
|
||||||
|
"""
|
||||||
|
if self.get_mask_key:
|
||||||
|
frame.get_mask_key = self.get_mask_key
|
||||||
|
data = frame.format()
|
||||||
|
length = len(data)
|
||||||
|
if (isEnabledForTrace()):
|
||||||
|
trace("send: " + repr(data))
|
||||||
|
|
||||||
|
with self.lock:
|
||||||
|
while data:
|
||||||
|
l = self._send(data)
|
||||||
|
data = data[l:]
|
||||||
|
|
||||||
|
return length
|
||||||
|
|
||||||
|
def send_binary(self, payload):
|
||||||
|
return self.send(payload, ABNF.OPCODE_BINARY)
|
||||||
|
|
||||||
|
def ping(self, payload=""):
|
||||||
|
"""
|
||||||
|
Send ping data.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
payload: <type>
|
||||||
|
data payload to send server.
|
||||||
|
"""
|
||||||
|
if isinstance(payload, six.text_type):
|
||||||
|
payload = payload.encode("utf-8")
|
||||||
|
self.send(payload, ABNF.OPCODE_PING)
|
||||||
|
|
||||||
|
def pong(self, payload=""):
|
||||||
|
"""
|
||||||
|
Send pong data.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
payload: <type>
|
||||||
|
data payload to send server.
|
||||||
|
"""
|
||||||
|
if isinstance(payload, six.text_type):
|
||||||
|
payload = payload.encode("utf-8")
|
||||||
|
self.send(payload, ABNF.OPCODE_PONG)
|
||||||
|
|
||||||
|
def recv(self):
|
||||||
|
"""
|
||||||
|
Receive string data(byte array) from the server.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
----------
|
||||||
|
data: string (byte array) value.
|
||||||
|
"""
|
||||||
|
with self.readlock:
|
||||||
|
opcode, data = self.recv_data()
|
||||||
|
if six.PY3 and opcode == ABNF.OPCODE_TEXT:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
elif opcode == ABNF.OPCODE_TEXT or opcode == ABNF.OPCODE_BINARY:
|
||||||
|
return data
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def recv_data(self, control_frame=False):
|
||||||
|
"""
|
||||||
|
Receive data with operation code.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
control_frame: bool
|
||||||
|
a boolean flag indicating whether to return control frame
|
||||||
|
data, defaults to False
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
opcode, frame.data: tuple
|
||||||
|
tuple of operation code and string(byte array) value.
|
||||||
|
"""
|
||||||
|
opcode, frame = self.recv_data_frame(control_frame)
|
||||||
|
return opcode, frame.data
|
||||||
|
|
||||||
|
def recv_data_frame(self, control_frame=False):
|
||||||
|
"""
|
||||||
|
Receive data with operation code.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
control_frame: bool
|
||||||
|
a boolean flag indicating whether to return control frame
|
||||||
|
data, defaults to False
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
frame.opcode, frame: tuple
|
||||||
|
tuple of operation code and string(byte array) value.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
frame = self.recv_frame()
|
||||||
|
if not frame:
|
||||||
|
# handle error:
|
||||||
|
# 'NoneType' object has no attribute 'opcode'
|
||||||
|
raise WebSocketProtocolException(
|
||||||
|
"Not a valid frame %s" % frame)
|
||||||
|
elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY, ABNF.OPCODE_CONT):
|
||||||
|
self.cont_frame.validate(frame)
|
||||||
|
self.cont_frame.add(frame)
|
||||||
|
|
||||||
|
if self.cont_frame.is_fire(frame):
|
||||||
|
return self.cont_frame.extract(frame)
|
||||||
|
|
||||||
|
elif frame.opcode == ABNF.OPCODE_CLOSE:
|
||||||
|
self.send_close()
|
||||||
|
return frame.opcode, frame
|
||||||
|
elif frame.opcode == ABNF.OPCODE_PING:
|
||||||
|
if len(frame.data) < 126:
|
||||||
|
self.pong(frame.data)
|
||||||
|
else:
|
||||||
|
raise WebSocketProtocolException(
|
||||||
|
"Ping message is too long")
|
||||||
|
if control_frame:
|
||||||
|
return frame.opcode, frame
|
||||||
|
elif frame.opcode == ABNF.OPCODE_PONG:
|
||||||
|
if control_frame:
|
||||||
|
return frame.opcode, frame
|
||||||
|
|
||||||
|
def recv_frame(self):
|
||||||
|
"""
|
||||||
|
Receive data as frame from server.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
self.frame_buffer.recv_frame(): ABNF frame object
|
||||||
|
"""
|
||||||
|
return self.frame_buffer.recv_frame()
|
||||||
|
|
||||||
|
def send_close(self, status=STATUS_NORMAL, reason=six.b("")):
|
||||||
|
"""
|
||||||
|
Send close data to the server.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
status: <type>
|
||||||
|
status code to send. see STATUS_XXX.
|
||||||
|
reason: str or bytes
|
||||||
|
the reason to close. This must be string or bytes.
|
||||||
|
"""
|
||||||
|
if status < 0 or status >= ABNF.LENGTH_16:
|
||||||
|
raise ValueError("code is invalid range")
|
||||||
|
self.connected = False
|
||||||
|
self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE)
|
||||||
|
|
||||||
|
def close(self, status=STATUS_NORMAL, reason=six.b(""), timeout=3):
|
||||||
|
"""
|
||||||
|
Close Websocket object
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
status: <type>
|
||||||
|
status code to send. see STATUS_XXX.
|
||||||
|
reason: <type>
|
||||||
|
the reason to close. This must be string.
|
||||||
|
timeout: int or float
|
||||||
|
timeout until receive a close frame.
|
||||||
|
If None, it will wait forever until receive a close frame.
|
||||||
|
"""
|
||||||
|
if self.connected:
|
||||||
|
if status < 0 or status >= ABNF.LENGTH_16:
|
||||||
|
raise ValueError("code is invalid range")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.connected = False
|
||||||
|
self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE)
|
||||||
|
sock_timeout = self.sock.gettimeout()
|
||||||
|
self.sock.settimeout(timeout)
|
||||||
|
start_time = time.time()
|
||||||
|
while timeout is None or time.time() - start_time < timeout:
|
||||||
|
try:
|
||||||
|
frame = self.recv_frame()
|
||||||
|
if frame.opcode != ABNF.OPCODE_CLOSE:
|
||||||
|
continue
|
||||||
|
if isEnabledForError():
|
||||||
|
recv_status = struct.unpack("!H", frame.data[0:2])[0]
|
||||||
|
if recv_status >= 3000 and recv_status <= 4999:
|
||||||
|
debug("close status: " + repr(recv_status))
|
||||||
|
elif recv_status != STATUS_NORMAL:
|
||||||
|
error("close status: " + repr(recv_status))
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
break
|
||||||
|
self.sock.settimeout(sock_timeout)
|
||||||
|
self.sock.shutdown(socket.SHUT_RDWR)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.shutdown()
|
||||||
|
|
||||||
|
def abort(self):
|
||||||
|
"""
|
||||||
|
Low-level asynchronous abort, wakes up other threads that are waiting in recv_*
|
||||||
|
"""
|
||||||
|
if self.connected:
|
||||||
|
self.sock.shutdown(socket.SHUT_RDWR)
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
"""
|
||||||
|
close socket, immediately.
|
||||||
|
"""
|
||||||
|
if self.sock:
|
||||||
|
self.sock.close()
|
||||||
|
self.sock = None
|
||||||
|
self.connected = False
|
||||||
|
|
||||||
|
def _send(self, data):
|
||||||
|
return send(self.sock, data)
|
||||||
|
|
||||||
|
def _recv(self, bufsize):
|
||||||
|
try:
|
||||||
|
return recv(self.sock, bufsize)
|
||||||
|
except WebSocketConnectionClosedException:
|
||||||
|
if self.sock:
|
||||||
|
self.sock.close()
|
||||||
|
self.sock = None
|
||||||
|
self.connected = False
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def create_connection(url, timeout=None, class_=WebSocket, **options):
|
||||||
|
"""
|
||||||
|
Connect to url and return websocket object.
|
||||||
|
|
||||||
|
Connect to url and return the WebSocket object.
|
||||||
|
Passing optional timeout parameter will set the timeout on the socket.
|
||||||
|
If no timeout is supplied,
|
||||||
|
the global default timeout setting returned by getdefaulttimeout() is used.
|
||||||
|
You can customize using 'options'.
|
||||||
|
If you set "header" list object, you can set your own custom header.
|
||||||
|
|
||||||
|
>>> conn = create_connection("ws://echo.websocket.org/",
|
||||||
|
... header=["User-Agent: MyProgram",
|
||||||
|
... "x-custom: header"])
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
timeout: int or float
|
||||||
|
socket timeout time. This value could be either float/integer.
|
||||||
|
if you set None for this value,
|
||||||
|
it means "use default_timeout value"
|
||||||
|
class_: <type>
|
||||||
|
class to instantiate when creating the connection. It has to implement
|
||||||
|
settimeout and connect. It's __init__ should be compatible with
|
||||||
|
WebSocket.__init__, i.e. accept all of it's kwargs.
|
||||||
|
options: <type>
|
||||||
|
- header: list or dict
|
||||||
|
custom http header list or dict.
|
||||||
|
- cookie: str
|
||||||
|
cookie value.
|
||||||
|
- origin: str
|
||||||
|
custom origin url.
|
||||||
|
- suppress_origin: bool
|
||||||
|
suppress outputting origin header.
|
||||||
|
- host: <type>
|
||||||
|
custom host header string.
|
||||||
|
- http_proxy_host: <type>
|
||||||
|
http proxy host name.
|
||||||
|
- http_proxy_port: <type>
|
||||||
|
http proxy port. If not set, set to 80.
|
||||||
|
- http_no_proxy: <type>
|
||||||
|
host names, which doesn't use proxy.
|
||||||
|
- http_proxy_auth: <type>
|
||||||
|
http proxy auth information. tuple of username and password. default is None
|
||||||
|
- enable_multithread: bool
|
||||||
|
enable lock for multithread.
|
||||||
|
- redirect_limit: <type>
|
||||||
|
number of redirects to follow.
|
||||||
|
- sockopt: <type>
|
||||||
|
socket options
|
||||||
|
- sslopt: <type>
|
||||||
|
ssl option
|
||||||
|
- subprotocols: <type>
|
||||||
|
array of available sub protocols. default is None.
|
||||||
|
- skip_utf8_validation: bool
|
||||||
|
skip utf8 validation.
|
||||||
|
- socket: <type>
|
||||||
|
pre-initialized stream socket.
|
||||||
|
"""
|
||||||
|
sockopt = options.pop("sockopt", [])
|
||||||
|
sslopt = options.pop("sslopt", {})
|
||||||
|
fire_cont_frame = options.pop("fire_cont_frame", False)
|
||||||
|
enable_multithread = options.pop("enable_multithread", False)
|
||||||
|
skip_utf8_validation = options.pop("skip_utf8_validation", False)
|
||||||
|
websock = class_(sockopt=sockopt, sslopt=sslopt,
|
||||||
|
fire_cont_frame=fire_cont_frame,
|
||||||
|
enable_multithread=enable_multithread,
|
||||||
|
skip_utf8_validation=skip_utf8_validation, **options)
|
||||||
|
websock.settimeout(timeout if timeout is not None else getdefaulttimeout())
|
||||||
|
websock.connect(url, **options)
|
||||||
|
return websock
|
86
resources/lib/websocket/_exceptions.py
Normal file
86
resources/lib/websocket/_exceptions.py
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
"""
|
||||||
|
Define WebSocket exceptions
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketException(Exception):
|
||||||
|
"""
|
||||||
|
WebSocket exception class.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketProtocolException(WebSocketException):
|
||||||
|
"""
|
||||||
|
If the WebSocket protocol is invalid, this exception will be raised.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketPayloadException(WebSocketException):
|
||||||
|
"""
|
||||||
|
If the WebSocket payload is invalid, this exception will be raised.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketConnectionClosedException(WebSocketException):
|
||||||
|
"""
|
||||||
|
If remote host closed the connection or some network error happened,
|
||||||
|
this exception will be raised.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketTimeoutException(WebSocketException):
|
||||||
|
"""
|
||||||
|
WebSocketTimeoutException will be raised at socket timeout during read/write data.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketProxyException(WebSocketException):
|
||||||
|
"""
|
||||||
|
WebSocketProxyException will be raised when proxy error occurred.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketBadStatusException(WebSocketException):
|
||||||
|
"""
|
||||||
|
WebSocketBadStatusException will be raised when we get bad handshake status code.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, message, status_code, status_message=None, resp_headers=None):
|
||||||
|
msg = message % (status_code, status_message)
|
||||||
|
super(WebSocketBadStatusException, self).__init__(msg)
|
||||||
|
self.status_code = status_code
|
||||||
|
self.resp_headers = resp_headers
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketAddressException(WebSocketException):
|
||||||
|
"""
|
||||||
|
If the websocket address info cannot be found, this exception will be raised.
|
||||||
|
"""
|
||||||
|
pass
|
212
resources/lib/websocket/_handshake.py
Normal file
212
resources/lib/websocket/_handshake.py
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import os
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from ._cookiejar import SimpleCookieJar
|
||||||
|
from ._exceptions import *
|
||||||
|
from ._http import *
|
||||||
|
from ._logging import *
|
||||||
|
from ._socket import *
|
||||||
|
|
||||||
|
if hasattr(six, 'PY3') and six.PY3:
|
||||||
|
from base64 import encodebytes as base64encode
|
||||||
|
else:
|
||||||
|
from base64 import encodestring as base64encode
|
||||||
|
|
||||||
|
if hasattr(six, 'PY3') and six.PY3:
|
||||||
|
if hasattr(six, 'PY34') and six.PY34:
|
||||||
|
from http import client as HTTPStatus
|
||||||
|
else:
|
||||||
|
from http import HTTPStatus
|
||||||
|
else:
|
||||||
|
import httplib as HTTPStatus
|
||||||
|
|
||||||
|
__all__ = ["handshake_response", "handshake", "SUPPORTED_REDIRECT_STATUSES"]
|
||||||
|
|
||||||
|
if hasattr(hmac, "compare_digest"):
|
||||||
|
compare_digest = hmac.compare_digest
|
||||||
|
else:
|
||||||
|
def compare_digest(s1, s2):
|
||||||
|
return s1 == s2
|
||||||
|
|
||||||
|
# websocket supported version.
|
||||||
|
VERSION = 13
|
||||||
|
|
||||||
|
SUPPORTED_REDIRECT_STATUSES = (HTTPStatus.MOVED_PERMANENTLY, HTTPStatus.FOUND, HTTPStatus.SEE_OTHER,)
|
||||||
|
SUCCESS_STATUSES = SUPPORTED_REDIRECT_STATUSES + (HTTPStatus.SWITCHING_PROTOCOLS,)
|
||||||
|
|
||||||
|
CookieJar = SimpleCookieJar()
|
||||||
|
|
||||||
|
|
||||||
|
class handshake_response(object):
|
||||||
|
|
||||||
|
def __init__(self, status, headers, subprotocol):
|
||||||
|
self.status = status
|
||||||
|
self.headers = headers
|
||||||
|
self.subprotocol = subprotocol
|
||||||
|
CookieJar.add(headers.get("set-cookie"))
|
||||||
|
|
||||||
|
|
||||||
|
def handshake(sock, hostname, port, resource, **options):
|
||||||
|
headers, key = _get_handshake_headers(resource, hostname, port, options)
|
||||||
|
|
||||||
|
header_str = "\r\n".join(headers)
|
||||||
|
send(sock, header_str)
|
||||||
|
dump("request header", header_str)
|
||||||
|
|
||||||
|
status, resp = _get_resp_headers(sock)
|
||||||
|
if status in SUPPORTED_REDIRECT_STATUSES:
|
||||||
|
return handshake_response(status, resp, None)
|
||||||
|
success, subproto = _validate(resp, key, options.get("subprotocols"))
|
||||||
|
if not success:
|
||||||
|
raise WebSocketException("Invalid WebSocket Header")
|
||||||
|
|
||||||
|
return handshake_response(status, resp, subproto)
|
||||||
|
|
||||||
|
|
||||||
|
def _pack_hostname(hostname):
|
||||||
|
# IPv6 address
|
||||||
|
if ':' in hostname:
|
||||||
|
return '[' + hostname + ']'
|
||||||
|
|
||||||
|
return hostname
|
||||||
|
|
||||||
|
|
||||||
|
def _get_handshake_headers(resource, host, port, options):
|
||||||
|
headers = [
|
||||||
|
"GET %s HTTP/1.1" % resource,
|
||||||
|
"Upgrade: websocket"
|
||||||
|
]
|
||||||
|
if port == 80 or port == 443:
|
||||||
|
hostport = _pack_hostname(host)
|
||||||
|
else:
|
||||||
|
hostport = "%s:%d" % (_pack_hostname(host), port)
|
||||||
|
if "host" in options and options["host"] is not None:
|
||||||
|
headers.append("Host: %s" % options["host"])
|
||||||
|
else:
|
||||||
|
headers.append("Host: %s" % hostport)
|
||||||
|
|
||||||
|
if "suppress_origin" not in options or not options["suppress_origin"]:
|
||||||
|
if "origin" in options and options["origin"] is not None:
|
||||||
|
headers.append("Origin: %s" % options["origin"])
|
||||||
|
else:
|
||||||
|
headers.append("Origin: http://%s" % hostport)
|
||||||
|
|
||||||
|
key = _create_sec_websocket_key()
|
||||||
|
|
||||||
|
# Append Sec-WebSocket-Key & Sec-WebSocket-Version if not manually specified
|
||||||
|
if 'header' not in options or 'Sec-WebSocket-Key' not in options['header']:
|
||||||
|
key = _create_sec_websocket_key()
|
||||||
|
headers.append("Sec-WebSocket-Key: %s" % key)
|
||||||
|
else:
|
||||||
|
key = options['header']['Sec-WebSocket-Key']
|
||||||
|
|
||||||
|
if 'header' not in options or 'Sec-WebSocket-Version' not in options['header']:
|
||||||
|
headers.append("Sec-WebSocket-Version: %s" % VERSION)
|
||||||
|
|
||||||
|
if 'connection' not in options or options['connection'] is None:
|
||||||
|
headers.append('Connection: Upgrade')
|
||||||
|
else:
|
||||||
|
headers.append(options['connection'])
|
||||||
|
|
||||||
|
subprotocols = options.get("subprotocols")
|
||||||
|
if subprotocols:
|
||||||
|
headers.append("Sec-WebSocket-Protocol: %s" % ",".join(subprotocols))
|
||||||
|
|
||||||
|
if "header" in options:
|
||||||
|
header = options["header"]
|
||||||
|
if isinstance(header, dict):
|
||||||
|
header = [
|
||||||
|
": ".join([k, v])
|
||||||
|
for k, v in header.items()
|
||||||
|
if v is not None
|
||||||
|
]
|
||||||
|
headers.extend(header)
|
||||||
|
|
||||||
|
server_cookie = CookieJar.get(host)
|
||||||
|
client_cookie = options.get("cookie", None)
|
||||||
|
|
||||||
|
cookie = "; ".join(filter(None, [server_cookie, client_cookie]))
|
||||||
|
|
||||||
|
if cookie:
|
||||||
|
headers.append("Cookie: %s" % cookie)
|
||||||
|
|
||||||
|
headers.append("")
|
||||||
|
headers.append("")
|
||||||
|
|
||||||
|
return headers, key
|
||||||
|
|
||||||
|
|
||||||
|
def _get_resp_headers(sock, success_statuses=SUCCESS_STATUSES):
|
||||||
|
status, resp_headers, status_message = read_headers(sock)
|
||||||
|
if status not in success_statuses:
|
||||||
|
raise WebSocketBadStatusException("Handshake status %d %s", status, status_message, resp_headers)
|
||||||
|
return status, resp_headers
|
||||||
|
|
||||||
|
|
||||||
|
_HEADERS_TO_CHECK = {
|
||||||
|
"upgrade": "websocket",
|
||||||
|
"connection": "upgrade",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _validate(headers, key, subprotocols):
|
||||||
|
subproto = None
|
||||||
|
for k, v in _HEADERS_TO_CHECK.items():
|
||||||
|
r = headers.get(k, None)
|
||||||
|
if not r:
|
||||||
|
return False, None
|
||||||
|
r = [x.strip().lower() for x in r.split(',')]
|
||||||
|
if v not in r:
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
if subprotocols:
|
||||||
|
subproto = headers.get("sec-websocket-protocol", None)
|
||||||
|
if not subproto or subproto.lower() not in [s.lower() for s in subprotocols]:
|
||||||
|
error("Invalid subprotocol: " + str(subprotocols))
|
||||||
|
return False, None
|
||||||
|
subproto = subproto.lower()
|
||||||
|
|
||||||
|
result = headers.get("sec-websocket-accept", None)
|
||||||
|
if not result:
|
||||||
|
return False, None
|
||||||
|
result = result.lower()
|
||||||
|
|
||||||
|
if isinstance(result, six.text_type):
|
||||||
|
result = result.encode('utf-8')
|
||||||
|
|
||||||
|
value = (key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode('utf-8')
|
||||||
|
hashed = base64encode(hashlib.sha1(value).digest()).strip().lower()
|
||||||
|
success = compare_digest(hashed, result)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return True, subproto
|
||||||
|
else:
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
|
def _create_sec_websocket_key():
|
||||||
|
randomness = os.urandom(16)
|
||||||
|
return base64encode(randomness).decode('utf-8').strip()
|
335
resources/lib/websocket/_http.py
Normal file
335
resources/lib/websocket/_http.py
Normal file
|
@ -0,0 +1,335 @@
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
import errno
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from ._exceptions import *
|
||||||
|
from ._logging import *
|
||||||
|
from ._socket import*
|
||||||
|
from ._ssl_compat import *
|
||||||
|
from ._url import *
|
||||||
|
|
||||||
|
if six.PY3:
|
||||||
|
from base64 import encodebytes as base64encode
|
||||||
|
else:
|
||||||
|
from base64 import encodestring as base64encode
|
||||||
|
|
||||||
|
__all__ = ["proxy_info", "connect", "read_headers"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
import socks
|
||||||
|
ProxyConnectionError = socks.ProxyConnectionError
|
||||||
|
HAS_PYSOCKS = True
|
||||||
|
except:
|
||||||
|
class ProxyConnectionError(BaseException):
|
||||||
|
pass
|
||||||
|
HAS_PYSOCKS = False
|
||||||
|
|
||||||
|
|
||||||
|
class proxy_info(object):
|
||||||
|
|
||||||
|
def __init__(self, **options):
|
||||||
|
self.type = options.get("proxy_type") or "http"
|
||||||
|
if not(self.type in ['http', 'socks4', 'socks5', 'socks5h']):
|
||||||
|
raise ValueError("proxy_type must be 'http', 'socks4', 'socks5' or 'socks5h'")
|
||||||
|
self.host = options.get("http_proxy_host", None)
|
||||||
|
if self.host:
|
||||||
|
self.port = options.get("http_proxy_port", 0)
|
||||||
|
self.auth = options.get("http_proxy_auth", None)
|
||||||
|
self.no_proxy = options.get("http_no_proxy", None)
|
||||||
|
else:
|
||||||
|
self.port = 0
|
||||||
|
self.auth = None
|
||||||
|
self.no_proxy = None
|
||||||
|
|
||||||
|
|
||||||
|
def _open_proxied_socket(url, options, proxy):
|
||||||
|
hostname, port, resource, is_secure = parse_url(url)
|
||||||
|
|
||||||
|
if not HAS_PYSOCKS:
|
||||||
|
raise WebSocketException("PySocks module not found.")
|
||||||
|
|
||||||
|
ptype = socks.SOCKS5
|
||||||
|
rdns = False
|
||||||
|
if proxy.type == "socks4":
|
||||||
|
ptype = socks.SOCKS4
|
||||||
|
if proxy.type == "http":
|
||||||
|
ptype = socks.HTTP
|
||||||
|
if proxy.type[-1] == "h":
|
||||||
|
rdns = True
|
||||||
|
|
||||||
|
sock = socks.create_connection(
|
||||||
|
(hostname, port),
|
||||||
|
proxy_type=ptype,
|
||||||
|
proxy_addr=proxy.host,
|
||||||
|
proxy_port=proxy.port,
|
||||||
|
proxy_rdns=rdns,
|
||||||
|
proxy_username=proxy.auth[0] if proxy.auth else None,
|
||||||
|
proxy_password=proxy.auth[1] if proxy.auth else None,
|
||||||
|
timeout=options.timeout,
|
||||||
|
socket_options=DEFAULT_SOCKET_OPTION + options.sockopt
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_secure:
|
||||||
|
if HAVE_SSL:
|
||||||
|
sock = _ssl_socket(sock, options.sslopt, hostname)
|
||||||
|
else:
|
||||||
|
raise WebSocketException("SSL not available.")
|
||||||
|
|
||||||
|
return sock, (hostname, port, resource)
|
||||||
|
|
||||||
|
|
||||||
|
def connect(url, options, proxy, socket):
|
||||||
|
if proxy.host and not socket and not (proxy.type == 'http'):
|
||||||
|
return _open_proxied_socket(url, options, proxy)
|
||||||
|
|
||||||
|
hostname, port, resource, is_secure = parse_url(url)
|
||||||
|
|
||||||
|
if socket:
|
||||||
|
return socket, (hostname, port, resource)
|
||||||
|
|
||||||
|
addrinfo_list, need_tunnel, auth = _get_addrinfo_list(
|
||||||
|
hostname, port, is_secure, proxy)
|
||||||
|
if not addrinfo_list:
|
||||||
|
raise WebSocketException(
|
||||||
|
"Host not found.: " + hostname + ":" + str(port))
|
||||||
|
|
||||||
|
sock = None
|
||||||
|
try:
|
||||||
|
sock = _open_socket(addrinfo_list, options.sockopt, options.timeout)
|
||||||
|
if need_tunnel:
|
||||||
|
sock = _tunnel(sock, hostname, port, auth)
|
||||||
|
|
||||||
|
if is_secure:
|
||||||
|
if HAVE_SSL:
|
||||||
|
sock = _ssl_socket(sock, options.sslopt, hostname)
|
||||||
|
else:
|
||||||
|
raise WebSocketException("SSL not available.")
|
||||||
|
|
||||||
|
return sock, (hostname, port, resource)
|
||||||
|
except:
|
||||||
|
if sock:
|
||||||
|
sock.close()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _get_addrinfo_list(hostname, port, is_secure, proxy):
|
||||||
|
phost, pport, pauth = get_proxy_info(
|
||||||
|
hostname, is_secure, proxy.host, proxy.port, proxy.auth, proxy.no_proxy)
|
||||||
|
try:
|
||||||
|
# when running on windows 10, getaddrinfo without socktype returns a socktype 0.
|
||||||
|
# This generates an error exception: `_on_error: exception Socket type must be stream or datagram, not 0`
|
||||||
|
# or `OSError: [Errno 22] Invalid argument` when creating socket. Force the socket type to SOCK_STREAM.
|
||||||
|
if not phost:
|
||||||
|
addrinfo_list = socket.getaddrinfo(
|
||||||
|
hostname, port, 0, socket.SOCK_STREAM, socket.SOL_TCP)
|
||||||
|
return addrinfo_list, False, None
|
||||||
|
else:
|
||||||
|
pport = pport and pport or 80
|
||||||
|
# when running on windows 10, the getaddrinfo used above
|
||||||
|
# returns a socktype 0. This generates an error exception:
|
||||||
|
# _on_error: exception Socket type must be stream or datagram, not 0
|
||||||
|
# Force the socket type to SOCK_STREAM
|
||||||
|
addrinfo_list = socket.getaddrinfo(phost, pport, 0, socket.SOCK_STREAM, socket.SOL_TCP)
|
||||||
|
return addrinfo_list, True, pauth
|
||||||
|
except socket.gaierror as e:
|
||||||
|
raise WebSocketAddressException(e)
|
||||||
|
|
||||||
|
|
||||||
|
def _open_socket(addrinfo_list, sockopt, timeout):
|
||||||
|
err = None
|
||||||
|
for addrinfo in addrinfo_list:
|
||||||
|
family, socktype, proto = addrinfo[:3]
|
||||||
|
sock = socket.socket(family, socktype, proto)
|
||||||
|
sock.settimeout(timeout)
|
||||||
|
for opts in DEFAULT_SOCKET_OPTION:
|
||||||
|
sock.setsockopt(*opts)
|
||||||
|
for opts in sockopt:
|
||||||
|
sock.setsockopt(*opts)
|
||||||
|
|
||||||
|
address = addrinfo[4]
|
||||||
|
err = None
|
||||||
|
while not err:
|
||||||
|
try:
|
||||||
|
sock.connect(address)
|
||||||
|
except ProxyConnectionError as error:
|
||||||
|
err = WebSocketProxyException(str(error))
|
||||||
|
err.remote_ip = str(address[0])
|
||||||
|
continue
|
||||||
|
except socket.error as error:
|
||||||
|
error.remote_ip = str(address[0])
|
||||||
|
try:
|
||||||
|
eConnRefused = (errno.ECONNREFUSED, errno.WSAECONNREFUSED)
|
||||||
|
except:
|
||||||
|
eConnRefused = (errno.ECONNREFUSED, )
|
||||||
|
if error.errno == errno.EINTR:
|
||||||
|
continue
|
||||||
|
elif error.errno in eConnRefused:
|
||||||
|
err = error
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise error
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if err:
|
||||||
|
raise err
|
||||||
|
|
||||||
|
return sock
|
||||||
|
|
||||||
|
|
||||||
|
def _can_use_sni():
|
||||||
|
return six.PY2 and sys.version_info >= (2, 7, 9) or sys.version_info >= (3, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap_sni_socket(sock, sslopt, hostname, check_hostname):
|
||||||
|
context = ssl.SSLContext(sslopt.get('ssl_version', ssl.PROTOCOL_SSLv23))
|
||||||
|
|
||||||
|
if sslopt.get('cert_reqs', ssl.CERT_NONE) != ssl.CERT_NONE:
|
||||||
|
cafile = sslopt.get('ca_certs', None)
|
||||||
|
capath = sslopt.get('ca_cert_path', None)
|
||||||
|
if cafile or capath:
|
||||||
|
context.load_verify_locations(cafile=cafile, capath=capath)
|
||||||
|
elif hasattr(context, 'load_default_certs'):
|
||||||
|
context.load_default_certs(ssl.Purpose.SERVER_AUTH)
|
||||||
|
if sslopt.get('certfile', None):
|
||||||
|
context.load_cert_chain(
|
||||||
|
sslopt['certfile'],
|
||||||
|
sslopt.get('keyfile', None),
|
||||||
|
sslopt.get('password', None),
|
||||||
|
)
|
||||||
|
# see
|
||||||
|
# https://github.com/liris/websocket-client/commit/b96a2e8fa765753e82eea531adb19716b52ca3ca#commitcomment-10803153
|
||||||
|
context.verify_mode = sslopt['cert_reqs']
|
||||||
|
if HAVE_CONTEXT_CHECK_HOSTNAME:
|
||||||
|
context.check_hostname = check_hostname
|
||||||
|
if 'ciphers' in sslopt:
|
||||||
|
context.set_ciphers(sslopt['ciphers'])
|
||||||
|
if 'cert_chain' in sslopt:
|
||||||
|
certfile, keyfile, password = sslopt['cert_chain']
|
||||||
|
context.load_cert_chain(certfile, keyfile, password)
|
||||||
|
if 'ecdh_curve' in sslopt:
|
||||||
|
context.set_ecdh_curve(sslopt['ecdh_curve'])
|
||||||
|
|
||||||
|
return context.wrap_socket(
|
||||||
|
sock,
|
||||||
|
do_handshake_on_connect=sslopt.get('do_handshake_on_connect', True),
|
||||||
|
suppress_ragged_eofs=sslopt.get('suppress_ragged_eofs', True),
|
||||||
|
server_hostname=hostname,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _ssl_socket(sock, user_sslopt, hostname):
|
||||||
|
sslopt = dict(cert_reqs=ssl.CERT_REQUIRED)
|
||||||
|
sslopt.update(user_sslopt)
|
||||||
|
|
||||||
|
certPath = os.environ.get('WEBSOCKET_CLIENT_CA_BUNDLE')
|
||||||
|
if certPath and os.path.isfile(certPath) \
|
||||||
|
and user_sslopt.get('ca_certs', None) is None \
|
||||||
|
and user_sslopt.get('ca_cert', None) is None:
|
||||||
|
sslopt['ca_certs'] = certPath
|
||||||
|
elif certPath and os.path.isdir(certPath) \
|
||||||
|
and user_sslopt.get('ca_cert_path', None) is None:
|
||||||
|
sslopt['ca_cert_path'] = certPath
|
||||||
|
|
||||||
|
check_hostname = sslopt["cert_reqs"] != ssl.CERT_NONE and sslopt.pop(
|
||||||
|
'check_hostname', True)
|
||||||
|
|
||||||
|
if _can_use_sni():
|
||||||
|
sock = _wrap_sni_socket(sock, sslopt, hostname, check_hostname)
|
||||||
|
else:
|
||||||
|
sslopt.pop('check_hostname', True)
|
||||||
|
sock = ssl.wrap_socket(sock, **sslopt)
|
||||||
|
|
||||||
|
if not HAVE_CONTEXT_CHECK_HOSTNAME and check_hostname:
|
||||||
|
match_hostname(sock.getpeercert(), hostname)
|
||||||
|
|
||||||
|
return sock
|
||||||
|
|
||||||
|
|
||||||
|
def _tunnel(sock, host, port, auth):
|
||||||
|
debug("Connecting proxy...")
|
||||||
|
connect_header = "CONNECT %s:%d HTTP/1.1\r\n" % (host, port)
|
||||||
|
connect_header += "Host: %s:%d\r\n" % (host, port)
|
||||||
|
|
||||||
|
# TODO: support digest auth.
|
||||||
|
if auth and auth[0]:
|
||||||
|
auth_str = auth[0]
|
||||||
|
if auth[1]:
|
||||||
|
auth_str += ":" + auth[1]
|
||||||
|
encoded_str = base64encode(auth_str.encode()).strip().decode().replace('\n', '')
|
||||||
|
connect_header += "Proxy-Authorization: Basic %s\r\n" % encoded_str
|
||||||
|
connect_header += "\r\n"
|
||||||
|
dump("request header", connect_header)
|
||||||
|
|
||||||
|
send(sock, connect_header)
|
||||||
|
|
||||||
|
try:
|
||||||
|
status, resp_headers, status_message = read_headers(sock)
|
||||||
|
except Exception as e:
|
||||||
|
raise WebSocketProxyException(str(e))
|
||||||
|
|
||||||
|
if status != 200:
|
||||||
|
raise WebSocketProxyException(
|
||||||
|
"failed CONNECT via proxy status: %r" % status)
|
||||||
|
|
||||||
|
return sock
|
||||||
|
|
||||||
|
|
||||||
|
def read_headers(sock):
|
||||||
|
status = None
|
||||||
|
status_message = None
|
||||||
|
headers = {}
|
||||||
|
trace("--- response header ---")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
line = recv_line(sock)
|
||||||
|
line = line.decode('utf-8').strip()
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
trace(line)
|
||||||
|
if not status:
|
||||||
|
|
||||||
|
status_info = line.split(" ", 2)
|
||||||
|
status = int(status_info[1])
|
||||||
|
if len(status_info) > 2:
|
||||||
|
status_message = status_info[2]
|
||||||
|
else:
|
||||||
|
kv = line.split(":", 1)
|
||||||
|
if len(kv) == 2:
|
||||||
|
key, value = kv
|
||||||
|
if key.lower() == "set-cookie" and headers.get("set-cookie"):
|
||||||
|
headers["set-cookie"] = headers.get("set-cookie") + "; " + value.strip()
|
||||||
|
else:
|
||||||
|
headers[key.lower()] = value.strip()
|
||||||
|
else:
|
||||||
|
raise WebSocketException("Invalid header")
|
||||||
|
|
||||||
|
trace("-----------------------")
|
||||||
|
|
||||||
|
return status, headers, status_message
|
92
resources/lib/websocket/_logging.py
Normal file
92
resources/lib/websocket/_logging.py
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger('websocket')
|
||||||
|
try:
|
||||||
|
from logging import NullHandler
|
||||||
|
except ImportError:
|
||||||
|
class NullHandler(logging.Handler):
|
||||||
|
def emit(self, record):
|
||||||
|
pass
|
||||||
|
|
||||||
|
_logger.addHandler(NullHandler())
|
||||||
|
|
||||||
|
_traceEnabled = False
|
||||||
|
|
||||||
|
__all__ = ["enableTrace", "dump", "error", "warning", "debug", "trace",
|
||||||
|
"isEnabledForError", "isEnabledForDebug", "isEnabledForTrace"]
|
||||||
|
|
||||||
|
|
||||||
|
def enableTrace(traceable, handler=logging.StreamHandler()):
|
||||||
|
"""
|
||||||
|
Turn on/off the traceability.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
traceable: bool
|
||||||
|
If set to True, traceability is enabled.
|
||||||
|
"""
|
||||||
|
global _traceEnabled
|
||||||
|
_traceEnabled = traceable
|
||||||
|
if traceable:
|
||||||
|
_logger.addHandler(handler)
|
||||||
|
_logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
def dump(title, message):
|
||||||
|
if _traceEnabled:
|
||||||
|
_logger.debug("--- " + title + " ---")
|
||||||
|
_logger.debug(message)
|
||||||
|
_logger.debug("-----------------------")
|
||||||
|
|
||||||
|
|
||||||
|
def error(msg):
|
||||||
|
_logger.error(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def warning(msg):
|
||||||
|
_logger.warning(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def debug(msg):
|
||||||
|
_logger.debug(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def trace(msg):
|
||||||
|
if _traceEnabled:
|
||||||
|
_logger.debug(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def isEnabledForError():
|
||||||
|
return _logger.isEnabledFor(logging.ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
def isEnabledForDebug():
|
||||||
|
return _logger.isEnabledFor(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
def isEnabledForTrace():
|
||||||
|
return _traceEnabled
|
176
resources/lib/websocket/_socket.py
Normal file
176
resources/lib/websocket/_socket.py
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
import errno
|
||||||
|
import select
|
||||||
|
import socket
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from ._exceptions import *
|
||||||
|
from ._ssl_compat import *
|
||||||
|
from ._utils import *
|
||||||
|
|
||||||
|
DEFAULT_SOCKET_OPTION = [(socket.SOL_TCP, socket.TCP_NODELAY, 1)]
|
||||||
|
if hasattr(socket, "SO_KEEPALIVE"):
|
||||||
|
DEFAULT_SOCKET_OPTION.append((socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1))
|
||||||
|
if hasattr(socket, "TCP_KEEPIDLE"):
|
||||||
|
DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPIDLE, 30))
|
||||||
|
if hasattr(socket, "TCP_KEEPINTVL"):
|
||||||
|
DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPINTVL, 10))
|
||||||
|
if hasattr(socket, "TCP_KEEPCNT"):
|
||||||
|
DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPCNT, 3))
|
||||||
|
|
||||||
|
_default_timeout = None
|
||||||
|
|
||||||
|
__all__ = ["DEFAULT_SOCKET_OPTION", "sock_opt", "setdefaulttimeout", "getdefaulttimeout",
|
||||||
|
"recv", "recv_line", "send"]
|
||||||
|
|
||||||
|
|
||||||
|
class sock_opt(object):
|
||||||
|
|
||||||
|
def __init__(self, sockopt, sslopt):
|
||||||
|
if sockopt is None:
|
||||||
|
sockopt = []
|
||||||
|
if sslopt is None:
|
||||||
|
sslopt = {}
|
||||||
|
self.sockopt = sockopt
|
||||||
|
self.sslopt = sslopt
|
||||||
|
self.timeout = None
|
||||||
|
|
||||||
|
|
||||||
|
def setdefaulttimeout(timeout):
|
||||||
|
"""
|
||||||
|
Set the global timeout setting to connect.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
timeout: int or float
|
||||||
|
default socket timeout time (in seconds)
|
||||||
|
"""
|
||||||
|
global _default_timeout
|
||||||
|
_default_timeout = timeout
|
||||||
|
|
||||||
|
|
||||||
|
def getdefaulttimeout():
|
||||||
|
"""
|
||||||
|
Get default timeout
|
||||||
|
|
||||||
|
Returns
|
||||||
|
----------
|
||||||
|
_default_timeout: int or float
|
||||||
|
Return the global timeout setting (in seconds) to connect.
|
||||||
|
"""
|
||||||
|
return _default_timeout
|
||||||
|
|
||||||
|
|
||||||
|
def recv(sock, bufsize):
|
||||||
|
if not sock:
|
||||||
|
raise WebSocketConnectionClosedException("socket is already closed.")
|
||||||
|
|
||||||
|
def _recv():
|
||||||
|
try:
|
||||||
|
return sock.recv(bufsize)
|
||||||
|
except SSLWantReadError:
|
||||||
|
pass
|
||||||
|
except socket.error as exc:
|
||||||
|
error_code = extract_error_code(exc)
|
||||||
|
if error_code is None:
|
||||||
|
raise
|
||||||
|
if error_code != errno.EAGAIN or error_code != errno.EWOULDBLOCK:
|
||||||
|
raise
|
||||||
|
|
||||||
|
r, w, e = select.select((sock, ), (), (), sock.gettimeout())
|
||||||
|
if r:
|
||||||
|
return sock.recv(bufsize)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if sock.gettimeout() == 0:
|
||||||
|
bytes_ = sock.recv(bufsize)
|
||||||
|
else:
|
||||||
|
bytes_ = _recv()
|
||||||
|
except socket.timeout as e:
|
||||||
|
message = extract_err_message(e)
|
||||||
|
raise WebSocketTimeoutException(message)
|
||||||
|
except SSLError as e:
|
||||||
|
message = extract_err_message(e)
|
||||||
|
if isinstance(message, str) and 'timed out' in message:
|
||||||
|
raise WebSocketTimeoutException(message)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
if not bytes_:
|
||||||
|
raise WebSocketConnectionClosedException(
|
||||||
|
"Connection is already closed.")
|
||||||
|
|
||||||
|
return bytes_
|
||||||
|
|
||||||
|
|
||||||
|
def recv_line(sock):
|
||||||
|
line = []
|
||||||
|
while True:
|
||||||
|
c = recv(sock, 1)
|
||||||
|
line.append(c)
|
||||||
|
if c == six.b("\n"):
|
||||||
|
break
|
||||||
|
return six.b("").join(line)
|
||||||
|
|
||||||
|
|
||||||
|
def send(sock, data):
|
||||||
|
if isinstance(data, six.text_type):
|
||||||
|
data = data.encode('utf-8')
|
||||||
|
|
||||||
|
if not sock:
|
||||||
|
raise WebSocketConnectionClosedException("socket is already closed.")
|
||||||
|
|
||||||
|
def _send():
|
||||||
|
try:
|
||||||
|
return sock.send(data)
|
||||||
|
except SSLWantWriteError:
|
||||||
|
pass
|
||||||
|
except socket.error as exc:
|
||||||
|
error_code = extract_error_code(exc)
|
||||||
|
if error_code is None:
|
||||||
|
raise
|
||||||
|
if error_code != errno.EAGAIN or error_code != errno.EWOULDBLOCK:
|
||||||
|
raise
|
||||||
|
|
||||||
|
r, w, e = select.select((), (sock, ), (), sock.gettimeout())
|
||||||
|
if w:
|
||||||
|
return sock.send(data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if sock.gettimeout() == 0:
|
||||||
|
return sock.send(data)
|
||||||
|
else:
|
||||||
|
return _send()
|
||||||
|
except socket.timeout as e:
|
||||||
|
message = extract_err_message(e)
|
||||||
|
raise WebSocketTimeoutException(message)
|
||||||
|
except Exception as e:
|
||||||
|
message = extract_err_message(e)
|
||||||
|
if isinstance(message, str) and "timed out" in message:
|
||||||
|
raise WebSocketTimeoutException(message)
|
||||||
|
else:
|
||||||
|
raise
|
53
resources/lib/websocket/_ssl_compat.py
Normal file
53
resources/lib/websocket/_ssl_compat.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
__all__ = ["HAVE_SSL", "ssl", "SSLError", "SSLWantReadError", "SSLWantWriteError"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ssl
|
||||||
|
from ssl import SSLError
|
||||||
|
from ssl import SSLWantReadError
|
||||||
|
from ssl import SSLWantWriteError
|
||||||
|
if hasattr(ssl, 'SSLContext') and hasattr(ssl.SSLContext, 'check_hostname'):
|
||||||
|
HAVE_CONTEXT_CHECK_HOSTNAME = True
|
||||||
|
else:
|
||||||
|
HAVE_CONTEXT_CHECK_HOSTNAME = False
|
||||||
|
if hasattr(ssl, "match_hostname"):
|
||||||
|
from ssl import match_hostname
|
||||||
|
else:
|
||||||
|
from backports.ssl_match_hostname import match_hostname
|
||||||
|
__all__.append("match_hostname")
|
||||||
|
__all__.append("HAVE_CONTEXT_CHECK_HOSTNAME")
|
||||||
|
|
||||||
|
HAVE_SSL = True
|
||||||
|
except ImportError:
|
||||||
|
# dummy class of SSLError for ssl none-support environment.
|
||||||
|
class SSLError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class SSLWantReadError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class SSLWantWriteError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
ssl = None
|
||||||
|
|
||||||
|
HAVE_SSL = False
|
178
resources/lib/websocket/_url.py
Normal file
178
resources/lib/websocket/_url.py
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
|
||||||
|
from six.moves.urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["parse_url", "get_proxy_info"]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_url(url):
|
||||||
|
"""
|
||||||
|
parse url and the result is tuple of
|
||||||
|
(hostname, port, resource path and the flag of secure mode)
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
url: str
|
||||||
|
url string.
|
||||||
|
"""
|
||||||
|
if ":" not in url:
|
||||||
|
raise ValueError("url is invalid")
|
||||||
|
|
||||||
|
scheme, url = url.split(":", 1)
|
||||||
|
|
||||||
|
parsed = urlparse(url, scheme="http")
|
||||||
|
if parsed.hostname:
|
||||||
|
hostname = parsed.hostname
|
||||||
|
else:
|
||||||
|
raise ValueError("hostname is invalid")
|
||||||
|
port = 0
|
||||||
|
if parsed.port:
|
||||||
|
port = parsed.port
|
||||||
|
|
||||||
|
is_secure = False
|
||||||
|
if scheme == "ws":
|
||||||
|
if not port:
|
||||||
|
port = 80
|
||||||
|
elif scheme == "wss":
|
||||||
|
is_secure = True
|
||||||
|
if not port:
|
||||||
|
port = 443
|
||||||
|
else:
|
||||||
|
raise ValueError("scheme %s is invalid" % scheme)
|
||||||
|
|
||||||
|
if parsed.path:
|
||||||
|
resource = parsed.path
|
||||||
|
else:
|
||||||
|
resource = "/"
|
||||||
|
|
||||||
|
if parsed.query:
|
||||||
|
resource += "?" + parsed.query
|
||||||
|
|
||||||
|
return hostname, port, resource, is_secure
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_NO_PROXY_HOST = ["localhost", "127.0.0.1"]
|
||||||
|
|
||||||
|
|
||||||
|
def _is_ip_address(addr):
|
||||||
|
try:
|
||||||
|
socket.inet_aton(addr)
|
||||||
|
except socket.error:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _is_subnet_address(hostname):
|
||||||
|
try:
|
||||||
|
addr, netmask = hostname.split("/")
|
||||||
|
return _is_ip_address(addr) and 0 <= int(netmask) < 32
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _is_address_in_network(ip, net):
|
||||||
|
ipaddr = struct.unpack('!I', socket.inet_aton(ip))[0]
|
||||||
|
netaddr, netmask = net.split('/')
|
||||||
|
netaddr = struct.unpack('!I', socket.inet_aton(netaddr))[0]
|
||||||
|
|
||||||
|
netmask = (0xFFFFFFFF << (32 - int(netmask))) & 0xFFFFFFFF
|
||||||
|
return ipaddr & netmask == netaddr
|
||||||
|
|
||||||
|
|
||||||
|
def _is_no_proxy_host(hostname, no_proxy):
|
||||||
|
if not no_proxy:
|
||||||
|
v = os.environ.get("no_proxy", "").replace(" ", "")
|
||||||
|
if v:
|
||||||
|
no_proxy = v.split(",")
|
||||||
|
if not no_proxy:
|
||||||
|
no_proxy = DEFAULT_NO_PROXY_HOST
|
||||||
|
|
||||||
|
if '*' in no_proxy:
|
||||||
|
return True
|
||||||
|
if hostname in no_proxy:
|
||||||
|
return True
|
||||||
|
if _is_ip_address(hostname):
|
||||||
|
return any([_is_address_in_network(hostname, subnet) for subnet in no_proxy if _is_subnet_address(subnet)])
|
||||||
|
for domain in [domain for domain in no_proxy if domain.startswith('.')]:
|
||||||
|
if hostname.endswith(domain):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_proxy_info(
|
||||||
|
hostname, is_secure, proxy_host=None, proxy_port=0, proxy_auth=None,
|
||||||
|
no_proxy=None, proxy_type='http'):
|
||||||
|
"""
|
||||||
|
Try to retrieve proxy host and port from environment
|
||||||
|
if not provided in options.
|
||||||
|
Result is (proxy_host, proxy_port, proxy_auth).
|
||||||
|
proxy_auth is tuple of username and password
|
||||||
|
of proxy authentication information.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
hostname: <type>
|
||||||
|
websocket server name.
|
||||||
|
is_secure: <type>
|
||||||
|
is the connection secure? (wss) looks for "https_proxy" in env
|
||||||
|
before falling back to "http_proxy"
|
||||||
|
options: <type>
|
||||||
|
- http_proxy_host: <type>
|
||||||
|
http proxy host name.
|
||||||
|
- http_proxy_port: <type>
|
||||||
|
http proxy port.
|
||||||
|
- http_no_proxy: <type>
|
||||||
|
host names, which doesn't use proxy.
|
||||||
|
- http_proxy_auth: <type>
|
||||||
|
http proxy auth information. tuple of username and password. default is None
|
||||||
|
- proxy_type: <type>
|
||||||
|
if set to "socks5" PySocks wrapper will be used in place of a http proxy. default is "http"
|
||||||
|
"""
|
||||||
|
if _is_no_proxy_host(hostname, no_proxy):
|
||||||
|
return None, 0, None
|
||||||
|
|
||||||
|
if proxy_host:
|
||||||
|
port = proxy_port
|
||||||
|
auth = proxy_auth
|
||||||
|
return proxy_host, port, auth
|
||||||
|
|
||||||
|
env_keys = ["http_proxy"]
|
||||||
|
if is_secure:
|
||||||
|
env_keys.insert(0, "https_proxy")
|
||||||
|
|
||||||
|
for key in env_keys:
|
||||||
|
value = os.environ.get(key, None)
|
||||||
|
if value:
|
||||||
|
proxy = urlparse(value)
|
||||||
|
auth = (proxy.username, proxy.password) if proxy.username else None
|
||||||
|
return proxy.hostname, proxy.port, auth
|
||||||
|
|
||||||
|
return None, 0, None
|
110
resources/lib/websocket/_utils.py
Normal file
110
resources/lib/websocket/_utils.py
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
import six
|
||||||
|
|
||||||
|
__all__ = ["NoLock", "validate_utf8", "extract_err_message", "extract_error_code"]
|
||||||
|
|
||||||
|
|
||||||
|
class NoLock(object):
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
# If wsaccel is available we use compiled routines to validate UTF-8
|
||||||
|
# strings.
|
||||||
|
from wsaccel.utf8validator import Utf8Validator
|
||||||
|
|
||||||
|
def _validate_utf8(utfbytes):
|
||||||
|
return Utf8Validator().validate(utfbytes)[0]
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
# UTF-8 validator
|
||||||
|
# python implementation of http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
|
||||||
|
|
||||||
|
_UTF8_ACCEPT = 0
|
||||||
|
_UTF8_REJECT = 12
|
||||||
|
|
||||||
|
_UTF8D = [
|
||||||
|
# The first part of the table maps bytes to character classes that
|
||||||
|
# to reduce the size of the transition table and create bitmasks.
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,
|
||||||
|
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
|
||||||
|
8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
|
||||||
|
10,3,3,3,3,3,3,3,3,3,3,3,3,4,3,3, 11,6,6,6,5,8,8,8,8,8,8,8,8,8,8,8,
|
||||||
|
|
||||||
|
# The second part is a transition table that maps a combination
|
||||||
|
# of a state of the automaton and a character class to a state.
|
||||||
|
0,12,24,36,60,96,84,12,12,12,48,72, 12,12,12,12,12,12,12,12,12,12,12,12,
|
||||||
|
12, 0,12,12,12,12,12, 0,12, 0,12,12, 12,24,12,12,12,12,12,24,12,24,12,12,
|
||||||
|
12,12,12,12,12,12,12,24,12,12,12,12, 12,24,12,12,12,12,12,12,12,24,12,12,
|
||||||
|
12,12,12,12,12,12,12,36,12,36,12,12, 12,36,12,12,12,12,12,36,12,36,12,12,
|
||||||
|
12,36,12,12,12,12,12,12,12,12,12,12, ]
|
||||||
|
|
||||||
|
def _decode(state, codep, ch):
|
||||||
|
tp = _UTF8D[ch]
|
||||||
|
|
||||||
|
codep = (ch & 0x3f) | (codep << 6) if (
|
||||||
|
state != _UTF8_ACCEPT) else (0xff >> tp) & ch
|
||||||
|
state = _UTF8D[256 + state + tp]
|
||||||
|
|
||||||
|
return state, codep
|
||||||
|
|
||||||
|
def _validate_utf8(utfbytes):
|
||||||
|
state = _UTF8_ACCEPT
|
||||||
|
codep = 0
|
||||||
|
for i in utfbytes:
|
||||||
|
if six.PY2:
|
||||||
|
i = ord(i)
|
||||||
|
state, codep = _decode(state, codep, i)
|
||||||
|
if state == _UTF8_REJECT:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def validate_utf8(utfbytes):
|
||||||
|
"""
|
||||||
|
validate utf8 byte string.
|
||||||
|
utfbytes: utf byte string to check.
|
||||||
|
return value: if valid utf8 string, return true. Otherwise, return false.
|
||||||
|
"""
|
||||||
|
return _validate_utf8(utfbytes)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_err_message(exception):
|
||||||
|
if exception.args:
|
||||||
|
return exception.args[0]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def extract_error_code(exception):
|
||||||
|
if exception.args and len(exception.args) > 1:
|
||||||
|
return exception.args[0] if isinstance(exception.args[0], int) else None
|
0
resources/lib/websocket/tests/__init__.py
Normal file
0
resources/lib/websocket/tests/__init__.py
Normal file
6
resources/lib/websocket/tests/data/header01.txt
Normal file
6
resources/lib/websocket/tests/data/header01.txt
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
HTTP/1.1 101 WebSocket Protocol Handshake
|
||||||
|
Connection: Upgrade
|
||||||
|
Upgrade: WebSocket
|
||||||
|
Sec-WebSocket-Accept: Kxep+hNu9n51529fGidYu7a3wO0=
|
||||||
|
some_header: something
|
||||||
|
|
6
resources/lib/websocket/tests/data/header02.txt
Normal file
6
resources/lib/websocket/tests/data/header02.txt
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
HTTP/1.1 101 WebSocket Protocol Handshake
|
||||||
|
Connection: Upgrade
|
||||||
|
Upgrade WebSocket
|
||||||
|
Sec-WebSocket-Accept: Kxep+hNu9n51529fGidYu7a3wO0=
|
||||||
|
some_header: something
|
||||||
|
|
6
resources/lib/websocket/tests/data/header03.txt
Normal file
6
resources/lib/websocket/tests/data/header03.txt
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
HTTP/1.1 101 WebSocket Protocol Handshake
|
||||||
|
Connection: Upgrade, Keep-Alive
|
||||||
|
Upgrade: WebSocket
|
||||||
|
Sec-WebSocket-Accept: Kxep+hNu9n51529fGidYu7a3wO0=
|
||||||
|
some_header: something
|
||||||
|
|
77
resources/lib/websocket/tests/test_abnf.py
Normal file
77
resources/lib/websocket/tests/test_abnf.py
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import websocket as ws
|
||||||
|
from websocket._abnf import *
|
||||||
|
import sys
|
||||||
|
sys.path[0:0] = [""]
|
||||||
|
|
||||||
|
if sys.version_info[0] == 2 and sys.version_info[1] < 7:
|
||||||
|
import unittest2 as unittest
|
||||||
|
else:
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
class ABNFTest(unittest.TestCase):
|
||||||
|
|
||||||
|
def testInit(self):
|
||||||
|
a = ABNF(0,0,0,0, opcode=ABNF.OPCODE_PING)
|
||||||
|
self.assertEqual(a.fin, 0)
|
||||||
|
self.assertEqual(a.rsv1, 0)
|
||||||
|
self.assertEqual(a.rsv2, 0)
|
||||||
|
self.assertEqual(a.rsv3, 0)
|
||||||
|
self.assertEqual(a.opcode, 9)
|
||||||
|
self.assertEqual(a.data, '')
|
||||||
|
a_bad = ABNF(0,1,0,0, opcode=77)
|
||||||
|
self.assertEqual(a_bad.rsv1, 1)
|
||||||
|
self.assertEqual(a_bad.opcode, 77)
|
||||||
|
|
||||||
|
def testValidate(self):
|
||||||
|
a = ABNF(0,0,0,0, opcode=ABNF.OPCODE_PING)
|
||||||
|
self.assertRaises(ws.WebSocketProtocolException, a.validate)
|
||||||
|
a_bad = ABNF(0,1,0,0, opcode=77)
|
||||||
|
self.assertRaises(ws.WebSocketProtocolException, a_bad.validate)
|
||||||
|
a_close = ABNF(0,1,0,0, opcode=ABNF.OPCODE_CLOSE, data="abcdefgh1234567890abcdefgh1234567890abcdefgh1234567890abcdefgh1234567890")
|
||||||
|
self.assertRaises(ws.WebSocketProtocolException, a_close.validate)
|
||||||
|
|
||||||
|
# This caused an error in the Python 2.7 Github Actions build
|
||||||
|
# Uncomment test case when Python 2 support no longer wanted
|
||||||
|
# def testMask(self):
|
||||||
|
# ab = ABNF(0,0,0,0, opcode=ABNF.OPCODE_PING)
|
||||||
|
# bytes_val = bytes("aaaa", 'utf-8')
|
||||||
|
# self.assertEqual(ab._get_masked(bytes_val), bytes_val)
|
||||||
|
|
||||||
|
def testFrameBuffer(self):
|
||||||
|
fb = frame_buffer(0, True)
|
||||||
|
self.assertEqual(fb.recv, 0)
|
||||||
|
self.assertEqual(fb.skip_utf8_validation, True)
|
||||||
|
fb.clear
|
||||||
|
self.assertEqual(fb.header, None)
|
||||||
|
self.assertEqual(fb.length, None)
|
||||||
|
self.assertEqual(fb.mask, None)
|
||||||
|
self.assertEqual(fb.has_mask(), False)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
137
resources/lib/websocket/tests/test_app.py
Normal file
137
resources/lib/websocket/tests/test_app.py
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import websocket as ws
|
||||||
|
import sys
|
||||||
|
sys.path[0:0] = [""]
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ssl
|
||||||
|
except ImportError:
|
||||||
|
HAVE_SSL = False
|
||||||
|
|
||||||
|
if sys.version_info[0] == 2 and sys.version_info[1] < 7:
|
||||||
|
import unittest2 as unittest
|
||||||
|
else:
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
# Skip test to access the internet.
|
||||||
|
TEST_WITH_INTERNET = os.environ.get('TEST_WITH_INTERNET', '0') == '1'
|
||||||
|
TRACEABLE = True
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketAppTest(unittest.TestCase):
|
||||||
|
|
||||||
|
class NotSetYet(object):
|
||||||
|
""" A marker class for signalling that a value hasn't been set yet.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
ws.enableTrace(TRACEABLE)
|
||||||
|
|
||||||
|
WebSocketAppTest.keep_running_open = WebSocketAppTest.NotSetYet()
|
||||||
|
WebSocketAppTest.keep_running_close = WebSocketAppTest.NotSetYet()
|
||||||
|
WebSocketAppTest.get_mask_key_id = WebSocketAppTest.NotSetYet()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
WebSocketAppTest.keep_running_open = WebSocketAppTest.NotSetYet()
|
||||||
|
WebSocketAppTest.keep_running_close = WebSocketAppTest.NotSetYet()
|
||||||
|
WebSocketAppTest.get_mask_key_id = WebSocketAppTest.NotSetYet()
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testKeepRunning(self):
|
||||||
|
""" A WebSocketApp should keep running as long as its self.keep_running
|
||||||
|
is not False (in the boolean context).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def on_open(self, *args, **kwargs):
|
||||||
|
""" Set the keep_running flag for later inspection and immediately
|
||||||
|
close the connection.
|
||||||
|
"""
|
||||||
|
WebSocketAppTest.keep_running_open = self.keep_running
|
||||||
|
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def on_close(self, *args, **kwargs):
|
||||||
|
""" Set the keep_running flag for the test to use.
|
||||||
|
"""
|
||||||
|
WebSocketAppTest.keep_running_close = self.keep_running
|
||||||
|
|
||||||
|
app = ws.WebSocketApp('ws://echo.websocket.org/', on_open=on_open, on_close=on_close)
|
||||||
|
app.run_forever()
|
||||||
|
|
||||||
|
# if numpy is installed, this assertion fail
|
||||||
|
# self.assertFalse(isinstance(WebSocketAppTest.keep_running_open,
|
||||||
|
# WebSocketAppTest.NotSetYet))
|
||||||
|
|
||||||
|
# self.assertFalse(isinstance(WebSocketAppTest.keep_running_close,
|
||||||
|
# WebSocketAppTest.NotSetYet))
|
||||||
|
|
||||||
|
# self.assertEqual(True, WebSocketAppTest.keep_running_open)
|
||||||
|
# self.assertEqual(False, WebSocketAppTest.keep_running_close)
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testSockMaskKey(self):
|
||||||
|
""" A WebSocketApp should forward the received mask_key function down
|
||||||
|
to the actual socket.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def my_mask_key_func():
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_open(self, *args, **kwargs):
|
||||||
|
""" Set the value so the test can use it later on and immediately
|
||||||
|
close the connection.
|
||||||
|
"""
|
||||||
|
WebSocketAppTest.get_mask_key_id = id(self.get_mask_key)
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
app = ws.WebSocketApp('ws://echo.websocket.org/', on_open=on_open, get_mask_key=my_mask_key_func)
|
||||||
|
app.run_forever()
|
||||||
|
|
||||||
|
# if numpy is installed, this assertion fail
|
||||||
|
# Note: We can't use 'is' for comparing the functions directly, need to use 'id'.
|
||||||
|
# self.assertEqual(WebSocketAppTest.get_mask_key_id, id(my_mask_key_func))
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testPingInterval(self):
|
||||||
|
""" A WebSocketApp should ping regularly
|
||||||
|
"""
|
||||||
|
|
||||||
|
def on_ping(app, msg):
|
||||||
|
print("Got a ping!")
|
||||||
|
app.close()
|
||||||
|
|
||||||
|
def on_pong(app, msg):
|
||||||
|
print("Got a pong! No need to respond")
|
||||||
|
app.close()
|
||||||
|
|
||||||
|
app = ws.WebSocketApp('wss://api-pub.bitfinex.com/ws/1', on_ping=on_ping, on_pong=on_pong)
|
||||||
|
app.run_forever(ping_interval=2, ping_timeout=1) # , sslopt={"cert_reqs": ssl.CERT_NONE}
|
||||||
|
self.assertRaises(ws.WebSocketException, app.run_forever, ping_interval=2, ping_timeout=3, sslopt={"cert_reqs": ssl.CERT_NONE})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
117
resources/lib/websocket/tests/test_cookiejar.py
Normal file
117
resources/lib/websocket/tests/test_cookiejar.py
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from websocket._cookiejar import SimpleCookieJar
|
||||||
|
|
||||||
|
|
||||||
|
class CookieJarTest(unittest.TestCase):
|
||||||
|
def testAdd(self):
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.add("")
|
||||||
|
self.assertFalse(cookie_jar.jar, "Cookie with no domain should not be added to the jar")
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.add("a=b")
|
||||||
|
self.assertFalse(cookie_jar.jar, "Cookie with no domain should not be added to the jar")
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.add("a=b; domain=.abc")
|
||||||
|
self.assertTrue(".abc" in cookie_jar.jar)
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.add("a=b; domain=abc")
|
||||||
|
self.assertTrue(".abc" in cookie_jar.jar)
|
||||||
|
self.assertTrue("abc" not in cookie_jar.jar)
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.add("a=b; c=d; domain=abc")
|
||||||
|
self.assertEqual(cookie_jar.get("abc"), "a=b; c=d")
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.add("a=b; c=d; domain=abc")
|
||||||
|
cookie_jar.add("e=f; domain=abc")
|
||||||
|
self.assertEqual(cookie_jar.get("abc"), "a=b; c=d; e=f")
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.add("a=b; c=d; domain=abc")
|
||||||
|
cookie_jar.add("e=f; domain=.abc")
|
||||||
|
self.assertEqual(cookie_jar.get("abc"), "a=b; c=d; e=f")
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.add("a=b; c=d; domain=abc")
|
||||||
|
cookie_jar.add("e=f; domain=xyz")
|
||||||
|
self.assertEqual(cookie_jar.get("abc"), "a=b; c=d")
|
||||||
|
self.assertEqual(cookie_jar.get("xyz"), "e=f")
|
||||||
|
self.assertEqual(cookie_jar.get("something"), "")
|
||||||
|
|
||||||
|
def testSet(self):
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.set("a=b")
|
||||||
|
self.assertFalse(cookie_jar.jar, "Cookie with no domain should not be added to the jar")
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.set("a=b; domain=.abc")
|
||||||
|
self.assertTrue(".abc" in cookie_jar.jar)
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.set("a=b; domain=abc")
|
||||||
|
self.assertTrue(".abc" in cookie_jar.jar)
|
||||||
|
self.assertTrue("abc" not in cookie_jar.jar)
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.set("a=b; c=d; domain=abc")
|
||||||
|
self.assertEqual(cookie_jar.get("abc"), "a=b; c=d")
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.set("a=b; c=d; domain=abc")
|
||||||
|
cookie_jar.set("e=f; domain=abc")
|
||||||
|
self.assertEqual(cookie_jar.get("abc"), "e=f")
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.set("a=b; c=d; domain=abc")
|
||||||
|
cookie_jar.set("e=f; domain=.abc")
|
||||||
|
self.assertEqual(cookie_jar.get("abc"), "e=f")
|
||||||
|
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.set("a=b; c=d; domain=abc")
|
||||||
|
cookie_jar.set("e=f; domain=xyz")
|
||||||
|
self.assertEqual(cookie_jar.get("abc"), "a=b; c=d")
|
||||||
|
self.assertEqual(cookie_jar.get("xyz"), "e=f")
|
||||||
|
self.assertEqual(cookie_jar.get("something"), "")
|
||||||
|
|
||||||
|
def testGet(self):
|
||||||
|
cookie_jar = SimpleCookieJar()
|
||||||
|
cookie_jar.set("a=b; c=d; domain=abc.com")
|
||||||
|
self.assertEqual(cookie_jar.get("abc.com"), "a=b; c=d")
|
||||||
|
self.assertEqual(cookie_jar.get("x.abc.com"), "a=b; c=d")
|
||||||
|
self.assertEqual(cookie_jar.get("abc.com.es"), "")
|
||||||
|
self.assertEqual(cookie_jar.get("xabc.com"), "")
|
||||||
|
|
||||||
|
cookie_jar.set("a=b; c=d; domain=.abc.com")
|
||||||
|
self.assertEqual(cookie_jar.get("abc.com"), "a=b; c=d")
|
||||||
|
self.assertEqual(cookie_jar.get("x.abc.com"), "a=b; c=d")
|
||||||
|
self.assertEqual(cookie_jar.get("abc.com.es"), "")
|
||||||
|
self.assertEqual(cookie_jar.get("xabc.com"), "")
|
109
resources/lib/websocket/tests/test_http.py
Normal file
109
resources/lib/websocket/tests/test_http.py
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import websocket as ws
|
||||||
|
from websocket._http import proxy_info, read_headers, _open_proxied_socket, _tunnel
|
||||||
|
import sys
|
||||||
|
sys.path[0:0] = [""]
|
||||||
|
|
||||||
|
if sys.version_info[0] == 2 and sys.version_info[1] < 7:
|
||||||
|
import unittest2 as unittest
|
||||||
|
else:
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
class SockMock(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.data = []
|
||||||
|
self.sent = []
|
||||||
|
|
||||||
|
def add_packet(self, data):
|
||||||
|
self.data.append(data)
|
||||||
|
|
||||||
|
def gettimeout(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def recv(self, bufsize):
|
||||||
|
if self.data:
|
||||||
|
e = self.data.pop(0)
|
||||||
|
if isinstance(e, Exception):
|
||||||
|
raise e
|
||||||
|
if len(e) > bufsize:
|
||||||
|
self.data.insert(0, e[bufsize:])
|
||||||
|
return e[:bufsize]
|
||||||
|
|
||||||
|
def send(self, data):
|
||||||
|
self.sent.append(data)
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class HeaderSockMock(SockMock):
|
||||||
|
|
||||||
|
def __init__(self, fname):
|
||||||
|
SockMock.__init__(self)
|
||||||
|
path = os.path.join(os.path.dirname(__file__), fname)
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
self.add_packet(f.read())
|
||||||
|
|
||||||
|
|
||||||
|
class OptsList():
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.timeout = 0
|
||||||
|
self.sockopt = []
|
||||||
|
|
||||||
|
|
||||||
|
class HttpTest(unittest.TestCase):
|
||||||
|
|
||||||
|
def testReadHeader(self):
|
||||||
|
status, header, status_message = read_headers(HeaderSockMock("data/header01.txt"))
|
||||||
|
self.assertEqual(status, 101)
|
||||||
|
self.assertEqual(header["connection"], "Upgrade")
|
||||||
|
# header02.txt is intentionally malformed
|
||||||
|
self.assertRaises(ws.WebSocketException, read_headers, HeaderSockMock("data/header02.txt"))
|
||||||
|
|
||||||
|
def testTunnel(self):
|
||||||
|
self.assertRaises(ws.WebSocketProxyException, _tunnel, HeaderSockMock("data/header01.txt"), "example.com", 80, ("username", "password"))
|
||||||
|
self.assertRaises(ws.WebSocketProxyException, _tunnel, HeaderSockMock("data/header02.txt"), "example.com", 80, ("username", "password"))
|
||||||
|
|
||||||
|
def testConnect(self):
|
||||||
|
# Not currently testing an actual proxy connection, so just check whether TypeError is raised
|
||||||
|
self.assertRaises(TypeError, _open_proxied_socket, "wss://example.com", OptsList(), proxy_info(http_proxy_host="example.com", http_proxy_port="8080", proxy_type="http"))
|
||||||
|
self.assertRaises(TypeError, _open_proxied_socket, "wss://example.com", OptsList(), proxy_info(http_proxy_host="example.com", http_proxy_port="8080", proxy_type="socks4"))
|
||||||
|
self.assertRaises(TypeError, _open_proxied_socket, "wss://example.com", OptsList(), proxy_info(http_proxy_host="example.com", http_proxy_port="8080", proxy_type="socks5h"))
|
||||||
|
|
||||||
|
def testProxyInfo(self):
|
||||||
|
self.assertEqual(proxy_info(http_proxy_host="127.0.0.1", http_proxy_port="8080", proxy_type="http").type, "http")
|
||||||
|
self.assertRaises(ValueError, proxy_info, http_proxy_host="127.0.0.1", http_proxy_port="8080", proxy_type="badval")
|
||||||
|
self.assertEqual(proxy_info(http_proxy_host="example.com", http_proxy_port="8080", proxy_type="http").host, "example.com")
|
||||||
|
self.assertEqual(proxy_info(http_proxy_host="127.0.0.1", http_proxy_port="8080", proxy_type="http").port, "8080")
|
||||||
|
self.assertEqual(proxy_info(http_proxy_host="127.0.0.1", http_proxy_port="8080", proxy_type="http").auth, None)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
309
resources/lib/websocket/tests/test_url.py
Normal file
309
resources/lib/websocket/tests/test_url.py
Normal file
|
@ -0,0 +1,309 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
from websocket._url import get_proxy_info, parse_url, _is_address_in_network, _is_no_proxy_host
|
||||||
|
|
||||||
|
if sys.version_info[0] == 2 and sys.version_info[1] < 7:
|
||||||
|
import unittest2 as unittest
|
||||||
|
else:
|
||||||
|
import unittest
|
||||||
|
sys.path[0:0] = [""]
|
||||||
|
|
||||||
|
|
||||||
|
class UrlTest(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_address_in_network(self):
|
||||||
|
self.assertTrue(_is_address_in_network('127.0.0.1', '127.0.0.0/8'))
|
||||||
|
self.assertTrue(_is_address_in_network('127.1.0.1', '127.0.0.0/8'))
|
||||||
|
self.assertFalse(_is_address_in_network('127.1.0.1', '127.0.0.0/24'))
|
||||||
|
|
||||||
|
def testParseUrl(self):
|
||||||
|
p = parse_url("ws://www.example.com/r")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 80)
|
||||||
|
self.assertEqual(p[2], "/r")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("ws://www.example.com/r/")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 80)
|
||||||
|
self.assertEqual(p[2], "/r/")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("ws://www.example.com/")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 80)
|
||||||
|
self.assertEqual(p[2], "/")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("ws://www.example.com")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 80)
|
||||||
|
self.assertEqual(p[2], "/")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("ws://www.example.com:8080/r")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 8080)
|
||||||
|
self.assertEqual(p[2], "/r")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("ws://www.example.com:8080/")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 8080)
|
||||||
|
self.assertEqual(p[2], "/")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("ws://www.example.com:8080")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 8080)
|
||||||
|
self.assertEqual(p[2], "/")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("wss://www.example.com:8080/r")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 8080)
|
||||||
|
self.assertEqual(p[2], "/r")
|
||||||
|
self.assertEqual(p[3], True)
|
||||||
|
|
||||||
|
p = parse_url("wss://www.example.com:8080/r?key=value")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 8080)
|
||||||
|
self.assertEqual(p[2], "/r?key=value")
|
||||||
|
self.assertEqual(p[3], True)
|
||||||
|
|
||||||
|
self.assertRaises(ValueError, parse_url, "http://www.example.com/r")
|
||||||
|
|
||||||
|
if sys.version_info[0] == 2 and sys.version_info[1] < 7:
|
||||||
|
return
|
||||||
|
|
||||||
|
p = parse_url("ws://[2a03:4000:123:83::3]/r")
|
||||||
|
self.assertEqual(p[0], "2a03:4000:123:83::3")
|
||||||
|
self.assertEqual(p[1], 80)
|
||||||
|
self.assertEqual(p[2], "/r")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("ws://[2a03:4000:123:83::3]:8080/r")
|
||||||
|
self.assertEqual(p[0], "2a03:4000:123:83::3")
|
||||||
|
self.assertEqual(p[1], 8080)
|
||||||
|
self.assertEqual(p[2], "/r")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("wss://[2a03:4000:123:83::3]/r")
|
||||||
|
self.assertEqual(p[0], "2a03:4000:123:83::3")
|
||||||
|
self.assertEqual(p[1], 443)
|
||||||
|
self.assertEqual(p[2], "/r")
|
||||||
|
self.assertEqual(p[3], True)
|
||||||
|
|
||||||
|
p = parse_url("wss://[2a03:4000:123:83::3]:8080/r")
|
||||||
|
self.assertEqual(p[0], "2a03:4000:123:83::3")
|
||||||
|
self.assertEqual(p[1], 8080)
|
||||||
|
self.assertEqual(p[2], "/r")
|
||||||
|
self.assertEqual(p[3], True)
|
||||||
|
|
||||||
|
|
||||||
|
class IsNoProxyHostTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.no_proxy = os.environ.get("no_proxy", None)
|
||||||
|
if "no_proxy" in os.environ:
|
||||||
|
del os.environ["no_proxy"]
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
if self.no_proxy:
|
||||||
|
os.environ["no_proxy"] = self.no_proxy
|
||||||
|
elif "no_proxy" in os.environ:
|
||||||
|
del os.environ["no_proxy"]
|
||||||
|
|
||||||
|
def testMatchAll(self):
|
||||||
|
self.assertTrue(_is_no_proxy_host("any.websocket.org", ['*']))
|
||||||
|
self.assertTrue(_is_no_proxy_host("192.168.0.1", ['*']))
|
||||||
|
self.assertTrue(_is_no_proxy_host("any.websocket.org", ['other.websocket.org', '*']))
|
||||||
|
os.environ['no_proxy'] = '*'
|
||||||
|
self.assertTrue(_is_no_proxy_host("any.websocket.org", None))
|
||||||
|
self.assertTrue(_is_no_proxy_host("192.168.0.1", None))
|
||||||
|
os.environ['no_proxy'] = 'other.websocket.org, *'
|
||||||
|
self.assertTrue(_is_no_proxy_host("any.websocket.org", None))
|
||||||
|
|
||||||
|
def testIpAddress(self):
|
||||||
|
self.assertTrue(_is_no_proxy_host("127.0.0.1", ['127.0.0.1']))
|
||||||
|
self.assertFalse(_is_no_proxy_host("127.0.0.2", ['127.0.0.1']))
|
||||||
|
self.assertTrue(_is_no_proxy_host("127.0.0.1", ['other.websocket.org', '127.0.0.1']))
|
||||||
|
self.assertFalse(_is_no_proxy_host("127.0.0.2", ['other.websocket.org', '127.0.0.1']))
|
||||||
|
os.environ['no_proxy'] = '127.0.0.1'
|
||||||
|
self.assertTrue(_is_no_proxy_host("127.0.0.1", None))
|
||||||
|
self.assertFalse(_is_no_proxy_host("127.0.0.2", None))
|
||||||
|
os.environ['no_proxy'] = 'other.websocket.org, 127.0.0.1'
|
||||||
|
self.assertTrue(_is_no_proxy_host("127.0.0.1", None))
|
||||||
|
self.assertFalse(_is_no_proxy_host("127.0.0.2", None))
|
||||||
|
|
||||||
|
def testIpAddressInRange(self):
|
||||||
|
self.assertTrue(_is_no_proxy_host("127.0.0.1", ['127.0.0.0/8']))
|
||||||
|
self.assertTrue(_is_no_proxy_host("127.0.0.2", ['127.0.0.0/8']))
|
||||||
|
self.assertFalse(_is_no_proxy_host("127.1.0.1", ['127.0.0.0/24']))
|
||||||
|
os.environ['no_proxy'] = '127.0.0.0/8'
|
||||||
|
self.assertTrue(_is_no_proxy_host("127.0.0.1", None))
|
||||||
|
self.assertTrue(_is_no_proxy_host("127.0.0.2", None))
|
||||||
|
os.environ['no_proxy'] = '127.0.0.0/24'
|
||||||
|
self.assertFalse(_is_no_proxy_host("127.1.0.1", None))
|
||||||
|
|
||||||
|
def testHostnameMatch(self):
|
||||||
|
self.assertTrue(_is_no_proxy_host("my.websocket.org", ['my.websocket.org']))
|
||||||
|
self.assertTrue(_is_no_proxy_host("my.websocket.org", ['other.websocket.org', 'my.websocket.org']))
|
||||||
|
self.assertFalse(_is_no_proxy_host("my.websocket.org", ['other.websocket.org']))
|
||||||
|
os.environ['no_proxy'] = 'my.websocket.org'
|
||||||
|
self.assertTrue(_is_no_proxy_host("my.websocket.org", None))
|
||||||
|
self.assertFalse(_is_no_proxy_host("other.websocket.org", None))
|
||||||
|
os.environ['no_proxy'] = 'other.websocket.org, my.websocket.org'
|
||||||
|
self.assertTrue(_is_no_proxy_host("my.websocket.org", None))
|
||||||
|
|
||||||
|
def testHostnameMatchDomain(self):
|
||||||
|
self.assertTrue(_is_no_proxy_host("any.websocket.org", ['.websocket.org']))
|
||||||
|
self.assertTrue(_is_no_proxy_host("my.other.websocket.org", ['.websocket.org']))
|
||||||
|
self.assertTrue(_is_no_proxy_host("any.websocket.org", ['my.websocket.org', '.websocket.org']))
|
||||||
|
self.assertFalse(_is_no_proxy_host("any.websocket.com", ['.websocket.org']))
|
||||||
|
os.environ['no_proxy'] = '.websocket.org'
|
||||||
|
self.assertTrue(_is_no_proxy_host("any.websocket.org", None))
|
||||||
|
self.assertTrue(_is_no_proxy_host("my.other.websocket.org", None))
|
||||||
|
self.assertFalse(_is_no_proxy_host("any.websocket.com", None))
|
||||||
|
os.environ['no_proxy'] = 'my.websocket.org, .websocket.org'
|
||||||
|
self.assertTrue(_is_no_proxy_host("any.websocket.org", None))
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyInfoTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.http_proxy = os.environ.get("http_proxy", None)
|
||||||
|
self.https_proxy = os.environ.get("https_proxy", None)
|
||||||
|
self.no_proxy = os.environ.get("no_proxy", None)
|
||||||
|
if "http_proxy" in os.environ:
|
||||||
|
del os.environ["http_proxy"]
|
||||||
|
if "https_proxy" in os.environ:
|
||||||
|
del os.environ["https_proxy"]
|
||||||
|
if "no_proxy" in os.environ:
|
||||||
|
del os.environ["no_proxy"]
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
if self.http_proxy:
|
||||||
|
os.environ["http_proxy"] = self.http_proxy
|
||||||
|
elif "http_proxy" in os.environ:
|
||||||
|
del os.environ["http_proxy"]
|
||||||
|
|
||||||
|
if self.https_proxy:
|
||||||
|
os.environ["https_proxy"] = self.https_proxy
|
||||||
|
elif "https_proxy" in os.environ:
|
||||||
|
del os.environ["https_proxy"]
|
||||||
|
|
||||||
|
if self.no_proxy:
|
||||||
|
os.environ["no_proxy"] = self.no_proxy
|
||||||
|
elif "no_proxy" in os.environ:
|
||||||
|
del os.environ["no_proxy"]
|
||||||
|
|
||||||
|
def testProxyFromArgs(self):
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost"), ("localhost", 0, None))
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost", proxy_port=3128),
|
||||||
|
("localhost", 3128, None))
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost"), ("localhost", 0, None))
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128),
|
||||||
|
("localhost", 3128, None))
|
||||||
|
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost", proxy_auth=("a", "b")),
|
||||||
|
("localhost", 0, ("a", "b")))
|
||||||
|
self.assertEqual(
|
||||||
|
get_proxy_info("echo.websocket.org", False, proxy_host="localhost", proxy_port=3128, proxy_auth=("a", "b")),
|
||||||
|
("localhost", 3128, ("a", "b")))
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_auth=("a", "b")),
|
||||||
|
("localhost", 0, ("a", "b")))
|
||||||
|
self.assertEqual(
|
||||||
|
get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128, proxy_auth=("a", "b")),
|
||||||
|
("localhost", 3128, ("a", "b")))
|
||||||
|
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128,
|
||||||
|
no_proxy=["example.com"], proxy_auth=("a", "b")),
|
||||||
|
("localhost", 3128, ("a", "b")))
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128,
|
||||||
|
no_proxy=["echo.websocket.org"], proxy_auth=("a", "b")),
|
||||||
|
(None, 0, None))
|
||||||
|
|
||||||
|
def testProxyFromEnv(self):
|
||||||
|
os.environ["http_proxy"] = "http://localhost/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, None))
|
||||||
|
os.environ["http_proxy"] = "http://localhost:3128/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, None))
|
||||||
|
|
||||||
|
os.environ["http_proxy"] = "http://localhost/"
|
||||||
|
os.environ["https_proxy"] = "http://localhost2/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, None))
|
||||||
|
os.environ["http_proxy"] = "http://localhost:3128/"
|
||||||
|
os.environ["https_proxy"] = "http://localhost2:3128/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, None))
|
||||||
|
|
||||||
|
os.environ["http_proxy"] = "http://localhost/"
|
||||||
|
os.environ["https_proxy"] = "http://localhost2/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", None, None))
|
||||||
|
os.environ["http_proxy"] = "http://localhost:3128/"
|
||||||
|
os.environ["https_proxy"] = "http://localhost2:3128/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", 3128, None))
|
||||||
|
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, ("a", "b")))
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost:3128/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, ("a", "b")))
|
||||||
|
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost/"
|
||||||
|
os.environ["https_proxy"] = "http://a:b@localhost2/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, ("a", "b")))
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost:3128/"
|
||||||
|
os.environ["https_proxy"] = "http://a:b@localhost2:3128/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, ("a", "b")))
|
||||||
|
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost/"
|
||||||
|
os.environ["https_proxy"] = "http://a:b@localhost2/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", None, ("a", "b")))
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost:3128/"
|
||||||
|
os.environ["https_proxy"] = "http://a:b@localhost2:3128/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", 3128, ("a", "b")))
|
||||||
|
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost/"
|
||||||
|
os.environ["https_proxy"] = "http://a:b@localhost2/"
|
||||||
|
os.environ["no_proxy"] = "example1.com,example2.com"
|
||||||
|
self.assertEqual(get_proxy_info("example.1.com", True), ("localhost2", None, ("a", "b")))
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost:3128/"
|
||||||
|
os.environ["https_proxy"] = "http://a:b@localhost2:3128/"
|
||||||
|
os.environ["no_proxy"] = "example1.com,example2.com, echo.websocket.org"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True), (None, 0, None))
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost:3128/"
|
||||||
|
os.environ["https_proxy"] = "http://a:b@localhost2:3128/"
|
||||||
|
os.environ["no_proxy"] = "example1.com,example2.com, .websocket.org"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True), (None, 0, None))
|
||||||
|
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost:3128/"
|
||||||
|
os.environ["https_proxy"] = "http://a:b@localhost2:3128/"
|
||||||
|
os.environ["no_proxy"] = "127.0.0.0/8, 192.168.0.0/16"
|
||||||
|
self.assertEqual(get_proxy_info("127.0.0.1", False), (None, 0, None))
|
||||||
|
self.assertEqual(get_proxy_info("192.168.1.1", False), (None, 0, None))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
433
resources/lib/websocket/tests/test_websocket.py
Normal file
433
resources/lib/websocket/tests/test_websocket.py
Normal file
|
@ -0,0 +1,433 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path[0:0] = [""]
|
||||||
|
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import socket
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
# websocket-client
|
||||||
|
import websocket as ws
|
||||||
|
from websocket._handshake import _create_sec_websocket_key, \
|
||||||
|
_validate as _validate_header
|
||||||
|
from websocket._http import read_headers
|
||||||
|
from websocket._utils import validate_utf8
|
||||||
|
|
||||||
|
if six.PY3:
|
||||||
|
from base64 import decodebytes as base64decode
|
||||||
|
else:
|
||||||
|
from base64 import decodestring as base64decode
|
||||||
|
|
||||||
|
if sys.version_info[0] == 2 and sys.version_info[1] < 7:
|
||||||
|
import unittest2 as unittest
|
||||||
|
else:
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ssl import SSLError
|
||||||
|
except ImportError:
|
||||||
|
# dummy class of SSLError for ssl none-support environment.
|
||||||
|
class SSLError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Skip test to access the internet.
|
||||||
|
TEST_WITH_INTERNET = os.environ.get('TEST_WITH_INTERNET', '0') == '1'
|
||||||
|
TRACEABLE = True
|
||||||
|
|
||||||
|
|
||||||
|
def create_mask_key(_):
|
||||||
|
return "abcd"
|
||||||
|
|
||||||
|
|
||||||
|
class SockMock(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.data = []
|
||||||
|
self.sent = []
|
||||||
|
|
||||||
|
def add_packet(self, data):
|
||||||
|
self.data.append(data)
|
||||||
|
|
||||||
|
def gettimeout(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def recv(self, bufsize):
|
||||||
|
if self.data:
|
||||||
|
e = self.data.pop(0)
|
||||||
|
if isinstance(e, Exception):
|
||||||
|
raise e
|
||||||
|
if len(e) > bufsize:
|
||||||
|
self.data.insert(0, e[bufsize:])
|
||||||
|
return e[:bufsize]
|
||||||
|
|
||||||
|
def send(self, data):
|
||||||
|
self.sent.append(data)
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class HeaderSockMock(SockMock):
|
||||||
|
|
||||||
|
def __init__(self, fname):
|
||||||
|
SockMock.__init__(self)
|
||||||
|
path = os.path.join(os.path.dirname(__file__), fname)
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
self.add_packet(f.read())
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
ws.enableTrace(TRACEABLE)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def testDefaultTimeout(self):
|
||||||
|
self.assertEqual(ws.getdefaulttimeout(), None)
|
||||||
|
ws.setdefaulttimeout(10)
|
||||||
|
self.assertEqual(ws.getdefaulttimeout(), 10)
|
||||||
|
ws.setdefaulttimeout(None)
|
||||||
|
|
||||||
|
def testWSKey(self):
|
||||||
|
key = _create_sec_websocket_key()
|
||||||
|
self.assertTrue(key != 24)
|
||||||
|
self.assertTrue(six.u("¥n") not in key)
|
||||||
|
|
||||||
|
def testNonce(self):
|
||||||
|
""" WebSocket key should be a random 16-byte nonce.
|
||||||
|
"""
|
||||||
|
key = _create_sec_websocket_key()
|
||||||
|
nonce = base64decode(key.encode("utf-8"))
|
||||||
|
self.assertEqual(16, len(nonce))
|
||||||
|
|
||||||
|
def testWsUtils(self):
|
||||||
|
key = "c6b8hTg4EeGb2gQMztV1/g=="
|
||||||
|
required_header = {
|
||||||
|
"upgrade": "websocket",
|
||||||
|
"connection": "upgrade",
|
||||||
|
"sec-websocket-accept": "Kxep+hNu9n51529fGidYu7a3wO0="}
|
||||||
|
self.assertEqual(_validate_header(required_header, key, None), (True, None))
|
||||||
|
|
||||||
|
header = required_header.copy()
|
||||||
|
header["upgrade"] = "http"
|
||||||
|
self.assertEqual(_validate_header(header, key, None), (False, None))
|
||||||
|
del header["upgrade"]
|
||||||
|
self.assertEqual(_validate_header(header, key, None), (False, None))
|
||||||
|
|
||||||
|
header = required_header.copy()
|
||||||
|
header["connection"] = "something"
|
||||||
|
self.assertEqual(_validate_header(header, key, None), (False, None))
|
||||||
|
del header["connection"]
|
||||||
|
self.assertEqual(_validate_header(header, key, None), (False, None))
|
||||||
|
|
||||||
|
header = required_header.copy()
|
||||||
|
header["sec-websocket-accept"] = "something"
|
||||||
|
self.assertEqual(_validate_header(header, key, None), (False, None))
|
||||||
|
del header["sec-websocket-accept"]
|
||||||
|
self.assertEqual(_validate_header(header, key, None), (False, None))
|
||||||
|
|
||||||
|
header = required_header.copy()
|
||||||
|
header["sec-websocket-protocol"] = "sub1"
|
||||||
|
self.assertEqual(_validate_header(header, key, ["sub1", "sub2"]), (True, "sub1"))
|
||||||
|
self.assertEqual(_validate_header(header, key, ["sub2", "sub3"]), (False, None))
|
||||||
|
|
||||||
|
header = required_header.copy()
|
||||||
|
header["sec-websocket-protocol"] = "sUb1"
|
||||||
|
self.assertEqual(_validate_header(header, key, ["Sub1", "suB2"]), (True, "sub1"))
|
||||||
|
|
||||||
|
header = required_header.copy()
|
||||||
|
self.assertEqual(_validate_header(header, key, ["Sub1", "suB2"]), (False, None))
|
||||||
|
|
||||||
|
def testReadHeader(self):
|
||||||
|
status, header, status_message = read_headers(HeaderSockMock("data/header01.txt"))
|
||||||
|
self.assertEqual(status, 101)
|
||||||
|
self.assertEqual(header["connection"], "Upgrade")
|
||||||
|
|
||||||
|
status, header, status_message = read_headers(HeaderSockMock("data/header03.txt"))
|
||||||
|
self.assertEqual(status, 101)
|
||||||
|
self.assertEqual(header["connection"], "Upgrade, Keep-Alive")
|
||||||
|
|
||||||
|
HeaderSockMock("data/header02.txt")
|
||||||
|
self.assertRaises(ws.WebSocketException, read_headers, HeaderSockMock("data/header02.txt"))
|
||||||
|
|
||||||
|
def testSend(self):
|
||||||
|
# TODO: add longer frame data
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
sock.set_mask_key(create_mask_key)
|
||||||
|
s = sock.sock = HeaderSockMock("data/header01.txt")
|
||||||
|
sock.send("Hello")
|
||||||
|
self.assertEqual(s.sent[0], six.b("\x81\x85abcd)\x07\x0f\x08\x0e"))
|
||||||
|
|
||||||
|
sock.send("こんにちは")
|
||||||
|
self.assertEqual(s.sent[1], six.b("\x81\x8fabcd\x82\xe3\xf0\x87\xe3\xf1\x80\xe5\xca\x81\xe2\xc5\x82\xe3\xcc"))
|
||||||
|
|
||||||
|
sock.send(u"こんにちは")
|
||||||
|
self.assertEqual(s.sent[1], six.b("\x81\x8fabcd\x82\xe3\xf0\x87\xe3\xf1\x80\xe5\xca\x81\xe2\xc5\x82\xe3\xcc"))
|
||||||
|
|
||||||
|
# sock.send("x" * 5000)
|
||||||
|
# self.assertEqual(s.sent[1], six.b("\x81\x8fabcd\x82\xe3\xf0\x87\xe3\xf1\x80\xe5\xca\x81\xe2\xc5\x82\xe3\xcc"))
|
||||||
|
|
||||||
|
self.assertEqual(sock.send_binary(b'1111111111101'), 19)
|
||||||
|
|
||||||
|
def testRecv(self):
|
||||||
|
# TODO: add longer frame data
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
something = six.b("\x81\x8fabcd\x82\xe3\xf0\x87\xe3\xf1\x80\xe5\xca\x81\xe2\xc5\x82\xe3\xcc")
|
||||||
|
s.add_packet(something)
|
||||||
|
data = sock.recv()
|
||||||
|
self.assertEqual(data, "こんにちは")
|
||||||
|
|
||||||
|
s.add_packet(six.b("\x81\x85abcd)\x07\x0f\x08\x0e"))
|
||||||
|
data = sock.recv()
|
||||||
|
self.assertEqual(data, "Hello")
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testIter(self):
|
||||||
|
count = 2
|
||||||
|
for _ in ws.create_connection('wss://stream.meetup.com/2/rsvps'):
|
||||||
|
count -= 1
|
||||||
|
if count == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testNext(self):
|
||||||
|
sock = ws.create_connection('wss://stream.meetup.com/2/rsvps')
|
||||||
|
self.assertEqual(str, type(next(sock)))
|
||||||
|
|
||||||
|
def testInternalRecvStrict(self):
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
s.add_packet(six.b("foo"))
|
||||||
|
s.add_packet(socket.timeout())
|
||||||
|
s.add_packet(six.b("bar"))
|
||||||
|
# s.add_packet(SSLError("The read operation timed out"))
|
||||||
|
s.add_packet(six.b("baz"))
|
||||||
|
with self.assertRaises(ws.WebSocketTimeoutException):
|
||||||
|
sock.frame_buffer.recv_strict(9)
|
||||||
|
# if six.PY2:
|
||||||
|
# with self.assertRaises(ws.WebSocketTimeoutException):
|
||||||
|
# data = sock._recv_strict(9)
|
||||||
|
# else:
|
||||||
|
# with self.assertRaises(SSLError):
|
||||||
|
# data = sock._recv_strict(9)
|
||||||
|
data = sock.frame_buffer.recv_strict(9)
|
||||||
|
self.assertEqual(data, six.b("foobarbaz"))
|
||||||
|
with self.assertRaises(ws.WebSocketConnectionClosedException):
|
||||||
|
sock.frame_buffer.recv_strict(1)
|
||||||
|
|
||||||
|
def testRecvTimeout(self):
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
s.add_packet(six.b("\x81"))
|
||||||
|
s.add_packet(socket.timeout())
|
||||||
|
s.add_packet(six.b("\x8dabcd\x29\x07\x0f\x08\x0e"))
|
||||||
|
s.add_packet(socket.timeout())
|
||||||
|
s.add_packet(six.b("\x4e\x43\x33\x0e\x10\x0f\x00\x40"))
|
||||||
|
with self.assertRaises(ws.WebSocketTimeoutException):
|
||||||
|
sock.recv()
|
||||||
|
with self.assertRaises(ws.WebSocketTimeoutException):
|
||||||
|
sock.recv()
|
||||||
|
data = sock.recv()
|
||||||
|
self.assertEqual(data, "Hello, World!")
|
||||||
|
with self.assertRaises(ws.WebSocketConnectionClosedException):
|
||||||
|
sock.recv()
|
||||||
|
|
||||||
|
def testRecvWithSimpleFragmentation(self):
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
# OPCODE=TEXT, FIN=0, MSG="Brevity is "
|
||||||
|
s.add_packet(six.b("\x01\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C"))
|
||||||
|
# OPCODE=CONT, FIN=1, MSG="the soul of wit"
|
||||||
|
s.add_packet(six.b("\x80\x8fabcd\x15\n\x06D\x12\r\x16\x08A\r\x05D\x16\x0b\x17"))
|
||||||
|
data = sock.recv()
|
||||||
|
self.assertEqual(data, "Brevity is the soul of wit")
|
||||||
|
with self.assertRaises(ws.WebSocketConnectionClosedException):
|
||||||
|
sock.recv()
|
||||||
|
|
||||||
|
def testRecvWithFireEventOfFragmentation(self):
|
||||||
|
sock = ws.WebSocket(fire_cont_frame=True)
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
# OPCODE=TEXT, FIN=0, MSG="Brevity is "
|
||||||
|
s.add_packet(six.b("\x01\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C"))
|
||||||
|
# OPCODE=CONT, FIN=0, MSG="Brevity is "
|
||||||
|
s.add_packet(six.b("\x00\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C"))
|
||||||
|
# OPCODE=CONT, FIN=1, MSG="the soul of wit"
|
||||||
|
s.add_packet(six.b("\x80\x8fabcd\x15\n\x06D\x12\r\x16\x08A\r\x05D\x16\x0b\x17"))
|
||||||
|
|
||||||
|
_, data = sock.recv_data()
|
||||||
|
self.assertEqual(data, six.b("Brevity is "))
|
||||||
|
_, data = sock.recv_data()
|
||||||
|
self.assertEqual(data, six.b("Brevity is "))
|
||||||
|
_, data = sock.recv_data()
|
||||||
|
self.assertEqual(data, six.b("the soul of wit"))
|
||||||
|
|
||||||
|
# OPCODE=CONT, FIN=0, MSG="Brevity is "
|
||||||
|
s.add_packet(six.b("\x80\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C"))
|
||||||
|
|
||||||
|
with self.assertRaises(ws.WebSocketException):
|
||||||
|
sock.recv_data()
|
||||||
|
|
||||||
|
with self.assertRaises(ws.WebSocketConnectionClosedException):
|
||||||
|
sock.recv()
|
||||||
|
|
||||||
|
def testClose(self):
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
sock.sock = SockMock()
|
||||||
|
sock.connected = True
|
||||||
|
sock.close()
|
||||||
|
self.assertEqual(sock.connected, False)
|
||||||
|
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
sock.connected = True
|
||||||
|
s.add_packet(six.b('\x88\x80\x17\x98p\x84'))
|
||||||
|
sock.recv()
|
||||||
|
self.assertEqual(sock.connected, False)
|
||||||
|
|
||||||
|
def testRecvContFragmentation(self):
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
# OPCODE=CONT, FIN=1, MSG="the soul of wit"
|
||||||
|
s.add_packet(six.b("\x80\x8fabcd\x15\n\x06D\x12\r\x16\x08A\r\x05D\x16\x0b\x17"))
|
||||||
|
self.assertRaises(ws.WebSocketException, sock.recv)
|
||||||
|
|
||||||
|
def testRecvWithProlongedFragmentation(self):
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
# OPCODE=TEXT, FIN=0, MSG="Once more unto the breach, "
|
||||||
|
s.add_packet(six.b("\x01\x9babcd.\x0c\x00\x01A\x0f\x0c\x16\x04B\x16\n\x15"
|
||||||
|
"\rC\x10\t\x07C\x06\x13\x07\x02\x07\tNC"))
|
||||||
|
# OPCODE=CONT, FIN=0, MSG="dear friends, "
|
||||||
|
s.add_packet(six.b("\x00\x8eabcd\x05\x07\x02\x16A\x04\x11\r\x04\x0c\x07"
|
||||||
|
"\x17MB"))
|
||||||
|
# OPCODE=CONT, FIN=1, MSG="once more"
|
||||||
|
s.add_packet(six.b("\x80\x89abcd\x0e\x0c\x00\x01A\x0f\x0c\x16\x04"))
|
||||||
|
data = sock.recv()
|
||||||
|
self.assertEqual(
|
||||||
|
data,
|
||||||
|
"Once more unto the breach, dear friends, once more")
|
||||||
|
with self.assertRaises(ws.WebSocketConnectionClosedException):
|
||||||
|
sock.recv()
|
||||||
|
|
||||||
|
def testRecvWithFragmentationAndControlFrame(self):
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
sock.set_mask_key(create_mask_key)
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
# OPCODE=TEXT, FIN=0, MSG="Too much "
|
||||||
|
s.add_packet(six.b("\x01\x89abcd5\r\x0cD\x0c\x17\x00\x0cA"))
|
||||||
|
# OPCODE=PING, FIN=1, MSG="Please PONG this"
|
||||||
|
s.add_packet(six.b("\x89\x90abcd1\x0e\x06\x05\x12\x07C4.,$D\x15\n\n\x17"))
|
||||||
|
# OPCODE=CONT, FIN=1, MSG="of a good thing"
|
||||||
|
s.add_packet(six.b("\x80\x8fabcd\x0e\x04C\x05A\x05\x0c\x0b\x05B\x17\x0c"
|
||||||
|
"\x08\x0c\x04"))
|
||||||
|
data = sock.recv()
|
||||||
|
self.assertEqual(data, "Too much of a good thing")
|
||||||
|
with self.assertRaises(ws.WebSocketConnectionClosedException):
|
||||||
|
sock.recv()
|
||||||
|
self.assertEqual(
|
||||||
|
s.sent[0],
|
||||||
|
six.b("\x8a\x90abcd1\x0e\x06\x05\x12\x07C4.,$D\x15\n\n\x17"))
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testWebSocket(self):
|
||||||
|
s = ws.create_connection("ws://echo.websocket.org/")
|
||||||
|
self.assertNotEqual(s, None)
|
||||||
|
s.send("Hello, World")
|
||||||
|
result = s.recv()
|
||||||
|
self.assertEqual(result, "Hello, World")
|
||||||
|
|
||||||
|
s.send(u"こにゃにゃちは、世界")
|
||||||
|
result = s.recv()
|
||||||
|
self.assertEqual(result, "こにゃにゃちは、世界")
|
||||||
|
self.assertRaises(ValueError, s.send_close, -1, "")
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testPingPong(self):
|
||||||
|
s = ws.create_connection("ws://echo.websocket.org/")
|
||||||
|
self.assertNotEqual(s, None)
|
||||||
|
s.ping("Hello")
|
||||||
|
s.pong("Hi")
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testSecureWebSocket(self):
|
||||||
|
import ssl
|
||||||
|
s = ws.create_connection("wss://api.bitfinex.com/ws/2")
|
||||||
|
self.assertNotEqual(s, None)
|
||||||
|
self.assertTrue(isinstance(s.sock, ssl.SSLSocket))
|
||||||
|
self.assertEqual(s.getstatus(), 101)
|
||||||
|
self.assertNotEqual(s.getheaders(), None)
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testWebSocketWithCustomHeader(self):
|
||||||
|
s = ws.create_connection("ws://echo.websocket.org/",
|
||||||
|
headers={"User-Agent": "PythonWebsocketClient"})
|
||||||
|
self.assertNotEqual(s, None)
|
||||||
|
s.send("Hello, World")
|
||||||
|
result = s.recv()
|
||||||
|
self.assertEqual(result, "Hello, World")
|
||||||
|
self.assertRaises(ValueError, s.close, -1, "")
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testAfterClose(self):
|
||||||
|
s = ws.create_connection("ws://echo.websocket.org/")
|
||||||
|
self.assertNotEqual(s, None)
|
||||||
|
s.close()
|
||||||
|
self.assertRaises(ws.WebSocketConnectionClosedException, s.send, "Hello")
|
||||||
|
self.assertRaises(ws.WebSocketConnectionClosedException, s.recv)
|
||||||
|
|
||||||
|
|
||||||
|
class SockOptTest(unittest.TestCase):
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testSockOpt(self):
|
||||||
|
sockopt = ((socket.IPPROTO_TCP, socket.TCP_NODELAY, 1),)
|
||||||
|
s = ws.create_connection("ws://echo.websocket.org", sockopt=sockopt)
|
||||||
|
self.assertNotEqual(s.sock.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY), 0)
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
|
||||||
|
class UtilsTest(unittest.TestCase):
|
||||||
|
def testUtf8Validator(self):
|
||||||
|
state = validate_utf8(six.b('\xf0\x90\x80\x80'))
|
||||||
|
self.assertEqual(state, True)
|
||||||
|
state = validate_utf8(six.b('\xce\xba\xe1\xbd\xb9\xcf\x83\xce\xbc\xce\xb5\xed\xa0\x80edited'))
|
||||||
|
self.assertEqual(state, False)
|
||||||
|
state = validate_utf8(six.b(''))
|
||||||
|
self.assertEqual(state, True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
|
@ -1,57 +1,160 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from json import loads
|
import json
|
||||||
from ssl import CERT_NONE
|
|
||||||
|
|
||||||
from . import backgroundthread, websocket, utils, companion, app, variables as v
|
from . import websocket
|
||||||
|
from . import backgroundthread, app, variables as v, utils, companion
|
||||||
|
|
||||||
###############################################################################
|
log = getLogger('PLEX.websocket')
|
||||||
|
|
||||||
LOG = getLogger('PLEX.websocket_client')
|
PMS_PATH = '/:/websockets/notifications'
|
||||||
|
|
||||||
###############################################################################
|
PMS_INTERESTING_MESSAGE_TYPES = ('playing', 'timeline', 'activity')
|
||||||
|
SETTINGS_STRING = '_status'
|
||||||
|
|
||||||
|
|
||||||
class WebSocket(backgroundthread.KillableThread):
|
def get_pms_uri():
|
||||||
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
|
uri = app.CONN.server
|
||||||
|
if not uri:
|
||||||
|
return
|
||||||
|
# Get the appropriate prefix for the websocket
|
||||||
|
if uri.startswith('https'):
|
||||||
|
uri = "wss%s" % uri[5:]
|
||||||
|
else:
|
||||||
|
uri = "ws%s" % uri[4:]
|
||||||
|
uri += PMS_PATH
|
||||||
|
log.debug('uri to connect pms websocket: %s', uri)
|
||||||
|
if app.ACCOUNT.pms_token:
|
||||||
|
uri += '?X-Plex-Token=' + app.ACCOUNT.pms_token
|
||||||
|
return uri
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.ws = None
|
|
||||||
self.redirect_uri = None
|
|
||||||
self.sleeptime = 0.0
|
|
||||||
super(WebSocket, self).__init__()
|
|
||||||
|
|
||||||
def close_websocket(self):
|
def get_alexa_uri():
|
||||||
if self.ws is not None:
|
if not app.ACCOUNT.plex_token:
|
||||||
self.ws.close()
|
return
|
||||||
self.ws = None
|
return ('wss://pubsub.plex.tv/sub/websockets/%s/%s?X-Plex-Token=%s'
|
||||||
|
% (app.ACCOUNT.plex_user_id,
|
||||||
|
v.PKC_MACHINE_IDENTIFIER,
|
||||||
|
app.ACCOUNT.plex_token))
|
||||||
|
|
||||||
def process(self, opcode, message):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def receive(self, ws):
|
def pms_on_message(ws, message):
|
||||||
# Not connected yet
|
"""
|
||||||
if ws is None:
|
Called when we receive a message from the PMS, e.g. when a new library
|
||||||
raise websocket.WebSocketConnectionClosedException
|
item has been added.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
message = json.loads(message)
|
||||||
|
except ValueError as err:
|
||||||
|
log.error('Error decoding PMS websocket message: %s', err)
|
||||||
|
log.error('message: %s', message)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
message = message['NotificationContainer']
|
||||||
|
typus = message['type']
|
||||||
|
except KeyError:
|
||||||
|
log.error('Could not parse PMS message: %s', message)
|
||||||
|
return
|
||||||
|
# Triage
|
||||||
|
if typus not in PMS_INTERESTING_MESSAGE_TYPES:
|
||||||
|
# Drop everything we're not interested in
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# Put PMS message on queue and let libsync take care of it
|
||||||
|
app.APP.websocket_queue.put(message)
|
||||||
|
|
||||||
frame = ws.recv_frame()
|
|
||||||
|
|
||||||
if not frame:
|
def alexa_on_message(ws, message):
|
||||||
raise websocket.WebSocketException("Not a valid frame %s" % frame)
|
"""
|
||||||
elif frame.opcode in self.opcode_data:
|
Called when we receive a message from Alexa
|
||||||
return frame.opcode, frame.data
|
"""
|
||||||
elif frame.opcode == websocket.ABNF.OPCODE_CLOSE:
|
log.debug('alexa message received: %s', message)
|
||||||
ws.send_close()
|
try:
|
||||||
return frame.opcode, None
|
message = utils.etree.fromstring(message)
|
||||||
elif frame.opcode == websocket.ABNF.OPCODE_PING:
|
except Exception as err:
|
||||||
ws.pong("Hi!")
|
log.error('Error decoding message from Alexa: %s %s', type(err), err)
|
||||||
return None, None
|
log.error('message from Alexa: %s', message)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
if message.attrib['command'] == 'processRemoteControlCommand':
|
||||||
|
message = message[0]
|
||||||
|
else:
|
||||||
|
log.error('Unknown Alexa message received: %s', message)
|
||||||
|
return
|
||||||
|
companion.process_command(message.attrib['path'][1:], message.attrib)
|
||||||
|
except Exception as err:
|
||||||
|
log.exception('Could not parse Alexa message, error: %s %s',
|
||||||
|
type(err), err)
|
||||||
|
log.error('message: %s', message)
|
||||||
|
|
||||||
def getUri(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def _sleep_cycle(self):
|
def on_error(ws, error):
|
||||||
|
status = ws.name + SETTINGS_STRING
|
||||||
|
if isinstance(error, IOError):
|
||||||
|
# We are probably offline
|
||||||
|
log.debug('%s: IOError connecting', ws.name)
|
||||||
|
# Status = IOError - not connected
|
||||||
|
utils.settings(status, value=utils.lang(39092))
|
||||||
|
ws.sleep_cycle()
|
||||||
|
elif isinstance(error, websocket.WebSocketTimeoutException):
|
||||||
|
log.debug('%s: WebSocketTimeoutException', ws.name)
|
||||||
|
# Status = 'Timeout - not connected'
|
||||||
|
utils.settings(status, value=utils.lang(39091))
|
||||||
|
ws.sleep_cycle()
|
||||||
|
elif isinstance(error, websocket.WebSocketConnectionClosedException):
|
||||||
|
log.debug('%s: WebSocketConnectionClosedException', ws.name)
|
||||||
|
# Status = Not connected
|
||||||
|
utils.settings(ws.name + SETTINGS_STRING, value=utils.lang(15208))
|
||||||
|
elif isinstance(error, websocket.WebSocketBadStatusException):
|
||||||
|
# Most likely Alexa not connecting, throwing a 403
|
||||||
|
log.debug('%s: got a bad HTTP status: %s', ws.name, error)
|
||||||
|
# Status = <value of exception>
|
||||||
|
utils.settings(status, value=str(error))
|
||||||
|
ws.sleep_cycle()
|
||||||
|
elif isinstance(error, websocket.WebSocketException):
|
||||||
|
log.error('%s: got another websocket exception %s: %s',
|
||||||
|
ws.name, type(error), error)
|
||||||
|
# Status = Error
|
||||||
|
utils.settings(status, value=utils.lang(257))
|
||||||
|
ws.sleep_cycle()
|
||||||
|
elif isinstance(error, SystemExit):
|
||||||
|
log.debug('%s: SystemExit detected', ws.name)
|
||||||
|
# Status = Not connected
|
||||||
|
utils.settings(ws.name + SETTINGS_STRING, value=utils.lang(15208))
|
||||||
|
else:
|
||||||
|
log.exception('%s: got an unexpected exception of type %s: %s',
|
||||||
|
ws.name, type(error), error)
|
||||||
|
# Status = Error
|
||||||
|
utils.settings(status, value=utils.lang(257))
|
||||||
|
raise RuntimeError
|
||||||
|
|
||||||
|
|
||||||
|
def on_close(ws):
|
||||||
|
"""
|
||||||
|
This does not seem to get called by our websocket client :-(
|
||||||
|
"""
|
||||||
|
log.debug('%s: connection closed', ws.name)
|
||||||
|
# Status = Not connected
|
||||||
|
utils.settings(ws.name + SETTINGS_STRING, value=utils.lang(15208))
|
||||||
|
|
||||||
|
|
||||||
|
def on_open(ws):
|
||||||
|
log.debug('%s: connected', ws.name)
|
||||||
|
# Status = Connected
|
||||||
|
utils.settings(ws.name + SETTINGS_STRING, value=utils.lang(13296))
|
||||||
|
ws.sleeptime = 0
|
||||||
|
|
||||||
|
|
||||||
|
class PlexWebSocketApp(websocket.WebSocketApp,
|
||||||
|
backgroundthread.KillableThread):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.sleeptime = 0
|
||||||
|
backgroundthread.KillableThread.__init__(self)
|
||||||
|
websocket.WebSocketApp.__init__(self, self.get_uri(), **kwargs)
|
||||||
|
|
||||||
|
def sleep_cycle(self):
|
||||||
"""
|
"""
|
||||||
Sleeps for 2^self.sleeptime where sleeping period will be doubled with
|
Sleeps for 2^self.sleeptime where sleeping period will be doubled with
|
||||||
each unsuccessful connection attempt.
|
each unsuccessful connection attempt.
|
||||||
|
@ -59,203 +162,141 @@ class WebSocket(backgroundthread.KillableThread):
|
||||||
"""
|
"""
|
||||||
self.sleep(2 ** self.sleeptime)
|
self.sleep(2 ** self.sleeptime)
|
||||||
if self.sleeptime < 6:
|
if self.sleeptime < 6:
|
||||||
self.sleeptime += 1.0
|
self.sleeptime += 1
|
||||||
|
|
||||||
|
def close(self, **kwargs):
|
||||||
|
"""websocket.WebSocketApp is not yet thread-safe. close() might
|
||||||
|
encounter websockets that have already been closed"""
|
||||||
|
try:
|
||||||
|
websocket.WebSocketApp.close(self, **kwargs)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def suspend(self, block=False, timeout=None):
|
||||||
|
"""
|
||||||
|
Call this method from another thread to suspend this websocket thread
|
||||||
|
"""
|
||||||
|
self.close()
|
||||||
|
backgroundthread.KillableThread.suspend(self, block, timeout)
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
"""
|
||||||
|
Call this method from another thread to cancel this websocket thread
|
||||||
|
"""
|
||||||
|
self.close()
|
||||||
|
backgroundthread.KillableThread.cancel(self)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
LOG.info("----===## Starting %s ##===----", self.__class__.__name__)
|
"""
|
||||||
|
Ensure that sockets will be closed no matter what
|
||||||
|
"""
|
||||||
|
log.info("----===## Starting %s ##===----", self.name)
|
||||||
app.APP.register_thread(self)
|
app.APP.register_thread(self)
|
||||||
try:
|
try:
|
||||||
self._run()
|
self._run()
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
except Exception as err:
|
||||||
|
log.exception('Exception of type %s occured: %s', type(err), err)
|
||||||
finally:
|
finally:
|
||||||
self.close_websocket()
|
self.close()
|
||||||
|
# Status = Not connected
|
||||||
|
utils.settings(self.name + SETTINGS_STRING,
|
||||||
|
value=utils.lang(15208))
|
||||||
app.APP.deregister_thread(self)
|
app.APP.deregister_thread(self)
|
||||||
LOG.info("##===---- %s Stopped ----===##", self.__class__.__name__)
|
log.info("----===## %s stopped ##===----", self.name)
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
while not self.should_cancel():
|
while not self.should_cancel():
|
||||||
# In the event the server goes offline
|
# In the event the server goes offline
|
||||||
if self.should_suspend():
|
while self.should_suspend():
|
||||||
# Set in service.py
|
# We will be caught in this loop if either another thread
|
||||||
self.close_websocket()
|
# called the suspend() method, thus setting _suspended = True
|
||||||
|
# OR if there any other conditions to not open a websocket
|
||||||
|
# connection - see methods should_suspend() below
|
||||||
|
# Status = Suspended - not connected
|
||||||
|
self.set_suspension_settings_status()
|
||||||
if self.wait_while_suspended():
|
if self.wait_while_suspended():
|
||||||
# Abort was requested while waiting. We should exit
|
# Abort was requested while waiting. We should exit
|
||||||
return
|
return
|
||||||
try:
|
if not self._suspended:
|
||||||
self.process(*self.receive(self.ws))
|
# because wait_while_suspended will return instantly if
|
||||||
except websocket.WebSocketTimeoutException:
|
# this thread did not get suspended from another thread
|
||||||
# No worries if read timed out
|
self.sleep_cycle()
|
||||||
pass
|
self.url = self.get_uri()
|
||||||
except websocket.WebSocketConnectionClosedException:
|
if not self.url:
|
||||||
LOG.debug("%s: connection closed, (re)connecting",
|
self.sleep_cycle()
|
||||||
self.__class__.__name__)
|
continue
|
||||||
uri, sslopt = self.getUri()
|
self.run_forever()
|
||||||
try:
|
|
||||||
# Low timeout - let's us shut this thread down!
|
|
||||||
self.ws = websocket.create_connection(
|
|
||||||
uri,
|
|
||||||
timeout=1,
|
|
||||||
sslopt=sslopt,
|
|
||||||
enable_multithread=True)
|
|
||||||
except IOError:
|
|
||||||
# Server is probably offline
|
|
||||||
LOG.debug("%s: IOError connecting", self.__class__.__name__)
|
|
||||||
self.ws = None
|
|
||||||
self._sleep_cycle()
|
|
||||||
except websocket.WebSocketTimeoutException:
|
|
||||||
LOG.debug("%s: WebSocketTimeoutException", self.__class__.__name__)
|
|
||||||
self.ws = None
|
|
||||||
self._sleep_cycle()
|
|
||||||
except websocket.WebsocketRedirect as e:
|
|
||||||
LOG.debug('301 redirect detected: %s', e)
|
|
||||||
self.redirect_uri = e.headers.get('location',
|
|
||||||
e.headers.get('Location'))
|
|
||||||
if self.redirect_uri:
|
|
||||||
self.redirect_uri = self.redirect_uri.decode('utf-8')
|
|
||||||
self.ws = None
|
|
||||||
self._sleep_cycle()
|
|
||||||
except websocket.WebSocketException as e:
|
|
||||||
LOG.debug('%s: WebSocketException: %s', self.__class__.__name__, e)
|
|
||||||
self.ws = None
|
|
||||||
self._sleep_cycle()
|
|
||||||
except Exception as e:
|
|
||||||
LOG.error('%s: Unknown exception encountered when '
|
|
||||||
'connecting: %s', self.__class__.__name__, e)
|
|
||||||
import traceback
|
|
||||||
LOG.error("%s: Traceback:\n%s",
|
|
||||||
self.__class__.__name__, traceback.format_exc())
|
|
||||||
self.ws = None
|
|
||||||
self._sleep_cycle()
|
|
||||||
else:
|
|
||||||
self.sleeptime = 0.0
|
|
||||||
except Exception as e:
|
|
||||||
LOG.error("%s: Unknown exception encountered: %s",
|
|
||||||
self.__class__.__name__, e)
|
|
||||||
import traceback
|
|
||||||
LOG.error("%s: Traceback:\n%s",
|
|
||||||
self.__class__.__name__, traceback.format_exc())
|
|
||||||
self.close_websocket()
|
|
||||||
|
|
||||||
|
|
||||||
class PMS_Websocket(WebSocket):
|
class PMSWebsocketApp(PlexWebSocketApp):
|
||||||
"""
|
name = 'pms_websocket'
|
||||||
Websocket connection with the PMS for Plex Companion
|
|
||||||
"""
|
def get_uri(self):
|
||||||
|
return get_pms_uri()
|
||||||
|
|
||||||
def should_suspend(self):
|
def should_suspend(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the thread is suspended.
|
Returns True if the thread needs to suspend.
|
||||||
"""
|
"""
|
||||||
suspend = self._suspended or app.SYNC.background_sync_disabled
|
return (self._suspended or
|
||||||
if suspend:
|
utils.settings('enableBackgroundSync') != 'true')
|
||||||
# This thread needs to clear the Event() _is_not_suspended itself!
|
|
||||||
self.suspend()
|
|
||||||
return suspend
|
|
||||||
|
|
||||||
def getUri(self):
|
def set_suspension_settings_status(self):
|
||||||
if self.redirect_uri:
|
if utils.settings('enableBackgroundSync') != 'true':
|
||||||
uri = self.redirect_uri
|
# Status = Disabled
|
||||||
self.redirect_uri = None
|
utils.settings(self.name + SETTINGS_STRING,
|
||||||
|
value=utils.lang(24023))
|
||||||
else:
|
else:
|
||||||
server = app.CONN.server
|
# Status = 'Suspended - not connected'
|
||||||
# Get the appropriate prefix for the websocket
|
utils.settings(self.name + SETTINGS_STRING,
|
||||||
if server.startswith('https'):
|
value=utils.lang(39093))
|
||||||
server = "wss%s" % server[5:]
|
|
||||||
else:
|
|
||||||
server = "ws%s" % server[4:]
|
|
||||||
uri = "%s/:/websockets/notifications" % server
|
|
||||||
if app.ACCOUNT.pms_token:
|
|
||||||
uri += '?X-Plex-Token=%s' % app.ACCOUNT.pms_token
|
|
||||||
sslopt = {}
|
|
||||||
if v.KODIVERSION == 17 and utils.settings('sslverify') == "false":
|
|
||||||
sslopt["cert_reqs"] = CERT_NONE
|
|
||||||
LOG.debug("%s: Uri: %s, sslopt: %s",
|
|
||||||
self.__class__.__name__, uri, sslopt)
|
|
||||||
return uri, sslopt
|
|
||||||
|
|
||||||
def process(self, opcode, message):
|
|
||||||
if opcode not in self.opcode_data:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
message = loads(message)
|
|
||||||
except ValueError:
|
|
||||||
LOG.error('%s: Error decoding message from websocket',
|
|
||||||
self.__class__.__name__)
|
|
||||||
LOG.error(message)
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
message = message['NotificationContainer']
|
|
||||||
except KeyError:
|
|
||||||
LOG.error('%s: Could not parse PMS message: %s',
|
|
||||||
self.__class__.__name__, message)
|
|
||||||
return
|
|
||||||
# Triage
|
|
||||||
typus = message.get('type')
|
|
||||||
if typus is None:
|
|
||||||
LOG.error('%s: No message type, dropping message: %s',
|
|
||||||
self.__class__.__name__, message)
|
|
||||||
return
|
|
||||||
LOG.debug('%s: Received message from PMS server: %s',
|
|
||||||
self.__class__.__name__, message)
|
|
||||||
# Drop everything we're not interested in
|
|
||||||
if typus not in ('playing', 'timeline', 'activity'):
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
# Put PMS message on queue and let libsync take care of it
|
|
||||||
app.APP.websocket_queue.put(message)
|
|
||||||
|
|
||||||
|
|
||||||
class Alexa_Websocket(WebSocket):
|
class AlexaWebsocketApp(PlexWebSocketApp):
|
||||||
"""
|
name = 'alexa_websocket'
|
||||||
Websocket connection to talk to Amazon Alexa.
|
|
||||||
"""
|
def get_uri(self):
|
||||||
|
return get_alexa_uri()
|
||||||
|
|
||||||
def should_suspend(self):
|
def should_suspend(self):
|
||||||
"""
|
"""
|
||||||
Overwrite method since we need to check for plex token
|
Returns True if the thread needs to suspend.
|
||||||
"""
|
"""
|
||||||
suspend = self._suspended or \
|
return self._suspended or \
|
||||||
not app.SYNC.enable_alexa or \
|
utils.settings('enable_alexa') != 'true' or \
|
||||||
app.ACCOUNT.restricted_user or \
|
app.ACCOUNT.restricted_user or \
|
||||||
not app.ACCOUNT.plex_token
|
not app.ACCOUNT.plex_token
|
||||||
if suspend:
|
|
||||||
# This thread needs to clear the Event() _is_not_suspended itself!
|
|
||||||
self.suspend()
|
|
||||||
return suspend
|
|
||||||
|
|
||||||
def getUri(self):
|
def set_suspension_settings_status(self):
|
||||||
if self.redirect_uri:
|
if utils.settings('enable_alexa') != 'true':
|
||||||
uri = self.redirect_uri
|
# Status = Disabled
|
||||||
self.redirect_uri = None
|
utils.settings(self.name + SETTINGS_STRING,
|
||||||
|
value=utils.lang(24023))
|
||||||
|
elif app.ACCOUNT.restricted_user:
|
||||||
|
# Status = Managed Plex User - not connected
|
||||||
|
utils.settings(self.name + SETTINGS_STRING,
|
||||||
|
value=utils.lang(39094))
|
||||||
|
elif not app.ACCOUNT.plex_token:
|
||||||
|
# Status = Not logged in to plex.tv
|
||||||
|
utils.settings(self.name + SETTINGS_STRING,
|
||||||
|
value=utils.lang(39226))
|
||||||
else:
|
else:
|
||||||
uri = ('wss://pubsub.plex.tv/sub/websockets/%s/%s?X-Plex-Token=%s'
|
# Status = 'Suspended - not connected'
|
||||||
% (app.ACCOUNT.plex_user_id,
|
utils.settings(self.name + SETTINGS_STRING,
|
||||||
v.PKC_MACHINE_IDENTIFIER,
|
value=utils.lang(39093))
|
||||||
app.ACCOUNT.plex_token))
|
|
||||||
sslopt = {}
|
|
||||||
LOG.debug("%s: Uri: %s, sslopt: %s",
|
|
||||||
self.__class__.__name__, uri, sslopt)
|
|
||||||
return uri, sslopt
|
|
||||||
|
|
||||||
def process(self, opcode, message):
|
|
||||||
if opcode not in self.opcode_data:
|
def get_pms_websocketapp():
|
||||||
return
|
return PMSWebsocketApp(on_open=on_open,
|
||||||
LOG.debug('%s: Received the following message from Alexa:',
|
on_message=pms_on_message,
|
||||||
self.__class__.__name__)
|
on_error=on_error,
|
||||||
LOG.debug('%s: %s', self.__class__.__name__, message)
|
on_close=on_close)
|
||||||
try:
|
|
||||||
message = utils.defused_etree.fromstring(message)
|
|
||||||
except Exception as ex:
|
def get_alexa_websocketapp():
|
||||||
LOG.error('%s: Error decoding message from Alexa: %s',
|
return AlexaWebsocketApp(on_open=on_open,
|
||||||
self.__class__.__name__, ex)
|
on_message=alexa_on_message,
|
||||||
return
|
on_error=on_error,
|
||||||
try:
|
on_close=on_close)
|
||||||
if message.attrib['command'] == 'processRemoteControlCommand':
|
|
||||||
message = message[0]
|
|
||||||
else:
|
|
||||||
LOG.error('%s: Unknown Alexa message received',
|
|
||||||
self.__class__.__name__)
|
|
||||||
return
|
|
||||||
except Exception:
|
|
||||||
LOG.error('%s: Could not parse Alexa message',
|
|
||||||
self.__class__.__name__)
|
|
||||||
return
|
|
||||||
companion.process_command(message.attrib['path'][1:], message.attrib)
|
|
||||||
|
|
|
@ -131,75 +131,89 @@ def _generate_content(api):
|
||||||
# Item is synched to the Kodi db - let's use that info
|
# Item is synched to the Kodi db - let's use that info
|
||||||
# (will thus e.g. include additional artwork or metadata)
|
# (will thus e.g. include additional artwork or metadata)
|
||||||
item = js.item_details(api.kodi_id, api.kodi_type)
|
item = js.item_details(api.kodi_id, api.kodi_type)
|
||||||
else:
|
|
||||||
cast = [{
|
|
||||||
'name': x[0],
|
|
||||||
'thumbnail': x[1],
|
|
||||||
'role': x[2],
|
|
||||||
'order': x[3],
|
|
||||||
} for x in api.people()['actor']]
|
|
||||||
item = {
|
|
||||||
'cast': cast,
|
|
||||||
'country': api.countries(),
|
|
||||||
'dateadded': api.date_created(), # e.g '2019-01-03 19:40:59'
|
|
||||||
'director': api.directors(), # list of [str]
|
|
||||||
'duration': api.runtime(),
|
|
||||||
'episode': api.index(),
|
|
||||||
# 'file': '', # e.g. 'videodb://tvshows/titles/20'
|
|
||||||
'genre': api.genres(),
|
|
||||||
# 'imdbnumber': '', # e.g.'341663'
|
|
||||||
'label': api.title(), # e.g. '1x05. Category 55 Emergency Doomsday Crisis'
|
|
||||||
'lastplayed': api.lastplayed(), # e.g. '2019-01-04 16:05:03'
|
|
||||||
'mpaa': api.content_rating(), # e.g. 'TV-MA'
|
|
||||||
'originaltitle': '', # e.g. 'Titans (2018)'
|
|
||||||
'playcount': api.viewcount(), # [int]
|
|
||||||
'plot': api.plot(), # [str]
|
|
||||||
'plotoutline': api.tagline(),
|
|
||||||
'premiered': api.premiere_date(), # '2018-10-12'
|
|
||||||
'rating': api.rating(), # [float]
|
|
||||||
'season': api.season_number(),
|
|
||||||
'sorttitle': api.sorttitle(), # 'Titans (2018)'
|
|
||||||
'studio': api.studios(),
|
|
||||||
'tag': [], # List of tags this item belongs to
|
|
||||||
'tagline': api.tagline(),
|
|
||||||
'thumbnail': '', # e.g. 'image://https%3a%2f%2fassets.tv'
|
|
||||||
'title': api.title(), # 'Titans (2018)'
|
|
||||||
'type': api.kodi_type,
|
|
||||||
'trailer': api.trailer(),
|
|
||||||
'tvshowtitle': api.show_title(),
|
|
||||||
'uniqueid': {
|
|
||||||
'imdbnumber': api.guids.get('imdb') or '',
|
|
||||||
'tvdb_id': api.guids.get('tvdb') or '',
|
|
||||||
'tmdb_id': api.guids.get('tmdb') or ''
|
|
||||||
},
|
|
||||||
'votes': '0', # [str]!
|
|
||||||
'writer': api.writers(), # list of [str]
|
|
||||||
'year': api.year(), # [int]
|
|
||||||
}
|
|
||||||
|
|
||||||
if plex_type in (v.PLEX_TYPE_EPISODE, v.PLEX_TYPE_SEASON, v.PLEX_TYPE_SHOW):
|
# In rare cases, Kodi's JSON reply does not provide 'title' plus potentially
|
||||||
leaves = api.leave_count()
|
# other fields - let's use the PMS answer to be safe
|
||||||
if leaves:
|
# See https://github.com/croneter/PlexKodiConnect/issues/1129
|
||||||
item['extraproperties'] = leaves
|
if not api.kodi_id or 'title' not in item:
|
||||||
|
if api.plex_type == v.PLEX_TYPE_PHOTO:
|
||||||
|
item = {
|
||||||
|
'title': api.title(),
|
||||||
|
'label': api.title(),
|
||||||
|
'type': api.kodi_type,
|
||||||
|
'dateadded': api.date_created(), # e.g '2019-01-03 19:40:59'
|
||||||
|
'lastplayed': api.lastplayed(), # e.g. '2019-01-04 16:05:03'
|
||||||
|
'playcount': api.viewcount(),
|
||||||
|
}
|
||||||
|
item.update(api.picture_codec())
|
||||||
|
else:
|
||||||
|
cast = [{
|
||||||
|
'name': x[0],
|
||||||
|
'thumbnail': x[1],
|
||||||
|
'role': x[2],
|
||||||
|
'order': x[3],
|
||||||
|
} for x in api.people()['actor']]
|
||||||
|
item = {
|
||||||
|
'cast': cast,
|
||||||
|
'country': api.countries(),
|
||||||
|
'dateadded': api.date_created(), # e.g '2019-01-03 19:40:59'
|
||||||
|
'director': api.directors(), # list of [str]
|
||||||
|
'duration': api.runtime(),
|
||||||
|
'episode': api.index(),
|
||||||
|
# 'file': '', # e.g. 'videodb://tvshows/titles/20'
|
||||||
|
'genre': api.genres(),
|
||||||
|
# 'imdbnumber': '', # e.g.'341663'
|
||||||
|
'label': api.title(), # e.g. '1x05. Category 55 Emergency Doomsday Crisis'
|
||||||
|
'lastplayed': api.lastplayed(), # e.g. '2019-01-04 16:05:03'
|
||||||
|
'mpaa': api.content_rating(), # e.g. 'TV-MA'
|
||||||
|
'originaltitle': '', # e.g. 'Titans (2018)'
|
||||||
|
'playcount': api.viewcount(), # [int]
|
||||||
|
'plot': api.plot(), # [str]
|
||||||
|
'plotoutline': api.tagline(),
|
||||||
|
'premiered': api.premiere_date(), # '2018-10-12'
|
||||||
|
'rating': api.rating(), # [float]
|
||||||
|
'season': api.season_number(),
|
||||||
|
'sorttitle': api.sorttitle(), # 'Titans (2018)'
|
||||||
|
'studio': api.studios(),
|
||||||
|
'tag': [], # List of tags this item belongs to
|
||||||
|
'tagline': api.tagline(),
|
||||||
|
'thumbnail': '', # e.g. 'image://https%3a%2f%2fassets.tv'
|
||||||
|
'title': api.title(), # 'Titans (2018)'
|
||||||
|
'type': api.kodi_type,
|
||||||
|
'trailer': api.trailer(),
|
||||||
|
'tvshowtitle': api.show_title(),
|
||||||
|
'uniqueid': {
|
||||||
|
'imdbnumber': api.guids.get('imdb') or '',
|
||||||
|
'tvdb_id': api.guids.get('tvdb') or '',
|
||||||
|
'tmdb_id': api.guids.get('tmdb') or ''
|
||||||
|
},
|
||||||
|
'votes': '0', # [str]!
|
||||||
|
'writer': api.writers(), # list of [str]
|
||||||
|
'year': api.year(), # [int]
|
||||||
|
}
|
||||||
|
# Add all info for e.g. video and audio streams
|
||||||
|
item['streamdetails'] = api.mediastreams()
|
||||||
|
# Cleanup required due to the way metadatautils works
|
||||||
|
if not item['lastplayed']:
|
||||||
|
del item['lastplayed']
|
||||||
|
for stream in item['streamdetails']['video']:
|
||||||
|
stream['height'] = utils.cast(int, stream['height'])
|
||||||
|
stream['width'] = utils.cast(int, stream['width'])
|
||||||
|
stream['aspect'] = utils.cast(float, stream['aspect'])
|
||||||
|
item['streamdetails']['subtitle'] = [{'language': x} for x in item['streamdetails']['subtitle']]
|
||||||
|
# Resume point
|
||||||
|
resume = api.resume_point()
|
||||||
|
if resume:
|
||||||
|
item['resume'] = {
|
||||||
|
'position': resume,
|
||||||
|
'total': api.runtime()
|
||||||
|
}
|
||||||
|
if plex_type in (v.PLEX_TYPE_EPISODE, v.PLEX_TYPE_SEASON, v.PLEX_TYPE_SHOW):
|
||||||
|
leaves = api.leave_count()
|
||||||
|
if leaves:
|
||||||
|
item['extraproperties'] = leaves
|
||||||
# Add all the artwork we can
|
# Add all the artwork we can
|
||||||
item['art'] = api.artwork(full_artwork=True)
|
item['art'] = api.artwork(full_artwork=True)
|
||||||
# Add all info for e.g. video and audio streams
|
|
||||||
item['streamdetails'] = api.mediastreams()
|
|
||||||
# Cleanup required due to the way metadatautils works
|
|
||||||
if not item['lastplayed']:
|
|
||||||
del item['lastplayed']
|
|
||||||
for stream in item['streamdetails']['video']:
|
|
||||||
stream['height'] = utils.cast(int, stream['height'])
|
|
||||||
stream['width'] = utils.cast(int, stream['width'])
|
|
||||||
stream['aspect'] = utils.cast(float, stream['aspect'])
|
|
||||||
item['streamdetails']['subtitle'] = [{'language': x} for x in item['streamdetails']['subtitle']]
|
|
||||||
# Resume point
|
|
||||||
resume = api.resume_point()
|
|
||||||
if resume:
|
|
||||||
item['resume'] = {
|
|
||||||
'position': resume,
|
|
||||||
'total': api.runtime()
|
|
||||||
}
|
|
||||||
|
|
||||||
item['icon'] = v.ICON_FROM_PLEXTYPE[plex_type]
|
item['icon'] = v.ICON_FROM_PLEXTYPE[plex_type]
|
||||||
# Some customization
|
# Some customization
|
||||||
|
@ -468,16 +482,18 @@ def create_listitem(item, as_tuple=True, offscreen=True,
|
||||||
elif "plugin://script.skin.helper" not in item['file']:
|
elif "plugin://script.skin.helper" not in item['file']:
|
||||||
liz.setProperty('IsPlayable', 'true')
|
liz.setProperty('IsPlayable', 'true')
|
||||||
|
|
||||||
nodetype = "Video"
|
|
||||||
if item["type"] in ["song", "album", "artist"]:
|
if item["type"] in ["song", "album", "artist"]:
|
||||||
nodetype = "Music"
|
nodetype = "music"
|
||||||
|
elif item['type'] == 'photo':
|
||||||
|
nodetype = 'pictures'
|
||||||
|
else:
|
||||||
|
nodetype = 'video'
|
||||||
|
|
||||||
# extra properties
|
# extra properties
|
||||||
for key, value in item["extraproperties"].iteritems():
|
for key, value in item["extraproperties"].iteritems():
|
||||||
liz.setProperty(key, value)
|
liz.setProperty(key, value)
|
||||||
|
|
||||||
# video infolabels
|
if nodetype == 'video':
|
||||||
if nodetype == "Video":
|
|
||||||
infolabels = {
|
infolabels = {
|
||||||
"title": item.get("title"),
|
"title": item.get("title"),
|
||||||
"size": item.get("size"),
|
"size": item.get("size"),
|
||||||
|
@ -528,8 +544,7 @@ def create_listitem(item, as_tuple=True, offscreen=True,
|
||||||
if "date" in item:
|
if "date" in item:
|
||||||
infolabels["date"] = item["date"]
|
infolabels["date"] = item["date"]
|
||||||
|
|
||||||
# music infolabels
|
elif nodetype == 'music':
|
||||||
else:
|
|
||||||
infolabels = {
|
infolabels = {
|
||||||
"title": item.get("title"),
|
"title": item.get("title"),
|
||||||
"size": item.get("size"),
|
"size": item.get("size"),
|
||||||
|
@ -549,12 +564,18 @@ def create_listitem(item, as_tuple=True, offscreen=True,
|
||||||
if "lastplayed" in item:
|
if "lastplayed" in item:
|
||||||
infolabels["lastplayed"] = item["lastplayed"]
|
infolabels["lastplayed"] = item["lastplayed"]
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Pictures
|
||||||
|
infolabels = {
|
||||||
|
"title": item.get("title"),
|
||||||
|
'picturepath': item['file']
|
||||||
|
}
|
||||||
# setting the dbtype and dbid is supported from kodi krypton and up
|
# setting the dbtype and dbid is supported from kodi krypton and up
|
||||||
# PKC hack: ignore empty type
|
# PKC hack: ignore empty type
|
||||||
if item["type"] not in ["recording", "channel", "favourite", ""]:
|
if item["type"] not in ["recording", "channel", "favourite", ""]:
|
||||||
infolabels["mediatype"] = item["type"]
|
infolabels["mediatype"] = item["type"]
|
||||||
# setting the dbid on music items is not supported ?
|
# setting the dbid on music items is not supported ?
|
||||||
if nodetype == "Video" and "DBID" in item["extraproperties"]:
|
if nodetype == "video" and "DBID" in item["extraproperties"]:
|
||||||
infolabels["dbid"] = item["extraproperties"]["DBID"]
|
infolabels["dbid"] = item["extraproperties"]["DBID"]
|
||||||
|
|
||||||
if "lastplayed" in item:
|
if "lastplayed" in item:
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
:module: plexkodiconnect.userselect
|
|
||||||
:synopsis: Prompts the user to add network paths and username passwords for
|
:synopsis: Prompts the user to add network paths and username passwords for
|
||||||
e.g. smb paths
|
e.g. smb paths
|
||||||
"""
|
"""
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue