Compare commits
403 commits
Author | SHA1 | Date | |
---|---|---|---|
|
10ff206c58 | ||
|
ce37820649 | ||
|
902c56b8f7 | ||
|
59a3dd0010 | ||
|
ab52521f73 | ||
|
9f18288e80 | ||
|
cf149558be | ||
|
d605cfd685 | ||
|
20f5d9d561 | ||
|
2850889c98 | ||
|
937265dfc9 | ||
|
49fce5a2cd | ||
|
0385c25e2f | ||
|
4f7e54591c | ||
|
0d41321d7f | ||
|
aa14e8259f | ||
|
f3754fa2e3 | ||
|
8c64e2a17c | ||
|
e6171127dc | ||
|
d37fbb6c1a | ||
|
c107eb2ed8 | ||
|
492e235a53 | ||
|
4d3e36fbdb | ||
|
d8db463423 | ||
|
f2e03e878e | ||
|
56a3cdbbd8 | ||
|
741ae76cf9 | ||
|
e0451e4a15 | ||
|
58e800b9d9 | ||
|
7d37da6177 | ||
|
ff5df50bb7 | ||
|
048f11a4ce | ||
|
aa917a233f | ||
|
e0e899c6d0 | ||
|
8bf9ca5a15 | ||
|
87eda6a207 | ||
|
56755cf506 | ||
|
b2634d6f96 | ||
|
389be72ee2 | ||
|
3a095e6ef6 | ||
|
d867ac537d | ||
|
3b7c6c535b | ||
|
266e2975a9 | ||
|
2d636d8e08 | ||
|
c737e1f662 | ||
|
a7e4d49db1 | ||
|
11572f8284 | ||
|
d4f9dd427f | ||
|
7e3a1c9ddc | ||
|
5f0a256a16 | ||
|
678e4b5ab1 | ||
|
6e0c3c6567 | ||
|
3d535fe2bf | ||
|
79e912e2bb | ||
|
fa28ebfac1 | ||
|
fee6e23a23 | ||
|
594ca9b667 | ||
|
33ad095080 | ||
|
6c44b5e392 | ||
|
352b36d32b | ||
|
b5e6ecf9d5 | ||
|
71777d80ef | ||
|
22f4d22af8 | ||
|
2843b5da87 | ||
|
c17dcc053e | ||
|
3480c8fb49 | ||
|
e65ce668b7 | ||
|
a1f2a676fb | ||
|
bce51224f2 | ||
|
b500b7b659 | ||
|
b4ec68bf82 | ||
|
2c16ce12ae | ||
|
d7b0b670d1 | ||
|
727cf98771 | ||
|
41f26cdf3b | ||
|
b6cc7d0ab1 | ||
|
c4fd45c6bb | ||
|
6e5dac46bb | ||
|
a019e73dfc | ||
|
92a42a4529 | ||
|
ec8d215a9c | ||
|
f1433c3e97 | ||
|
3ac81c7a18 | ||
|
f654b700fc | ||
|
9273c9d78f | ||
|
b4c78cc575 | ||
|
0011a915ba | ||
|
930d930301 | ||
|
87809009f7 | ||
|
9d8d95dd8b | ||
|
310e829138 | ||
|
fee440d8c4 | ||
|
90e9c9c27f | ||
|
32dc7697bc | ||
|
68cc54c2f4 | ||
|
787b387674 | ||
|
04da30aaee | ||
|
2dd60a4f4a | ||
|
789d8459d1 | ||
|
30ad8fe880 | ||
|
43cc23505a | ||
|
b738688096 | ||
|
a38f7ce396 | ||
|
6a60329f8a | ||
|
a23469c5d6 | ||
|
325b3a7846 | ||
|
3ef55f5526 | ||
|
eee1902301 | ||
|
72d4273f9f | ||
|
a2a417f949 | ||
|
a0c280f377 | ||
|
ab737ca5d0 | ||
|
ab954350b5 | ||
|
aae4af6c41 | ||
|
337b61b20c | ||
|
e835a2d34f | ||
|
c7615049bc | ||
|
2a06103eba | ||
|
9a779cf116 | ||
|
2477040375 | ||
|
b222df108d | ||
|
0835869256 | ||
|
3582043179 | ||
|
f2d94d9cb5 | ||
|
953b8383bb | ||
|
6040e40a7a | ||
|
5b32021e26 | ||
|
ef324cc615 | ||
|
6974cff389 | ||
|
cbcc4d1a74 | ||
|
5bde2c6f98 | ||
|
61cd214911 | ||
|
b1979262fe | ||
|
0cc271031c | ||
|
96e67d31fb | ||
|
cf9189380c | ||
|
38e4f6e20f | ||
|
a83bac03aa | ||
|
1711beaf95 | ||
|
ce72f07fec | ||
|
bb20626c9b | ||
|
145a592716 | ||
|
7c2ad31a21 | ||
|
143c6271aa | ||
|
07a69a8fa5 | ||
|
c664f05718 | ||
|
e36656dc81 | ||
|
78a3cc434a | ||
|
cc543d4af3 | ||
|
c9f93b105b | ||
|
4ca89490b3 | ||
|
423e87046d | ||
|
b8569df1b3 | ||
|
9d4bd141a3 | ||
|
3983011066 | ||
|
bc35f55ab7 | ||
|
defa43d596 | ||
|
9b9f1cc1a8 | ||
|
4f9c2d88ea | ||
|
81076f039e | ||
|
321d418a00 | ||
|
f3b97e42f8 | ||
|
f53d817908 | ||
|
dcedd252bb | ||
|
a5a587f2b6 | ||
|
4ac7a52b57 | ||
|
cd0815bdab | ||
|
adead34f23 | ||
|
6f959680ef | ||
|
bb3fa955b2 | ||
|
fea166abf2 | ||
|
485dfeceb6 | ||
|
9be5448ee3 | ||
|
e2e9aa9f56 | ||
|
8d2b3ac1af | ||
|
c0ec4cc23e | ||
|
7787a47026 | ||
|
6d566c6cd2 | ||
|
9872266c61 | ||
|
11304c792c | ||
|
fd84960b66 | ||
|
7720d3f392 | ||
|
f5b8084543 | ||
|
6322bfd645 | ||
|
cb7a5c04e0 | ||
|
edf4369454 | ||
|
0e1d6c5832 | ||
|
118693c980 | ||
|
273f7a79f2 | ||
|
bf5591354b | ||
|
5a0de0e5f7 | ||
|
cfdcfb4bc4 | ||
|
84d677b7a2 | ||
|
2cc60a8f70 | ||
|
a5ab94e6fa | ||
|
125ccda075 | ||
|
f33152049e | ||
|
a0a6ef00ec | ||
|
23527814bd | ||
|
4f8b4a9f44 | ||
|
56a74255b6 | ||
|
0c5d478974 | ||
|
e787ac06b2 | ||
|
81fa71ddb1 | ||
|
7760174900 | ||
|
a6eb60bf1a | ||
|
991eaad5df | ||
|
e6bf68b6f2 | ||
|
74a19966d2 | ||
|
287cb55941 | ||
|
ebcc6020d0 | ||
|
054332079d | ||
|
a355aee718 | ||
|
f716df0c29 | ||
|
9c8cb61c48 | ||
|
9aa283eea3 | ||
|
f5af67427f | ||
|
8f41b5bf79 | ||
|
f634554699 | ||
|
7d4a144521 | ||
|
114377da0f | ||
|
35c9aeff8b | ||
|
8147d8383d | ||
|
281fe05158 | ||
|
fbc1ee8985 | ||
|
18599e2e81 | ||
|
0de035307d | ||
|
2ec5cf026e | ||
|
2dd79dbe03 | ||
|
8d565bf21f | ||
|
0032e6a106 | ||
|
f8f9b98f70 | ||
|
0ac09dcf59 | ||
|
17ad1de429 | ||
|
fa681c9c0e | ||
|
3e84b2b6c2 | ||
|
4415ad39cf | ||
|
63852cdcaf | ||
|
bc7eb4c85b | ||
|
db4f75da0c | ||
|
f0aae64214 | ||
|
b8c1b514cb | ||
|
9151c149e5 | ||
|
92daa29592 | ||
|
6929a4a3a7 | ||
|
c4433644ef | ||
|
736f072ccf | ||
|
8cdb9c999a | ||
|
9866737de3 | ||
|
3918810338 | ||
|
4c563e7936 | ||
|
32d3896781 | ||
|
35824fe4d0 | ||
|
73bcd85658 | ||
|
33483364c5 | ||
|
ed9a2ca0ac | ||
|
5780f1b1a1 | ||
|
de91987464 | ||
|
c7ff3f573a | ||
|
5e94e008ff | ||
|
61940baaca | ||
|
ed93771d12 | ||
|
ec3616c66a | ||
|
ebda635137 | ||
|
142c84b501 | ||
|
22d481a806 | ||
|
2dc2b0d99b | ||
|
dd69928b20 | ||
|
64ac1f5349 | ||
|
47251337f1 | ||
|
ca1ca4103d | ||
|
54fba4778d | ||
|
c39151e746 | ||
|
98e6dfc303 | ||
|
c49fe06d0f | ||
|
0ad7f89a62 | ||
|
6128832140 | ||
|
cd940d60f7 | ||
|
caf8903873 | ||
|
2e3a1c311e | ||
|
ac01ff4c60 | ||
|
b1e6ea58a3 | ||
|
8dd533e071 | ||
|
97ea2768df | ||
|
3ea4b1fa46 | ||
|
61b57540f4 | ||
|
6bf43ea39f | ||
|
e6e99ba52b | ||
|
b3ba45f900 | ||
|
39a27f750e | ||
|
47550e9367 | ||
|
9c8ad27f43 | ||
|
de2f8941f9 | ||
|
7f8939cee7 | ||
|
6719b97d87 | ||
|
f326e49ba7 | ||
|
0b577aaabe | ||
|
6cea9464c5 | ||
|
919253dc95 | ||
|
63d7732021 | ||
|
d73d0b42d9 | ||
|
b9f1aefdc3 | ||
|
0db8ae490c | ||
|
69d473c92b | ||
|
b9a32d2a3d | ||
|
e21f4c143d | ||
|
82e38366f5 | ||
|
7465117b00 | ||
|
08cea5b677 | ||
|
f6e54ac2b6 | ||
|
a6defcc05a | ||
|
584bcdbaaf | ||
|
a261117c70 | ||
|
06f7d88d22 | ||
|
78f1099a4f | ||
|
5346a1f0a7 | ||
|
ae3ea91c10 | ||
|
21788624b9 | ||
|
7acaafbd69 | ||
|
d9f022bcd1 | ||
|
98ac67058e | ||
|
b442a54723 | ||
|
43921b1f9b | ||
|
4f307e6eab | ||
|
0e6ca0d290 | ||
|
dd70170caa | ||
|
9d02e19a68 | ||
|
e535a8cf70 | ||
|
068c49683d | ||
|
c5d1927775 | ||
|
3825072c5e | ||
|
c1727e2b5b | ||
|
9226784b2a | ||
|
8fe72d281f | ||
|
83bb5a54c1 | ||
|
73955357e1 | ||
|
e8f730ef34 | ||
|
d97a7fdb44 | ||
|
baa2b17615 | ||
|
8d34e66764 | ||
|
a9a4d43cb2 | ||
|
48034d60ed | ||
|
c6291eaba6 | ||
|
d2985925c6 | ||
|
b0bc436dbc | ||
|
fd001fa496 | ||
|
cc68bb0c43 | ||
|
55de6a1572 | ||
|
48805461d0 | ||
|
189b3ce60c | ||
|
390a832887 | ||
|
e2bdfb9c7f | ||
|
110bf3ff9a | ||
|
cb69a2ecbe | ||
|
b7bc919608 | ||
|
a77c6b81f7 | ||
|
6fa19e3495 | ||
|
b4a7a9ec41 | ||
|
5e4cfdef52 | ||
|
427fc47e7a | ||
|
8f5c64b33e | ||
|
4cf7c753e0 | ||
|
1f2b19ce42 | ||
|
0dda58ebd3 | ||
|
dc2967c8da | ||
|
3bfe05c5bb | ||
|
9c975cfe24 | ||
|
193778f0f4 | ||
|
88a84672c3 | ||
|
5c81c15cfd | ||
|
2ef95b1480 | ||
|
f4923eda22 | ||
|
4da58d72dd | ||
|
4b9fda9e81 | ||
|
71d0cccdaa | ||
|
b5093eb6be | ||
|
09b2c54675 | ||
|
bd8af8652e | ||
|
da66c62f81 | ||
|
cc20464c15 | ||
|
8bdfcbabc8 | ||
|
8ca9613d62 | ||
|
cc587ed714 | ||
|
c22b4c782d | ||
|
ab73d3c1fd | ||
|
ae949c45ae | ||
|
6904494e31 | ||
|
d306f36869 | ||
|
d7525274e9 | ||
|
382411bff0 | ||
|
e32fa567bc | ||
|
599f134204 | ||
|
1d46779d57 | ||
|
a56655356c | ||
|
dcd6756a7d | ||
|
a1f4bc75e6 | ||
|
4b4dc1afbf | ||
|
f771b8d3aa | ||
|
ac4b6fc7b5 | ||
|
750cf953da | ||
|
58eaa14043 | ||
|
8545f939fe | ||
|
436b1fda83 |
203 changed files with 16574 additions and 5604 deletions
|
@ -1,5 +1,4 @@
|
|||
exclude_paths:
|
||||
- 'resources/lib/watchdog/**'
|
||||
- 'resources/lib/pathtools/**'
|
||||
- 'resources/lib/pathtools/**'
|
||||
- 'resources/lib/defused_etree.py'
|
||||
- 'resources/lib/defusedxml/**'
|
||||
|
|
12
README.md
12
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)
|
||||
[![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 stable version](https://img.shields.io/badge/Kodi_Leia_STABLE-latest-blue.svg?maxAge=60&style=flat) ](https://croneter.github.io/pkc-source/repository.plexkodiconnect.Kodi-Leia.STABLE.zip)
|
||||
[![Kodi Leia beta version](https://img.shields.io/badge/Kodi_Leia_BETA-latest-red.svg?maxAge=60&style=flat) ](https://croneter.github.io/pkc-source/repository.plexkodiconnect.Kodi-Leia.BETA.zip)
|
||||
[![Kodi Matrix 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)
|
||||
[![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq)
|
||||
|
@ -50,10 +52,11 @@ Some people argue that PKC is 'hacky' because of the way it directly accesses th
|
|||
|
||||
### PKC Features
|
||||
|
||||
- Kodi 19 Matrix is not yet supported (PKC is written in Python 2)
|
||||
- Support for Kodi 18 Leia
|
||||
- Support for Kodi 18 Leia and Kodi 19 Matrix
|
||||
- Preliminary support for Kodi 19 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
|
||||
- [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/)
|
||||
- If Plex did not provide a trailer, automatically get one using the Kodi add-on [The Movie Database](https://kodi.wiki/view/Add-on:The_Movie_Database)
|
||||
- [Plex Watch Later / Plex It!](https://support.plex.tv/hc/en-us/sections/200211783-Plex-It-)
|
||||
- [Plex Companion](https://support.plex.tv/hc/en-us/sections/200276908-Plex-Companion): fling Plex media (or anything else) from other Plex devices to PlexKodiConnect
|
||||
- Automatically sync Plex playlists to Kodi playlists and vice-versa
|
||||
|
@ -79,6 +82,7 @@ Some people argue that PKC is 'hacky' because of the way it directly accesses th
|
|||
+ Hungarian, thanks @savage93
|
||||
+ Ukrainian, thanks @uniss
|
||||
+ Lithuanian, thanks @egidusm
|
||||
+ Korean, thanks @so-o-bima
|
||||
|
||||
### Additional Artwork
|
||||
PKC uses additional artwork for free from [TheMovieDB](https://www.themoviedb.org). Many thanks for lettings us use the API, guys!
|
||||
|
|
262
changelog.txt
262
changelog.txt
|
@ -1,3 +1,265 @@
|
|||
version 3.5.8:
|
||||
- Fix UnboundLocalError: local variable 'identifier' referenced before assignment
|
||||
- versions 3.5.6-3.5.7 for everyone
|
||||
|
||||
version 3.5.7 (beta only):
|
||||
- Fix Kodi JSON racing condition on playback startup and KeyError
|
||||
|
||||
version 3.5.6 (beta only):
|
||||
- Fix Plex Companion not working by fixing some issues with PKC's http.server's BaseHTTPRequestHandler
|
||||
|
||||
version 3.5.5:
|
||||
- Lost patience with Kodi 19: drop use of Python multiprocessing entirely
|
||||
|
||||
version 3.5.4:
|
||||
- Fix Receiving init() missing 1 required positional argument: ‘certification_country’
|
||||
- Update translations from Transifex
|
||||
|
||||
version 3.5.3:
|
||||
- Add playback settings to let the user choose whether Plex or Kodi provides the default audio or subtitle stream on playback start
|
||||
|
||||
version 3.5.2:
|
||||
- version 3.5.1 for everyone
|
||||
|
||||
version 3.5.1 (beta only):
|
||||
- Refactor stream code and fix Kodi not activating subtitle when it should
|
||||
- Direct Paths: Fix TypeError: "element indices must be integers" on playback startup
|
||||
- Android: Fix broken Python multiprocessing module (a Kodi 19.2 bug)
|
||||
- Fix logging if fanart.tv lookup fails: be less verbose
|
||||
|
||||
version 3.5.0:
|
||||
- versions 3.4.5-3.4.7 for everyone
|
||||
|
||||
version 3.4.7 (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
|
||||
- Large refactoring of playlist and playqueue code
|
||||
- Refactor usage of a media part's id
|
||||
|
||||
version 3.4.6 (beta only):
|
||||
- Fix RecursionError if a video lies in a root directory
|
||||
|
||||
version 3.4.5 (beta only):
|
||||
- Implement "Reset resume position" from the Kodi context menu
|
||||
|
||||
version 3.4.4:
|
||||
- Initial compatibility with 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
|
||||
- version 3.4.3 for everyone
|
||||
|
||||
version 3.4.3 (beta ony):
|
||||
- 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 3.4.2:
|
||||
- Fix PlexKodiConnect changing or removing subtitles for every video on the PMS
|
||||
|
||||
version 3.4.1:
|
||||
- Fix PMS setting `List of IP addresses and networks that are allowed without auth` causing Kodi to take forever to start playback
|
||||
|
||||
version 3.4.0:
|
||||
- Improve logging for converting Unix timestamps
|
||||
- Remove dependency on script.module.defusedxml - that module is now included in PKC
|
||||
- version 3.3.3-3.3.5 for everyone
|
||||
|
||||
version 3.3.5 (beta only):
|
||||
- Rewire defusedxml and xml.etree.ElementTree: Fix AttributeError: module 'resources.lib.utils' has no attribute 'ParseError'
|
||||
- Fix errors when PKC tries to edit files that don't exist yet
|
||||
|
||||
version 3.3.4 (beta only):
|
||||
- Fix a racing condition that could lead to the sync getting stuck
|
||||
- Fix RecursionError: maximum recursion depth exceeded
|
||||
- Bump websocket client: fix AttributeError: 'NoneType' object has no attribute 'is_ssl'
|
||||
|
||||
version 3.3.3 (beta only):
|
||||
- Fix a racing condition that could lead to the sync process getting stuck
|
||||
- Fix likelyhood of `database is locked` error occuring
|
||||
- Fix AttributeError: module 'urllib' has no attribute 'parse'
|
||||
- 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 3.3.2:
|
||||
- version 3.3.1 for everyone
|
||||
|
||||
version 3.3.1 (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
|
||||
- Make PKC compatible with Kodi 20 N* by using xbmcvfs for translatePath
|
||||
- Update translations
|
||||
|
||||
version 3.3.0:
|
||||
WARNING: Database reset and full resync required
|
||||
- versions 3.2.1-3.2.4 for everyone
|
||||
|
||||
version 3.2.4 (beta only):
|
||||
- Fix websockets and AttributeError: 'NoneType' object has no attribute
|
||||
|
||||
version 3.2.3 (beta only):
|
||||
- Attempt to fix websocket threading issues and AttributeError: 'NoneType' object has no attribute 'is_ssl' or 'settimeout'
|
||||
- Get rid of Python arrow; hopefully fix many Python import errors (also occuring in other add-ons!)
|
||||
|
||||
version 3.2.2 (beta only):
|
||||
- Fix videos not starting due to a TypeError
|
||||
- Show warning message to remind user to use Estuary for database resets
|
||||
- Update websocket client to 1.0.0
|
||||
|
||||
version 3.2.1 (beta only):
|
||||
WARNING: Database reset and full resync required
|
||||
- Fix PKC widgets not working at all in some cases
|
||||
- Direct Paths: fix several issues with episodes
|
||||
- New Python-dependency: arrow
|
||||
|
||||
version 3.2.0:
|
||||
WARNING: Database reset and full resync required
|
||||
- version 3.1.1-3.1.4 for everyone
|
||||
|
||||
version 3.1.4 (beta only):
|
||||
- Fix Alexa and RuntimeError: dictionary keys changed during iteration
|
||||
- Fix AttributeError: module 'shutil' has no attribute 'copy_tree'
|
||||
|
||||
version 3.1.3 (beta only):
|
||||
- Add PKC setting to disable verification whether we can access a media file
|
||||
- Direct paths: corrections to more closely mirror Kodi's way of saving movie and tv show files to the db
|
||||
- Make sure that the correct file system encoding is used for playlists
|
||||
- Fix a rare AttributeError when using playlists
|
||||
- Fix regression: fix add-on paths always falling back to direct paths
|
||||
|
||||
version 3.1.2 (beta only):
|
||||
- Fix ImportError: cannot import name 'dir_util' from 'distutils' on PKC startup
|
||||
- Fix UnicodeEncodeError if Plex playlist name contains illegal chars
|
||||
- Fix PKC not showing up as a casting target in some cases
|
||||
|
||||
version 3.1.1 (beta only):
|
||||
- Direct paths: fix filename showing instead of full video metadata during playback
|
||||
- Update translations
|
||||
|
||||
version 3.1.0:
|
||||
- version 3.0.16 and 3.0.17 for everyone
|
||||
- Fix resume not working if Kodi player start-up is slow
|
||||
|
||||
version 3.0.17 (beta only):
|
||||
- Fix instantaneous background sync and Alexa not working
|
||||
- Hopefully fix RuntimeError: no add-on id "plugin.video.plexkodiconnect"
|
||||
- Fix error socket.timeout: timed out
|
||||
|
||||
version 3.0.16 (beta only):
|
||||
- Add information to PKC settings for background sync and Alexa whether a connection has been successfully made
|
||||
|
||||
version 3.0.15:
|
||||
- 3.0.14 for everyone
|
||||
- Rename skip intro skin file
|
||||
|
||||
version 3.0.14 (beta only):
|
||||
- Quickly sync recently watched items before synching the playstates of the entire Plex library
|
||||
- Fix TypeError: function missing required argument 'message'
|
||||
- Fix PlexKodiConnect Kodi add-on icon and fanart not showing
|
||||
- Improve logging for websocket JSON loads
|
||||
|
||||
version 3.0.13:
|
||||
- Fix UnboundLocalError: local variable 'user' referenced before assignment
|
||||
|
||||
version 3.0.12:
|
||||
- Sync name and user rating of a TV show season to Kodi
|
||||
- Fix rare TypeError: expected string or buffer on playback start
|
||||
|
||||
version 3.0.11:
|
||||
- Fix TypeError: function missing required argument 'message'
|
||||
|
||||
version 3.0.10:
|
||||
- Fix skip intros sometimes not working due to a RuntimeError
|
||||
- Update translations
|
||||
|
||||
version 3.0.9:
|
||||
- Add skip intro functionality
|
||||
- Fix Kodi add-on NextUp not working
|
||||
|
||||
version 3.0.8:
|
||||
- Fix KeyError: u'game' if Plex Arcade has been activated
|
||||
- Fix AttributeError: 'App' object has no attribute 'threads' when sync is cancelled
|
||||
|
||||
version 3.0.7:
|
||||
- Hopefully fix rare case when sync would get stuck indefinitely
|
||||
- Fix ValueError: invalid literal for int() for invalid dates sent by Plex
|
||||
|
||||
version 3.0.6:
|
||||
- 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 3.0.5:
|
||||
- Fix pictures from Plex picture libraries not working/displaying
|
||||
- Fix sqlite3.OperationalError on PKC upgrade
|
||||
|
||||
version 3.0.4:
|
||||
- Automatically look for missing movie trailers using TMDB
|
||||
|
||||
version 3.0.3:
|
||||
- Fix PKC suddenly using main Plex user's credentials, e.g. when the PMS address changed
|
||||
- Fix missing Kodi tags for movie collections/sets
|
||||
- Change `thread.isAlive` to `thread.is_alive`
|
||||
|
||||
version 3.0.2:
|
||||
- Fix AttributeError: module has no attribute try_decode
|
||||
|
||||
version 3.0.1:
|
||||
- Fix rare KeyError when using PKC widgets
|
||||
- Update translations
|
||||
|
||||
version 3.0.0:
|
||||
- Major upgrade from Python 2 to Python 3, allowing use of Kodi 19 Matrix
|
||||
|
||||
version 2.12.18 (beta only):
|
||||
- Quickly sync recently watched items before synching the playstates of the entire Plex library
|
||||
- Improve logging for websocket JSON loads
|
||||
|
||||
version 2.12.17 (beta only):
|
||||
- Sync name and user rating of a TV show season to Kodi
|
||||
- Fix rare TypeError: expected string or buffer on playback start
|
||||
|
||||
version 2.12.16:
|
||||
- versions 2.12.14 and 2.12.15 for everyone
|
||||
|
||||
version 2.12.15 (beta only):
|
||||
- Fix skip intros sometimes not working due to a RuntimeError
|
||||
- Update translations
|
||||
|
||||
version 2.12.14 (beta only):
|
||||
- Add skip intro functionality
|
||||
|
||||
version 2.12.13:
|
||||
- Fix KeyError: u'game' if Plex Arcade has been activated
|
||||
- Fix AttributeError: 'App' object has no attribute 'threads' when sync is cancelled
|
||||
|
||||
version 2.12.12:
|
||||
- Hopefully fix rare case when sync would get stuck indefinitely
|
||||
- Fix ValueError: invalid literal for int() for invalid dates sent by Plex
|
||||
- version 2.12.11 for everyone
|
||||
|
||||
version 2.12.11 (beta only):
|
||||
- Fix PKC not auto-picking audio/subtitle stream when transcoding
|
||||
- Fix ValueError when deleting a music album
|
||||
- Fix OSError: Invalid argument when Plex returns an invalid timestamp
|
||||
|
||||
version 2.12.10:
|
||||
- Fix pictures from Plex picture libraries not working/displaying
|
||||
|
||||
version 2.12.9:
|
||||
- Fix Local variable 'user' referenced before assignement
|
||||
|
||||
version 2.12.8:
|
||||
- version 2.12.7 for everyone
|
||||
|
||||
version 2.12.7 (beta only):
|
||||
- Fix PKC suddenly using main Plex user's credentials, e.g. when the PMS address changed
|
||||
- Fix missing Kodi tags for movie collections/sets
|
||||
|
||||
version 2.12.6:
|
||||
- Fix rare KeyError when using PKC widgets
|
||||
- Fix suspension of artwork caching and PKC becoming unresponsive
|
||||
- Update translations
|
||||
- Versions 2.12.4 and 2.12.5 for everyone
|
||||
|
||||
version 2.12.5 (beta only):
|
||||
- Greatly improve matching logic for The Movie Database if Plex does not provide an appropriate id
|
||||
- Fix high transcoding resolutions not being available for Win10
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
###############################################################################
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from sys import listitem
|
||||
from urllib import urlencode
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from xbmc import getCondVisibility, sleep
|
||||
from xbmcgui import Window
|
||||
|
@ -11,7 +10,7 @@ from xbmcgui import Window
|
|||
|
||||
|
||||
def _get_kodi_type():
|
||||
kodi_type = listitem.getVideoInfoTag().getMediaType().decode('utf-8')
|
||||
kodi_type = listitem.getVideoInfoTag().getMediaType()
|
||||
if not kodi_type:
|
||||
if getCondVisibility('Container.Content(albums)'):
|
||||
kodi_type = "album"
|
||||
|
|
15
default.py
15
default.py
|
@ -1,17 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
###############################################################################
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from builtins import object
|
||||
import logging
|
||||
from sys import argv
|
||||
from urlparse import parse_qsl
|
||||
from urllib.parse import parse_qsl
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcplugin
|
||||
|
||||
from resources.lib import entrypoint, utils, transfer, variables as v, loghandler
|
||||
from resources.lib.tools import unicode_paths
|
||||
|
||||
###############################################################################
|
||||
|
||||
|
@ -23,19 +22,15 @@ LOG = logging.getLogger('PLEX.default')
|
|||
HANDLE = int(argv[1])
|
||||
|
||||
|
||||
class Main():
|
||||
class Main(object):
|
||||
# MAIN ENTRY POINT
|
||||
# @utils.profiling()
|
||||
def __init__(self):
|
||||
LOG.debug('Full sys.argv received: %s', argv)
|
||||
# Parse parameters
|
||||
params = dict(parse_qsl(argv[2][1:]))
|
||||
arguments = unicode_paths.decode(argv[2])
|
||||
path = unicode_paths.decode(argv[0])
|
||||
# Ensure unicode
|
||||
for key, value in params.iteritems():
|
||||
params[key.decode('utf-8')] = params.pop(key)
|
||||
params[key] = value.decode('utf-8')
|
||||
arguments = argv[2]
|
||||
path = argv[0]
|
||||
mode = params.get('mode', '')
|
||||
itemid = params.get('id', '')
|
||||
|
||||
|
|
|
@ -45,6 +45,13 @@ msgstr ""
|
|||
"Varování: Máte v Kodi zapnuté nastavení \"Automaticky přehrát další video\"."
|
||||
" Toto může narušit funkčnost PKC. Deaktivovat?"
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid ""
|
||||
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||
"random password automatically if you haven't done so already. Please confirm"
|
||||
" the next dialog that you want to enable the webserver now with Yes."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username: "
|
||||
msgstr "Uživ. jméno: "
|
||||
|
@ -161,6 +168,14 @@ msgctxt "#30028"
|
|||
msgid "PKC-only image caching completed"
|
||||
msgstr "Stahování obrázků PKC dokončeno"
|
||||
|
||||
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||
msgctxt "#30029"
|
||||
msgid ""
|
||||
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
|
||||
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
|
||||
"database resets. Continue?"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Port Number"
|
||||
msgstr "Číslo portu"
|
||||
|
@ -596,6 +611,11 @@ msgctxt "#30524"
|
|||
msgid "Select Plex libraries to sync"
|
||||
msgstr "Zvolte knihovny Plexu k synchronizaci"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -660,8 +680,8 @@ msgstr "Stahovat obrázky filmových kolekcí z FanArtTV"
|
|||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30541"
|
||||
msgid "Don't ask to pick a certain stream/quality"
|
||||
msgstr "Nepožadovat výběr proudu nebo kvality"
|
||||
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30542"
|
||||
|
@ -682,6 +702,21 @@ msgctxt "#30545"
|
|||
msgid "Force transcode pictures"
|
||||
msgstr "Vynutit překódování obrázků"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30546"
|
||||
msgid "Pick the first video if several versions are present"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30547"
|
||||
msgid "Who picks the audio stream on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30548"
|
||||
msgid "Who picks subtitles on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# Welcome to Plex notification
|
||||
msgctxt "#33000"
|
||||
msgid "Welcome"
|
||||
|
@ -950,6 +985,11 @@ msgctxt "#39036"
|
|||
msgid "Escape special characters in path (e.g. space to %20)"
|
||||
msgstr "Nahrazovat speciální znaky v cestě (např. z mezery na %20)"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39090"
|
||||
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39037"
|
||||
msgid "Original Plex MOVIE path to replace:"
|
||||
|
@ -1133,6 +1173,11 @@ msgctxt "#39071"
|
|||
msgid "Current plex.tv status:"
|
||||
msgstr "Současný stav plex.tv:"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39072"
|
||||
msgid "Background sync connection:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#39073"
|
||||
msgid "Appearance Tweaks"
|
||||
|
@ -1143,6 +1188,11 @@ msgctxt "#39074"
|
|||
msgid "TV Shows"
|
||||
msgstr "Seriály"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39075"
|
||||
msgid "Verify access to media files while synching"
|
||||
msgstr ""
|
||||
|
||||
# Pop-up during initial sync
|
||||
msgctxt "#39076"
|
||||
msgid ""
|
||||
|
@ -1208,6 +1258,31 @@ msgctxt "#39085"
|
|||
msgid "Reload Kodi node files to apply all the settings below"
|
||||
msgstr "Znovu načíst Kodi pro aplikování nastavení níže"
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39089"
|
||||
msgid "Alexa connection status:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39091"
|
||||
msgid "Timeout - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39092"
|
||||
msgid "IOError - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39093"
|
||||
msgid "Suspended - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39094"
|
||||
msgid "Managed Plex User - not connected"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39200"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "Odhlásit uživatele Plex Home "
|
||||
|
|
|
@ -46,6 +46,13 @@ msgstr ""
|
|||
"Advarsel: Kodi indstillingen \"Afspil næste video automatisk\" er aktiveret."
|
||||
" Dette kan ødelægge PKC funktionalitet. Deaktiver? "
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid ""
|
||||
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||
"random password automatically if you haven't done so already. Please confirm"
|
||||
" the next dialog that you want to enable the webserver now with Yes."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username: "
|
||||
msgstr "Brugernavn: "
|
||||
|
@ -161,6 +168,14 @@ msgctxt "#30028"
|
|||
msgid "PKC-only image caching completed"
|
||||
msgstr "PKC billede caching er færdiggjort"
|
||||
|
||||
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||
msgctxt "#30029"
|
||||
msgid ""
|
||||
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
|
||||
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
|
||||
"database resets. Continue?"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Port Number"
|
||||
msgstr "Portnummer"
|
||||
|
@ -596,6 +611,11 @@ msgctxt "#30524"
|
|||
msgid "Select Plex libraries to sync"
|
||||
msgstr "Vælg Plex biblioteker der skal synkroniseres"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -660,8 +680,8 @@ msgstr "Download film sæt/samling info fra FanArtTV"
|
|||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30541"
|
||||
msgid "Don't ask to pick a certain stream/quality"
|
||||
msgstr "Spørg ikke at vælge en bestemt stream/kvalitet"
|
||||
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30542"
|
||||
|
@ -682,6 +702,21 @@ msgctxt "#30545"
|
|||
msgid "Force transcode pictures"
|
||||
msgstr "Transcode billeder"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30546"
|
||||
msgid "Pick the first video if several versions are present"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30547"
|
||||
msgid "Who picks the audio stream on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30548"
|
||||
msgid "Who picks subtitles on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# Welcome to Plex notification
|
||||
msgctxt "#33000"
|
||||
msgid "Welcome"
|
||||
|
@ -954,6 +989,11 @@ msgctxt "#39036"
|
|||
msgid "Escape special characters in path (e.g. space to %20)"
|
||||
msgstr "Escape special characters in path (e.g. space to %20)"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39090"
|
||||
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39037"
|
||||
msgid "Original Plex MOVIE path to replace:"
|
||||
|
@ -1136,6 +1176,11 @@ msgctxt "#39071"
|
|||
msgid "Current plex.tv status:"
|
||||
msgstr "Nuværende plex.tv status:"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39072"
|
||||
msgid "Background sync connection:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#39073"
|
||||
msgid "Appearance Tweaks"
|
||||
|
@ -1146,6 +1191,11 @@ msgctxt "#39074"
|
|||
msgid "TV Shows"
|
||||
msgstr "TV-udsendelser"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39075"
|
||||
msgid "Verify access to media files while synching"
|
||||
msgstr ""
|
||||
|
||||
# Pop-up during initial sync
|
||||
msgctxt "#39076"
|
||||
msgid ""
|
||||
|
@ -1213,6 +1263,31 @@ msgstr ""
|
|||
"Reload Kodi node filer for alle indstillinger\n"
|
||||
"nedeunder"
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39089"
|
||||
msgid "Alexa connection status:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39091"
|
||||
msgid "Timeout - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39092"
|
||||
msgid "IOError - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39093"
|
||||
msgid "Suspended - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39094"
|
||||
msgid "Managed Plex User - not connected"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39200"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "Log ud Plex hjemme bruger "
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# XBMC Media Center language file
|
||||
# Translators:
|
||||
# Croneter None <croneter@gmail.com>, 2020
|
||||
# Croneter None <croneter@gmail.com>, 2021
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgstr ""
|
|||
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
|
||||
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
|
||||
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
|
||||
"Last-Translator: Croneter None <croneter@gmail.com>, 2020\n"
|
||||
"Last-Translator: Croneter None <croneter@gmail.com>, 2021\n"
|
||||
"Language-Team: German (Germany) (https://www.transifex.com/croneter/teams/73837/de_DE/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
@ -44,6 +44,17 @@ msgstr ""
|
|||
"Achtung: Kodi Einstellung \"Nächsten Video automatisch abspielen\" ist "
|
||||
"aktiviert. Dies kann PKC stören. Deaktivieren?"
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid ""
|
||||
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||
"random password automatically if you haven't done so already. Please confirm"
|
||||
" the next dialog that you want to enable the webserver now with Yes."
|
||||
msgstr ""
|
||||
"Der Kodi-Webserver wird für Artwork-Caching benötigt. PKC hat bereits "
|
||||
"automatisch ein starkes, zufälliges Passwort gesetzt, falls Sie dies nicht "
|
||||
"schon getan haben. Bitte bestätigen Sie den nächsten Dialog mit Ja, dass der"
|
||||
" Webserver aktiviert werden kann."
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username: "
|
||||
msgstr "Benutzername: "
|
||||
|
@ -159,6 +170,17 @@ msgctxt "#30028"
|
|||
msgid "PKC-only image caching completed"
|
||||
msgstr "PKC Bilder-Caching beendet"
|
||||
|
||||
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||
msgctxt "#30029"
|
||||
msgid ""
|
||||
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
|
||||
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
|
||||
"database resets. Continue?"
|
||||
msgstr ""
|
||||
"Um ein reibungsloses PlexKodiConnect-Erlebnis zu gewährleisten, wird "
|
||||
"DRINGEND empfohlen, für die Ersteinrichtung und für mögliche Datenbank-"
|
||||
"Resets den Standard-Skin \"Estuary\" von Kodi zu verwenden. Weiterfahren?"
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Port Number"
|
||||
msgstr "Portnummer"
|
||||
|
@ -602,6 +624,11 @@ msgctxt "#30524"
|
|||
msgid "Select Plex libraries to sync"
|
||||
msgstr "Zu synchronisierende Plex Bibliotheken auswählen"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr "Intro überspringen"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -665,8 +692,9 @@ msgstr "FanArtTV Bilder für Film-Sets/Collections herunterladen"
|
|||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30541"
|
||||
msgid "Don't ask to pick a certain stream/quality"
|
||||
msgstr "Nicht nachfragen, welcher Stream oder Qualität gespielt werden soll"
|
||||
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||
msgstr ""
|
||||
"Transkodierung: Plex-Standards für Audio- und Untertitel-Streams verwenden"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30542"
|
||||
|
@ -687,6 +715,21 @@ msgctxt "#30545"
|
|||
msgid "Force transcode pictures"
|
||||
msgstr "Bilder immer transkodieren"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30546"
|
||||
msgid "Pick the first video if several versions are present"
|
||||
msgstr "Ersten Videostream wählen, wenn mehrere Versionen vorhanden sind"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30547"
|
||||
msgid "Who picks the audio stream on playback start?"
|
||||
msgstr "Wer wählt den Audiotrack beim Start der Wiedergabe?"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30548"
|
||||
msgid "Who picks subtitles on playback start?"
|
||||
msgstr "Wer wählt Untertitel beim Start der Wiedergabe?"
|
||||
|
||||
# Welcome to Plex notification
|
||||
msgctxt "#33000"
|
||||
msgid "Welcome"
|
||||
|
@ -966,6 +1009,11 @@ msgctxt "#39036"
|
|||
msgid "Escape special characters in path (e.g. space to %20)"
|
||||
msgstr "Sonderzeichen im Pfad escapen (z.B. Leerzeichen zu %20)"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39090"
|
||||
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||
msgstr "Sichere Zeichen für http(s), dav(s) und (s)ftp urls"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39037"
|
||||
msgid "Original Plex MOVIE path to replace:"
|
||||
|
@ -1149,6 +1197,11 @@ msgctxt "#39071"
|
|||
msgid "Current plex.tv status:"
|
||||
msgstr "Aktueller plex.tv Status:"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39072"
|
||||
msgid "Background sync connection:"
|
||||
msgstr "Verbindungsstatus Hintergrund-Synchronisation:"
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#39073"
|
||||
msgid "Appearance Tweaks"
|
||||
|
@ -1159,6 +1212,11 @@ msgctxt "#39074"
|
|||
msgid "TV Shows"
|
||||
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
|
||||
msgctxt "#39076"
|
||||
msgid ""
|
||||
|
@ -1224,6 +1282,31 @@ msgctxt "#39085"
|
|||
msgid "Reload Kodi node files to apply all the settings below"
|
||||
msgstr "Kodi neu laden um Einstellungen unten zu übernehmen"
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39089"
|
||||
msgid "Alexa connection status:"
|
||||
msgstr "Alexa Verbindungsstatus:"
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39091"
|
||||
msgid "Timeout - not connected"
|
||||
msgstr "Timeout - nicht verbunden"
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39092"
|
||||
msgid "IOError - not connected"
|
||||
msgstr "IOError - nicht verbunden"
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39093"
|
||||
msgid "Suspended - not connected"
|
||||
msgstr "Angehalten - nicht verbunden"
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39094"
|
||||
msgid "Managed Plex User - not connected"
|
||||
msgstr "Managed Plex User - nicht verbunden"
|
||||
|
||||
msgctxt "#39200"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "Plex Home Benutzer abmelden: "
|
||||
|
|
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?"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid "The Kodi webserver is needed for artwork caching. PKC already set a strong, random password automatically if you haven't done so already. Please confirm the next dialog that you want to enable the webserver now with Yes."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username: "
|
||||
msgstr ""
|
||||
|
@ -151,6 +155,11 @@ msgctxt "#30028"
|
|||
msgid "PKC-only image caching completed"
|
||||
msgstr ""
|
||||
|
||||
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||
msgctxt "#30029"
|
||||
msgid "To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to use Kodi's default skin \"Estuary\" for initial set-up and for possible database resets. Continue?"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Port Number"
|
||||
msgstr ""
|
||||
|
@ -567,6 +576,10 @@ msgctxt "#30524"
|
|||
msgid "Select Plex libraries to sync"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
|
@ -630,7 +643,7 @@ msgstr ""
|
|||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30541"
|
||||
msgid "Don't ask to pick a certain stream/quality"
|
||||
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
|
@ -652,6 +665,21 @@ msgctxt "#30545"
|
|||
msgid "Force transcode pictures"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30546"
|
||||
msgid "Pick the first video if several versions are present"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30547"
|
||||
msgid "Who picks the audio stream on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30548"
|
||||
msgid "Who picks subtitles on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# Welcome to Plex notification
|
||||
msgctxt "#33000"
|
||||
msgid "Welcome"
|
||||
|
@ -1055,6 +1083,11 @@ msgctxt "#39071"
|
|||
msgid "Current plex.tv status:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39072"
|
||||
msgid "Background sync connection:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#39073"
|
||||
msgid "Appearance Tweaks"
|
||||
|
@ -1065,6 +1098,11 @@ msgctxt "#39074"
|
|||
msgid "TV Shows"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39075"
|
||||
msgid "Verify access to media files while synching"
|
||||
msgstr ""
|
||||
|
||||
# Pop-up during initial sync
|
||||
msgctxt "#39076"
|
||||
msgid "If you use several Plex libraries of one kind, e.g. \"Kids Movies\" and \"Parents Movies\", be sure to check the Wiki: https://goo.gl/JFtQV9"
|
||||
|
@ -1116,6 +1154,31 @@ msgctxt "#39085"
|
|||
msgid "Reload Kodi node files to apply all the settings below"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39089"
|
||||
msgid "Alexa connection status:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39091"
|
||||
msgid "Timeout - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39092"
|
||||
msgid "IOError - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39093"
|
||||
msgid "Suspended - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39094"
|
||||
msgid "Managed Plex User - not connected"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39200"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr ""
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# XBMC Media Center language file
|
||||
# Translators:
|
||||
# Croneter None <croneter@gmail.com>, 2019
|
||||
# Croneter None <croneter@gmail.com>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgstr ""
|
|||
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
|
||||
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
|
||||
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
|
||||
"Last-Translator: Croneter None <croneter@gmail.com>, 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"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
@ -44,6 +44,13 @@ msgstr ""
|
|||
"Advertencia: El ajuste de Kodi \"Reproducir el siguiente video "
|
||||
"automáticamente\" está activado. Esto puede dañar PKC. ¿Desactivar?"
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid ""
|
||||
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||
"random password automatically if you haven't done so already. Please confirm"
|
||||
" the next dialog that you want to enable the webserver now with Yes."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username: "
|
||||
msgstr "Usuario: "
|
||||
|
@ -164,6 +171,14 @@ msgctxt "#30028"
|
|||
msgid "PKC-only image caching completed"
|
||||
msgstr "El caché de imágenes solo-PKC fue completado"
|
||||
|
||||
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||
msgctxt "#30029"
|
||||
msgid ""
|
||||
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
|
||||
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
|
||||
"database resets. Continue?"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Port Number"
|
||||
msgstr "Número de puerto"
|
||||
|
@ -603,6 +618,11 @@ msgctxt "#30524"
|
|||
msgid "Select Plex libraries to sync"
|
||||
msgstr "Seleccionar librerias de Plex para sincronizar"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -668,8 +688,8 @@ msgstr "Descargar arte de sagas de FanArtTV"
|
|||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30541"
|
||||
msgid "Don't ask to pick a certain stream/quality"
|
||||
msgstr "No solicitar elegir un stream o una calidad en particular"
|
||||
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30542"
|
||||
|
@ -690,6 +710,21 @@ msgctxt "#30545"
|
|||
msgid "Force transcode pictures"
|
||||
msgstr "Obligar transcodificar fotografías"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30546"
|
||||
msgid "Pick the first video if several versions are present"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30547"
|
||||
msgid "Who picks the audio stream on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30548"
|
||||
msgid "Who picks subtitles on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# Welcome to Plex notification
|
||||
msgctxt "#33000"
|
||||
msgid "Welcome"
|
||||
|
@ -935,7 +970,7 @@ msgid ""
|
|||
"Kodi cannot locate the file %s. Please verify your PKC settings. Stop "
|
||||
"syncing?"
|
||||
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?"
|
||||
|
||||
# Pop-up on initial sync
|
||||
|
@ -964,7 +999,12 @@ msgstr ""
|
|||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39036"
|
||||
msgid "Escape special characters in path (e.g. space to %20)"
|
||||
msgstr "Escapar caracteres especiales en la ruta (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
|
||||
msgctxt "#39037"
|
||||
|
@ -1149,6 +1189,11 @@ msgctxt "#39071"
|
|||
msgid "Current plex.tv status:"
|
||||
msgstr "Estado actual de plex.tv:"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39072"
|
||||
msgid "Background sync connection:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#39073"
|
||||
msgid "Appearance Tweaks"
|
||||
|
@ -1159,6 +1204,11 @@ msgctxt "#39074"
|
|||
msgid "TV Shows"
|
||||
msgstr "Series"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39075"
|
||||
msgid "Verify access to media files while synching"
|
||||
msgstr ""
|
||||
|
||||
# Pop-up during initial sync
|
||||
msgctxt "#39076"
|
||||
msgid ""
|
||||
|
@ -1226,6 +1276,31 @@ msgctxt "#39085"
|
|||
msgid "Reload Kodi node files to apply all the settings below"
|
||||
msgstr "Recargar Kodi para aplicar todos los ajustes."
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39089"
|
||||
msgid "Alexa connection status:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39091"
|
||||
msgid "Timeout - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39092"
|
||||
msgid "IOError - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39093"
|
||||
msgid "Suspended - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39094"
|
||||
msgid "Managed Plex User - not connected"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39200"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "Terminar sesión del usuario de Plex Home "
|
||||
|
@ -1430,7 +1505,7 @@ msgid ""
|
|||
"The current Kodi version is not supported by PKC. Please consult the Plex "
|
||||
"forum."
|
||||
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."
|
||||
|
||||
msgctxt "#39405"
|
||||
|
@ -1474,7 +1549,7 @@ msgstr "Sagas"
|
|||
|
||||
msgctxt "#39502"
|
||||
msgid "PKC On Deck (faster)"
|
||||
msgstr "Tablero de PKC (mas rapido)"
|
||||
msgstr "On Deck de PKC (más rápido)"
|
||||
|
||||
msgctxt "#39600"
|
||||
msgid ""
|
||||
|
@ -1616,8 +1691,8 @@ msgid ""
|
|||
"Do you want to replace your custom user ratings with an indicator of how "
|
||||
"many versions of a media item you posses?"
|
||||
msgstr ""
|
||||
"¿Quiere reemplazar su valoración personalizada con cuántas versione posee de"
|
||||
" un elemento de medios?"
|
||||
"¿Quiere reemplazar su valoración personalizada por cuántas versiones posee "
|
||||
"de un elemento de medios?"
|
||||
|
||||
# In PKC Settings under Sync
|
||||
msgctxt "#39719"
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
# Translators:
|
||||
# Dani <danichispa@gmail.com>, 2019
|
||||
# Bartolome Soriano <bsoriano@gmail.com>, 2019
|
||||
# Croneter None <croneter@gmail.com>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
|
@ -9,7 +10,7 @@ msgstr ""
|
|||
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
|
||||
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
|
||||
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
|
||||
"Last-Translator: 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"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
@ -45,6 +46,13 @@ msgstr ""
|
|||
"Advertencia: El ajuste de Kodi \"Reproducir el siguiente video "
|
||||
"automáticamente\" está activado. Esto puede dañar PKC. ¿Desactivar?"
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid ""
|
||||
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||
"random password automatically if you haven't done so already. Please confirm"
|
||||
" the next dialog that you want to enable the webserver now with Yes."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username: "
|
||||
msgstr "Usuario: "
|
||||
|
@ -165,6 +173,14 @@ msgctxt "#30028"
|
|||
msgid "PKC-only image caching completed"
|
||||
msgstr "El caché de imágenes solo-PKC fue completado"
|
||||
|
||||
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||
msgctxt "#30029"
|
||||
msgid ""
|
||||
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
|
||||
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
|
||||
"database resets. Continue?"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Port Number"
|
||||
msgstr "Número de puerto"
|
||||
|
@ -604,6 +620,11 @@ msgctxt "#30524"
|
|||
msgid "Select Plex libraries to sync"
|
||||
msgstr "Seleccionar librerias de Plex para sincronizar"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -669,8 +690,8 @@ msgstr "Descargar arte de sagas de FanArtTV"
|
|||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30541"
|
||||
msgid "Don't ask to pick a certain stream/quality"
|
||||
msgstr "No solicitar elegir un stream o una calidad en particular"
|
||||
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30542"
|
||||
|
@ -691,6 +712,21 @@ msgctxt "#30545"
|
|||
msgid "Force transcode pictures"
|
||||
msgstr "Obligar transcodificar fotografías"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30546"
|
||||
msgid "Pick the first video if several versions are present"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30547"
|
||||
msgid "Who picks the audio stream on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30548"
|
||||
msgid "Who picks subtitles on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# Welcome to Plex notification
|
||||
msgctxt "#33000"
|
||||
msgid "Welcome"
|
||||
|
@ -1155,6 +1191,11 @@ msgctxt "#39071"
|
|||
msgid "Current plex.tv status:"
|
||||
msgstr "Estado actual de plex.tv:"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39072"
|
||||
msgid "Background sync connection:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#39073"
|
||||
msgid "Appearance Tweaks"
|
||||
|
@ -1165,6 +1206,11 @@ msgctxt "#39074"
|
|||
msgid "TV Shows"
|
||||
msgstr "Series"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39075"
|
||||
msgid "Verify access to media files while synching"
|
||||
msgstr ""
|
||||
|
||||
# Pop-up during initial sync
|
||||
msgctxt "#39076"
|
||||
msgid ""
|
||||
|
@ -1232,6 +1278,31 @@ msgctxt "#39085"
|
|||
msgid "Reload Kodi node files to apply all the settings below"
|
||||
msgstr "Recargar Kodi para aplicar todos los ajustes."
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39089"
|
||||
msgid "Alexa connection status:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39091"
|
||||
msgid "Timeout - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39092"
|
||||
msgid "IOError - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39093"
|
||||
msgid "Suspended - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39094"
|
||||
msgid "Managed Plex User - not connected"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39200"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "Terminar sesión del usuario de Plex Home "
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# XBMC Media Center language file
|
||||
# Translators:
|
||||
# Croneter None <croneter@gmail.com>, 2019
|
||||
# Croneter None <croneter@gmail.com>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgstr ""
|
|||
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
|
||||
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
|
||||
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
|
||||
"Last-Translator: Croneter None <croneter@gmail.com>, 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"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
@ -44,6 +44,13 @@ msgstr ""
|
|||
"Advertencia: El ajuste de Kodi \"Reproducir el siguiente video "
|
||||
"automáticamente\" está activado. Esto puede dañar PKC. ¿Desactivar?"
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid ""
|
||||
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||
"random password automatically if you haven't done so already. Please confirm"
|
||||
" the next dialog that you want to enable the webserver now with Yes."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username: "
|
||||
msgstr "Usuario: "
|
||||
|
@ -164,6 +171,14 @@ msgctxt "#30028"
|
|||
msgid "PKC-only image caching completed"
|
||||
msgstr "El caché de imágenes solo-PKC fue completado"
|
||||
|
||||
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||
msgctxt "#30029"
|
||||
msgid ""
|
||||
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
|
||||
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
|
||||
"database resets. Continue?"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Port Number"
|
||||
msgstr "Número de puerto"
|
||||
|
@ -603,6 +618,11 @@ msgctxt "#30524"
|
|||
msgid "Select Plex libraries to sync"
|
||||
msgstr "Seleccionar librerias de Plex para sincronizar"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -668,8 +688,8 @@ msgstr "Descargar arte de sagas de FanArtTV"
|
|||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30541"
|
||||
msgid "Don't ask to pick a certain stream/quality"
|
||||
msgstr "No solicitar elegir un stream o una calidad en particular"
|
||||
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30542"
|
||||
|
@ -690,6 +710,21 @@ msgctxt "#30545"
|
|||
msgid "Force transcode pictures"
|
||||
msgstr "Obligar transcodificar fotografías"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30546"
|
||||
msgid "Pick the first video if several versions are present"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30547"
|
||||
msgid "Who picks the audio stream on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30548"
|
||||
msgid "Who picks subtitles on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# Welcome to Plex notification
|
||||
msgctxt "#33000"
|
||||
msgid "Welcome"
|
||||
|
@ -935,7 +970,7 @@ msgid ""
|
|||
"Kodi cannot locate the file %s. Please verify your PKC settings. Stop "
|
||||
"syncing?"
|
||||
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?"
|
||||
|
||||
# Pop-up on initial sync
|
||||
|
@ -964,7 +999,12 @@ msgstr ""
|
|||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39036"
|
||||
msgid "Escape special characters in path (e.g. space to %20)"
|
||||
msgstr "Escapar caracteres especiales en la ruta (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
|
||||
msgctxt "#39037"
|
||||
|
@ -1149,6 +1189,11 @@ msgctxt "#39071"
|
|||
msgid "Current plex.tv status:"
|
||||
msgstr "Estado actual de plex.tv:"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39072"
|
||||
msgid "Background sync connection:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#39073"
|
||||
msgid "Appearance Tweaks"
|
||||
|
@ -1159,6 +1204,11 @@ msgctxt "#39074"
|
|||
msgid "TV Shows"
|
||||
msgstr "Series"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39075"
|
||||
msgid "Verify access to media files while synching"
|
||||
msgstr ""
|
||||
|
||||
# Pop-up during initial sync
|
||||
msgctxt "#39076"
|
||||
msgid ""
|
||||
|
@ -1226,6 +1276,31 @@ msgctxt "#39085"
|
|||
msgid "Reload Kodi node files to apply all the settings below"
|
||||
msgstr "Recargar Kodi para aplicar todos los ajustes."
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39089"
|
||||
msgid "Alexa connection status:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39091"
|
||||
msgid "Timeout - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39092"
|
||||
msgid "IOError - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39093"
|
||||
msgid "Suspended - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39094"
|
||||
msgid "Managed Plex User - not connected"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39200"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "Terminar sesión del usuario de Plex Home "
|
||||
|
@ -1430,7 +1505,7 @@ msgid ""
|
|||
"The current Kodi version is not supported by PKC. Please consult the Plex "
|
||||
"forum."
|
||||
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."
|
||||
|
||||
msgctxt "#39405"
|
||||
|
@ -1474,7 +1549,7 @@ msgstr "Sagas"
|
|||
|
||||
msgctxt "#39502"
|
||||
msgid "PKC On Deck (faster)"
|
||||
msgstr "Tablero de PKC (mas rapido)"
|
||||
msgstr "On Deck de PKC (más rápido)"
|
||||
|
||||
msgctxt "#39600"
|
||||
msgid ""
|
||||
|
@ -1616,8 +1691,8 @@ msgid ""
|
|||
"Do you want to replace your custom user ratings with an indicator of how "
|
||||
"many versions of a media item you posses?"
|
||||
msgstr ""
|
||||
"¿Quiere reemplazar su valoración personalizada con cuántas versione posee de"
|
||||
" un elemento de medios?"
|
||||
"¿Quiere reemplazar su valoración personalizada por cuántas versiones posee "
|
||||
"de un elemento de medios?"
|
||||
|
||||
# In PKC Settings under Sync
|
||||
msgctxt "#39719"
|
||||
|
|
|
@ -46,6 +46,13 @@ msgstr ""
|
|||
"Avertissement : Le paramètre Kodi \"Lecture automatique de la vidéo "
|
||||
"suivante\" est activé. Cela pourrait casser PKC. Désactiver ?"
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid ""
|
||||
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||
"random password automatically if you haven't done so already. Please confirm"
|
||||
" the next dialog that you want to enable the webserver now with Yes."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username: "
|
||||
msgstr "Identifiant : "
|
||||
|
@ -165,6 +172,14 @@ msgctxt "#30028"
|
|||
msgid "PKC-only image caching completed"
|
||||
msgstr "Mise en cache de images pour PKC-seulement terminée"
|
||||
|
||||
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||
msgctxt "#30029"
|
||||
msgid ""
|
||||
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
|
||||
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
|
||||
"database resets. Continue?"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Port Number"
|
||||
msgstr "Numéro de port"
|
||||
|
@ -608,6 +623,11 @@ msgctxt "#30524"
|
|||
msgid "Select Plex libraries to sync"
|
||||
msgstr "Sélectionner les bibliothèques Plex à synchroniser"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -673,8 +693,8 @@ msgstr "Télécharger les affiches des sagas sur FanArtTV"
|
|||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30541"
|
||||
msgid "Don't ask to pick a certain stream/quality"
|
||||
msgstr "Ne pas demander de choisir un certain flux/qualité"
|
||||
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30542"
|
||||
|
@ -695,6 +715,21 @@ msgctxt "#30545"
|
|||
msgid "Force transcode pictures"
|
||||
msgstr "Forcer le transcodage des images"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30546"
|
||||
msgid "Pick the first video if several versions are present"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30547"
|
||||
msgid "Who picks the audio stream on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30548"
|
||||
msgid "Who picks subtitles on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# Welcome to Plex notification
|
||||
msgctxt "#33000"
|
||||
msgid "Welcome"
|
||||
|
@ -975,6 +1010,11 @@ msgstr ""
|
|||
"Echapper les caractères spéciaux dans le chemin (ex: %20 au lieu des "
|
||||
"espaces)"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39090"
|
||||
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||
msgstr "Caractères sûrs pour les urls http(s), dav(s) et (s)ftp"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39037"
|
||||
msgid "Original Plex MOVIE path to replace:"
|
||||
|
@ -1163,6 +1203,11 @@ msgctxt "#39071"
|
|||
msgid "Current plex.tv status:"
|
||||
msgstr "État actuel de plex.tv: "
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39072"
|
||||
msgid "Background sync connection:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#39073"
|
||||
msgid "Appearance Tweaks"
|
||||
|
@ -1173,6 +1218,11 @@ msgctxt "#39074"
|
|||
msgid "TV Shows"
|
||||
msgstr "Séries TV"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39075"
|
||||
msgid "Verify access to media files while synching"
|
||||
msgstr ""
|
||||
|
||||
# Pop-up during initial sync
|
||||
msgctxt "#39076"
|
||||
msgid ""
|
||||
|
@ -1240,6 +1290,31 @@ msgid "Reload Kodi node files to apply all the settings below"
|
|||
msgstr ""
|
||||
"Recharger les fichiers de Kodi pour appliquer tous les paramètres ci-dessous"
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39089"
|
||||
msgid "Alexa connection status:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39091"
|
||||
msgid "Timeout - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39092"
|
||||
msgid "IOError - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39093"
|
||||
msgid "Suspended - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39094"
|
||||
msgid "Managed Plex User - not connected"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39200"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "Log-out Plex Home User "
|
||||
|
|
|
@ -50,6 +50,13 @@ msgstr ""
|
|||
"Avertissement : Le paramètre Kodi \"Lecture automatique de la vidéo "
|
||||
"suivante\" est activé. Cela pourrait casser PKC. Désactiver ?"
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid ""
|
||||
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||
"random password automatically if you haven't done so already. Please confirm"
|
||||
" the next dialog that you want to enable the webserver now with Yes."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username: "
|
||||
msgstr "Identifiant : "
|
||||
|
@ -169,6 +176,14 @@ msgctxt "#30028"
|
|||
msgid "PKC-only image caching completed"
|
||||
msgstr "Mise en cache de images pour PKC-seulement terminée"
|
||||
|
||||
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||
msgctxt "#30029"
|
||||
msgid ""
|
||||
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
|
||||
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
|
||||
"database resets. Continue?"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Port Number"
|
||||
msgstr "Numéro de port"
|
||||
|
@ -612,6 +627,11 @@ msgctxt "#30524"
|
|||
msgid "Select Plex libraries to sync"
|
||||
msgstr "Sélectionner les bibliothèques Plex à synchroniser"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -677,8 +697,8 @@ msgstr "Télécharger les affiches des sagas sur FanArtTV"
|
|||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30541"
|
||||
msgid "Don't ask to pick a certain stream/quality"
|
||||
msgstr "Ne pas demander de choisir un certain flux/qualité"
|
||||
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30542"
|
||||
|
@ -699,6 +719,21 @@ msgctxt "#30545"
|
|||
msgid "Force transcode pictures"
|
||||
msgstr "Forcer le transcodage des images"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30546"
|
||||
msgid "Pick the first video if several versions are present"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30547"
|
||||
msgid "Who picks the audio stream on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30548"
|
||||
msgid "Who picks subtitles on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# Welcome to Plex notification
|
||||
msgctxt "#33000"
|
||||
msgid "Welcome"
|
||||
|
@ -979,6 +1014,11 @@ msgstr ""
|
|||
"Echapper les caractères spéciaux dans le chemin (ex: %20 au lieu des "
|
||||
"espaces)"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39090"
|
||||
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||
msgstr "Caractères sûrs pour les urls http(s), dav(s) et (s)ftp"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39037"
|
||||
msgid "Original Plex MOVIE path to replace:"
|
||||
|
@ -1167,6 +1207,11 @@ msgctxt "#39071"
|
|||
msgid "Current plex.tv status:"
|
||||
msgstr "État actuel de plex.tv: "
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39072"
|
||||
msgid "Background sync connection:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#39073"
|
||||
msgid "Appearance Tweaks"
|
||||
|
@ -1177,6 +1222,11 @@ msgctxt "#39074"
|
|||
msgid "TV Shows"
|
||||
msgstr "Séries TV"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39075"
|
||||
msgid "Verify access to media files while synching"
|
||||
msgstr ""
|
||||
|
||||
# Pop-up during initial sync
|
||||
msgctxt "#39076"
|
||||
msgid ""
|
||||
|
@ -1244,6 +1294,31 @@ msgid "Reload Kodi node files to apply all the settings below"
|
|||
msgstr ""
|
||||
"Recharger les fichiers de Kodi pour appliquer tous les paramètres ci-dessous"
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39089"
|
||||
msgid "Alexa connection status:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39091"
|
||||
msgid "Timeout - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39092"
|
||||
msgid "IOError - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39093"
|
||||
msgid "Suspended - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39094"
|
||||
msgid "Managed Plex User - not connected"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39200"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "Log-out Plex Home User "
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# XBMC Media Center language file
|
||||
# Translators:
|
||||
# Croneter None <croneter@gmail.com>, 2019
|
||||
# Savage93 <savageistheking@gmail.com>, 2020
|
||||
# Savage93 <savageistheking@gmail.com>, 2021
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
|
@ -9,7 +9,7 @@ msgstr ""
|
|||
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
|
||||
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
|
||||
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
|
||||
"Last-Translator: 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"
|
||||
"MIME-Version: 1.0\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 "
|
||||
"megakadályozhatja a PKC megfelelő működését. Kikapcsolja?"
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid ""
|
||||
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||
"random password automatically if you haven't done so already. Please confirm"
|
||||
" the next dialog that you want to enable the webserver now with Yes."
|
||||
msgstr ""
|
||||
"A művészképek gyorsítótárazásához szükség van a Kodi webszerverének "
|
||||
"bekapcsolására. A PKC beállított egy erős, véletlenszerű jelszót ehhez, "
|
||||
"amennyiben ezt korábban nem tette meg. Kérem erősítse meg a következő "
|
||||
"dialógusablakban, hogy be kívánja kapcsolni a webszervert."
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username: "
|
||||
msgstr "Felhasználónév: "
|
||||
|
@ -162,6 +173,14 @@ msgctxt "#30028"
|
|||
msgid "PKC-only image caching completed"
|
||||
msgstr "PKC képek gyorsítótárazása befejeződött"
|
||||
|
||||
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||
msgctxt "#30029"
|
||||
msgid ""
|
||||
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
|
||||
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
|
||||
"database resets. Continue?"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Port Number"
|
||||
msgstr "Portszám"
|
||||
|
@ -606,6 +625,11 @@ msgctxt "#30524"
|
|||
msgid "Select Plex libraries to sync"
|
||||
msgstr "Szinkronizálni kívánt Plex könyvtárak kiválasztása"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr "Bevezető kihagyása"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -670,8 +694,10 @@ msgstr "Film-szett/kollekció képek letöltése a FanArtTV-ről"
|
|||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30541"
|
||||
msgid "Don't ask to pick a certain stream/quality"
|
||||
msgstr "Ne kérdezze meg melyik stream/minőség kerüljön lejátszásra"
|
||||
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||
msgstr ""
|
||||
"Transzkódolás: hang- és feliratsávok automatikus kiválasztása a Plex "
|
||||
"alapértelmezések alapján"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30542"
|
||||
|
@ -692,6 +718,21 @@ msgctxt "#30545"
|
|||
msgid "Force transcode pictures"
|
||||
msgstr "Képek transzkódolásának kényszerítése"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30546"
|
||||
msgid "Pick the first video if several versions are present"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30547"
|
||||
msgid "Who picks the audio stream on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30548"
|
||||
msgid "Who picks subtitles on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# Welcome to Plex notification
|
||||
msgctxt "#33000"
|
||||
msgid "Welcome"
|
||||
|
@ -968,6 +1009,11 @@ msgid "Escape special characters in path (e.g. space to %20)"
|
|||
msgstr ""
|
||||
"A speciális karakterek feloldása az elérési útban (pl. szóköz helyett %20)"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39090"
|
||||
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||
msgstr "Biztonságos karakterek http(s), dav(s) és (s)ftp elérési utakhoz"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39037"
|
||||
msgid "Original Plex MOVIE path to replace:"
|
||||
|
@ -1151,6 +1197,11 @@ msgctxt "#39071"
|
|||
msgid "Current plex.tv status:"
|
||||
msgstr "Jelenlegi plex.tv állapot:"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39072"
|
||||
msgid "Background sync connection:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#39073"
|
||||
msgid "Appearance Tweaks"
|
||||
|
@ -1161,6 +1212,11 @@ msgctxt "#39074"
|
|||
msgid "TV Shows"
|
||||
msgstr "TV sorozatok"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39075"
|
||||
msgid "Verify access to media files while synching"
|
||||
msgstr ""
|
||||
|
||||
# Pop-up during initial sync
|
||||
msgctxt "#39076"
|
||||
msgid ""
|
||||
|
@ -1229,6 +1285,31 @@ msgid "Reload Kodi node files to apply all the settings below"
|
|||
msgstr ""
|
||||
"Kodi csomópont fájlok újratöltése az alábbi beállítások alkalmazásához"
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39089"
|
||||
msgid "Alexa connection status:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39091"
|
||||
msgid "Timeout - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39092"
|
||||
msgid "IOError - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39093"
|
||||
msgid "Suspended - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39094"
|
||||
msgid "Managed Plex User - not connected"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39200"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "Kijelentkezés az otthoni Plex felhasználó fiókból: "
|
||||
|
|
|
@ -47,6 +47,13 @@ msgstr ""
|
|||
"Attenzione: l'impostazione Kodi \"Avvia il video successivo "
|
||||
"automaticamente\" è attivata. Questo può interrompere PKC. Disattivare?"
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid ""
|
||||
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||
"random password automatically if you haven't done so already. Please confirm"
|
||||
" the next dialog that you want to enable the webserver now with Yes."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username: "
|
||||
msgstr "Nome utente:"
|
||||
|
@ -164,6 +171,14 @@ msgctxt "#30028"
|
|||
msgid "PKC-only image caching completed"
|
||||
msgstr "Cache delle immagini di PKC completato"
|
||||
|
||||
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||
msgctxt "#30029"
|
||||
msgid ""
|
||||
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
|
||||
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
|
||||
"database resets. Continue?"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Port Number"
|
||||
msgstr "Porta"
|
||||
|
@ -603,6 +618,11 @@ msgctxt "#30524"
|
|||
msgid "Select Plex libraries to sync"
|
||||
msgstr "Seleziona le librerie Plex da sincronizzazare"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -668,8 +688,8 @@ msgstr "Scarica collezioni/cofanetti film da FanartTV"
|
|||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30541"
|
||||
msgid "Don't ask to pick a certain stream/quality"
|
||||
msgstr "Non chiedere di scegliere la qualità dello stream"
|
||||
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30542"
|
||||
|
@ -690,6 +710,21 @@ msgctxt "#30545"
|
|||
msgid "Force transcode pictures"
|
||||
msgstr "Forza transcodifica immagini"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30546"
|
||||
msgid "Pick the first video if several versions are present"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30547"
|
||||
msgid "Who picks the audio stream on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30548"
|
||||
msgid "Who picks subtitles on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# Welcome to Plex notification
|
||||
msgctxt "#33000"
|
||||
msgid "Welcome"
|
||||
|
@ -967,6 +1002,11 @@ msgstr ""
|
|||
"Esegui l'escape dei caratteri speciali nel percorso (es. \"spazio\" "
|
||||
"trasformato in \"%20\")"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39090"
|
||||
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39037"
|
||||
msgid "Original Plex MOVIE path to replace:"
|
||||
|
@ -1152,6 +1192,11 @@ msgctxt "#39071"
|
|||
msgid "Current plex.tv status:"
|
||||
msgstr "Stato attuale di plex.tv:"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39072"
|
||||
msgid "Background sync connection:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#39073"
|
||||
msgid "Appearance Tweaks"
|
||||
|
@ -1162,6 +1207,11 @@ msgctxt "#39074"
|
|||
msgid "TV Shows"
|
||||
msgstr "Serie TV"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39075"
|
||||
msgid "Verify access to media files while synching"
|
||||
msgstr ""
|
||||
|
||||
# Pop-up during initial sync
|
||||
msgctxt "#39076"
|
||||
msgid ""
|
||||
|
@ -1229,6 +1279,31 @@ msgstr ""
|
|||
"Ricarica i nodi di file di Kodi per applicare tutte le impostazioni di "
|
||||
"sotto"
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39089"
|
||||
msgid "Alexa connection status:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39091"
|
||||
msgid "Timeout - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39092"
|
||||
msgid "IOError - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39093"
|
||||
msgid "Suspended - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39094"
|
||||
msgid "Managed Plex User - not connected"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39200"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "Logout utente Plex "
|
||||
|
|
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 "
|
||||
"į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"
|
||||
msgid "Username: "
|
||||
msgstr "Vartotojo vardas:"
|
||||
|
@ -164,6 +171,14 @@ msgctxt "#30028"
|
|||
msgid "PKC-only image caching completed"
|
||||
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"
|
||||
msgid "Port Number"
|
||||
msgstr "Prievado numeris"
|
||||
|
@ -600,6 +615,11 @@ msgctxt "#30524"
|
|||
msgid "Select Plex libraries to sync"
|
||||
msgstr "Pasirinkti sinchronizuojamas „Plex“ bibliotekas"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -664,8 +684,8 @@ msgstr "Atsisiųskite filmų komplekto / rinkinio iliustraciją iš „FanArtTV
|
|||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30541"
|
||||
msgid "Don't ask to pick a certain stream/quality"
|
||||
msgstr "Neprašykite pasirinkti tam tikro srauto / kokybės"
|
||||
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30542"
|
||||
|
@ -686,6 +706,21 @@ msgctxt "#30545"
|
|||
msgid "Force transcode pictures"
|
||||
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
|
||||
msgctxt "#33000"
|
||||
msgid "Welcome"
|
||||
|
@ -963,6 +998,11 @@ msgctxt "#39036"
|
|||
msgid "Escape special characters in path (e.g. space to %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
|
||||
msgctxt "#39037"
|
||||
msgid "Original Plex MOVIE path to replace:"
|
||||
|
@ -1145,6 +1185,11 @@ msgctxt "#39071"
|
|||
msgid "Current plex.tv status:"
|
||||
msgstr "Dabartinė „plex.tv“ būsena:"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39072"
|
||||
msgid "Background sync connection:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#39073"
|
||||
msgid "Appearance Tweaks"
|
||||
|
@ -1155,6 +1200,11 @@ msgctxt "#39074"
|
|||
msgid "TV Shows"
|
||||
msgstr "TV Laidos"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39075"
|
||||
msgid "Verify access to media files while synching"
|
||||
msgstr ""
|
||||
|
||||
# Pop-up during initial sync
|
||||
msgctxt "#39076"
|
||||
msgid ""
|
||||
|
@ -1222,6 +1272,31 @@ msgstr ""
|
|||
"Atnaujinkite „Kodi“ mazgų failus, kad galėtumėte taikyti visus toliau "
|
||||
"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"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "Atjungti „Plex“ namų vartotoją"
|
||||
|
|
|
@ -44,6 +44,13 @@ msgstr ""
|
|||
"Brīdinājums: Kodi iestatījums \"Atskaņot nākamo video automātiski\" ir "
|
||||
"ieslēgts. Tas var salauzt PKC. Izslēgt?"
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid ""
|
||||
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||
"random password automatically if you haven't done so already. Please confirm"
|
||||
" the next dialog that you want to enable the webserver now with Yes."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username: "
|
||||
msgstr "Lietotājvārds:"
|
||||
|
@ -159,6 +166,14 @@ msgctxt "#30028"
|
|||
msgid "PKC-only image caching completed"
|
||||
msgstr "Tikai-PKC attēlu kešošana pabeigta"
|
||||
|
||||
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||
msgctxt "#30029"
|
||||
msgid ""
|
||||
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
|
||||
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
|
||||
"database resets. Continue?"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Port Number"
|
||||
msgstr "Porta Numurs"
|
||||
|
@ -593,6 +608,11 @@ msgctxt "#30524"
|
|||
msgid "Select Plex libraries to sync"
|
||||
msgstr "Izvēlies kuras Plex bibliotēkas sinhronizēt"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -657,8 +677,8 @@ msgstr "Lejupielādēt filmu komplektu/kolekciju attēlus no FanArtTV"
|
|||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30541"
|
||||
msgid "Don't ask to pick a certain stream/quality"
|
||||
msgstr "Nejautāt par konkrētas kvalitātes/straumes izvēli"
|
||||
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30542"
|
||||
|
@ -679,6 +699,21 @@ msgctxt "#30545"
|
|||
msgid "Force transcode pictures"
|
||||
msgstr "Uzspiest attēlu pārkodēšanu"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30546"
|
||||
msgid "Pick the first video if several versions are present"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30547"
|
||||
msgid "Who picks the audio stream on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30548"
|
||||
msgid "Who picks subtitles on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# Welcome to Plex notification
|
||||
msgctxt "#33000"
|
||||
msgid "Welcome"
|
||||
|
@ -946,6 +981,11 @@ msgctxt "#39036"
|
|||
msgid "Escape special characters in path (e.g. space to %20)"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39090"
|
||||
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39037"
|
||||
msgid "Original Plex MOVIE path to replace:"
|
||||
|
@ -1100,7 +1140,7 @@ msgstr "Uzspiest atjaunošanu Kodi ādiņai apturot atskaņošanu"
|
|||
# PKC Settings - Appearance Tweaks
|
||||
msgctxt "#39066"
|
||||
msgid "Recently Added: Also show already watched movies"
|
||||
msgstr ""
|
||||
msgstr "Nesen Pievienots: Rādīt arī jau skatītas filmas"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39067"
|
||||
|
@ -1110,7 +1150,7 @@ msgstr "Tavs pašreizējais Plex Media Serveris:"
|
|||
# PKC Settings - Connection
|
||||
msgctxt "#39068"
|
||||
msgid "Manually enter Plex Media Server address"
|
||||
msgstr ""
|
||||
msgstr "Pats ievadi Plex Media Server adresi"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39069"
|
||||
|
@ -1127,6 +1167,11 @@ msgctxt "#39071"
|
|||
msgid "Current plex.tv status:"
|
||||
msgstr "Pašreizējais plex.tv statuss:"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39072"
|
||||
msgid "Background sync connection:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#39073"
|
||||
msgid "Appearance Tweaks"
|
||||
|
@ -1137,6 +1182,11 @@ msgctxt "#39074"
|
|||
msgid "TV Shows"
|
||||
msgstr "Seriāli"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39075"
|
||||
msgid "Verify access to media files while synching"
|
||||
msgstr ""
|
||||
|
||||
# Pop-up during initial sync
|
||||
msgctxt "#39076"
|
||||
msgid ""
|
||||
|
@ -1173,28 +1223,53 @@ msgstr ""
|
|||
# Button text for choosing PKC mode
|
||||
msgctxt "#39081"
|
||||
msgid "Add-on Paths"
|
||||
msgstr ""
|
||||
msgstr "Spraudņu Ceļš"
|
||||
|
||||
# Button text for choosing PKC mode
|
||||
msgctxt "#39082"
|
||||
msgid "Direct Paths"
|
||||
msgstr ""
|
||||
msgstr "Tiešie Ceļi"
|
||||
|
||||
# Dialog for manually entering PMS
|
||||
msgctxt "#39083"
|
||||
msgid "Enter PMS IP or URL"
|
||||
msgstr ""
|
||||
msgstr "Ievadi PMS IP vai URL"
|
||||
|
||||
# Dialog for manually entering PMS
|
||||
msgctxt "#39084"
|
||||
msgid "Enter PMS port"
|
||||
msgstr ""
|
||||
msgstr "Ievadi PMS portu"
|
||||
|
||||
# PKC settings - Appearance Tweaks
|
||||
msgctxt "#39085"
|
||||
msgid "Reload Kodi node files to apply all the settings below"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39089"
|
||||
msgid "Alexa connection status:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39091"
|
||||
msgid "Timeout - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39092"
|
||||
msgid "IOError - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39093"
|
||||
msgid "Suspended - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39094"
|
||||
msgid "Managed Plex User - not connected"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39200"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr ""
|
||||
|
@ -1291,23 +1366,23 @@ msgstr "Tikai trūkstošo"
|
|||
# Message in the PKC settings if user has not logged in to plex.tv
|
||||
msgctxt "#39226"
|
||||
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
|
||||
msgctxt "#39227"
|
||||
msgid "Logged in to plex.tv"
|
||||
msgstr ""
|
||||
msgstr "Pieteicies plex.tv"
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
msgctxt "#39228"
|
||||
msgid "Plex admin user"
|
||||
msgstr ""
|
||||
msgstr "Plex admin user"
|
||||
|
||||
# Error message if user could not log in; the actual user name will be
|
||||
# appended at the end of the string
|
||||
msgctxt "#39229"
|
||||
msgid "Login failed with plex.tv for user"
|
||||
msgstr ""
|
||||
msgstr "Lietotāja pieteikšanās plex.tv neizdevās"
|
||||
|
||||
# Message in the PKC settings to display the plex.tv username
|
||||
msgctxt "#39230"
|
||||
|
@ -1472,7 +1547,7 @@ msgstr ""
|
|||
# Addon Disclaimer
|
||||
msgctxt "#39705"
|
||||
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
|
||||
msgctxt "#39706"
|
||||
|
@ -1530,7 +1605,7 @@ msgstr "Sinhronizēt"
|
|||
# Shown during sync process
|
||||
msgctxt "#39715"
|
||||
msgid "Synching playlists"
|
||||
msgstr ""
|
||||
msgstr "Sinhronizē spēļsarakstus"
|
||||
|
||||
# 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}!
|
||||
|
|
|
@ -48,6 +48,13 @@ msgstr ""
|
|||
"Waarschuwing: De kodi instelling 'Automatisch volgende video afspelen' is "
|
||||
"actief. Dit kan voor problemen zorgen. Instelling deactiveren?"
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid ""
|
||||
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||
"random password automatically if you haven't done so already. Please confirm"
|
||||
" the next dialog that you want to enable the webserver now with Yes."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username: "
|
||||
msgstr "Gebruikersnaam: "
|
||||
|
@ -163,6 +170,14 @@ msgctxt "#30028"
|
|||
msgid "PKC-only image caching completed"
|
||||
msgstr "PKC afbeelding caching voltooid"
|
||||
|
||||
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||
msgctxt "#30029"
|
||||
msgid ""
|
||||
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
|
||||
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
|
||||
"database resets. Continue?"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Port Number"
|
||||
msgstr "Poortnummer"
|
||||
|
@ -601,6 +616,11 @@ msgctxt "#30524"
|
|||
msgid "Select Plex libraries to sync"
|
||||
msgstr "Selecteer Plex-bibliotheken om te synchroniseren"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -665,8 +685,8 @@ msgstr "Download film set/collectie artwork van FanArtTV"
|
|||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30541"
|
||||
msgid "Don't ask to pick a certain stream/quality"
|
||||
msgstr "Niet vragen om een bepaalde stream/kwaliteit te kiezen"
|
||||
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30542"
|
||||
|
@ -687,6 +707,21 @@ msgctxt "#30545"
|
|||
msgid "Force transcode pictures"
|
||||
msgstr "Forceer transcoden van foto's"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30546"
|
||||
msgid "Pick the first video if several versions are present"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30547"
|
||||
msgid "Who picks the audio stream on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30548"
|
||||
msgid "Who picks subtitles on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# Welcome to Plex notification
|
||||
msgctxt "#33000"
|
||||
msgid "Welcome"
|
||||
|
@ -957,6 +992,11 @@ msgctxt "#39036"
|
|||
msgid "Escape special characters in path (e.g. space to %20)"
|
||||
msgstr "Pas speciale tekens aan in pad (b.v. spatie naar %20)"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39090"
|
||||
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39037"
|
||||
msgid "Original Plex MOVIE path to replace:"
|
||||
|
@ -1139,6 +1179,11 @@ msgctxt "#39071"
|
|||
msgid "Current plex.tv status:"
|
||||
msgstr "Huidige status van de plex.tv:"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39072"
|
||||
msgid "Background sync connection:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#39073"
|
||||
msgid "Appearance Tweaks"
|
||||
|
@ -1149,6 +1194,11 @@ msgctxt "#39074"
|
|||
msgid "TV Shows"
|
||||
msgstr "TV series"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39075"
|
||||
msgid "Verify access to media files while synching"
|
||||
msgstr ""
|
||||
|
||||
# Pop-up during initial sync
|
||||
msgctxt "#39076"
|
||||
msgid ""
|
||||
|
@ -1217,6 +1267,31 @@ msgstr ""
|
|||
"Herlaad de Kodi node bestanden om alles onderstaande instellingen door te "
|
||||
"voeren"
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39089"
|
||||
msgid "Alexa connection status:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39091"
|
||||
msgid "Timeout - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39092"
|
||||
msgid "IOError - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39093"
|
||||
msgid "Suspended - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39094"
|
||||
msgid "Managed Plex User - not connected"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39200"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "Log-out Plex Home gebruiker "
|
||||
|
|
|
@ -46,6 +46,13 @@ msgstr ""
|
|||
"Advarsel: Kodi instilling \"Automatisk avspilling av neste video\" er "
|
||||
"aktivert. Det kan medføre problemer med PKC. Ønsker du å deaktivere?"
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid ""
|
||||
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||
"random password automatically if you haven't done so already. Please confirm"
|
||||
" the next dialog that you want to enable the webserver now with Yes."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username: "
|
||||
msgstr "Brukernavn:"
|
||||
|
@ -165,6 +172,14 @@ msgctxt "#30028"
|
|||
msgid "PKC-only image caching completed"
|
||||
msgstr "PKC mellomlagring av bilder gjennomført"
|
||||
|
||||
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||
msgctxt "#30029"
|
||||
msgid ""
|
||||
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
|
||||
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
|
||||
"database resets. Continue?"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Port Number"
|
||||
msgstr "Portnummer"
|
||||
|
@ -600,6 +615,11 @@ msgctxt "#30524"
|
|||
msgid "Select Plex libraries to sync"
|
||||
msgstr "Velg Plex bibliotek som skal synkroniseres"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -662,8 +682,8 @@ msgstr "Last ned filmsamling-kunst fra FanArtTV"
|
|||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30541"
|
||||
msgid "Don't ask to pick a certain stream/quality"
|
||||
msgstr "Ikke spør om å velge en utvalgt strøm/kvalitet"
|
||||
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30542"
|
||||
|
@ -684,6 +704,21 @@ msgctxt "#30545"
|
|||
msgid "Force transcode pictures"
|
||||
msgstr "Tving transkoding av bilde"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30546"
|
||||
msgid "Pick the first video if several versions are present"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30547"
|
||||
msgid "Who picks the audio stream on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30548"
|
||||
msgid "Who picks subtitles on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# Welcome to Plex notification
|
||||
msgctxt "#33000"
|
||||
msgid "Welcome"
|
||||
|
@ -951,6 +986,11 @@ msgctxt "#39036"
|
|||
msgid "Escape special characters in path (e.g. space to %20)"
|
||||
msgstr "Unngå spesielle tegn i stier (eksempel mellomrom til %20)"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39090"
|
||||
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39037"
|
||||
msgid "Original Plex MOVIE path to replace:"
|
||||
|
@ -1132,6 +1172,11 @@ msgctxt "#39071"
|
|||
msgid "Current plex.tv status:"
|
||||
msgstr "Aktuell plex.tv statys:"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39072"
|
||||
msgid "Background sync connection:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#39073"
|
||||
msgid "Appearance Tweaks"
|
||||
|
@ -1142,6 +1187,11 @@ msgctxt "#39074"
|
|||
msgid "TV Shows"
|
||||
msgstr "TV-show"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39075"
|
||||
msgid "Verify access to media files while synching"
|
||||
msgstr ""
|
||||
|
||||
# Pop-up during initial sync
|
||||
msgctxt "#39076"
|
||||
msgid ""
|
||||
|
@ -1207,6 +1257,31 @@ msgctxt "#39085"
|
|||
msgid "Reload Kodi node files to apply all the settings below"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39089"
|
||||
msgid "Alexa connection status:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39091"
|
||||
msgid "Timeout - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39092"
|
||||
msgid "IOError - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39093"
|
||||
msgid "Suspended - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39094"
|
||||
msgid "Managed Plex User - not connected"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39200"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "Logg av Plex Home User"
|
||||
|
|
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"
|
||||
" no Kodi. Isto pode travar o PKC. Desativar?"
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid ""
|
||||
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||
"random password automatically if you haven't done so already. Please confirm"
|
||||
" the next dialog that you want to enable the webserver now with Yes."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username: "
|
||||
msgstr "Utilizador: "
|
||||
|
@ -162,6 +169,14 @@ msgctxt "#30028"
|
|||
msgid "PKC-only image caching completed"
|
||||
msgstr "Armazenamento PKC somente imagens finalizado"
|
||||
|
||||
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||
msgctxt "#30029"
|
||||
msgid ""
|
||||
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
|
||||
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
|
||||
"database resets. Continue?"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Port Number"
|
||||
msgstr "Número da Porta"
|
||||
|
@ -590,6 +605,11 @@ msgctxt "#30524"
|
|||
msgid "Select Plex libraries to sync"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -652,8 +672,8 @@ msgstr "Descarregar arte para o conjunto/coleção de filmes da FanArtTV "
|
|||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30541"
|
||||
msgid "Don't ask to pick a certain stream/quality"
|
||||
msgstr "Não perguntar para escolher uma certa qualidade/transmissão"
|
||||
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30542"
|
||||
|
@ -674,6 +694,21 @@ msgctxt "#30545"
|
|||
msgid "Force transcode pictures"
|
||||
msgstr "Forçar transcodificação de imagens"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30546"
|
||||
msgid "Pick the first video if several versions are present"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30547"
|
||||
msgid "Who picks the audio stream on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30548"
|
||||
msgid "Who picks subtitles on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# Welcome to Plex notification
|
||||
msgctxt "#33000"
|
||||
msgid "Welcome"
|
||||
|
@ -945,6 +980,11 @@ msgctxt "#39036"
|
|||
msgid "Escape special characters in path (e.g. space to %20)"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39090"
|
||||
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39037"
|
||||
msgid "Original Plex MOVIE path to replace:"
|
||||
|
@ -1128,6 +1168,11 @@ msgctxt "#39071"
|
|||
msgid "Current plex.tv status:"
|
||||
msgstr "Estado atual da plex.tv:"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39072"
|
||||
msgid "Background sync connection:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#39073"
|
||||
msgid "Appearance Tweaks"
|
||||
|
@ -1138,6 +1183,11 @@ msgctxt "#39074"
|
|||
msgid "TV Shows"
|
||||
msgstr "Programas de TV"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39075"
|
||||
msgid "Verify access to media files while synching"
|
||||
msgstr ""
|
||||
|
||||
# Pop-up during initial sync
|
||||
msgctxt "#39076"
|
||||
msgid ""
|
||||
|
@ -1200,6 +1250,31 @@ msgctxt "#39085"
|
|||
msgid "Reload Kodi node files to apply all the settings below"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39089"
|
||||
msgid "Alexa connection status:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39091"
|
||||
msgid "Timeout - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39092"
|
||||
msgid "IOError - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39093"
|
||||
msgid "Suspended - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39094"
|
||||
msgid "Managed Plex User - not connected"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39200"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "Sair da sessão do Utilizador Caseiro Plex"
|
||||
|
|
|
@ -44,6 +44,13 @@ msgid ""
|
|||
"could break PKC. Deactivate?"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid ""
|
||||
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||
"random password automatically if you haven't done so already. Please confirm"
|
||||
" the next dialog that you want to enable the webserver now with Yes."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username: "
|
||||
msgstr "Utilizador: "
|
||||
|
@ -163,6 +170,14 @@ msgctxt "#30028"
|
|||
msgid "PKC-only image caching completed"
|
||||
msgstr ""
|
||||
|
||||
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||
msgctxt "#30029"
|
||||
msgid ""
|
||||
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
|
||||
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
|
||||
"database resets. Continue?"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Port Number"
|
||||
msgstr "Número da Porta"
|
||||
|
@ -593,6 +608,11 @@ msgctxt "#30524"
|
|||
msgid "Select Plex libraries to sync"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -655,8 +675,8 @@ msgstr "Descarregar arte para o conjunto/coleção de filmes da FanArtTV "
|
|||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30541"
|
||||
msgid "Don't ask to pick a certain stream/quality"
|
||||
msgstr "Não perguntar para escolher uma certa qualidade/transmissão"
|
||||
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30542"
|
||||
|
@ -677,6 +697,21 @@ msgctxt "#30545"
|
|||
msgid "Force transcode pictures"
|
||||
msgstr "Forçar transcodificação de imagens"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30546"
|
||||
msgid "Pick the first video if several versions are present"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30547"
|
||||
msgid "Who picks the audio stream on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30548"
|
||||
msgid "Who picks subtitles on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# Welcome to Plex notification
|
||||
msgctxt "#33000"
|
||||
msgid "Welcome"
|
||||
|
@ -948,6 +983,11 @@ msgctxt "#39036"
|
|||
msgid "Escape special characters in path (e.g. space to %20)"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39090"
|
||||
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39037"
|
||||
msgid "Original Plex MOVIE path to replace:"
|
||||
|
@ -1131,6 +1171,11 @@ msgctxt "#39071"
|
|||
msgid "Current plex.tv status:"
|
||||
msgstr "Estado atual da plex.tv:"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39072"
|
||||
msgid "Background sync connection:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#39073"
|
||||
msgid "Appearance Tweaks"
|
||||
|
@ -1141,6 +1186,11 @@ msgctxt "#39074"
|
|||
msgid "TV Shows"
|
||||
msgstr "Programas de TV"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39075"
|
||||
msgid "Verify access to media files while synching"
|
||||
msgstr ""
|
||||
|
||||
# Pop-up during initial sync
|
||||
msgctxt "#39076"
|
||||
msgid ""
|
||||
|
@ -1203,6 +1253,31 @@ msgctxt "#39085"
|
|||
msgid "Reload Kodi node files to apply all the settings below"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39089"
|
||||
msgid "Alexa connection status:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39091"
|
||||
msgid "Timeout - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39092"
|
||||
msgid "IOError - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39093"
|
||||
msgid "Suspended - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39094"
|
||||
msgid "Managed Plex User - not connected"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39200"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "Sair da sessão do Utilizador Caseiro Plex"
|
||||
|
|
|
@ -49,6 +49,13 @@ msgstr ""
|
|||
"Предупреждение: включена настройка Kodi «Воспроизвести следующее видео "
|
||||
"автоматически». Это может сломать PKC. Деактивировать?"
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid ""
|
||||
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||
"random password automatically if you haven't done so already. Please confirm"
|
||||
" the next dialog that you want to enable the webserver now with Yes."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username: "
|
||||
msgstr "Имя пользователя: "
|
||||
|
@ -166,6 +173,14 @@ msgctxt "#30028"
|
|||
msgid "PKC-only image caching completed"
|
||||
msgstr "Кеширование изображений PKC завершено"
|
||||
|
||||
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||
msgctxt "#30029"
|
||||
msgid ""
|
||||
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
|
||||
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
|
||||
"database resets. Continue?"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Port Number"
|
||||
msgstr "Порт"
|
||||
|
@ -605,6 +620,11 @@ msgctxt "#30524"
|
|||
msgid "Select Plex libraries to sync"
|
||||
msgstr "Выбор библиотек Plex для синхронизации"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -669,8 +689,8 @@ msgstr "Загружать иллюстрации сборников с FanArtTV
|
|||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30541"
|
||||
msgid "Don't ask to pick a certain stream/quality"
|
||||
msgstr "Не просить выбрать качество потока"
|
||||
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30542"
|
||||
|
@ -691,6 +711,21 @@ msgctxt "#30545"
|
|||
msgid "Force transcode pictures"
|
||||
msgstr "Принудительно транскодировать изображения"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30546"
|
||||
msgid "Pick the first video if several versions are present"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30547"
|
||||
msgid "Who picks the audio stream on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30548"
|
||||
msgid "Who picks subtitles on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# Welcome to Plex notification
|
||||
msgctxt "#33000"
|
||||
msgid "Welcome"
|
||||
|
@ -961,6 +996,11 @@ msgctxt "#39036"
|
|||
msgid "Escape special characters in path (e.g. space to %20)"
|
||||
msgstr "Преобразуйте специальные символы в пути. (например пробел в %20)"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39090"
|
||||
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39037"
|
||||
msgid "Original Plex MOVIE path to replace:"
|
||||
|
@ -1144,6 +1184,11 @@ msgctxt "#39071"
|
|||
msgid "Current plex.tv status:"
|
||||
msgstr "Текущий статус на plex.tv:"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39072"
|
||||
msgid "Background sync connection:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#39073"
|
||||
msgid "Appearance Tweaks"
|
||||
|
@ -1154,6 +1199,11 @@ msgctxt "#39074"
|
|||
msgid "TV Shows"
|
||||
msgstr "Сериалы"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39075"
|
||||
msgid "Verify access to media files while synching"
|
||||
msgstr ""
|
||||
|
||||
# Pop-up during initial sync
|
||||
msgctxt "#39076"
|
||||
msgid ""
|
||||
|
@ -1219,6 +1269,31 @@ msgctxt "#39085"
|
|||
msgid "Reload Kodi node files to apply all the settings below"
|
||||
msgstr "Перезаписать узлы БД Kodi, чтобы применить следующие настройки"
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39089"
|
||||
msgid "Alexa connection status:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39091"
|
||||
msgid "Timeout - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39092"
|
||||
msgid "IOError - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39093"
|
||||
msgid "Suspended - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39094"
|
||||
msgid "Managed Plex User - not connected"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39200"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "Выйти из Plex"
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
# Samuel Linde <samuel@linde.im>, 2018
|
||||
# Nisse Karlsson <transifex@xcorp.at>, 2019
|
||||
# Ludwig Johnson <public@ludwigjohnson.se>, 2019
|
||||
# namob <boman.d@gmail.com>, 2021
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
|
@ -13,7 +14,7 @@ msgstr ""
|
|||
"Report-Msgid-Bugs-To: croneter@gmail.com\n"
|
||||
"POT-Creation-Date: 2017-04-15 13:13+0000\n"
|
||||
"PO-Revision-Date: 2017-04-30 08:30+0000\n"
|
||||
"Last-Translator: 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"
|
||||
"MIME-Version: 1.0\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. "
|
||||
"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"
|
||||
msgid "Username: "
|
||||
msgstr "Användarnamn:"
|
||||
|
@ -165,6 +173,14 @@ msgctxt "#30028"
|
|||
msgid "PKC-only image caching completed"
|
||||
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"
|
||||
msgid "Port Number"
|
||||
msgstr "Portnummer"
|
||||
|
@ -266,7 +282,7 @@ msgstr "Videokvalitet då omkodning krävs"
|
|||
|
||||
msgctxt "#30161"
|
||||
msgid "Auto-adjust transcoding quality (deactivate for Chromecast)"
|
||||
msgstr ""
|
||||
msgstr "Justera automatiskt omkodningskvaliteten (inaktivera för Chromecast)"
|
||||
|
||||
msgctxt "#30165"
|
||||
msgid "Direct Play"
|
||||
|
@ -553,12 +569,12 @@ msgstr ""
|
|||
# PKC Settings - Sync Options
|
||||
msgctxt "#30515"
|
||||
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
|
||||
msgctxt "#30516"
|
||||
msgid "Playback"
|
||||
msgstr "uppspelning"
|
||||
msgstr "Uppspelning"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#30517"
|
||||
|
@ -578,17 +594,17 @@ msgstr "Fråga om uppspelning av trailers."
|
|||
# PKC Settings - Plex
|
||||
msgctxt "#30520"
|
||||
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
|
||||
msgctxt "#30521"
|
||||
msgid "Jump back on resume (in seconds)"
|
||||
msgstr "Spola tillbaka vid återuppta(i sekunder)"
|
||||
msgstr "Spola tillbaka vid återuppta (i sekunder)"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30522"
|
||||
msgid "Force transcode h265/HEVC"
|
||||
msgstr "Tvinga omkodning (trancoding) av h265/hevc"
|
||||
msgstr "Tvinga omkodning av H.265/HEVC"
|
||||
|
||||
# PKC Settings - Sync Options
|
||||
msgctxt "#30523"
|
||||
|
@ -600,46 +616,51 @@ msgctxt "#30524"
|
|||
msgid "Select Plex libraries to sync"
|
||||
msgstr "Välj Plex-bibliotek att synkronisera "
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
msgstr "ignorera specialer av nästa episoder"
|
||||
msgstr "ignorera specialer i nästa episoder"
|
||||
|
||||
msgctxt "#30528"
|
||||
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
|
||||
msgctxt "#30529"
|
||||
msgid "Startup delay (in seconds)"
|
||||
msgstr "Uppstartnings dröjsmål (i sekunder)"
|
||||
msgstr "Fördröjning vid uppstart (i sekunder)"
|
||||
|
||||
msgctxt "#30531"
|
||||
msgid "Enable new content notification"
|
||||
msgstr "Aktivera nytt innehål notifiering"
|
||||
msgstr "Aktivera notifiering vid nytt innehåll"
|
||||
|
||||
msgctxt "#30532"
|
||||
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"
|
||||
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"
|
||||
msgid "Server messages"
|
||||
msgstr "Server meddelanden"
|
||||
msgstr "Servermeddelanden"
|
||||
|
||||
# PKC Settings - Advanced
|
||||
msgctxt "#30535"
|
||||
msgid "Generate a new unique Plex device Id (e.g. to clone Kodi)"
|
||||
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
|
||||
msgctxt "#30536"
|
||||
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
|
||||
msgctxt "#30537"
|
||||
|
@ -654,7 +675,7 @@ msgstr "Fullständig återställning av Kodi-databasen krävs, se \"Avancerad\""
|
|||
# PKC Settings - Artwork
|
||||
msgctxt "#30539"
|
||||
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
|
||||
msgctxt "#30540"
|
||||
|
@ -663,8 +684,8 @@ msgstr "Ladda ner film set affischer från FanArtTV"
|
|||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30541"
|
||||
msgid "Don't ask to pick a certain stream/quality"
|
||||
msgstr "Fråga inte om välja stream kvalitet"
|
||||
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30542"
|
||||
|
@ -678,12 +699,27 @@ msgstr "Föredra Kodi-bilder för kollektioner"
|
|||
|
||||
msgctxt "#30544"
|
||||
msgid "Artwork"
|
||||
msgstr "affischer"
|
||||
msgstr "Affischer"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30545"
|
||||
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
|
||||
msgctxt "#33000"
|
||||
|
@ -708,18 +744,18 @@ msgstr "Server är online"
|
|||
# Plex notification when we need to transcode
|
||||
msgctxt "#33004"
|
||||
msgid "PMS enforced transcoding"
|
||||
msgstr ""
|
||||
msgstr "PMS-tvingad omkodning"
|
||||
|
||||
# Plex notification when we need to use direct streaming (instead of
|
||||
# transcoding)
|
||||
msgctxt "#33005"
|
||||
msgid "PMS enforced direct streaming"
|
||||
msgstr ""
|
||||
msgstr "PMS-tvingad direkt ström"
|
||||
|
||||
# Error notification
|
||||
msgctxt "#33009"
|
||||
msgid "Invalid username or password"
|
||||
msgstr "fel användarnamn eller lösenord"
|
||||
msgstr "Fel användarnamn eller lösenord"
|
||||
|
||||
msgctxt "#33010"
|
||||
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
|
||||
msgctxt "#33013"
|
||||
msgid "Choose the audio stream"
|
||||
msgstr "välj ljudfil"
|
||||
msgstr "Välj ljudström"
|
||||
|
||||
# Dialog before playback
|
||||
msgctxt "#33014"
|
||||
|
@ -742,19 +778,20 @@ msgstr "Välj undertext"
|
|||
# Dialog before playback
|
||||
msgctxt "#33016"
|
||||
msgid "Play trailers?"
|
||||
msgstr "spela upp trailer?"
|
||||
msgstr "Spela upp trailer?"
|
||||
|
||||
# Error message
|
||||
msgctxt "#33032"
|
||||
msgid ""
|
||||
"Failed to generate a new device Id. See your logs for more information."
|
||||
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
|
||||
msgctxt "#33033"
|
||||
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
|
||||
msgctxt "#33041"
|
||||
|
@ -762,22 +799,22 @@ msgid ""
|
|||
"Delete file(s) from Plex Server? This will also delete the file(s) from "
|
||||
"disk!"
|
||||
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
|
||||
msgctxt "#39000"
|
||||
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
|
||||
msgctxt "#39001"
|
||||
msgid "Boost audio when transcoding"
|
||||
msgstr "öka ljudet när det omkodas(transcoding)"
|
||||
msgstr "Öka ljudet när det omkodas"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#39002"
|
||||
msgid "Burnt-in subtitle size"
|
||||
msgstr "inbränd undertext storlek"
|
||||
msgstr "Storlek på inbränd undertext"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39003"
|
||||
|
@ -787,47 +824,47 @@ msgstr "Antal samtidiga nedladdningstrådar"
|
|||
# PKC Settings - Plex
|
||||
msgctxt "#39004"
|
||||
msgid "Enable Plex Companion (restart Kodi!)"
|
||||
msgstr "aktivera Plex Companion (restart kodi)"
|
||||
msgstr "Aktivera Plex Companion (kräver omstart av Kodi!)"
|
||||
|
||||
# PKC Settings - Plex
|
||||
msgctxt "#39005"
|
||||
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
|
||||
msgctxt "#39008"
|
||||
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
|
||||
msgctxt "#39009"
|
||||
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
|
||||
msgctxt "#39010"
|
||||
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
|
||||
msgctxt "#39011"
|
||||
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
|
||||
msgctxt "#39012"
|
||||
msgid "Choose your Plex server"
|
||||
msgstr "välj din plex server."
|
||||
msgstr "Välj din Plex-server"
|
||||
|
||||
# Error message
|
||||
msgctxt "#39013"
|
||||
msgid "Not yet authorized for Plex server "
|
||||
msgstr "inte authoriserad ännu"
|
||||
msgstr "Ännu inte auktoriserad för Plex-servern"
|
||||
|
||||
# Error message
|
||||
msgctxt "#39014"
|
||||
msgid "Please sign in to plex.tv."
|
||||
msgstr "logga in på plex.tv"
|
||||
msgstr "Vänligen logga in mot plex.tv"
|
||||
|
||||
# Error message
|
||||
msgctxt "#39015"
|
||||
|
@ -840,8 +877,9 @@ msgid ""
|
|||
"Disable Plex music library? (It is HIGHLY recommended to use Plex music only"
|
||||
" with direct paths for large music libraries. Kodi might crash otherwise)"
|
||||
msgstr ""
|
||||
"avaktivera Plex music bibliotek (rekommenderat att endast använda plex musik"
|
||||
" med direkt paths till stora musik bibliotek.)"
|
||||
"Inaktivera Plex musikbibliotek? (Det är STARKT rekommenderat att endast "
|
||||
"använda Direct Path tillsammans med stora musikbibliotek, Kodi kan krasha "
|
||||
"annars)"
|
||||
|
||||
# Pop-up on initial sync
|
||||
msgctxt "#39017"
|
||||
|
@ -875,17 +913,17 @@ msgstr "lokal"
|
|||
# Error message
|
||||
msgctxt "#39023"
|
||||
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
|
||||
msgctxt "#39025"
|
||||
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
|
||||
msgctxt "#39026"
|
||||
msgid "Enable constant background sync"
|
||||
msgstr "aktivera konstant bakgrunds synkronisering"
|
||||
msgstr "Aktivera konstant bakgrundssynkronisering"
|
||||
|
||||
# Pop-up on initial sync
|
||||
msgctxt "#39028"
|
||||
|
@ -895,14 +933,15 @@ msgid ""
|
|||
"shares need to use direct paths (e.g. smb://myNAS/mymovie.mkv or "
|
||||
"\\\\myNAS/mymovie.mkv)!"
|
||||
msgstr ""
|
||||
"VARNING! om du väljer native läge, kanske du förlorar tillgång till vissa plex funktioner som t.ex.\n"
|
||||
"plex trailer och omkodning(transcoding) alternativ. alla plex shares behöver använda direct paths\n"
|
||||
"(t.ex. smb://myNAS/mymovie.mkv or \\\\myNAS/mymovie.mkv)!"
|
||||
"VARNING! Om du väljer \"Nativt\" läge kan du förlora tillgång till vissa "
|
||||
"Plex-funktioner såsom Plextrailer och omkodningsalternativ. ALLA "
|
||||
"Plexutdelningar måste använda Direct Path (t.ex smb://myNAS/mymovie.mkv "
|
||||
"eller \\\\myNAS\\mymovie.mkv)!"
|
||||
|
||||
# Pop-up on initial sync
|
||||
msgctxt "#39029"
|
||||
msgid "Network credentials"
|
||||
msgstr "nätverks inloggningsuppgifter"
|
||||
msgstr "Inloggningsuppgifter för nätverk"
|
||||
|
||||
# Pop-up on initial sync
|
||||
msgctxt "#39030"
|
||||
|
@ -921,7 +960,7 @@ msgid ""
|
|||
"Kodi cannot locate the file %s. Please verify your PKC settings. Stop "
|
||||
"syncing?"
|
||||
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
|
||||
msgctxt "#39033"
|
||||
|
@ -929,13 +968,13 @@ msgid ""
|
|||
"Transform Plex UNC library paths \\\\myNas\\mymovie.mkv automatically to smb"
|
||||
" paths, smb://myNas/mymovie.mkv? (recommended)"
|
||||
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)"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39034"
|
||||
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
|
||||
msgctxt "#39035"
|
||||
|
@ -943,18 +982,23 @@ msgid ""
|
|||
"Replace Plex paths /volume1/media or \\\\myserver\\media with custom SMB "
|
||||
"paths smb://NAS/mystuff"
|
||||
msgstr ""
|
||||
"Ersätt Plex sökväg /volume1/media eller \\\\myserver\\media med anpassade "
|
||||
"SMB sökvägar smb://NAS/mystuff"
|
||||
"Ersätt Plex-sökväg /volume1/media eller \\\\myserver\\media med anpassade "
|
||||
"SMB-sökvägar smb://NAS/mystuff"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39036"
|
||||
msgid "Escape special characters in path (e.g. space to %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
|
||||
msgctxt "#39037"
|
||||
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
|
||||
msgctxt "#39038"
|
||||
|
@ -964,7 +1008,7 @@ msgstr "Ersätt Plex MOVIE med:"
|
|||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39039"
|
||||
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
|
||||
msgctxt "#39040"
|
||||
|
@ -974,7 +1018,7 @@ msgstr "Ersätt Plex TV SHOWS med:"
|
|||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39041"
|
||||
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
|
||||
msgctxt "#39042"
|
||||
|
@ -997,8 +1041,8 @@ msgid ""
|
|||
"Please enter your custom smb paths in the settings under \"Sync Options\" "
|
||||
"and then restart Kodi"
|
||||
msgstr ""
|
||||
"Ange din anpassade smb-sökväg i inställningarna under \"Synkroniserings "
|
||||
"inställningar\" och starta sedan om Kodi"
|
||||
"Ange din anpassade SMB-sökväg i inställningarna under "
|
||||
"\"Synkroniseringsinställningar\" och starta om Kodi"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39045"
|
||||
|
@ -1033,12 +1077,12 @@ msgstr "Välj en Plex Server från en lista"
|
|||
# PKC Settings - Sync
|
||||
msgctxt "#39051"
|
||||
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
|
||||
msgctxt "#39052"
|
||||
msgid "Background Sync"
|
||||
msgstr "Bakgrundssynkning."
|
||||
msgstr "Bakgrundssynkronisering."
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39053"
|
||||
|
@ -1060,6 +1104,8 @@ msgctxt "#39056"
|
|||
msgid ""
|
||||
"Used by sync and when attempting Direct Paths. Restart Kodi on changes!"
|
||||
msgstr ""
|
||||
"Används av sync samt vid användning av Direct Paths. Starta om Kodi vid "
|
||||
"förändringar!"
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#39057"
|
||||
|
@ -1133,6 +1179,11 @@ msgctxt "#39071"
|
|||
msgid "Current plex.tv status:"
|
||||
msgstr "Nuvarande plex.tv status:"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39072"
|
||||
msgid "Background sync connection:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#39073"
|
||||
msgid "Appearance Tweaks"
|
||||
|
@ -1143,6 +1194,11 @@ msgctxt "#39074"
|
|||
msgid "TV Shows"
|
||||
msgstr "TV-Serier"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39075"
|
||||
msgid "Verify access to media files while synching"
|
||||
msgstr ""
|
||||
|
||||
# Pop-up during initial sync
|
||||
msgctxt "#39076"
|
||||
msgid ""
|
||||
|
@ -1160,7 +1216,7 @@ msgstr "Maximalt antal filmer att visa i widgets"
|
|||
# PKC Settings - Plex
|
||||
msgctxt "#39078"
|
||||
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
|
||||
msgctxt "#39079"
|
||||
|
@ -1168,7 +1224,7 @@ msgid ""
|
|||
"Plex Companion could not open the GDM port. Please change it in the PKC "
|
||||
"settings."
|
||||
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.
|
||||
# Check that next translations for Add-on Paths and Direct Paths are
|
||||
|
@ -1205,7 +1261,32 @@ msgstr "Ange PMS port"
|
|||
# PKC settings - Appearance Tweaks
|
||||
msgctxt "#39085"
|
||||
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"
|
||||
msgid "Log-out Plex Home User "
|
||||
|
@ -1223,8 +1304,8 @@ msgstr "Utför manuell bibliotekssynkronisering"
|
|||
msgctxt "#39205"
|
||||
msgid "Unable to run the sync, the add-on is not connected to a Plex server."
|
||||
msgstr ""
|
||||
"Kunde inte köra synkronisering, tillägget är inte ansluten till en Plex "
|
||||
"server."
|
||||
"Kunde inte köra synkronisering, tillägget är inte ansluten till en "
|
||||
"Plexserver."
|
||||
|
||||
msgctxt "#39206"
|
||||
msgid ""
|
||||
|
@ -1267,7 +1348,7 @@ msgstr "Ange din Plex Media Server IP eller URL, exempelvis:"
|
|||
|
||||
msgctxt "#39217"
|
||||
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"
|
||||
msgid "Error contacting PMS"
|
||||
|
@ -1403,7 +1484,7 @@ msgstr ""
|
|||
|
||||
msgctxt "#39402"
|
||||
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"
|
||||
msgid ""
|
||||
|
@ -1516,7 +1597,7 @@ msgstr "Använd på egen risk"
|
|||
# If user gets prompted to choose between several subtitles to burn in
|
||||
msgctxt "#39706"
|
||||
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
|
||||
# language is unknown
|
||||
|
@ -1556,7 +1637,7 @@ msgstr ""
|
|||
# Shown during sync process
|
||||
msgctxt "#39712"
|
||||
msgid "downloaded"
|
||||
msgstr "Nedladdade"
|
||||
msgstr "nedladdade"
|
||||
|
||||
# Shown during sync process
|
||||
msgctxt "#39713"
|
||||
|
|
|
@ -44,6 +44,13 @@ msgstr ""
|
|||
"Попередження: налаштування Kodi \"відтворювати наступне відео автоматично\" "
|
||||
"включено. Це може перервати роботу PKC. Вимкнути?"
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid ""
|
||||
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||
"random password automatically if you haven't done so already. Please confirm"
|
||||
" the next dialog that you want to enable the webserver now with Yes."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username: "
|
||||
msgstr "Ім'я користувача:"
|
||||
|
@ -160,6 +167,14 @@ msgctxt "#30028"
|
|||
msgid "PKC-only image caching completed"
|
||||
msgstr "Кешування зображень PKC завершено"
|
||||
|
||||
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||
msgctxt "#30029"
|
||||
msgid ""
|
||||
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
|
||||
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
|
||||
"database resets. Continue?"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Port Number"
|
||||
msgstr "Номер порту"
|
||||
|
@ -600,6 +615,11 @@ msgctxt "#30524"
|
|||
msgid "Select Plex libraries to sync"
|
||||
msgstr "Обрати бібліотеки Plex для синхронізації"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -664,8 +684,8 @@ msgstr "Завантажувати матеріали набору фільмі
|
|||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30541"
|
||||
msgid "Don't ask to pick a certain stream/quality"
|
||||
msgstr "Не запитувати обирання певного потоку або якості"
|
||||
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30542"
|
||||
|
@ -686,6 +706,21 @@ msgctxt "#30545"
|
|||
msgid "Force transcode pictures"
|
||||
msgstr "Примусове перекодування зображень"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30546"
|
||||
msgid "Pick the first video if several versions are present"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30547"
|
||||
msgid "Who picks the audio stream on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30548"
|
||||
msgid "Who picks subtitles on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# Welcome to Plex notification
|
||||
msgctxt "#33000"
|
||||
msgid "Welcome"
|
||||
|
@ -957,6 +992,11 @@ msgctxt "#39036"
|
|||
msgid "Escape special characters in path (e.g. space to %20)"
|
||||
msgstr "Замінювати спеціальні символи у шляхах (наприклад, пробіл у %20)"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39090"
|
||||
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||
msgstr "Безпечні символи для URL-адрес http(s), dav(s) та (s)ftp"
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39037"
|
||||
msgid "Original Plex MOVIE path to replace:"
|
||||
|
@ -1141,6 +1181,11 @@ msgctxt "#39071"
|
|||
msgid "Current plex.tv status:"
|
||||
msgstr "Поточний plex.tv статус:"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39072"
|
||||
msgid "Background sync connection:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#39073"
|
||||
msgid "Appearance Tweaks"
|
||||
|
@ -1151,6 +1196,11 @@ msgctxt "#39074"
|
|||
msgid "TV Shows"
|
||||
msgstr "Серіали"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39075"
|
||||
msgid "Verify access to media files while synching"
|
||||
msgstr ""
|
||||
|
||||
# Pop-up during initial sync
|
||||
msgctxt "#39076"
|
||||
msgid ""
|
||||
|
@ -1218,6 +1268,31 @@ msgid "Reload Kodi node files to apply all the settings below"
|
|||
msgstr ""
|
||||
"Перезавантажити файли вузла Kodi для застосування всіх наступних налаштувань"
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39089"
|
||||
msgid "Alexa connection status:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39091"
|
||||
msgid "Timeout - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39092"
|
||||
msgid "IOError - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39093"
|
||||
msgid "Suspended - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39094"
|
||||
msgid "Managed Plex User - not connected"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39200"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "Вийти з профілю користувача Plex Home"
|
||||
|
@ -1528,7 +1603,7 @@ msgstr "Використовуйте на свій ризик"
|
|||
# If user gets prompted to choose between several subtitles to burn in
|
||||
msgctxt "#39706"
|
||||
msgid "Don't burn-in any subtitle"
|
||||
msgstr ""
|
||||
msgstr "Не виводити жодних субтитрів"
|
||||
|
||||
# If user gets prompted to choose between several audio/subtitle tracks and
|
||||
# language is unknown
|
||||
|
|
|
@ -44,6 +44,13 @@ msgid ""
|
|||
"could break PKC. Deactivate?"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid ""
|
||||
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||
"random password automatically if you haven't done so already. Please confirm"
|
||||
" the next dialog that you want to enable the webserver now with Yes."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username: "
|
||||
msgstr "用户名 "
|
||||
|
@ -159,6 +166,14 @@ msgctxt "#30028"
|
|||
msgid "PKC-only image caching completed"
|
||||
msgstr ""
|
||||
|
||||
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||
msgctxt "#30029"
|
||||
msgid ""
|
||||
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
|
||||
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
|
||||
"database resets. Continue?"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Port Number"
|
||||
msgstr "端口号"
|
||||
|
@ -585,6 +600,11 @@ msgctxt "#30524"
|
|||
msgid "Select Plex libraries to sync"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -647,8 +667,8 @@ msgstr "从FanArtTV下载额外的电影集/收藏art"
|
|||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30541"
|
||||
msgid "Don't ask to pick a certain stream/quality"
|
||||
msgstr "无需询问挑选特定的串流/质量"
|
||||
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30542"
|
||||
|
@ -669,6 +689,21 @@ msgctxt "#30545"
|
|||
msgid "Force transcode pictures"
|
||||
msgstr "强制图片转码"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30546"
|
||||
msgid "Pick the first video if several versions are present"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30547"
|
||||
msgid "Who picks the audio stream on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30548"
|
||||
msgid "Who picks subtitles on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# Welcome to Plex notification
|
||||
msgctxt "#33000"
|
||||
msgid "Welcome"
|
||||
|
@ -918,6 +953,11 @@ msgctxt "#39036"
|
|||
msgid "Escape special characters in path (e.g. space to %20)"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39090"
|
||||
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39037"
|
||||
msgid "Original Plex MOVIE path to replace:"
|
||||
|
@ -1097,6 +1137,11 @@ msgctxt "#39071"
|
|||
msgid "Current plex.tv status:"
|
||||
msgstr "当前plex.tv状态:"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39072"
|
||||
msgid "Background sync connection:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#39073"
|
||||
msgid "Appearance Tweaks"
|
||||
|
@ -1107,6 +1152,11 @@ msgctxt "#39074"
|
|||
msgid "TV Shows"
|
||||
msgstr "电视节目"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39075"
|
||||
msgid "Verify access to media files while synching"
|
||||
msgstr ""
|
||||
|
||||
# Pop-up during initial sync
|
||||
msgctxt "#39076"
|
||||
msgid ""
|
||||
|
@ -1165,6 +1215,31 @@ msgctxt "#39085"
|
|||
msgid "Reload Kodi node files to apply all the settings below"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39089"
|
||||
msgid "Alexa connection status:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39091"
|
||||
msgid "Timeout - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39092"
|
||||
msgid "IOError - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39093"
|
||||
msgid "Suspended - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39094"
|
||||
msgid "Managed Plex User - not connected"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39200"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "退出Plex家庭用户 "
|
||||
|
|
|
@ -42,6 +42,13 @@ msgid ""
|
|||
"could break PKC. Deactivate?"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30004"
|
||||
msgid ""
|
||||
"The Kodi webserver is needed for artwork caching. PKC already set a strong, "
|
||||
"random password automatically if you haven't done so already. Please confirm"
|
||||
" the next dialog that you want to enable the webserver now with Yes."
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username: "
|
||||
msgstr "使用者: "
|
||||
|
@ -157,6 +164,14 @@ msgctxt "#30028"
|
|||
msgid "PKC-only image caching completed"
|
||||
msgstr ""
|
||||
|
||||
# Warning shown when PKC switches to the Kodi default skin Estuary
|
||||
msgctxt "#30029"
|
||||
msgid ""
|
||||
"To ensure a smooth PlexKodiConnect experience, it is HIGHLY recommended to "
|
||||
"use Kodi's default skin \"Estuary\" for initial set-up and for possible "
|
||||
"database resets. Continue?"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30030"
|
||||
msgid "Port Number"
|
||||
msgstr "埠號"
|
||||
|
@ -583,6 +598,11 @@ msgctxt "#30524"
|
|||
msgid "Select Plex libraries to sync"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30525"
|
||||
msgid "Skip intro"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30527"
|
||||
msgid "Ignore specials in next episodes"
|
||||
|
@ -645,8 +665,8 @@ msgstr "從 FanArtTV 下載電影合輯海報"
|
|||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30541"
|
||||
msgid "Don't ask to pick a certain stream/quality"
|
||||
msgstr "不要要求挑選特定的 串流/品質"
|
||||
msgid "Transcoding: Auto-pick audio and subtitle stream using Plex defaults"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30542"
|
||||
|
@ -667,6 +687,21 @@ msgctxt "#30545"
|
|||
msgid "Force transcode pictures"
|
||||
msgstr "強制圖片轉碼"
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30546"
|
||||
msgid "Pick the first video if several versions are present"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30547"
|
||||
msgid "Who picks the audio stream on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Playback
|
||||
msgctxt "#30548"
|
||||
msgid "Who picks subtitles on playback start?"
|
||||
msgstr ""
|
||||
|
||||
# Welcome to Plex notification
|
||||
msgctxt "#33000"
|
||||
msgid "Welcome"
|
||||
|
@ -916,6 +951,11 @@ msgctxt "#39036"
|
|||
msgid "Escape special characters in path (e.g. space to %20)"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39090"
|
||||
msgid "Safe characters for http(s), dav(s) and (s)ftp urls"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Customize Paths
|
||||
msgctxt "#39037"
|
||||
msgid "Original Plex MOVIE path to replace:"
|
||||
|
@ -1093,6 +1133,11 @@ msgctxt "#39071"
|
|||
msgid "Current plex.tv status:"
|
||||
msgstr "plex.tv 狀態︰"
|
||||
|
||||
# PKC Settings - Connection
|
||||
msgctxt "#39072"
|
||||
msgid "Background sync connection:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings, category name
|
||||
msgctxt "#39073"
|
||||
msgid "Appearance Tweaks"
|
||||
|
@ -1103,6 +1148,11 @@ msgctxt "#39074"
|
|||
msgid "TV Shows"
|
||||
msgstr "電視節目"
|
||||
|
||||
# PKC Settings - Sync
|
||||
msgctxt "#39075"
|
||||
msgid "Verify access to media files while synching"
|
||||
msgstr ""
|
||||
|
||||
# Pop-up during initial sync
|
||||
msgctxt "#39076"
|
||||
msgid ""
|
||||
|
@ -1161,6 +1211,31 @@ msgctxt "#39085"
|
|||
msgid "Reload Kodi node files to apply all the settings below"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39089"
|
||||
msgid "Alexa connection status:"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39091"
|
||||
msgid "Timeout - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39092"
|
||||
msgid "IOError - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39093"
|
||||
msgid "Suspended - not connected"
|
||||
msgstr ""
|
||||
|
||||
# PKC Settings - Connection - Background sync connection status
|
||||
msgctxt "#39094"
|
||||
msgid "Managed Plex User - not connected"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#39200"
|
||||
msgid "Log-out Plex Home User "
|
||||
msgstr "登出Plex Home用戶 "
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
Used to save PKC's application state and share between modules. Be careful
|
||||
if you invoke another PKC Python instance (!!) when e.g. PKC.movies is called
|
||||
"""
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from .account import Account
|
||||
from .application import App
|
||||
from .connection import Connection
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from .. import utils
|
||||
|
@ -15,6 +14,8 @@ class Account(object):
|
|||
self.plex_username = None
|
||||
self.plex_user_id = None
|
||||
self.plex_token = None
|
||||
# Personal access token per specific user and PMS
|
||||
# As a rule of thumb, always use this token!
|
||||
self.pms_token = None
|
||||
self.avatar = None
|
||||
self.myplexlogin = None
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import Queue
|
||||
import queue
|
||||
from threading import Lock, RLock
|
||||
|
||||
import xbmc
|
||||
|
@ -19,6 +18,8 @@ class App(object):
|
|||
def __init__(self, entrypoint=False):
|
||||
self.fetch_pms_item_number = None
|
||||
self.force_reload_skin = None
|
||||
# All thread instances
|
||||
self.threads = []
|
||||
if entrypoint:
|
||||
self.load_entrypoint()
|
||||
else:
|
||||
|
@ -38,19 +39,19 @@ class App(object):
|
|||
self.lock_playlists = Lock()
|
||||
|
||||
# Plex Companion Queue()
|
||||
self.companion_queue = Queue.Queue(maxsize=100)
|
||||
self.companion_queue = queue.Queue(maxsize=100)
|
||||
# Websocket_client queue to communicate with librarysync
|
||||
self.websocket_queue = Queue.Queue()
|
||||
self.websocket_queue = queue.Queue()
|
||||
# xbmc.Monitor() instance from kodimonitor.py
|
||||
self.monitor = None
|
||||
# xbmc.Player() instance
|
||||
self.player = None
|
||||
# All thread instances
|
||||
self.threads = []
|
||||
# Instance of FanartThread()
|
||||
self.fanart_thread = None
|
||||
# Instance of MetadataThread()
|
||||
self.metadata_thread = None
|
||||
# Instance of ImageCachingThread()
|
||||
self.caching_thread = None
|
||||
# Dialog to skip intro
|
||||
self.skip_intro_dialog = None
|
||||
|
||||
@property
|
||||
def is_playing(self):
|
||||
|
@ -60,24 +61,24 @@ class App(object):
|
|||
def is_playing_video(self):
|
||||
return self.player.isPlayingVideo() == 1
|
||||
|
||||
def register_fanart_thread(self, thread):
|
||||
self.fanart_thread = thread
|
||||
def register_metadata_thread(self, thread):
|
||||
self.metadata_thread = thread
|
||||
self.threads.append(thread)
|
||||
|
||||
def deregister_fanart_thread(self, thread):
|
||||
self.fanart_thread.unblock_callers()
|
||||
self.fanart_thread = None
|
||||
def deregister_metadata_thread(self, thread):
|
||||
self.metadata_thread.unblock_callers()
|
||||
self.metadata_thread = None
|
||||
self.threads.remove(thread)
|
||||
|
||||
def suspend_fanart_thread(self, block=True):
|
||||
def suspend_metadata_thread(self, block=True):
|
||||
try:
|
||||
self.fanart_thread.suspend(block=block)
|
||||
self.metadata_thread.suspend(block=block)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def resume_fanart_thread(self):
|
||||
def resume_metadata_thread(self):
|
||||
try:
|
||||
self.fanart_thread.resume()
|
||||
self.metadata_thread.resume()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import secrets
|
||||
|
||||
from .. import utils, json_rpc as js, variables as v
|
||||
from .. import utils, json_rpc as js
|
||||
|
||||
LOG = getLogger('PLEX.connection')
|
||||
|
||||
|
@ -38,22 +38,36 @@ class Connection(object):
|
|||
PKC needs Kodi webserver to work correctly
|
||||
"""
|
||||
LOG.debug('Loading Kodi webserver details')
|
||||
# Kodi webserver details
|
||||
if js.get_setting('services.webserver') in (None, False):
|
||||
# Enable the webserver, it is disabled
|
||||
if not utils.settings('enableTextureCache') == 'true':
|
||||
LOG.info('Artwork caching disabled')
|
||||
return
|
||||
self.webserver_password = js.get_setting('services.webserverpassword')
|
||||
if not self.webserver_password:
|
||||
LOG.warn('No password set for the Kodi web server. Generating a '
|
||||
'new random password')
|
||||
self.webserver_password = secrets.token_urlsafe(16)
|
||||
js.set_setting('services.webserverpassword', self.webserver_password)
|
||||
if not js.get_setting('services.webserver'):
|
||||
# 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.
|
||||
utils.messageDialog(utils.lang(29999), utils.lang(30004))
|
||||
# Enable the webserver, it is disabled. Will force a Kodi pop-up
|
||||
js.set_setting('services.webserver', True)
|
||||
if not js.get_setting('services.webserver'):
|
||||
LOG.warn('User chose to not enable Kodi webserver')
|
||||
utils.settings('enableTextureCache', value='false')
|
||||
self.webserver_host = 'localhost'
|
||||
self.webserver_port = js.get_setting('services.webserverport')
|
||||
self.webserver_username = js.get_setting('services.webserverusername')
|
||||
self.webserver_password = js.get_setting('services.webserverpassword')
|
||||
|
||||
def load(self):
|
||||
LOG.debug('Loading connection settings')
|
||||
# Shall we verify SSL certificates? "None" will leave SSL enabled
|
||||
# Ignore this setting for Kodi >= 18 as Kodi 18 is much stricter
|
||||
# with checking SSL certs
|
||||
self.verify_ssl_cert = None if v.KODIVERSION >= 18 or utils.settings('sslverify') == 'true' \
|
||||
else False
|
||||
self.verify_ssl_cert = None
|
||||
# Do we have an ssl certificate for PKC we need to use?
|
||||
self.ssl_cert_path = utils.settings('sslcert') \
|
||||
if utils.settings('sslcert') != 'None' else None
|
||||
|
@ -74,8 +88,7 @@ class Connection(object):
|
|||
self.server_name, self.machine_identifier, self.server)
|
||||
|
||||
def load_entrypoint(self):
|
||||
self.verify_ssl_cert = None if v.KODIVERSION >= 18 or utils.settings('sslverify') == 'true' \
|
||||
else False
|
||||
self.verify_ssl_cert = None
|
||||
self.ssl_cert_path = utils.settings('sslcert') \
|
||||
if utils.settings('sslcert') != 'None' else None
|
||||
self.https = utils.settings('https') == 'true'
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
|
||||
from .. import utils
|
||||
|
||||
|
||||
|
@ -57,8 +55,6 @@ class Sync(object):
|
|||
|
||||
# How often shall we sync?
|
||||
self.full_sync_intervall = None
|
||||
# Background Sync disabled?
|
||||
self.background_sync_disabled = None
|
||||
# How long shall we wait with synching a new item to make sure Plex got all
|
||||
# metadata?
|
||||
self.backgroundsync_saftymargin = None
|
||||
|
@ -73,6 +69,8 @@ class Sync(object):
|
|||
self.run_lib_scan = None
|
||||
# Set if user decided to cancel sync
|
||||
self.stop_sync = False
|
||||
# Do we check whether we can access a media file?
|
||||
self.check_media_file_existence = False
|
||||
# Could we access the paths?
|
||||
self.path_verified = False
|
||||
|
||||
|
@ -81,7 +79,6 @@ class Sync(object):
|
|||
# List of section_ids we're synching to Kodi - will be automatically
|
||||
# re-built if sections are set a-new
|
||||
self.section_ids = set()
|
||||
self.enable_alexa = None
|
||||
|
||||
self.load()
|
||||
|
||||
|
@ -97,6 +94,8 @@ class Sync(object):
|
|||
|
||||
def load(self):
|
||||
self.direct_paths = utils.settings('useDirectPaths') == '1'
|
||||
self.check_media_file_existence = \
|
||||
utils.settings('check_media_file_existence') == '1'
|
||||
self.enable_music = utils.settings('enableMusic') == 'true'
|
||||
self.artwork = utils.settings('usePlexArtwork') == 'true'
|
||||
self.replace_smb_path = utils.settings('replaceSMB') == 'true'
|
||||
|
@ -110,7 +109,7 @@ class Sync(object):
|
|||
self.remapSMBphotoOrg = remove_trailing_slash(utils.settings('remapSMBphotoOrg'))
|
||||
self.remapSMBphotoNew = remove_trailing_slash(utils.settings('remapSMBphotoNew'))
|
||||
self.escape_path = utils.settings('escapePath') == 'true'
|
||||
self.escape_path_safe_chars = utils.settings('escapePathSafeChars').encode('utf-8')
|
||||
self.escape_path_safe_chars = utils.settings('escapePathSafeChars')
|
||||
self.indicate_media_versions = utils.settings('indicate_media_versions') == "true"
|
||||
self.sync_specific_plex_playlists = utils.settings('syncSpecificPlexPlaylists') == 'true'
|
||||
self.sync_specific_kodi_playlists = utils.settings('syncSpecificKodiPlaylists') == 'true'
|
||||
|
@ -122,8 +121,6 @@ class Sync(object):
|
|||
Any settings unrelated to syncs to the Kodi database - can thus be
|
||||
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.full_sync_intervall = int(utils.settings('fullSyncInterval')) * 60
|
||||
self.backgroundsync_saftymargin = int(utils.settings('backgroundsync_saftyMargin'))
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
|
||||
|
||||
class PlayState(object):
|
||||
# "empty" dict for the PLAYER_STATES above. Use copy.deepcopy to duplicate!
|
||||
template = {
|
||||
|
@ -36,7 +33,8 @@ class PlayState(object):
|
|||
'muted': False,
|
||||
'playmethod': None,
|
||||
'playcount': None,
|
||||
'external_player': False # bool - xbmc.Player().isExternalPlayer()
|
||||
'external_player': False, # bool - xbmc.Player().isExternalPlayer()
|
||||
'intro_markers': [],
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import requests
|
||||
|
||||
|
@ -82,7 +81,7 @@ class ImageCachingThread(backgroundthread.KillableThread):
|
|||
for url in self._url_generator(kind, kodi_type):
|
||||
if self.should_suspend() or self.should_cancel():
|
||||
return False
|
||||
cache_url(url)
|
||||
cache_url(url, self.should_suspend)
|
||||
# Toggles Image caching completed to Yes
|
||||
utils.settings('plex_status_image_caching', value=utils.lang(107))
|
||||
return True
|
||||
|
@ -95,16 +94,13 @@ class ImageCachingThread(backgroundthread.KillableThread):
|
|||
break
|
||||
|
||||
|
||||
def cache_url(url):
|
||||
def cache_url(url, should_suspend=None):
|
||||
url = double_urlencode(url)
|
||||
sleeptime = 0
|
||||
while True:
|
||||
try:
|
||||
requests.head(
|
||||
url="http://%s:%s/image/image://%s"
|
||||
% (app.CONN.webserver_host,
|
||||
app.CONN.webserver_port,
|
||||
url),
|
||||
url=f'http://{app.CONN.webserver_username}:{app.CONN.webserver_password}@{app.CONN.webserver_host}:{app.CONN.webserver_port}/image/image://{url}',
|
||||
auth=(app.CONN.webserver_username,
|
||||
app.CONN.webserver_password),
|
||||
timeout=TIMEOUT)
|
||||
|
@ -113,11 +109,11 @@ def cache_url(url):
|
|||
# download. All is well
|
||||
break
|
||||
except requests.ConnectionError:
|
||||
if app.APP.stop_pkc:
|
||||
# Kodi terminated
|
||||
if app.APP.stop_pkc or (should_suspend and should_suspend()):
|
||||
break
|
||||
# Server thinks its a DOS attack, ('error 10053')
|
||||
# Wait before trying again
|
||||
# OR: Kodi refuses Webserver connection (no password set)
|
||||
if sleeptime > 5:
|
||||
LOG.error('Repeatedly got ConnectionError for url %s',
|
||||
double_urldecode(url))
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
from time import time as _time
|
||||
import threading
|
||||
import Queue
|
||||
import queue
|
||||
import heapq
|
||||
from collections import deque
|
||||
from functools import total_ordering
|
||||
|
||||
from . import utils, app, variables as v
|
||||
|
||||
|
@ -113,7 +113,7 @@ class KillableThread(threading.Thread):
|
|||
self._suspension_reached.set()
|
||||
|
||||
|
||||
class ProcessingQueue(Queue.Queue, object):
|
||||
class ProcessingQueue(queue.Queue, object):
|
||||
"""
|
||||
Queue of queues that processes a queue completely before moving on to the
|
||||
next queue. There's one queue per Section(). You need to initialize each
|
||||
|
@ -135,38 +135,6 @@ class ProcessingQueue(Queue.Queue, object):
|
|||
def _qsize(self):
|
||||
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):
|
||||
for i, section in enumerate(self._sections):
|
||||
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
|
||||
you've thus exhausted the queue
|
||||
"""
|
||||
self.not_full.acquire()
|
||||
try:
|
||||
with self.not_full:
|
||||
section.number_of_items = 1
|
||||
self._add_section(section)
|
||||
# Add the actual sentinel to the queue we just added
|
||||
self._queues[-1]._put((None, None))
|
||||
self.unfinished_tasks += 1
|
||||
self.not_empty.notify()
|
||||
finally:
|
||||
self.not_full.release()
|
||||
|
||||
def add_section(self, section):
|
||||
"""
|
||||
|
@ -202,17 +167,32 @@ class ProcessingQueue(Queue.Queue, object):
|
|||
Be sure to set section.number_of_items correctly as it will signal
|
||||
when processing is completely done for a specific section!
|
||||
"""
|
||||
self.mutex.acquire()
|
||||
try:
|
||||
with self.mutex:
|
||||
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):
|
||||
self._sections.append(section)
|
||||
self._queues.append(
|
||||
OrderedQueue() if section.plex_type == v.PLEX_TYPE_ALBUM
|
||||
else Queue.Queue())
|
||||
else queue.Queue())
|
||||
if self._current_section is None:
|
||||
self._activate_next_section()
|
||||
|
||||
|
@ -237,7 +217,7 @@ class ProcessingQueue(Queue.Queue, object):
|
|||
return item[1]
|
||||
|
||||
|
||||
class OrderedQueue(Queue.PriorityQueue, object):
|
||||
class OrderedQueue(queue.PriorityQueue, object):
|
||||
"""
|
||||
Queue that enforces an order on the items it returns. An item you push
|
||||
onto the queue must be a tuple
|
||||
|
@ -253,15 +233,15 @@ class OrderedQueue(Queue.PriorityQueue, object):
|
|||
self.next_index = 0
|
||||
super(OrderedQueue, self).__init__(maxsize)
|
||||
|
||||
def _qsize(self, len=len):
|
||||
def _qsize(self):
|
||||
try:
|
||||
return len(self.queue) if self.queue[0][0] == self.next_index else 0
|
||||
except IndexError:
|
||||
return 0
|
||||
|
||||
def _get(self, heappop=heapq.heappop):
|
||||
def _get(self):
|
||||
self.next_index += 1
|
||||
return heappop(self.queue)
|
||||
return heapq.heappop(self.queue)
|
||||
|
||||
|
||||
class Tasks(list):
|
||||
|
@ -280,14 +260,20 @@ class Tasks(list):
|
|||
self.pop().cancel()
|
||||
|
||||
|
||||
@total_ordering
|
||||
class Task(object):
|
||||
def __init__(self, priority=None):
|
||||
self.priority = priority
|
||||
self._canceled = False
|
||||
self.finished = False
|
||||
|
||||
def __cmp__(self, other):
|
||||
return self.priority - other.priority
|
||||
def __lt__(self, other):
|
||||
"""Magic method Task<Other Task; compares the tasks' priorities."""
|
||||
return self.priority - other.priority > 0
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Magic method Task=Other Task; compares the tasks' priorities."""
|
||||
return self.priority == other.priority
|
||||
|
||||
def start(self):
|
||||
BGThreader.addTask(self)
|
||||
|
@ -328,10 +314,10 @@ class FunctionAsTask(Task):
|
|||
self._callback(result)
|
||||
|
||||
|
||||
class MutablePriorityQueue(Queue.PriorityQueue):
|
||||
def _get(self, heappop=heapq.heappop):
|
||||
class MutablePriorityQueue(queue.PriorityQueue):
|
||||
def _get(self):
|
||||
self.queue.sort()
|
||||
return heappop(self.queue)
|
||||
return heapq.heappop(self.queue)
|
||||
|
||||
def lowest(self):
|
||||
"""Return the lowest priority item in the queue (not reliable!)."""
|
||||
|
@ -371,7 +357,7 @@ class BackgroundWorker(object):
|
|||
return self._abort or app.APP.monitor.abortRequested()
|
||||
|
||||
def start(self):
|
||||
if self._thread and self._thread.isAlive():
|
||||
if self._thread and self._thread.is_alive():
|
||||
return
|
||||
|
||||
self._thread = KillableThread(target=self._queueLoop, name='BACKGROUND-WORKER({0})'.format(self.name))
|
||||
|
@ -388,7 +374,7 @@ class BackgroundWorker(object):
|
|||
self._runTask(self._task)
|
||||
self._queue.task_done()
|
||||
self._task = None
|
||||
except Queue.Empty:
|
||||
except queue.Empty:
|
||||
LOG.debug('(%s): Idle', self.name)
|
||||
|
||||
def shutdown(self, block=True):
|
||||
|
@ -397,13 +383,13 @@ class BackgroundWorker(object):
|
|||
if self._task:
|
||||
self._task.cancel()
|
||||
|
||||
if block and self._thread and self._thread.isAlive():
|
||||
if block and self._thread and self._thread.is_alive():
|
||||
LOG.debug('thread (%s): Waiting...', self.name)
|
||||
self._thread.join()
|
||||
LOG.debug('thread (%s): Done', self.name)
|
||||
|
||||
def working(self):
|
||||
return self._thread and self._thread.isAlive()
|
||||
return self._thread and self._thread.is_alive()
|
||||
|
||||
|
||||
class NonstoppingBackgroundWorker(BackgroundWorker):
|
||||
|
@ -428,7 +414,7 @@ class NonstoppingBackgroundWorker(BackgroundWorker):
|
|||
return self._working
|
||||
|
||||
|
||||
class BackgroundThreader:
|
||||
class BackgroundThreader(object):
|
||||
def __init__(self, name=None, worker=BackgroundWorker, worker_count=6):
|
||||
self.name = name
|
||||
self._queue = MutablePriorityQueue()
|
||||
|
@ -508,7 +494,7 @@ class BackgroundThreader:
|
|||
qitem.priority = lowest - 1
|
||||
|
||||
|
||||
class ThreaderManager:
|
||||
class ThreaderManager(object):
|
||||
def __init__(self,
|
||||
worker=NonstoppingBackgroundWorker,
|
||||
worker_count=WORKER_COUNT):
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
import xbmc
|
||||
|
@ -30,7 +29,6 @@ def getXArgsDeviceInfo(options=None, include_token=True):
|
|||
"""
|
||||
xargs = {
|
||||
'Accept': '*/*',
|
||||
'Connection': 'keep-alive',
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
# "Access-Control-Allow-Origin": "*",
|
||||
'Accept-Language': xbmc.getLanguage(xbmc.ISO_639_1),
|
||||
|
@ -43,6 +41,8 @@ def getXArgsDeviceInfo(options=None, include_token=True):
|
|||
'X-Plex-Version': v.ADDON_VERSION,
|
||||
'X-Plex-Client-Identifier': getDeviceId(),
|
||||
'X-Plex-Provides': 'client,controller,player,pubsub-player',
|
||||
'X-Plex-Protocol': '1.0',
|
||||
'Cache-Control': 'no-cache'
|
||||
}
|
||||
if include_token and utils.window('pms_token'):
|
||||
xargs['X-Plex-Token'] = utils.window('pms_token')
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
"""
|
||||
Processes Plex companion inputs from the plexbmchelper to Kodi commands
|
||||
"""
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
from xbmc import Player
|
||||
|
||||
|
@ -28,7 +27,7 @@ def skip_to(params):
|
|||
LOG.debug('Skipping to playQueueItemID %s, plex_id %s',
|
||||
playqueue_item_id, plex_id)
|
||||
found = True
|
||||
for player in js.get_players().values():
|
||||
for player in list(js.get_players().values()):
|
||||
playqueue = PQ.PLAYQUEUES[player['playerid']]
|
||||
for i, item in enumerate(playqueue.items):
|
||||
if item.id == playqueue_item_id:
|
||||
|
@ -49,7 +48,7 @@ def convert_alexa_to_companion(dictionary):
|
|||
"""
|
||||
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:
|
||||
dictionary[v.ALEXA_TO_COMPANION[key]] = dictionary[key]
|
||||
del dictionary[key]
|
||||
|
@ -86,7 +85,7 @@ def process_command(request_path, params):
|
|||
elif request_path == "player/playback/stop":
|
||||
js.stop()
|
||||
elif request_path == "player/playback/seekTo":
|
||||
js.seek_to(int(params.get('offset', 0)))
|
||||
js.seek_to(float(params.get('offset', 0.0)) / 1000.0)
|
||||
elif request_path == "player/playback/stepForward":
|
||||
js.smallforward()
|
||||
elif request_path == "player/playback/stepBack":
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import xbmcgui
|
||||
|
||||
|
@ -60,14 +59,14 @@ class ContextMenu(xbmcgui.WindowXMLDialog):
|
|||
if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK):
|
||||
if self.getFocusId() == LIST:
|
||||
option = self.list_.getSelectedItem()
|
||||
self.selected_option = option.getLabel().decode('utf-8')
|
||||
self.selected_option = option.getLabel()
|
||||
LOG.info('option selected: %s', self.selected_option)
|
||||
self.close()
|
||||
|
||||
def _add_editcontrol(self, x, y, height, width, password=None):
|
||||
media = path_ops.path.join(
|
||||
v.ADDON_PATH, 'resources', 'skins', 'default', 'media')
|
||||
filename = utils.try_encode(path_ops.path.join(media, 'white.png'))
|
||||
filename = path_ops.path.join(media, 'white.png')
|
||||
control = xbmcgui.ControlImage(0, 0, 0, 0,
|
||||
filename=filename,
|
||||
aspectRatio=0,
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
|
@ -92,7 +91,7 @@ class ContextMenu(object):
|
|||
options.append(OPTIONS['Addon'])
|
||||
context_menu = context.ContextMenu(
|
||||
"script-plex-context.xml",
|
||||
utils.try_encode(v.ADDON_PATH),
|
||||
v.ADDON_PATH,
|
||||
"default",
|
||||
"1080i")
|
||||
context_menu.set_options(options)
|
||||
|
@ -126,13 +125,13 @@ class ContextMenu(object):
|
|||
"""
|
||||
delete = True
|
||||
if utils.settings('skipContextMenu') != "true":
|
||||
if not utils.dialog("yesno", heading="{plex}", line1=utils.lang(33041)):
|
||||
if not utils.dialog("yesno", heading="{plex}", message=utils.lang(33041)):
|
||||
LOG.info("User skipped deletion for: %s", self.plex_id)
|
||||
delete = False
|
||||
if delete:
|
||||
LOG.info("Deleting Plex item with id %s", self.plex_id)
|
||||
if PF.delete_item_from_pms(self.plex_id) is False:
|
||||
utils.dialog("ok", heading="{plex}", line1=utils.lang(30414))
|
||||
utils.dialog("ok", heading="{plex}", message=utils.lang(30414))
|
||||
|
||||
def _PMS_play(self):
|
||||
"""
|
||||
|
@ -143,8 +142,8 @@ class ContextMenu(object):
|
|||
playqueue.clear()
|
||||
app.PLAYSTATE.context_menu_play = True
|
||||
handle = self.api.fullpath(force_addon=True)[0]
|
||||
handle = 'RunPlugin(%s)' % handle
|
||||
xbmc.executebuiltin(handle.encode('utf-8'))
|
||||
handle = f'RunPlugin({handle})'
|
||||
xbmc.executebuiltin(handle)
|
||||
|
||||
def _extras(self):
|
||||
"""
|
||||
|
|
|
@ -4,18 +4,13 @@ import sqlite3
|
|||
from functools import wraps
|
||||
|
||||
from . import variables as v, app
|
||||
from .exceptions import LockedDatabase
|
||||
|
||||
DB_WRITE_ATTEMPTS = 100
|
||||
DB_WRITE_ATTEMPTS_TIMEOUT = 1 # in seconds
|
||||
DB_CONNECTION_TIMEOUT = 10
|
||||
|
||||
|
||||
class LockedDatabase(Exception):
|
||||
"""
|
||||
Dedicated class to make sure we're not silently catching locked DBs.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def catch_operationalerrors(method):
|
||||
"""
|
||||
sqlite.OperationalError is raised immediately if another DB connection
|
||||
|
@ -32,7 +27,7 @@ def catch_operationalerrors(method):
|
|||
try:
|
||||
return method(self, *args, **kwargs)
|
||||
except sqlite3.OperationalError as err:
|
||||
if 'database is locked' not in err:
|
||||
if err.args[0] and 'database is locked' not in err.args[0]:
|
||||
# Not an error we want to catch, so reraise it
|
||||
raise
|
||||
attempts -= 1
|
||||
|
@ -43,7 +38,7 @@ def catch_operationalerrors(method):
|
|||
self.kodiconn.commit()
|
||||
if self.artconn:
|
||||
self.artconn.commit()
|
||||
if app.APP.monitor.waitForAbort(0.1):
|
||||
if app.APP.monitor.waitForAbort(DB_WRITE_ATTEMPTS_TIMEOUT):
|
||||
# PKC needs to quit
|
||||
return
|
||||
# Start new transactions
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
xml.etree.ElementTree tries to encode with text.encode('ascii') - which is
|
||||
just plain BS. This etree will always return unicode, not string
|
||||
"""
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
# Originally tried faster cElementTree, but does NOT work reliably with Kodi
|
||||
from defusedxml.ElementTree import DefusedXMLParser, _generate_etree_functions
|
||||
|
||||
from xml.etree.ElementTree import TreeBuilder as _TreeBuilder
|
||||
from xml.etree.ElementTree import parse as _parse
|
||||
from xml.etree.ElementTree import iterparse as _iterparse
|
||||
from xml.etree.ElementTree import tostring
|
||||
|
||||
|
||||
class UnicodeXMLParser(DefusedXMLParser):
|
||||
"""
|
||||
PKC Hack to ensure we're always receiving unicode, not str
|
||||
"""
|
||||
@staticmethod
|
||||
def _fixtext(text):
|
||||
"""
|
||||
Do NOT try to convert every entry to str with entry.encode('ascii')!
|
||||
"""
|
||||
return text
|
||||
|
||||
|
||||
# aliases
|
||||
XMLTreeBuilder = XMLParse = UnicodeXMLParser
|
||||
|
||||
parse, iterparse, fromstring = _generate_etree_functions(UnicodeXMLParser,
|
||||
_TreeBuilder, _parse,
|
||||
_iterparse)
|
||||
XML = fromstring
|
||||
|
||||
|
||||
__all__ = ['XML', 'XMLParse', 'XMLTreeBuilder', 'fromstring', 'iterparse',
|
||||
'parse', 'tostring']
|
188
resources/lib/defusedxml/ElementTree.py
Normal file
188
resources/lib/defusedxml/ElementTree.py
Normal file
|
@ -0,0 +1,188 @@
|
|||
# defusedxml
|
||||
#
|
||||
# Copyright (c) 2013-2020 by Christian Heimes <christian@python.org>
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See https://www.python.org/psf/license for licensing details.
|
||||
"""Defused xml.etree.ElementTree facade
|
||||
"""
|
||||
from __future__ import print_function, absolute_import
|
||||
|
||||
import sys
|
||||
import warnings
|
||||
from xml.etree.ElementTree import ParseError
|
||||
from xml.etree.ElementTree import TreeBuilder as _TreeBuilder
|
||||
from xml.etree.ElementTree import parse as _parse
|
||||
from xml.etree.ElementTree import tostring
|
||||
|
||||
import importlib
|
||||
|
||||
|
||||
from .common import DTDForbidden, EntitiesForbidden, ExternalReferenceForbidden
|
||||
|
||||
__origin__ = "xml.etree.ElementTree"
|
||||
|
||||
|
||||
def _get_py3_cls():
|
||||
"""Python 3.3 hides the pure Python code but defusedxml requires it.
|
||||
|
||||
The code is based on test.support.import_fresh_module().
|
||||
"""
|
||||
pymodname = "xml.etree.ElementTree"
|
||||
cmodname = "_elementtree"
|
||||
|
||||
pymod = sys.modules.pop(pymodname, None)
|
||||
cmod = sys.modules.pop(cmodname, None)
|
||||
|
||||
sys.modules[cmodname] = None
|
||||
try:
|
||||
pure_pymod = importlib.import_module(pymodname)
|
||||
finally:
|
||||
# restore module
|
||||
sys.modules[pymodname] = pymod
|
||||
if cmod is not None:
|
||||
sys.modules[cmodname] = cmod
|
||||
else:
|
||||
sys.modules.pop(cmodname, None)
|
||||
# restore attribute on original package
|
||||
etree_pkg = sys.modules["xml.etree"]
|
||||
if pymod is not None:
|
||||
etree_pkg.ElementTree = pymod
|
||||
elif hasattr(etree_pkg, "ElementTree"):
|
||||
del etree_pkg.ElementTree
|
||||
|
||||
_XMLParser = pure_pymod.XMLParser
|
||||
_iterparse = pure_pymod.iterparse
|
||||
# patch pure module to use ParseError from C extension
|
||||
pure_pymod.ParseError = ParseError
|
||||
|
||||
return _XMLParser, _iterparse
|
||||
|
||||
|
||||
_XMLParser, _iterparse = _get_py3_cls()
|
||||
|
||||
_sentinel = object()
|
||||
|
||||
|
||||
class DefusedXMLParser(_XMLParser):
|
||||
def __init__(
|
||||
self,
|
||||
html=_sentinel,
|
||||
target=None,
|
||||
encoding=None,
|
||||
forbid_dtd=False,
|
||||
forbid_entities=True,
|
||||
forbid_external=True,
|
||||
):
|
||||
super().__init__(target=target, encoding=encoding)
|
||||
if html is not _sentinel:
|
||||
# the 'html' argument has been deprecated and ignored in all
|
||||
# supported versions of Python. Python 3.8 finally removed it.
|
||||
if html:
|
||||
raise TypeError("'html=True' is no longer supported.")
|
||||
else:
|
||||
warnings.warn(
|
||||
"'html' keyword argument is no longer supported. Pass "
|
||||
"in arguments as keyword arguments.",
|
||||
category=DeprecationWarning,
|
||||
)
|
||||
|
||||
self.forbid_dtd = forbid_dtd
|
||||
self.forbid_entities = forbid_entities
|
||||
self.forbid_external = forbid_external
|
||||
parser = self.parser
|
||||
if self.forbid_dtd:
|
||||
parser.StartDoctypeDeclHandler = self.defused_start_doctype_decl
|
||||
if self.forbid_entities:
|
||||
parser.EntityDeclHandler = self.defused_entity_decl
|
||||
parser.UnparsedEntityDeclHandler = self.defused_unparsed_entity_decl
|
||||
if self.forbid_external:
|
||||
parser.ExternalEntityRefHandler = self.defused_external_entity_ref_handler
|
||||
|
||||
def defused_start_doctype_decl(self, name, sysid, pubid, has_internal_subset):
|
||||
raise DTDForbidden(name, sysid, pubid)
|
||||
|
||||
def defused_entity_decl(
|
||||
self, name, is_parameter_entity, value, base, sysid, pubid, notation_name
|
||||
):
|
||||
raise EntitiesForbidden(name, value, base, sysid, pubid, notation_name)
|
||||
|
||||
def defused_unparsed_entity_decl(self, name, base, sysid, pubid, notation_name):
|
||||
# expat 1.2
|
||||
raise EntitiesForbidden(name, None, base, sysid, pubid, notation_name) # pragma: no cover
|
||||
|
||||
def defused_external_entity_ref_handler(self, context, base, sysid, pubid):
|
||||
raise ExternalReferenceForbidden(context, base, sysid, pubid)
|
||||
|
||||
|
||||
# aliases
|
||||
# XMLParse is a typo, keep it for backwards compatibility
|
||||
XMLTreeBuilder = XMLParse = XMLParser = DefusedXMLParser
|
||||
|
||||
|
||||
def parse(source, parser=None, forbid_dtd=False, forbid_entities=True, forbid_external=True):
|
||||
if parser is None:
|
||||
parser = DefusedXMLParser(
|
||||
target=_TreeBuilder(),
|
||||
forbid_dtd=forbid_dtd,
|
||||
forbid_entities=forbid_entities,
|
||||
forbid_external=forbid_external,
|
||||
)
|
||||
return _parse(source, parser)
|
||||
|
||||
|
||||
def iterparse(
|
||||
source,
|
||||
events=None,
|
||||
parser=None,
|
||||
forbid_dtd=False,
|
||||
forbid_entities=True,
|
||||
forbid_external=True,
|
||||
):
|
||||
if parser is None:
|
||||
parser = DefusedXMLParser(
|
||||
target=_TreeBuilder(),
|
||||
forbid_dtd=forbid_dtd,
|
||||
forbid_entities=forbid_entities,
|
||||
forbid_external=forbid_external,
|
||||
)
|
||||
return _iterparse(source, events, parser)
|
||||
|
||||
|
||||
def fromstring(text, forbid_dtd=False, forbid_entities=True, forbid_external=True):
|
||||
parser = DefusedXMLParser(
|
||||
target=_TreeBuilder(),
|
||||
forbid_dtd=forbid_dtd,
|
||||
forbid_entities=forbid_entities,
|
||||
forbid_external=forbid_external,
|
||||
)
|
||||
parser.feed(text)
|
||||
return parser.close()
|
||||
|
||||
|
||||
XML = fromstring
|
||||
|
||||
|
||||
def fromstringlist(sequence, forbid_dtd=False, forbid_entities=True, forbid_external=True):
|
||||
parser = DefusedXMLParser(
|
||||
target=_TreeBuilder(),
|
||||
forbid_dtd=forbid_dtd,
|
||||
forbid_entities=forbid_entities,
|
||||
forbid_external=forbid_external,
|
||||
)
|
||||
for text in sequence:
|
||||
parser.feed(text)
|
||||
return parser.close()
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ParseError",
|
||||
"XML",
|
||||
"XMLParse",
|
||||
"XMLParser",
|
||||
"XMLTreeBuilder",
|
||||
"fromstring",
|
||||
"fromstringlist",
|
||||
"iterparse",
|
||||
"parse",
|
||||
"tostring",
|
||||
]
|
67
resources/lib/defusedxml/__init__.py
Normal file
67
resources/lib/defusedxml/__init__.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
# defusedxml
|
||||
#
|
||||
# Copyright (c) 2013 by Christian Heimes <christian@python.org>
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See https://www.python.org/psf/license for licensing details.
|
||||
"""Defuse XML bomb denial of service vulnerabilities
|
||||
"""
|
||||
from __future__ import print_function, absolute_import
|
||||
|
||||
import warnings
|
||||
|
||||
from .common import (
|
||||
DefusedXmlException,
|
||||
DTDForbidden,
|
||||
EntitiesForbidden,
|
||||
ExternalReferenceForbidden,
|
||||
NotSupportedError,
|
||||
_apply_defusing,
|
||||
)
|
||||
|
||||
|
||||
def defuse_stdlib():
|
||||
"""Monkey patch and defuse all stdlib packages
|
||||
|
||||
:warning: The monkey patch is an EXPERIMETNAL feature.
|
||||
"""
|
||||
defused = {}
|
||||
|
||||
with warnings.catch_warnings():
|
||||
from . import cElementTree
|
||||
from . import ElementTree
|
||||
from . import minidom
|
||||
from . import pulldom
|
||||
from . import sax
|
||||
from . import expatbuilder
|
||||
from . import expatreader
|
||||
from . import xmlrpc
|
||||
|
||||
xmlrpc.monkey_patch()
|
||||
defused[xmlrpc] = None
|
||||
|
||||
defused_mods = [
|
||||
cElementTree,
|
||||
ElementTree,
|
||||
minidom,
|
||||
pulldom,
|
||||
sax,
|
||||
expatbuilder,
|
||||
expatreader,
|
||||
]
|
||||
|
||||
for defused_mod in defused_mods:
|
||||
stdlib_mod = _apply_defusing(defused_mod)
|
||||
defused[defused_mod] = stdlib_mod
|
||||
|
||||
return defused
|
||||
|
||||
|
||||
__version__ = "0.8.0.dev1"
|
||||
|
||||
__all__ = [
|
||||
"DefusedXmlException",
|
||||
"DTDForbidden",
|
||||
"EntitiesForbidden",
|
||||
"ExternalReferenceForbidden",
|
||||
"NotSupportedError",
|
||||
]
|
47
resources/lib/defusedxml/cElementTree.py
Normal file
47
resources/lib/defusedxml/cElementTree.py
Normal file
|
@ -0,0 +1,47 @@
|
|||
# defusedxml
|
||||
#
|
||||
# Copyright (c) 2013 by Christian Heimes <christian@python.org>
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See https://www.python.org/psf/license for licensing details.
|
||||
"""Defused xml.etree.cElementTree
|
||||
"""
|
||||
import warnings
|
||||
|
||||
# This module is an alias for ElementTree just like xml.etree.cElementTree
|
||||
from .ElementTree import (
|
||||
XML,
|
||||
XMLParse,
|
||||
XMLParser,
|
||||
XMLTreeBuilder,
|
||||
fromstring,
|
||||
fromstringlist,
|
||||
iterparse,
|
||||
parse,
|
||||
tostring,
|
||||
DefusedXMLParser,
|
||||
ParseError,
|
||||
)
|
||||
|
||||
__origin__ = "xml.etree.cElementTree"
|
||||
|
||||
|
||||
warnings.warn(
|
||||
"defusedxml.cElementTree is deprecated, import from defusedxml.ElementTree instead.",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ParseError",
|
||||
"XML",
|
||||
"XMLParse",
|
||||
"XMLParser",
|
||||
"XMLTreeBuilder",
|
||||
"fromstring",
|
||||
"fromstringlist",
|
||||
"iterparse",
|
||||
"parse",
|
||||
"tostring",
|
||||
# backwards compatibility
|
||||
"DefusedXMLParser",
|
||||
]
|
85
resources/lib/defusedxml/common.py
Normal file
85
resources/lib/defusedxml/common.py
Normal file
|
@ -0,0 +1,85 @@
|
|||
# defusedxml
|
||||
#
|
||||
# Copyright (c) 2013-2020 by Christian Heimes <christian@python.org>
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See https://www.python.org/psf/license for licensing details.
|
||||
"""Common constants, exceptions and helpe functions
|
||||
"""
|
||||
import sys
|
||||
import xml.parsers.expat
|
||||
|
||||
PY3 = True
|
||||
|
||||
# Fail early when pyexpat is not installed correctly
|
||||
if not hasattr(xml.parsers.expat, "ParserCreate"):
|
||||
raise ImportError("pyexpat") # pragma: no cover
|
||||
|
||||
|
||||
class DefusedXmlException(ValueError):
|
||||
"""Base exception"""
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
|
||||
class DTDForbidden(DefusedXmlException):
|
||||
"""Document type definition is forbidden"""
|
||||
|
||||
def __init__(self, name, sysid, pubid):
|
||||
super().__init__()
|
||||
self.name = name
|
||||
self.sysid = sysid
|
||||
self.pubid = pubid
|
||||
|
||||
def __str__(self):
|
||||
tpl = "DTDForbidden(name='{}', system_id={!r}, public_id={!r})"
|
||||
return tpl.format(self.name, self.sysid, self.pubid)
|
||||
|
||||
|
||||
class EntitiesForbidden(DefusedXmlException):
|
||||
"""Entity definition is forbidden"""
|
||||
|
||||
def __init__(self, name, value, base, sysid, pubid, notation_name):
|
||||
super().__init__()
|
||||
self.name = name
|
||||
self.value = value
|
||||
self.base = base
|
||||
self.sysid = sysid
|
||||
self.pubid = pubid
|
||||
self.notation_name = notation_name
|
||||
|
||||
def __str__(self):
|
||||
tpl = "EntitiesForbidden(name='{}', system_id={!r}, public_id={!r})"
|
||||
return tpl.format(self.name, self.sysid, self.pubid)
|
||||
|
||||
|
||||
class ExternalReferenceForbidden(DefusedXmlException):
|
||||
"""Resolving an external reference is forbidden"""
|
||||
|
||||
def __init__(self, context, base, sysid, pubid):
|
||||
super().__init__()
|
||||
self.context = context
|
||||
self.base = base
|
||||
self.sysid = sysid
|
||||
self.pubid = pubid
|
||||
|
||||
def __str__(self):
|
||||
tpl = "ExternalReferenceForbidden(system_id='{}', public_id={})"
|
||||
return tpl.format(self.sysid, self.pubid)
|
||||
|
||||
|
||||
class NotSupportedError(DefusedXmlException):
|
||||
"""The operation is not supported"""
|
||||
|
||||
|
||||
def _apply_defusing(defused_mod):
|
||||
assert defused_mod is sys.modules[defused_mod.__name__]
|
||||
stdlib_name = defused_mod.__origin__
|
||||
__import__(stdlib_name, {}, {}, ["*"])
|
||||
stdlib_mod = sys.modules[stdlib_name]
|
||||
stdlib_names = set(dir(stdlib_mod))
|
||||
for name, obj in vars(defused_mod).items():
|
||||
if name.startswith("_") or name not in stdlib_names:
|
||||
continue
|
||||
setattr(stdlib_mod, name, obj)
|
||||
return stdlib_mod
|
107
resources/lib/defusedxml/expatbuilder.py
Normal file
107
resources/lib/defusedxml/expatbuilder.py
Normal file
|
@ -0,0 +1,107 @@
|
|||
# defusedxml
|
||||
#
|
||||
# Copyright (c) 2013 by Christian Heimes <christian@python.org>
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See https://www.python.org/psf/license for licensing details.
|
||||
"""Defused xml.dom.expatbuilder
|
||||
"""
|
||||
from __future__ import print_function, absolute_import
|
||||
|
||||
from xml.dom.expatbuilder import ExpatBuilder as _ExpatBuilder
|
||||
from xml.dom.expatbuilder import Namespaces as _Namespaces
|
||||
|
||||
from .common import DTDForbidden, EntitiesForbidden, ExternalReferenceForbidden
|
||||
|
||||
__origin__ = "xml.dom.expatbuilder"
|
||||
|
||||
|
||||
class DefusedExpatBuilder(_ExpatBuilder):
|
||||
"""Defused document builder"""
|
||||
|
||||
def __init__(
|
||||
self, options=None, forbid_dtd=False, forbid_entities=True, forbid_external=True
|
||||
):
|
||||
_ExpatBuilder.__init__(self, options)
|
||||
self.forbid_dtd = forbid_dtd
|
||||
self.forbid_entities = forbid_entities
|
||||
self.forbid_external = forbid_external
|
||||
|
||||
def defused_start_doctype_decl(self, name, sysid, pubid, has_internal_subset):
|
||||
raise DTDForbidden(name, sysid, pubid)
|
||||
|
||||
def defused_entity_decl(
|
||||
self, name, is_parameter_entity, value, base, sysid, pubid, notation_name
|
||||
):
|
||||
raise EntitiesForbidden(name, value, base, sysid, pubid, notation_name)
|
||||
|
||||
def defused_unparsed_entity_decl(self, name, base, sysid, pubid, notation_name):
|
||||
# expat 1.2
|
||||
raise EntitiesForbidden(name, None, base, sysid, pubid, notation_name) # pragma: no cover
|
||||
|
||||
def defused_external_entity_ref_handler(self, context, base, sysid, pubid):
|
||||
raise ExternalReferenceForbidden(context, base, sysid, pubid)
|
||||
|
||||
def install(self, parser):
|
||||
_ExpatBuilder.install(self, parser)
|
||||
|
||||
if self.forbid_dtd:
|
||||
parser.StartDoctypeDeclHandler = self.defused_start_doctype_decl
|
||||
if self.forbid_entities:
|
||||
# if self._options.entities:
|
||||
parser.EntityDeclHandler = self.defused_entity_decl
|
||||
parser.UnparsedEntityDeclHandler = self.defused_unparsed_entity_decl
|
||||
if self.forbid_external:
|
||||
parser.ExternalEntityRefHandler = self.defused_external_entity_ref_handler
|
||||
|
||||
|
||||
class DefusedExpatBuilderNS(_Namespaces, DefusedExpatBuilder):
|
||||
"""Defused document builder that supports namespaces."""
|
||||
|
||||
def install(self, parser):
|
||||
DefusedExpatBuilder.install(self, parser)
|
||||
if self._options.namespace_declarations:
|
||||
parser.StartNamespaceDeclHandler = self.start_namespace_decl_handler
|
||||
|
||||
def reset(self):
|
||||
DefusedExpatBuilder.reset(self)
|
||||
self._initNamespaces()
|
||||
|
||||
|
||||
def parse(file, namespaces=True, forbid_dtd=False, forbid_entities=True, forbid_external=True):
|
||||
"""Parse a document, returning the resulting Document node.
|
||||
|
||||
'file' may be either a file name or an open file object.
|
||||
"""
|
||||
if namespaces:
|
||||
build_builder = DefusedExpatBuilderNS
|
||||
else:
|
||||
build_builder = DefusedExpatBuilder
|
||||
builder = build_builder(
|
||||
forbid_dtd=forbid_dtd, forbid_entities=forbid_entities, forbid_external=forbid_external
|
||||
)
|
||||
|
||||
if isinstance(file, str):
|
||||
fp = open(file, "rb")
|
||||
try:
|
||||
result = builder.parseFile(fp)
|
||||
finally:
|
||||
fp.close()
|
||||
else:
|
||||
result = builder.parseFile(file)
|
||||
return result
|
||||
|
||||
|
||||
def parseString(
|
||||
string, namespaces=True, forbid_dtd=False, forbid_entities=True, forbid_external=True
|
||||
):
|
||||
"""Parse a document from a string, returning the resulting
|
||||
Document node.
|
||||
"""
|
||||
if namespaces:
|
||||
build_builder = DefusedExpatBuilderNS
|
||||
else:
|
||||
build_builder = DefusedExpatBuilder
|
||||
builder = build_builder(
|
||||
forbid_dtd=forbid_dtd, forbid_entities=forbid_entities, forbid_external=forbid_external
|
||||
)
|
||||
return builder.parseString(string)
|
61
resources/lib/defusedxml/expatreader.py
Normal file
61
resources/lib/defusedxml/expatreader.py
Normal file
|
@ -0,0 +1,61 @@
|
|||
# defusedxml
|
||||
#
|
||||
# Copyright (c) 2013 by Christian Heimes <christian@python.org>
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See https://www.python.org/psf/license for licensing details.
|
||||
"""Defused xml.sax.expatreader
|
||||
"""
|
||||
from __future__ import print_function, absolute_import
|
||||
|
||||
from xml.sax.expatreader import ExpatParser as _ExpatParser
|
||||
|
||||
from .common import DTDForbidden, EntitiesForbidden, ExternalReferenceForbidden
|
||||
|
||||
__origin__ = "xml.sax.expatreader"
|
||||
|
||||
|
||||
class DefusedExpatParser(_ExpatParser):
|
||||
"""Defused SAX driver for the pyexpat C module."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
namespaceHandling=0,
|
||||
bufsize=2 ** 16 - 20,
|
||||
forbid_dtd=False,
|
||||
forbid_entities=True,
|
||||
forbid_external=True,
|
||||
):
|
||||
super().__init__(namespaceHandling, bufsize)
|
||||
self.forbid_dtd = forbid_dtd
|
||||
self.forbid_entities = forbid_entities
|
||||
self.forbid_external = forbid_external
|
||||
|
||||
def defused_start_doctype_decl(self, name, sysid, pubid, has_internal_subset):
|
||||
raise DTDForbidden(name, sysid, pubid)
|
||||
|
||||
def defused_entity_decl(
|
||||
self, name, is_parameter_entity, value, base, sysid, pubid, notation_name
|
||||
):
|
||||
raise EntitiesForbidden(name, value, base, sysid, pubid, notation_name)
|
||||
|
||||
def defused_unparsed_entity_decl(self, name, base, sysid, pubid, notation_name):
|
||||
# expat 1.2
|
||||
raise EntitiesForbidden(name, None, base, sysid, pubid, notation_name) # pragma: no cover
|
||||
|
||||
def defused_external_entity_ref_handler(self, context, base, sysid, pubid):
|
||||
raise ExternalReferenceForbidden(context, base, sysid, pubid)
|
||||
|
||||
def reset(self):
|
||||
super().reset()
|
||||
parser = self._parser
|
||||
if self.forbid_dtd:
|
||||
parser.StartDoctypeDeclHandler = self.defused_start_doctype_decl
|
||||
if self.forbid_entities:
|
||||
parser.EntityDeclHandler = self.defused_entity_decl
|
||||
parser.UnparsedEntityDeclHandler = self.defused_unparsed_entity_decl
|
||||
if self.forbid_external:
|
||||
parser.ExternalEntityRefHandler = self.defused_external_entity_ref_handler
|
||||
|
||||
|
||||
def create_parser(*args, **kwargs):
|
||||
return DefusedExpatParser(*args, **kwargs)
|
153
resources/lib/defusedxml/lxml.py
Normal file
153
resources/lib/defusedxml/lxml.py
Normal file
|
@ -0,0 +1,153 @@
|
|||
# defusedxml
|
||||
#
|
||||
# Copyright (c) 2013 by Christian Heimes <christian@python.org>
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See https://www.python.org/psf/license for licensing details.
|
||||
"""DEPRECATED Example code for lxml.etree protection
|
||||
|
||||
The code has NO protection against decompression bombs.
|
||||
"""
|
||||
from __future__ import print_function, absolute_import
|
||||
|
||||
import threading
|
||||
import warnings
|
||||
|
||||
from lxml import etree as _etree
|
||||
|
||||
from .common import DTDForbidden, EntitiesForbidden, NotSupportedError
|
||||
|
||||
LXML3 = _etree.LXML_VERSION[0] >= 3
|
||||
|
||||
__origin__ = "lxml.etree"
|
||||
|
||||
tostring = _etree.tostring
|
||||
|
||||
|
||||
warnings.warn(
|
||||
"defusedxml.lxml is no longer supported and will be removed in a future release.",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
|
||||
class RestrictedElement(_etree.ElementBase):
|
||||
"""A restricted Element class that filters out instances of some classes"""
|
||||
|
||||
__slots__ = ()
|
||||
# blacklist = (etree._Entity, etree._ProcessingInstruction, etree._Comment)
|
||||
blacklist = _etree._Entity
|
||||
|
||||
def _filter(self, iterator):
|
||||
blacklist = self.blacklist
|
||||
for child in iterator:
|
||||
if isinstance(child, blacklist):
|
||||
continue
|
||||
yield child
|
||||
|
||||
def __iter__(self):
|
||||
iterator = super(RestrictedElement, self).__iter__()
|
||||
return self._filter(iterator)
|
||||
|
||||
def iterchildren(self, tag=None, reversed=False):
|
||||
iterator = super(RestrictedElement, self).iterchildren(tag=tag, reversed=reversed)
|
||||
return self._filter(iterator)
|
||||
|
||||
def iter(self, tag=None, *tags):
|
||||
iterator = super(RestrictedElement, self).iter(tag=tag, *tags)
|
||||
return self._filter(iterator)
|
||||
|
||||
def iterdescendants(self, tag=None, *tags):
|
||||
iterator = super(RestrictedElement, self).iterdescendants(tag=tag, *tags)
|
||||
return self._filter(iterator)
|
||||
|
||||
def itersiblings(self, tag=None, preceding=False):
|
||||
iterator = super(RestrictedElement, self).itersiblings(tag=tag, preceding=preceding)
|
||||
return self._filter(iterator)
|
||||
|
||||
def getchildren(self):
|
||||
iterator = super(RestrictedElement, self).__iter__()
|
||||
return list(self._filter(iterator))
|
||||
|
||||
def getiterator(self, tag=None):
|
||||
iterator = super(RestrictedElement, self).getiterator(tag)
|
||||
return self._filter(iterator)
|
||||
|
||||
|
||||
class GlobalParserTLS(threading.local):
|
||||
"""Thread local context for custom parser instances"""
|
||||
|
||||
parser_config = {
|
||||
"resolve_entities": False,
|
||||
# 'remove_comments': True,
|
||||
# 'remove_pis': True,
|
||||
}
|
||||
|
||||
element_class = RestrictedElement
|
||||
|
||||
def createDefaultParser(self):
|
||||
parser = _etree.XMLParser(**self.parser_config)
|
||||
element_class = self.element_class
|
||||
if self.element_class is not None:
|
||||
lookup = _etree.ElementDefaultClassLookup(element=element_class)
|
||||
parser.set_element_class_lookup(lookup)
|
||||
return parser
|
||||
|
||||
def setDefaultParser(self, parser):
|
||||
self._default_parser = parser
|
||||
|
||||
def getDefaultParser(self):
|
||||
parser = getattr(self, "_default_parser", None)
|
||||
if parser is None:
|
||||
parser = self.createDefaultParser()
|
||||
self.setDefaultParser(parser)
|
||||
return parser
|
||||
|
||||
|
||||
_parser_tls = GlobalParserTLS()
|
||||
getDefaultParser = _parser_tls.getDefaultParser
|
||||
|
||||
|
||||
def check_docinfo(elementtree, forbid_dtd=False, forbid_entities=True):
|
||||
"""Check docinfo of an element tree for DTD and entity declarations
|
||||
|
||||
The check for entity declarations needs lxml 3 or newer. lxml 2.x does
|
||||
not support dtd.iterentities().
|
||||
"""
|
||||
docinfo = elementtree.docinfo
|
||||
if docinfo.doctype:
|
||||
if forbid_dtd:
|
||||
raise DTDForbidden(docinfo.doctype, docinfo.system_url, docinfo.public_id)
|
||||
if forbid_entities and not LXML3:
|
||||
# lxml < 3 has no iterentities()
|
||||
raise NotSupportedError("Unable to check for entity declarations " "in lxml 2.x")
|
||||
|
||||
if forbid_entities:
|
||||
for dtd in docinfo.internalDTD, docinfo.externalDTD:
|
||||
if dtd is None:
|
||||
continue
|
||||
for entity in dtd.iterentities():
|
||||
raise EntitiesForbidden(entity.name, entity.content, None, None, None, None)
|
||||
|
||||
|
||||
def parse(source, parser=None, base_url=None, forbid_dtd=False, forbid_entities=True):
|
||||
if parser is None:
|
||||
parser = getDefaultParser()
|
||||
elementtree = _etree.parse(source, parser, base_url=base_url)
|
||||
check_docinfo(elementtree, forbid_dtd, forbid_entities)
|
||||
return elementtree
|
||||
|
||||
|
||||
def fromstring(text, parser=None, base_url=None, forbid_dtd=False, forbid_entities=True):
|
||||
if parser is None:
|
||||
parser = getDefaultParser()
|
||||
rootelement = _etree.fromstring(text, parser, base_url=base_url)
|
||||
elementtree = rootelement.getroottree()
|
||||
check_docinfo(elementtree, forbid_dtd, forbid_entities)
|
||||
return rootelement
|
||||
|
||||
|
||||
XML = fromstring
|
||||
|
||||
|
||||
def iterparse(*args, **kwargs):
|
||||
raise NotSupportedError("defused lxml.etree.iterparse not available")
|
63
resources/lib/defusedxml/minidom.py
Normal file
63
resources/lib/defusedxml/minidom.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
# defusedxml
|
||||
#
|
||||
# Copyright (c) 2013 by Christian Heimes <christian@python.org>
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See https://www.python.org/psf/license for licensing details.
|
||||
"""Defused xml.dom.minidom
|
||||
"""
|
||||
from __future__ import print_function, absolute_import
|
||||
|
||||
from xml.dom.minidom import _do_pulldom_parse
|
||||
from . import expatbuilder as _expatbuilder
|
||||
from . import pulldom as _pulldom
|
||||
|
||||
__origin__ = "xml.dom.minidom"
|
||||
|
||||
|
||||
def parse(
|
||||
file, parser=None, bufsize=None, forbid_dtd=False, forbid_entities=True, forbid_external=True
|
||||
):
|
||||
"""Parse a file into a DOM by filename or file object."""
|
||||
if parser is None and not bufsize:
|
||||
return _expatbuilder.parse(
|
||||
file,
|
||||
forbid_dtd=forbid_dtd,
|
||||
forbid_entities=forbid_entities,
|
||||
forbid_external=forbid_external,
|
||||
)
|
||||
else:
|
||||
return _do_pulldom_parse(
|
||||
_pulldom.parse,
|
||||
(file,),
|
||||
{
|
||||
"parser": parser,
|
||||
"bufsize": bufsize,
|
||||
"forbid_dtd": forbid_dtd,
|
||||
"forbid_entities": forbid_entities,
|
||||
"forbid_external": forbid_external,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def parseString(
|
||||
string, parser=None, forbid_dtd=False, forbid_entities=True, forbid_external=True
|
||||
):
|
||||
"""Parse a file into a DOM from a string."""
|
||||
if parser is None:
|
||||
return _expatbuilder.parseString(
|
||||
string,
|
||||
forbid_dtd=forbid_dtd,
|
||||
forbid_entities=forbid_entities,
|
||||
forbid_external=forbid_external,
|
||||
)
|
||||
else:
|
||||
return _do_pulldom_parse(
|
||||
_pulldom.parseString,
|
||||
(string,),
|
||||
{
|
||||
"parser": parser,
|
||||
"forbid_dtd": forbid_dtd,
|
||||
"forbid_entities": forbid_entities,
|
||||
"forbid_external": forbid_external,
|
||||
},
|
||||
)
|
41
resources/lib/defusedxml/pulldom.py
Normal file
41
resources/lib/defusedxml/pulldom.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
# defusedxml
|
||||
#
|
||||
# Copyright (c) 2013 by Christian Heimes <christian@python.org>
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See https://www.python.org/psf/license for licensing details.
|
||||
"""Defused xml.dom.pulldom
|
||||
"""
|
||||
from __future__ import print_function, absolute_import
|
||||
|
||||
from xml.dom.pulldom import parse as _parse
|
||||
from xml.dom.pulldom import parseString as _parseString
|
||||
from .sax import make_parser
|
||||
|
||||
__origin__ = "xml.dom.pulldom"
|
||||
|
||||
|
||||
def parse(
|
||||
stream_or_string,
|
||||
parser=None,
|
||||
bufsize=None,
|
||||
forbid_dtd=False,
|
||||
forbid_entities=True,
|
||||
forbid_external=True,
|
||||
):
|
||||
if parser is None:
|
||||
parser = make_parser()
|
||||
parser.forbid_dtd = forbid_dtd
|
||||
parser.forbid_entities = forbid_entities
|
||||
parser.forbid_external = forbid_external
|
||||
return _parse(stream_or_string, parser, bufsize)
|
||||
|
||||
|
||||
def parseString(
|
||||
string, parser=None, forbid_dtd=False, forbid_entities=True, forbid_external=True
|
||||
):
|
||||
if parser is None:
|
||||
parser = make_parser()
|
||||
parser.forbid_dtd = forbid_dtd
|
||||
parser.forbid_entities = forbid_entities
|
||||
parser.forbid_external = forbid_external
|
||||
return _parseString(string, parser)
|
60
resources/lib/defusedxml/sax.py
Normal file
60
resources/lib/defusedxml/sax.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
# defusedxml
|
||||
#
|
||||
# Copyright (c) 2013 by Christian Heimes <christian@python.org>
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See https://www.python.org/psf/license for licensing details.
|
||||
"""Defused xml.sax
|
||||
"""
|
||||
from __future__ import print_function, absolute_import
|
||||
|
||||
from xml.sax import InputSource as _InputSource
|
||||
from xml.sax import ErrorHandler as _ErrorHandler
|
||||
|
||||
from . import expatreader
|
||||
|
||||
__origin__ = "xml.sax"
|
||||
|
||||
|
||||
def parse(
|
||||
source,
|
||||
handler,
|
||||
errorHandler=_ErrorHandler(),
|
||||
forbid_dtd=False,
|
||||
forbid_entities=True,
|
||||
forbid_external=True,
|
||||
):
|
||||
parser = make_parser()
|
||||
parser.setContentHandler(handler)
|
||||
parser.setErrorHandler(errorHandler)
|
||||
parser.forbid_dtd = forbid_dtd
|
||||
parser.forbid_entities = forbid_entities
|
||||
parser.forbid_external = forbid_external
|
||||
parser.parse(source)
|
||||
|
||||
|
||||
def parseString(
|
||||
string,
|
||||
handler,
|
||||
errorHandler=_ErrorHandler(),
|
||||
forbid_dtd=False,
|
||||
forbid_entities=True,
|
||||
forbid_external=True,
|
||||
):
|
||||
from io import BytesIO
|
||||
|
||||
if errorHandler is None:
|
||||
errorHandler = _ErrorHandler()
|
||||
parser = make_parser()
|
||||
parser.setContentHandler(handler)
|
||||
parser.setErrorHandler(errorHandler)
|
||||
parser.forbid_dtd = forbid_dtd
|
||||
parser.forbid_entities = forbid_entities
|
||||
parser.forbid_external = forbid_external
|
||||
|
||||
inpsrc = _InputSource()
|
||||
inpsrc.setByteStream(BytesIO(string))
|
||||
parser.parse(inpsrc)
|
||||
|
||||
|
||||
def make_parser(parser_list=[]):
|
||||
return expatreader.create_parser()
|
144
resources/lib/defusedxml/xmlrpc.py
Normal file
144
resources/lib/defusedxml/xmlrpc.py
Normal file
|
@ -0,0 +1,144 @@
|
|||
# defusedxml
|
||||
#
|
||||
# Copyright (c) 2013 by Christian Heimes <christian@python.org>
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See https://www.python.org/psf/license for licensing details.
|
||||
"""Defused xmlrpclib
|
||||
|
||||
Also defuses gzip bomb
|
||||
"""
|
||||
from __future__ import print_function, absolute_import
|
||||
|
||||
import io
|
||||
|
||||
from .common import DTDForbidden, EntitiesForbidden, ExternalReferenceForbidden
|
||||
|
||||
__origin__ = "xmlrpc.client"
|
||||
from xmlrpc.client import ExpatParser
|
||||
from xmlrpc import client as xmlrpc_client
|
||||
from xmlrpc import server as xmlrpc_server
|
||||
from xmlrpc.client import gzip_decode as _orig_gzip_decode
|
||||
from xmlrpc.client import GzipDecodedResponse as _OrigGzipDecodedResponse
|
||||
|
||||
try:
|
||||
import gzip
|
||||
except ImportError: # pragma: no cover
|
||||
gzip = None
|
||||
|
||||
|
||||
# Limit maximum request size to prevent resource exhaustion DoS
|
||||
# Also used to limit maximum amount of gzip decoded data in order to prevent
|
||||
# decompression bombs
|
||||
# A value of -1 or smaller disables the limit
|
||||
MAX_DATA = 30 * 1024 * 1024 # 30 MB
|
||||
|
||||
|
||||
def defused_gzip_decode(data, limit=None):
|
||||
"""gzip encoded data -> unencoded data
|
||||
|
||||
Decode data using the gzip content encoding as described in RFC 1952
|
||||
"""
|
||||
if not gzip: # pragma: no cover
|
||||
raise NotImplementedError
|
||||
if limit is None:
|
||||
limit = MAX_DATA
|
||||
f = io.BytesIO(data)
|
||||
gzf = gzip.GzipFile(mode="rb", fileobj=f)
|
||||
try:
|
||||
if limit < 0: # no limit
|
||||
decoded = gzf.read()
|
||||
else:
|
||||
decoded = gzf.read(limit + 1)
|
||||
except IOError: # pragma: no cover
|
||||
raise ValueError("invalid data")
|
||||
f.close()
|
||||
gzf.close()
|
||||
if limit >= 0 and len(decoded) > limit:
|
||||
raise ValueError("max gzipped payload length exceeded")
|
||||
return decoded
|
||||
|
||||
|
||||
class DefusedGzipDecodedResponse(gzip.GzipFile if gzip else object):
|
||||
"""a file-like object to decode a response encoded with the gzip
|
||||
method, as described in RFC 1952.
|
||||
"""
|
||||
|
||||
def __init__(self, response, limit=None):
|
||||
# response doesn't support tell() and read(), required by
|
||||
# GzipFile
|
||||
if not gzip: # pragma: no cover
|
||||
raise NotImplementedError
|
||||
self.limit = limit = limit if limit is not None else MAX_DATA
|
||||
if limit < 0: # no limit
|
||||
data = response.read()
|
||||
self.readlength = None
|
||||
else:
|
||||
data = response.read(limit + 1)
|
||||
self.readlength = 0
|
||||
if limit >= 0 and len(data) > limit:
|
||||
raise ValueError("max payload length exceeded")
|
||||
self.stringio = io.BytesIO(data)
|
||||
super().__init__(mode="rb", fileobj=self.stringio)
|
||||
|
||||
def read(self, n):
|
||||
if self.limit >= 0:
|
||||
left = self.limit - self.readlength
|
||||
n = min(n, left + 1)
|
||||
data = gzip.GzipFile.read(self, n)
|
||||
self.readlength += len(data)
|
||||
if self.readlength > self.limit:
|
||||
raise ValueError("max payload length exceeded")
|
||||
return data
|
||||
else:
|
||||
return super().read(n)
|
||||
|
||||
def close(self):
|
||||
super().close()
|
||||
self.stringio.close()
|
||||
|
||||
|
||||
class DefusedExpatParser(ExpatParser):
|
||||
def __init__(self, target, forbid_dtd=False, forbid_entities=True, forbid_external=True):
|
||||
super().__init__(target)
|
||||
self.forbid_dtd = forbid_dtd
|
||||
self.forbid_entities = forbid_entities
|
||||
self.forbid_external = forbid_external
|
||||
parser = self._parser
|
||||
if self.forbid_dtd:
|
||||
parser.StartDoctypeDeclHandler = self.defused_start_doctype_decl
|
||||
if self.forbid_entities:
|
||||
parser.EntityDeclHandler = self.defused_entity_decl
|
||||
parser.UnparsedEntityDeclHandler = self.defused_unparsed_entity_decl
|
||||
if self.forbid_external:
|
||||
parser.ExternalEntityRefHandler = self.defused_external_entity_ref_handler
|
||||
|
||||
def defused_start_doctype_decl(self, name, sysid, pubid, has_internal_subset):
|
||||
raise DTDForbidden(name, sysid, pubid)
|
||||
|
||||
def defused_entity_decl(
|
||||
self, name, is_parameter_entity, value, base, sysid, pubid, notation_name
|
||||
):
|
||||
raise EntitiesForbidden(name, value, base, sysid, pubid, notation_name)
|
||||
|
||||
def defused_unparsed_entity_decl(self, name, base, sysid, pubid, notation_name):
|
||||
# expat 1.2
|
||||
raise EntitiesForbidden(name, None, base, sysid, pubid, notation_name) # pragma: no cover
|
||||
|
||||
def defused_external_entity_ref_handler(self, context, base, sysid, pubid):
|
||||
raise ExternalReferenceForbidden(context, base, sysid, pubid)
|
||||
|
||||
|
||||
def monkey_patch():
|
||||
xmlrpc_client.FastParser = DefusedExpatParser
|
||||
xmlrpc_client.GzipDecodedResponse = DefusedGzipDecodedResponse
|
||||
xmlrpc_client.gzip_decode = defused_gzip_decode
|
||||
if xmlrpc_server:
|
||||
xmlrpc_server.gzip_decode = defused_gzip_decode
|
||||
|
||||
|
||||
def unmonkey_patch():
|
||||
xmlrpc_client.FastParser = None
|
||||
xmlrpc_client.GzipDecodedResponse = _OrigGzipDecodedResponse
|
||||
xmlrpc_client.gzip_decode = _orig_gzip_decode
|
||||
if xmlrpc_server:
|
||||
xmlrpc_server.gzip_decode = _orig_gzip_decode
|
|
@ -1,6 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import requests
|
||||
import requests.exceptions as exceptions
|
||||
|
@ -18,7 +17,7 @@ LOG = getLogger('PLEX.download')
|
|||
###############################################################################
|
||||
|
||||
|
||||
class DownloadUtils():
|
||||
class DownloadUtils(object):
|
||||
"""
|
||||
Manages any up/downloads with PKC. Careful to initiate correctly
|
||||
Use startSession() to initiate.
|
||||
|
@ -224,7 +223,11 @@ class DownloadUtils():
|
|||
if r.status_code != 401:
|
||||
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
|
||||
# But read (empty) content to release connection back to pool
|
||||
# (see requests: keep-alive documentation)
|
||||
|
@ -258,12 +261,9 @@ class DownloadUtils():
|
|||
elif r.status_code in (200, 201):
|
||||
# 200: OK
|
||||
# 201: Created
|
||||
if return_response is True:
|
||||
# return the entire response object
|
||||
return r
|
||||
try:
|
||||
# xml response
|
||||
r = utils.defused_etree.fromstring(r.content)
|
||||
r = utils.etree.fromstring(r.content)
|
||||
return r
|
||||
except Exception:
|
||||
r.encoding = 'utf-8'
|
||||
|
|
|
@ -4,9 +4,10 @@
|
|||
Loads of different functions called in SEPARATE Python instances through
|
||||
e.g. plugin://... calls. Hence be careful to only rely on window variables.
|
||||
"""
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import sys
|
||||
import copy
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
import xbmc
|
||||
import xbmcplugin
|
||||
|
@ -87,12 +88,10 @@ def directory_item(label, path, folder=True):
|
|||
Adds a xbmcplugin.addDirectoryItem() directory itemlistitem
|
||||
"""
|
||||
listitem = ListItem(label, path=path)
|
||||
listitem.setThumbnailImage(
|
||||
"special://home/addons/plugin.video.plexkodiconnect/icon.png")
|
||||
listitem.setArt(
|
||||
{"fanart": "special://home/addons/plugin.video.plexkodiconnect/fanart.jpg"})
|
||||
listitem.setArt(
|
||||
{"landscape":"special://home/addons/plugin.video.plexkodiconnect/fanart.jpg"})
|
||||
{'landscape':'special://home/addons/plugin.video.plexkodiconnect/fanart.jpg',
|
||||
'fanart': 'special://home/addons/plugin.video.plexkodiconnect/fanart.jpg',
|
||||
'thumb': 'special://home/addons/plugin.video.plexkodiconnect/icon.png'})
|
||||
xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]),
|
||||
url=path,
|
||||
listitem=listitem,
|
||||
|
@ -248,12 +247,10 @@ def show_listing(xml, plex_type=None, section_id=None, synched=True, key=None):
|
|||
widgets.KEY = key
|
||||
# Process all items to show
|
||||
all_items = mass_api(xml)
|
||||
all_items = utils.process_method_on_list(widgets.generate_item, all_items)
|
||||
all_items = utils.process_method_on_list(widgets.prepare_listitem,
|
||||
all_items)
|
||||
all_items = [widgets.generate_item(api) for api in all_items]
|
||||
all_items = [widgets.prepare_listitem(item) for item in all_items]
|
||||
# fill that listing...
|
||||
all_items = utils.process_method_on_list(widgets.create_listitem,
|
||||
all_items)
|
||||
all_items = [widgets.create_listitem(item) for item in all_items]
|
||||
xbmcplugin.addDirectoryItems(int(sys.argv[1]), all_items, len(all_items))
|
||||
# end directory listing
|
||||
xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_UNSORTED)
|
||||
|
@ -287,7 +284,7 @@ def get_video_files(plex_id, params):
|
|||
app.init(entrypoint=True)
|
||||
item = PF.GetPlexMetadata(plex_id)
|
||||
try:
|
||||
path = utils.try_decode(item[0][0][0].attrib['file'])
|
||||
path = item[0][0][0].attrib['file']
|
||||
except (TypeError, IndexError, AttributeError, KeyError):
|
||||
LOG.error('Could not get file path for item %s', plex_id)
|
||||
return xbmcplugin.endOfDirectory(int(sys.argv[1]))
|
||||
|
@ -303,15 +300,14 @@ def get_video_files(plex_id, params):
|
|||
if path_ops.exists(path):
|
||||
for root, dirs, files in path_ops.walk(path):
|
||||
for directory in dirs:
|
||||
item_path = utils.try_encode(path_ops.path.join(root,
|
||||
directory))
|
||||
item_path = path_ops.path.join(root, directory)
|
||||
listitem = ListItem(item_path, path=item_path)
|
||||
xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]),
|
||||
url=item_path,
|
||||
listitem=listitem,
|
||||
isFolder=True)
|
||||
for file in files:
|
||||
item_path = utils.try_encode(path_ops.path.join(root, file))
|
||||
item_path = path_ops.path.join(root, file)
|
||||
listitem = ListItem(item_path, path=item_path)
|
||||
xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]),
|
||||
url=file,
|
||||
|
@ -356,23 +352,20 @@ def extra_fanart(plex_id, plex_path):
|
|||
backdrops = api.artwork()['Backdrop']
|
||||
for count, backdrop in enumerate(backdrops):
|
||||
# Same ordering as in artwork
|
||||
art_file = utils.try_encode(path_ops.path.join(
|
||||
fanart_dir, "fanart%.3d.jpg" % count))
|
||||
art_file = path_ops.path.join(fanart_dir, "fanart%.3d.jpg" % count)
|
||||
listitem = ListItem("%.3d" % count, path=art_file)
|
||||
xbmcplugin.addDirectoryItem(
|
||||
handle=int(sys.argv[1]),
|
||||
url=art_file,
|
||||
listitem=listitem)
|
||||
path_ops.copyfile(backdrop, utils.try_decode(art_file))
|
||||
path_ops.copyfile(backdrop, art_file)
|
||||
else:
|
||||
LOG.info("Found cached backdrop.")
|
||||
# Use existing cached images
|
||||
fanart_dir = utils.try_decode(fanart_dir)
|
||||
fanart_dir = fanart_dir
|
||||
for root, _, files in path_ops.walk(fanart_dir):
|
||||
root = utils.decode_path(root)
|
||||
for file in files:
|
||||
file = utils.decode_path(file)
|
||||
art_file = utils.try_encode(path_ops.path.join(root, file))
|
||||
art_file = path_ops.path.join(root, file)
|
||||
listitem = ListItem(file, path=art_file)
|
||||
xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]),
|
||||
url=art_file,
|
||||
|
@ -423,6 +416,7 @@ def hub(content_type):
|
|||
# We need to make sure that only entries that WORK are displayed
|
||||
# WARNING: using xml.remove(child) in for-loop requires traversing from
|
||||
# the end!
|
||||
pkc_cont_watching = None
|
||||
for entry in reversed(xml):
|
||||
api = API(entry)
|
||||
append = False
|
||||
|
@ -439,6 +433,21 @@ def hub(content_type):
|
|||
append = True
|
||||
if not append:
|
||||
xml.remove(entry)
|
||||
|
||||
# HACK ##################
|
||||
# Merge Plex's "Continue watching" with "On deck"
|
||||
if entry.get('key') == '/hubs/home/continueWatching':
|
||||
pkc_cont_watching = copy.deepcopy(entry)
|
||||
pkc_cont_watching.set('key', '/hubs/continueWatching')
|
||||
title = pkc_cont_watching.get('title') or 'Continue Watching'
|
||||
pkc_cont_watching.set('title', 'PKC %s' % title)
|
||||
if pkc_cont_watching:
|
||||
for i, entry in enumerate(xml):
|
||||
if entry.get('key') == '/hubs/home/continueWatching':
|
||||
xml.insert(i + 1, pkc_cont_watching)
|
||||
break
|
||||
# END HACK ##################
|
||||
|
||||
show_listing(xml)
|
||||
|
||||
|
||||
|
@ -488,7 +497,7 @@ def browse_plex(key=None, plex_type=None, section_id=None, synched=True,
|
|||
if prompt is None:
|
||||
# User cancelled
|
||||
return
|
||||
prompt = prompt.strip().decode('utf-8')
|
||||
prompt = prompt.strip()
|
||||
args['query'] = prompt
|
||||
xml = DU().downloadUrl(utils.extend_url('{server}%s' % key, args))
|
||||
try:
|
||||
|
@ -499,7 +508,7 @@ def browse_plex(key=None, plex_type=None, section_id=None, synched=True,
|
|||
return
|
||||
if xml[0].tag == 'Hub':
|
||||
# E.g. when hitting the endpoint '/hubs/search'
|
||||
answ = utils.etree.Element(xml.tag, attrib=xml.attrib)
|
||||
answ = etree.Element(xml.tag, attrib=xml.attrib)
|
||||
for hub in xml:
|
||||
if not utils.cast(int, hub.get('size')):
|
||||
# Empty category
|
||||
|
|
31
resources/lib/exceptions.py
Normal file
31
resources/lib/exceptions.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
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
|
|
@ -1,12 +1,11 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from xbmc import executebuiltin
|
||||
|
||||
from . import utils
|
||||
from .utils import etree
|
||||
import xml.etree.ElementTree as etree
|
||||
from . import path_ops
|
||||
from . import migration
|
||||
from .downloadutils import DownloadUtils as DU, exceptions
|
||||
|
@ -212,8 +211,7 @@ class InitialSetup(object):
|
|||
not set before
|
||||
"""
|
||||
answer = True
|
||||
chk = PF.check_connection(app.CONN.server,
|
||||
verifySSL=True if v.KODIVERSION >= 18 else False)
|
||||
chk = PF.check_connection(app.CONN.server, verifySSL=True)
|
||||
if chk is False:
|
||||
LOG.warn('Could not reach PMS %s', app.CONN.server)
|
||||
answer = False
|
||||
|
@ -241,18 +239,13 @@ class InitialSetup(object):
|
|||
"""
|
||||
Checks for server's connectivity. Returns check_connection result
|
||||
"""
|
||||
if server['local']:
|
||||
# Deactive SSL verification if the server is local for Kodi 17
|
||||
verifySSL = True if v.KODIVERSION >= 18 else False
|
||||
else:
|
||||
verifySSL = True
|
||||
if not server['token']:
|
||||
# Plex GDM: we only get the token from plex.tv after
|
||||
# Sign-in to plex.tv
|
||||
server['token'] = utils.settings('plexToken') or None
|
||||
return PF.check_connection(server['baseURL'],
|
||||
token=server['token'],
|
||||
verifySSL=verifySSL)
|
||||
verifySSL=True)
|
||||
|
||||
def pick_pms(self, showDialog=False, inform_of_search=False):
|
||||
"""
|
||||
|
@ -287,7 +280,6 @@ class InitialSetup(object):
|
|||
}
|
||||
or None if unsuccessful
|
||||
"""
|
||||
server = None
|
||||
# If no server is set, let user choose one
|
||||
if not app.CONN.server or not app.CONN.machine_identifier:
|
||||
showDialog = True
|
||||
|
@ -411,7 +403,7 @@ class InitialSetup(object):
|
|||
utils.messageDialog(
|
||||
utils.lang(29999),
|
||||
'%s %s\n%s' % (utils.lang(39013),
|
||||
server['name'].decode('utf-8'),
|
||||
server['name'],
|
||||
utils.lang(39014)))
|
||||
if self.plex_tv_sign_in() is False:
|
||||
# Exit while loop if user cancels
|
||||
|
@ -435,7 +427,6 @@ class InitialSetup(object):
|
|||
utils.settings('plex_servername', server['name'])
|
||||
utils.settings('plex_serverowned',
|
||||
'true' if server['owned'] else 'false')
|
||||
utils.settings('accessToken', server['token'])
|
||||
# Careful to distinguish local from remote PMS
|
||||
if server['local']:
|
||||
scheme = server['scheme']
|
||||
|
@ -551,22 +542,14 @@ class InitialSetup(object):
|
|||
|
||||
# Display a warning if Kodi puts ALL movies into the queue, basically
|
||||
# breaking playback reporting for PKC
|
||||
warn = False
|
||||
settings = js.settings_getsettingvalue('videoplayer.autoplaynextitem')
|
||||
if v.KODIVERSION >= 18:
|
||||
# Answer for videoplayer.autoplaynextitem:
|
||||
# [{u'label': u'Music videos', u'value': 0},
|
||||
# {u'label': u'TV shows', u'value': 1},
|
||||
# {u'label': u'Episodes', u'value': 2},
|
||||
# {u'label': u'Movies', u'value': 3},
|
||||
# {u'label': u'Uncategorized', u'value': 4}]
|
||||
if 1 in settings or 2 in settings or 3 in settings:
|
||||
warn = True
|
||||
else:
|
||||
# Kodi Krypton: answer is boolean
|
||||
if settings:
|
||||
warn = True
|
||||
if warn:
|
||||
# Answer for videoplayer.autoplaynextitem:
|
||||
# [{u'label': u'Music videos', u'value': 0},
|
||||
# {u'label': u'TV shows', u'value': 1},
|
||||
# {u'label': u'Episodes', u'value': 2},
|
||||
# {u'label': u'Movies', u'value': 3},
|
||||
# {u'label': u'Uncategorized', u'value': 4}]
|
||||
if 1 in settings or 2 in settings or 3 in settings:
|
||||
LOG.warn('Kodi setting videoplayer.autoplaynextitem is: %s',
|
||||
settings)
|
||||
if utils.settings('warned_setting_videoplayer.autoplaynextitem') == 'false':
|
||||
|
@ -576,17 +559,13 @@ class InitialSetup(object):
|
|||
# Warning: Kodi setting "Play next video automatically" is
|
||||
# enabled. This could break PKC. Deactivate?
|
||||
if utils.yesno_dialog(utils.lang(29999), utils.lang(30003)):
|
||||
if v.KODIVERSION >= 18:
|
||||
for i in (1, 2, 3):
|
||||
try:
|
||||
settings.remove(i)
|
||||
except ValueError:
|
||||
pass
|
||||
js.settings_setsettingvalue('videoplayer.autoplaynextitem',
|
||||
settings)
|
||||
else:
|
||||
js.settings_setsettingvalue('videoplayer.autoplaynextitem',
|
||||
False)
|
||||
for i in (1, 2, 3):
|
||||
try:
|
||||
settings.remove(i)
|
||||
except ValueError:
|
||||
pass
|
||||
js.settings_setsettingvalue('videoplayer.autoplaynextitem',
|
||||
settings)
|
||||
# Set any video library updates to happen in the background in order to
|
||||
# hide "Compressing database"
|
||||
js.settings_setsettingvalue('videolibrary.backgroundupdate', True)
|
||||
|
@ -634,7 +613,11 @@ class InitialSetup(object):
|
|||
app.ACCOUNT.load()
|
||||
app.SYNC.load()
|
||||
return
|
||||
|
||||
LOG.info('Showing install questions')
|
||||
if not utils.default_kodi_skin_warning_message():
|
||||
LOG.info('Aborting initial setup due to skin')
|
||||
return
|
||||
# Additional settings where the user needs to choose
|
||||
# Direct paths (\\NAS\mymovie.mkv) or addon (http)?
|
||||
goto_settings = False
|
||||
|
@ -689,10 +672,10 @@ class InitialSetup(object):
|
|||
|
||||
# If you use several Plex libraries of one kind, e.g. "Kids Movies" and
|
||||
# "Parents Movies", be sure to check https://goo.gl/JFtQV9
|
||||
# dialog.ok(heading=utils.lang(29999), line1=utils.lang(39076))
|
||||
# dialog.ok(heading=utils.lang(29999), message=utils.lang(39076))
|
||||
|
||||
# Need to tell about our image source for collections: themoviedb.org
|
||||
# dialog.ok(heading=utils.lang(29999), line1=utils.lang(39717))
|
||||
# dialog.ok(heading=utils.lang(29999), message=utils.lang(39717))
|
||||
# Make sure that we only ask these questions upon first installation
|
||||
utils.settings('InstallQuestionsAnswered', value='true')
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from .movies import Movie
|
||||
from .tvshows import Show, Season, Episode
|
||||
from .music import Artist, Album, Song
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
from ntpath import dirname
|
||||
|
||||
|
@ -162,7 +161,7 @@ class ItemBase(object):
|
|||
Returns a dict of the Kodi ids: {<provider>: <kodi_unique_id>}
|
||||
"""
|
||||
kodi_unique_ids = api.guids.copy()
|
||||
for provider, provider_id in api.guids.iteritems():
|
||||
for provider, provider_id in api.guids.items():
|
||||
kodi_unique_ids[provider] = self.kodidb.add_uniqueid(
|
||||
kodi_id,
|
||||
api.kodi_type,
|
||||
|
|
|
@ -1,14 +1,26 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import re
|
||||
import os
|
||||
import string
|
||||
|
||||
from .common import ItemBase
|
||||
from ..plex_api import API
|
||||
from .. import app, variables as v, plex_functions as PF
|
||||
from ..path_ops import append_os_sep
|
||||
|
||||
LOG = getLogger('PLEX.movies')
|
||||
|
||||
# Tolerance in years if comparing videos as equal
|
||||
VIDEOYEAR_TOLERANCE = 1
|
||||
PUNCTUATION_TRANSLATION = {ord(char): None for char in string.punctuation}
|
||||
# Punctuation removed in original strings!!
|
||||
# Matches '2010 The Year We Make Contact 1984'
|
||||
# from '2010 The Year We Make Contact 1984 720p webrip'
|
||||
REGEX_MOVIENAME_AND_YEAR = re.compile(
|
||||
r'''(.+)((?:19|20)\d{2}).*(?!((19|20)\d{2}))''')
|
||||
|
||||
|
||||
class Movie(ItemBase):
|
||||
"""
|
||||
|
@ -38,11 +50,39 @@ class Movie(ItemBase):
|
|||
|
||||
fullpath, path, filename = api.fullpath()
|
||||
if app.SYNC.direct_paths and not fullpath.startswith('http'):
|
||||
kodi_pathid = self.kodidb.add_path(path,
|
||||
content='movies',
|
||||
scraper='metadata.local')
|
||||
if api.subtype:
|
||||
# E.g. homevideos, which have "subtype" flag set
|
||||
# Homevideo directories need to be flat by Plex' instructions
|
||||
library_path, video_path = path, path
|
||||
else:
|
||||
# Normal movie libraries
|
||||
library_path, video_path, filename = split_movie_path(fullpath)
|
||||
if library_path == video_path:
|
||||
# "Flat" folder structure where e.g. movies lie all in 1 dir
|
||||
# E.g.
|
||||
# 'C:\\Movies\\Pulp Fiction (1994).mkv'
|
||||
kodi_pathid = self.kodidb.add_path(library_path,
|
||||
content='movies',
|
||||
scraper='metadata.local')
|
||||
path = library_path
|
||||
kodi_parent_pathid = kodi_pathid
|
||||
else:
|
||||
# Plex library contains folders named identical to the
|
||||
# video file, e.g.
|
||||
# 'C:\\Movies\\Pulp Fiction (1994)\\Pulp Fiction (1994).mkv'
|
||||
# Add the "parent" path for the Plex library
|
||||
kodi_parent_pathid = self.kodidb.add_path(
|
||||
library_path,
|
||||
content='movies',
|
||||
scraper='metadata.local')
|
||||
# Add this movie's path
|
||||
kodi_pathid = self.kodidb.add_path(
|
||||
video_path,
|
||||
id_parent_path=kodi_parent_pathid)
|
||||
path = video_path
|
||||
else:
|
||||
kodi_pathid = self.kodidb.get_path(path)
|
||||
kodi_parent_pathid = kodi_pathid
|
||||
|
||||
if update_item:
|
||||
LOG.info('UPDATE movie plex_id: %s - %s', plex_id, api.title())
|
||||
|
@ -106,8 +146,8 @@ class Movie(ItemBase):
|
|||
api.list_to_string(api.studios()),
|
||||
api.trailer(),
|
||||
api.list_to_string(api.countries()),
|
||||
fullpath,
|
||||
kodi_pathid,
|
||||
path,
|
||||
kodi_parent_pathid,
|
||||
api.premiere_date(),
|
||||
api.userrating())
|
||||
|
||||
|
@ -133,6 +173,7 @@ class Movie(ItemBase):
|
|||
kodi_id=kodi_id,
|
||||
kodi_fileid=file_id,
|
||||
kodi_pathid=kodi_pathid,
|
||||
trailer_synced=bool(api.trailer()),
|
||||
last_sync=self.last_sync)
|
||||
|
||||
def remove(self, plex_id, plex_type=None):
|
||||
|
@ -195,9 +236,10 @@ class Movie(ItemBase):
|
|||
return True
|
||||
|
||||
def _process_collections(self, api, tags, kodi_id, section_id, children):
|
||||
for _, set_name in api.collections():
|
||||
tags.append(set_name)
|
||||
for plex_set_id, set_name in api.collections():
|
||||
set_api = None
|
||||
tags.append(set_name)
|
||||
# Add any sets from Plex collection tags
|
||||
kodi_set_id = self.kodidb.create_collection(set_name)
|
||||
self.kodidb.assign_collection(kodi_set_id, kodi_id)
|
||||
|
@ -242,3 +284,73 @@ class Movie(ItemBase):
|
|||
return unique_ids.get('imdb',
|
||||
unique_ids.get('tmdb',
|
||||
unique_ids.get('tvdb')))
|
||||
|
||||
|
||||
def split_movie_path(path):
|
||||
"""
|
||||
Implements Plex' video naming convention for movies:
|
||||
https://support.plex.tv/articles/naming-and-organizing-your-movie-media-files/
|
||||
|
||||
Splits a video's path into its librarypath, potential video folder, and
|
||||
filename.
|
||||
E.g. path = 'C:\\Movies\\Pulp Fiction (1994)\\Pulp Fiction (1994).mkv'
|
||||
returns the tuple
|
||||
('C:\\Movies\\',
|
||||
'C:\\Movies\\Pulp Fiction (1994)\\',
|
||||
'Pulp Fiction (1994).mkv')
|
||||
|
||||
E.g. path = 'C:\\Movies\\Pulp Fiction (1994).mkv'
|
||||
returns the tuple
|
||||
('C:\\Movies\\',
|
||||
'C:\\Movies\\',
|
||||
'Pulp Fiction (1994).mkv')
|
||||
"""
|
||||
basename, filename = os.path.split(path)
|
||||
library_path, videofolder = os.path.split(basename)
|
||||
|
||||
clean_filename = _clean_name(os.path.splitext(filename)[0])
|
||||
clean_videofolder = _clean_name(videofolder)
|
||||
|
||||
try:
|
||||
parsed_filename = _parse_videoname_and_year(clean_filename)
|
||||
except (TypeError, IndexError):
|
||||
LOG.warn('Could not parse video path, be sure to follow the Plex '
|
||||
'naming guidelines!! We failed to parse this path: %s', path)
|
||||
# Be on the safe side and assume that the movie folder structure is
|
||||
# flat
|
||||
return append_os_sep(basename), append_os_sep(basename), filename
|
||||
try:
|
||||
parsed_videofolder = _parse_videoname_and_year(clean_videofolder)
|
||||
except (TypeError, IndexError):
|
||||
# e.g. no year to parse => flat structure
|
||||
return append_os_sep(basename), append_os_sep(basename), filename
|
||||
if _parsed_names_alike(parsed_filename, parsed_videofolder):
|
||||
# e.g.
|
||||
# filename = The Master.(2012).720p.Blu-ray.axed.mkv
|
||||
# videofolder = The Master 2012
|
||||
# or
|
||||
# filename = National Lampoon's Christmas Vacation (1989)
|
||||
# [x264-Bluray-1080p DTS-2.0]
|
||||
# videofolder = Christmas Vacation 1989
|
||||
return append_os_sep(library_path), append_os_sep(basename), filename
|
||||
else:
|
||||
# Flat movie file-stuctrue, all movies in one big directory
|
||||
return append_os_sep(basename), append_os_sep(basename), filename
|
||||
|
||||
|
||||
def _parsed_names_alike(name1, name2):
|
||||
return (abs(name2[1] - name1[1]) <= VIDEOYEAR_TOLERANCE and
|
||||
(name1[0] in name2[0] or name2[0] in name1[0]))
|
||||
|
||||
|
||||
def _clean_name(name):
|
||||
"""
|
||||
Returns name with all whitespaces (regex "\\s") and punctuation
|
||||
(string.punctuation) characters removed; all characters in lowercase
|
||||
"""
|
||||
return re.sub('\\s', '', name).translate(PUNCTUATION_TRANSLATION).lower()
|
||||
|
||||
|
||||
def _parse_videoname_and_year(name):
|
||||
parsed = REGEX_MOVIENAME_AND_YEAR.search(name)
|
||||
return parsed[1], int(parsed[2])
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from .common import ItemBase
|
||||
|
@ -127,17 +126,12 @@ class MusicMixin(object):
|
|||
# Check whether we have orphaned path entries
|
||||
if not self.kodidb.path_id_from_song(kodi_id):
|
||||
self.kodidb.remove_path(path_id)
|
||||
if v.KODIVERSION < 18:
|
||||
self.kodidb.remove_albuminfosong(kodi_id)
|
||||
self.kodidb.delete_artwork(kodi_id, v.KODI_TYPE_SONG)
|
||||
|
||||
def remove_album(self, kodi_id):
|
||||
'''
|
||||
Remove an album
|
||||
'''
|
||||
self.kodidb.delete_album_from_discography(kodi_id)
|
||||
if v.KODIVERSION < 18:
|
||||
self.kodidb.delete_album_from_album_genre(kodi_id)
|
||||
self.kodidb.remove_album(kodi_id)
|
||||
self.kodidb.delete_artwork(kodi_id, v.KODI_TYPE_ALBUM)
|
||||
|
||||
|
@ -177,16 +171,6 @@ class Artist(MusicMixin, ItemBase):
|
|||
|
||||
if app.SYNC.artwork:
|
||||
artworks = api.artwork()
|
||||
if 'poster' in artworks:
|
||||
thumb = "<thumb>%s</thumb>" % artworks['poster']
|
||||
else:
|
||||
thumb = None
|
||||
if 'fanart' in artworks:
|
||||
fanart = "<fanart>%s</fanart>" % artworks['fanart']
|
||||
else:
|
||||
fanart = None
|
||||
else:
|
||||
thumb, fanart = None, None
|
||||
|
||||
# UPDATE THE ARTIST #####
|
||||
if update_item:
|
||||
|
@ -201,8 +185,6 @@ class Artist(MusicMixin, ItemBase):
|
|||
kodi_id = self.kodidb.add_artist(api.title(), musicBrainzId)
|
||||
self.kodidb.update_artist(api.list_to_string(api.genres()),
|
||||
api.plot(),
|
||||
thumb,
|
||||
fanart,
|
||||
timing.unix_date_to_kodi(self.last_sync),
|
||||
kodi_id)
|
||||
if app.SYNC.artwork:
|
||||
|
@ -282,82 +264,47 @@ class Album(MusicMixin, ItemBase):
|
|||
genre = api.list_to_string(api.genres())
|
||||
if app.SYNC.artwork:
|
||||
artworks = api.artwork()
|
||||
if 'poster' in artworks:
|
||||
thumb = "<thumb>%s</thumb>" % artworks['poster']
|
||||
else:
|
||||
thumb = None
|
||||
else:
|
||||
thumb = None
|
||||
|
||||
# UPDATE THE ALBUM #####
|
||||
if update_item:
|
||||
LOG.info("UPDATE album plex_id: %s - Name: %s", plex_id, name)
|
||||
if v.KODIVERSION >= 18:
|
||||
self.kodidb.update_album(name,
|
||||
musicBrainzId,
|
||||
api.artist_name(),
|
||||
genre,
|
||||
api.year(),
|
||||
compilation,
|
||||
api.plot(),
|
||||
thumb,
|
||||
api.list_to_string(api.studios()),
|
||||
api.userrating(),
|
||||
timing.unix_date_to_kodi(self.last_sync),
|
||||
'album',
|
||||
kodi_id)
|
||||
else:
|
||||
self.kodidb.update_album_17(name,
|
||||
musicBrainzId,
|
||||
api.artist_name(),
|
||||
genre,
|
||||
api.year(),
|
||||
compilation,
|
||||
api.plot(),
|
||||
thumb,
|
||||
api.list_to_string(api.studios()),
|
||||
api.userrating(),
|
||||
timing.unix_date_to_kodi(self.last_sync),
|
||||
'album',
|
||||
kodi_id)
|
||||
self.kodidb.update_album(name,
|
||||
musicBrainzId,
|
||||
api.artist_name(),
|
||||
genre,
|
||||
api.premiere_date(),
|
||||
# TODO: as soon as Plex supports the original
|
||||
# release date (Kodi: strOrigReleaseDate)
|
||||
api.premiere_date(),
|
||||
compilation,
|
||||
api.plot(),
|
||||
api.list_to_string(api.studios()),
|
||||
api.kodi_type,
|
||||
api.userrating(),
|
||||
timing.unix_date_to_kodi(self.last_sync),
|
||||
api.kodi_type,
|
||||
kodi_id)
|
||||
# OR ADD THE ALBUM #####
|
||||
else:
|
||||
LOG.info("ADD album plex_id: %s - Name: %s", plex_id, name)
|
||||
kodi_id = self.kodidb.new_album_id()
|
||||
if v.KODIVERSION >= 18:
|
||||
self.kodidb.add_album(kodi_id,
|
||||
name,
|
||||
musicBrainzId,
|
||||
api.artist_name(),
|
||||
genre,
|
||||
api.year(),
|
||||
compilation,
|
||||
api.plot(),
|
||||
thumb,
|
||||
api.list_to_string(api.studios()),
|
||||
api.userrating(),
|
||||
timing.unix_date_to_kodi(self.last_sync),
|
||||
'album')
|
||||
else:
|
||||
self.kodidb.add_album_17(kodi_id,
|
||||
name,
|
||||
musicBrainzId,
|
||||
api.artist_name(),
|
||||
genre,
|
||||
api.year(),
|
||||
compilation,
|
||||
api.plot(),
|
||||
thumb,
|
||||
api.list_to_string(api.studios()),
|
||||
api.userrating(),
|
||||
timing.unix_date_to_kodi(self.last_sync),
|
||||
'album')
|
||||
self.kodidb.add_album(kodi_id,
|
||||
name,
|
||||
musicBrainzId,
|
||||
api.artist_name(),
|
||||
genre,
|
||||
api.premiere_date(),
|
||||
# TODO: as soon as Plex supports the original
|
||||
# release date (Kodi: strOrigReleaseDate)
|
||||
api.premiere_date(),
|
||||
compilation,
|
||||
api.plot(),
|
||||
api.list_to_string(api.studios()),
|
||||
api.kodi_type,
|
||||
api.userrating(),
|
||||
timing.unix_date_to_kodi(self.last_sync),
|
||||
api.kodi_type)
|
||||
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:
|
||||
self.kodidb.modify_artwork(artworks,
|
||||
kodi_id,
|
||||
|
@ -438,34 +385,23 @@ class Song(MusicMixin, ItemBase):
|
|||
# No album found, create a single's album
|
||||
LOG.info('Creating singles album')
|
||||
parent_id = self.kodidb.new_album_id()
|
||||
if v.KODIVERSION >= 18:
|
||||
self.kodidb.add_album(kodi_id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
genre,
|
||||
api.year(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
timing.unix_date_to_kodi(self.last_sync),
|
||||
'single')
|
||||
else:
|
||||
self.kodidb.add_album_17(kodi_id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
genre,
|
||||
api.year(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
timing.unix_date_to_kodi(self.last_sync),
|
||||
'single')
|
||||
self.kodidb.add_album(kodi_id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
genre,
|
||||
api.premiere_date(),
|
||||
# TODO: as soon as Plex supports the original
|
||||
# release date (Kodi: strOrigReleaseDate)
|
||||
api.premiere_date(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
'single',
|
||||
None,
|
||||
None,
|
||||
timing.unix_date_to_kodi(self.last_sync),
|
||||
'single')
|
||||
else:
|
||||
album = self.plexdb.album(album_id)
|
||||
if not album:
|
||||
|
@ -521,97 +457,62 @@ class Song(MusicMixin, ItemBase):
|
|||
moods.append(entry.attrib['tag'])
|
||||
mood = api.list_to_string(moods)
|
||||
_, path, filename = api.fullpath()
|
||||
audio_codec = api.audio_codec()
|
||||
# UPDATE THE SONG #####
|
||||
if update_item:
|
||||
LOG.info("UPDATE song plex_id: %s - %s", plex_id, title)
|
||||
# Use dummy strHash '123' for Kodi
|
||||
self.kodidb.update_path(path, kodi_pathid)
|
||||
# Update the song entry
|
||||
if v.KODIVERSION >= 18:
|
||||
# Kodi Leia
|
||||
self.kodidb.update_song(parent_id,
|
||||
artists,
|
||||
genre,
|
||||
title,
|
||||
track,
|
||||
api.runtime(),
|
||||
year,
|
||||
filename,
|
||||
api.viewcount(),
|
||||
api.lastplayed(),
|
||||
api.userrating(),
|
||||
comment,
|
||||
mood,
|
||||
api.date_created(),
|
||||
kodi_id)
|
||||
else:
|
||||
self.kodidb.update_song_17(parent_id,
|
||||
artists,
|
||||
genre,
|
||||
title,
|
||||
track,
|
||||
api.runtime(),
|
||||
year,
|
||||
filename,
|
||||
api.viewcount(),
|
||||
api.lastplayed(),
|
||||
api.userrating(),
|
||||
comment,
|
||||
mood,
|
||||
api.date_created(),
|
||||
kodi_id)
|
||||
self.kodidb.update_song(parent_id,
|
||||
artists,
|
||||
genre,
|
||||
title,
|
||||
track,
|
||||
api.runtime(),
|
||||
api.premiere_date(),
|
||||
# TODO: as soon as Plex supports the original
|
||||
# release date (Kodi: strOrigReleaseDate)
|
||||
api.premiere_date(),
|
||||
filename,
|
||||
api.viewcount(),
|
||||
api.lastplayed(),
|
||||
api.userrating(),
|
||||
comment,
|
||||
mood,
|
||||
audio_codec['bitrate'] or 0,
|
||||
audio_codec['samplingrate'] or 0,
|
||||
audio_codec['channels'] or 0,
|
||||
api.date_created(),
|
||||
kodi_id)
|
||||
# OR ADD THE SONG #####
|
||||
else:
|
||||
LOG.info("ADD song plex_id: %s - %s", plex_id, title)
|
||||
# Add path
|
||||
kodi_pathid = self.kodidb.add_path(path)
|
||||
# Create the song entry
|
||||
if v.KODIVERSION >= 18:
|
||||
# Kodi Leia
|
||||
self.kodidb.add_song(kodi_id,
|
||||
parent_id,
|
||||
kodi_pathid,
|
||||
artists,
|
||||
genre,
|
||||
title,
|
||||
track,
|
||||
api.runtime(),
|
||||
year,
|
||||
filename,
|
||||
musicBrainzId,
|
||||
api.viewcount(),
|
||||
api.lastplayed(),
|
||||
api.userrating(),
|
||||
0,
|
||||
0,
|
||||
mood,
|
||||
api.date_created())
|
||||
else:
|
||||
self.kodidb.add_song_17(kodi_id,
|
||||
parent_id,
|
||||
kodi_pathid,
|
||||
artists,
|
||||
genre,
|
||||
title,
|
||||
track,
|
||||
api.runtime(),
|
||||
year,
|
||||
filename,
|
||||
musicBrainzId,
|
||||
api.viewcount(),
|
||||
api.lastplayed(),
|
||||
api.userrating(),
|
||||
0,
|
||||
0,
|
||||
mood,
|
||||
api.date_created())
|
||||
if v.KODIVERSION < 18:
|
||||
# Link song to album
|
||||
self.kodidb.add_albuminfosong(kodi_id,
|
||||
parent_id,
|
||||
track,
|
||||
title,
|
||||
api.runtime())
|
||||
self.kodidb.add_song(kodi_id,
|
||||
parent_id,
|
||||
kodi_pathid,
|
||||
artists,
|
||||
genre,
|
||||
title,
|
||||
track,
|
||||
api.runtime(),
|
||||
api.premiere_date(),
|
||||
# TODO: as soon as Plex supports the original
|
||||
# release date (Kodi: strOrigReleaseDate)
|
||||
api.premiere_date(),
|
||||
filename,
|
||||
musicBrainzId,
|
||||
api.viewcount(),
|
||||
api.lastplayed(),
|
||||
api.userrating(),
|
||||
0,
|
||||
0,
|
||||
mood,
|
||||
audio_codec['bitrate'] or 0,
|
||||
audio_codec['samplingrate'] or 0,
|
||||
audio_codec['channels'] or 0,
|
||||
api.date_created())
|
||||
# Link song to artists
|
||||
artist_name = api.grandparent_title()
|
||||
# Do the actual linking
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from .common import ItemBase, process_path
|
||||
|
@ -270,6 +269,7 @@ class Show(TvShowMixin, ItemBase):
|
|||
unique_ids.get('imdb',
|
||||
unique_ids.get('tmdb')))
|
||||
|
||||
|
||||
class Season(TvShowMixin, ItemBase):
|
||||
def add_update(self, xml, section_name=None, section_id=None,
|
||||
children=None):
|
||||
|
@ -279,7 +279,7 @@ class Season(TvShowMixin, ItemBase):
|
|||
api = API(xml)
|
||||
if not self.sync_this_item(section_id or api.library_section_id()):
|
||||
LOG.debug('Skipping sync of %s %s: %s - section %s not synched to '
|
||||
'Kodi', api.plex_type, api.plex_id, api.title(),
|
||||
'Kodi', api.plex_type, api.plex_id, api.season_name(),
|
||||
section_id or api.library_section_id())
|
||||
return
|
||||
plex_id = api.plex_id
|
||||
|
@ -317,15 +317,24 @@ class Season(TvShowMixin, ItemBase):
|
|||
if key in artwork and artwork[key] == parent_artwork[key]:
|
||||
del artwork[key]
|
||||
if update_item:
|
||||
LOG.info('UPDATE season plex_id %s - %s', plex_id, api.title())
|
||||
LOG.info('UPDATE season plex_id %s - %s',
|
||||
plex_id, api.season_name())
|
||||
kodi_id = season['kodi_id']
|
||||
self.kodidb.update_season(kodi_id,
|
||||
parent_id,
|
||||
api.index(),
|
||||
api.season_name(),
|
||||
api.userrating() or None)
|
||||
if app.SYNC.artwork:
|
||||
self.kodidb.modify_artwork(artwork,
|
||||
kodi_id,
|
||||
v.KODI_TYPE_SEASON)
|
||||
else:
|
||||
LOG.info('ADD season plex_id %s - %s', plex_id, api.title())
|
||||
kodi_id = self.kodidb.add_season(parent_id, api.index())
|
||||
LOG.info('ADD season plex_id %s - %s', plex_id, api.season_name())
|
||||
kodi_id = self.kodidb.add_season(parent_id,
|
||||
api.index(),
|
||||
api.season_name(),
|
||||
api.userrating() or None)
|
||||
if app.SYNC.artwork:
|
||||
self.kodidb.add_artwork(artwork,
|
||||
kodi_id,
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
Collection of functions using the Kodi JSON RPC interface.
|
||||
See http://kodi.wiki/view/JSON-RPC_API
|
||||
"""
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from json import loads, dumps
|
||||
from xbmc import executeJSONRPC
|
||||
|
||||
|
@ -85,7 +84,7 @@ def get_player_ids():
|
|||
Returns a list of all the active Kodi player ids (usually 3) as int
|
||||
"""
|
||||
ret = []
|
||||
for player in get_players().values():
|
||||
for player in list(get_players().values()):
|
||||
ret.append(player['playerid'])
|
||||
return ret
|
||||
|
||||
|
@ -170,12 +169,12 @@ def stop():
|
|||
|
||||
def seek_to(offset):
|
||||
"""
|
||||
Seeks all Kodi players to offset [int] in milliseconds
|
||||
Seeks all Kodi players to offset [int] in seconds
|
||||
"""
|
||||
for playerid in get_player_ids():
|
||||
return JsonRPC("Player.Seek").execute(
|
||||
{"playerid": playerid,
|
||||
"value": timing.millis_to_kodi_time(offset)})
|
||||
"value": {'time': timing.millis_to_kodi_time(int(offset * 1000))}})
|
||||
|
||||
|
||||
def smallforward():
|
||||
|
@ -421,6 +420,50 @@ def get_item(playerid):
|
|||
'properties': ['title', 'file']})['result']['item']
|
||||
|
||||
|
||||
def get_current_audio_stream_index(playerid):
|
||||
"""
|
||||
Returns the currently active audio stream index [int]
|
||||
"""
|
||||
return JsonRPC('Player.GetProperties').execute({
|
||||
'playerid': playerid,
|
||||
'properties': ['currentaudiostream']})['result']['currentaudiostream']['index']
|
||||
|
||||
|
||||
def get_current_video_stream_index(playerid):
|
||||
"""
|
||||
Returns the currently active video stream index [int]
|
||||
"""
|
||||
return JsonRPC('Player.GetProperties').execute({
|
||||
'playerid': playerid,
|
||||
'properties': ['currentvideostream']})['result']['currentvideostream']['index']
|
||||
|
||||
|
||||
def get_current_subtitle_stream_index(playerid):
|
||||
"""
|
||||
Returns the currently active subtitle stream index [int] or None if there
|
||||
are no subs
|
||||
PICKING UP CHANGES ON SUBTITLES IS CURRENTLY BROKEN ON THE KODI SIDE! The
|
||||
JSON reply won't change even though subtitles are changed :-(
|
||||
"""
|
||||
try:
|
||||
return JsonRPC('Player.GetProperties').execute({
|
||||
'playerid': playerid,
|
||||
'properties': ['currentsubtitle', ]})['result']['currentsubtitle']['index']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
def get_subtitle_enabled(playerid):
|
||||
"""
|
||||
Returns True if a subtitle is currently enabled, False otherwise.
|
||||
PICKING UP CHANGES ON SUBTITLES IS CURRENTLY BROKEN ON THE KODI SIDE! The
|
||||
JSON reply won't change even though subtitles are changed :-(
|
||||
"""
|
||||
return JsonRPC('Player.GetProperties').execute({
|
||||
'playerid': playerid,
|
||||
'properties': ['subtitleenabled', ]})['result']['subtitleenabled']
|
||||
|
||||
|
||||
def get_player_props(playerid):
|
||||
"""
|
||||
Returns a dict for the active Kodi player with the following values:
|
||||
|
@ -586,6 +629,15 @@ def item_details(kodi_id, kodi_type):
|
|||
ret = JsonRPC(json).execute({'%sid' % kodi_type: kodi_id,
|
||||
'properties': fields})
|
||||
try:
|
||||
return ret['result']['%sdetails' % kodi_type]
|
||||
ret = ret['result']['%sdetails' % kodi_type]
|
||||
except (KeyError, TypeError):
|
||||
return {}
|
||||
if kodi_type == v.KODI_TYPE_SHOW:
|
||||
# append watched counts to tvshow details
|
||||
ret["extraproperties"] = {
|
||||
"totalseasons": str(ret["season"]),
|
||||
"totalepisodes": str(ret["episode"]),
|
||||
"watchedepisodes": str(ret["watchedepisodes"]),
|
||||
"unwatchedepisodes": str(ret["episode"] - ret["watchedepisodes"])
|
||||
}
|
||||
return ret
|
||||
|
|
|
@ -5,88 +5,45 @@ script.module.metadatautils
|
|||
kodi_constants.py
|
||||
Several common constants for use with Kodi json api
|
||||
'''
|
||||
FIELDS_BASE = ['dateadded', 'file', 'lastplayed', 'plot', 'title', 'art',
|
||||
'playcount']
|
||||
FIELDS_FILE = FIELDS_BASE + ['streamdetails', 'director', 'resume', 'runtime']
|
||||
FIELDS_MOVIES = FIELDS_FILE + ['plotoutline', 'sorttitle', 'cast', 'votes',
|
||||
'showlink', 'top250', 'trailer', 'year', 'country', 'studio', 'set',
|
||||
'genre', 'mpaa', 'setid', 'rating', 'tag', 'tagline', 'writer',
|
||||
'originaltitle', 'imdbnumber', 'uniqueid']
|
||||
FIELDS_TVSHOWS = FIELDS_BASE + ['sorttitle', 'mpaa', 'premiered', 'year',
|
||||
'episode', 'watchedepisodes', 'votes', 'rating', 'studio', 'season',
|
||||
'genre', 'cast', 'episodeguide', 'tag', 'originaltitle', 'imdbnumber']
|
||||
FIELDS_BASE = ["dateadded", "file", "lastplayed", "plot", "title", "art", "playcount"]
|
||||
FIELDS_FILE = FIELDS_BASE + ["streamdetails", "director", "resume", "runtime"]
|
||||
FIELDS_MOVIES = FIELDS_FILE + ["plotoutline", "sorttitle", "cast", "votes", "showlink", "top250", "trailer", "year",
|
||||
"country", "studio", "set", "genre", "mpaa", "setid", "rating", "tag", "tagline",
|
||||
"writer", "originaltitle",
|
||||
"imdbnumber"]
|
||||
FIELDS_MOVIES.append("uniqueid")
|
||||
FIELDS_TVSHOWS = FIELDS_BASE + ["sorttitle", "mpaa", "premiered", "year", "episode", "watchedepisodes", "votes",
|
||||
"rating", "studio", "season", "genre", "cast", "episodeguide", "tag", "originaltitle",
|
||||
"imdbnumber"]
|
||||
FIELDS_SEASON = ['art', 'playcount', 'season', 'showtitle', 'episode',
|
||||
'tvshowid', 'watchedepisodes', 'userrating', 'fanart', 'thumbnail']
|
||||
FIELDS_EPISODES = FIELDS_FILE + ['cast', 'productioncode', 'rating', 'votes',
|
||||
'episode', 'showtitle', 'tvshowid', 'season', 'firstaired', 'writer',
|
||||
'originaltitle']
|
||||
FIELDS_MUSICVIDEOS = FIELDS_FILE + ['genre', 'artist', 'tag', 'album', 'track',
|
||||
'studio', 'year']
|
||||
FIELDS_FILES = FIELDS_FILE + ['plotoutline', 'sorttitle', 'cast', 'votes',
|
||||
'trailer', 'year', 'country', 'studio', 'genre', 'mpaa', 'rating',
|
||||
'tagline', 'writer', 'originaltitle', 'imdbnumber', 'premiered', 'episode',
|
||||
'showtitle', 'firstaired', 'watchedepisodes', 'duration', 'season']
|
||||
FIELDS_SONGS = ['artist', 'displayartist', 'title', 'rating', 'fanart',
|
||||
'thumbnail', 'duration', 'disc', 'playcount', 'comment', 'file', 'album',
|
||||
'lastplayed', 'genre', 'musicbrainzartistid', 'track', 'dateadded']
|
||||
FIELDS_ALBUMS = ['title', 'fanart', 'thumbnail', 'genre', 'displayartist',
|
||||
'artist', 'musicbrainzalbumartistid', 'year', 'rating', 'artistid',
|
||||
'musicbrainzalbumid', 'theme', 'description', 'type', 'style', 'playcount',
|
||||
'albumlabel', 'mood', 'dateadded']
|
||||
FIELDS_ARTISTS = ['born', 'formed', 'died', 'style', 'yearsactive', 'mood',
|
||||
'fanart', 'thumbnail', 'musicbrainzartistid', 'disbanded', 'description',
|
||||
'instrument']
|
||||
FIELDS_RECORDINGS = ['art', 'channel', 'directory', 'endtime', 'file', 'genre',
|
||||
'icon', 'playcount', 'plot', 'plotoutline', 'resume', 'runtime',
|
||||
'starttime', 'streamurl', 'title']
|
||||
FIELDS_CHANNELS = ['broadcastnow', 'channeltype', 'hidden', 'locked',
|
||||
'lastplayed', 'thumbnail', 'channel']
|
||||
'tvshowid', 'watchedepisodes', 'userrating', 'fanart', 'thumbnail']
|
||||
FIELDS_EPISODES = FIELDS_FILE + ["cast", "productioncode", "rating", "votes", "episode", "showtitle", "tvshowid",
|
||||
"season", "firstaired", "writer", "originaltitle"]
|
||||
FIELDS_MUSICVIDEOS = FIELDS_FILE + ["genre", "artist", "tag", "album", "track", "studio", "year"]
|
||||
FIELDS_FILES = FIELDS_FILE + ["plotoutline", "sorttitle", "cast", "votes", "trailer", "year", "country", "studio",
|
||||
"genre", "mpaa", "rating", "tagline", "writer", "originaltitle", "imdbnumber",
|
||||
"premiered", "episode", "showtitle",
|
||||
"firstaired", "watchedepisodes", "duration", "season"]
|
||||
FIELDS_SONGS = ["artist", "displayartist", "title", "rating", "fanart", "thumbnail", "duration", "disc",
|
||||
"playcount", "comment", "file", "album", "lastplayed", "genre", "musicbrainzartistid", "track",
|
||||
"dateadded"]
|
||||
FIELDS_ALBUMS = ["title", "fanart", "thumbnail", "genre", "displayartist", "artist",
|
||||
"musicbrainzalbumartistid", "year", "rating", "artistid", "musicbrainzalbumid", "theme", "description",
|
||||
"type", "style", "playcount", "albumlabel", "mood", "dateadded"]
|
||||
FIELDS_ARTISTS = ["born", "formed", "died", "style", "yearsactive", "mood", "fanart", "thumbnail",
|
||||
"musicbrainzartistid", "disbanded", "description", "instrument"]
|
||||
FIELDS_RECORDINGS = ["art", "channel", "directory", "endtime", "file", "genre", "icon", "playcount", "plot",
|
||||
"plotoutline", "resume", "runtime", "starttime", "streamurl", "title"]
|
||||
FIELDS_CHANNELS = ["broadcastnow", "channeltype", "hidden", "locked", "lastplayed", "thumbnail", "channel"]
|
||||
|
||||
FILTER_UNWATCHED = {
|
||||
'operator': 'lessthan',
|
||||
'field': 'playcount',
|
||||
'value': '1'
|
||||
}
|
||||
FILTER_WATCHED = {
|
||||
'operator': 'isnot',
|
||||
'field': 'playcount',
|
||||
'value': '0'
|
||||
}
|
||||
FILTER_RATING = {
|
||||
'operator': 'greaterthan',
|
||||
'field': 'rating',
|
||||
'value': '7'
|
||||
}
|
||||
FILTER_RATING_MUSIC = {
|
||||
'operator': 'greaterthan',
|
||||
'field': 'rating',
|
||||
'value': '3'
|
||||
}
|
||||
FILTER_INPROGRESS = {
|
||||
'operator': 'true',
|
||||
'field': 'inprogress',
|
||||
'value': ''
|
||||
}
|
||||
SORT_RATING = {
|
||||
'method': 'rating',
|
||||
'order': 'descending'
|
||||
}
|
||||
SORT_RANDOM = {
|
||||
'method': 'random',
|
||||
'order': 'descending'
|
||||
}
|
||||
SORT_TITLE = {
|
||||
'method': 'title',
|
||||
'order': 'ascending'
|
||||
}
|
||||
SORT_DATEADDED = {
|
||||
'method': 'dateadded',
|
||||
'order': 'descending'
|
||||
}
|
||||
SORT_LASTPLAYED = {
|
||||
'method': 'lastplayed',
|
||||
'order': 'descending'
|
||||
}
|
||||
SORT_EPISODE = {
|
||||
'method': 'episode'
|
||||
}
|
||||
FILTER_UNWATCHED = {"operator": "lessthan", "field": "playcount", "value": "1"}
|
||||
FILTER_WATCHED = {"operator": "isnot", "field": "playcount", "value": "0"}
|
||||
FILTER_RATING = {"operator": "greaterthan", "field": "rating", "value": "7"}
|
||||
FILTER_RATING_MUSIC = {"operator": "greaterthan", "field": "rating", "value": "3"}
|
||||
FILTER_INPROGRESS = {"operator": "true", "field": "inprogress", "value": ""}
|
||||
SORT_RATING = {"method": "rating", "order": "descending"}
|
||||
SORT_RANDOM = {"method": "random", "order": "descending"}
|
||||
SORT_TITLE = {"method": "title", "order": "ascending"}
|
||||
SORT_DATEADDED = {"method": "dateadded", "order": "descending"}
|
||||
SORT_LASTPLAYED = {"method": "lastplayed", "order": "descending"}
|
||||
SORT_EPISODE = {"method": "episode"}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from .common import KODIDB_LOCK
|
||||
|
@ -21,7 +20,6 @@ def kodiid_from_filename(path, kodi_type=None, db_type=None):
|
|||
Returns None, <kodi_type> if not possible
|
||||
"""
|
||||
kodi_id = None
|
||||
path = utils.try_decode(path)
|
||||
# Make sure path ends in either '/' or '\'
|
||||
# We CANNOT use path_ops.path.join as this can result in \ where we need /
|
||||
try:
|
||||
|
@ -74,7 +72,7 @@ def reset_cached_images():
|
|||
for path in paths:
|
||||
new_path = path_ops.translate_path('special://thumbnails/%s' % path)
|
||||
try:
|
||||
path_ops.makedirs(path_ops.encode_path(new_path))
|
||||
path_ops.makedirs(new_path)
|
||||
except OSError as err:
|
||||
LOG.warn('Could not create thumbnail directory %s: %s',
|
||||
new_path, err)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from threading import Lock
|
||||
|
||||
from .. import db, path_ops
|
||||
|
@ -65,7 +64,7 @@ class KodiDBBase(object):
|
|||
"""
|
||||
Pass in an artworks dict (see PlexAPI) to set an items artwork.
|
||||
"""
|
||||
for kodi_art, url in artworks.iteritems():
|
||||
for kodi_art, url in artworks.items():
|
||||
self.add_art(url, kodi_id, kodi_type, kodi_art)
|
||||
|
||||
@db.catch_operationalerrors
|
||||
|
@ -84,7 +83,7 @@ class KodiDBBase(object):
|
|||
"""
|
||||
Pass in an artworks dict (see PlexAPI) to set an items artwork.
|
||||
"""
|
||||
for kodi_art, url in artworks.iteritems():
|
||||
for kodi_art, url in artworks.items():
|
||||
self.modify_art(url, kodi_id, kodi_type, kodi_art)
|
||||
|
||||
@db.catch_operationalerrors
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from . import common
|
||||
|
@ -49,17 +48,16 @@ class KodiMusicDB(common.KodiDBBase):
|
|||
strRole)
|
||||
VALUES (?, ?)
|
||||
''', (1, 'Artist'))
|
||||
if v.KODIVERSION >= 18:
|
||||
self.cursor.execute('DELETE FROM versiontagscan')
|
||||
self.cursor.execute('''
|
||||
INSERT INTO versiontagscan(
|
||||
idVersion,
|
||||
iNeedsScan,
|
||||
lastscanned)
|
||||
VALUES (?, ?, ?)
|
||||
''', (v.DB_MUSIC_VERSION,
|
||||
0,
|
||||
timing.kodi_now()))
|
||||
self.cursor.execute('DELETE FROM versiontagscan')
|
||||
self.cursor.execute('''
|
||||
INSERT INTO versiontagscan(
|
||||
idVersion,
|
||||
iNeedsScan,
|
||||
lastscanned)
|
||||
VALUES (?, ?, ?)
|
||||
''', (v.DB_MUSIC_VERSION,
|
||||
0,
|
||||
timing.kodi_now()))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def update_path(self, path, kodi_pathid):
|
||||
|
@ -106,26 +104,6 @@ class KodiMusicDB(common.KodiDBBase):
|
|||
self.cursor.execute('DELETE FROM song_artist WHERE idSong = ?',
|
||||
(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
|
||||
def delete_song_from_song_genre(self, song_id):
|
||||
"""
|
||||
|
@ -180,87 +158,6 @@ class KodiMusicDB(common.KodiDBBase):
|
|||
self.cursor.execute('SELECT COALESCE(MAX(idAlbum), 0) FROM album')
|
||||
return self.cursor.fetchone()[0] + 1
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def add_album_17(self, *args):
|
||||
"""
|
||||
strReleaseType: 'album' or 'single'
|
||||
"""
|
||||
if app.SYNC.artwork:
|
||||
self.cursor.execute('''
|
||||
INSERT INTO album(
|
||||
idAlbum,
|
||||
strAlbum,
|
||||
strMusicBrainzAlbumID,
|
||||
strArtists,
|
||||
strGenres,
|
||||
iYear,
|
||||
bCompilation,
|
||||
strReview,
|
||||
strImage,
|
||||
strLabel,
|
||||
iUserrating,
|
||||
lastScraped,
|
||||
strReleaseType)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (args))
|
||||
else:
|
||||
args = list(args)
|
||||
del args[8]
|
||||
self.cursor.execute('''
|
||||
INSERT INTO album(
|
||||
idAlbum,
|
||||
strAlbum,
|
||||
strMusicBrainzAlbumID,
|
||||
strArtists,
|
||||
strGenres,
|
||||
iYear,
|
||||
bCompilation,
|
||||
strReview,
|
||||
strLabel,
|
||||
iUserrating,
|
||||
lastScraped,
|
||||
strReleaseType)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (args))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def update_album_17(self, *args):
|
||||
if app.SYNC.artwork:
|
||||
self.cursor.execute('''
|
||||
UPDATE album
|
||||
SET strAlbum = ?,
|
||||
strMusicBrainzAlbumID = ?,
|
||||
strArtists = ?,
|
||||
strGenres = ?,
|
||||
iYear = ?,
|
||||
bCompilation = ?,
|
||||
strReview = ?,
|
||||
strImage = ?,
|
||||
strLabel = ?,
|
||||
iUserrating = ?,
|
||||
lastScraped = ?,
|
||||
strReleaseType = ?
|
||||
WHERE idAlbum = ?
|
||||
''', (args))
|
||||
else:
|
||||
args = list(args)
|
||||
del args[7]
|
||||
self.cursor.execute('''
|
||||
UPDATE album
|
||||
SET strAlbum = ?,
|
||||
strMusicBrainzAlbumID = ?,
|
||||
strArtists = ?,
|
||||
strGenres = ?,
|
||||
iYear = ?,
|
||||
bCompilation = ?,
|
||||
strReview = ?,
|
||||
strLabel = ?,
|
||||
iUserrating = ?,
|
||||
lastScraped = ?,
|
||||
strReleaseType = ?
|
||||
WHERE idAlbum = ?
|
||||
''', (args))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def add_album(self, *args):
|
||||
"""
|
||||
|
@ -274,15 +171,16 @@ class KodiMusicDB(common.KodiDBBase):
|
|||
strMusicBrainzAlbumID,
|
||||
strArtistDisp,
|
||||
strGenres,
|
||||
iYear,
|
||||
strReleaseDate,
|
||||
strOrigReleaseDate,
|
||||
bCompilation,
|
||||
strReview,
|
||||
strImage,
|
||||
strLabel,
|
||||
strType,
|
||||
iUserrating,
|
||||
lastScraped,
|
||||
strReleaseType)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (args))
|
||||
else:
|
||||
args = list(args)
|
||||
|
@ -294,14 +192,16 @@ class KodiMusicDB(common.KodiDBBase):
|
|||
strMusicBrainzAlbumID,
|
||||
strArtistDisp,
|
||||
strGenres,
|
||||
iYear,
|
||||
strReleaseDate,
|
||||
strOrigReleaseDate,
|
||||
bCompilation,
|
||||
strReview,
|
||||
strLabel,
|
||||
strType,
|
||||
iUserrating,
|
||||
lastScraped,
|
||||
strReleaseType)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (args))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
|
@ -313,11 +213,12 @@ class KodiMusicDB(common.KodiDBBase):
|
|||
strMusicBrainzAlbumID = ?,
|
||||
strArtistDisp = ?,
|
||||
strGenres = ?,
|
||||
iYear = ?,
|
||||
strReleaseDate = ?,
|
||||
strOrigReleaseDate = ?,
|
||||
bCompilation = ?,
|
||||
strReview = ?,
|
||||
strImage = ?,
|
||||
strLabel = ?,
|
||||
strType = ?,
|
||||
iUserrating = ?,
|
||||
lastScraped = ?,
|
||||
strReleaseType = ?
|
||||
|
@ -332,7 +233,8 @@ class KodiMusicDB(common.KodiDBBase):
|
|||
strMusicBrainzAlbumID = ?,
|
||||
strArtistDisp = ?,
|
||||
strGenres = ?,
|
||||
iYear = ?,
|
||||
strReleaseDate = ?,
|
||||
strOrigReleaseDate = ?,
|
||||
bCompilation = ?,
|
||||
strReview = ?,
|
||||
strLabel = ?,
|
||||
|
@ -352,16 +254,6 @@ class KodiMusicDB(common.KodiDBBase):
|
|||
VALUES (?, ?, ?)
|
||||
''', (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
|
||||
def add_music_genres(self, kodiid, genres, mediatype):
|
||||
"""
|
||||
|
@ -425,7 +317,8 @@ class KodiMusicDB(common.KodiDBBase):
|
|||
strTitle,
|
||||
iTrack,
|
||||
iDuration,
|
||||
iYear,
|
||||
strReleaseDate,
|
||||
strOrigReleaseDate,
|
||||
strFileName,
|
||||
strMusicBrainzTrackID,
|
||||
iTimesPlayed,
|
||||
|
@ -434,33 +327,11 @@ class KodiMusicDB(common.KodiDBBase):
|
|||
iStartOffset,
|
||||
iEndOffset,
|
||||
mood,
|
||||
iBitRate,
|
||||
iSampleRate,
|
||||
iChannels,
|
||||
dateAdded)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (args))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def add_song_17(self, *args):
|
||||
self.cursor.execute('''
|
||||
INSERT INTO song(
|
||||
idSong,
|
||||
idAlbum,
|
||||
idPath,
|
||||
strArtists,
|
||||
strGenres,
|
||||
strTitle,
|
||||
iTrack,
|
||||
iDuration,
|
||||
iYear,
|
||||
strFileName,
|
||||
strMusicBrainzTrackID,
|
||||
iTimesPlayed,
|
||||
lastplayed,
|
||||
rating,
|
||||
iStartOffset,
|
||||
iEndOffset,
|
||||
mood,
|
||||
dateAdded)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (args))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
|
@ -473,13 +344,17 @@ class KodiMusicDB(common.KodiDBBase):
|
|||
strTitle = ?,
|
||||
iTrack = ?,
|
||||
iDuration = ?,
|
||||
iYear = ?,
|
||||
strReleaseDate = ?,
|
||||
strOrigReleaseDate = ?,
|
||||
strFilename = ?,
|
||||
iTimesPlayed = ?,
|
||||
lastplayed = ?,
|
||||
rating = ?,
|
||||
comment = ?,
|
||||
mood = ?,
|
||||
iBitRate = ?,
|
||||
iSampleRate = ?,
|
||||
iChannels = ?,
|
||||
dateAdded = ?
|
||||
WHERE idSong = ?
|
||||
''', (args))
|
||||
|
@ -493,27 +368,6 @@ class KodiMusicDB(common.KodiDBBase):
|
|||
WHERE idSong = ?
|
||||
''', (args))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def update_song_17(self, *args):
|
||||
self.cursor.execute('''
|
||||
UPDATE song
|
||||
SET idAlbum = ?,
|
||||
strArtists = ?,
|
||||
strGenres = ?,
|
||||
strTitle = ?,
|
||||
iTrack = ?,
|
||||
iDuration = ?,
|
||||
iYear = ?,
|
||||
strFilename = ?,
|
||||
iTimesPlayed = ?,
|
||||
lastplayed = ?,
|
||||
rating = ?,
|
||||
comment = ?,
|
||||
mood = ?,
|
||||
dateAdded = ?
|
||||
WHERE idSong = ?
|
||||
''', (args))
|
||||
|
||||
def path_id_from_song(self, kodi_id):
|
||||
self.cursor.execute('SELECT idPath FROM song WHERE idSong = ? LIMIT 1',
|
||||
(kodi_id, ))
|
||||
|
@ -557,26 +411,13 @@ class KodiMusicDB(common.KodiDBBase):
|
|||
|
||||
@db.catch_operationalerrors
|
||||
def update_artist(self, *args):
|
||||
if app.SYNC.artwork:
|
||||
self.cursor.execute('''
|
||||
UPDATE artist
|
||||
SET strGenres = ?,
|
||||
strBiography = ?,
|
||||
strImage = ?,
|
||||
strFanart = ?,
|
||||
lastScraped = ?
|
||||
WHERE idArtist = ?
|
||||
''', (args))
|
||||
else:
|
||||
args = list(args)
|
||||
del args[3], args[2]
|
||||
self.cursor.execute('''
|
||||
UPDATE artist
|
||||
SET strGenres = ?,
|
||||
strBiography = ?,
|
||||
lastScraped = ?
|
||||
WHERE idArtist = ?
|
||||
''', (args))
|
||||
self.cursor.execute('''
|
||||
UPDATE artist
|
||||
SET strGenres = ?,
|
||||
strBiography = ?,
|
||||
lastScraped = ?
|
||||
WHERE idArtist = ?
|
||||
''', (args))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def remove_song(self, kodi_id):
|
||||
|
@ -598,22 +439,6 @@ class KodiMusicDB(common.KodiDBBase):
|
|||
VALUES (?, ?, ?, ?, ?)
|
||||
''', (artist_id, song_id, 1, 0, artist_name))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def add_albuminfosong(self, song_id, album_id, track_no, track_title,
|
||||
runtime):
|
||||
"""
|
||||
Kodi 17 only
|
||||
"""
|
||||
self.cursor.execute('''
|
||||
INSERT OR REPLACE INTO albuminfosong(
|
||||
idAlbumInfoSong,
|
||||
idAlbumInfo,
|
||||
iTrack,
|
||||
strTitle,
|
||||
iDuration)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', (song_id, album_id, track_no, track_title, runtime))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def update_userrating(self, kodi_id, kodi_type, userrating):
|
||||
"""
|
||||
|
@ -641,9 +466,6 @@ class KodiMusicDB(common.KodiDBBase):
|
|||
|
||||
@db.catch_operationalerrors
|
||||
def remove_album(self, kodi_id):
|
||||
if v.KODIVERSION < 18:
|
||||
self.cursor.execute('DELETE FROM albuminfosong WHERE idAlbumInfo = ?',
|
||||
(kodi_id, ))
|
||||
self.cursor.execute('DELETE FROM album_artist WHERE idAlbum = ?',
|
||||
(kodi_id, ))
|
||||
self.cursor.execute('DELETE FROM album WHERE idAlbum = ?', (kodi_id, ))
|
||||
|
@ -656,5 +478,3 @@ class KodiMusicDB(common.KodiDBBase):
|
|||
(kodi_id, ))
|
||||
self.cursor.execute('DELETE FROM song_artist WHERE idArtist = ?',
|
||||
(kodi_id, ))
|
||||
self.cursor.execute('DELETE FROM discography WHERE idArtist = ?',
|
||||
(kodi_id, ))
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
|
||||
from . import common
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
from sqlite3 import IntegrityError
|
||||
|
||||
|
@ -46,13 +45,15 @@ class KodiVideoDB(common.KodiDBBase):
|
|||
strContent,
|
||||
strScraper,
|
||||
noUpdate,
|
||||
exclude)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
exclude,
|
||||
allAudio)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
'''
|
||||
self.cursor.execute(query, (path,
|
||||
kind,
|
||||
'metadata.local',
|
||||
1,
|
||||
0,
|
||||
0))
|
||||
|
||||
@db.catch_operationalerrors
|
||||
|
@ -61,9 +62,8 @@ class KodiVideoDB(common.KodiDBBase):
|
|||
Video DB: Adds all subdirectories to path table while setting a "trail"
|
||||
of parent path ids
|
||||
"""
|
||||
parentpath = path_ops.path.abspath(
|
||||
path_ops.path.join(path,
|
||||
path_ops.decode_path(path_ops.path.pardir)))
|
||||
parentpath = path_ops.path.split(path_ops.path.split(path)[0])[0]
|
||||
parentpath = path_ops.append_os_sep(parentpath)
|
||||
pathid = self.get_path(parentpath)
|
||||
if pathid is None:
|
||||
self.cursor.execute('''
|
||||
|
@ -110,11 +110,13 @@ class KodiVideoDB(common.KodiDBBase):
|
|||
idParentPath,
|
||||
strContent,
|
||||
strScraper,
|
||||
noUpdate)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
noUpdate,
|
||||
exclude,
|
||||
allAudio)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''',
|
||||
(path, date_added, id_parent_path, content,
|
||||
scraper, 1))
|
||||
scraper, 1, 0, 0))
|
||||
pathid = self.cursor.lastrowid
|
||||
return pathid
|
||||
|
||||
|
@ -318,7 +320,7 @@ class KodiVideoDB(common.KodiDBBase):
|
|||
for the elmement kodi_id, kodi_type.
|
||||
Will also delete a freshly orphaned actor entry.
|
||||
"""
|
||||
for kind, people_list in people.iteritems():
|
||||
for kind, people_list in people.items():
|
||||
self._add_people_kind(kodi_id, kodi_type, kind, people_list)
|
||||
|
||||
@db.catch_operationalerrors
|
||||
|
@ -363,7 +365,7 @@ class KodiVideoDB(common.KodiDBBase):
|
|||
for kind, people_list in (people if people else
|
||||
{'actor': [],
|
||||
'director': [],
|
||||
'writer': []}).iteritems():
|
||||
'writer': []}).items():
|
||||
self._modify_people_kind(kodi_id, kodi_type, kind, people_list)
|
||||
|
||||
@db.catch_operationalerrors
|
||||
|
@ -479,6 +481,31 @@ class KodiVideoDB(common.KodiDBBase):
|
|||
(kodi_id, kodi_type))
|
||||
return dict(self.cursor.fetchall())
|
||||
|
||||
def get_trailer(self, kodi_id, kodi_type):
|
||||
"""
|
||||
Returns the trailer's URL for kodi_type from the Kodi database or None
|
||||
"""
|
||||
if kodi_type == v.KODI_TYPE_MOVIE:
|
||||
self.cursor.execute('SELECT c19 FROM movie WHERE idMovie=?',
|
||||
(kodi_id, ))
|
||||
else:
|
||||
raise NotImplementedError(f'trailers for {kodi_type} not implemented')
|
||||
try:
|
||||
return self.cursor.fetchone()[0]
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def set_trailer(self, kodi_id, kodi_type, url):
|
||||
"""
|
||||
Writes the trailer's url to the Kodi DB
|
||||
"""
|
||||
if kodi_type == v.KODI_TYPE_MOVIE:
|
||||
self.cursor.execute('UPDATE movie SET c19=? WHERE idMovie=?',
|
||||
(url, kodi_id))
|
||||
else:
|
||||
raise NotImplementedError(f'trailers for {kodi_type} not implemented')
|
||||
|
||||
@db.catch_operationalerrors
|
||||
def modify_streams(self, fileid, streamdetails=None, runtime=None):
|
||||
"""
|
||||
|
@ -575,6 +602,24 @@ class KodiVideoDB(common.KodiDBBase):
|
|||
return
|
||||
return movie_id, typus
|
||||
|
||||
def file_id_from_id(self, kodi_id, kodi_type):
|
||||
"""
|
||||
Returns the Kodi file_id for the item with kodi_id and kodi_type or
|
||||
None
|
||||
"""
|
||||
if kodi_type == v.KODI_TYPE_MOVIE:
|
||||
identifier = 'idMovie'
|
||||
elif kodi_type == v.KODI_TYPE_EPISODE:
|
||||
identifier = 'idEpisode'
|
||||
else:
|
||||
return
|
||||
self.cursor.execute('SELECT idFile FROM %s WHERE %s = ? LIMIT 1'
|
||||
% (kodi_type, identifier), (kodi_id, ))
|
||||
try:
|
||||
return self.cursor.fetchone()[0]
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
def get_resume(self, file_id):
|
||||
"""
|
||||
Returns the first resume point in seconds (int) if found, else None for
|
||||
|
@ -718,15 +763,32 @@ class KodiVideoDB(common.KodiDBBase):
|
|||
self.cursor.execute('DELETE FROM sets WHERE idSet = ?', (set_id,))
|
||||
|
||||
@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,
|
||||
if there already is an entry in the DB
|
||||
"""
|
||||
self.cursor.execute('INSERT INTO seasons(idShow, season) VALUES (?, ?)',
|
||||
(showid, seasonnumber))
|
||||
self.cursor.execute('''
|
||||
INSERT INTO seasons(
|
||||
idShow, season, name, userrating)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (showid, seasonnumber, name, userrating))
|
||||
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
|
||||
def add_uniqueid(self, *args):
|
||||
"""
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
"""
|
||||
PKC Kodi Monitoring implementation
|
||||
"""
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
from json import loads
|
||||
import copy
|
||||
|
@ -14,11 +13,13 @@ import xbmc
|
|||
|
||||
from .plex_api import API
|
||||
from .plex_db import PlexDB
|
||||
from .kodi_db import KodiVideoDB
|
||||
from . import kodi_db
|
||||
from .downloadutils import DownloadUtils as DU
|
||||
from . import utils, timing, plex_functions as PF
|
||||
from . import json_rpc as js, playqueue as PQ, playlist_func as PL
|
||||
from . import backgroundthread, app, variables as v
|
||||
from . import exceptions
|
||||
|
||||
LOG = getLogger('PLEX.kodimonitor')
|
||||
|
||||
|
@ -27,8 +28,10 @@ class KodiMonitor(xbmc.Monitor):
|
|||
"""
|
||||
PKC implementation of the Kodi Monitor class. Invoke only once.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._already_slept = False
|
||||
self._switched_to_plex_streams = True
|
||||
xbmc.Monitor.__init__(self)
|
||||
for playerid in app.PLAYSTATE.player_states:
|
||||
app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
|
||||
|
@ -58,12 +61,15 @@ class KodiMonitor(xbmc.Monitor):
|
|||
Called when a bunch of different stuff happens on the Kodi side
|
||||
"""
|
||||
if data:
|
||||
data = loads(data, 'utf-8')
|
||||
data = loads(data)
|
||||
LOG.debug("Method: %s Data: %s", method, data)
|
||||
|
||||
if method == "Player.OnPlay":
|
||||
with app.APP.lock_playqueues:
|
||||
self.PlayBackStart(data)
|
||||
elif method == 'Player.OnAVChange':
|
||||
with app.APP.lock_playqueues:
|
||||
self._on_av_change(data)
|
||||
elif method == "Player.OnStop":
|
||||
with app.APP.lock_playqueues:
|
||||
_playback_cleanup(ended=data.get('end'))
|
||||
|
@ -82,7 +88,8 @@ class KodiMonitor(xbmc.Monitor):
|
|||
with app.APP.lock_playqueues:
|
||||
self._playlist_onclear(data)
|
||||
elif method == "VideoLibrary.OnUpdate":
|
||||
_videolibrary_onupdate(data)
|
||||
with app.APP.lock_playqueues:
|
||||
_videolibrary_onupdate(data)
|
||||
elif method == "VideoLibrary.OnRemove":
|
||||
pass
|
||||
elif method == "System.OnSleep":
|
||||
|
@ -175,7 +182,7 @@ class KodiMonitor(xbmc.Monitor):
|
|||
try:
|
||||
for i, item in enumerate(items):
|
||||
PL.add_item_to_plex_playqueue(playqueue, i + 1, kodi_item=item)
|
||||
except PL.PlaylistError:
|
||||
except exceptions.PlaylistError:
|
||||
LOG.info('Could not build Plex playlist for: %s', items)
|
||||
|
||||
def _json_item(self, playerid):
|
||||
|
@ -210,7 +217,7 @@ class KodiMonitor(xbmc.Monitor):
|
|||
play_info = json.loads(play_info)
|
||||
app.APP.player.stop()
|
||||
handle = 'RunPlugin(%s)' % play_info.get('handle')
|
||||
xbmc.executebuiltin(handle.encode('utf-8'))
|
||||
xbmc.executebuiltin(handle)
|
||||
|
||||
def PlayBackStart(self, data):
|
||||
"""
|
||||
|
@ -289,7 +296,7 @@ class KodiMonitor(xbmc.Monitor):
|
|||
LOG.debug('Detected different path')
|
||||
try:
|
||||
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')
|
||||
initialize = True
|
||||
else:
|
||||
|
@ -312,7 +319,7 @@ class KodiMonitor(xbmc.Monitor):
|
|||
return
|
||||
try:
|
||||
item = PL.init_plex_playqueue(playqueue, plex_id=plex_id)
|
||||
except PL.PlaylistError:
|
||||
except exceptions.PlaylistError:
|
||||
LOG.info('Could not initialize the Plex playlist')
|
||||
return
|
||||
item.file = path
|
||||
|
@ -335,6 +342,9 @@ class KodiMonitor(xbmc.Monitor):
|
|||
container_key = '/playQueues/%s' % playqueue.id
|
||||
else:
|
||||
container_key = '/library/metadata/%s' % plex_id
|
||||
# Mechanik for Plex skip intro feature
|
||||
if utils.settings('enableSkipIntro') == 'true':
|
||||
status['intro_markers'] = item.api.intro_markers()
|
||||
# Remember the currently playing item
|
||||
app.PLAYSTATE.item = item
|
||||
# Remember that this player has been active
|
||||
|
@ -349,14 +359,50 @@ class KodiMonitor(xbmc.Monitor):
|
|||
status['plex_type'] = plex_type
|
||||
status['playmethod'] = item.playmethod
|
||||
status['playcount'] = item.playcount
|
||||
try:
|
||||
status['external_player'] = app.APP.player.isExternalPlayer() == 1
|
||||
except AttributeError:
|
||||
# Kodi version < 17
|
||||
pass
|
||||
status['external_player'] = app.APP.player.isExternalPlayer() == 1
|
||||
LOG.debug('Set the player state: %s', status)
|
||||
|
||||
# Workaround for the Kodi add-on Up Next
|
||||
if not app.SYNC.direct_paths:
|
||||
_notify_upnext(item)
|
||||
self._switched_to_plex_streams = False
|
||||
|
||||
def _on_av_change(self, data):
|
||||
"""
|
||||
Will be called when Kodi has a video, audio or subtitle stream. Also
|
||||
happens when the stream changes.
|
||||
|
||||
Example data as returned by Kodi:
|
||||
{'item': {'id': 5, 'type': 'movie'},
|
||||
'player': {'playerid': 1, 'speed': 1}}
|
||||
|
||||
PICKING UP CHANGES ON SUBTITLES IS CURRENTLY BROKEN ON THE KODI SIDE!
|
||||
Kodi subs will never change. Also see json_rpc.py
|
||||
"""
|
||||
playerid = data['player']['playerid']
|
||||
if not playerid == v.KODI_VIDEO_PLAYER_ID:
|
||||
# We're just messing with Kodi's videoplayer
|
||||
return
|
||||
item = app.PLAYSTATE.item
|
||||
if item is None:
|
||||
# Player might've quit
|
||||
return
|
||||
if not self._switched_to_plex_streams:
|
||||
# We need to switch to the Plex streams ONCE upon playback start
|
||||
# after onavchange has been fired
|
||||
# Wait a bit because JSON responses won't be ready otherwise
|
||||
if app.APP.monitor.waitForAbort(2):
|
||||
# In case PKC needs to quit
|
||||
return
|
||||
item.init_kodi_streams()
|
||||
item.switch_to_plex_stream('video')
|
||||
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):
|
||||
|
@ -367,6 +413,9 @@ def _playback_cleanup(ended=False):
|
|||
"""
|
||||
LOG.debug('playback_cleanup called. Active players: %s',
|
||||
app.PLAYSTATE.active_players)
|
||||
if app.APP.skip_intro_dialog:
|
||||
app.APP.skip_intro_dialog.close()
|
||||
app.APP.skip_intro_dialog = None
|
||||
# We might have saved a transient token from a user flinging media via
|
||||
# Companion (if we could not use the playqueue to store the token)
|
||||
app.CONN.plex_transient_token = None
|
||||
|
@ -529,11 +578,10 @@ def _next_episode(current_api):
|
|||
current_api.grandparent_title())
|
||||
return
|
||||
try:
|
||||
next_api = API(xml[counter + 1])
|
||||
return API(xml[counter + 1])
|
||||
except IndexError:
|
||||
# Was the last episode
|
||||
return
|
||||
return next_api
|
||||
pass
|
||||
|
||||
|
||||
def _complete_artwork_keys(info):
|
||||
|
@ -559,7 +607,7 @@ def _notify_upnext(item):
|
|||
"""
|
||||
if not item.plex_type == v.PLEX_TYPE_EPISODE:
|
||||
return
|
||||
this_api = API(item.xml)
|
||||
this_api = item.api
|
||||
next_api = _next_episode(this_api)
|
||||
if next_api is None:
|
||||
return
|
||||
|
@ -583,11 +631,11 @@ def _notify_upnext(item):
|
|||
}
|
||||
_complete_artwork_keys(info[key])
|
||||
info['play_info'] = {'handle': next_api.fullpath(force_addon=True)[0]}
|
||||
sender = v.ADDON_ID.encode('utf-8')
|
||||
method = 'upnext_data'.encode('utf-8')
|
||||
data = binascii.hexlify(json.dumps(info))
|
||||
sender = v.ADDON_ID
|
||||
method = 'upnext_data'
|
||||
data = binascii.hexlify(json.dumps(info).encode('utf-8'))
|
||||
data = '\\"[\\"{0}\\"]\\"'.format(data)
|
||||
xbmc.executebuiltin('NotifyAll(%s, %s, %s)' % (sender, method, data))
|
||||
xbmc.executebuiltin(f'NotifyAll({sender}, {method}, {data})')
|
||||
|
||||
|
||||
def _videolibrary_onupdate(data):
|
||||
|
@ -595,17 +643,34 @@ def _videolibrary_onupdate(data):
|
|||
A specific Kodi library item has been updated. This seems to happen if the
|
||||
user marks an item as watched/unwatched or if playback of the item just
|
||||
stopped
|
||||
|
||||
2 kinds of messages possible, e.g.
|
||||
Method: VideoLibrary.OnUpdate Data: ("Reset resume position" and also
|
||||
fired just after stopping playback - BEFORE OnStop fires)
|
||||
{'id': 1, 'type': 'movie'}
|
||||
Method: VideoLibrary.OnUpdate Data: ("Mark as watched")
|
||||
{'item': {'id': 1, 'type': 'movie'}, 'playcount': 1}
|
||||
"""
|
||||
playcount = data.get('playcount')
|
||||
item = data.get('item')
|
||||
if playcount is None or item is None:
|
||||
return
|
||||
item = data.get('item') if 'item' in data else data
|
||||
try:
|
||||
kodi_id = item['id']
|
||||
kodi_type = item['type']
|
||||
except (KeyError, TypeError):
|
||||
LOG.info("Item is invalid for playstate update.")
|
||||
LOG.debug("Item is invalid for a Plex playstate update")
|
||||
return
|
||||
playcount = data.get('playcount')
|
||||
if playcount is None:
|
||||
# "Reset resume position"
|
||||
# Kodi might set as watched or unwatched!
|
||||
with KodiVideoDB(lock=False) as kodidb:
|
||||
file_id = kodidb.file_id_from_id(kodi_id, kodi_type)
|
||||
if file_id is None:
|
||||
return
|
||||
if kodidb.get_resume(file_id):
|
||||
# We do have an existing bookmark entry - not toggling to
|
||||
# either watched or unwatched on the Plex side
|
||||
return
|
||||
playcount = kodidb.get_playcount(file_id) or 0
|
||||
if app.PLAYSTATE.item and kodi_id == app.PLAYSTATE.item.kodi_id and \
|
||||
kodi_type == app.PLAYSTATE.item.kodi_type:
|
||||
# Kodi updates an item immediately after playback. Hence we do NOT
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
|
||||
from .full_sync import start
|
||||
from .websocket import store_websocket_message, process_websocket_messages, \
|
||||
WEBSOCKET_MESSAGES, PLAYSTATE_SESSIONS
|
||||
from .common import update_kodi_library, PLAYLIST_SYNC_ENABLED
|
||||
from .fanart import FanartThread, FanartTask
|
||||
from .additional_metadata import MetadataThread, ProcessMetadataTask
|
||||
from .sections import force_full_sync, delete_files, clear_window_vars
|
||||
|
|
112
resources/lib/library_sync/additional_metadata.py
Normal file
112
resources/lib/library_sync/additional_metadata.py
Normal file
|
@ -0,0 +1,112 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from logging import getLogger
|
||||
|
||||
from . import additional_metadata_tmdb
|
||||
from ..plex_db import PlexDB
|
||||
from .. import backgroundthread, utils
|
||||
from .. import variables as v, app
|
||||
from ..exceptions import ProcessingNotDone
|
||||
|
||||
|
||||
logger = getLogger('PLEX.sync.metadata')
|
||||
|
||||
BATCH_SIZE = 500
|
||||
|
||||
SUPPORTED_METADATA = {
|
||||
v.PLEX_TYPE_MOVIE: (
|
||||
('missing_trailers', additional_metadata_tmdb.process_trailers),
|
||||
('missing_fanart', additional_metadata_tmdb.process_fanart),
|
||||
),
|
||||
v.PLEX_TYPE_SHOW: (
|
||||
('missing_fanart', additional_metadata_tmdb.process_fanart),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def processing_is_activated(item_getter):
|
||||
"""Checks the PKC settings whether processing is even activated."""
|
||||
if item_getter == 'missing_fanart':
|
||||
return utils.settings('FanartTV') == 'true'
|
||||
return True
|
||||
|
||||
|
||||
class MetadataThread(backgroundthread.KillableThread):
|
||||
"""This will potentially take hours!"""
|
||||
def __init__(self, callback, refresh=False):
|
||||
self.callback = callback
|
||||
self.refresh = refresh
|
||||
super(MetadataThread, self).__init__()
|
||||
|
||||
def should_suspend(self):
|
||||
return self._suspended or app.APP.is_playing_video
|
||||
|
||||
def _process_in_batches(self, item_getter, processor, plex_type):
|
||||
offset = 0
|
||||
while True:
|
||||
with PlexDB() as plexdb:
|
||||
# Keep DB connection open only for a short period of time!
|
||||
if self.refresh:
|
||||
# Simply grab every single item if we want to refresh
|
||||
func = plexdb.every_plex_id
|
||||
else:
|
||||
func = getattr(plexdb, item_getter)
|
||||
batch = list(func(plex_type, offset, BATCH_SIZE))
|
||||
for plex_id in batch:
|
||||
# Do the actual, time-consuming processing
|
||||
if self.should_suspend() or self.should_cancel():
|
||||
raise ProcessingNotDone()
|
||||
processor(plex_id, plex_type, self.refresh)
|
||||
if len(batch) < BATCH_SIZE:
|
||||
break
|
||||
offset += BATCH_SIZE
|
||||
|
||||
def _loop(self):
|
||||
for plex_type in SUPPORTED_METADATA:
|
||||
for item_getter, processor in SUPPORTED_METADATA[plex_type]:
|
||||
if not processing_is_activated(item_getter):
|
||||
continue
|
||||
self._process_in_batches(item_getter, processor, plex_type)
|
||||
|
||||
def _run(self):
|
||||
finished = False
|
||||
while not finished:
|
||||
try:
|
||||
self._loop()
|
||||
except ProcessingNotDone:
|
||||
finished = False
|
||||
else:
|
||||
finished = True
|
||||
if self.wait_while_suspended():
|
||||
break
|
||||
logger.info('MetadataThread finished completely: %s', finished)
|
||||
self.callback(finished)
|
||||
|
||||
def run(self):
|
||||
logger.info('Starting MetadataThread')
|
||||
app.APP.register_metadata_thread(self)
|
||||
try:
|
||||
self._run()
|
||||
except Exception:
|
||||
utils.ERROR(notify=True)
|
||||
finally:
|
||||
app.APP.deregister_metadata_thread(self)
|
||||
|
||||
|
||||
class ProcessMetadataTask(backgroundthread.Task):
|
||||
"""This task will also be executed while library sync is suspended!"""
|
||||
def setup(self, plex_id, plex_type, refresh=False):
|
||||
self.plex_id = plex_id
|
||||
self.plex_type = plex_type
|
||||
self.refresh = refresh
|
||||
|
||||
def run(self):
|
||||
if self.plex_type not in SUPPORTED_METADATA:
|
||||
return
|
||||
for item_getter, processor in SUPPORTED_METADATA[self.plex_type]:
|
||||
if self.should_cancel():
|
||||
# Just don't process this item at all. Next full sync will
|
||||
# take care of it
|
||||
return
|
||||
if not processing_is_activated(item_getter):
|
||||
continue
|
||||
processor(self.plex_id, self.plex_type, self.refresh)
|
159
resources/lib/library_sync/additional_metadata_tmdb.py
Normal file
159
resources/lib/library_sync/additional_metadata_tmdb.py
Normal file
|
@ -0,0 +1,159 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import xbmcvfs
|
||||
import xbmcaddon
|
||||
|
||||
from ..plex_api import API
|
||||
from ..kodi_db import KodiVideoDB
|
||||
from ..plex_db import PlexDB
|
||||
from .. import itemtypes, plex_functions as PF, utils, variables as v
|
||||
|
||||
# Import the existing Kodi add-on metadata.themoviedb.org.python
|
||||
__ADDON__ = xbmcaddon.Addon(id='metadata.themoviedb.org.python')
|
||||
__TEMP_PATH__ = os.path.join(__ADDON__.getAddonInfo('path'), 'python', 'lib')
|
||||
__BASE__ = xbmcvfs.translatePath(__TEMP_PATH__)
|
||||
sys.path.append(__BASE__)
|
||||
import tmdbscraper.tmdb as tmdb
|
||||
|
||||
logger = logging.getLogger('PLEX.metadata_movies')
|
||||
PREFER_KODI_COLLECTION_ART = utils.settings('PreferKodiCollectionArt') == 'false'
|
||||
TMDB_SUPPORTED_IDS = ('tmdb', 'imdb')
|
||||
|
||||
|
||||
def get_tmdb_scraper(settings):
|
||||
language = settings.getSettingString('language')
|
||||
certcountry = settings.getSettingString('tmdbcertcountry')
|
||||
# Simplify this in the future
|
||||
# See https://github.com/croneter/PlexKodiConnect/issues/1657
|
||||
search_language = settings.getSettingString('searchlanguage')
|
||||
if search_language:
|
||||
return tmdb.TMDBMovieScraper(settings, language, certcountry, search_language)
|
||||
else:
|
||||
return tmdb.TMDBMovieScraper(settings, language, certcountry)
|
||||
|
||||
|
||||
def get_tmdb_details(unique_ids):
|
||||
settings = xbmcaddon.Addon(id='metadata.themoviedb.org.python')
|
||||
details = get_tmdb_scraper(settings).get_details(unique_ids)
|
||||
if 'error' in details:
|
||||
logger.debug('Could not get tmdb details for %s. Error: %s',
|
||||
unique_ids, details)
|
||||
return details
|
||||
|
||||
|
||||
def process_trailers(plex_id, plex_type, refresh=False):
|
||||
done = True
|
||||
try:
|
||||
with PlexDB() as plexdb:
|
||||
db_item = plexdb.item_by_id(plex_id, plex_type)
|
||||
if not db_item:
|
||||
logger.error('Could not get Kodi id for %s %s', plex_type, plex_id)
|
||||
done = False
|
||||
return
|
||||
with KodiVideoDB() as kodidb:
|
||||
trailer = kodidb.get_trailer(db_item['kodi_id'],
|
||||
db_item['kodi_type'])
|
||||
if trailer and (trailer.startswith(f'plugin://{v.ADDON_ID}') or
|
||||
not refresh):
|
||||
# No need to get a trailer
|
||||
return
|
||||
logger.debug('Processing trailer for %s %s', plex_type, plex_id)
|
||||
xml = PF.GetPlexMetadata(plex_id)
|
||||
try:
|
||||
xml[0].attrib
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
logger.warn('Could not get metadata for %s. Skipping that %s '
|
||||
'for now', plex_id, plex_type)
|
||||
done = False
|
||||
return
|
||||
api = API(xml[0])
|
||||
if (not api.guids or
|
||||
not [x for x in api.guids if x in TMDB_SUPPORTED_IDS]):
|
||||
logger.debug('No unique ids found for %s %s, cannot get a trailer',
|
||||
plex_type, api.title())
|
||||
return
|
||||
trailer = get_tmdb_details(api.guids)
|
||||
trailer = trailer.get('info', {}).get('trailer')
|
||||
if trailer:
|
||||
with KodiVideoDB() as kodidb:
|
||||
kodidb.set_trailer(db_item['kodi_id'],
|
||||
db_item['kodi_type'],
|
||||
trailer)
|
||||
logger.debug('Found a new trailer for %s %s: %s',
|
||||
plex_type, api.title(), trailer)
|
||||
else:
|
||||
logger.debug('No trailer found for %s %s', plex_type, api.title())
|
||||
finally:
|
||||
if done is True:
|
||||
with PlexDB() as plexdb:
|
||||
plexdb.set_trailer_synced(plex_id, plex_type)
|
||||
|
||||
|
||||
def process_fanart(plex_id, plex_type, refresh=False):
|
||||
"""
|
||||
Will look for additional fanart for the plex_type item with plex_id.
|
||||
Will check if we already got all artwork and only look if some are indeed
|
||||
missing.
|
||||
Will set the fanart_synced flag in the Plex DB if successful.
|
||||
"""
|
||||
done = True
|
||||
try:
|
||||
artworks = None
|
||||
with PlexDB() as plexdb:
|
||||
db_item = plexdb.item_by_id(plex_id, plex_type)
|
||||
if not db_item:
|
||||
logger.error('Could not get Kodi id for %s %s', plex_type, plex_id)
|
||||
done = False
|
||||
return
|
||||
if not refresh:
|
||||
with KodiVideoDB() as kodidb:
|
||||
artworks = kodidb.get_art(db_item['kodi_id'],
|
||||
db_item['kodi_type'])
|
||||
# Check if we even need to get additional art
|
||||
for key in v.ALL_KODI_ARTWORK:
|
||||
if key not in artworks:
|
||||
break
|
||||
else:
|
||||
return
|
||||
xml = PF.GetPlexMetadata(plex_id)
|
||||
try:
|
||||
xml[0].attrib
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
logger.debug('Could not get metadata for %s %s. Skipping that '
|
||||
'item for now', plex_type, plex_id)
|
||||
done = False
|
||||
return
|
||||
api = API(xml[0])
|
||||
if artworks is None:
|
||||
artworks = api.artwork()
|
||||
# Get additional missing artwork from fanart artwork sites
|
||||
artworks = api.fanart_artwork(artworks)
|
||||
with itemtypes.ITEMTYPE_FROM_PLEXTYPE[plex_type](None) as context:
|
||||
context.set_fanart(artworks,
|
||||
db_item['kodi_id'],
|
||||
db_item['kodi_type'])
|
||||
# Additional fanart for sets/collections
|
||||
if plex_type == v.PLEX_TYPE_MOVIE:
|
||||
for _, setname in api.collections():
|
||||
logger.debug('Getting artwork for movie set %s', setname)
|
||||
with KodiVideoDB() as kodidb:
|
||||
setid = kodidb.create_collection(setname)
|
||||
external_set_artwork = api.set_artwork()
|
||||
if external_set_artwork and PREFER_KODI_COLLECTION_ART:
|
||||
kodi_artwork = api.artwork(kodi_id=setid,
|
||||
kodi_type=v.KODI_TYPE_SET)
|
||||
for art in kodi_artwork:
|
||||
if art in external_set_artwork:
|
||||
del external_set_artwork[art]
|
||||
with itemtypes.Movie(None) as movie:
|
||||
movie.kodidb.modify_artwork(external_set_artwork,
|
||||
setid,
|
||||
v.KODI_TYPE_SET)
|
||||
finally:
|
||||
if done is True:
|
||||
with PlexDB() as plexdb:
|
||||
plexdb.set_fanart_synced(plex_id, plex_type)
|
|
@ -1,6 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import xbmc
|
||||
|
||||
|
|
|
@ -1,154 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from ..plex_api import API
|
||||
from ..plex_db import PlexDB
|
||||
from ..kodi_db import KodiVideoDB
|
||||
from .. import backgroundthread, utils
|
||||
from .. import itemtypes, plex_functions as PF, variables as v, app
|
||||
|
||||
|
||||
LOG = getLogger('PLEX.sync.fanart')
|
||||
|
||||
SUPPORTED_TYPES = (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)
|
||||
SYNC_FANART = (utils.settings('FanartTV') == 'true' and
|
||||
utils.settings('usePlexArtwork') == 'true')
|
||||
PREFER_KODI_COLLECTION_ART = utils.settings('PreferKodiCollectionArt') == 'false'
|
||||
BATCH_SIZE = 500
|
||||
|
||||
|
||||
class FanartThread(backgroundthread.KillableThread):
|
||||
"""
|
||||
This will potentially take hours!
|
||||
"""
|
||||
def __init__(self, callback, refresh=False):
|
||||
self.callback = callback
|
||||
self.refresh = refresh
|
||||
super(FanartThread, self).__init__()
|
||||
|
||||
def should_suspend(self):
|
||||
return self._suspended or app.APP.is_playing_video
|
||||
|
||||
def run(self):
|
||||
LOG.info('Starting FanartThread')
|
||||
app.APP.register_fanart_thread(self)
|
||||
try:
|
||||
self._run()
|
||||
except Exception:
|
||||
utils.ERROR(notify=True)
|
||||
finally:
|
||||
app.APP.deregister_fanart_thread(self)
|
||||
|
||||
def _loop(self):
|
||||
for typus in SUPPORTED_TYPES:
|
||||
offset = 0
|
||||
while True:
|
||||
with PlexDB() as plexdb:
|
||||
# Keep DB connection open only for a short period of time!
|
||||
if self.refresh:
|
||||
batch = list(plexdb.every_plex_id(typus,
|
||||
offset,
|
||||
BATCH_SIZE))
|
||||
else:
|
||||
batch = list(plexdb.missing_fanart(typus,
|
||||
offset,
|
||||
BATCH_SIZE))
|
||||
for plex_id in batch:
|
||||
# Do the actual, time-consuming processing
|
||||
if self.should_suspend() or self.should_cancel():
|
||||
return False
|
||||
process_fanart(plex_id, typus, self.refresh)
|
||||
if len(batch) < BATCH_SIZE:
|
||||
break
|
||||
offset += BATCH_SIZE
|
||||
return True
|
||||
|
||||
def _run(self):
|
||||
finished = False
|
||||
while not finished:
|
||||
finished = self._loop()
|
||||
if self.wait_while_suspended():
|
||||
break
|
||||
LOG.info('FanartThread finished: %s', finished)
|
||||
self.callback(finished)
|
||||
|
||||
|
||||
class FanartTask(backgroundthread.Task):
|
||||
"""
|
||||
This task will also be executed while library sync is suspended!
|
||||
"""
|
||||
def setup(self, plex_id, plex_type, refresh=False):
|
||||
self.plex_id = plex_id
|
||||
self.plex_type = plex_type
|
||||
self.refresh = refresh
|
||||
|
||||
def run(self):
|
||||
process_fanart(self.plex_id, self.plex_type, self.refresh)
|
||||
|
||||
|
||||
def process_fanart(plex_id, plex_type, refresh=False):
|
||||
"""
|
||||
Will look for additional fanart for the plex_type item with plex_id.
|
||||
Will check if we already got all artwork and only look if some are indeed
|
||||
missing.
|
||||
Will set the fanart_synced flag in the Plex DB if successful.
|
||||
"""
|
||||
done = False
|
||||
try:
|
||||
artworks = None
|
||||
with PlexDB() as plexdb:
|
||||
db_item = plexdb.item_by_id(plex_id,
|
||||
plex_type)
|
||||
if not db_item:
|
||||
LOG.error('Could not get Kodi id for plex id %s', plex_id)
|
||||
return
|
||||
if not refresh:
|
||||
with KodiVideoDB() as kodidb:
|
||||
artworks = kodidb.get_art(db_item['kodi_id'],
|
||||
db_item['kodi_type'])
|
||||
# Check if we even need to get additional art
|
||||
for key in v.ALL_KODI_ARTWORK:
|
||||
if key not in artworks:
|
||||
break
|
||||
else:
|
||||
done = True
|
||||
return
|
||||
xml = PF.GetPlexMetadata(plex_id)
|
||||
try:
|
||||
xml[0].attrib
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
LOG.warn('Could not get metadata for %s. Skipping that item '
|
||||
'for now', plex_id)
|
||||
return
|
||||
api = API(xml[0])
|
||||
if artworks is None:
|
||||
artworks = api.artwork()
|
||||
# Get additional missing artwork from fanart artwork sites
|
||||
artworks = api.fanart_artwork(artworks)
|
||||
with itemtypes.ITEMTYPE_FROM_PLEXTYPE[plex_type](None) as context:
|
||||
context.set_fanart(artworks,
|
||||
db_item['kodi_id'],
|
||||
db_item['kodi_type'])
|
||||
# Additional fanart for sets/collections
|
||||
if plex_type == v.PLEX_TYPE_MOVIE:
|
||||
for _, setname in api.collections():
|
||||
LOG.debug('Getting artwork for movie set %s', setname)
|
||||
with KodiVideoDB() as kodidb:
|
||||
setid = kodidb.create_collection(setname)
|
||||
external_set_artwork = api.set_artwork()
|
||||
if external_set_artwork and PREFER_KODI_COLLECTION_ART:
|
||||
kodi_artwork = api.artwork(kodi_id=setid,
|
||||
kodi_type=v.KODI_TYPE_SET)
|
||||
for art in kodi_artwork:
|
||||
if art in external_set_artwork:
|
||||
del external_set_artwork[art]
|
||||
with itemtypes.Movie(None) as movie:
|
||||
movie.kodidb.modify_artwork(external_set_artwork,
|
||||
setid,
|
||||
v.KODI_TYPE_SET)
|
||||
done = True
|
||||
finally:
|
||||
if done is True:
|
||||
with PlexDB() as plexdb:
|
||||
plexdb.set_fanart_synced(plex_id, plex_type)
|
|
@ -1,7 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
from Queue import Full
|
||||
from queue import Full
|
||||
|
||||
from . import common, sections
|
||||
from ..plex_db import PlexDB
|
||||
|
@ -41,11 +40,15 @@ class FillMetadataQueue(common.LibrarySyncMixin,
|
|||
plex_id = int(xml.get('ratingKey'))
|
||||
checksum = int('{}{}'.format(
|
||||
plex_id,
|
||||
xml.get('updatedAt',
|
||||
xml.get('addedAt', '1541572987')).replace('-', '')))
|
||||
abs(int(xml.get('updatedAt',
|
||||
xml.get('addedAt', '1541572987'))))))
|
||||
if (not self.repair and
|
||||
plexdb.checksum(plex_id, section.plex_type) == checksum):
|
||||
continue
|
||||
if not do_process_section:
|
||||
do_process_section = True
|
||||
self.processing_queue.add_section(section)
|
||||
LOG.debug('Put section in processing queue: %s', section)
|
||||
try:
|
||||
self.get_metadata_queue.put((count, plex_id, section),
|
||||
timeout=QUEUE_TIMEOUT)
|
||||
|
@ -54,16 +57,14 @@ class FillMetadataQueue(common.LibrarySyncMixin,
|
|||
'aborting sync now', plex_id)
|
||||
section.sync_successful = False
|
||||
break
|
||||
count += 1
|
||||
if not do_process_section:
|
||||
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)
|
||||
else:
|
||||
count += 1
|
||||
# We might have received LESS items from the PMS than anticipated.
|
||||
# Ensures that our queues finish
|
||||
LOG.debug('%s items to process for section %s', count, section)
|
||||
section.number_of_items = count
|
||||
self.processing_queue.change_section_number_of_items(section,
|
||||
count)
|
||||
LOG.debug('%s items to process for section %s',
|
||||
section.number_of_items, section)
|
||||
|
||||
def _run(self):
|
||||
while not self.should_cancel():
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import Queue
|
||||
import queue
|
||||
|
||||
import xbmcgui
|
||||
|
||||
|
@ -80,7 +79,7 @@ class FullSync(common.LibrarySyncMixin, bg.KillableThread):
|
|||
@utils.log_time
|
||||
def process_new_and_changed_items(self, section_queue, processing_queue):
|
||||
LOG.debug('Start working')
|
||||
get_metadata_queue = Queue.Queue(maxsize=BACKLOG_QUEUE_SIZE)
|
||||
get_metadata_queue = queue.Queue(maxsize=BACKLOG_QUEUE_SIZE)
|
||||
scanner_thread = FillMetadataQueue(self.repair,
|
||||
section_queue,
|
||||
get_metadata_queue,
|
||||
|
@ -137,7 +136,7 @@ class FullSync(common.LibrarySyncMixin, bg.KillableThread):
|
|||
LOG.error('Could not entirely process section %s', section)
|
||||
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
|
||||
"""
|
||||
|
@ -154,17 +153,28 @@ class FullSync(common.LibrarySyncMixin, bg.KillableThread):
|
|||
continue
|
||||
section = sections.get_sync_section(section,
|
||||
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
|
||||
else:
|
||||
updated_at = section.last_sync - UPDATED_AT_SAFETY \
|
||||
if section.last_sync else None
|
||||
last_viewed_at = None
|
||||
elif items == 'watched':
|
||||
if not timestamp:
|
||||
# No need to sync playstate updates since section
|
||||
# has not yet been synched
|
||||
continue
|
||||
else:
|
||||
updated_at = None
|
||||
last_viewed_at = timestamp
|
||||
elif items == 'updated':
|
||||
updated_at = timestamp
|
||||
last_viewed_at = None
|
||||
try:
|
||||
section.iterator = PF.get_section_iterator(
|
||||
section.section_id,
|
||||
plex_type=section.plex_type,
|
||||
updated_at=updated_at,
|
||||
last_viewed_at=None)
|
||||
last_viewed_at=last_viewed_at)
|
||||
except RuntimeError:
|
||||
LOG.error('Sync at least partially unsuccessful!')
|
||||
LOG.error('Error getting section iterator %s', section)
|
||||
|
@ -182,7 +192,7 @@ class FullSync(common.LibrarySyncMixin, bg.KillableThread):
|
|||
LOG.debug('Exiting threaded_get_generators')
|
||||
|
||||
def full_library_sync(self):
|
||||
section_queue = Queue.Queue()
|
||||
section_queue = queue.Queue()
|
||||
processing_queue = bg.ProcessingQueue(maxsize=XML_QUEUE_SIZE)
|
||||
kinds = [
|
||||
(v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_MOVIE),
|
||||
|
@ -195,19 +205,42 @@ class FullSync(common.LibrarySyncMixin, bg.KillableThread):
|
|||
(v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_ARTIST),
|
||||
(v.PLEX_TYPE_ALBUM, v.PLEX_TYPE_ARTIST),
|
||||
])
|
||||
|
||||
# ADD NEW ITEMS
|
||||
# We need to enforce syncing e.g. show before season before episode
|
||||
bg.FunctionAsTask(self.threaded_get_generators,
|
||||
None,
|
||||
kinds, section_queue, False).start()
|
||||
kinds,
|
||||
section_queue,
|
||||
items='all' if self.repair else 'updated').start()
|
||||
# Do the heavy lifting
|
||||
self.process_new_and_changed_items(section_queue, processing_queue)
|
||||
common.update_kodi_library(video=True, music=True)
|
||||
if self.should_cancel() or not self.successful:
|
||||
return
|
||||
|
||||
# In order to not delete all your songs again for playstate synch
|
||||
if app.SYNC.enable_music:
|
||||
kinds.extend([
|
||||
(v.PLEX_TYPE_SONG, v.PLEX_TYPE_ARTIST),
|
||||
])
|
||||
|
||||
# Update playstate progress since last sync - especially useful for
|
||||
# users of very large libraries since this step is very fast
|
||||
# These playstates will be synched twice
|
||||
LOG.debug('Start synching playstate for last watched items')
|
||||
bg.FunctionAsTask(self.threaded_get_generators,
|
||||
None,
|
||||
kinds,
|
||||
section_queue,
|
||||
items='watched').start()
|
||||
self.processing_loop_playstates(section_queue)
|
||||
if self.should_cancel() or not self.successful:
|
||||
return
|
||||
|
||||
# Sync Plex playlists to Kodi and vice-versa
|
||||
if common.PLAYLIST_SYNC_ENABLED:
|
||||
LOG.debug('Start playlist sync')
|
||||
if self.show_dialog:
|
||||
if self.dialog:
|
||||
self.dialog.close()
|
||||
|
@ -218,14 +251,9 @@ class FullSync(common.LibrarySyncMixin, bg.KillableThread):
|
|||
return
|
||||
|
||||
# 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
|
||||
# to delete the ones still in Kodi
|
||||
# were set to unwatched or changed user ratings). Also mark all items on
|
||||
# the PMS to be able to delete the ones still in Kodi
|
||||
LOG.debug('Start synching playstate and userdata for every item')
|
||||
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
|
||||
if not self.show_dialog_userdata and self.dialog:
|
||||
# Close the progress indicator dialog
|
||||
|
@ -233,7 +261,9 @@ class FullSync(common.LibrarySyncMixin, bg.KillableThread):
|
|||
self.dialog = None
|
||||
bg.FunctionAsTask(self.threaded_get_generators,
|
||||
None,
|
||||
kinds, section_queue, True).start()
|
||||
kinds,
|
||||
section_queue,
|
||||
items='all').start()
|
||||
self.processing_loop_playstates(section_queue)
|
||||
if self.should_cancel() or not self.successful:
|
||||
return
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from . import common
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
import urllib
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
import copy
|
||||
|
||||
from ..utils import etree
|
||||
import xml.etree.ElementTree as etree
|
||||
from .. import variables as v, utils
|
||||
|
||||
ICON_PATH = 'special://home/addons/plugin.video.plexkodiconnect/icon.png'
|
||||
|
@ -56,7 +55,7 @@ NODE_TYPES = {
|
|||
{
|
||||
'mode': 'browseplex',
|
||||
'key': ('/library/sections/{self.section_id}&%s'
|
||||
% urllib.urlencode({'sort': 'rating:desc'})),
|
||||
% urllib.parse.urlencode({'sort': 'rating:desc'})),
|
||||
'section_id': '{self.section_id}'
|
||||
},
|
||||
v.CONTENT_TYPE_MOVIE,
|
||||
|
@ -84,7 +83,7 @@ NODE_TYPES = {
|
|||
{
|
||||
'mode': 'browseplex',
|
||||
'key': ('/library/sections/{self.section_id}&%s'
|
||||
% urllib.urlencode({'sort': 'random'})),
|
||||
% urllib.parse.urlencode({'sort': 'random'})),
|
||||
'section_id': '{self.section_id}'
|
||||
},
|
||||
v.CONTENT_TYPE_MOVIE,
|
||||
|
@ -154,7 +153,7 @@ NODE_TYPES = {
|
|||
{
|
||||
'mode': 'browseplex',
|
||||
'key': ('/library/sections/{self.section_id}&%s'
|
||||
% urllib.urlencode({'sort': 'rating:desc'})),
|
||||
% urllib.parse.urlencode({'sort': 'rating:desc'})),
|
||||
'section_id': '{self.section_id}'
|
||||
},
|
||||
v.CONTENT_TYPE_SHOW,
|
||||
|
@ -182,7 +181,7 @@ NODE_TYPES = {
|
|||
{
|
||||
'mode': 'browseplex',
|
||||
'key': ('/library/sections/{self.section_id}&%s'
|
||||
% urllib.urlencode({'sort': 'random'})),
|
||||
% urllib.parse.urlencode({'sort': 'random'})),
|
||||
'section_id': '{self.section_id}'
|
||||
},
|
||||
v.CONTENT_TYPE_SHOW,
|
||||
|
@ -192,7 +191,7 @@ NODE_TYPES = {
|
|||
{
|
||||
'mode': 'browseplex',
|
||||
'key': ('/library/sections/{self.section_id}/recentlyViewed&%s'
|
||||
% urllib.urlencode({'type': v.PLEX_TYPE_NUMBER_FROM_PLEX_TYPE[v.PLEX_TYPE_EPISODE]})),
|
||||
% urllib.parse.urlencode({'type': v.PLEX_TYPE_NUMBER_FROM_PLEX_TYPE[v.PLEX_TYPE_EPISODE]})),
|
||||
'section_id': '{self.section_id}'
|
||||
},
|
||||
v.CONTENT_TYPE_EPISODE,
|
||||
|
@ -236,7 +235,7 @@ def node_pms(section, node_name, args):
|
|||
else:
|
||||
folder = False
|
||||
xml = etree.Element('node',
|
||||
attrib={'order': unicode(section.order),
|
||||
attrib={'order': str(section.order),
|
||||
'type': 'folder' if folder else 'filter'})
|
||||
etree.SubElement(xml, 'label').text = node_name
|
||||
etree.SubElement(xml, 'icon').text = ICON_PATH
|
||||
|
@ -249,7 +248,7 @@ def node_ondeck(section, node_name):
|
|||
"""
|
||||
For movies only - returns in-progress movies sorted by last played
|
||||
"""
|
||||
xml = etree.Element('node', attrib={'order': unicode(section.order),
|
||||
xml = etree.Element('node', attrib={'order': str(section.order),
|
||||
'type': 'filter'})
|
||||
etree.SubElement(xml, 'match').text = 'all'
|
||||
rule = etree.SubElement(xml, 'rule', attrib={'field': 'tag',
|
||||
|
@ -270,7 +269,7 @@ def node_ondeck(section, node_name):
|
|||
|
||||
def node_recent(section, node_name):
|
||||
xml = etree.Element('node',
|
||||
attrib={'order': unicode(section.order),
|
||||
attrib={'order': str(section.order),
|
||||
'type': 'filter'})
|
||||
etree.SubElement(xml, 'match').text = 'all'
|
||||
rule = etree.SubElement(xml, 'rule', attrib={'field': 'tag',
|
||||
|
@ -299,7 +298,7 @@ def node_recent(section, node_name):
|
|||
|
||||
|
||||
def node_all(section, node_name):
|
||||
xml = etree.Element('node', attrib={'order': unicode(section.order),
|
||||
xml = etree.Element('node', attrib={'order': str(section.order),
|
||||
'type': 'filter'})
|
||||
etree.SubElement(xml, 'match').text = 'all'
|
||||
rule = etree.SubElement(xml, 'rule', attrib={'field': 'tag',
|
||||
|
@ -316,7 +315,7 @@ def node_all(section, node_name):
|
|||
|
||||
|
||||
def node_recommended(section, node_name):
|
||||
xml = etree.Element('node', attrib={'order': unicode(section.order),
|
||||
xml = etree.Element('node', attrib={'order': str(section.order),
|
||||
'type': 'filter'})
|
||||
etree.SubElement(xml, 'match').text = 'all'
|
||||
rule = etree.SubElement(xml, 'rule', attrib={'field': 'tag',
|
||||
|
@ -337,7 +336,7 @@ def node_recommended(section, node_name):
|
|||
|
||||
|
||||
def node_genres(section, node_name):
|
||||
xml = etree.Element('node', attrib={'order': unicode(section.order),
|
||||
xml = etree.Element('node', attrib={'order': str(section.order),
|
||||
'type': 'filter'})
|
||||
etree.SubElement(xml, 'match').text = 'all'
|
||||
rule = etree.SubElement(xml, 'rule', attrib={'field': 'tag',
|
||||
|
@ -355,7 +354,7 @@ def node_genres(section, node_name):
|
|||
|
||||
|
||||
def node_sets(section, node_name):
|
||||
xml = etree.Element('node', attrib={'order': unicode(section.order),
|
||||
xml = etree.Element('node', attrib={'order': str(section.order),
|
||||
'type': 'filter'})
|
||||
etree.SubElement(xml, 'match').text = 'all'
|
||||
rule = etree.SubElement(xml, 'rule', attrib={'field': 'tag',
|
||||
|
@ -374,7 +373,7 @@ def node_sets(section, node_name):
|
|||
|
||||
|
||||
def node_random(section, node_name):
|
||||
xml = etree.Element('node', attrib={'order': unicode(section.order),
|
||||
xml = etree.Element('node', attrib={'order': str(section.order),
|
||||
'type': 'filter'})
|
||||
etree.SubElement(xml, 'match').text = 'all'
|
||||
rule = etree.SubElement(xml, 'rule', attrib={'field': 'tag',
|
||||
|
@ -392,7 +391,7 @@ def node_random(section, node_name):
|
|||
|
||||
|
||||
def node_lastplayed(section, node_name):
|
||||
xml = etree.Element('node', attrib={'order': unicode(section.order),
|
||||
xml = etree.Element('node', attrib={'order': str(section.order),
|
||||
'type': 'filter'})
|
||||
etree.SubElement(xml, 'match').text = 'all'
|
||||
rule = etree.SubElement(xml, 'rule', attrib={'field': 'tag',
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from . import common, sections
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import copy
|
||||
|
||||
|
@ -10,7 +9,7 @@ from ..plex_api import API
|
|||
from .. import kodi_db
|
||||
from .. import itemtypes, path_ops
|
||||
from .. import plex_functions as PF, music, utils, variables as v, app
|
||||
from ..utils import etree
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
LOG = getLogger('PLEX.sync.sections')
|
||||
|
||||
|
@ -21,10 +20,10 @@ SHOULD_CANCEL = None
|
|||
LIBRARY_PATH = path_ops.translate_path('special://profile/library/video/')
|
||||
# The video library might not yet exist for this user - create it
|
||||
if not path_ops.exists(LIBRARY_PATH):
|
||||
path_ops.copy_tree(
|
||||
path_ops.copytree(
|
||||
src=path_ops.translate_path('special://xbmc/system/library/video'),
|
||||
dst=LIBRARY_PATH,
|
||||
preserve_mode=0) # dont copy permission bits so we have write access!
|
||||
copy_function=path_ops.shutil.copyfile)
|
||||
PLAYLISTS_PATH = path_ops.translate_path("special://profile/playlists/video/")
|
||||
if not path_ops.exists(PLAYLISTS_PATH):
|
||||
path_ops.makedirs(PLAYLISTS_PATH)
|
||||
|
@ -93,21 +92,22 @@ class Section(object):
|
|||
"'name': '{self.name}', "
|
||||
"'section_id': {self.section_id}, "
|
||||
"'section_type': '{self.section_type}', "
|
||||
"'plex_type': '{self.plex_type}', "
|
||||
"'sync_to_kodi': {self.sync_to_kodi}, "
|
||||
"'last_sync': {self.last_sync}"
|
||||
"}}").format(self=self).encode('utf-8')
|
||||
__str__ = __repr__
|
||||
"}}").format(self=self)
|
||||
|
||||
def __nonzero__(self):
|
||||
def __bool__(self):
|
||||
"""bool(Section) returns True if section_id, name and section_type are set."""
|
||||
return (self.section_id is not None and
|
||||
self.name is not None and
|
||||
self.section_type is not None)
|
||||
|
||||
def __eq__(self, section):
|
||||
"""Sections compare equal if their section_id, name and plex_type (first prio) OR section_type (if there is no plex_type is set) compare equal.
|
||||
"""
|
||||
Sections compare equal if their section_id, name and plex_type (first
|
||||
prio) OR section_type (if there is no plex_type is set) compare equal
|
||||
"""
|
||||
if not isinstance(section, Section):
|
||||
return False
|
||||
return (self.section_id == section.section_id and
|
||||
self.name == section.name and
|
||||
(self.plex_type == section.plex_type if self.plex_type else
|
||||
|
@ -236,7 +236,7 @@ class Section(object):
|
|||
{key: '{self.<Section attribute>}'}
|
||||
"""
|
||||
args = copy.deepcopy(args)
|
||||
for key, value in args.iteritems():
|
||||
for key, value in args.items():
|
||||
args[key] = value.format(self=self)
|
||||
return utils.extend_url('plugin://%s' % v.ADDON_ID, args)
|
||||
|
||||
|
@ -265,7 +265,7 @@ class Section(object):
|
|||
args = {
|
||||
'mode': 'browseplex',
|
||||
'key': '/library/sections/%s' % self.section_id,
|
||||
'section_id': unicode(self.section_id)
|
||||
'section_id': str(self.section_id)
|
||||
}
|
||||
if not self.sync_to_kodi:
|
||||
args['synched'] = 'false'
|
||||
|
@ -276,7 +276,7 @@ class Section(object):
|
|||
args = {
|
||||
'mode': 'browseplex',
|
||||
'key': '/library/sections/%s/all' % self.section_id,
|
||||
'section_id': unicode(self.section_id)
|
||||
'section_id': str(self.section_id)
|
||||
}
|
||||
if not self.sync_to_kodi:
|
||||
args['synched'] = 'false'
|
||||
|
@ -318,7 +318,7 @@ class Section(object):
|
|||
if not path_ops.exists(path_ops.path.join(self.path, 'index.xml')):
|
||||
LOG.debug('Creating index.xml for section %s', self.name)
|
||||
xml = etree.Element('node',
|
||||
attrib={'order': unicode(self.order)})
|
||||
attrib={'order': str(self.order)})
|
||||
etree.SubElement(xml, 'label').text = self.name
|
||||
etree.SubElement(xml, 'icon').text = self.icon or nodes.ICON_PATH
|
||||
self._write_xml(xml, 'index.xml')
|
||||
|
@ -650,6 +650,9 @@ def _sync_from_pms(pick_libraries):
|
|||
sections = []
|
||||
old_sections = []
|
||||
for i, xml_element in enumerate(xml.findall('Directory')):
|
||||
api = API(xml_element)
|
||||
if api.plex_type in v.UNSUPPORTED_PLEX_TYPES:
|
||||
continue
|
||||
sections.append(Section(index=i, xml_element=xml_element))
|
||||
with PlexDB() as plexdb:
|
||||
for section_db in plexdb.all_sections():
|
||||
|
@ -708,7 +711,7 @@ def _clear_window_vars(index):
|
|||
utils.window('%s.path' % node, clear=True)
|
||||
utils.window('%s.id' % node, clear=True)
|
||||
# Just clear everything here, ignore the plex_type
|
||||
for typus in (x[0] for y in nodes.NODE_TYPES.values() for x in y):
|
||||
for typus in (x[0] for y in list(nodes.NODE_TYPES.values()) for x in y):
|
||||
for kind in WINDOW_ARGS:
|
||||
node = 'Plex.nodes.%s.%s.%s' % (index, typus, kind)
|
||||
utils.window(node, clear=True)
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from .common import update_kodi_library, PLAYLIST_SYNC_ENABLED
|
||||
from .fanart import SYNC_FANART, FanartTask
|
||||
from .additional_metadata import ProcessMetadataTask
|
||||
from ..plex_api import API
|
||||
from ..plex_db import PlexDB
|
||||
from .. import kodi_db
|
||||
|
@ -85,9 +84,8 @@ def process_websocket_messages():
|
|||
continue
|
||||
else:
|
||||
successful, video, music = process_new_item_message(message)
|
||||
if (successful and SYNC_FANART and
|
||||
message['plex_type'] in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)):
|
||||
task = FanartTask()
|
||||
if successful:
|
||||
task = ProcessMetadataTask()
|
||||
task.setup(message['plex_id'],
|
||||
message['plex_type'],
|
||||
refresh=False)
|
||||
|
@ -160,7 +158,7 @@ def store_timeline_message(data):
|
|||
continue
|
||||
status = int(message['state'])
|
||||
if typus == 'playlist' and PLAYLIST_SYNC_ENABLED:
|
||||
playlists.websocket(plex_id=unicode(message['itemID']),
|
||||
playlists.websocket(plex_id=str(message['itemID']),
|
||||
status=status)
|
||||
elif status == 9:
|
||||
# Immediately and always process deletions (as the PMS will
|
||||
|
|
|
@ -1,34 +1,17 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
import logging
|
||||
import xbmc
|
||||
###############################################################################
|
||||
LEVELS = {
|
||||
logging.ERROR: xbmc.LOGERROR,
|
||||
logging.WARNING: xbmc.LOGWARNING,
|
||||
logging.INFO: xbmc.LOGNOTICE,
|
||||
logging.INFO: xbmc.LOGINFO,
|
||||
logging.DEBUG: xbmc.LOGDEBUG
|
||||
}
|
||||
###############################################################################
|
||||
|
||||
|
||||
def try_encode(uniString, encoding='utf-8'):
|
||||
"""
|
||||
Will try to encode uniString (in unicode) to encoding. This possibly
|
||||
fails with e.g. Android TV's Python, which does not accept arguments for
|
||||
string.encode()
|
||||
"""
|
||||
if isinstance(uniString, str):
|
||||
# already encoded
|
||||
return uniString
|
||||
try:
|
||||
uniString = uniString.encode(encoding, "ignore")
|
||||
except TypeError:
|
||||
uniString = uniString.encode()
|
||||
return uniString
|
||||
|
||||
|
||||
def config():
|
||||
logger = logging.getLogger('PLEX')
|
||||
logger.addHandler(LogHandler())
|
||||
|
@ -38,13 +21,7 @@ def config():
|
|||
class LogHandler(logging.StreamHandler):
|
||||
def __init__(self):
|
||||
logging.StreamHandler.__init__(self)
|
||||
self.setFormatter(logging.Formatter(fmt=b"%(name)s: %(message)s"))
|
||||
self.setFormatter(logging.Formatter(fmt='%(name)s: %(message)s'))
|
||||
|
||||
def emit(self, record):
|
||||
if isinstance(record.msg, unicode):
|
||||
record.msg = record.msg.encode('utf-8')
|
||||
try:
|
||||
xbmc.log(self.format(record), level=LEVELS[record.levelno])
|
||||
except UnicodeEncodeError:
|
||||
xbmc.log(try_encode(self.format(record)),
|
||||
level=LEVELS[record.levelno])
|
||||
xbmc.log(self.format(record), level=LEVELS[record.levelno])
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from . import variables as v
|
||||
|
@ -23,80 +22,13 @@ def check_migration():
|
|||
LOG.info('Already migrated to PKC version %s' % v.ADDON_VERSION)
|
||||
return
|
||||
|
||||
if not utils.compare_version(last_migration, '1.8.2'):
|
||||
LOG.info('Migrating to version 1.8.1')
|
||||
# Set the new PKC theMovieDB key
|
||||
utils.settings('themoviedbAPIKey',
|
||||
value='19c90103adb9e98f2172c6a6a3d85dc4')
|
||||
|
||||
if not utils.compare_version(last_migration, '2.0.25'):
|
||||
LOG.info('Migrating to version 2.0.24')
|
||||
# Need to re-connect with PMS to pick up on plex.direct URIs
|
||||
utils.settings('ipaddress', value='')
|
||||
utils.settings('port', value='')
|
||||
|
||||
if not utils.compare_version(last_migration, '2.7.6'):
|
||||
LOG.info('Migrating to version 2.7.5')
|
||||
from .library_sync.sections import delete_files
|
||||
delete_files()
|
||||
|
||||
if not utils.compare_version(last_migration, '2.8.3'):
|
||||
LOG.info('Migrating to version 2.8.2')
|
||||
from .library_sync import sections
|
||||
sections.clear_window_vars()
|
||||
sections.delete_videonode_files()
|
||||
|
||||
if not utils.compare_version(last_migration, '2.8.7'):
|
||||
LOG.info('Migrating to version 2.8.6')
|
||||
# Need to delete the UNIQUE index that prevents creating several
|
||||
# playlist entries with the same kodi_hash
|
||||
if not utils.compare_version(last_migration, '3.0.4'):
|
||||
LOG.info('Migrating to version 3.0.4')
|
||||
# Add an additional column `trailer_synced` in the Plex movie table
|
||||
from .plex_db import PlexDB
|
||||
with PlexDB() as plexdb:
|
||||
plexdb.cursor.execute('DROP INDEX IF EXISTS ix_playlists_3')
|
||||
query = 'ALTER TABLE movie ADD trailer_synced BOOLEAN'
|
||||
plexdb.cursor.execute(query)
|
||||
# Index will be automatically recreated on next PKC startup
|
||||
|
||||
if not utils.compare_version(last_migration, '2.8.9'):
|
||||
LOG.info('Migrating to version 2.8.8')
|
||||
from .library_sync import sections
|
||||
sections.clear_window_vars()
|
||||
sections.delete_videonode_files()
|
||||
|
||||
if not utils.compare_version(last_migration, '2.9.3'):
|
||||
LOG.info('Migrating to version 2.9.2')
|
||||
# Re-sync all playlists to Kodi
|
||||
from .playlists import remove_synced_playlists
|
||||
remove_synced_playlists()
|
||||
|
||||
if not utils.compare_version(last_migration, '2.9.7'):
|
||||
LOG.info('Migrating to version 2.9.6')
|
||||
# Allow for a new "Direct Stream" setting (number 2), so shift the
|
||||
# last setting for "force transcoding"
|
||||
current_playback_type = utils.cast(int, utils.settings('playType')) or 0
|
||||
if current_playback_type == 2:
|
||||
current_playback_type = 3
|
||||
utils.settings('playType', value=str(current_playback_type))
|
||||
|
||||
if not utils.compare_version(last_migration, '2.9.8'):
|
||||
LOG.info('Migrating to version 2.9.7')
|
||||
# Force-scan every single item in the library - seems like we could
|
||||
# loose some recently added items otherwise
|
||||
# Caused by 65a921c3cc2068c4a34990d07289e2958f515156
|
||||
from . import library_sync
|
||||
library_sync.force_full_sync()
|
||||
|
||||
if not utils.compare_version(last_migration, '2.11.3'):
|
||||
LOG.info('Migrating to version 2.11.2')
|
||||
# Re-sync all playlists to Kodi
|
||||
from .playlists import remove_synced_playlists
|
||||
remove_synced_playlists()
|
||||
|
||||
if not utils.compare_version(last_migration, '2.12.2'):
|
||||
LOG.info('Migrating to version 2.12.1')
|
||||
# Sign user out to make sure he needs to sign in again
|
||||
utils.settings('username', value='')
|
||||
utils.settings('userid', value='')
|
||||
utils.settings('plex_restricteduser', value='')
|
||||
utils.settings('accessToken', value='')
|
||||
utils.settings('plexAvatar', value='')
|
||||
|
||||
utils.settings('last_migrated_PKC_version', value=v.ADDON_VERSION)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import re
|
||||
|
||||
|
|
|
@ -14,41 +14,25 @@ WARNING: os.path won't really work with smb paths (possibly others). For
|
|||
xbmcvfs functions to work with smb paths, they need to be both in passwords.xml
|
||||
as well as sources.xml
|
||||
"""
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
import shutil
|
||||
import os
|
||||
from os import path # allows to use path_ops.path.join, for example
|
||||
from distutils import dir_util
|
||||
import re
|
||||
|
||||
import xbmc
|
||||
import xbmcvfs
|
||||
|
||||
from .tools import unicode_paths
|
||||
|
||||
# Kodi seems to encode in utf-8 in ALL cases (unlike e.g. the OS filesystem)
|
||||
KODI_ENCODING = 'utf-8'
|
||||
REGEX_FILE_NUMBERING = re.compile(r'''_(\d\d)\.\w+$''')
|
||||
|
||||
|
||||
def encode_path(path):
|
||||
def append_os_sep(path):
|
||||
"""
|
||||
Filenames and paths are not necessarily utf-8 encoded. Use this function
|
||||
instead of try_encode/trydecode if working with filenames and paths!
|
||||
(os.walk only feeds on encoded paths. sys.getfilesystemencoding returns None
|
||||
for Raspberry Pi)
|
||||
Appends either a '\\' or '/' - IRRELEVANT of the host OS!! (os.path.join is
|
||||
dependant on the host OS)
|
||||
"""
|
||||
return unicode_paths.encode(path)
|
||||
|
||||
|
||||
def decode_path(path):
|
||||
"""
|
||||
Filenames and paths are not necessarily utf-8 encoded. Use this function
|
||||
instead of try_encode/trydecode if working with filenames and paths!
|
||||
(os.walk only feeds on encoded paths. sys.getfilesystemencoding returns None
|
||||
for Raspberry Pi)
|
||||
"""
|
||||
return unicode_paths.decode(path)
|
||||
separator = '/' if '/' in path else '\\'
|
||||
return path if path.endswith(separator) else path + separator
|
||||
|
||||
|
||||
def translate_path(path):
|
||||
|
@ -57,8 +41,7 @@ def translate_path(path):
|
|||
e.g. Converts 'special://masterprofile/script_data'
|
||||
-> '/home/user/XBMC/UserData/script_data' on Linux.
|
||||
"""
|
||||
translated = xbmc.translatePath(path.encode(KODI_ENCODING, 'strict'))
|
||||
return translated.decode(KODI_ENCODING, 'strict')
|
||||
return xbmcvfs.translatePath(path)
|
||||
|
||||
|
||||
def exists(path):
|
||||
|
@ -66,7 +49,7 @@ def exists(path):
|
|||
Returns True if the path [unicode] exists. Folders NEED a trailing slash or
|
||||
backslash!!
|
||||
"""
|
||||
return xbmcvfs.exists(path.encode(KODI_ENCODING, 'strict')) == 1
|
||||
return xbmcvfs.exists(path) == 1
|
||||
|
||||
|
||||
def rmtree(path, *args, **kwargs):
|
||||
|
@ -80,12 +63,12 @@ def rmtree(path, *args, **kwargs):
|
|||
is false and onerror is None, an exception is raised.
|
||||
|
||||
"""
|
||||
return shutil.rmtree(encode_path(path), *args, **kwargs)
|
||||
return shutil.rmtree(path, *args, **kwargs)
|
||||
|
||||
|
||||
def copyfile(src, dst):
|
||||
"""Copy data from src to dst"""
|
||||
return shutil.copyfile(encode_path(src), encode_path(dst))
|
||||
return shutil.copyfile(src, dst)
|
||||
|
||||
|
||||
def makedirs(path, *args, **kwargs):
|
||||
|
@ -95,7 +78,7 @@ def makedirs(path, *args, **kwargs):
|
|||
mkdir, except that any intermediate path segment (not just the rightmost)
|
||||
will be created if it does not exist. This is recursive.
|
||||
"""
|
||||
return os.makedirs(encode_path(path), *args, **kwargs)
|
||||
return os.makedirs(path, *args, **kwargs)
|
||||
|
||||
|
||||
def remove(path):
|
||||
|
@ -107,7 +90,7 @@ def remove(path):
|
|||
removed but the storage allocated to the file is not made available until
|
||||
the original file is no longer in use.
|
||||
"""
|
||||
return os.remove(encode_path(path))
|
||||
return os.remove(path)
|
||||
|
||||
|
||||
def walk(top, topdown=True, onerror=None, followlinks=False):
|
||||
|
@ -170,40 +153,57 @@ def walk(top, topdown=True, onerror=None, followlinks=False):
|
|||
|
||||
"""
|
||||
# Get all the results from os.walk and store them in a list
|
||||
walker = list(os.walk(encode_path(top),
|
||||
walker = list(os.walk(top,
|
||||
topdown,
|
||||
onerror,
|
||||
followlinks))
|
||||
for top, dirs, nondirs in walker:
|
||||
yield (decode_path(top),
|
||||
[decode_path(x) for x in dirs],
|
||||
[decode_path(x) for x in nondirs])
|
||||
yield (top,
|
||||
[x for x in dirs],
|
||||
[x for x in nondirs])
|
||||
|
||||
|
||||
def copy_tree(src, dst, *args, **kwargs):
|
||||
def copytree(src, dst, *args, **kwargs):
|
||||
"""
|
||||
Copy an entire directory tree 'src' to a new location 'dst'.
|
||||
Recursively copy an entire directory tree rooted at src to a directory named
|
||||
dst and return the destination directory. dirs_exist_ok dictates whether to
|
||||
raise an exception in case dst or any missing parent directory already
|
||||
exists.
|
||||
|
||||
Both 'src' and 'dst' must be directory names. If 'src' is not a
|
||||
directory, raise DistutilsFileError. If 'dst' does not exist, it is
|
||||
created with 'mkpath()'. The end result of the copy is that every
|
||||
file in 'src' is copied to 'dst', and directories under 'src' are
|
||||
recursively copied to 'dst'. Return the list of files that were
|
||||
copied or might have been copied, using their output name. The
|
||||
return value is unaffected by 'update' or 'dry_run': it is simply
|
||||
the list of all files under 'src', with the names changed to be
|
||||
under 'dst'.
|
||||
Permissions and times of directories are copied with copystat(), individual
|
||||
files are copied using copy2().
|
||||
|
||||
'preserve_mode' and 'preserve_times' are the same as for
|
||||
'copy_file'; note that they only apply to regular files, not to
|
||||
directories. If 'preserve_symlinks' is true, symlinks will be
|
||||
copied as symlinks (on platforms that support them!); otherwise
|
||||
(the default), the destination of the symlink will be copied.
|
||||
'update' and 'verbose' are the same as for 'copy_file'.
|
||||
If symlinks is true, symbolic links in the source tree are represented as
|
||||
symbolic links in the new tree and the metadata of the original links will
|
||||
be copied as far as the platform allows; if false or omitted, the contents
|
||||
and metadata of the linked files are copied to the new tree.
|
||||
|
||||
When symlinks is false, if the file pointed by the symlink doesn’t exist, an
|
||||
exception will be added in the list of errors raised in an Error exception
|
||||
at the end of the copy process. You can set the optional
|
||||
ignore_dangling_symlinks flag to true if you want to silence this exception.
|
||||
Notice that this option has no effect on platforms that don’t support
|
||||
os.symlink().
|
||||
|
||||
If ignore is given, it must be a callable that will receive as its arguments
|
||||
the directory being visited by copytree(), and a list of its contents, as
|
||||
returned by os.listdir(). Since copytree() is called recursively, the ignore
|
||||
callable will be called once for each directory that is copied. The callable
|
||||
must return a sequence of directory and file names relative to the current
|
||||
directory (i.e. a subset of the items in its second argument); these names
|
||||
will then be ignored in the copy process. ignore_patterns() can be used to
|
||||
create such a callable that ignores names based on glob-style patterns.
|
||||
|
||||
If exception(s) occur, an Error is raised with a list of reasons.
|
||||
|
||||
If copy_function is given, it must be a callable that will be used to copy
|
||||
each file. It will be called with the source path and the destination path
|
||||
as arguments. By default, copy2() is used, but any function that supports
|
||||
the same signature (like copy()) can be used.
|
||||
|
||||
Raises an auditing event shutil.copytree with arguments src, dst.
|
||||
"""
|
||||
src = encode_path(src)
|
||||
dst = encode_path(dst)
|
||||
return dir_util.copy_tree(src, dst, *args, **kwargs)
|
||||
return shutil.copytree(src, dst, *args, **kwargs)
|
||||
|
||||
|
||||
def basename(path):
|
||||
|
|
|
@ -38,7 +38,6 @@ Functions
|
|||
.. autofunction:: real_absolute_path
|
||||
.. autofunction:: parent_dir_path
|
||||
"""
|
||||
|
||||
import os.path
|
||||
from functools import partial
|
||||
|
||||
|
@ -72,7 +71,7 @@ def get_dir_walker(recursive, topdown=True, followlinks=False):
|
|||
try:
|
||||
yield next(os.walk(path, topdown=topdown, followlinks=followlinks))
|
||||
except NameError:
|
||||
yield os.walk(path, topdown=topdown, followlinks=followlinks).next() #IGNORE:E1101
|
||||
yield next(os.walk(path, topdown=topdown, followlinks=followlinks)) #IGNORE:E1101
|
||||
return walk
|
||||
|
||||
|
||||
|
|
|
@ -33,7 +33,6 @@ Functions
|
|||
.. autofunction:: match_path_against
|
||||
.. autofunction:: filter_paths
|
||||
"""
|
||||
|
||||
from fnmatch import fnmatch, fnmatchcase
|
||||
|
||||
__all__ = ['match_path',
|
||||
|
|
35
resources/lib/pathvalidate/__init__.py
Normal file
35
resources/lib/pathvalidate/__init__.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
"""
|
||||
.. codeauthor:: Tsuyoshi Hombashi <tsuyoshi.hombashi@gmail.com>
|
||||
"""
|
||||
|
||||
from .__version__ import __author__, __copyright__, __email__, __license__, __version__
|
||||
from ._common import (
|
||||
Platform,
|
||||
ascii_symbols,
|
||||
normalize_platform,
|
||||
replace_unprintable_char,
|
||||
unprintable_ascii_chars,
|
||||
validate_null_string,
|
||||
validate_pathtype,
|
||||
)
|
||||
from ._filename import FileNameSanitizer, is_valid_filename, sanitize_filename, validate_filename
|
||||
from ._filepath import (
|
||||
FilePathSanitizer,
|
||||
is_valid_filepath,
|
||||
sanitize_file_path,
|
||||
sanitize_filepath,
|
||||
validate_file_path,
|
||||
validate_filepath,
|
||||
)
|
||||
from ._ltsv import sanitize_ltsv_label, validate_ltsv_label
|
||||
from ._symbol import replace_symbol, validate_symbol
|
||||
from .error import (
|
||||
ErrorReason,
|
||||
InvalidCharError,
|
||||
InvalidLengthError,
|
||||
InvalidReservedNameError,
|
||||
NullNameError,
|
||||
ReservedNameError,
|
||||
ValidationError,
|
||||
ValidReservedNameError,
|
||||
)
|
6
resources/lib/pathvalidate/__version__.py
Normal file
6
resources/lib/pathvalidate/__version__.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
__author__ = "Tsuyoshi Hombashi"
|
||||
__copyright__ = "Copyright 2016, {}".format(__author__)
|
||||
__license__ = "MIT License"
|
||||
__version__ = "2.4.1"
|
||||
__maintainer__ = __author__
|
||||
__email__ = "tsuyoshi.hombashi@gmail.com"
|
137
resources/lib/pathvalidate/_base.py
Normal file
137
resources/lib/pathvalidate/_base.py
Normal file
|
@ -0,0 +1,137 @@
|
|||
"""
|
||||
.. codeauthor:: Tsuyoshi Hombashi <tsuyoshi.hombashi@gmail.com>
|
||||
"""
|
||||
|
||||
import abc
|
||||
import os
|
||||
from typing import Optional, Tuple, cast
|
||||
|
||||
from ._common import PathType, Platform, PlatformType, normalize_platform, unprintable_ascii_chars
|
||||
from .error import ReservedNameError, ValidationError
|
||||
|
||||
|
||||
class BaseFile:
|
||||
_INVALID_PATH_CHARS = "".join(unprintable_ascii_chars)
|
||||
_INVALID_FILENAME_CHARS = _INVALID_PATH_CHARS + "/"
|
||||
_INVALID_WIN_PATH_CHARS = _INVALID_PATH_CHARS + ':*?"<>|\t\n\r\x0b\x0c'
|
||||
_INVALID_WIN_FILENAME_CHARS = _INVALID_FILENAME_CHARS + _INVALID_WIN_PATH_CHARS + "\\"
|
||||
|
||||
_ERROR_MSG_TEMPLATE = "invalid char found: invalids=({invalid}), value={value}"
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
return self.__platform
|
||||
|
||||
@property
|
||||
def reserved_keywords(self) -> Tuple[str, ...]:
|
||||
return tuple()
|
||||
|
||||
@property
|
||||
def min_len(self) -> int:
|
||||
return self._min_len
|
||||
|
||||
@property
|
||||
def max_len(self) -> int:
|
||||
return self._max_len
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
min_len: Optional[int],
|
||||
max_len: Optional[int],
|
||||
check_reserved: bool,
|
||||
platform_max_len: Optional[int] = None,
|
||||
platform: PlatformType = None,
|
||||
) -> None:
|
||||
self.__platform = normalize_platform(platform)
|
||||
self._check_reserved = check_reserved
|
||||
|
||||
if min_len is None:
|
||||
min_len = 1
|
||||
self._min_len = max(min_len, 1)
|
||||
|
||||
if platform_max_len is None:
|
||||
platform_max_len = self._get_default_max_path_len()
|
||||
|
||||
if max_len in [None, -1]:
|
||||
self._max_len = platform_max_len
|
||||
else:
|
||||
self._max_len = cast(int, max_len)
|
||||
|
||||
self._max_len = min(self._max_len, platform_max_len)
|
||||
self._validate_max_len()
|
||||
|
||||
def _is_posix(self) -> bool:
|
||||
return self.platform == Platform.POSIX
|
||||
|
||||
def _is_universal(self) -> bool:
|
||||
return self.platform == Platform.UNIVERSAL
|
||||
|
||||
def _is_linux(self) -> bool:
|
||||
return self.platform == Platform.LINUX
|
||||
|
||||
def _is_windows(self) -> bool:
|
||||
return self.platform == Platform.WINDOWS
|
||||
|
||||
def _is_macos(self) -> bool:
|
||||
return self.platform == Platform.MACOS
|
||||
|
||||
def _validate_max_len(self) -> None:
|
||||
if self.max_len < 1:
|
||||
raise ValueError("max_len must be greater or equals to one")
|
||||
|
||||
if self.min_len > self.max_len:
|
||||
raise ValueError("min_len must be lower than max_len")
|
||||
|
||||
def _get_default_max_path_len(self) -> int:
|
||||
if self._is_linux():
|
||||
return 4096
|
||||
|
||||
if self._is_windows():
|
||||
return 260
|
||||
|
||||
if self._is_posix() or self._is_macos():
|
||||
return 1024
|
||||
|
||||
return 260 # universal
|
||||
|
||||
|
||||
class AbstractValidator(BaseFile, metaclass=abc.ABCMeta):
|
||||
@abc.abstractmethod
|
||||
def validate(self, value: PathType) -> None: # pragma: no cover
|
||||
pass
|
||||
|
||||
def is_valid(self, value: PathType) -> bool:
|
||||
try:
|
||||
self.validate(value)
|
||||
except (TypeError, ValidationError):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _is_reserved_keyword(self, value: str) -> bool:
|
||||
return value in self.reserved_keywords
|
||||
|
||||
|
||||
class AbstractSanitizer(BaseFile, metaclass=abc.ABCMeta):
|
||||
@abc.abstractmethod
|
||||
def sanitize(self, value: PathType, replacement_text: str = "") -> PathType: # pragma: no cover
|
||||
pass
|
||||
|
||||
|
||||
class BaseValidator(AbstractValidator):
|
||||
def _validate_reserved_keywords(self, name: str) -> None:
|
||||
if not self._check_reserved:
|
||||
return
|
||||
|
||||
root_name = self.__extract_root_name(name)
|
||||
if self._is_reserved_keyword(root_name.upper()):
|
||||
raise ReservedNameError(
|
||||
"'{}' is a reserved name".format(root_name),
|
||||
reusable_name=False,
|
||||
reserved_name=root_name,
|
||||
platform=self.platform,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def __extract_root_name(path: str) -> str:
|
||||
return os.path.splitext(os.path.basename(path))[0]
|
147
resources/lib/pathvalidate/_common.py
Normal file
147
resources/lib/pathvalidate/_common.py
Normal file
|
@ -0,0 +1,147 @@
|
|||
"""
|
||||
.. codeauthor:: Tsuyoshi Hombashi <tsuyoshi.hombashi@gmail.com>
|
||||
"""
|
||||
|
||||
import enum
|
||||
import platform
|
||||
import re
|
||||
import string
|
||||
from pathlib import Path
|
||||
from typing import Any, List, Optional, Union, cast
|
||||
|
||||
|
||||
_re_whitespaces = re.compile(r"^[\s]+$")
|
||||
|
||||
|
||||
@enum.unique
|
||||
class Platform(enum.Enum):
|
||||
POSIX = "POSIX"
|
||||
UNIVERSAL = "universal"
|
||||
|
||||
LINUX = "Linux"
|
||||
WINDOWS = "Windows"
|
||||
MACOS = "macOS"
|
||||
|
||||
|
||||
PathType = Union[str, Path]
|
||||
PlatformType = Union[str, Platform, None]
|
||||
|
||||
|
||||
def is_pathlike_obj(value: PathType) -> bool:
|
||||
return isinstance(value, Path)
|
||||
|
||||
|
||||
def validate_pathtype(
|
||||
text: PathType, allow_whitespaces: bool = False, error_msg: Optional[str] = None
|
||||
) -> None:
|
||||
from .error import ErrorReason, ValidationError
|
||||
|
||||
if _is_not_null_string(text) or is_pathlike_obj(text):
|
||||
return
|
||||
|
||||
if allow_whitespaces and _re_whitespaces.search(str(text)):
|
||||
return
|
||||
|
||||
if is_null_string(text):
|
||||
if not error_msg:
|
||||
error_msg = "the value must be a not empty"
|
||||
|
||||
raise ValidationError(
|
||||
description=error_msg,
|
||||
reason=ErrorReason.NULL_NAME,
|
||||
)
|
||||
|
||||
raise TypeError("text must be a string: actual={}".format(type(text)))
|
||||
|
||||
|
||||
def validate_null_string(text: PathType, error_msg: Optional[str] = None) -> None:
|
||||
# Deprecated: alias to validate_pathtype
|
||||
validate_pathtype(text, False, error_msg)
|
||||
|
||||
|
||||
def preprocess(name: PathType) -> str:
|
||||
if is_pathlike_obj(name):
|
||||
name = str(name)
|
||||
|
||||
return cast(str, name)
|
||||
|
||||
|
||||
def is_null_string(value: Any) -> bool:
|
||||
if value is None:
|
||||
return True
|
||||
|
||||
try:
|
||||
return len(value.strip()) == 0
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
|
||||
def _is_not_null_string(value: Any) -> bool:
|
||||
try:
|
||||
return len(value.strip()) > 0
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
|
||||
def _get_unprintable_ascii_chars() -> List[str]:
|
||||
return [chr(c) for c in range(128) if chr(c) not in string.printable]
|
||||
|
||||
|
||||
unprintable_ascii_chars = tuple(_get_unprintable_ascii_chars())
|
||||
|
||||
|
||||
def _get_ascii_symbols() -> List[str]:
|
||||
symbol_list = [] # type: List[str]
|
||||
|
||||
for i in range(128):
|
||||
c = chr(i)
|
||||
|
||||
if c in unprintable_ascii_chars or c in string.digits + string.ascii_letters:
|
||||
continue
|
||||
|
||||
symbol_list.append(c)
|
||||
|
||||
return symbol_list
|
||||
|
||||
|
||||
ascii_symbols = tuple(_get_ascii_symbols())
|
||||
|
||||
__RE_UNPRINTABLE_CHARS = re.compile(
|
||||
"[{}]".format(re.escape("".join(unprintable_ascii_chars))), re.UNICODE
|
||||
)
|
||||
|
||||
|
||||
def replace_unprintable_char(text: str, replacement_text: str = "") -> str:
|
||||
try:
|
||||
return __RE_UNPRINTABLE_CHARS.sub(replacement_text, text)
|
||||
except (TypeError, AttributeError):
|
||||
raise TypeError("text must be a string")
|
||||
|
||||
|
||||
def normalize_platform(name: PlatformType) -> Platform:
|
||||
if isinstance(name, Platform):
|
||||
return name
|
||||
|
||||
if name:
|
||||
name = name.strip().lower()
|
||||
|
||||
if name == "posix":
|
||||
return Platform.POSIX
|
||||
|
||||
if name == "auto":
|
||||
name = platform.system().lower()
|
||||
|
||||
if name in ["linux"]:
|
||||
return Platform.LINUX
|
||||
|
||||
if name and name.startswith("win"):
|
||||
return Platform.WINDOWS
|
||||
|
||||
if name in ["mac", "macos", "darwin"]:
|
||||
return Platform.MACOS
|
||||
|
||||
return Platform.UNIVERSAL
|
||||
|
||||
|
||||
def findall_to_str(match: List[Any]) -> str:
|
||||
return ", ".join([repr(text) for text in match])
|
16
resources/lib/pathvalidate/_const.py
Normal file
16
resources/lib/pathvalidate/_const.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
_NTFS_RESERVED_FILE_NAMES = (
|
||||
"$Mft",
|
||||
"$MftMirr",
|
||||
"$LogFile",
|
||||
"$Volume",
|
||||
"$AttrDef",
|
||||
"$Bitmap",
|
||||
"$Boot",
|
||||
"$BadClus",
|
||||
"$Secure",
|
||||
"$Upcase",
|
||||
"$Extend",
|
||||
"$Quota",
|
||||
"$ObjId",
|
||||
"$Reparse",
|
||||
) # Only in root directory
|
341
resources/lib/pathvalidate/_filename.py
Normal file
341
resources/lib/pathvalidate/_filename.py
Normal file
|
@ -0,0 +1,341 @@
|
|||
"""
|
||||
.. codeauthor:: Tsuyoshi Hombashi <tsuyoshi.hombashi@gmail.com>
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import ntpath
|
||||
import posixpath
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Optional, Pattern, Tuple
|
||||
|
||||
from ._base import AbstractSanitizer, BaseFile, BaseValidator
|
||||
from ._common import (
|
||||
PathType,
|
||||
Platform,
|
||||
PlatformType,
|
||||
findall_to_str,
|
||||
is_pathlike_obj,
|
||||
preprocess,
|
||||
validate_pathtype,
|
||||
)
|
||||
from .error import ErrorReason, InvalidCharError, InvalidLengthError, ValidationError
|
||||
|
||||
|
||||
_DEFAULT_MAX_FILENAME_LEN = 255
|
||||
_RE_INVALID_FILENAME = re.compile(
|
||||
"[{:s}]".format(re.escape(BaseFile._INVALID_FILENAME_CHARS)), re.UNICODE
|
||||
)
|
||||
_RE_INVALID_WIN_FILENAME = re.compile(
|
||||
"[{:s}]".format(re.escape(BaseFile._INVALID_WIN_FILENAME_CHARS)), re.UNICODE
|
||||
)
|
||||
|
||||
|
||||
class FileNameSanitizer(AbstractSanitizer):
|
||||
def __init__(
|
||||
self,
|
||||
min_len: Optional[int] = 1,
|
||||
max_len: Optional[int] = _DEFAULT_MAX_FILENAME_LEN,
|
||||
platform: PlatformType = None,
|
||||
check_reserved: bool = True,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
min_len=min_len,
|
||||
max_len=max_len,
|
||||
check_reserved=check_reserved,
|
||||
platform_max_len=_DEFAULT_MAX_FILENAME_LEN,
|
||||
platform=platform,
|
||||
)
|
||||
|
||||
self._sanitize_regexp = self._get_sanitize_regexp()
|
||||
self.__validator = FileNameValidator(
|
||||
min_len=self.min_len,
|
||||
max_len=self.max_len,
|
||||
check_reserved=check_reserved,
|
||||
platform=self.platform,
|
||||
)
|
||||
|
||||
def sanitize(self, value: PathType, replacement_text: str = "") -> PathType:
|
||||
try:
|
||||
validate_pathtype(value, allow_whitespaces=True if not self._is_windows() else False)
|
||||
except ValidationError as e:
|
||||
if e.reason == ErrorReason.NULL_NAME:
|
||||
return ""
|
||||
raise
|
||||
|
||||
sanitized_filename = self._sanitize_regexp.sub(replacement_text, str(value))
|
||||
sanitized_filename = sanitized_filename[: self.max_len]
|
||||
|
||||
try:
|
||||
self.__validator.validate(sanitized_filename)
|
||||
except ValidationError as e:
|
||||
if e.reason == ErrorReason.RESERVED_NAME and e.reusable_name is False:
|
||||
sanitized_filename = re.sub(
|
||||
re.escape(e.reserved_name), "{}_".format(e.reserved_name), sanitized_filename
|
||||
)
|
||||
elif e.reason == ErrorReason.INVALID_CHARACTER:
|
||||
if self.platform in [Platform.UNIVERSAL, Platform.WINDOWS]:
|
||||
sanitized_filename = sanitized_filename.rstrip(" .")
|
||||
|
||||
if is_pathlike_obj(value):
|
||||
return Path(sanitized_filename)
|
||||
|
||||
return sanitized_filename
|
||||
|
||||
def _get_sanitize_regexp(self) -> Pattern:
|
||||
if self.platform in [Platform.UNIVERSAL, Platform.WINDOWS]:
|
||||
return _RE_INVALID_WIN_FILENAME
|
||||
|
||||
return _RE_INVALID_FILENAME
|
||||
|
||||
|
||||
class FileNameValidator(BaseValidator):
|
||||
_WINDOWS_RESERVED_FILE_NAMES = ("CON", "PRN", "AUX", "CLOCK$", "NUL") + tuple(
|
||||
"{:s}{:d}".format(name, num)
|
||||
for name, num in itertools.product(("COM", "LPT"), range(1, 10))
|
||||
)
|
||||
_MACOS_RESERVED_FILE_NAMES = (":",)
|
||||
|
||||
@property
|
||||
def reserved_keywords(self) -> Tuple[str, ...]:
|
||||
common_keywords = super().reserved_keywords
|
||||
|
||||
if self._is_universal():
|
||||
return (
|
||||
common_keywords
|
||||
+ self._WINDOWS_RESERVED_FILE_NAMES
|
||||
+ self._MACOS_RESERVED_FILE_NAMES
|
||||
)
|
||||
|
||||
if self._is_windows():
|
||||
return common_keywords + self._WINDOWS_RESERVED_FILE_NAMES
|
||||
|
||||
if self._is_posix() or self._is_macos():
|
||||
return common_keywords + self._MACOS_RESERVED_FILE_NAMES
|
||||
|
||||
return common_keywords
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
min_len: Optional[int] = 1,
|
||||
max_len: Optional[int] = _DEFAULT_MAX_FILENAME_LEN,
|
||||
platform: PlatformType = None,
|
||||
check_reserved: bool = True,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
min_len=min_len,
|
||||
max_len=max_len,
|
||||
check_reserved=check_reserved,
|
||||
platform_max_len=_DEFAULT_MAX_FILENAME_LEN,
|
||||
platform=platform,
|
||||
)
|
||||
|
||||
def validate(self, value: PathType) -> None:
|
||||
validate_pathtype(
|
||||
value,
|
||||
allow_whitespaces=False
|
||||
if self.platform in [Platform.UNIVERSAL, Platform.WINDOWS]
|
||||
else True,
|
||||
)
|
||||
|
||||
unicode_filename = preprocess(value)
|
||||
value_len = len(unicode_filename)
|
||||
|
||||
self.validate_abspath(unicode_filename)
|
||||
|
||||
if value_len > self.max_len:
|
||||
raise InvalidLengthError(
|
||||
"filename is too long: expected<={:d}, actual={:d}".format(self.max_len, value_len)
|
||||
)
|
||||
if value_len < self.min_len:
|
||||
raise InvalidLengthError(
|
||||
"filename is too short: expected>={:d}, actual={:d}".format(self.min_len, value_len)
|
||||
)
|
||||
|
||||
self._validate_reserved_keywords(unicode_filename)
|
||||
|
||||
if self._is_universal() or self._is_windows():
|
||||
self.__validate_win_filename(unicode_filename)
|
||||
else:
|
||||
self.__validate_unix_filename(unicode_filename)
|
||||
|
||||
def validate_abspath(self, value: str) -> None:
|
||||
err = ValidationError(
|
||||
description="found an absolute path ({}), expected a filename".format(value),
|
||||
platform=self.platform,
|
||||
reason=ErrorReason.FOUND_ABS_PATH,
|
||||
)
|
||||
|
||||
if self._is_universal() or self._is_windows():
|
||||
if ntpath.isabs(value):
|
||||
raise err
|
||||
|
||||
if posixpath.isabs(value):
|
||||
raise err
|
||||
|
||||
def __validate_unix_filename(self, unicode_filename: str) -> None:
|
||||
match = _RE_INVALID_FILENAME.findall(unicode_filename)
|
||||
if match:
|
||||
raise InvalidCharError(
|
||||
self._ERROR_MSG_TEMPLATE.format(
|
||||
invalid=findall_to_str(match), value=repr(unicode_filename)
|
||||
)
|
||||
)
|
||||
|
||||
def __validate_win_filename(self, unicode_filename: str) -> None:
|
||||
match = _RE_INVALID_WIN_FILENAME.findall(unicode_filename)
|
||||
if match:
|
||||
raise InvalidCharError(
|
||||
self._ERROR_MSG_TEMPLATE.format(
|
||||
invalid=findall_to_str(match), value=repr(unicode_filename)
|
||||
),
|
||||
platform=Platform.WINDOWS,
|
||||
)
|
||||
|
||||
if unicode_filename in (".", ".."):
|
||||
return
|
||||
|
||||
if unicode_filename[-1] in (" ", "."):
|
||||
raise InvalidCharError(
|
||||
self._ERROR_MSG_TEMPLATE.format(
|
||||
invalid=re.escape(unicode_filename[-1]), value=repr(unicode_filename)
|
||||
),
|
||||
platform=Platform.WINDOWS,
|
||||
description="Do not end a file or directory name with a space or a period",
|
||||
)
|
||||
|
||||
|
||||
def validate_filename(
|
||||
filename: PathType,
|
||||
platform: Optional[str] = None,
|
||||
min_len: int = 1,
|
||||
max_len: int = _DEFAULT_MAX_FILENAME_LEN,
|
||||
check_reserved: bool = True,
|
||||
) -> None:
|
||||
"""Verifying whether the ``filename`` is a valid file name or not.
|
||||
|
||||
Args:
|
||||
filename:
|
||||
Filename to validate.
|
||||
platform:
|
||||
Target platform name of the filename.
|
||||
|
||||
.. include:: platform.txt
|
||||
min_len:
|
||||
Minimum length of the ``filename``. The value must be greater or equal to one.
|
||||
Defaults to ``1``.
|
||||
max_len:
|
||||
Maximum length of the ``filename``. The value must be lower than:
|
||||
|
||||
- ``Linux``: 4096
|
||||
- ``macOS``: 1024
|
||||
- ``Windows``: 260
|
||||
- ``universal``: 260
|
||||
|
||||
Defaults to ``255``.
|
||||
check_reserved:
|
||||
If |True|, check reserved names of the ``platform``.
|
||||
|
||||
Raises:
|
||||
ValidationError (ErrorReason.INVALID_LENGTH):
|
||||
If the ``filename`` is longer than ``max_len`` characters.
|
||||
ValidationError (ErrorReason.INVALID_CHARACTER):
|
||||
If the ``filename`` includes invalid character(s) for a filename:
|
||||
|invalid_filename_chars|.
|
||||
The following characters are also invalid for Windows platform:
|
||||
|invalid_win_filename_chars|.
|
||||
ValidationError (ErrorReason.RESERVED_NAME):
|
||||
If the ``filename`` equals reserved name by OS.
|
||||
Windows reserved name is as follows:
|
||||
``"CON"``, ``"PRN"``, ``"AUX"``, ``"NUL"``, ``"COM[1-9]"``, ``"LPT[1-9]"``.
|
||||
|
||||
Example:
|
||||
:ref:`example-validate-filename`
|
||||
|
||||
See Also:
|
||||
`Naming Files, Paths, and Namespaces - Win32 apps | Microsoft Docs
|
||||
<https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file>`__
|
||||
"""
|
||||
|
||||
FileNameValidator(
|
||||
platform=platform, min_len=min_len, max_len=max_len, check_reserved=check_reserved
|
||||
).validate(filename)
|
||||
|
||||
|
||||
def is_valid_filename(
|
||||
filename: PathType,
|
||||
platform: Optional[str] = None,
|
||||
min_len: int = 1,
|
||||
max_len: Optional[int] = None,
|
||||
check_reserved: bool = True,
|
||||
) -> bool:
|
||||
"""Check whether the ``filename`` is a valid name or not.
|
||||
|
||||
Args:
|
||||
filename:
|
||||
A filename to be checked.
|
||||
|
||||
Example:
|
||||
:ref:`example-is-valid-filename`
|
||||
|
||||
See Also:
|
||||
:py:func:`.validate_filename()`
|
||||
"""
|
||||
|
||||
return FileNameValidator(
|
||||
platform=platform, min_len=min_len, max_len=max_len, check_reserved=check_reserved
|
||||
).is_valid(filename)
|
||||
|
||||
|
||||
def sanitize_filename(
|
||||
filename: PathType,
|
||||
replacement_text: str = "",
|
||||
platform: Optional[str] = None,
|
||||
max_len: Optional[int] = _DEFAULT_MAX_FILENAME_LEN,
|
||||
check_reserved: bool = True,
|
||||
) -> PathType:
|
||||
"""Make a valid filename from a string.
|
||||
|
||||
To make a valid filename the function does:
|
||||
|
||||
- Replace invalid characters as file names included in the ``filename``
|
||||
with the ``replacement_text``. Invalid characters are:
|
||||
|
||||
- unprintable characters
|
||||
- |invalid_filename_chars|
|
||||
- for Windows (or universal) only: |invalid_win_filename_chars|
|
||||
|
||||
- Append underscore (``"_"``) at the tail of the name if sanitized name
|
||||
is one of the reserved names by operating systems
|
||||
(only when ``check_reserved`` is |True|).
|
||||
|
||||
Args:
|
||||
filename: Filename to sanitize.
|
||||
replacement_text:
|
||||
Replacement text for invalid characters. Defaults to ``""``.
|
||||
platform:
|
||||
Target platform name of the filename.
|
||||
|
||||
.. include:: platform.txt
|
||||
max_len:
|
||||
Maximum length of the ``filename`` length. Truncate the name length if
|
||||
the ``filename`` length exceeds this value.
|
||||
Defaults to ``255``.
|
||||
check_reserved:
|
||||
If |True|, sanitize reserved names of the ``platform``.
|
||||
|
||||
Returns:
|
||||
Same type as the ``filename`` (str or PathLike object):
|
||||
Sanitized filename.
|
||||
|
||||
Raises:
|
||||
ValueError:
|
||||
If the ``filename`` is an invalid filename.
|
||||
|
||||
Example:
|
||||
:ref:`example-sanitize-filename`
|
||||
"""
|
||||
|
||||
return FileNameSanitizer(
|
||||
platform=platform, max_len=max_len, check_reserved=check_reserved
|
||||
).sanitize(filename, replacement_text)
|
427
resources/lib/pathvalidate/_filepath.py
Normal file
427
resources/lib/pathvalidate/_filepath.py
Normal file
|
@ -0,0 +1,427 @@
|
|||
"""
|
||||
.. codeauthor:: Tsuyoshi Hombashi <tsuyoshi.hombashi@gmail.com>
|
||||
"""
|
||||
|
||||
import ntpath
|
||||
import os.path
|
||||
import posixpath
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Pattern, Tuple # noqa
|
||||
|
||||
from ._base import AbstractSanitizer, BaseFile, BaseValidator
|
||||
from ._common import (
|
||||
PathType,
|
||||
Platform,
|
||||
PlatformType,
|
||||
findall_to_str,
|
||||
is_pathlike_obj,
|
||||
preprocess,
|
||||
validate_pathtype,
|
||||
)
|
||||
from ._const import _NTFS_RESERVED_FILE_NAMES
|
||||
from ._filename import FileNameSanitizer, FileNameValidator
|
||||
from .error import (
|
||||
ErrorReason,
|
||||
InvalidCharError,
|
||||
InvalidLengthError,
|
||||
ReservedNameError,
|
||||
ValidationError,
|
||||
)
|
||||
|
||||
|
||||
_RE_INVALID_PATH = re.compile("[{:s}]".format(re.escape(BaseFile._INVALID_PATH_CHARS)), re.UNICODE)
|
||||
_RE_INVALID_WIN_PATH = re.compile(
|
||||
"[{:s}]".format(re.escape(BaseFile._INVALID_WIN_PATH_CHARS)), re.UNICODE
|
||||
)
|
||||
|
||||
|
||||
class FilePathSanitizer(AbstractSanitizer):
|
||||
def __init__(
|
||||
self,
|
||||
min_len: Optional[int] = 1,
|
||||
max_len: Optional[int] = None,
|
||||
platform: PlatformType = None,
|
||||
check_reserved: bool = True,
|
||||
normalize: bool = True,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
min_len=min_len,
|
||||
max_len=max_len,
|
||||
check_reserved=check_reserved,
|
||||
platform=platform,
|
||||
)
|
||||
|
||||
self._sanitize_regexp = self._get_sanitize_regexp()
|
||||
self.__fpath_validator = FilePathValidator(
|
||||
min_len=self.min_len,
|
||||
max_len=self.max_len,
|
||||
check_reserved=check_reserved,
|
||||
platform=self.platform,
|
||||
)
|
||||
self.__fname_sanitizer = FileNameSanitizer(
|
||||
min_len=self.min_len,
|
||||
max_len=self.max_len,
|
||||
check_reserved=check_reserved,
|
||||
platform=self.platform,
|
||||
)
|
||||
self.__normalize = normalize
|
||||
|
||||
if self._is_universal() or self._is_windows():
|
||||
self.__split_drive = ntpath.splitdrive
|
||||
else:
|
||||
self.__split_drive = posixpath.splitdrive
|
||||
|
||||
def sanitize(self, value: PathType, replacement_text: str = "") -> PathType:
|
||||
if not value:
|
||||
return ""
|
||||
|
||||
self.__fpath_validator.validate_abspath(value)
|
||||
|
||||
unicode_filepath = preprocess(value)
|
||||
|
||||
if self.__normalize:
|
||||
unicode_filepath = os.path.normpath(unicode_filepath)
|
||||
|
||||
drive, unicode_filepath = self.__split_drive(unicode_filepath)
|
||||
sanitized_path = self._sanitize_regexp.sub(replacement_text, unicode_filepath)
|
||||
if self._is_windows():
|
||||
path_separator = "\\"
|
||||
else:
|
||||
path_separator = "/"
|
||||
|
||||
sanitized_entries = [] # type: List[str]
|
||||
if drive:
|
||||
sanitized_entries.append(drive)
|
||||
for entry in sanitized_path.replace("\\", "/").split("/"):
|
||||
if entry in _NTFS_RESERVED_FILE_NAMES:
|
||||
sanitized_entries.append("{}_".format(entry))
|
||||
continue
|
||||
|
||||
sanitized_entry = str(self.__fname_sanitizer.sanitize(entry))
|
||||
if not sanitized_entry:
|
||||
if not sanitized_entries:
|
||||
sanitized_entries.append("")
|
||||
continue
|
||||
|
||||
sanitized_entries.append(sanitized_entry)
|
||||
|
||||
sanitized_path = path_separator.join(sanitized_entries)
|
||||
|
||||
if is_pathlike_obj(value):
|
||||
return Path(sanitized_path)
|
||||
|
||||
return sanitized_path
|
||||
|
||||
def _get_sanitize_regexp(self) -> Pattern:
|
||||
if self.platform in [Platform.UNIVERSAL, Platform.WINDOWS]:
|
||||
return _RE_INVALID_WIN_PATH
|
||||
|
||||
return _RE_INVALID_PATH
|
||||
|
||||
|
||||
class FilePathValidator(BaseValidator):
|
||||
_RE_NTFS_RESERVED = re.compile(
|
||||
"|".join("^/{}$".format(re.escape(pattern)) for pattern in _NTFS_RESERVED_FILE_NAMES),
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_MACOS_RESERVED_FILE_PATHS = ("/", ":")
|
||||
|
||||
@property
|
||||
def reserved_keywords(self) -> Tuple[str, ...]:
|
||||
common_keywords = super().reserved_keywords
|
||||
|
||||
if any([self._is_universal(), self._is_posix(), self._is_macos()]):
|
||||
return common_keywords + self._MACOS_RESERVED_FILE_PATHS
|
||||
|
||||
if self._is_linux():
|
||||
return common_keywords + ("/",)
|
||||
|
||||
return common_keywords
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
min_len: Optional[int] = 1,
|
||||
max_len: Optional[int] = None,
|
||||
platform: PlatformType = None,
|
||||
check_reserved: bool = True,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
min_len=min_len,
|
||||
max_len=max_len,
|
||||
check_reserved=check_reserved,
|
||||
platform=platform,
|
||||
)
|
||||
|
||||
self.__fname_validator = FileNameValidator(
|
||||
min_len=min_len, max_len=max_len, check_reserved=check_reserved, platform=platform
|
||||
)
|
||||
|
||||
if self._is_universal() or self._is_windows():
|
||||
self.__split_drive = ntpath.splitdrive
|
||||
else:
|
||||
self.__split_drive = posixpath.splitdrive
|
||||
|
||||
def validate(self, value: PathType) -> None:
|
||||
validate_pathtype(
|
||||
value,
|
||||
allow_whitespaces=False
|
||||
if self.platform in [Platform.UNIVERSAL, Platform.WINDOWS]
|
||||
else True,
|
||||
)
|
||||
self.validate_abspath(value)
|
||||
|
||||
_drive, value = self.__split_drive(str(value))
|
||||
if not value:
|
||||
return
|
||||
|
||||
filepath = os.path.normpath(value)
|
||||
unicode_filepath = preprocess(filepath)
|
||||
value_len = len(unicode_filepath)
|
||||
|
||||
if value_len > self.max_len:
|
||||
raise InvalidLengthError(
|
||||
"file path is too long: expected<={:d}, actual={:d}".format(self.max_len, value_len)
|
||||
)
|
||||
if value_len < self.min_len:
|
||||
raise InvalidLengthError(
|
||||
"file path is too short: expected>={:d}, actual={:d}".format(
|
||||
self.min_len, value_len
|
||||
)
|
||||
)
|
||||
|
||||
self._validate_reserved_keywords(unicode_filepath)
|
||||
unicode_filepath = unicode_filepath.replace("\\", "/")
|
||||
for entry in unicode_filepath.split("/"):
|
||||
if not entry or entry in (".", ".."):
|
||||
continue
|
||||
|
||||
self.__fname_validator._validate_reserved_keywords(entry)
|
||||
|
||||
if self._is_universal() or self._is_windows():
|
||||
self.__validate_win_filepath(unicode_filepath)
|
||||
else:
|
||||
self.__validate_unix_filepath(unicode_filepath)
|
||||
|
||||
def validate_abspath(self, value: PathType) -> None:
|
||||
value = str(value)
|
||||
is_posix_abs = posixpath.isabs(value)
|
||||
is_nt_abs = ntpath.isabs(value)
|
||||
err_object = ValidationError(
|
||||
description=(
|
||||
"an invalid absolute file path ({}) for the platform ({}).".format(
|
||||
value, self.platform.value
|
||||
)
|
||||
+ " to avoid the error, specify an appropriate platform correspond"
|
||||
+ " with the path format, or 'auto'."
|
||||
),
|
||||
platform=self.platform,
|
||||
reason=ErrorReason.MALFORMED_ABS_PATH,
|
||||
)
|
||||
|
||||
if any([self._is_windows() and is_nt_abs, self._is_linux() and is_posix_abs]):
|
||||
return
|
||||
|
||||
if self._is_universal() and any([is_posix_abs, is_nt_abs]):
|
||||
ValidationError(
|
||||
description=(
|
||||
"{}. expected a platform independent file path".format(
|
||||
"POSIX absolute file path found"
|
||||
if is_posix_abs
|
||||
else "NT absolute file path found"
|
||||
)
|
||||
),
|
||||
platform=self.platform,
|
||||
reason=ErrorReason.MALFORMED_ABS_PATH,
|
||||
)
|
||||
|
||||
if any([self._is_windows(), self._is_universal()]) and is_posix_abs:
|
||||
raise err_object
|
||||
|
||||
drive, _tail = ntpath.splitdrive(value)
|
||||
if not self._is_windows() and drive and is_nt_abs:
|
||||
raise err_object
|
||||
|
||||
def __validate_unix_filepath(self, unicode_filepath: str) -> None:
|
||||
match = _RE_INVALID_PATH.findall(unicode_filepath)
|
||||
if match:
|
||||
raise InvalidCharError(
|
||||
self._ERROR_MSG_TEMPLATE.format(
|
||||
invalid=findall_to_str(match), value=repr(unicode_filepath)
|
||||
)
|
||||
)
|
||||
|
||||
def __validate_win_filepath(self, unicode_filepath: str) -> None:
|
||||
match = _RE_INVALID_WIN_PATH.findall(unicode_filepath)
|
||||
if match:
|
||||
raise InvalidCharError(
|
||||
self._ERROR_MSG_TEMPLATE.format(
|
||||
invalid=findall_to_str(match), value=repr(unicode_filepath)
|
||||
),
|
||||
platform=Platform.WINDOWS,
|
||||
)
|
||||
|
||||
_drive, value = self.__split_drive(unicode_filepath)
|
||||
if value:
|
||||
match_reserved = self._RE_NTFS_RESERVED.search(value)
|
||||
if match_reserved:
|
||||
reserved_name = match_reserved.group()
|
||||
raise ReservedNameError(
|
||||
"'{}' is a reserved name".format(reserved_name),
|
||||
reusable_name=False,
|
||||
reserved_name=reserved_name,
|
||||
platform=self.platform,
|
||||
)
|
||||
|
||||
|
||||
def validate_filepath(
|
||||
file_path: PathType,
|
||||
platform: Optional[str] = None,
|
||||
min_len: int = 1,
|
||||
max_len: Optional[int] = None,
|
||||
check_reserved: bool = True,
|
||||
) -> None:
|
||||
"""Verifying whether the ``file_path`` is a valid file path or not.
|
||||
|
||||
Args:
|
||||
file_path:
|
||||
File path to validate.
|
||||
platform:
|
||||
Target platform name of the file path.
|
||||
|
||||
.. include:: platform.txt
|
||||
min_len:
|
||||
Minimum length of the ``file_path``. The value must be greater or equal to one.
|
||||
Defaults to ``1``.
|
||||
max_len:
|
||||
Maximum length of the ``file_path`` length. If the value is |None|,
|
||||
automatically determined by the ``platform``:
|
||||
|
||||
- ``Linux``: 4096
|
||||
- ``macOS``: 1024
|
||||
- ``Windows``: 260
|
||||
- ``universal``: 260
|
||||
check_reserved:
|
||||
If |True|, check reserved names of the ``platform``.
|
||||
|
||||
Raises:
|
||||
ValidationError (ErrorReason.INVALID_CHARACTER):
|
||||
If the ``file_path`` includes invalid char(s):
|
||||
|invalid_file_path_chars|.
|
||||
The following characters are also invalid for Windows platform:
|
||||
|invalid_win_file_path_chars|
|
||||
ValidationError (ErrorReason.INVALID_LENGTH):
|
||||
If the ``file_path`` is longer than ``max_len`` characters.
|
||||
ValidationError:
|
||||
If ``file_path`` include invalid values.
|
||||
|
||||
Example:
|
||||
:ref:`example-validate-file-path`
|
||||
|
||||
See Also:
|
||||
`Naming Files, Paths, and Namespaces - Win32 apps | Microsoft Docs
|
||||
<https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file>`__
|
||||
"""
|
||||
|
||||
FilePathValidator(
|
||||
platform=platform, min_len=min_len, max_len=max_len, check_reserved=check_reserved
|
||||
).validate(file_path)
|
||||
|
||||
|
||||
def validate_file_path(file_path, platform=None, max_path_len=None):
|
||||
# Deprecated
|
||||
validate_filepath(file_path, platform, max_path_len)
|
||||
|
||||
|
||||
def is_valid_filepath(
|
||||
file_path: PathType,
|
||||
platform: Optional[str] = None,
|
||||
min_len: int = 1,
|
||||
max_len: Optional[int] = None,
|
||||
check_reserved: bool = True,
|
||||
) -> bool:
|
||||
"""Check whether the ``file_path`` is a valid name or not.
|
||||
|
||||
Args:
|
||||
file_path:
|
||||
A filepath to be checked.
|
||||
|
||||
Example:
|
||||
:ref:`example-is-valid-filepath`
|
||||
|
||||
See Also:
|
||||
:py:func:`.validate_filepath()`
|
||||
"""
|
||||
|
||||
return FilePathValidator(
|
||||
platform=platform, min_len=min_len, max_len=max_len, check_reserved=check_reserved
|
||||
).is_valid(file_path)
|
||||
|
||||
|
||||
def sanitize_filepath(
|
||||
file_path: PathType,
|
||||
replacement_text: str = "",
|
||||
platform: Optional[str] = None,
|
||||
max_len: Optional[int] = None,
|
||||
check_reserved: bool = True,
|
||||
normalize: bool = True,
|
||||
) -> PathType:
|
||||
"""Make a valid file path from a string.
|
||||
|
||||
To make a valid file path the function does:
|
||||
|
||||
- replace invalid characters for a file path within the ``file_path``
|
||||
with the ``replacement_text``. Invalid characters are as follows:
|
||||
|
||||
- unprintable characters
|
||||
- |invalid_file_path_chars|
|
||||
- for Windows (or universal) only: |invalid_win_file_path_chars|
|
||||
|
||||
- Append underscore (``"_"``) at the tail of the name if sanitized name
|
||||
is one of the reserved names by operating systems
|
||||
(only when ``check_reserved`` is |True|).
|
||||
|
||||
Args:
|
||||
file_path:
|
||||
File path to sanitize.
|
||||
replacement_text:
|
||||
Replacement text for invalid characters.
|
||||
Defaults to ``""``.
|
||||
platform:
|
||||
Target platform name of the file path.
|
||||
|
||||
.. include:: platform.txt
|
||||
max_len:
|
||||
Maximum length of the ``file_path`` length. Truncate the name if the ``file_path``
|
||||
length exceedd this value. If the value is |None|,
|
||||
``max_len`` will automatically determined by the ``platform``:
|
||||
|
||||
- ``Linux``: 4096
|
||||
- ``macOS``: 1024
|
||||
- ``Windows``: 260
|
||||
- ``universal``: 260
|
||||
check_reserved:
|
||||
If |True|, sanitize reserved names of the ``platform``.
|
||||
normalize:
|
||||
If |True|, normalize the the file path.
|
||||
|
||||
Returns:
|
||||
Same type as the argument (str or PathLike object):
|
||||
Sanitized filepath.
|
||||
|
||||
Raises:
|
||||
ValueError:
|
||||
If the ``file_path`` is an invalid file path.
|
||||
|
||||
Example:
|
||||
:ref:`example-sanitize-file-path`
|
||||
"""
|
||||
|
||||
return FilePathSanitizer(
|
||||
platform=platform, max_len=max_len, check_reserved=check_reserved, normalize=normalize
|
||||
).sanitize(file_path, replacement_text)
|
||||
|
||||
|
||||
def sanitize_file_path(file_path, replacement_text="", platform=None, max_path_len=None):
|
||||
# Deprecated
|
||||
return sanitize_filepath(file_path, platform, max_path_len)
|
45
resources/lib/pathvalidate/_ltsv.py
Normal file
45
resources/lib/pathvalidate/_ltsv.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
"""
|
||||
.. codeauthor:: Tsuyoshi Hombashi <tsuyoshi.hombashi@gmail.com>
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from ._common import preprocess, validate_pathtype
|
||||
from .error import InvalidCharError
|
||||
|
||||
|
||||
__RE_INVALID_LTSV_LABEL = re.compile("[^0-9A-Za-z_.-]", re.UNICODE)
|
||||
|
||||
|
||||
def validate_ltsv_label(label: str) -> None:
|
||||
"""
|
||||
Verifying whether ``label`` is a valid
|
||||
`Labeled Tab-separated Values (LTSV) <http://ltsv.org/>`__ label or not.
|
||||
|
||||
:param label: Label to validate.
|
||||
:raises pathvalidate.ValidationError:
|
||||
If invalid character(s) found in the ``label`` for a LTSV format label.
|
||||
"""
|
||||
|
||||
validate_pathtype(label, allow_whitespaces=False, error_msg="label is empty")
|
||||
|
||||
match_list = __RE_INVALID_LTSV_LABEL.findall(preprocess(label))
|
||||
if match_list:
|
||||
raise InvalidCharError(
|
||||
"invalid character found for a LTSV format label: {}".format(match_list)
|
||||
)
|
||||
|
||||
|
||||
def sanitize_ltsv_label(label: str, replacement_text: str = "") -> str:
|
||||
"""
|
||||
Replace all of the symbols in text.
|
||||
|
||||
:param label: Input text.
|
||||
:param replacement_text: Replacement text.
|
||||
:return: A replacement string.
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
validate_pathtype(label, allow_whitespaces=False, error_msg="label is empty")
|
||||
|
||||
return __RE_INVALID_LTSV_LABEL.sub(replacement_text, preprocess(label))
|
110
resources/lib/pathvalidate/_symbol.py
Normal file
110
resources/lib/pathvalidate/_symbol.py
Normal file
|
@ -0,0 +1,110 @@
|
|||
"""
|
||||
.. codeauthor:: Tsuyoshi Hombashi <tsuyoshi.hombashi@gmail.com>
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Sequence
|
||||
|
||||
from ._common import ascii_symbols, preprocess, unprintable_ascii_chars
|
||||
from .error import InvalidCharError
|
||||
|
||||
|
||||
__RE_UNPRINTABLE = re.compile(
|
||||
"[{}]".format(re.escape("".join(unprintable_ascii_chars))), re.UNICODE
|
||||
)
|
||||
__RE_SYMBOL = re.compile(
|
||||
"[{}]".format(re.escape("".join(ascii_symbols + unprintable_ascii_chars))), re.UNICODE
|
||||
)
|
||||
|
||||
|
||||
def validate_unprintable(text: str) -> None:
|
||||
# deprecated
|
||||
match_list = __RE_UNPRINTABLE.findall(preprocess(text))
|
||||
if match_list:
|
||||
raise InvalidCharError("unprintable character found: {}".format(match_list))
|
||||
|
||||
|
||||
def replace_unprintable(text: str, replacement_text: str = "") -> str:
|
||||
# deprecated
|
||||
try:
|
||||
return __RE_UNPRINTABLE.sub(replacement_text, preprocess(text))
|
||||
except (TypeError, AttributeError):
|
||||
raise TypeError("text must be a string")
|
||||
|
||||
|
||||
def validate_symbol(text: str) -> None:
|
||||
"""
|
||||
Verifying whether symbol(s) included in the ``text`` or not.
|
||||
|
||||
Args:
|
||||
text:
|
||||
Input text to validate.
|
||||
|
||||
Raises:
|
||||
ValidationError (ErrorReason.INVALID_CHARACTER):
|
||||
If symbol(s) included in the ``text``.
|
||||
"""
|
||||
|
||||
match_list = __RE_SYMBOL.findall(preprocess(text))
|
||||
if match_list:
|
||||
raise InvalidCharError("invalid symbols found: {}".format(match_list))
|
||||
|
||||
|
||||
def replace_symbol(
|
||||
text: str,
|
||||
replacement_text: str = "",
|
||||
exclude_symbols: Sequence[str] = [],
|
||||
is_replace_consecutive_chars: bool = False,
|
||||
is_strip: bool = False,
|
||||
) -> str:
|
||||
"""
|
||||
Replace all of the symbols in the ``text``.
|
||||
|
||||
Args:
|
||||
text:
|
||||
Input text.
|
||||
replacement_text:
|
||||
Replacement text.
|
||||
exclude_symbols:
|
||||
Symbols that exclude from the replacement.
|
||||
is_replace_consecutive_chars:
|
||||
If |True|, replace consecutive multiple ``replacement_text`` characters
|
||||
to a single character.
|
||||
is_strip:
|
||||
If |True|, strip ``replacement_text`` from the beginning/end of the replacement text.
|
||||
|
||||
Returns:
|
||||
A replacement string.
|
||||
|
||||
Example:
|
||||
|
||||
:ref:`example-sanitize-symbol`
|
||||
"""
|
||||
|
||||
if exclude_symbols:
|
||||
regexp = re.compile(
|
||||
"[{}]".format(
|
||||
re.escape(
|
||||
"".join(set(ascii_symbols + unprintable_ascii_chars) - set(exclude_symbols))
|
||||
)
|
||||
),
|
||||
re.UNICODE,
|
||||
)
|
||||
else:
|
||||
regexp = __RE_SYMBOL
|
||||
|
||||
try:
|
||||
new_text = regexp.sub(replacement_text, preprocess(text))
|
||||
except TypeError:
|
||||
raise TypeError("text must be a string")
|
||||
|
||||
if not replacement_text:
|
||||
return new_text
|
||||
|
||||
if is_replace_consecutive_chars:
|
||||
new_text = re.sub("{}+".format(re.escape(replacement_text)), replacement_text, new_text)
|
||||
|
||||
if is_strip:
|
||||
new_text = new_text.strip(replacement_text)
|
||||
|
||||
return new_text
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue