168 Commits

Author SHA1 Message Date
mcarlton00
d45c45a868 Merge pull request #107 from jellyfin/prepare-0.4.7
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
Prepare for release v0.4.7
2021-12-12 17:39:06 -05:00
jellyfin-bot
3b646da0a8 bump version to 0.4.7 2021-12-12 22:37:27 +00:00
mcarlton00
6121537216 Merge pull request #106 from mcarlton00/track-ordering
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
Only set the cast if they exist
2021-12-12 17:04:12 -05:00
mcarlton00
2482f11a5a Merge pull request #105 from mcarlton00/stopped-playback-v2
Fix stopped playback reporting
2021-12-12 17:03:19 -05:00
Matt
ade08f74a4 Only set the cast if they exist 2021-12-12 13:43:45 -05:00
Matt
5eade9abe5 Fix stopped playback reporting 2021-12-11 21:00:28 -05:00
mcarlton00
203986d54c Merge pull request #104 from mcarlton00/exclude-git-history
Exclude git history from published addon
2021-12-09 20:14:30 -05:00
Matt
8e8c376df3 Exclude git history from published addon 2021-12-09 18:31:18 -05:00
mcarlton00
8a6886c71d Merge pull request #103 from jellyfin/prepare-0.4.5
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
Prepare for release v0.4.5
2021-12-07 14:04:00 -05:00
jellyfin-bot
7f02ca1bca bump version to 0.4.5 2021-12-07 18:57:55 +00:00
mcarlton00
7df265b357 Merge pull request #102 from mcarlton00/playlist-reporting
Report tracks correctly when a playlist is playing
2021-12-07 13:46:53 -05:00
oxixes
5427168f01 Translated using Weblate (Spanish)
Currently translated at 88.8% (238 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/es/
2021-12-07 13:05:49 -05:00
Matt
8ce7d851cc Address linting issues 2021-12-05 22:43:21 -05:00
mcarlton00
22d3a23099 Merge pull request #101 from mcarlton00/image-cache-fix
Fix image caching
2021-12-05 22:30:28 -05:00
Matt
b6dd0285a8 Update string format method 2021-12-05 22:22:09 -05:00
Matt
9d45d42efe Report tracks correctly when a playlist is playing 2021-12-05 22:16:15 -05:00
Matt
6e6e753475 Remove unneeded double_urlencode function 2021-12-05 17:48:13 -05:00
mcarlton00
3b11c931d4 Merge pull request #95 from mcarlton00/stop-playback-reporting
Properly report stopped playback to the server
2021-12-04 12:12:58 -05:00
mcarlton00
742fbb224f Merge pull request #100 from mcarlton00/ci-dependencies
Ci dependencies
2021-12-04 11:44:48 -05:00
Matt
b35adac318 Stop failing on unimportant lines 2021-12-04 10:47:53 -05:00
Matt
c33274709e Add tox file for flake8 config 2021-12-04 10:25:24 -05:00
Matt
89748156a6 Fix lint 2021-12-04 10:13:07 -05:00
Matt
c01a792e25 put pyyaml back for build script 2021-12-04 10:00:29 -05:00
Matt
e4d0937782 Install required packages for CI to work properly 2021-12-04 09:58:17 -05:00
mcarlton00
524110dee9 Merge pull request #96 from mcarlton00/move-to-actions
Migrate CI to github actions
2021-12-04 08:54:27 -05:00
Alfonso Scarpino
ae480283a3 Translated using Weblate (Italian)
Currently translated at 38.8% (104 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/it/
2021-12-02 13:05:45 -05:00
Alfonso Scarpino
ccaf5878ae Translated using Weblate (Italian)
Currently translated at 20.1% (54 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/it/
2021-11-27 11:05:42 -05:00
Marcin Woliński
cb67d4b194 Translated using Weblate (Polish)
Currently translated at 60.0% (161 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/pl/
2021-11-22 22:05:41 -05:00
Marcin Woliński
701ca68db7 Added translation using Weblate (Polish) 2021-11-21 20:40:14 -05:00
Matt
cf9c3290b5 Migrate CI to github actions 2021-11-21 15:43:29 -05:00
Alfonso Scarpino
964994dd90 Translated using Weblate (Italian)
Currently translated at 14.1% (38 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/it/
2021-11-21 14:05:41 -05:00
Matt
edbd3d37da Properly report stopped playback to the server 2021-11-21 11:41:25 -05:00
mcarlton00
c2d36e2ac2 Merge pull request #93 from mcarlton00/reconnect-websockets
Attempt to reestablish websocket connection if it fails
2021-11-21 09:06:25 -05:00
mcarlton00
b2c0caaa43 Merge pull request #91 from mcarlton00/nextup-widget-error
Move inprogress call into relevant if block
2021-11-21 09:02:49 -05:00
mcarlton00
8155e77210 Merge pull request #90 from mcarlton00/respect-limits
Make API calls respect limits indicated in UI
2021-11-21 09:00:11 -05:00
Matt
fdda442dc8 Reset websocket retry count on successful connection 2021-11-20 09:40:33 -05:00
wolong gl
b962d9597b Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (268 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/zh_Hans/
2021-11-16 21:05:40 -05:00
Matt
9e624d0db2 Attempt to reestablish websocket connection if it fails 2021-11-14 12:57:32 -05:00
WWWesten
4f52ba2d4d Translated using Weblate (Russian)
Currently translated at 100.0% (268 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/ru/
2021-11-14 08:05:42 -05:00
Csaba
c9a22c517c Translated using Weblate (Hungarian)
Currently translated at 100.0% (268 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/hu/
2021-11-14 08:05:41 -05:00
WWWesten
023cd5f720 Translated using Weblate (Esperanto)
Currently translated at 100.0% (268 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/eo/
2021-11-14 08:05:39 -05:00
WWWesten
8b2b03bf0a Translated using Weblate (Kazakh)
Currently translated at 100.0% (268 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/kk/
2021-11-14 08:05:38 -05:00
Moritz
2a14caceeb Translated using Weblate (German)
Currently translated at 12.6% (34 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/de/
2021-11-12 14:59:18 -05:00
WWWesten
0fd3687843 Translated using Weblate (English)
Currently translated at 100.0% (268 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/en/
2021-11-12 14:59:18 -05:00
WWWesten
d1c205a588 Added translation using Weblate (Italian) 2021-11-12 13:59:13 -05:00
WWWesten
21103ecaac Added translation using Weblate (Greek) 2021-11-12 05:20:33 -05:00
WWWesten
2fcbbfce27 Added translation using Weblate (Dutch) 2021-11-12 05:19:26 -05:00
WWWesten
5e8eda0ea4 Added translation using Weblate (Turkish) 2021-11-09 12:52:44 -05:00
WWWesten
1e9ea80685 Added translation using Weblate (Ukrainian) 2021-11-09 12:52:16 -05:00
WWWesten
427ad71880 Added translation using Weblate (German) 2021-11-09 02:24:40 -05:00
WWWesten
fbe2ebe98f Added translation using Weblate (Hindi) 2021-11-09 02:24:02 -05:00
WWWesten
8b8d61eacf Translated using Weblate (Esperanto)
Currently translated at 68.2% (183 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/eo/
2021-11-08 23:12:33 -05:00
WWWesten
7d07980a15 Translated using Weblate (Esperanto)
Currently translated at 64.5% (173 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/eo/
2021-11-08 14:00:33 -05:00
WWWesten
84829ba83d Translated using Weblate (Esperanto)
Currently translated at 59.3% (159 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/eo/
2021-11-08 07:38:38 -05:00
WWWesten
bb3e52d27f Translated using Weblate (Esperanto)
Currently translated at 54.1% (145 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/eo/
2021-11-08 06:41:29 -05:00
WWWesten
63d2a4cffc Translated using Weblate (English)
Currently translated at 100.0% (268 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/en/
2021-11-08 06:41:28 -05:00
WWWesten
5c23ac47b2 Added translation using Weblate (Chinese (Traditional)) 2021-11-08 04:00:56 -05:00
WWWesten
a52a4a47f3 Added translation using Weblate (Arabic) 2021-11-08 04:00:14 -05:00
Adam Bokor
5f5328a280 Added translation using Weblate (Hungarian) 2021-11-03 11:58:35 -04:00
Matt
dd81b1babf Move inprogress call into relevant if block 2021-10-30 19:08:48 -04:00
Matt
8adf8a2a05 Make API calls respect limits indicated in UI 2021-10-30 18:51:38 -04:00
Abby
4c8914ad8d Merge pull request #89 from mcarlton00/zip-name
Correct addon name in build.py
2021-10-30 17:11:22 +01:00
Matt
cfe36f16f2 Correct addon name in build.py 2021-10-30 11:56:12 -04:00
mcarlton00
46a6d84101 Merge pull request #82 from Ozymandyaz/widgets
Combine NextUp and InProgress
2021-10-23 21:08:15 -04:00
Jessica
2a5dd1c418 Added translation using Weblate (Welsh) 2021-10-18 13:58:08 -04:00
WWWesten
61253d7c9d Added translation using Weblate (Esperanto) 2021-10-18 06:09:23 -04:00
mcarlton00
d4b7262105 Merge pull request #88 from oddstr13/fix/#87
Fix #87
2021-10-09 19:33:50 -04:00
Odd Stråbø
a644d4ffda Fix #87 2021-10-09 21:55:01 +02:00
mcarlton00
84ea523d16 Merge pull request #85 from mcarlton00/default-screensaver-settings
Disable screensaver settings by default
2021-10-05 22:56:10 -04:00
ozymandyaz
3ea93cbf13 Combine InProgress and NextUp 2021-10-05 14:10:52 -04:00
Ozymandyaz
3f4dc08dc7 Apply suggestions from code review
Co-authored-by: mcarlton00 <mcarlton00@gmail.com>
2021-10-05 12:29:38 -04:00
Matt
ee8ae6f492 Disable screensaver settings by default 2021-10-04 19:49:47 -04:00
ozymandyaz
984c2dab54 remove comment 2021-10-04 13:39:51 -04:00
mcarlton00
d215d087b3 Merge pull request #84 from mcarlton00/direct-play-fix
Fix direct play logic for 10.8
2021-10-02 10:42:18 -04:00
mcarlton00
42187327d6 Update resources/lib/utils.py
Co-authored-by: Claus Vium <cvium@users.noreply.github.com>
2021-10-02 09:53:12 -04:00
Matt
58a256c121 Fix direct play logic for 10.8 2021-10-01 17:49:19 -04:00
cocool97
e3ec31ae99 Added translation using Weblate (French) 2021-09-12 03:52:41 -04:00
Ozymandyaz
f81301f62d Update widgets.py 2021-09-11 11:03:20 -04:00
Ozymandyaz
b7601fda7b Combine NextUp and InProgress 2021-08-30 16:22:14 -04:00
WWWesten
e3b205046b Added translation using Weblate (Spanish) 2021-07-31 07:14:47 -04:00
WWWesten
2c70cedaa6 Added translation using Weblate (Russian) 2021-07-17 15:20:31 -04:00
WWWesten
cea6c532e0 Added translation using Weblate (Kazakh) 2021-07-17 15:20:20 -04:00
wolong gl
2e28b5904d Added translation using Weblate (Chinese (Simplified)) 2021-07-12 06:03:46 -04:00
mcarlton00
4288c032db Merge pull request #74 from mcarlton00/version-0.4.4
Version bump
2021-05-31 12:55:21 -04:00
Matt
04a5378a87 Version bump 2021-05-31 12:48:13 -04:00
mcarlton00
ca5918ded9 Merge pull request #73 from mcarlton00/next-episodes
Fix next episode dialog
2021-05-29 21:12:37 -04:00
Matt
2e7737c1af Use variable instead of calling player multiple times 2021-05-29 20:46:57 -04:00
Matt
441bb10624 Fix next episode dialog 2021-05-28 23:33:13 -04:00
mcarlton00
9adb23b280 Merge pull request #72 from mcarlton00/external-subs-again
Include position ticks in external sub call
2021-05-28 09:21:12 -04:00
Matt
7b547b2bc8 Include position ticks in external sub call 2021-05-27 22:45:37 -04:00
mcarlton00
4ec75ad266 Merge pull request #67 from nvllsvm/img
optimize images
2021-04-19 19:43:21 -04:00
Andrew Rabert
7dcf68d2be optimize images 2021-04-18 13:14:00 -04:00
mcarlton00
9199eb4290 Merge pull request #65 from mcarlton00/version-bump-0.4.3
Version bump
2021-04-18 11:19:32 -04:00
Matt
8831af3fb4 Version bump 2021-04-18 11:15:27 -04:00
mcarlton00
20b1686b04 Merge pull request #63 from mcarlton00/public-users
Fix displaying public user list
2021-04-18 11:05:11 -04:00
mcarlton00
ae028f485a Merge pull request #62 from danieladov/master
Change unicode to str
2021-04-18 11:04:48 -04:00
Matt
d5af0c8d7e Fix display public user list 2021-04-14 19:37:41 -04:00
Mister Rajoy
e596998a72 Change str to ensure_text to keep compatibility 2021-03-18 16:57:39 +01:00
Mister Rajoy
f224c0b94a Change unicode to str
Python 3 renamed the unicode type to str
2021-03-17 17:42:18 +01:00
Abby
bc06467784 Merge pull request #61 from mcarlton00/version-bump-0.4.2
Version bump to 0.4.2
2021-03-16 01:08:27 +00:00
Matt
b2f369de10 Version bump 2021-03-15 20:57:48 -04:00
mcarlton00
0e070308db Merge pull request #60 from danieladov/master
Remove multicast socket options from autodiscovery
2021-03-15 10:42:48 -04:00
Mister Rajoy
1b7c3ffae0 Remove multicast socket options from autodiscovery 2021-03-15 15:36:23 +01:00
mcarlton00
1069bf73e7 Merge pull request #58 from mcarlton00/version-0.4.1
Version bump
2021-03-08 17:10:57 -05:00
Matt
483b708def Version bump 2021-03-08 17:08:21 -05:00
mcarlton00
be12c0d21f Merge pull request #57 from mcarlton00/strings-bytes
Fix browsing by pages
2021-03-06 12:51:29 -05:00
Matt
bc57964aed Fix browsing by pages 2021-03-04 19:02:24 -05:00
mcarlton00
a6f2abaab9 Merge pull request #55 from mcarlton00/version-bump-0.4.0
Version bump - 0.4.0
2021-03-02 21:01:10 -05:00
Matt
304ff1a42c Version bump 2021-03-02 20:57:55 -05:00
mcarlton00
a5048b317d Merge pull request #54 from mcarlton00/matrix-dialogs
Fix yes/no dialogs in kodi 19
2021-02-28 19:58:22 -05:00
Matt
f42b5c2a99 Fix yes/no dialogs in kodi 19 2021-02-28 19:29:55 -05:00
mcarlton00
5827b42732 Merge pull request #53 from mcarlton00/build-script
Add build script and set up pipeline
2021-02-28 18:55:40 -05:00
Matt
6e62571cce Fix folder name in build pipeline 2021-02-28 17:18:06 -05:00
Matt
a68e42657f Remove commented code in build script 2021-02-28 17:11:42 -05:00
Matt
bad47421c0 Add build script and set up pipeline 2021-02-28 16:58:59 -05:00
mcarlton00
757f0a411c Merge pull request #52 from mcarlton00/future-imports
Use future imports for all library files
2021-02-25 20:12:47 -05:00
Matt
cba411658f Use future imports for all library files 2021-02-25 20:00:26 -05:00
mcarlton00
e560b1e591 Merge pull request #50 from mcarlton00/py3
Add support for Kodi Matrix
2021-02-24 21:52:56 -05:00
Matt
e280b82582 Fix sonarcloud bugs 2021-02-16 18:54:46 -05:00
Matt
a49900a2d7 More commented out code 2021-02-14 11:17:42 -05:00
Matt
8ece4ae651 Remove commented blocks of code 2021-02-14 11:14:03 -05:00
Matt
1949e8a9b7 Use upstream websockets library 2021-02-13 23:11:27 -05:00
Matt
52207a5ed8 Update cache dialog box for kodi 19 2021-02-13 19:19:15 -05:00
Matt
f90db72f8b End playback monitoring thread on Kodi exit 2021-01-30 23:28:01 -05:00
Matt
d298b4caa2 Fix hanging Kodi on exit 2021-01-30 23:27:16 -05:00
Matt
8109f5ae41 Move to upstream websocket library 2021-01-30 23:26:12 -05:00
Matt
e4ba7b0eba Fix deprecated abort system 2021-01-26 22:35:37 -05:00
Matt
ed3087a222 String manipulations and encoding fixes 2021-01-26 22:34:51 -05:00
Matt
c6f6601f3c Working playback 2021-01-26 22:34:39 -05:00
Matt
fb6a1c1329 Initial py3 pass 2021-01-02 23:04:24 -05:00
mcarlton00
920c012338 Merge pull request #44 from mcarlton00/translation-variable-replacements
Stop doing string manipulations on translations
2021-01-02 23:02:02 -05:00
Matt
b629756f3e Add log message for deleting items 2021-01-02 18:18:21 -05:00
Matt
0cf4643d5f Remove %s from languages file 2021-01-02 18:13:07 -05:00
Matt
73d757122a Stop doing string manipulations on translations 2021-01-02 18:10:59 -05:00
Matt
975c953d78 Version bump to 0.3.1 2020-12-23 21:33:39 -05:00
mcarlton00
9de1af4204 Merge pull request #40 from mcarlton00/external-sub-names
Show proper language names for external subs
2020-12-23 21:32:49 -05:00
Matt
7b7502fa2f Show proper language names for external subs 2020-12-19 17:07:16 -05:00
mcarlton00
b565005219 Merge pull request #39 from mcarlton00/clone-skin-fix
Fix clone skin function
2020-12-19 11:38:16 -05:00
Matt
68008c675e Fix errors when cloning default skin 2020-12-18 19:53:14 -05:00
mcarlton00
2cf86eb6ae Merge pull request #37 from mcarlton00/urlencoding-auth-is-dumb
Don't urlencode auth json payload
2020-12-18 17:18:00 -05:00
Matt
b0a1f9a680 Don't urlencode auth json payload 2020-12-17 23:29:26 -05:00
Matt
3f7816762e Version bump to 0.3.0 2020-12-11 20:33:51 -05:00
mcarlton00
8bade51eb5 Merge pull request #33 from mcarlton00/10.7-fixes
10.7 fixes
2020-12-11 20:30:49 -05:00
Matt
7c4398bfb5 When playback stops, only try to delete a transcode if we're transcoding 2020-12-05 18:30:40 -05:00
Matt
8f736e8bd3 We need the brackets for later 2020-12-05 18:28:20 -05:00
Matt
65a9b11dc5 Include URL when there's been an http error 2020-12-05 18:27:36 -05:00
Matt
7ffd16df4b Remove manually specifying return payload 2020-12-05 18:26:53 -05:00
mcarlton00
e2628d27dc Don't error on empty user list 2020-11-28 15:00:29 -05:00
mcarlton00
8799c2bb5e Use correct lookup URL 2020-11-28 14:59:55 -05:00
Matt
4ba0b64d2c Update auth for 10.7 2020-11-23 17:58:45 -05:00
mcarlton00
4b2f43e8a2 Merge pull request #32 from mcarlton00/json-payloads
Proper API json parsing
2020-11-15 14:44:11 -05:00
Matt
df774ca3c5 Simplify logic checks 2020-11-15 14:13:45 -05:00
Matt
084fab576e Remove debug statement 2020-11-15 11:04:13 -05:00
mcarlton00
1733e64403 Parse json payloads in centralized place 2020-11-11 22:50:26 -05:00
mcarlton00
1d0360c0c3 Merge pull request #29 from mcarlton00/words-r-hard
Connect to servers with special characters in the name
2020-10-06 22:22:34 -04:00
Matt
45823ccd96 Connect to servers with special characters in the name 2020-10-06 21:43:36 -04:00
mcarlton00
ef3b64cf51 Merge pull request #25 from mcarlton00/castaway
Fix casting from web UI
2020-09-10 17:41:24 -04:00
mcarlton00
a424fb8793 Merge pull request #24 from mcarlton00/when-is-a-string-not-a-string
Use future strings to fix unicode errors
2020-09-10 17:40:51 -04:00
mcarlton00
b6ae819d32 Merge pull request #23 from mcarlton00/i-know-my-abcs
Fix browsing libraries by letter
2020-09-10 17:39:41 -04:00
Matt
8711ae2452 Fix casting from web UI 2020-09-05 17:49:18 -04:00
Matt
a90c2c2fa8 Use future strings to fix unicode errors 2020-09-05 17:29:38 -04:00
Matt
03a89d4f43 Remove unnecessary log line 2020-09-05 16:15:54 -04:00
Matt
d48b2bdf2a Fix browsing libraries by letter 2020-09-05 16:11:15 -04:00
mcarlton00
6a6ca8c642 Merge pull request #22 from mcarlton00/noisy-logs-are-noisy
Fix log levels
2020-09-03 10:35:57 -04:00
Matt
d3ffecb866 Fix log levels 2020-09-02 23:04:13 -04:00
mcarlton00
083f91611a Merge pull request #16 from Shadowghost/websocket-url-fix
Fix websocket_url (fixes playing transcoded streams)
2020-08-18 08:29:05 -04:00
Shadowghost
01e9c45df6 Fix websocket_url (fixes playing transcoded streams) 2020-08-18 11:22:27 +02:00
Matt
b327ebc5bd Update authors field 2020-08-16 22:00:55 -04:00
73 changed files with 9350 additions and 2093 deletions

View File

@@ -1,14 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.jellycon"
name="JellyCon"
version="0.2.0"
provider-name="Team B">
version=""
provider-name="Jellyfin Contributors">
<requires>
<import addon="xbmc.python" version="2.25.0"/>
<import addon="script.module.pil" version="1.1.7"/>
<import addon="script.module.requests" version="2.22.0"/>
<import addon="script.module.six" version="1.13.0"/>
<import addon="script.module.kodi-six" />
</requires>
<extension point="xbmc.python.pluginsource" library="default.py">
<provides>video audio</provides>
@@ -23,7 +18,7 @@
<website>https://github.com/jellyfin/jellycon/wiki</website>
<source>https://github.com/jellyfin/jellycon</source>
<summary lang="en_GB">Browse and play your Jellyfin server media library.</summary>
<description lang="en_GB">An addon to allow you to browse and playback your Jellyfin (www.jellyfin.org) Movie, TV Show and Music collections.</description>
<description lang="en_GB">An addon to allow you to browse and playback your Jellyfin (https://jellyfin.org) Movie, TV Show and Music collections.</description>
<assets>
<icon>icon.png</icon>
<fanart>fanart.jpg</fanart>

16
.github/dependabot.yaml vendored Normal file
View File

@@ -0,0 +1,16 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
labels:
- ci
- github-actions
- package-ecosystem: pip
directory: /
schedule:
interval: weekly
labels:
- pip
- dependencies

22
.github/release-drafter.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
_extends: jellyfin/jellyfin-meta-plugins
name-template: "Release $RESOLVED_VERSION"
tag-template: "v$RESOLVED_VERSION"
version-template: "$MAJOR.$MINOR.$PATCH"
version-resolver:
major:
labels:
- 'major'
minor:
labels:
- 'minor'
patch:
labels:
- 'patch'
default: patch
template: |
## :sparkles: What's New
$CHANGES

12
.github/releasing.md vendored Normal file
View File

@@ -0,0 +1,12 @@
# Releasing a new Version via GitHub Actions
0. (optional) label the PRs you want to include in this release (if you want to group them in the GH release based on topics). \
Supported labels can be found in the Release Drafter [config-file](https://github.com/jellyfin/jellyfin-meta-plugins/blob/master/.github/release-drafter.yml) (currently inherited from `jellyfin/jellyfin-meta-plugins`)
1. ensure you have merged the PRs you want to include in the release and that the so far drafted GitHub release has captured them
2. Create a `release-prep` PR by manually triggering the 'Create Prepare-Release PR' Workflow from the Actions tab on GitHub
3. check the newly created `Prepare for release vx.y.z` PR if updated the `release.yaml` properly (update it manually if need be)
4. merge the `Prepare for release vx.y.z` and let the Actions triggered by doing that finis (should just be a couple of seconds)
5. FINALLY, trigger the `Publish JellyCon` manually from the Actions tab on GitHub.
1. this will release the up to that point drafted GitHub Release and tag the default branch accordingly
2. this will package and deploy `JellyCon` in the new version to the deployment server and trigger the 'kodirepo' script on it
6. Done, assuming everything ran successfully, you have now successfully published a new version! :tada:

87
.github/tools/reformat_changelog.py vendored Executable file
View File

@@ -0,0 +1,87 @@
#!/usr/bin/env python3.8
import argparse
import sys
import re
from typing import Dict, List, Pattern, Union, TypedDict
from emoji.core import emojize, demojize, replace_emoji
ITEM_FORMAT = "+ {title} (#{issue}) @{username}"
OUTPUT_EMOJI = False
ITEM_PATTERN: Pattern = re.compile(
r"^\s*(?P<old_listchar>[-*+])\s*(?P<title>.*?)\s*\(#(?P<issue>[0-9]+)\)\s*@(?P<username>[^\s]*)$"
)
class SectionType(TypedDict):
title: str
items: List[Dict[str, str]]
def reformat(item_format: str, output_emoji: bool) -> None:
data = [
emojize(x.strip(), use_aliases=True, variant="emoji_type")
for x in sys.stdin.readlines()
if x.strip()
]
sections = []
section: Union[SectionType, Dict] = {}
for line in data:
if line.startswith("## "):
pass
if line.startswith("### "):
if section:
sections.append(section)
_section: SectionType = {
"title": line.strip("# "),
"items": [],
}
section = _section
m = ITEM_PATTERN.match(line)
if m:
gd = m.groupdict()
section["items"].append(gd)
sections.append(section)
first = True
for section in sections:
if not section:
continue
if first:
first = False
else:
print()
title = section["title"]
if not output_emoji:
title = replace_emoji(title).strip()
print(title)
print("-" * len(title))
for item in section["items"]:
formatted_item = item_format.format(**item)
if not output_emoji:
formatted_item = demojize(formatted_item)
print(formatted_item)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--format", type=str, default=ITEM_FORMAT)
parser.add_argument("--no-emoji", dest="emoji", action="store_false")
parser.add_argument("--emoji", dest="emoji", action="store_true")
parser.set_defaults(emoji=OUTPUT_EMOJI)
args = parser.parse_args()
reformat(args.format, args.emoji)

39
.github/workflows/build.yaml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: Build JellyCon
on:
push:
branches:
- master
tags:
- '*'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
py_version: [ 'py2', 'py3' ]
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up Python 3.x
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install pyyaml
- name: Create ${{ matrix.py_version }} addon.xml
run: python build.py --version ${{ matrix.py_version }}
- name: Publish Build Artifact
uses: actions/upload-artifact@v2
with:
retention-days: 14
name: ${{ matrix.py_version }}-build-artifact
path: |
*.zip

41
.github/workflows/codeql.yaml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: CodeQL Analysis
on:
push:
branches:
- master
pull_request:
branches:
- master
schedule:
- cron: '38 8 * * 6'
jobs:
analyze:
runs-on: ubuntu-latest
if: ${{ github.repository == 'jellyfin/jellycon' }}
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
version: ['2.7', '3.9']
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
queries: +security-and-quality
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.version }}
- name: Autobuild
uses: github/codeql-action/autobuild@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -0,0 +1,72 @@
name: Create Prepare-Release PR
on:
workflow_dispatch:
jobs:
create_pr:
name: "Create Pump Version PR"
runs-on: ubuntu-latest
steps:
- name: Update Draft
uses: release-drafter/release-drafter@v5.15.0
id: draft
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
- name: Setup YQ
uses: chrisdickinson/setup-yq@latest
with:
yq-version: v4.9.1
- name: Checkout repository
uses: actions/checkout@v2
- name: Parse Changlog
run: |
pip install emoji
cat << EOF >> cl.md
${{ steps.draft.outputs.body }}
EOF
TAG="${{ steps.draft.outputs.tag_name }}"
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
echo "YAML_CHANGELOG<<EOF" >> $GITHUB_ENV
cat cl.md | python .github/tools/reformat_changelog.py --no-emoji >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
echo "CHANGELOG<<EOF" >> $GITHUB_ENV
cat cl.md | python .github/tools/reformat_changelog.py --emoji --format='+ #{issue} by @{username}' >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
rm cl.md
- name: Update release.yaml
run: |
yq eval '.version = env(VERSION) | .changelog = strenv(YAML_CHANGELOG) | .changelog style="literal"' -i release.yaml
- name: Commit Changes
run: |
git config user.name "jellyfin-bot"
git config user.email "team@jellyfin.org"
git checkout -b prepare-${{ env.VERSION }}
git commit -am "bump version to ${{ env.VERSION }}"
if [[ -z "$(git ls-remote --heads origin prepare-${{ env.VERSION }})" ]]; then
git push origin prepare-${{ env.VERSION }}
else
git push -f origin prepare-${{ env.VERSION }}
fi
- name: Create or Update PR
uses: k3rnels-actions/pr-update@v1
with:
token: ${{ secrets.JF_BOT_TOKEN }}
pr_title: Prepare for release ${{ steps.draft.outputs.tag_name }}
pr_source: prepare-${{ env.VERSION }}
pr_labels: 'release-prep,skip-changelog'
pr_body: |
:robot: This is a generated PR to bump the `release.yaml` version and update the changelog.
---
${{ env.CHANGELOG }}

64
.github/workflows/publish.yaml vendored Normal file
View File

@@ -0,0 +1,64 @@
name: Publish JellyCon
on:
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
strategy:
matrix:
py_version: [ 'py2', 'py3' ]
steps:
- name: Update Draft
uses: release-drafter/release-drafter@v5.15.0
if: ${{ matrix.py_version == 'py3' }}
with:
publish: true
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up Python 3.x
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install pyyaml
- name: Create ${{ matrix.py_version }} addon.xml
run: python build.py --version ${{ matrix.py_version }}
- name: Publish Build Artifact
uses: actions/upload-artifact@v2
with:
retention-days: 14
name: ${{ matrix.py_version }}-build-artifact
path: |
*.zip
- name: Upload to repo server
uses: burnett01/rsync-deployments@5.1
with:
switches: -vrptz
path: '*.zip'
remote_path: /srv/repository/incoming/kodi
remote_host: ${{ secrets.DEPLOY_HOST }}
remote_user: ${{ secrets.DEPLOY_USER }}
remote_key: ${{ secrets.DEPLOY_KEY }}
- name: Add to Kodi repo and clean up
uses: appleboy/ssh-action@v0.1.4
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_KEY }}
script_stop: true
script: |
python3 /usr/local/bin/kodirepo add /srv/repository/incoming/kodi/plugin.video.jellycon+${{ matrix.py_version }}.zip --datadir /srv/repository/releases/client/kodi/${{ matrix.py_version }};
rm /srv/repository/incoming/kodi/plugin.video.jellycon+${{ matrix.py_version }}.zip;

16
.github/workflows/release-drafter.yaml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: Release Drafter
on:
push:
branches:
- master
jobs:
update_release_draft:
name: Update release draft
runs-on: ubuntu-latest
steps:
- name: Update Release Draft
uses: release-drafter/release-drafter@v5.15.0
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}

49
.github/workflows/test.yaml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: Test JellyCon
on:
push:
branches:
- master
pull_request:
branches:
- master
env:
PR_TRIGGERED: ${{ github.event_name == 'pull_request' && github.repository == 'jellyfin/jellycon' }}
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
py_version: ['2.7', '3.9']
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up Python ${{ matrix.py_version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.py_version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements-dev.txt
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics --output-file=flake8.output
cat flake8.output
- name: Publish Test Atrifact
uses: actions/upload-artifact@v2
with:
retention-days: 14
name: ${{ matrix.py_version }}-test-results
path: |
flake8.output

3
.gitignore vendored
View File

@@ -220,3 +220,6 @@ pip-log.txt
#Mr Developer
.mr.developer.cfg
# Addon files
addon.xml

144
build.py Executable file
View File

@@ -0,0 +1,144 @@
#!/usr/bin/env python
import argparse
import os
import xml.etree.ElementTree as ET
import zipfile
from datetime import datetime
from pathlib import Path
import yaml
def indent(elem: ET.Element, level: int = 0) -> None:
"""
Nicely formats output xml with newlines and spaces
https://stackoverflow.com/a/33956544
"""
i = "\n" + level * " "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
if not elem.tail or not elem.tail.strip():
elem.tail = i
for elem in elem:
indent(elem, level + 1)
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
def create_addon_xml(config: dict, source: str, py_version: str) -> None:
"""
Create addon.xml from template file
"""
# Load template file
with open('{}/.config/template.xml'.format(source), 'r') as f:
tree = ET.parse(f)
root = tree.getroot()
# Populate dependencies in template
dependencies = config['dependencies'].get(py_version)
for dep in dependencies:
ET.SubElement(root.find('requires'), 'import', attrib=dep)
# Populate version string
addon_version = config.get('version')
root.attrib['version'] = '{}+{}'.format(addon_version, py_version)
# Populate Changelog
date = datetime.today().strftime('%Y-%m-%d')
changelog = config.get('changelog')
for section in root.findall('extension'):
news = section.findall('news')
if news:
news[0].text = 'v{} ({}):\n{}'.format(addon_version, date, changelog)
# Format xml tree
indent(root)
# Write addon.xml
tree.write('{}/addon.xml'.format(source), encoding='utf-8', xml_declaration=True)
def zip_files(py_version: str, source: str, target: str, dev: bool) -> None:
"""
Create installable addon zip archive
"""
archive_name = 'plugin.video.jellycon+{}.zip'.format(py_version)
with zipfile.ZipFile('{}/{}'.format(target, archive_name), 'w') as z:
for root, dirs, files in os.walk(args.source):
for filename in filter(file_filter, files):
file_path = os.path.join(root, filename)
if dev or folder_filter(file_path):
relative_path = os.path.join('plugin.video.jellycon', os.path.relpath(file_path, source))
z.write(file_path, relative_path)
def file_filter(file_name: str) -> bool:
"""
True if file_name is meant to be included
"""
return (
not (file_name.startswith('plugin.video.jellycon') and file_name.endswith('.zip'))
and not file_name.endswith('.pyo')
and not file_name.endswith('.pyc')
and not file_name.endswith('.pyd')
)
def folder_filter(folder_name: str) -> bool:
"""
True if folder_name is meant to be included
"""
filters = [
'.ci',
'.git',
'.github',
'.config',
'.mypy_cache',
'.pytest_cache',
'__pycache__',
]
for f in filters:
if f in folder_name.split(os.path.sep):
return False
return True
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Build flags:')
parser.add_argument(
'--version',
type=str,
choices=('py2', 'py3'),
default='py3')
parser.add_argument(
'--source',
type=Path,
default=Path(__file__).absolute().parent)
parser.add_argument(
'--target',
type=Path,
default=Path(__file__).absolute().parent)
parser.add_argument('--dev', dest='dev', action='store_true')
parser.set_defaults(dev=False)
args = parser.parse_args()
# Load config file
config_path = os.path.join(args.source, 'release.yaml')
with open(config_path, 'r') as fh:
release_config = yaml.safe_load(fh)
create_addon_xml(release_config, args.source, args.version)
zip_files(args.version, args.source, args.target, args.dev)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 32 KiB

BIN
icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
kodi.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 15 KiB

35
release.yaml Normal file
View File

@@ -0,0 +1,35 @@
version: '0.4.7'
changelog: ""
dependencies:
py2:
- addon: 'xbmc.python'
version: '2.25.0'
- addon: 'script.module.requests'
version: '2.22.0'
- addon: 'script.module.dateutil'
version: '2.8.1'
- addon: 'script.module.six'
version: '1.13.0'
- addon: 'script.module.kodi-six'
version: '0.0.7'
- addon: 'script.module.addon.signals'
version: '0.0.5'
- addon: 'script.module.futures'
version: '2.2.0'
- addon: 'script.module.websocket'
version: '0.57.0'
py3:
- addon: 'xbmc.python'
version: '3.0.0'
- addon: 'script.module.requests'
version: '2.22.0+matrix.1'
- addon: 'script.module.dateutil'
version: '2.8.1+matrix.1'
- addon: 'script.module.six'
version: '1.14.0+matrix.2'
- addon: 'script.module.kodi-six'
version: '0.1.3+1'
- addon: 'script.module.addon.signals'
version: '0.0.5+matrix.1'
- addon: 'script.module.websocket'
version: '0.57.0+matrix.1'

16
requirements-dev.txt Normal file
View File

@@ -0,0 +1,16 @@
pyyaml
setuptools >= 44.1.1 # Old setuptools causes script.module.addon.signals to fail installing
six >= 1.13
python-dateutil >= 2.8.1
requests >= 2.22
futures >= 2.2; python_version < '3.0'
Kodistubs ~= 18.0; python_version < '3.0'
Kodistubs ~= 19.0; python_version >= '3.6'
git+https://github.com/romanvm/kodi.six
git+https://github.com/ruuk/script.module.addon.signals
flake8 >= 3.8
flake8-import-order >= 0.18
websocket-client >= 0.57.0

View File

@@ -0,0 +1,2 @@
msgid ""
msgstr "X-Generator: Weblate\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit"

View File

@@ -0,0 +1,2 @@
msgid ""
msgstr "X-Generator: Weblate\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit"

View File

@@ -0,0 +1,149 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2021-11-12 19:59+0000\n"
"Last-Translator: Moritz <moritz.leick@googlemail.com>\n"
"Language-Team: German <https://translate.jellyfin.org/projects/jellycon/"
"jellycon/de/>\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.5.2\n"
msgctxt "#30120"
msgid "Show load progress"
msgstr "Anzeige des Ladefortschritts"
msgctxt "#30114"
msgid "Jump back seconds"
msgstr "Sekunden zurückspringen"
msgctxt "#30113"
msgid "Retrieving Data"
msgstr "Abrufen von Daten"
msgctxt "#30112"
msgid "Loading Content"
msgstr "Laden von Inhalten"
msgctxt "#30111"
msgid "Services"
msgstr "Dienste"
msgctxt "#30110"
msgid "Interface"
msgstr "Schnittstelle"
msgctxt "#30092"
msgid "Warning: This action will delete the media files from the server."
msgstr ""
"Warnung: Bei dieser Aktion werden die Mediendateien vom Server gelöscht."
msgctxt "#30091"
msgid "Confirm delete?"
msgstr "Bestätigen Sie das Löschen?"
msgctxt "#30053"
msgid "Waiting for server to delete"
msgstr "Warten auf Server zum Löschen"
msgctxt "#30052"
msgid "Deleting"
msgstr "Löschen"
msgctxt "#30045"
msgid "Username not found"
msgstr "Benutzername nicht gefunden"
msgctxt "#30044"
msgid "Incorrect Username/Password"
msgstr "Falscher Benutzername/Passwort"
msgctxt "#30027"
msgid "Enable debug logging"
msgstr "Debug-Protokollierung einschalten"
msgctxt "#30025"
msgid "Password:"
msgstr "Passwort:"
msgctxt "#30024"
msgid "Username:"
msgstr "Benutzername:"
msgctxt "#30023"
msgid "Hide unwatched episode details"
msgstr "Details zu nicht gesehenen Episoden ausblenden"
msgctxt "#30022"
msgid "Advanced"
msgstr "Erweiterte"
msgctxt "#30020"
msgid "Flatten single season"
msgstr "Einzelne Staffel reduzieren"
msgctxt "#30019"
msgid "Filtered episode name format"
msgstr "Gefiltertes Format für Episodennamen"
msgctxt "#30018"
msgid "Number of items to show in filtered lists"
msgstr "Anzahl der Elemente, die in gefilterten Listen angezeigt werden sollen"
msgctxt "#30017"
msgid "Show connected clients"
msgstr "Verbundene Clients anzeigen"
msgctxt "#30016"
msgid "Device display name"
msgstr "Anzeigename des Geräts"
msgctxt "#30015"
msgid "Log timing data"
msgstr "Zeitdaten protokollieren"
msgctxt "#30014"
msgid "Jellyfin"
msgstr "Jellyfin"
msgctxt "#30012"
msgid "[Change user]"
msgstr "[Benutzer wechseln]"
msgctxt "#30011"
msgid "[Detect local server]"
msgstr "[Lokalen Server ermitteln]"
msgctxt "#30010"
msgid "Number of performance profiles to capture"
msgstr "Anzahl der zu erfassenden Leistungsprofile"
msgctxt "#30008"
msgid "Samba password"
msgstr "Samba-Passwort"
msgctxt "#30007"
msgid "Samba username"
msgstr "Samba-Benutzername"
msgctxt "#30006"
msgid "Password"
msgstr "Passwort"
msgctxt "#30005"
msgid "Username"
msgstr "Benutzername"
msgctxt "#30003"
msgid "Verify HTTPS certificate"
msgstr "HTTPS-Zertifikat prüfen"
msgctxt "#30001"
msgid "Port"
msgstr "Port"
msgctxt "#30000"
msgid "Host"
msgstr "Host"

View File

@@ -0,0 +1,2 @@
msgid ""
msgstr "X-Generator: Weblate\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,988 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2021-12-07 18:05+0000\n"
"Last-Translator: oxixes <adrianquevedobenito@gmail.com>\n"
"Language-Team: Spanish <https://translate.jellyfin.org/projects/jellycon/"
"jellycon/es/>\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.5.2\n"
msgctxt "#30442"
msgid "Simple new content check"
msgstr "Comprobación simple de nuevo contenido"
msgctxt "#30440"
msgid "Play next"
msgstr "Reproducir siguiente"
msgctxt "#30439"
msgid "Show play next episode at time left"
msgstr "Mostrar reproducir siguiente episodio al tiempo restante"
#, fuzzy
msgctxt "#30438"
msgid "Play cinema intros"
msgstr "Reproducir introducciones de cine"
msgctxt "#30437"
msgid "Playback options"
msgstr "Opciones de reproducción"
msgctxt "#30436"
msgid "Speed test data size (MB)"
msgstr "Tamaño de los datos de la prueba de velocidad (MB)"
msgctxt "#30435"
msgid "Connection speed test"
msgstr "Prueba de velocidad de conexión"
msgctxt "#30434"
msgid "Force transcode stream bitrate (Kbits)"
msgstr "Tasa de bits de la transcodificación forzada (Kbps)"
msgctxt "#30433"
msgid "Allow direct file playback"
msgstr "Permitir reproducción directa de archivo"
msgctxt "#30432"
msgid "Hide watched items in lists"
msgstr "Esconder elementos vistos en las listas"
msgctxt "#30431"
msgid "Seasons"
msgstr "Temporadas"
msgctxt "#30430"
msgid "Label"
msgstr "Etiqueta"
msgctxt "#30429"
msgid "Genre"
msgstr "Género"
msgctxt "#30428"
msgid "Rating"
msgstr "Calificación"
msgctxt "#30427"
msgid "Added"
msgstr "Añadido"
msgctxt "#30426"
msgid "Title"
msgstr "Título"
msgctxt "#30425"
msgid "Year"
msgstr "Año"
msgctxt "#30424"
msgid "Default"
msgstr "Por defecto"
msgctxt "#30422"
msgid "Sorting"
msgstr "Ordenado"
msgctxt "#30420"
msgid "Audio max channels"
msgstr "Máximos canales de audio"
msgctxt "#30419"
msgid "Audio codec"
msgstr "Códec de audio"
msgctxt "#30418"
msgid "Audio bitrate (Kbits)"
msgstr "Tasa de bits de audio (Kbps)"
msgctxt "#30417"
msgid "You do not have permision to delete this item"
msgstr "No tienes permiso para borrar este elemento"
msgctxt "#30416"
msgid "HTTP timeout seconds"
msgstr "Segundos del tiempo de espera de HTTP"
msgctxt "#30415"
msgid " - Favorite Collections"
msgstr "- Colecciones favoritas"
msgctxt "#30414"
msgid " - Favorites"
msgstr "- Favoritos"
msgctxt "#30413"
msgid " - Tags"
msgstr "- Etiquetas"
msgctxt "#30412"
msgid " - Decades"
msgstr "- Décadas"
msgctxt "#30411"
msgid " - Years"
msgstr "- Años"
msgctxt "#30410"
msgid " - Collections"
msgstr "- Colecciones"
msgctxt "#30409"
msgid "Add-on Actions"
msgstr "Acciones del complemento"
msgctxt "#30407"
msgid "Global Lists"
msgstr "Listas globales"
msgctxt "#30406"
msgid "Jellyfin Libraries"
msgstr "Bibliotecas de Jellyfin"
msgctxt "#30405"
msgid " - Show All"
msgstr "- Mostrar todo"
msgctxt "#30404"
msgid " - A-Z"
msgstr "- A-Z"
msgctxt "#30403"
msgid "Movies - Recommendations"
msgstr "Películas - Recomendaciones"
msgctxt "#30402"
msgid "Add to Kodi Playlist"
msgstr "Añadir a la lista de reproducción de Kodi"
msgctxt "#30401"
msgid "Info"
msgstr "Información"
msgctxt "#30399"
msgid "Hide"
msgstr "Esconder"
msgctxt "#30398"
msgid "Refresh Jellyfin Metadata"
msgstr "Recargar metadatos de Jellyfin"
msgctxt "#30397"
msgid " - Pages"
msgstr "- Páginas"
msgctxt "#30395"
msgid "Clear cached server data"
msgstr "Borrar datos del servidor en caché"
msgctxt "#30394"
msgid "Cache files deleted"
msgstr "Archivos del caché borrados"
msgctxt "#30393"
msgid "Clear Cache Result"
msgstr "Borrar caché del resultado"
msgctxt "#30392"
msgid "HTTPS"
msgstr "HTTPS"
msgctxt "#30391"
msgid "HTTP"
msgstr "HTTP"
msgctxt "#30390"
msgid "Protocol"
msgstr "Protocolo"
msgctxt "#30389"
msgid "User details"
msgstr "Detalles del usuario"
msgctxt "#30388"
msgid "Server details"
msgstr "Detalles del servidor"
msgctxt "#30387"
msgid "Unused images removed : "
msgstr "Imágenes no usadas eliminadas:"
msgctxt "#30386"
msgid "Unused Jellyfin images : "
msgstr "Imágenes de Jellyfin no usadas:"
msgctxt "#30385"
msgid "Existing images before delete : "
msgstr "Imágenes existentes antes de borrar:"
msgctxt "#30383"
msgid "System - "
msgstr "Sistema -"
msgctxt "#30382"
msgid "Always"
msgstr "Siempre"
msgctxt "#30381"
msgid "More than one"
msgstr "Más de una"
msgctxt "#30380"
msgid "Never"
msgstr "Nunca"
msgctxt "#30378"
msgid "Persist user details"
msgstr "Conservar detalles del usuario"
msgctxt "#30377"
msgid "Sending request"
msgstr "Enviando petición"
msgctxt "#30376"
msgid "Checking server url"
msgstr "Comprobando la URL del servidor"
msgctxt "#30375"
msgid "Receiving data packet"
msgstr "Recibiendo paquete de datos"
msgctxt "#30374"
msgid "Sending request"
msgstr "Enviando petición"
msgctxt "#30373"
msgid "Scanning for local servers"
msgstr "Escaneando servidores locales"
msgctxt "#30372"
msgid "Server URL"
msgstr "URL del servidor"
msgctxt "#30371"
msgid "Could not connect to the URL you entered, do you want to try again?"
msgstr ""
"No se ha podido conectar a la URL introducida, ¿quieres intentarlo otra vez?"
msgctxt "#30370"
msgid "Do you want to manually enter a server url?"
msgstr "¿Quieres introducir una URL de servidor manualmente?"
msgctxt "#30369"
msgid "Do you want to clear your saved password?"
msgstr "¿Quieres borrar tu contraseña guardada?"
msgctxt "#30368"
msgid "Clear Password?"
msgstr "¿Borrar contraseña?"
#, fuzzy
msgctxt "#30367"
msgid "Allow fast user switching password saving"
msgstr "Permitir guardado rápido de la contraseña de usuario cambiada"
msgctxt "#30366"
msgid "Manually enter user details"
msgstr "Introduce los detalles del usuario manualmente"
msgctxt "#30365"
msgid "Manual Login"
msgstr "Inicio de sesión manual"
msgctxt "#30364"
msgid "Do you want to save the password?"
msgstr "¿Quieres guardar la contraseña?"
msgctxt "#30363"
msgid "Save Password?"
msgstr "¿Guardar contraseña?"
msgctxt "#30362"
msgid " - Recordings"
msgstr "- Grabaciones"
msgctxt "#30361"
msgid " - Programs"
msgstr "- Programas"
msgctxt "#30360"
msgid " - Channels"
msgstr "- Canales"
msgctxt "#30359"
msgid "Building full image list"
msgstr "Construyendo la lista de imágenes completa"
msgctxt "#30358"
msgid "Retreiving remote image list"
msgstr "Recuperando la lista de imágenes remota"
msgctxt "#30357"
msgid "Processing existing image list"
msgstr "Procesando la lista de imágenes existentes"
msgctxt "#30356"
msgid "Loading existing image list"
msgstr "Cargando lista de imágenes existentes"
msgctxt "#30355"
msgid "Kodi Settings->Services->Allow remote control via HTTP"
msgstr ""
"Configuración de Kodi -> Servicios -> Permitir control remoto mediante HTTP"
msgctxt "#30354"
msgid "Go To Series"
msgstr "Ir a las series"
msgctxt "#30353"
msgid " - Frequently Played"
msgstr "- Reproducido frecuentemente"
msgctxt "#30321"
msgid " - Album Artists"
msgstr "- Artistas del álbum"
msgctxt "#30319"
msgid "Music - All Album Artists"
msgstr "Música - Todos los artistas del álbum"
msgctxt "#30352"
msgid "Music - Frequently Played"
msgstr "Música - Frecuentemente reproducida"
msgctxt "#30351"
msgid "Music - Recently Played"
msgstr "Música - Recientemente reproducida"
msgctxt "#30350"
msgid "Music - Recently Added"
msgstr "Música - Recientemente añadida"
msgctxt "#30349"
msgid " - Recently Played"
msgstr "- Reproducido recientemente"
msgctxt "#30348"
msgid "Add user ratings"
msgstr "Añadir valoración de los usuarios"
msgctxt "#30347"
msgid "Getting Existing Images"
msgstr "Obteniendo las imágenes existentes"
msgctxt "#30346"
msgid "Deleteing Cached Images"
msgstr "Borrando imágenes en caché"
msgctxt "#30344"
msgid "Number of images removed from cache"
msgstr "Número de imágenes eliminadas del caché"
msgctxt "#30343"
msgid "Changes Require Kodi Restart"
msgstr "Los cambios requieren que Kodi se reinicie"
msgctxt "#30342"
msgid "New content check interval (0 = disabled)"
msgstr "Intervalo de comprobación de nuevo contenido (0 = deshabilitado)"
msgctxt "#30341"
msgid "Background image update interval (0 = disabled)"
msgstr "Intervalo de actualización de la imagen de fondo (0 = deshabilitado)"
msgctxt "#30340"
msgid "Group movies into collections"
msgstr "Agrupar películas en colecciones"
msgctxt "#30339"
msgid "Person"
msgstr "Persona"
msgctxt "#30338"
msgid "Album"
msgstr "Álbum"
msgctxt "#30337"
msgid "Song"
msgstr "Canción"
msgctxt "#30334"
msgid "Use JellyCon context menu"
msgstr "Usar el menú contextual de JellyCon"
msgctxt "#30332"
msgid "Stop media playback on screensaver activation"
msgstr "Parar la reproducción de medios al activar el salvapantallas"
msgctxt "#30331"
msgid "Movies per page"
msgstr "Películas por página"
msgctxt "#30330"
msgid "Show change user dialog"
msgstr "Mostrar diálogo de cambio de usuario"
msgctxt "#30329"
msgid "Screensaver"
msgstr "Salvapantallas"
msgctxt "#30328"
msgid "Show empty folders (shows, seasons, collections)"
msgstr "Mostrar carpetas vacías (series, temporadas, colecciones)"
msgctxt "#30327"
msgid "Go To Season"
msgstr "Ir a la temporada"
msgctxt "#30325"
msgid " - Genres"
msgstr "- Géneros"
msgctxt "#30322"
msgid "Auto resume"
msgstr "Reanudar automáticamente"
msgctxt "#30320"
msgid " - Albums"
msgstr "- Álbumes"
msgctxt "#30318"
msgid "Music - Albums"
msgstr "Música - Álbumes"
msgctxt "#30317"
msgid "Play All"
msgstr "Reproducir todo"
msgctxt "#30316"
msgid "Connection Error"
msgstr "Error de conexión"
msgctxt "#30315"
msgid "Suppress notifications for connection errors"
msgstr "Suprimir notificaciones por errores de conexión"
msgctxt "#30314"
msgid "Play"
msgstr "Reproducir"
msgctxt "#30313"
msgid "Menu"
msgstr "Menú"
msgctxt "#30312"
msgid "All - "
msgstr "Todo -"
msgctxt "#30311"
msgid "Library - "
msgstr "Biblioteca -"
msgctxt "#30310"
msgid "Enable Jellyfin remote control"
msgstr "Activar el control remoto de Jellyfin"
msgctxt "#30309"
msgid "Select Media Source"
msgstr "Elegir fuente de medios"
msgctxt "#30308"
msgid "Select Trailer"
msgstr "Elegir tráiler"
msgctxt "#30307"
msgid "Play Trailer"
msgstr "Reproducir tráiler"
msgctxt "#30306"
msgid "Playback starting"
msgstr "Empezando la reproducción"
msgctxt "#30305"
msgid "Not Found"
msgstr "No encontrado"
msgctxt "#30304"
msgid "Cached Jellyfin images : "
msgstr "Imágenes de Jellyfin en caché:"
msgctxt "#30303"
msgid "Missing Jellyfin images : "
msgstr "Imágenes de Jellyfin faltantes:"
msgctxt "#30302"
msgid "Existing images : "
msgstr "Imágenes existentes:"
msgctxt "#30301"
msgid "Caching Images"
msgstr "Guardando imágenes en caché"
msgctxt "#30300"
msgid "Cache all Jellyfin images as local Kodi images?"
msgstr ""
"¿Guardar en caché todas las imágenes de Jellyfin como imágenes de Kodi "
"locales?"
msgctxt "#30298"
msgid "Deleting Kodi Images"
msgstr "Borrando las imágenes de Kodi"
msgctxt "#30297"
msgid "Delete unused images?"
msgstr "¿Borrar imágenes no usadas?"
msgctxt "#30296"
msgid "Delete"
msgstr "Borrar"
#, fuzzy
msgctxt "#30295"
msgid "To use this feature you need HTTP control enabled"
msgstr "Para usar esta característica necesitas activar el control HTTP"
msgctxt "#30294"
msgid "Notice"
msgstr "Aviso"
msgctxt "#30293"
msgid "Cache images"
msgstr "Guardar imágenes en caché"
msgctxt "#30292"
msgid "Select Subtitle Stream"
msgstr "Seleccionar pista de subtítulos"
msgctxt "#30291"
msgid "Select Audio Stream"
msgstr "Seleccionar pista de audio"
msgctxt "#30290"
msgid "All"
msgstr "Todo"
msgctxt "#30289"
msgid "TV Shows - Genres"
msgstr "Series - Generos"
msgctxt "#30288"
msgid " - Latest"
msgstr "- Último"
msgctxt "#30287"
msgid "TV Shows - Latest"
msgstr "Series - Último"
msgctxt "#30286"
msgid "Movies - Unwatched"
msgstr "Películas - No vistas"
msgctxt "#30285"
msgid " - Unwatched"
msgstr "- No visto"
msgctxt "#30283"
msgid "Play Next Episode?"
msgstr "¿Reproducir episodio siguiente?"
msgctxt "#30282"
msgid "No Jellyfin servers detected on your local network."
msgstr "No se han detectado servidores de Jellyfin en tu red local."
msgctxt "#30281"
msgid "Refresh Cached Images"
msgstr "Actualizar imágenes en caché"
msgctxt "#30280"
msgid "Missing Title"
msgstr "Título faltante"
msgctxt "#30279"
msgid "TV Shows - Unwatched"
msgstr "Series - No vistas"
msgctxt "#30278"
msgid " - Next Up"
msgstr "- A continuación"
msgctxt "#30275"
msgid "Force Transcode"
msgstr "Forzar transcodificación"
msgctxt "#30274"
msgid "Delete"
msgstr "Borrar"
msgctxt "#30273"
msgid "Unset Favourite"
msgstr "Quitar favorito"
msgctxt "#30272"
msgid "Set Favourite"
msgstr "Establecer como favorito"
msgctxt "#30271"
msgid "Mark Unwatched"
msgstr "Marcar como no visto"
msgctxt "#30270"
msgid "Mark Watched"
msgstr "Marcar como visto"
msgctxt "#30269"
msgid "Movies - Random"
msgstr "Películas - Al azar"
msgctxt "#30268"
msgid " - Recently Added"
msgstr "- Añadido recientemente"
msgctxt "#30267"
msgid " - In Progress"
msgstr "- En progreso"
msgctxt "#30266"
msgid "Movies - Pages"
msgstr "Películas - Páginas"
msgctxt "#30265"
msgid "Episodes - Next Up"
msgstr "Episodios - A continuación"
msgctxt "#30264"
msgid "Episodes - In Progress"
msgstr "Episodios - En progreso"
msgctxt "#30263"
msgid "Episodes - Recently Added"
msgstr "Episodios - Añadidos recientemente"
msgctxt "#30262"
msgid "TV Shows - Favorites"
msgstr "Series - Favoritos"
msgctxt "#30261"
msgid "TV Shows"
msgstr "Series"
msgctxt "#30259"
msgid "Movies - Favorites"
msgstr "Películas - Favoritos"
msgctxt "#30258"
msgid "Movies - In Progress"
msgstr "Películas - En progreso"
msgctxt "#30257"
msgid "Movies - Recently Added"
msgstr "Películas - Añadidas recientemente"
msgctxt "#30256"
msgid "Movies"
msgstr "Películas"
msgctxt "#30255"
msgid "TV Shows - A-Z"
msgstr "Series - A-Z"
msgctxt "#30254"
msgid "Show add-on settings"
msgstr "Mostrar configuración del complemento"
msgctxt "#30252"
msgid "Movies - A-Z"
msgstr "Películas - A-Z"
msgctxt "#30251"
msgid "Movies - Genres"
msgstr "Películas - Géneros"
msgctxt "#30250"
msgid "Unknown"
msgstr "Desconocido"
msgctxt "#30246"
msgid "Search"
msgstr "Buscar"
msgctxt "#30241"
msgid "Force transcode mpeg4"
msgstr "Forzar transcodificación MPEG4"
msgctxt "#30240"
msgid "Force transcode msmpeg4v3 (divx)"
msgstr "Forzar transcodificación MSMPEG4v3 (DivX)"
msgctxt "#30239"
msgid "Force transcode mpeg2"
msgstr "Forzar transcodificación MPEG2"
msgctxt "#30238"
msgid "Playback stream options"
msgstr "Opciones de reproducción"
msgctxt "#30237"
msgid "Start from beginning"
msgstr "Empezar desde el principio"
msgctxt "#30236"
msgid "Force transcode h265 (hevc)"
msgstr "Forzar transcodificación H.265 (HEVC)"
msgctxt "#30235"
msgid "Episodes"
msgstr "Episodios"
msgctxt "#30231"
msgid "Movies"
msgstr "Películas"
msgctxt "#30229"
msgid "TV Shows"
msgstr "Series"
msgctxt "#30224"
msgid "Interaction"
msgstr "Interacción"
msgctxt "#30223"
msgid "Page Size and Filtering"
msgstr "Tamaño de página y filtrado"
msgctxt "#30222"
msgid "Item Layout"
msgstr "Disposición del elemento"
msgctxt "#30220"
msgid "Prompt to delete movie after %"
msgstr "Preguntar para borrar la película después de %"
msgctxt "#30219"
msgid " - Prompt before play"
msgstr "- Preguntar antes de reproducir"
msgctxt "#30218"
msgid "Play next episode after %"
msgstr "Reproducir siguiente episodio después de %"
msgctxt "#30217"
msgid "Prompt to delete episode after %"
msgstr "Preguntar para borrar el episodio después de %"
msgctxt "#30216"
msgid "Item Details"
msgstr "Detalles del elemento"
msgctxt "#30214"
msgid "Events"
msgstr "Eventos"
msgctxt "#30213"
msgid "Video force 8 bit"
msgstr "Forzar 8 bits en el vídeo"
msgctxt "#30212"
msgid "Video max width"
msgstr "Ancho máximo del vídeo"
msgctxt "#30211"
msgid "Transcode options"
msgstr "Opciones de transcodificación"
#, fuzzy
msgctxt "#30209"
msgid "File direct path"
msgstr "Ruta directa al archivo"
msgctxt "#30208"
msgid "Max stream bitrate (Kbits)"
msgstr "Máxima tasa de bits (Kbps)"
msgctxt "#30207"
msgid "Playback"
msgstr "Reproducción"
msgctxt "#30206"
msgid "Playback type"
msgstr "Tipo de reproducción"
msgctxt "#30201"
msgid "Unable to connect to server"
msgstr "No se ha podido conectar al servidor"
msgctxt "#30200"
msgid "URL error"
msgstr "Error de URL"
msgctxt "#30183"
msgid "Include people"
msgstr "Incluir gente"
msgctxt "#30181"
msgid "Include plot"
msgstr "Incluir trama"
msgctxt "#30180"
msgid "Select User"
msgstr "Usuario seleccionado"
msgctxt "#30169"
msgid "Address: "
msgstr "Dirección:"
msgctxt "#30167"
msgid "Selected Server Address"
msgstr "Dirección del servidor seleccionado"
msgctxt "#30166"
msgid "Select Server"
msgstr "Seleccionar servidor"
msgctxt "#30135"
msgid "Error"
msgstr "Error"
msgctxt "#30126"
msgid "Processing Item : "
msgstr "Procesando el elemento:"
msgctxt "#30125"
msgid "Done"
msgstr "Hecho"
msgctxt "#30120"
msgid "Show load progress"
msgstr "Mostrar el progreso de carga"
msgctxt "#30118"
msgid "Add resume percent to names"
msgstr "Añadir el porcentaje de reanudación a los nombres"
msgctxt "#30113"
msgid "Retrieving Data"
msgstr "Recuperando los datos"
msgctxt "#30112"
msgid "Loading Content"
msgstr "Cargando contenido"
msgctxt "#30111"
msgid "Services"
msgstr "Servicios"
msgctxt "#30110"
msgid "Interface"
msgstr "Interfaz"
msgctxt "#30092"
msgid "Warning: This action will delete the media files from the server."
msgstr "Aviso: Esta acción borrará los archivos de los medios del servidor."
msgctxt "#30091"
msgid "Confirm delete?"
msgstr "¿Confirmar borrado?"
msgctxt "#30063"
msgid "N/A"
msgstr "N/A"
msgctxt "#30053"
msgid "Waiting for server to delete"
msgstr "Esperando al servidor para borrar"
msgctxt "#30052"
msgid "Deleting"
msgstr "Borrando"
msgctxt "#30045"
msgid "Username not found"
msgstr "Nombre de usuario no encontrado"
msgctxt "#30044"
msgid "Incorrect Username/Password"
msgstr "Nombre de usuario/Contraseña incorrectos"
msgctxt "#30027"
msgid "Enable debug logging"
msgstr "Activar registro de depuración"
msgctxt "#30025"
msgid "Password:"
msgstr "Contraseña:"
msgctxt "#30024"
msgid "Username:"
msgstr "Nombre de usuario:"
msgctxt "#30023"
msgid "Hide unwatched episode details"
msgstr "Esconder los detalles de los episodios no vistos"
msgctxt "#30022"
msgid "Advanced"
msgstr "Avanzado"
msgctxt "#30021"
msgid "Show all episodes item"
msgstr "Mostrar todos los episodios"
msgctxt "#30019"
msgid "Filtered episode name format"
msgstr "Formato de nombre de episodio filtrado"
msgctxt "#30018"
msgid "Number of items to show in filtered lists"
msgstr "Número de elementos a mostrar en las listas filtradas"
msgctxt "#30017"
msgid "Show connected clients"
msgstr "Mostrar clientes conectados"
msgctxt "#30016"
msgid "Device display name"
msgstr "Nombre a mostrar del dispositivo"
msgctxt "#30014"
msgid "Jellyfin"
msgstr "Jellyfin"
msgctxt "#30012"
msgid "[Change user]"
msgstr "[Cambiar de usuario]"
msgctxt "#30011"
msgid "[Detect local server]"
msgstr "[Detectar servidor local]"
msgctxt "#30010"
msgid "Number of performance profiles to capture"
msgstr "Número de perfiles de rendimiento a capturar"
msgctxt "#30008"
msgid "Samba password"
msgstr "Contraseña de Samba"
msgctxt "#30007"
msgid "Samba username"
msgstr "Nombre de usuario de Samba"
msgctxt "#30006"
msgid "Password"
msgstr "Contraseña"
msgctxt "#30005"
msgid "Username"
msgstr "Nombre de usuario"
msgctxt "#30003"
msgid "Verify HTTPS certificate"
msgstr "Verificar certificado HTTPS"
msgctxt "#30001"
msgid "Port"
msgstr "Puerto"

View File

@@ -0,0 +1,2 @@
msgid ""
msgstr "X-Generator: Weblate\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit"

View File

@@ -0,0 +1,2 @@
msgid ""
msgstr "X-Generator: Weblate\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,428 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2021-12-02 18:05+0000\n"
"Last-Translator: Alfonso Scarpino <alfonso.scarpino@gmail.com>\n"
"Language-Team: Italian <https://translate.jellyfin.org/projects/jellycon/"
"jellycon/it/>\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.5.2\n"
msgctxt "#30120"
msgid "Show load progress"
msgstr "Mostra avanzamento del caricamento"
msgctxt "#30118"
msgid "Add resume percent to names"
msgstr "Aggiungi percentuale guardata ai nomi"
msgctxt "#30116"
msgid "Add unwatched counts to names"
msgstr "Aggiungi il contatore non guardati ai nomi"
msgctxt "#30113"
msgid "Retrieving Data"
msgstr "Recupero dati in corso"
msgctxt "#30112"
msgid "Loading Content"
msgstr "Caricamento contenuto in corso"
msgctxt "#30111"
msgid "Services"
msgstr "Servizi"
msgctxt "#30110"
msgid "Interface"
msgstr "Interfaccia"
msgctxt "#30014"
msgid "Jellyfin"
msgstr "Jellyfin"
msgctxt "#30092"
msgid "Warning: This action will delete the media files from the server."
msgstr "Attenzione: questa operazione eliminerà i file multimediali dal server."
msgctxt "#30091"
msgid "Confirm delete?"
msgstr "Confermi l'eliminazione?"
msgctxt "#30063"
msgid "N/A"
msgstr "N/D"
msgctxt "#30053"
msgid "Waiting for server to delete"
msgstr "In attesa di eliminazione sul server"
msgctxt "#30052"
msgid "Deleting"
msgstr "Eliminazione in corso"
msgctxt "#30045"
msgid "Username not found"
msgstr "Nome utente sconosciuto"
msgctxt "#30044"
msgid "Incorrect Username/Password"
msgstr "Nome utente/Password errati"
msgctxt "#30027"
msgid "Enable debug logging"
msgstr "Attiva log a debug"
msgctxt "#30026"
msgid "Widget item select action"
msgstr "Azione di selezione elemento widget"
msgctxt "#30025"
msgid "Password:"
msgstr "Password:"
msgctxt "#30024"
msgid "Username:"
msgstr "Nome utente:"
msgctxt "#30023"
msgid "Hide unwatched episode details"
msgstr "Nascondi dettagli episodi non guardati"
msgctxt "#30022"
msgid "Advanced"
msgstr "Avanzate"
msgctxt "#30021"
msgid "Show all episodes item"
msgstr "Mostra elemento tutti gli episodi"
msgctxt "#30020"
msgid "Flatten single season"
msgstr "Appiattisci stagione unica"
msgctxt "#30019"
msgid "Filtered episode name format"
msgstr "Formato nome episodio filtrato"
msgctxt "#30018"
msgid "Number of items to show in filtered lists"
msgstr "Numero di elementi da mostrare nelle liste filtrate"
msgctxt "#30017"
msgid "Show connected clients"
msgstr "Mostra client connessi"
msgctxt "#30016"
msgid "Device display name"
msgstr "Nome dispositivo visualizzato"
msgctxt "#30015"
msgid "Log timing data"
msgstr "Dati di temporizzazione dei log"
msgctxt "#30012"
msgid "[Change user]"
msgstr "[Cambia utente]"
msgctxt "#30011"
msgid "[Detect local server]"
msgstr "[Rileva server locale]"
msgctxt "#30010"
msgid "Number of performance profiles to capture"
msgstr "Numero di profili di performance da acquisire"
msgctxt "#30008"
msgid "Samba password"
msgstr "Password Samba"
msgctxt "#30007"
msgid "Samba username"
msgstr "Nome utente Samba"
msgctxt "#30006"
msgid "Password"
msgstr "Password"
msgctxt "#30005"
msgid "Username"
msgstr "Nome utente"
msgctxt "#30003"
msgid "Verify HTTPS certificate"
msgstr "Verifica certificato HTTPS"
msgctxt "#30001"
msgid "Port"
msgstr "Porta"
msgctxt "#30000"
msgid "Host"
msgstr "Host"
msgctxt "#30207"
msgid "Playback"
msgstr "Riproduzione"
msgctxt "#30206"
msgid "Playback type"
msgstr "Tipo riproduzione"
msgctxt "#30201"
msgid "Unable to connect to server"
msgstr "Impossibile connettersi al server"
msgctxt "#30200"
msgid "URL error"
msgstr "Errore URL"
msgctxt "#30183"
msgid "Include people"
msgstr "Includi persone"
msgctxt "#30182"
msgid "Include media stream info"
msgstr "Includi info flusso multimediale"
msgctxt "#30181"
msgid "Include plot"
msgstr "Includi trama"
msgctxt "#30180"
msgid "Select User"
msgstr "Seleziona Utente"
msgctxt "#30169"
msgid "Address: "
msgstr "Indirizzo:"
msgctxt "#30167"
msgid "Selected Server Address"
msgstr "Indirizzo server selezionato"
msgctxt "#30166"
msgid "Select Server"
msgstr "Seleziona Server"
msgctxt "#30163"
msgid "Add (cc) if subtitle is available"
msgstr "Aggiungi (cc) se sono disponibili i sottotitoli"
msgctxt "#30139"
msgid "No Media Type Set"
msgstr "Nessun formato media impostato"
msgctxt "#30135"
msgid "Error"
msgstr "Errore"
msgctxt "#30126"
msgid "Processing Item : "
msgstr "Elaborazione Elemento:"
msgctxt "#30125"
msgid "Done"
msgstr "Finito"
msgctxt "#30275"
msgid "Force Transcode"
msgstr "Forza transcodifica"
msgctxt "#30274"
msgid "Delete"
msgstr "Elimina"
msgctxt "#30273"
msgid "Unset Favourite"
msgstr "Rimuovi dai preferiti"
msgctxt "#30272"
msgid "Set Favourite"
msgstr "Aggiungi ai preferiti"
msgctxt "#30271"
msgid "Mark Unwatched"
msgstr "Segna come non guardato"
msgctxt "#30270"
msgid "Mark Watched"
msgstr "Segna come guardato"
msgctxt "#30269"
msgid "Movies - Random"
msgstr "Film - Casuale"
msgctxt "#30268"
msgid " - Recently Added"
msgstr "- Aggiunti di recente"
msgctxt "#30267"
msgid " - In Progress"
msgstr "- In corso"
msgctxt "#30266"
msgid "Movies - Pages"
msgstr "Film - Pagine"
msgctxt "#30265"
msgid "Episodes - Next Up"
msgstr "Episodi - Prossimo"
msgctxt "#30264"
msgid "Episodes - In Progress"
msgstr "Episodi - In corso"
msgctxt "#30263"
msgid "Episodes - Recently Added"
msgstr "Episodi - Aggiunti di recente"
msgctxt "#30262"
msgid "TV Shows - Favorites"
msgstr "Serie TV - Preferiti"
msgctxt "#30261"
msgid "TV Shows"
msgstr "Serie TV"
msgctxt "#30260"
msgid "BoxSets"
msgstr "Collezioni"
msgctxt "#30259"
msgid "Movies - Favorites"
msgstr "Film - Preferiti"
msgctxt "#30258"
msgid "Movies - In Progress"
msgstr "Film - In corso"
msgctxt "#30257"
msgid "Movies - Recently Added"
msgstr "Film - Aggiunti di recente"
msgctxt "#30256"
msgid "Movies"
msgstr "Film"
msgctxt "#30255"
msgid "TV Shows - A-Z"
msgstr "Serie TV - A-Z"
msgctxt "#30254"
msgid "Show add-on settings"
msgstr "Mostra impostazioni add-on"
msgctxt "#30252"
msgid "Movies - A-Z"
msgstr "Film - A-Z"
msgctxt "#30251"
msgid "Movies - Genres"
msgstr "Film - Generi"
msgctxt "#30250"
msgid "Unknown"
msgstr "Sconosciuto"
msgctxt "#30247"
msgid "Custom Widget Content"
msgstr "Contenuto personalizzato widget"
msgctxt "#30246"
msgid "Search"
msgstr "Cerca"
msgctxt "#30241"
msgid "Force transcode mpeg4"
msgstr "Forza transcodifica mpeg4"
msgctxt "#30240"
msgid "Force transcode msmpeg4v3 (divx)"
msgstr "Forza transcodifica msmpeg4v3 (divx)"
msgctxt "#30239"
msgid "Force transcode mpeg2"
msgstr "Forza transcodifica mpeg2"
msgctxt "#30238"
msgid "Playback stream options"
msgstr "Opzioni di riproduzione flusso"
msgctxt "#30237"
msgid "Start from beginning"
msgstr "Ricomincia dall'inizio"
msgctxt "#30236"
msgid "Force transcode h265 (hevc)"
msgstr "Forza transcodifica h265 (hevc)"
msgctxt "#30235"
msgid "Episodes"
msgstr "Episodi"
msgctxt "#30231"
msgid "Movies"
msgstr "Film"
msgctxt "#30229"
msgid "TV Shows"
msgstr "Serie TV"
msgctxt "#30224"
msgid "Interaction"
msgstr "Interazione"
msgctxt "#30223"
msgid "Page Size and Filtering"
msgstr "Dimensione pagina e filtri"
msgctxt "#30222"
msgid "Item Layout"
msgstr "Disposizione elemento"
msgctxt "#30220"
msgid "Prompt to delete movie after %"
msgstr "Chiedi se eliminare il film dopo %"
msgctxt "#30219"
msgid " - Prompt before play"
msgstr "- Chiedi prima di riprodurre"
msgctxt "#30218"
msgid "Play next episode after %"
msgstr "Riproduci il prossimo episodio dopo %"
msgctxt "#30217"
msgid "Prompt to delete episode after %"
msgstr "Chiedi se eliminare l'episodio dopo %"
msgctxt "#30216"
msgid "Item Details"
msgstr "Dettagli elemento"
msgctxt "#30214"
msgid "Events"
msgstr "Eventi"
msgctxt "#30212"
msgid "Video max width"
msgstr "Larghezza massima video"
msgctxt "#30211"
msgid "Transcode options"
msgstr "Opzioni di transcodifica"
msgctxt "#30210"
msgid "HTTP direct stream"
msgstr "Flusso diretto HTTP"
msgctxt "#30209"
msgid "File direct path"
msgstr "Percorso diretto del file"
msgctxt "#30208"
msgid "Max stream bitrate (Kbits)"
msgstr "Bitrate massimo del flusso (Kbits)"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
msgid ""
msgstr "X-Generator: Weblate\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit"

View File

@@ -0,0 +1,661 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2021-11-23 03:05+0000\n"
"Last-Translator: Marcin Woliński <cierdek@gmail.com>\n"
"Language-Team: Polish <https://translate.jellyfin.org/projects/jellycon/"
"jellycon/pl/>\n"
"Language: pl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 4.5.2\n"
msgctxt "#30313"
msgid "Menu"
msgstr "Menu"
msgctxt "#30312"
msgid "All - "
msgstr "Wszystko -"
msgctxt "#30311"
msgid "Library - "
msgstr "Biblioteka -"
msgctxt "#30310"
msgid "Enable Jellyfin remote control"
msgstr "Włącz zdalne sterowanie Jellyfin"
msgctxt "#30309"
msgid "Select Media Source"
msgstr "Wybierz źródło multimediów"
msgctxt "#30308"
msgid "Select Trailer"
msgstr "Wybierz zwiastun"
msgctxt "#30307"
msgid "Play Trailer"
msgstr "Odtwórz zwiastun"
msgctxt "#30306"
msgid "Playback starting"
msgstr "Rozpoczęcie odtwarzania"
msgctxt "#30305"
msgid "Not Found"
msgstr "Nie znaleziono"
msgctxt "#30304"
msgid "Cached Jellyfin images : "
msgstr "Buforowane obrazy Jellyfin:"
msgctxt "#30303"
msgid "Missing Jellyfin images : "
msgstr "Brakujące obrazy Jellyfin:"
msgctxt "#30302"
msgid "Existing images : "
msgstr "Istniejące obrazy:"
msgctxt "#30301"
msgid "Caching Images"
msgstr "Buforowanie obrazów"
msgctxt "#30300"
msgid "Cache all Jellyfin images as local Kodi images?"
msgstr "Buforować wszystkie obrazy Jellyfin jako lokalne obrazy Kodi?"
msgctxt "#30299"
msgid "Cache Images"
msgstr "Obrazy w pamięci podręcznej"
msgctxt "#30298"
msgid "Deleting Kodi Images"
msgstr "Usuwanie obrazów Kodi"
msgctxt "#30297"
msgid "Delete unused images?"
msgstr "Usunąć nieużywane obrazy?"
msgctxt "#30296"
msgid "Delete"
msgstr "Usuń"
msgctxt "#30295"
msgid "To use this feature you need HTTP control enabled"
msgstr "Aby korzystać z tej funkcji, musisz mieć włączoną kontrolę HTTP"
msgctxt "#30294"
msgid "Notice"
msgstr "Uwagi"
msgctxt "#30293"
msgid "Cache images"
msgstr "Pamięć podręczna obrazów"
msgctxt "#30292"
msgid "Select Subtitle Stream"
msgstr "Wybierz ścieżkę napisów"
msgctxt "#30291"
msgid "Select Audio Stream"
msgstr "Wybierz ścieżkę audio"
msgctxt "#30290"
msgid "All"
msgstr "Wszystko"
msgctxt "#30289"
msgid "TV Shows - Genres"
msgstr "Seriale - Gatunki"
msgctxt "#30288"
msgid " - Latest"
msgstr "- Ostatnie"
msgctxt "#30287"
msgid "TV Shows - Latest"
msgstr "Seriale — ostatnie"
msgctxt "#30286"
msgid "Movies - Unwatched"
msgstr "Filmy nieobejrzane"
msgctxt "#30285"
msgid " - Unwatched"
msgstr "- Nieobejrzane"
msgctxt "#30283"
msgid "Play Next Episode?"
msgstr "Odtworzyć kolejny odcinek?"
msgctxt "#30282"
msgid "No Jellyfin servers detected on your local network."
msgstr "W twojej sieci lokalnej nie wykryto serwerów Jellyfin."
msgctxt "#30281"
msgid "Refresh Cached Images"
msgstr "Odśwież obrazy z pamięci podręcznej"
msgctxt "#30280"
msgid "Missing Title"
msgstr "Brak tytułu"
msgctxt "#30279"
msgid "TV Shows - Unwatched"
msgstr "Seriale - Nieoglądane"
msgctxt "#30278"
msgid " - Next Up"
msgstr "- Następna w górę"
msgctxt "#30277"
msgid "JellyCon needs to prompt for resume on partily played items, Kodi can also prompt, this can cause a double prompt. Do you want to remove the double prompt?"
msgstr ""
"JellyCon musi monitować o wznowienie w przypadku częściowo odtwarzanych "
"elementów, Kodi może również monitować, co może spowodować podwójny monit. "
"Czy chcesz usunąć podwójny monit?"
msgctxt "#30276"
msgid "Extra Resume Prompt Detected"
msgstr "Wykryto dodatkowy monit o wznowienie"
msgctxt "#30275"
msgid "Force Transcode"
msgstr "Wymuś transkodowanie"
msgctxt "#30274"
msgid "Delete"
msgstr "Usuń"
msgctxt "#30273"
msgid "Unset Favourite"
msgstr "Usuń z ulubionych"
msgctxt "#30272"
msgid "Set Favourite"
msgstr "Ustaw ulubione"
msgctxt "#30271"
msgid "Mark Unwatched"
msgstr "Oznacz nieoglądane"
msgctxt "#30270"
msgid "Mark Watched"
msgstr "Zaznacz obserwowane"
msgctxt "#30269"
msgid "Movies - Random"
msgstr "Filmy — losowe"
msgctxt "#30268"
msgid " - Recently Added"
msgstr "- Niedawno dodany"
msgctxt "#30267"
msgid " - In Progress"
msgstr "- W trakcie"
msgctxt "#30266"
msgid "Movies - Pages"
msgstr "Filmy Strony"
msgctxt "#30265"
msgid "Episodes - Next Up"
msgstr "Odcinki — Następny"
msgctxt "#30264"
msgid "Episodes - In Progress"
msgstr "Odcinki — w toku"
msgctxt "#30263"
msgid "Episodes - Recently Added"
msgstr "Odcinki — ostatnio dodane"
msgctxt "#30262"
msgid "TV Shows - Favorites"
msgstr "Seriale Ulubione"
msgctxt "#30261"
msgid "TV Shows"
msgstr "Seriale"
msgctxt "#30260"
msgid "BoxSets"
msgstr "Zestawy pudełek"
msgctxt "#30259"
msgid "Movies - Favorites"
msgstr "Filmy - Ulubione"
msgctxt "#30258"
msgid "Movies - In Progress"
msgstr "Filmy w toku"
msgctxt "#30257"
msgid "Movies - Recently Added"
msgstr "Filmy — ostatnio dodane"
msgctxt "#30256"
msgid "Movies"
msgstr "Filmy"
msgctxt "#30255"
msgid "TV Shows - A-Z"
msgstr "Seriale - A-Z"
msgctxt "#30254"
msgid "Show add-on settings"
msgstr "Pokaż ustawienia dodatków"
msgctxt "#30252"
msgid "Movies - A-Z"
msgstr "Filmy - A-Z"
msgctxt "#30251"
msgid "Movies - Genres"
msgstr "Filmy Gatunki"
msgctxt "#30250"
msgid "Unknown"
msgstr "Nieznane"
msgctxt "#30247"
msgid "Custom Widget Content"
msgstr "Niestandardowa treść widżetu"
msgctxt "#30246"
msgid "Search"
msgstr "Szukaj"
msgctxt "#30241"
msgid "Force transcode mpeg4"
msgstr "Wymuś transkodowanie mpeg4"
msgctxt "#30240"
msgid "Force transcode msmpeg4v3 (divx)"
msgstr "Wymuś transkodowanie msmpeg4v3 (divx)"
msgctxt "#30239"
msgid "Force transcode mpeg2"
msgstr "Wymuś transkodowanie mpeg2"
msgctxt "#30238"
msgid "Playback stream options"
msgstr "Opcje strumienia odtwarzania"
msgctxt "#30237"
msgid "Start from beginning"
msgstr "Zacznij od początku"
msgctxt "#30236"
msgid "Force transcode h265 (hevc)"
msgstr "Wymuś transkodowanie h265 (hevc)"
msgctxt "#30235"
msgid "Episodes"
msgstr "Odcinki"
msgctxt "#30231"
msgid "Movies"
msgstr "Filmy"
msgctxt "#30229"
msgid "TV Shows"
msgstr "Seriale"
msgctxt "#30213"
msgid "Video force 8 bit"
msgstr "Wymuś video 8 bit"
msgctxt "#30224"
msgid "Interaction"
msgstr "Interakcja"
msgctxt "#30223"
msgid "Page Size and Filtering"
msgstr "Rozmiar strony i filtrowanie"
msgctxt "#30222"
msgid "Item Layout"
msgstr "Układ przedmiotu"
msgctxt "#30220"
msgid "Prompt to delete movie after %"
msgstr "Pytaj o usunięcie filmu po %"
msgctxt "#30219"
msgid " - Prompt before play"
msgstr "- Pytaj przed odtworzeniem"
msgctxt "#30218"
msgid "Play next episode after %"
msgstr "Odtwórz następny odcinek po %"
msgctxt "#30217"
msgid "Prompt to delete episode after %"
msgstr "Monituj o usunięcie odcinka po %"
msgctxt "#30216"
msgid "Item Details"
msgstr "Szczegóły produktu"
msgctxt "#30215"
msgid "On playback stop (100% = disabled)"
msgstr "Po odtworzeniu zatrzymaj (100% = wyłączone)"
msgctxt "#30214"
msgid "Events"
msgstr "Zdarzenia"
msgctxt "#30212"
msgid "Video max width"
msgstr "Maksymalna szerokość wideo"
msgctxt "#30211"
msgid "Transcode options"
msgstr "Opcje transkodowania"
msgctxt "#30210"
msgid "HTTP direct stream"
msgstr "Bezpośredni strumień HTTP"
msgctxt "#30209"
msgid "File direct path"
msgstr "Bezpośrednia ścieżka pliku"
msgctxt "#30208"
msgid "Max stream bitrate (Kbits)"
msgstr "Maksymalna transmisja strumienia (Kb/s)"
msgctxt "#30207"
msgid "Playback"
msgstr "Odtwarzanie"
msgctxt "#30000"
msgid "Host"
msgstr "Host"
msgctxt "#30206"
msgid "Playback type"
msgstr "Typ odtwarzania"
msgctxt "#30201"
msgid "Unable to connect to server"
msgstr "Niemożna połączyć z serwerem"
msgctxt "#30200"
msgid "URL error"
msgstr "Błąd adresu URL"
msgctxt "#30183"
msgid "Include people"
msgstr "Uwzględnij osoby"
msgctxt "#30182"
msgid "Include media stream info"
msgstr "Dołącz informacje o strumieniu multimediów"
msgctxt "#30181"
msgid "Include plot"
msgstr "Uwzględnij fabułę"
msgctxt "#30180"
msgid "Select User"
msgstr "Wybierz użytkownika"
msgctxt "#30169"
msgid "Address: "
msgstr "Adres:"
msgctxt "#30167"
msgid "Selected Server Address"
msgstr "Wybierz adres serwera"
msgctxt "#30166"
msgid "Select Server"
msgstr "Wybierz serwer"
msgctxt "#30163"
msgid "Add (cc) if subtitle is available"
msgstr "Dodaj (cc), jeśli napisy są dostępne"
msgctxt "#30139"
msgid "No Media Type Set"
msgstr "Brak zestawu typu nośnika"
msgctxt "#30321"
msgid " - Album Artists"
msgstr "- Artyści albumów"
msgctxt "#30319"
msgid "Music - All Album Artists"
msgstr "Muzyka — wszyscy wykonawcy albumów"
msgctxt "#30332"
msgid "Stop media playback on screensaver activation"
msgstr "Zatrzymaj odtwarzanie multimediów po aktywacji wygaszacza ekranu"
msgctxt "#30331"
msgid "Movies per page"
msgstr "Filmów na stronę"
msgctxt "#30330"
msgid "Show change user dialog"
msgstr "Pokaż okno dialogowe zmiany użytkownika"
msgctxt "#30329"
msgid "Screensaver"
msgstr "Wygaszacz ekranu"
msgctxt "#30328"
msgid "Show empty folders (shows, seasons, collections)"
msgstr "Pokaż puste foldery (seriale, sezony, kolekcje)"
msgctxt "#30327"
msgid "Go To Season"
msgstr "Przejdź do sezonu"
msgctxt "#30325"
msgid " - Genres"
msgstr "- Gatunki"
msgctxt "#30322"
msgid "Auto resume"
msgstr "Automatyczne wznawianie"
msgctxt "#30320"
msgid " - Albums"
msgstr "- Albumy"
msgctxt "#30318"
msgid "Music - Albums"
msgstr "Muzyka - Albumy"
msgctxt "#30317"
msgid "Play All"
msgstr "Włącz wszystko"
msgctxt "#30316"
msgid "Connection Error"
msgstr "Błąd połączenia"
msgctxt "#30315"
msgid "Suppress notifications for connection errors"
msgstr "Pomiń powiadomienia o błędach połączenia"
msgctxt "#30314"
msgid "Play"
msgstr "Włącz"
msgctxt "#30135"
msgid "Error"
msgstr "Błąd"
msgctxt "#30126"
msgid "Processing Item : "
msgstr "Przetwarzanie przedmiotu:"
msgctxt "#30125"
msgid "Done"
msgstr "Gotowe"
msgctxt "#30121"
msgid "On resume"
msgstr "Po wznowieniu"
msgctxt "#30120"
msgid "Show load progress"
msgstr "Pokaż postęp ładowania"
msgctxt "#30118"
msgid "Add resume percent to names"
msgstr "Dodaj wznowiony procent do nazwisk"
msgctxt "#30116"
msgid "Add unwatched counts to names"
msgstr "Dodaj nieobejrzane liczby do nazwisk"
msgctxt "#30114"
msgid "Jump back seconds"
msgstr "Cofnij się o kilka sekund"
msgctxt "#30113"
msgid "Retrieving Data"
msgstr "Pobieranie danych"
msgctxt "#30112"
msgid "Loading Content"
msgstr "Ładowanie treści"
msgctxt "#30111"
msgid "Services"
msgstr "Usługi"
msgctxt "#30110"
msgid "Interface"
msgstr "Interfejs"
msgctxt "#30092"
msgid "Warning: This action will delete the media files from the server."
msgstr ""
"Ostrzeżenie: Ta akcja spowoduje usunięcie plików multimedialnych z serwera."
msgctxt "#30091"
msgid "Confirm delete?"
msgstr "Potwierdź usunięcie?"
msgctxt "#30063"
msgid "N/A"
msgstr "N/A"
msgctxt "#30053"
msgid "Waiting for server to delete"
msgstr "Czekam na usunięcie serwera"
msgctxt "#30052"
msgid "Deleting"
msgstr "Kasowanie"
msgctxt "#30045"
msgid "Username not found"
msgstr "Nazwa użytkownika nie znaleziona"
msgctxt "#30044"
msgid "Incorrect Username/Password"
msgstr "Nieprawidłowa nazwa użytkownika / hasło"
msgctxt "#30027"
msgid "Enable debug logging"
msgstr "Włącz rejestrowanie debugowania"
msgctxt "#30026"
msgid "Widget item select action"
msgstr "Akcja wyboru elementu widżetu"
msgctxt "#30025"
msgid "Password:"
msgstr "Hasło:"
msgctxt "#30024"
msgid "Username:"
msgstr "Nazwa użytkownika:"
msgctxt "#30023"
msgid "Hide unwatched episode details"
msgstr "Ukryj szczegóły nieobejrzanych odcinków"
msgctxt "#30022"
msgid "Advanced"
msgstr "Zaawansowane"
msgctxt "#30021"
msgid "Show all episodes item"
msgstr "Pokaż wszystkie odcinki"
msgctxt "#30020"
msgid "Flatten single season"
msgstr "Spłaszcz jeden sezon"
msgctxt "#30019"
msgid "Filtered episode name format"
msgstr "Filtrowany format nazwy odcinka"
msgctxt "#30018"
msgid "Number of items to show in filtered lists"
msgstr "Liczba pozycji do pokazania na przefiltrowanych listach"
msgctxt "#30017"
msgid "Show connected clients"
msgstr "Pokaż połączonych klientów"
msgctxt "#30016"
msgid "Device display name"
msgstr "Wyświetlana nazwa urządzenia"
msgctxt "#30015"
msgid "Log timing data"
msgstr "Rejestruj dane czasowe"
msgctxt "#30014"
msgid "Jellyfin"
msgstr "Jellyfin"
msgctxt "#30012"
msgid "[Change user]"
msgstr "[Zmień użytkownika]"
msgctxt "#30011"
msgid "[Detect local server]"
msgstr "[Wykryj serwer lokalny]"
msgctxt "#30010"
msgid "Number of performance profiles to capture"
msgstr "Liczba profili wydajności do uchwycenia"
msgctxt "#30008"
msgid "Samba password"
msgstr "Hasło użytkownika Samby"
msgctxt "#30007"
msgid "Samba username"
msgstr "Nazwa użytkownika Samby"
msgctxt "#30006"
msgid "Password"
msgstr "Hasło"
msgctxt "#30005"
msgid "Username"
msgstr "Nazwa użytkownika"
msgctxt "#30003"
msgid "Verify HTTPS certificate"
msgstr "Zweryfikuj certyfikat HTTPS"
msgctxt "#30001"
msgid "Port"
msgstr "Port"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
msgid ""
msgstr "X-Generator: Weblate\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit"

View File

@@ -0,0 +1,2 @@
msgid ""
msgstr "X-Generator: Weblate\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
msgid ""
msgstr "X-Generator: Weblate\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit"

View File

@@ -1,4 +1,5 @@
# Gnu General Public License - see LICENSE.TXT
from __future__ import division, absolute_import, print_function, unicode_literals
import time
import threading
@@ -25,7 +26,7 @@ class ActionAutoClose(threading.Thread):
def run(self):
log.debug("ActionAutoClose Running")
while not xbmc.abortRequested and not self.stop_thread:
while not xbmc.Monitor().abortRequested() and not self.stop_thread:
time_since_last = time.time() - self.last_interaction
log.debug("ActionAutoClose time_since_last : {0}".format(time_since_last))
@@ -69,9 +70,6 @@ class ActionMenu(xbmcgui.WindowXMLDialog):
self.listControl.addItems(self.action_items)
self.setFocus(self.listControl)
# bg_image = self.getControl(3010)
# bg_image.setHeight(50 * len(self.action_items) + 20)
def onFocus(self, control_id):
pass

View File

@@ -1,3 +1,5 @@
from __future__ import division, absolute_import, print_function, unicode_literals
import xbmc
import xbmcgui
@@ -39,8 +41,6 @@ class BitrateDialog(xbmcgui.WindowXMLDialog):
def onAction(self, action):
# log.debug("onAction: onAction: {0} {1}", action.getId(), self.slider_control.getInt())
bitrate_label_string = str(self.slider_control.getInt()) + " Kbs"
self.bitrate_label.setLabel(bitrate_label_string)
@@ -55,4 +55,3 @@ class BitrateDialog(xbmcgui.WindowXMLDialog):
def onClick(self, control_id):
if control_id == 3000:
log.debug("ActionMenu: Selected Item: {0}".format(control_id))
#self.close()

View File

@@ -1,7 +1,8 @@
# coding=utf-8
# Gnu General Public License - see LICENSE.TXT
from __future__ import division, absolute_import, print_function, unicode_literals
import urllib
from six.moves.urllib.parse import unquote
import requests
import base64
import sys
@@ -18,7 +19,7 @@ from .loghandler import LazyLogger
from .jsonrpc import JsonRpc, get_value
from .translation import string_load
from .datamanager import DataManager
from .utils import get_art, double_urlencode
from .utils import get_art
from .kodi_utils import HomeWindow
downloadUtils = DownloadUtils()
@@ -106,7 +107,7 @@ class CacheArtwork(threading.Thread):
progress.update(100, string_load(30125))
progress.close()
xbmcgui.Dialog().ok(string_load(30281), string_load(30344) % delete_count)
xbmcgui.Dialog().ok(string_load(30281), '{}: {}'.format(string_load(30344), delete_count))
def cache_artwork_interactive(self):
log.debug("cache_artwork_interactive")
@@ -118,7 +119,7 @@ class CacheArtwork(threading.Thread):
result = JsonRpc('Settings.GetSettingValue').execute(web_query)
xbmc_webserver_enabled = result['result']['value']
if not xbmc_webserver_enabled:
xbmcgui.Dialog().ok(string_load(30294), string_load(30295), string_load(30355))
xbmcgui.Dialog().ok(string_load(30294), '{} - {}'.format(string_load(30295), string_load(30355)))
xbmc.executebuiltin('ActivateWindow(servicesettings)')
return
@@ -149,11 +150,10 @@ class CacheArtwork(threading.Thread):
unused_texture_ids = set()
for texture in textures:
url = texture.get("url")
url = urllib.unquote(url)
url = unquote(url)
url = url.replace("image://", "")
url = url[0:-1]
if url.find("/") > -1 and url not in jellyfin_texture_urls or url.find("localhost:24276") > -1:
# log.debug("adding unused texture url: {0}", url)
unused_texture_ids.add(texture["textureid"])
total = len(unused_texture_ids)
@@ -242,7 +242,6 @@ class CacheArtwork(threading.Thread):
texture_urls = set()
# image_types = ["thumb", "poster", "banner", "clearlogo", "tvshow.poster", "tvshow.banner", "tvshow.landscape"]
for item in results:
art = get_art(item, server)
for art_type in art:
@@ -284,7 +283,7 @@ class CacheArtwork(threading.Thread):
texture_urls = set()
for texture in textures:
url = texture.get("url")
url = urllib.unquote(url)
url = unquote(url)
url = url.replace("image://", "")
url = url[0:-1]
texture_urls.add(url)
@@ -305,7 +304,6 @@ class CacheArtwork(threading.Thread):
missing_texture_urls = set()
# image_types = ["thumb", "poster", "banner", "clearlogo", "tvshow.poster", "tvshow.banner", "tvshow.landscape"]
for image_url in jellyfin_texture_urls:
if image_url not in texture_urls and not image_url.endswith("&Tag=") and len(image_url) > 0:
missing_texture_urls.add(image_url)
@@ -328,9 +326,7 @@ class CacheArtwork(threading.Thread):
count_done = 0
for index, get_url in enumerate(missing_texture_urls, 1):
# log.debug("texture_url: {0}", get_url)
url = double_urlencode(get_url)
kodi_texture_url = ("/image/image://%s" % url)
kodi_texture_url = "/image/image://{0}".format(get_url)
log.debug("kodi_texture_url: {0}".format(kodi_texture_url))
percentage = int((float(index) / float(total)) * 100)
@@ -344,8 +340,6 @@ class CacheArtwork(threading.Thread):
count_done += 1
log.debug("Get Image Result: {0}".format(data.status_code))
# if progress.iscanceled():
# if "iscanceled" in dir(progress) and progress.iscanceled():
if isinstance(progress, xbmcgui.DialogProgress) and progress.iscanceled():
break

View File

@@ -1,6 +1,8 @@
# Gnu General Public License - see LICENSE.TXT
from __future__ import division, absolute_import, print_function, unicode_literals
from uuid import uuid4 as uuid4
from uuid import uuid4
from kodi_six.utils import py2_decode
import xbmcaddon
import xbmc
import xbmcvfs
@@ -22,14 +24,15 @@ class ClientInformation:
if client_id:
return client_id
jellyfin_guid_path = xbmc.translatePath("special://temp/jellycon_guid").decode('utf-8')
jellyfin_guid_path = py2_decode(xbmc.translatePath("special://temp/jellycon_guid"))
log.debug("jellyfin_guid_path: {0}".format(jellyfin_guid_path))
guid = xbmcvfs.File(jellyfin_guid_path)
client_id = guid.read()
guid.close()
if not client_id:
client_id = str("%012X" % uuid4())
# Needs to be captilized for backwards compat
client_id = uuid4().hex.upper()
log.debug("Generating a new guid: {0}".format(client_id))
guid = xbmcvfs.File(jellyfin_guid_path, 'w')
guid.write(client_id)

View File

@@ -1,3 +1,4 @@
from __future__ import division, absolute_import, print_function, unicode_literals
import threading
import xbmc
@@ -16,7 +17,7 @@ class ContextMonitor(threading.Thread):
item_id = None
log.debug("ContextMonitor Thread Started")
while not xbmc.abortRequested and not self.stop_thread:
while not xbmc.Monitor().abortRequested() and not self.stop_thread:
if xbmc.getCondVisibility("Window.IsActive(fullscreenvideo) | Window.IsActive(visualisation)"):
xbmc.sleep(1000)

View File

@@ -1,12 +1,12 @@
# Gnu General Public License - see LICENSE.TXT
from __future__ import division, absolute_import, print_function, unicode_literals
import json
from collections import defaultdict
import threading
import hashlib
import os
import cPickle
import time
from six.moves import cPickle
from .downloadutils import DownloadUtils
from .loghandler import LazyLogger
@@ -46,15 +46,9 @@ class DataManager:
# log.debug("DataManager __init__")
pass
@staticmethod
def load_json_data(json_data):
return json.loads(json_data, object_hook=lambda d: defaultdict(lambda: None, d))
@timer
def get_content(self, url):
json_data = DownloadUtils().download_url(url)
result = self.load_json_data(json_data)
return result
return DownloadUtils().download_url(url)
@timer
def get_items(self, url, gui_options, use_cache=False):
@@ -68,7 +62,7 @@ class DataManager:
server = download_utils.get_server()
m = hashlib.md5()
m.update(user_id + "|" + str(server) + "|" + url)
m.update('{}|{}|{}'.format(user_id, server, url).encode())
url_hash = m.hexdigest()
cache_file = os.path.join(self.addon_dir, "cache_" + url_hash + ".pickle")
@@ -280,7 +274,8 @@ def clear_cached_server_data():
xbmcvfs.delete(os.path.join(addon_dir, filename))
del_count += 1
msg = string_load(30394) % del_count
log.debug('Deleted {} files'.format(del_count))
msg = string_load(30394)
xbmcgui.Dialog().ok(string_load(30393), msg)

View File

@@ -1,10 +1,11 @@
# Gnu General Public License - see LICENSE.TXT
from __future__ import division, absolute_import, print_function, unicode_literals
import xbmcaddon
import xbmcplugin
import xbmcgui
import urllib
from six.moves.urllib.parse import quote, unquote
import sys
import re
@@ -125,7 +126,7 @@ def get_content(url, params):
if url_prev:
list_item = xbmcgui.ListItem("Prev Page (" + str(start_index - page_limit + 1) + "-" + str(start_index) +
" of " + str(total_records) + ")")
u = sys.argv[0] + "?url=" + urllib.quote(url_prev) + "&mode=GET_CONTENT&media_type=movies"
u = sys.argv[0] + "?url=" + quote(url_prev) + "&mode=GET_CONTENT&media_type=movies"
log.debug("ADDING PREV ListItem: {0} - {1}".format(u, list_item))
dir_items.insert(0, (u, list_item, True))
@@ -135,7 +136,7 @@ def get_content(url, params):
upper_count = total_records
list_item = xbmcgui.ListItem("Next Page (" + str(start_index + page_limit + 1) + "-" +
str(upper_count) + " of " + str(total_records) + ")")
u = sys.argv[0] + "?url=" + urllib.quote(url_next) + "&mode=GET_CONTENT&media_type=movies"
u = sys.argv[0] + "?url=" + quote(url_next) + "&mode=GET_CONTENT&media_type=movies"
log.debug("ADDING NEXT ListItem: {0} - {1}".format(u, list_item))
dir_items.append((u, list_item, True))
@@ -172,11 +173,6 @@ def get_content(url, params):
else:
log.debug("No view id for view type:{0}".format(view_key))
# send display items event
# display_items_notification = {"view_type": view_type}
# log.debug("Sending display_items with data {0}", display_items_notification)
# send_event_notification("display_items", display_items_notification)
if progress is not None:
progress.update(100, string_load(30125))
progress.close()
@@ -237,7 +233,7 @@ def process_directory(url, progress, params, use_cache_data=False):
name_format = params.get("name_format", None)
name_format_type = None
if name_format is not None:
name_format = urllib.unquote(name_format)
name_format = unquote(name_format)
tokens = name_format.split("|")
if len(tokens) == 2:
name_format_type = tokens[0]

View File

@@ -1,5 +1,5 @@
# Gnu General Public License - see LICENSE.TXT
from __future__ import unicode_literals
from __future__ import division, absolute_import, print_function, unicode_literals
import xbmcgui
import xbmcaddon
@@ -7,14 +7,14 @@ import xbmcaddon
import requests
import hashlib
import ssl
import StringIO
import gzip
import json
from urlparse import urlparse
import urllib
from six.moves.urllib.parse import urlparse
from base64 import b64encode
from collections import defaultdict
from traceback import format_exc
from kodi_six.utils import py2_decode
from six import ensure_text
from .kodi_utils import HomeWindow
from .clientinfo import ClientInformation
@@ -158,7 +158,6 @@ class DownloadUtils:
addon_settings = xbmcaddon.Addon()
# ["hevc", "h265", "h264", "mpeg4", "msmpeg4v3", "mpeg2video", "vc1"]
filtered_codecs = []
if addon_settings.getSetting("force_transcode_h265") == "true":
filtered_codecs.append("hevc")
@@ -336,7 +335,6 @@ class DownloadUtils:
log.debug("PlaybackInfo : {0}".format(url))
log.debug("PlaybackInfo : {0}".format(profile))
play_info_result = self.download_url(url, post_body=playback_info, method="POST")
play_info_result = json.loads(play_info_result)
log.debug("PlaybackInfo : {0}".format(play_info_result))
return play_info_result
@@ -360,7 +358,6 @@ class DownloadUtils:
item_id = item["Id"]
item_type = item["Type"]
image_tags = item["ImageTags"]
# bg_item_tags = item["ParentBackdropImageTags"]
# All the image tags
for tag_name in image_tags:
@@ -388,31 +385,28 @@ class DownloadUtils:
item_id = data["SeriesId"]
image_tag = ""
# "e3ab56fe27d389446754d0fb04910a34" # a place holder tag, needs to be in this format
# for episodes always use the parent BG
if item_type == "Episode" and art_type == "Backdrop":
item_id = data["ParentBackdropItemId"]
bg_item_tags = data["ParentBackdropImageTags"]
if bg_item_tags is not None and len(bg_item_tags) > 0:
item_id = data.get("ParentBackdropItemId")
bg_item_tags = data.get("ParentBackdropImageTags", [])
if bg_item_tags:
image_tag = bg_item_tags[0]
elif art_type == "Backdrop" and parent is True:
item_id = data["ParentBackdropItemId"]
bg_item_tags = data["ParentBackdropImageTags"]
if bg_item_tags is not None and len(bg_item_tags) > 0:
item_id = data.get("ParentBackdropItemId")
bg_item_tags = data.get("ParentBackdropImageTags", [])
if bg_item_tags:
image_tag = bg_item_tags[0]
elif art_type == "Backdrop":
bg_tags = data["BackdropImageTags"]
if bg_tags is not None and len(bg_tags) > index:
bg_tags = data.get("BackdropImageTags", [])
if bg_tags:
image_tag = bg_tags[index]
# log.debug("Background Image Tag: {0}", imageTag)
elif parent is False:
image_tags = data["ImageTags"]
if image_tags is not None:
image_tag_type = image_tags[art_type]
if image_tag_type is not None:
image_tags = data.get("ImageTags", [])
if image_tags:
image_tag_type = image_tags.get(art_type)
if image_tag_type:
image_tag = image_tag_type
# log.debug("Image Tag: {0}", imageTag)
elif parent is True:
if (item_type == "Episode" or item_type == "Season") and art_type == 'Primary':
tag_name = 'SeriesPrimaryImageTag'
@@ -420,16 +414,14 @@ class DownloadUtils:
else:
tag_name = 'Parent%sImageTag' % art_type
id_name = 'Parent%sItemId' % art_type
parent_image_id = data[id_name]
parent_image_tag = data[tag_name]
parent_image_id = data.get(id_name)
parent_image_tag = data.get(tag_name)
if parent_image_id is not None and parent_image_tag is not None:
item_id = parent_image_id
image_tag = parent_image_tag
# log.debug("Parent Image Tag: {0}", imageTag)
# ParentTag not passed for Banner and Art
if not image_tag and not ((art_type == 'Banner' or art_type == 'Art') and parent is True):
# log.debug("No Image Tag for request:{0} item:{1} parent:{2}", art_type, item_type, parent)
return ""
artwork = "%s/Items/%s/Images/%s/%s?Format=original&Tag=%s" % (server, item_id, art_type, index, image_tag)
@@ -437,19 +429,9 @@ class DownloadUtils:
if self.use_https and not self.verify_cert:
artwork += "|verifypeer=false"
# log.debug("getArtwork: request:{0} item:{1} parent:{2} link:{3}", art_type, item_type, parent, artwork)
'''
# do not return non-existing images
if ( (art_type != "Backdrop" and imageTag == "") |
(art_type == "Backdrop" and data.get("BackdropImageTags") != None and len(data.get("BackdropImageTags")) == 0) |
(art_type == "Backdrop" and data.get("BackdropImageTag") != None and len(data.get("BackdropImageTag")) == 0)
):
artwork = ''
'''
return artwork
def image_url(self, item_id, art_type, index, width, height, image_tag, server):
# test imageTag e3ab56fe27d389446754d0fb04910a34
@@ -485,7 +467,7 @@ class DownloadUtils:
userid = window.get_property("userid")
user_image = window.get_property("userimage")
if userid and user_image:
if userid:
log.debug("JellyCon DownloadUtils -> Returning saved UserID: {0}".format(userid))
return userid
@@ -498,27 +480,21 @@ class DownloadUtils:
log.debug("Looking for user name: {0}".format(user_name))
try:
json_data = self.download_url("{server}/Users/Public?format=json", suppress=True, authenticate=False)
result = self.download_url("{server}/Users/Public?format=json", suppress=True, authenticate=False)
except Exception as msg:
log.error("Get User unable to connect: {0}".format(msg))
return ""
log.debug("GETUSER_JSONDATA_01: {0}".format(json_data))
log.debug("GETUSER_JSONDATA_01: {0}".format(py2_decode(result)))
try:
result = json.loads(json_data)
except Exception as e:
log.debug("Could not load user data: {0}".format(e))
return ""
if result is None:
if not result:
return ""
log.debug("GETUSER_JSONDATA_02: {0}".format(result))
secure = False
for user in result:
if user.get("Name") == unicode(user_name, "utf-8"):
if user.get("Name") == ensure_text(user_name, "utf-8"):
userid = user.get("Id")
user_image = self.get_user_artwork(user, 'Primary')
log.debug("Username Found: {0}".format(user.get("Name")))
@@ -567,30 +543,24 @@ class DownloadUtils:
url = "{server}/Users/AuthenticateByName?format=json"
user_details = load_user_details(settings)
user_name = urllib.quote(user_details.get("username", ""))
pwd_text = urllib.quote(user_details.get("password", ""))
user_name = user_details.get("username", "")
pwd_text = user_details.get("password", "")
message_data = "username=" + user_name + "&pw=" + pwd_text
message_data = {'username': user_name, 'pw': pwd_text}
resp = self.download_url(url, post_body=message_data, method="POST", suppress=True, authenticate=False)
log.debug("AuthenticateByName: {0}".format(resp))
result = self.download_url(url, post_body=message_data, method="POST", suppress=True, authenticate=False)
log.debug("AuthenticateByName: {0}".format(result))
access_token = None
userid = None
try:
result = json.loads(resp)
access_token = result.get("AccessToken")
# userid = result["SessionInfo"].get("UserId")
userid = result["User"].get("Id")
except:
pass
access_token = result.get("AccessToken")
userid = result["User"].get("Id")
if access_token is not None:
log.debug("User Authenticated: {0}".format(access_token))
log.debug("User Id: {0}".format(userid))
window.set_property("AccessToken", access_token)
window.set_property("userid", userid)
# WINDOW.setProperty("userimage", "")
self.post_capabilities()
@@ -611,7 +581,7 @@ class DownloadUtils:
settings = xbmcaddon.Addon()
device_name = settings.getSetting('deviceName')
# remove none ascii chars
device_name = device_name.decode("ascii", errors='ignore')
device_name = py2_decode(device_name)
# remove some chars not valid for names
device_name = device_name.replace("\"", "_")
if len(device_name) == 0:
@@ -649,7 +619,7 @@ class DownloadUtils:
http_timeout = int(settings.getSetting("http_timeout"))
if authenticate and username == "":
return "null"
return {}
if settings.getSetting("suppressErrors") == "true":
suppress = True
@@ -659,13 +629,13 @@ class DownloadUtils:
if url.find("{server}") != -1:
server = self.get_server()
if server is None:
return "null"
return {}
url = url.replace("{server}", server)
if url.find("{userid}") != -1:
userid = self.get_user_id()
if not userid:
return "null"
return {}
url = url.replace("{userid}", userid)
if url.find("{ItemLimit}") != -1:
@@ -680,7 +650,7 @@ class DownloadUtils:
home_window = HomeWindow()
random_movies = home_window.get_property("random-movies")
if not random_movies:
return "null"
return {}
url = url.replace("{random_movies}", random_movies)
log.debug("After: {0}".format(url))
@@ -735,12 +705,16 @@ class DownloadUtils:
settings.setSetting("saved_user_password_" + hashed_username, "")
save_user_details(settings, "", "")
log.error("HTTP response error: {0} {1}".format(data.status_code, data.content))
log.error("HTTP response error for {0}: {1} {2}".format(url, data.status_code, data.content))
if suppress is False:
xbmcgui.Dialog().notification(string_load(30316),
string_load(30200) % str(data.content),
'{}: {}'.format(string_load(30200), data.content),
icon="special://home/addons/plugin.video.jellycon/icon.png")
return data.content or "null"
try:
result = data.json()
except:
result = {}
return result
except Exception as msg:
log.error("{0}".format(format_exc()))
log.error("Unable to connect to {0} : {1}".format(server, msg))

View File

@@ -1,13 +1,14 @@
# Gnu General Public License - see LICENSE.TXT
from __future__ import division, absolute_import, print_function, unicode_literals
import urllib
from six.moves.urllib.parse import quote, unquote, parse_qsl
import sys
import os
import time
import cProfile
import pstats
import json
import StringIO
from six import StringIO
import xbmcplugin
import xbmcgui
@@ -33,6 +34,7 @@ from .cache_images import CacheArtwork
from .dir_functions import get_content, process_directory
from .tracking import timer
from .skin_cloner import clone_default_skin
from .play_utils import play_file
__addon__ = xbmcaddon.Addon()
__addondir__ = xbmc.translatePath(__addon__.getAddonInfo('profile'))
@@ -67,13 +69,13 @@ def main_entry_point():
log.debug("Script argument data: {0}".format(sys.argv))
params = get_params()
log.debug("Script params: {0}".format(params))
log.info("Script params: {0}".format(params))
request_path = params.get("request_path", None)
param_url = params.get('url', None)
if param_url:
param_url = urllib.unquote(param_url)
param_url = unquote(param_url)
mode = params.get("mode", None)
@@ -280,8 +282,7 @@ def unmark_item_favorite(item_id):
def delete(item_id):
json_data = downloadUtils.download_url("{server}/Users/{userid}/Items/" + item_id + "?format=json")
item = json.loads(json_data)
item = downloadUtils.download_url("{server}/Users/{userid}/Items/" + item_id + "?format=json")
item_id = item.get("Id")
item_name = item.get("Name", "")
@@ -302,7 +303,7 @@ def delete(item_id):
xbmcgui.Dialog().ok(string_load(30135), string_load(30417), final_name)
return
return_value = xbmcgui.Dialog().yesno(string_load(30091), final_name, string_load(30092))
return_value = xbmcgui.Dialog().yesno(string_load(30091), '{}\n{}'.format(final_name, string_load(30092)))
if return_value:
log.debug('Deleting Item: {0}'.format(item_id))
url = '{server}/Items/' + item_id
@@ -320,6 +321,9 @@ def delete(item_id):
def get_params():
'''
Retrieve the request data from Kodi
'''
plugin_path = sys.argv[0]
paramstring = sys.argv[2]
@@ -327,27 +331,12 @@ def get_params():
log.debug("Parameter string: {0}".format(paramstring))
log.debug("Plugin Path string: {0}".format(plugin_path))
param = {}
param = dict(parse_qsl(paramstring[1:]))
# add plugin path
request_path = plugin_path.replace("plugin://plugin.video.jellycon", "")
param["request_path"] = request_path
if len(paramstring) >= 2:
if paramstring[0] == "?":
paramstring = paramstring[1:]
if paramstring[len(paramstring) - 1] == '/':
paramstring = paramstring[0:len(paramstring) - 2]
pairsofparams = paramstring.split('&')
for i in range(len(pairsofparams)):
splitparams = pairsofparams[i].split('=')
if (len(splitparams)) == 2:
param[splitparams[0]] = splitparams[1]
elif (len(splitparams)) == 3:
param[splitparams[0]] = splitparams[1] + "=" + splitparams[2]
log.debug("JellyCon -> Detected parameters: {0}".format(param))
return param
@@ -478,8 +467,6 @@ def show_menu(params):
li.setProperty('menu_id', 'set_view')
action_items.append(li)
# xbmcplugin.endOfDirectory(int(sys.argv[1]), cacheToDisc=False)
action_menu = ActionMenu("ActionMenu.xml", PLUGINPATH, "default", "720p")
action_menu.setActionItems(action_items)
action_menu.doModal()
@@ -492,9 +479,6 @@ def show_menu(params):
if selected_action == "play":
log.debug("Play Item")
# list_item = populate_listitem(params["item_id"])
# result = xbmcgui.Dialog().info(list_item)
# log.debug("xbmcgui.Dialog().info: {0}", result)
play_action(params)
elif selected_action == "set_view":
@@ -577,8 +561,7 @@ def show_menu(params):
elif selected_action == "safe_delete":
url = "{server}/jellyfin_safe_delete/delete_item/" + item_id
delete_action = downloadUtils.download_url(url)
result = json.loads(delete_action)
result = downloadUtils.download_url(url)
dialog = xbmcgui.Dialog()
if result:
log.debug("Safe_Delete_Action: {0}".format(result))
@@ -621,11 +604,10 @@ def show_menu(params):
}
delete_action = downloadUtils.download_url(url, method="POST", post_body=playback_info)
log.debug("Delete result action: {0}".format(delete_action))
delete_action_result = json.loads(delete_action)
if not delete_action_result:
if not delete_action:
dialog.ok("Error", "Error deleting files", "Error in responce from server")
elif not delete_action_result["result"]:
dialog.ok("Error", "Error deleting files", delete_action_result["message"])
elif not delete_action.get("result"):
dialog.ok("Error", "Error deleting files", delete_action["message"])
else:
dialog.ok("Deleted", "Files deleted")
else:
@@ -634,7 +616,7 @@ def show_menu(params):
elif selected_action == "show_extras":
# "http://localhost:8096/Users/3138bed521e5465b9be26d2c63be94af/Items/78/SpecialFeatures"
u = "{server}/Users/{userid}/Items/" + item_id + "/SpecialFeatures"
action_url = ("plugin://plugin.video.jellycon/?url=" + urllib.quote(u) + "&mode=GET_CONTENT&media_type=Videos")
action_url = ("plugin://plugin.video.jellycon/?url=" + quote(u) + "&mode=GET_CONTENT&media_type=Videos")
built_in_command = 'ActivateWindow(Videos, ' + action_url + ', return)'
xbmc.executebuiltin(built_in_command)
@@ -650,7 +632,7 @@ def show_menu(params):
'&IsMissing=false' +
'&Fields=SpecialEpisodeNumbers,{field_filters}' +
'&format=json')
action_url = ("plugin://plugin.video.jellycon/?url=" + urllib.quote(u) + "&mode=GET_CONTENT&media_type=Season")
action_url = ("plugin://plugin.video.jellycon/?url=" + quote(u) + "&mode=GET_CONTENT&media_type=Season")
built_in_command = 'ActivateWindow(Videos, ' + action_url + ', return)'
xbmc.executebuiltin(built_in_command)
@@ -667,12 +649,11 @@ def show_menu(params):
'&Fields={field_filters}' +
'&format=json')
action_url = ("plugin://plugin.video.jellycon/?url=" + urllib.quote(u) + "&mode=GET_CONTENT&media_type=Series")
action_url = ("plugin://plugin.video.jellycon/?url=" + quote(u) + "&mode=GET_CONTENT&media_type=Series")
if xbmc.getCondVisibility("Window.IsActive(home)"):
built_in_command = 'ActivateWindow(Videos, ' + action_url + ', return)'
else:
# built_in_command = 'Container.Update(' + action_url + ', replace)'
built_in_command = 'Container.Update(' + action_url + ')'
xbmc.executebuiltin(built_in_command)
@@ -689,8 +670,7 @@ def populate_listitem(item_id):
log.debug("populate_listitem: {0}".format(item_id))
url = "{server}/Users/{userid}/Items/" + item_id + "?format=json"
json_data = downloadUtils.download_url(url)
result = json.loads(json_data)
result = downloadUtils.download_url(url)
log.debug("populate_listitem item info: {0}".format(result))
item_title = result.get("Name", string_load(30280))
@@ -754,7 +734,6 @@ def search_results_person(params):
person_id = params.get("person_id")
details_url = ('{server}/Users/{userid}/items' +
'?PersonIds=' + person_id +
# '&IncludeItemTypes=Movie' +
'&Recursive=true' +
'&Fields={field_filters}' +
'&format=json')
@@ -783,8 +762,6 @@ def search_results_person(params):
if content_type:
xbmcplugin.setContent(handle, content_type)
# xbmcplugin.setContent(handle, detected_type)
if dir_items is not None:
xbmcplugin.addDirectoryItems(handle, dir_items)
@@ -797,7 +774,7 @@ def search_results(params):
query_string = params.get('query')
if query_string:
log.debug("query_string : {0}".format(query_string))
query_string = urllib.unquote(query_string)
query_string = unquote(query_string)
log.debug("query_string : {0}".format(query_string))
item_type = item_type.lower()
@@ -844,7 +821,7 @@ def search_results(params):
else:
query = query_string
query = urllib.quote(query)
query = quote(query)
log.debug("query : {0}".format(query))
if (not item_type) or (not query):
@@ -886,8 +863,6 @@ def search_results(params):
for item in person_items:
person_id = item.get('Id')
person_name = item.get('Name')
# image_tags = item.get('ImageTags', {})
# image_tag = image_tags.get('PrimaryImageTag', '')
person_thumbnail = downloadUtils.get_artwork(item, "Primary", server=server)
action_url = sys.argv[0] + "?mode=NEW_SEARCH_PERSON&person_id=" + person_id
@@ -941,7 +916,14 @@ def play_action(params):
log.debug("PLAY ACTION PARAMS: {0}".format(params))
item_id = params.get("item_id")
auto_resume = int(params.get("auto_resume", "-1"))
auto_resume = params.get("auto_resume", "-1")
if auto_resume == 'None':
auto_resume = '-1'
if auto_resume:
auto_resume = int(auto_resume)
else:
auto_resume = -1
log.debug("AUTO_RESUME: {0}".format(auto_resume))
force_transcode = params.get("force_transcode", None) is not None
@@ -973,7 +955,7 @@ def play_action(params):
play_info["subtitle_stream_index"] = subtitle_stream_index
play_info["audio_stream_index"] = audio_stream_index
log.info("Sending jellycon_play_action : {0}".format(play_info))
send_event_notification("jellycon_play_action", play_info)
play_file(play_info)
def play_item_trailer(item_id):
@@ -981,8 +963,7 @@ def play_item_trailer(item_id):
url = ("{server}/Users/{userid}/Items/%s/LocalTrailers?format=json" % item_id)
json_data = downloadUtils.download_url(url)
result = json.loads(json_data)
result = downloadUtils.download_url(url)
if result is None:
return
@@ -1006,8 +987,7 @@ def play_item_trailer(item_id):
trailer_list.append(info)
url = ("{server}/Users/{userid}/Items/%s?format=json&Fields=RemoteTrailers" % item_id)
json_data = downloadUtils.download_url(url)
result = json.loads(json_data)
result = downloadUtils.download_url(url)
log.debug("RemoteTrailers: {0}".format(result))
count = 1
@@ -1052,8 +1032,4 @@ def play_item_trailer(item_id):
youtube_plugin = "RunPlugin(plugin://plugin.video.youtube/play/?video_id=%s)" % youtube_id
log.debug("youtube_plugin: {0}".format(youtube_plugin))
# play_info = {}
# play_info["url"] = youtube_plugin
# log.info("Sending jellycon_play_trailer_action : {0}", play_info)
# send_event_notification("jellycon_play_youtube_trailer_action", play_info)
xbmc.executebuiltin(youtube_plugin)

View File

@@ -1,14 +1,17 @@
from __future__ import division, absolute_import, print_function, unicode_literals
import xbmcvfs
import xbmc
import base64
import re
from urlparse import urlparse
from random import shuffle
from six.moves.BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from six.moves.urllib.parse import urlparse
from six import ensure_text
import threading
import requests
import io
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from .loghandler import LazyLogger
from .datamanager import DataManager
@@ -33,16 +36,6 @@ def get_image_links(url):
if server is None:
return []
# url = re.sub("(?i)limit=[0-9]+", "limit=4", url)
# url = url.replace("{ItemLimit}", "4")
# url = re.sub("(?i)SortBy=[a-zA-Z]+", "SortBy=Random", url)
# if not re.search('limit=', url, re.IGNORECASE):
# url += "&Limit=4"
# if not re.search('sortBy=', url, re.IGNORECASE):
# url += "&SortBy=Random"
url = re.sub("(?i)EnableUserData=[a-z]+", "EnableUserData=False", url)
url = re.sub("(?i)EnableImageTypes=[,a-z]+", "EnableImageTypes=Primary", url)
url = url.replace("{field_filters}", "BasicSyncInfo")
@@ -84,7 +77,7 @@ def build_image(path):
if request_path == "favicon.ico":
return []
decoded_url = base64.b64decode(request_path)
decoded_url = ensure_text(base64.b64decode(request_path))
log.debug("decoded_url : {0}".format(decoded_url))
image_urls = get_image_links(decoded_url)
@@ -107,8 +100,6 @@ def build_image(path):
host_name = url_bits.hostname
port = url_bits.port
# user_name = url_bits.username
# user_password = url_bits.password
url_path = url_bits.path
url_query = url_bits.query
@@ -196,20 +187,22 @@ class HttpImageHandler(BaseHTTPRequestHandler):
class HttpImageServerThread(threading.Thread):
keep_running = True
def __init__(self):
threading.Thread.__init__(self)
self.keep_running = True
def stop(self):
log.debug("HttpImageServerThread:stop called")
self.keep_running = False
self.server.shutdown()
def run(self):
log.debug("HttpImageServerThread:started")
server = HTTPServer(('', PORT_NUMBER), HttpImageHandler)
self.server = HTTPServer(('', PORT_NUMBER), HttpImageHandler)
while self.keep_running:
server.handle_request()
self.server.serve_forever()
xbmc.sleep(1000)
log.debug("HttpImageServerThread:exiting")

View File

@@ -1,7 +1,8 @@
from __future__ import division, absolute_import, print_function, unicode_literals
import sys
import os
import urllib
from six.moves.urllib.parse import quote
from datetime import datetime
@@ -15,6 +16,7 @@ from .utils import get_art, datetime_from_string
from .loghandler import LazyLogger
from .downloadutils import DownloadUtils
from .kodi_utils import HomeWindow
from six import ensure_text
log = LazyLogger(__name__)
kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2])
@@ -100,27 +102,27 @@ def extract_item_info(item, gui_options):
item_details = ItemDetails()
item_details.id = item["Id"]
item_details.etag = item["Etag"]
item_details.is_folder = item["IsFolder"]
item_details.item_type = item["Type"]
item_details.location_type = item["LocationType"]
item_details.name = item["Name"]
item_details.sort_name = item["SortName"]
item_details.id = item.get("Id")
item_details.etag = item.get("Etag")
item_details.is_folder = item.get("IsFolder")
item_details.item_type = item.get("Type")
item_details.location_type = item.get("LocationType")
item_details.name = item.get("Name")
item_details.sort_name = item.get("SortName")
item_details.original_title = item_details.name
if item_details.item_type == "Episode":
item_details.episode_number = item["IndexNumber"]
item_details.season_number = item["ParentIndexNumber"]
item_details.series_id = item["SeriesId"]
item_details.episode_number = item.get("IndexNumber")
item_details.season_number = item.get("ParentIndexNumber")
item_details.series_id = item.get("SeriesId")
if item_details.season_number != 0:
item_details.season_sort_number = item_details.season_number
item_details.episode_sort_number = item_details.episode_number
else:
special_after_season = item["AirsAfterSeasonNumber"]
special_before_season = item["AirsBeforeSeasonNumber"]
special_before_episode = item["AirsBeforeEpisodeNumber"]
special_after_season = item.get("AirsAfterSeasonNumber")
special_before_season = item.get("AirsBeforeSeasonNumber")
special_before_episode = item.get("AirsBeforeEpisodeNumber")
if special_after_season:
item_details.season_sort_number = special_after_season + 1
@@ -131,21 +133,21 @@ def extract_item_info(item, gui_options):
item_details.episode_sort_number = special_before_episode - 1
elif item_details.item_type == "Season":
item_details.season_number = item["IndexNumber"]
item_details.series_id = item["SeriesId"]
item_details.season_number = item.get("IndexNumber")
item_details.series_id = item.get("SeriesId")
elif item_details.item_type == "Series":
item_details.status = item["Status"]
item_details.status = item.get("Status")
elif item_details.item_type == "Audio":
item_details.track_number = item["IndexNumber"]
item_details.album_name = item["Album"]
artists = item["Artists"]
if artists is not None and len(artists) > 0:
item_details.track_number = item.get("IndexNumber")
item_details.album_name = item.get("Album")
artists = item.get("Artists", [])
if artists:
item_details.song_artist = artists[0] # get first artist
elif item_details.item_type == "MusicAlbum":
item_details.album_artist = item["AlbumArtist"]
item_details.album_artist = item.get("AlbumArtist")
item_details.album_name = item_details.name
if item_details.season_number is None:
@@ -153,23 +155,23 @@ def extract_item_info(item, gui_options):
if item_details.episode_number is None:
item_details.episode_number = 0
if item["Taglines"] is not None and len(item["Taglines"]) > 0:
item_details.tagline = item["Taglines"][0]
if item.get("Taglines", []):
item_details.tagline = item.get("Taglines")[0]
item_details.tags = []
if item["TagItems"] is not None and len(item["TagItems"]) > 0:
for tag_info in item["TagItems"]:
item_details.tags.append(tag_info["Name"])
if item.get("TagItems", []):
for tag_info in item.get("TagItems"):
item_details.tags.append(tag_info.get("Name"))
# set the item name
# override with name format string from request
name_format = gui_options["name_format"]
name_format_type = gui_options["name_format_type"]
name_format = gui_options.get("name_format")
name_format_type = gui_options.get("name_format_type")
if name_format is not None and item_details.item_type == name_format_type:
name_info = {}
name_info["ItemName"] = item["Name"]
season_name = item["SeriesName"]
name_info["ItemName"] = item.get("Name")
season_name = item.get("SeriesName")
if season_name:
name_info["SeriesName"] = season_name
else:
@@ -177,10 +179,10 @@ def extract_item_info(item, gui_options):
name_info["SeasonIndex"] = u"%02d" % item_details.season_number
name_info["EpisodeIndex"] = u"%02d" % item_details.episode_number
log.debug("FormatName: {0} | {1}".format(name_format, name_info))
item_details.name = unicode(name_format).format(**name_info).strip()
item_details.name = ensure_text(name_format).format(**name_info).strip()
year = item["ProductionYear"]
prem_date = item["PremiereDate"]
year = item.get("ProductionYear")
prem_date = item.get("PremiereDate")
if year is not None:
item_details.year = year
@@ -191,35 +193,35 @@ def extract_item_info(item, gui_options):
tokens = prem_date.split("T")
item_details.premiere_date = tokens[0]
create_date = item["DateCreated"]
if create_date is not None:
create_date = item.get("DateCreated")
if create_date:
item_details.date_added = create_date.split('.')[0].replace('T', " ")
# add the premiered date for Upcoming TV
if item_details.location_type == "Virtual":
airtime = item["AirTime"]
airtime = item.get("AirTime")
item_details.name = item_details.name + ' - ' + item_details.premiere_date + ' - ' + str(airtime)
if item_details.item_type == "Program":
item_details.program_channel_name = item["ChannelName"]
item_details.program_start_date = item["StartDate"]
item_details.program_end_date = item["EndDate"]
item_details.program_channel_name = item.get("ChannelName")
item_details.program_start_date = item.get("StartDate")
item_details.program_end_date = item.get("EndDate")
# Process MediaStreams
media_streams = item["MediaStreams"]
if media_streams is not None:
media_streams = item.get("MediaStreams", [])
if media_streams:
media_info_list = []
for mediaStream in media_streams:
stream_type = mediaStream["Type"]
stream_type = mediaStream.get("Type")
if stream_type == "Video":
media_info = {}
media_info["type"] = "video"
media_info["codec"] = mediaStream["Codec"]
media_info["height"] = mediaStream["Height"]
media_info["width"] = mediaStream["Width"]
aspect_ratio = mediaStream["AspectRatio"]
media_info["codec"] = mediaStream.get("Codec")
media_info["height"] = mediaStream.get("Height")
media_info["width"] = mediaStream.get("Width")
aspect_ratio = mediaStream.get("AspectRatio")
media_info["apect"] = aspect_ratio
if aspect_ratio is not None and len(aspect_ratio) >= 3:
if aspect_ratio and len(aspect_ratio) >= 3:
try:
aspect_width, aspect_height = aspect_ratio.split(':')
media_info["apect_ratio"] = float(aspect_width) / float(aspect_height)
@@ -231,36 +233,35 @@ def extract_item_info(item, gui_options):
if stream_type == "Audio":
media_info = {}
media_info["type"] = "audio"
media_info["codec"] = mediaStream["Codec"]
media_info["channels"] = mediaStream["Channels"]
media_info["language"] = mediaStream["Language"]
media_info["codec"] = mediaStream.get("Codec")
media_info["channels"] = mediaStream.get("Channels")
media_info["language"] = mediaStream.get("Language")
media_info_list.append(media_info)
if stream_type == "Subtitle":
item_details.subtitle_available = True
media_info = {}
media_info["type"] = "sub"
media_info["language"] = mediaStream["Language"]
media_info["language"] = mediaStream.get("Language", '')
media_info_list.append(media_info)
item_details.media_streams = media_info_list
# Process People
people = item["People"]
people = item.get("People", [])
if people is not None:
cast = []
for person in people:
person_type = person["Type"]
person_type = person.get("Type")
if person_type == "Director":
item_details.director = item_details.director + person["Name"] + ' '
item_details.director = item_details.director + person.get("Name") + ' '
elif person_type == "Writing":
item_details.writer = person["Name"]
elif person_type == "Actor":
# log.debug("Person: {0}", person)
person_name = person["Name"]
person_role = person["Role"]
person_id = person["Id"]
person_tag = person["PrimaryImageTag"]
if person_tag is not None:
person_name = person.get("Name")
person_role = person.get("Role")
person_id = person.get("Id")
person_tag = person.get("PrimaryImageTag")
if person_tag:
person_thumbnail = download_utils.image_url(person_id,
"Primary", 0, 400, 400,
person_tag,
@@ -272,64 +273,61 @@ def extract_item_info(item, gui_options):
item_details.cast = cast
# Process Studios
studios = item["Studios"]
studios = item.get("Studios", [])
if studios is not None:
for studio in studios:
if item_details.studio is None: # Just take the first one
studio_name = studio["Name"]
studio_name = studio.get("Name")
item_details.studio = studio_name
break
# production location
prod_location = item["ProductionLocations"]
# log.debug("ProductionLocations : {0}", prod_location)
if prod_location and len(prod_location) > 0:
prod_location = item.get("ProductionLocations", [])
if prod_location:
item_details.production_location = prod_location[0]
# Process Genres
genres = item["Genres"]
if genres is not None and len(genres) > 0:
genres = item.get("Genres", [])
if genres:
item_details.genres = genres
# Process UserData
user_data = item["UserData"]
if user_data is None:
user_data = defaultdict(lambda: None, {})
user_data = item.get("UserData", {})
if user_data["Played"] is True:
if user_data.get("Played"):
item_details.overlay = "6"
item_details.play_count = 1
else:
item_details.overlay = "7"
item_details.play_count = 0
if user_data["IsFavorite"] is True:
if user_data.get("IsFavorite"):
item_details.overlay = "5"
item_details.favorite = "true"
else:
item_details.favorite = "false"
reasonable_ticks = user_data["PlaybackPositionTicks"]
if reasonable_ticks is not None:
reasonable_ticks = user_data.get("PlaybackPositionTicks", 0)
if reasonable_ticks:
reasonable_ticks = int(reasonable_ticks) / 1000
item_details.resume_time = int(reasonable_ticks / 10000)
item_details.series_name = item["SeriesName"]
item_details.plot = item["Overview"]
item_details.series_name = item.get("SeriesName", '')
item_details.plot = item.get("Overview", '')
runtime = item["RunTimeTicks"]
if item_details.is_folder is False and runtime is not None:
item_details.duration = long(runtime) / 10000000
runtime = item.get("RunTimeTicks")
if item_details.is_folder is False and runtime:
item_details.duration = runtime / 10000000
child_count = item["ChildCount"]
if child_count is not None:
child_count = item.get("ChildCount")
if child_count:
item_details.total_seasons = child_count
recursive_item_count = item["RecursiveItemCount"]
if recursive_item_count is not None:
recursive_item_count = item.get("RecursiveItemCount")
if recursive_item_count:
item_details.total_episodes = recursive_item_count
unplayed_item_count = user_data["UnplayedItemCount"]
unplayed_item_count = user_data.get("UnplayedItemCount")
if unplayed_item_count is not None:
item_details.unwatched_episodes = unplayed_item_count
item_details.watched_episodes = item_details.total_episodes - unplayed_item_count
@@ -337,20 +335,20 @@ def extract_item_info(item, gui_options):
item_details.number_episodes = item_details.total_episodes
item_details.art = get_art(item, gui_options["server"])
item_details.rating = item["OfficialRating"]
item_details.mpaa = item["OfficialRating"]
item_details.rating = item.get("OfficialRating")
item_details.mpaa = item.get("OfficialRating")
item_details.community_rating = item["CommunityRating"]
if item_details.community_rating is None:
item_details.community_rating = item.get("CommunityRating")
if not item_details.community_rating:
item_details.community_rating = 0.0
item_details.critic_rating = item["CriticRating"]
if item_details.critic_rating is None:
item_details.critic_rating = item.get("CriticRating")
if not item_details.critic_rating:
item_details.critic_rating = 0.0
item_details.location_type = item["LocationType"]
item_details.recursive_item_count = item["RecursiveItemCount"]
item_details.recursive_unplayed_items_count = user_data["UnplayedItemCount"]
item_details.location_type = item.get("LocationType")
item_details.recursive_item_count = item.get("RecursiveItemCount")
item_details.recursive_unplayed_items_count = user_data.get("UnplayedItemCount")
item_details.mode = "GET_CONTENT"
@@ -359,8 +357,6 @@ def extract_item_info(item, gui_options):
def add_gui_item(url, item_details, display_options, folder=True, default_sort=False):
# log.debug("item_details: {0}", item_details.__dict__)
if not item_details.name:
return None
@@ -371,9 +367,9 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
# Create the URL to pass to the item
if folder:
u = sys.argv[0] + "?url=" + urllib.quote(url) + mode + "&media_type=" + item_details.item_type
u = sys.argv[0] + "?url=" + quote(url) + mode + "&media_type=" + item_details.item_type
if item_details.name_format:
u += '&name_format=' + urllib.quote(item_details.name_format)
u += '&name_format=' + quote(item_details.name_format)
if default_sort:
u += '&sort=none'
else:
@@ -450,8 +446,6 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
else:
list_item = xbmcgui.ListItem(list_item_name, iconImage=thumb_path, thumbnailImage=thumb_path)
# log.debug("Setting thumbnail as: {0}", thumbPath)
item_properties = {}
# calculate percentage
@@ -477,7 +471,7 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
info_labels = {}
# add cast
if item_details.cast is not None:
if item_details.cast:
if kodi_version >= 17:
list_item.setCast(item_details.cast)
else:
@@ -497,11 +491,11 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
info_labels["rating"] = item_details.rating
info_labels["year"] = item_details.year
if item_details.genres is not None and len(item_details.genres) > 0:
if item_details.genres:
genres_list = []
for genre in item_details.genres:
genres_list.append(urllib.quote(genre.encode('utf8')))
item_properties["genres"] = urllib.quote("|".join(genres_list))
genres_list.append(quote(genre.encode('utf8')))
item_properties["genres"] = quote("|".join(genres_list))
info_labels["genre"] = " / ".join(item_details.genres)
@@ -569,7 +563,6 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
info_labels["trailer"] = "plugin://plugin.video.jellycon?mode=playTrailer&id=" + item_details.id
list_item.setInfo('video', info_labels)
# log.debug("info_labels: {0}", info_labels)
if item_details.media_streams is not None:
for stream in item_details.media_streams:
@@ -596,7 +589,6 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
item_properties["NumEpisodes"] = str(item_details.number_episodes)
list_item.setRating("imdb", item_details.community_rating, 0, True)
# list_item.setRating("rt", item_details.critic_rating, 0, False)
item_properties["TotalTime"] = str(item_details.duration)
else:
@@ -607,7 +599,6 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
info_labels["artist"] = item_details.song_artist
info_labels["album"] = item_details.album_name
# log.debug("info_labels: {0}", info_labels)
list_item.setInfo('music', info_labels)
list_item.setContentLookup(False)
@@ -617,7 +608,6 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
if item_details.baseline_itemname is not None:
item_properties["suggested_from_watching"] = item_details.baseline_itemname
# log.debug("item_properties: {0}", item_properties)
if kodi_version > 17:
list_item.setProperties(item_properties)
else:

View File

@@ -1,3 +1,5 @@
from __future__ import division, absolute_import, print_function, unicode_literals
import json
import xbmc
@@ -9,7 +11,7 @@ class JsonRpc(object):
params = None
def __init__(self, method, **kwargs):
self.method = method
for arg in kwargs: # id_(int), jsonrpc(str)
@@ -18,7 +20,7 @@ class JsonRpc(object):
def _query(self):
query = {
'jsonrpc': self.jsonrpc,
'id': self.id_,
'method': self.method,

View File

@@ -1,3 +1,5 @@
from __future__ import division, absolute_import, print_function, unicode_literals
import xbmc
import xbmcgui
import xbmcplugin
@@ -24,17 +26,14 @@ class HomeWindow:
def get_property(self, key):
key = self.id_string % key
value = self.window.getProperty(key)
# log.debug('HomeWindow: getProperty |{0}| -> |{1}|', key, value)
return value
def set_property(self, key, value):
key = self.id_string % key
# log.debug('HomeWindow: setProperty |{0}| -> |{1}|', key, value)
self.window.setProperty(key, value)
def clear_property(self, key):
key = self.id_string % key
# log.debug('HomeWindow: clearProperty |{0}|', key)
self.window.clearProperty(key)

View File

@@ -1,3 +1,5 @@
from __future__ import division, absolute_import, print_function, unicode_literals
import threading
import time

View File

@@ -10,11 +10,7 @@ import traceback
from six import ensure_text
from kodi_six import xbmc, xbmcaddon
from urlparse import urlparse
#from .utils import get_filesystem_encoding
#from . import settings
from six.moves.urllib.parse import urlparse
##################################################################################################
@@ -41,37 +37,26 @@ class LogHandler(logging.StreamHandler):
self.sensitive = {'Token': [], 'Server': []}
settings = xbmcaddon.Addon()
server = settings.getSetting('server_address')
if server:
url_bits = urlparse(server)
server = url_bits.netloc
self.sensitive['Server'].append(server)
#for server in database.get_credentials()['Servers']:
# if server.get('AccessToken'):
# self.sensitive['Token'].append(server['AccessToken'])
# if server.get('address'):
# self.sensitive['Server'].append(server['address'].split('://')[1])
#self.mask_info = settings('maskInfo.bool')
self.server = settings.getSetting('server_address')
self.debug = settings.getSetting('log_debug')
def emit(self, record):
if self._get_log_level(record.levelno):
string = self.format(record)
#if self.mask_info:
for server in self.sensitive['Server']:
string = string.replace(server or "{server}", "{jellyfin-server}")
# Hide server URL in logs
string = string.replace(self.server or "{server}", "{jellyfin-server}")
# for token in self.sensitive['Token']:
# string = string.replace(token or "{token}", "{jellyfin-token}")
py_version = sys.version_info.major
# Log level notation changed in Kodi v19
if py_version > 2:
log_level = xbmc.LOGINFO
else:
log_level = xbmc.LOGNOTICE
xbmc.log(string, level=log_level)
xbmc.log(string, level=xbmc.LOGNOTICE)
@classmethod
def _get_log_level(cls, level):
def _get_log_level(self, level):
levels = {
logging.ERROR: 0,
@@ -79,12 +64,10 @@ class LogHandler(logging.StreamHandler):
logging.INFO: 1,
logging.DEBUG: 2
}
### TODO
log_level = 2
#try:
# log_level = int(settings('logLevel'))
#except ValueError:
# log_level = 2 # If getting settings fail, we probably want debug logging.
if self.debug == 'true':
log_level = 2
else:
log_level = 1
return log_level >= levels[level]

View File

@@ -1,10 +1,12 @@
# coding=utf-8
# Gnu General Public License - see LICENSE.TXT
from __future__ import division, absolute_import, print_function, unicode_literals
import sys
import json
import urllib
from six import ensure_binary, ensure_text
from six.moves.urllib.parse import quote
import base64
import string
import xbmcplugin
import xbmcaddon
@@ -73,9 +75,9 @@ def show_movie_tags(menu_params):
item_url = get_jellyfin_url("{server}/Users/{userid}/Items", url_params)
art = {"thumb": "http://localhost:24276/" + base64.b64encode(item_url)}
art = {"thumb": "http://localhost:24276/{}".format(ensure_text(base64.b64encode(ensure_binary(item_url))))}
content_url = urllib.quote(item_url)
content_url = quote(item_url)
url = sys.argv[0] + ("?url=" +
content_url +
"&mode=GET_CONTENT" +
@@ -159,9 +161,9 @@ def show_movie_years(menu_params):
item_url = get_jellyfin_url("{server}/Users/{userid}/Items", params)
art = {"thumb": "http://localhost:24276/" + base64.b64encode(item_url)}
art = {"thumb": "http://localhost:24276/{}".format(ensure_text(base64.b64encode(ensure_binary(item_url))))}
content_url = urllib.quote(item_url)
content_url = quote(item_url)
url = sys.argv[0] + ("?url=" +
content_url +
"&mode=GET_CONTENT" +
@@ -240,13 +242,13 @@ def show_movie_pages(menu_params):
item_data['path'] = item_url
item_data['media_type'] = 'movies'
item_data["art"] = {"thumb": "http://localhost:24276/" + base64.b64encode(item_url)}
item_data['art'] = {"thumb": "http://localhost:24276/{}".format(ensure_text(base64.b64encode(ensure_binary(item_url))))}
collections.append(item_data)
start_index = start_index + page_limit
for collection in collections:
content_url = urllib.quote(collection['path'])
content_url = quote(collection['path'])
url = sys.argv[0] + ("?url=" + content_url +
"&mode=GET_CONTENT" +
"&media_type=" + collection["media_type"])
@@ -321,14 +323,14 @@ def show_genre_list(menu_params):
url = get_jellyfin_url("{server}/Users/{userid}/Items", params)
art = {"thumb": "http://localhost:24276/" + base64.b64encode(url)}
art = {"thumb": "http://localhost:24276/{}".format(ensure_text(base64.b64encode(ensure_binary(url))))}
item_data['art'] = art
item_data['path'] = url
collections.append(item_data)
for collection in collections:
url = sys.argv[0] + ("?url=" + urllib.quote(collection['path']) +
url = sys.argv[0] + ("?url=" + quote(collection['path']) +
"&mode=GET_CONTENT" +
"&media_type=" + collection["media_type"])
log.debug("addMenuDirectoryItem: {0} - {1} - {2}".format(collection.get('title'), url, collection.get("art")))
@@ -360,22 +362,12 @@ def show_movie_alpha_list(menu_params):
if parent_id is not None:
url_params["ParentId"] = parent_id
prefix_url = get_jellyfin_url("{server}/Items/Prefixes", url_params)
data_manager = DataManager()
result = data_manager.get_content(prefix_url)
if not result:
return
alpha_list = []
for prefix in result:
alpha_list.append(prefix.get("Name"))
prefixes = '#' + string.ascii_uppercase
collections = []
for alphaName in alpha_list:
for alpha_name in prefixes:
item_data = {}
item_data['title'] = alphaName
item_data['title'] = alpha_name
item_data['media_type'] = "Movies"
params = {}
@@ -391,21 +383,21 @@ def show_movie_alpha_list(menu_params):
if parent_id is not None:
params["ParentId"] = parent_id
if alphaName == "#":
if alpha_name == "#":
params["NameLessThan"] = "A"
else:
params["NameStartsWith"] = alphaName
params["NameStartsWith"] = alpha_name
url = get_jellyfin_url("{server}/Users/{userid}/Items", params)
item_data['path'] = url
art = {"thumb": "http://localhost:24276/" + base64.b64encode(url)}
art = {"thumb": "http://localhost:24276/{}".format(ensure_text(base64.b64encode(ensure_binary(url))))}
item_data['art'] = art
collections.append(item_data)
for collection in collections:
url = (sys.argv[0] + "?url=" + urllib.quote(collection['path']) +
url = (sys.argv[0] + "?url=" + quote(collection['path']) +
"&mode=GET_CONTENT&media_type=" + collection["media_type"])
log.debug("addMenuDirectoryItem: {0} ({1})".format(collection.get('title'), url))
add_menu_directory_item(collection.get('title', string_load(30250)), url, art=collection.get("art"))
@@ -430,20 +422,11 @@ def show_tvshow_alpha_list(menu_params):
url_params["SortOrder"] = "Ascending"
if parent_id is not None:
menu_params["ParentId"] = parent_id
prefix_url = get_jellyfin_url("{server}/Items/Prefixes", url_params)
data_manager = DataManager()
result = data_manager.get_content(prefix_url)
if not result:
return
alpha_list = []
for prefix in result:
alpha_list.append(prefix.get("Name"))
prefixes = '#' + string.ascii_uppercase
collections = []
for alpha_name in alpha_list:
for alpha_name in prefixes:
item_data = {}
item_data['title'] = alpha_name
item_data['media_type'] = "tvshows"
@@ -469,13 +452,13 @@ def show_tvshow_alpha_list(menu_params):
item_data['path'] = path
art = {"thumb": "http://localhost:24276/" + base64.b64encode(path)}
art = {"thumb": "http://localhost:24276/{}".format(ensure_text(base64.b64encode(ensure_binary(path))))}
item_data['art'] = art
collections.append(item_data)
for collection in collections:
url = (sys.argv[0] + "?url=" + urllib.quote(collection['path']) +
url = (sys.argv[0] + "?url=" + quote(collection['path']) +
"&mode=GET_CONTENT&media_type=" + collection["media_type"])
log.debug("addMenuDirectoryItem: {0} ({1})".format(collection.get('title'), url))
add_menu_directory_item(collection.get('title', string_load(30250)), url, art=collection.get("art"))
@@ -547,7 +530,7 @@ def display_homevideos_type(menu_params, view):
base_params["Fields"] = "{field_filters}"
base_params["ImageTypeLimit"] = 1
path = get_jellyfin_url("{server}/Users/{userid}/Items", base_params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=homevideos"
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=homevideos"
add_menu_directory_item(view_name + string_load(30405), url)
# In progress home movies
@@ -557,7 +540,7 @@ def display_homevideos_type(menu_params, view):
params["Recursive"] = True
params["Limit"] = "{ItemLimit}"
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=homevideos"
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=homevideos"
add_menu_directory_item(view_name + string_load(30267) + " (" + show_x_filtered_items + ")", url)
# Recently added
@@ -571,7 +554,7 @@ def display_homevideos_type(menu_params, view):
params["IsPlayed"] = False
params["Limit"] = "{ItemLimit}"
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=homevideos"
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=homevideos"
add_menu_directory_item(view_name + string_load(30268) + " (" + show_x_filtered_items + ")", url)
xbmcplugin.endOfDirectory(handle)
@@ -613,7 +596,7 @@ def display_tvshow_type(menu_params, view):
base_params["IncludeItemTypes"] = "Series"
base_params["Recursive"] = True
path = get_jellyfin_url("{server}/Users/{userid}/Items", base_params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=tvshows"
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=tvshows"
add_menu_directory_item(view_name + string_load(30405), url)
# Favorite TV Shows
@@ -621,7 +604,7 @@ def display_tvshow_type(menu_params, view):
params.update(base_params)
params["Filters"] = "IsFavorite"
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=tvshows"
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=tvshows"
add_menu_directory_item(view_name + string_load(30414), url)
# Tv Shows with unplayed
@@ -629,7 +612,7 @@ def display_tvshow_type(menu_params, view):
params.update(base_params)
params["IsPlayed"] = False
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=tvshows"
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=tvshows"
add_menu_directory_item(view_name + string_load(30285), url)
# In progress episodes
@@ -641,8 +624,8 @@ def display_tvshow_type(menu_params, view):
params["Filters"] = "IsResumable"
params["IncludeItemTypes"] = "Episode"
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=Episodes&sort=none"
url += "&name_format=" + urllib.quote('Episode|episode_name_format')
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=Episodes&sort=none"
url += "&name_format=" + quote('Episode|episode_name_format')
add_menu_directory_item(view_name + string_load(30267) + " (" + show_x_filtered_items + ")", url)
# Latest Episodes
@@ -653,7 +636,7 @@ def display_tvshow_type(menu_params, view):
params["SortOrder"] = "Descending"
params["IncludeItemTypes"] = "Episode"
path = get_jellyfin_url("{server}/Users/{userid}/Items/Latest", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=tvshows&sort=none"
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=tvshows&sort=none"
add_menu_directory_item(view_name + string_load(30288) + " (" + show_x_filtered_items + ")", url)
# Recently Added
@@ -665,8 +648,8 @@ def display_tvshow_type(menu_params, view):
params["Filters"] = "IsNotFolder"
params["IncludeItemTypes"] = "Episode"
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=Episodes&sort=none"
url += "&name_format=" + urllib.quote('Episode|episode_name_format')
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=Episodes&sort=none"
url += "&name_format=" + quote('Episode|episode_name_format')
add_menu_directory_item(view_name + string_load(30268) + " (" + show_x_filtered_items + ")", url)
# Next Up Episodes
@@ -679,8 +662,8 @@ def display_tvshow_type(menu_params, view):
params["Filters"] = "IsNotFolder"
params["IncludeItemTypes"] = "Episode"
path = get_jellyfin_url("{server}/Shows/NextUp", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=Episodes&sort=none"
url += "&name_format=" + urllib.quote('Episode|episode_name_format')
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=Episodes&sort=none"
url += "&name_format=" + quote('Episode|episode_name_format')
add_menu_directory_item(view_name + string_load(30278) + " (" + show_x_filtered_items + ")", url)
# TV Show Genres
@@ -712,7 +695,7 @@ def display_music_type(menu_params, view):
params["ImageTypeLimit"] = 1
params["IncludeItemTypes"] = "MusicAlbum"
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=MusicAlbums"
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=MusicAlbums"
add_menu_directory_item(view_name + string_load(30320), url)
# recently added
@@ -722,7 +705,7 @@ def display_music_type(menu_params, view):
params["IncludeItemTypes"] = "Audio"
params["Limit"] = "{ItemLimit}"
path = get_jellyfin_url("{server}/Users/{userid}/Items/Latest", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=MusicAlbums"
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=MusicAlbums"
add_menu_directory_item(view_name + string_load(30268) + " (" + show_x_filtered_items + ")", url)
# recently played
@@ -736,7 +719,7 @@ def display_music_type(menu_params, view):
params["SortBy"] = "DatePlayed"
params["SortOrder"] = "Descending"
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=MusicAlbum"
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=MusicAlbum"
add_menu_directory_item(view_name + string_load(30349) + " (" + show_x_filtered_items + ")", url)
# most played
@@ -750,7 +733,7 @@ def display_music_type(menu_params, view):
params["SortBy"] = "PlayCount"
params["SortOrder"] = "Descending"
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=MusicAlbum"
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=MusicAlbum"
add_menu_directory_item(view_name + string_load(30353) + " (" + show_x_filtered_items + ")", url)
# artists
@@ -759,7 +742,7 @@ def display_music_type(menu_params, view):
params["Recursive"] = True
params["ImageTypeLimit"] = 1
path = get_jellyfin_url("{server}/Artists/AlbumArtists", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=MusicArtists"
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=MusicArtists"
add_menu_directory_item(view_name + string_load(30321), url)
xbmcplugin.endOfDirectory(handle)
@@ -779,7 +762,7 @@ def display_musicvideos_type(params, view):
params["IsMissing"] = False
params["Fields"] = "{field_filters}"
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=musicvideos"
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=musicvideos"
add_menu_directory_item(view_name + string_load(30405), url)
xbmcplugin.endOfDirectory(handle)
@@ -798,7 +781,7 @@ def display_livetv_type(menu_params, view):
params["ImageTypeLimit"] = 1
params["Fields"] = "{field_filters}"
path = get_jellyfin_url("{server}/LiveTv/Channels", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=livetv"
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=livetv"
add_menu_directory_item(view_name + string_load(30360), url)
# programs
@@ -809,7 +792,7 @@ def display_livetv_type(menu_params, view):
params["Fields"] = "ChannelInfo,{field_filters}"
params["EnableTotalRecordCount"] = False
path = get_jellyfin_url("{server}/LiveTv/Programs/Recommended", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=livetv"
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=livetv"
add_menu_directory_item(view_name + string_load(30361), url)
# recordings
@@ -820,7 +803,7 @@ def display_livetv_type(menu_params, view):
params["Fields"] = "{field_filters}"
params["EnableTotalRecordCount"] = False
path = get_jellyfin_url("{server}/LiveTv/Recordings", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=livetv"
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=livetv"
add_menu_directory_item(view_name + string_load(30362), url)
xbmcplugin.endOfDirectory(handle)
@@ -852,8 +835,8 @@ def display_movies_type(menu_params, view):
# All Movies
path = get_jellyfin_url("{server}/Users/{userid}/Items", base_params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=movies"
add_menu_directory_item(view_name + string_load(30405), url)
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=movies"
add_menu_directory_item('{}{}'.format(view_name, string_load(30405)), url)
# Favorite Movies
params = {}
@@ -862,8 +845,8 @@ def display_movies_type(menu_params, view):
params["GroupItemsIntoCollections"] = False
params["Filters"] = "IsFavorite"
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=movies"
add_menu_directory_item(view_name + string_load(30414), url)
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=movies"
add_menu_directory_item('{}{}'.format(view_name, string_load(30414)), url)
# Unwatched Movies
params = {}
@@ -872,8 +855,8 @@ def display_movies_type(menu_params, view):
params["GroupItemsIntoCollections"] = False
params["IsPlayed"] = False
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=movies"
add_menu_directory_item(view_name + string_load(30285), url)
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=movies"
add_menu_directory_item('{}{}'.format(view_name, string_load(30285)), url)
# Recently Watched Movies
params = {}
@@ -885,8 +868,8 @@ def display_movies_type(menu_params, view):
params["GroupItemsIntoCollections"] = False
params["Limit"] = "{ItemLimit}"
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=movies&sort=none"
add_menu_directory_item(view_name + string_load(30349) + " (" + show_x_filtered_items + ")", url)
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=movies&sort=none"
add_menu_directory_item('{}{} ({})'.format(view_name, string_load(30349), show_x_filtered_items), url)
# Resumable Movies
params = {}
@@ -896,8 +879,8 @@ def display_movies_type(menu_params, view):
params["SortOrder"] = "Descending"
params["Limit"] = "{ItemLimit}"
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=movies&sort=none"
add_menu_directory_item(view_name + string_load(30267) + " (" + show_x_filtered_items + ")", url)
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=movies&sort=none"
add_menu_directory_item('{}{} ({})'.format(view_name, string_load(30267), show_x_filtered_items), url)
# Recently Added Movies
params = {}
@@ -908,8 +891,8 @@ def display_movies_type(menu_params, view):
params["SortOrder"] = "Descending"
params["Filters"] = "IsNotFolder"
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=movies&sort=none"
add_menu_directory_item(view_name + string_load(30268) + " (" + show_x_filtered_items + ")", url)
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=movies&sort=none"
add_menu_directory_item('{}{} ({})'.format(view_name, string_load(30268), show_x_filtered_items), url)
# Collections
params = {}
@@ -920,50 +903,50 @@ def display_movies_type(menu_params, view):
params["IncludeItemTypes"] = "Boxset"
params["Recursive"] = True
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=boxsets"
add_menu_directory_item(view_name + string_load(30410), url)
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=boxsets"
add_menu_directory_item('{}{}'.format(view_name, string_load(30410)), url)
# Favorite Collections
params["Filters"] = "IsFavorite"
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=boxsets"
add_menu_directory_item(view_name + string_load(30415), url)
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=boxsets"
add_menu_directory_item('{}{}'.format(view_name, string_load(30415)), url)
# Genres
path = "plugin://plugin.video.jellycon/?mode=GENRES&item_type=movie"
if view is not None:
path += "&parent_id=" + view.get("Id")
add_menu_directory_item(view_name + string_load(30325), path)
add_menu_directory_item('{}{}'.format(view_name, string_load(30325)), path)
# Pages
path = "plugin://plugin.video.jellycon/?mode=MOVIE_PAGES"
if view is not None:
path += "&parent_id=" + view.get("Id")
add_menu_directory_item(view_name + string_load(30397), path)
add_menu_directory_item('{}{}'.format(view_name, string_load(30397)), path)
# Alpha Picker
path = "plugin://plugin.video.jellycon/?mode=MOVIE_ALPHA"
if view is not None:
path += "&parent_id=" + view.get("Id")
add_menu_directory_item(view_name + string_load(30404), path)
add_menu_directory_item('{}{}'.format(view_name, string_load(30404)), path)
# Years
path = "plugin://plugin.video.jellycon/?mode=SHOW_ADDON_MENU&type=show_movie_years"
if view is not None:
path += "&parent_id=" + view.get("Id")
add_menu_directory_item(view_name + string_load(30411), path)
add_menu_directory_item('{}{}'.format(view_name, string_load(30411)), path)
# Decades
path = "plugin://plugin.video.jellycon/?mode=SHOW_ADDON_MENU&type=show_movie_years&group=true"
if view is not None:
path += "&parent_id=" + view.get("Id")
add_menu_directory_item(view_name + string_load(30412), path)
add_menu_directory_item('{}{}'.format(view_name, string_load(30412)), path)
# Tags
path = "plugin://plugin.video.jellycon/?mode=SHOW_ADDON_MENU&type=show_movie_tags"
if view is not None:
path += "&parent_id=" + view.get("Id")
add_menu_directory_item(view_name + string_load(30413), path)
add_menu_directory_item('{}{}'.format(view_name, string_load(30413)), path)
xbmcplugin.endOfDirectory(handle)
@@ -1014,7 +997,7 @@ def get_playlist_path(view_info):
params["ImageTypeLimit"] = 1
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=playlists"
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=playlists"
return url
@@ -1030,7 +1013,7 @@ def get_collection_path(view_info):
params["IsMissing"] = False
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=boxsets"
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=boxsets"
return url
@@ -1042,7 +1025,7 @@ def get_channel_path(view):
params["Fields"] = "{field_filters}"
path = get_jellyfin_url("{server}/Users/{userid}/Items", params)
url = sys.argv[0] + "?url=" + urllib.quote(path) + "&mode=GET_CONTENT&media_type=files"
url = sys.argv[0] + "?url=" + quote(path) + "&mode=GET_CONTENT&media_type=files"
return url

View File

@@ -1,3 +1,5 @@
from __future__ import division, absolute_import, print_function, unicode_literals
import xbmc
import xbmcaddon
import xbmcgui
@@ -21,11 +23,6 @@ class PictureViewer(xbmcgui.WindowXMLDialog):
picture_control = self.getControl(3010)
picture_control.setImage(self.picture_url)
# self.listControl.addItems(self.action_items)
# self.setFocus(self.listControl)
# bg_image = self.getControl(3010)
# bg_image.setHeight(50 * len(self.action_items) + 20)
def onFocus(self, controlId):
pass

View File

@@ -1,4 +1,5 @@
# Gnu General Public License - see LICENSE.TXT
from __future__ import division, absolute_import, print_function, unicode_literals
import xbmc
import xbmcgui
@@ -8,6 +9,7 @@ from datetime import timedelta
import json
import os
import base64
from six.moves.urllib.parse import urlparse
from .loghandler import LazyLogger
from .downloadutils import DownloadUtils
@@ -18,7 +20,6 @@ from .translation import string_load
from .datamanager import DataManager, clear_old_cache_data
from .item_functions import extract_item_info, add_gui_item
from .clientinfo import ClientInformation
from .functions import delete
from .cache_images import CacheArtwork
from .picture_viewer import PictureViewer
from .tracking import timer
@@ -28,13 +29,16 @@ log = LazyLogger(__name__)
download_utils = DownloadUtils()
def play_all_files(items, monitor, play_items=True):
def play_all_files(items, play_items=True):
home_window = HomeWindow()
log.debug("playAllFiles called with items: {0}", items)
server = download_utils.get_server()
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
playlist.clear()
playlist_data = {}
for item in items:
item_id = item.get("Id")
@@ -82,14 +86,17 @@ def play_all_files(items, monitor, play_items=True):
list_item = xbmcgui.ListItem(label=item_title)
# add playurl and data to the monitor
data = {}
data["item_id"] = item_id
data["source_id"] = source_id
data["playback_type"] = playback_type_string
data["play_session_id"] = play_session_id
data["play_action_type"] = "play_all"
monitor.played_information[playurl] = data
log.debug("Add to played_information: {0}".format(monitor.played_information))
playlist_data[playurl] = {}
playlist_data[playurl]["item_id"] = item_id
playlist_data[playurl]["source_id"] = source_id
playlist_data[playurl]["playback_type"] = playback_type_string
playlist_data[playurl]["play_session_id"] = play_session_id
playlist_data[playurl]["play_action_type"] = "play_all"
home_window.set_property('playlist', json.dumps(playlist_data))
# Set now_playing to the first track
if len(playlist_data) == 1:
home_window.set_property('now_playing', json.dumps(playlist_data[playurl]))
list_item.setPath(playurl)
list_item = set_list_item_props(item_id, list_item, item, server, listitem_props, item_title)
@@ -103,7 +110,7 @@ def play_all_files(items, monitor, play_items=True):
return playlist
def play_list_of_items(id_list, monitor):
def play_list_of_items(id_list):
log.debug("Loading all items in the list")
data_manager = DataManager()
items = []
@@ -117,10 +124,10 @@ def play_list_of_items(id_list, monitor):
return
items.append(result)
return play_all_files(items, monitor)
return play_all_files(items)
def add_to_playlist(play_info, monitor):
def add_to_playlist(play_info):
log.debug("Adding item to playlist : {0}".format(play_info))
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
@@ -152,7 +159,6 @@ def add_to_playlist(play_info, monitor):
play_session_id = playback_info.get("PlaySessionId")
# select the media source to use
# sources = item.get("MediaSources")
sources = playback_info.get('MediaSources')
selected_media_source = sources[0]
@@ -187,8 +193,6 @@ def add_to_playlist(play_info, monitor):
data["playback_type"] = playback_type_string
data["play_session_id"] = play_session_id
data["play_action_type"] = "play_all"
monitor.played_information[playurl] = data
log.debug("Add to played_information: {0}".format(monitor.played_information))
list_item.setPath(playurl)
list_item = set_list_item_props(item_id, list_item, item, server, listitem_props, item_title)
@@ -215,7 +219,7 @@ def get_playback_intros(item_id):
@timer
def play_file(play_info, monitor):
def play_file(play_info):
item_id = play_info.get("item_id")
home_window = HomeWindow()
@@ -225,12 +229,12 @@ def play_file(play_info, monitor):
action = play_info.get("action", "play")
if action == "add_to_playlist":
add_to_playlist(play_info, monitor)
add_to_playlist(play_info)
return
# if this is a list of items them add them all to the play list
if isinstance(item_id, list):
return play_list_of_items(item_id, monitor)
return play_list_of_items(item_id)
auto_resume = play_info.get("auto_resume", "-1")
force_transcode = play_info.get("force_transcode", False)
@@ -272,7 +276,7 @@ def play_file(play_info, monitor):
items = result["Items"]
if items is None:
items = []
return play_all_files(items, monitor)
return play_all_files(items)
# if this is a program from live tv epg then play the actual channel
if result.get("Type") == "Program":
@@ -368,22 +372,6 @@ def play_file(play_info, monitor):
del resume_dialog
log.debug("Resume Dialog Result: {0}".format(resume_result))
# check system settings for play action
# if prompt is set ask to set it to auto resume
# remove for now as the context dialog is now handeled in the monitor thread
# params = {"setting": "myvideos.selectaction"}
# setting_result = json_rpc('Settings.getSettingValue').execute(params)
# log.debug("Current Setting (myvideos.selectaction): {0}", setting_result)
# current_value = setting_result.get("result", None)
# if current_value is not None:
# current_value = current_value.get("value", -1)
# if current_value not in (2,3):
# return_value = xbmcgui.Dialog().yesno(string_load(30276), string_load(30277))
# if return_value:
# params = {"setting": "myvideos.selectaction", "value": 2}
# json_rpc_result = json_rpc('Settings.setSettingValue').execute(params)
# log.debug("Save Setting (myvideos.selectaction): {0}", json_rpc_result)
if resume_result == 1:
seek_time = 0
elif resume_result == -1:
@@ -445,8 +433,13 @@ def play_file(play_info, monitor):
data["play_action_type"] = "play"
data["item_type"] = result.get("Type", None)
data["can_delete"] = result.get("CanDelete", False)
monitor.played_information[playurl] = data
log.debug("Add to played_information: {0}".format(monitor.played_information))
# Check for next episodes
if result.get('Type') == 'Episode':
next_episode = get_next_episode(result)
data["next_episode"] = next_episode
home_window.set_property('now_playing', json.dumps(data))
list_item.setPath(playurl)
list_item = set_list_item_props(item_id, list_item, result, server, listitem_props, item_title)
@@ -458,7 +451,7 @@ def play_file(play_info, monitor):
intro_items = get_playback_intros(item_id)
if len(intro_items) > 0:
playlist = play_all_files(intro_items, monitor, play_items=False)
playlist = play_all_files(intro_items, play_items=False)
playlist.add(playurl, list_item)
else:
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
@@ -513,10 +506,6 @@ def play_file(play_info, monitor):
else:
log.info("PlaybackResumrAction : Playback resumed")
next_episode = get_next_episode(result)
data["next_episode"] = next_episode
send_next_episode_details(result, next_episode)
def __build_label2_from(source):
videos = [item for item in source.get('MediaStreams', {}) if item.get('Type') == "Video"]
@@ -524,7 +513,6 @@ def __build_label2_from(source):
subtitles = [item for item in source.get('MediaStreams', {}) if item.get('Type') == "Subtitle"]
details = [str(convert_size(source.get('Size', 0)))]
# details.append(source.get('Container', ''))
for video in videos:
details.append('{} {} {}bit'.format(video.get('DisplayTitle', ''),
video.get('VideoRange', ''),
@@ -545,18 +533,18 @@ def __build_label2_from(source):
def get_next_episode(item):
if item.get("Type", "na") != "Episode":
if item.get("Type") != "Episode":
log.debug("Not an episode, can not get next")
return None
parent_id = item.get("ParentId", "na")
item_index = item.get("IndexNumber", -1)
parent_id = item.get("ParentId")
item_index = item.get("IndexNumber")
if parent_id == "na":
if parent_id is None:
log.debug("No parent id, can not get next")
return None
if item_index == -1:
if item_index is None:
log.debug("No episode number, can not get next")
return None
@@ -577,12 +565,11 @@ def get_next_episode(item):
log.debug("get_next_episode no results")
return None
item_list = items_result.get("Items", [])
item_list = items_result.get("Items") or []
for item in item_list:
index = item.get("IndexNumber", -1)
# find the very next episode in the season
if index == item_index + 1:
if item.get("IndexNumber") == item_index + 1:
log.debug("get_next_episode, found next episode: {0}".format(item))
return item
@@ -650,16 +637,13 @@ def send_next_episode_details(item, next_episode):
"force_transcode": False
}
}
send_event_notification("upnext_data", next_info)
send_event_notification("upnext_data", next_info, True)
def set_list_item_props(item_id, list_item, result, server, extra_props, title):
# set up item and item info
art = get_art(result, server=server)
list_item.setIconImage(art['thumb']) # back compat
list_item.setProperty('fanart_image', art['fanart']) # back compat
list_item.setProperty('discart', art['discart']) # not avail to setArt
list_item.setArt(art)
list_item.setProperty('IsPlayable', 'false')
@@ -853,12 +837,15 @@ def external_subs(media_source, list_item, item_id):
source_id = media_source['Id']
server = download_utils.get_server()
token = download_utils.authenticate()
language = stream.get('Language', '')
codec = stream.get('Codec', '')
if stream.get('DeliveryUrl', '').lower().startswith('/videos'):
url = "%s%s" % (server, stream.get('DeliveryUrl'))
url_root = '{}/Videos/{}/{}/Subtitles/{}'.format(server, item_id, source_id, index)
if language:
url = '{}/0/Stream.{}.{}?api_key={}'.format(
url_root, language, codec, token)
else:
url = ("%s/Videos/%s/%s/Subtitles/%s/Stream.%s?api_key=%s"
% (server, item_id, source_id, index, stream['Codec'], token))
url = '{}/0/Stream.{}?api_key={}'.format(url_root, codec, token)
default = ""
if stream['IsDefault']:
@@ -867,7 +854,7 @@ def external_subs(media_source, list_item, item_id):
if stream['IsForced']:
forced = "forced"
sub_name = stream.get('Language', "n/a") + " (" + stream.get('Codec', "n/a") + ") " + default + " " + forced
sub_name = '{} ( {} ) {} {}'.format(language, codec, default, forced)
sub_names.append(sub_name)
externalsubs.append(url)
@@ -888,8 +875,9 @@ def external_subs(media_source, list_item, item_id):
list_item.setSubtitles([selected_sub])
def send_progress(monitor):
play_data = get_playing_data(monitor.played_information)
def send_progress():
home_window = HomeWindow()
play_data = get_playing_data()
if play_data is None:
return
@@ -897,15 +885,18 @@ def send_progress(monitor):
log.debug("Sending Progress Update")
player = xbmc.Player()
item_id = play_data.get("item_id")
if item_id is None:
return
play_time = player.getTime()
total_play_time = player.getTotalTime()
play_data["currentPossition"] = play_time
play_data["current_position"] = play_time
play_data["duration"] = total_play_time
play_data["currently_playing"] = True
item_id = play_data.get("item_id")
if item_id is None:
return
home_window.set_property('now_playing', json.dumps(play_data))
source_id = play_data.get("source_id")
@@ -958,7 +949,7 @@ def prompt_for_stop_actions(item_id, data):
log.debug("prompt_for_stop_actions Called : {0}".format(data))
settings = xbmcaddon.Addon()
current_position = data.get("currentPossition", 0)
current_position = data.get("current_position", 0)
duration = data.get("duration", 0)
# media_source_id = data.get("source_id")
next_episode = data.get("next_episode")
@@ -984,7 +975,6 @@ def prompt_for_stop_actions(item_id, data):
return
# item percentage complete
# percenatge_complete = int(((current_position * 10000000) / runtime) * 100)
percenatge_complete = int((current_position / duration) * 100)
log.debug("Episode Percentage Complete: {0}".format(percenatge_complete))
@@ -1000,21 +990,14 @@ def prompt_for_stop_actions(item_id, data):
percenatge_complete > prompt_delete_movie_percentage):
prompt_to_delete = True
if prompt_to_delete:
log.debug("Prompting for delete")
delete(item_id)
# prompt for next episode
if (next_episode is not None and
prompt_next_percentage < 100 and
item_type == "Episode" and
percenatge_complete > prompt_next_percentage):
# resp = True
index = next_episode.get("IndexNumber", -1)
if play_prompt:
# series_name = next_episode.get("SeriesName")
# next_epp_name = "Episode %02d - (%s)" % (index, next_episode.get("Name", "n/a"))
plugin_path = settings.getAddonInfo('path')
plugin_path_real = xbmc.translatePath(os.path.join(plugin_path))
@@ -1026,44 +1009,32 @@ def prompt_for_stop_actions(item_id, data):
if not play_next_dialog.get_play_called():
xbmc.executebuiltin("Container.Refresh")
# resp = xbmcgui.Dialog().yesno(string_load(30283),
# series_name,
# next_epp_name,
# autoclose=20000)
"""
if resp:
next_item_id = next_episode.get("Id")
log.debug("Playing Next Episode: {0}", next_item_id)
play_info = {}
play_info["item_id"] = next_item_id
play_info["auto_resume"] = "-1"
play_info["force_transcode"] = False
send_event_notification("jellycon_play_action", play_info)
def stop_all_playback():
else:
xbmc.executebuiltin("Container.Refresh")
"""
home_window = HomeWindow()
played_information_string = home_window.get_property('played_information')
if played_information_string:
played_information = json.loads(played_information_string)
else:
played_information = {}
def stop_all_playback(played_information):
log.debug("stop_all_playback : {0}".format(played_information))
if len(played_information) == 0:
return
log.debug("played_information: {0}".format(played_information))
clear_entries = []
home_screen = HomeWindow()
home_screen.clear_property("currently_playing_id")
home_window.clear_property("currently_playing_id")
for item_url in played_information:
data = played_information.get(item_url)
for item in played_information:
data = played_information.get(item)
if data.get("currently_playing", False) is True:
log.debug("item_url: {0}".format(item_url))
log.debug("item_data: {0}".format(data))
current_position = data.get("currentPossition", 0)
current_position = data.get("current_position", 0)
duration = data.get("duration", 0)
jellyfin_item_id = data.get("item_id")
jellyfin_source_id = data.get("source_id")
@@ -1086,45 +1057,81 @@ def stop_all_playback(played_information):
if data.get("play_action_type", "") == "play":
prompt_for_stop_actions(jellyfin_item_id, data)
device_id = ClientInformation().get_device_id()
url = "{server}/Videos/ActiveEncodings?DeviceId=%s" % device_id
download_utils.download_url(url, method="DELETE")
clear_entries.append(item)
if data.get('playback_type') == 'Transcode':
device_id = ClientInformation().get_device_id()
url = "{server}/Videos/ActiveEncodings?DeviceId=%s" % device_id
download_utils.download_url(url, method="DELETE")
for entry in clear_entries:
del played_information[entry]
home_window.set_property('played_information', json.dumps(played_information))
def get_playing_data(play_data_map):
def get_playing_data():
player = xbmc.Player()
home_window = HomeWindow()
play_data_string = home_window.get_property('now_playing')
play_data = json.loads(play_data_string)
played_information_string = home_window.get_property('played_information')
if played_information_string:
played_information = json.loads(played_information_string)
else:
played_information = {}
playlist_data_string = home_window.get_property('playlist')
if playlist_data_string:
playlist_data = json.loads(playlist_data_string)
else:
playlist_data = {}
item_id = play_data.get("item_id")
settings = xbmcaddon.Addon()
server = settings.getSetting('server_address')
try:
playing_file = xbmc.Player().getPlayingFile()
playing_file = player.getPlayingFile()
except Exception as e:
log.error("get_playing_data : getPlayingFile() : {0}".format(e))
return None
log.debug("get_playing_data : getPlayingFile() : {0}".format(playing_file))
if playing_file not in play_data_map:
infolabel_path_and_file = xbmc.getInfoLabel("Player.Filenameandpath")
log.debug("get_playing_data : Filenameandpath : {0}".format(infolabel_path_and_file))
if infolabel_path_and_file not in play_data_map:
log.debug("get_playing_data : play data not found")
return None
else:
playing_file = infolabel_path_and_file
if server in playing_file and item_id is not None:
play_time = player.getTime()
total_play_time = player.getTotalTime()
return play_data_map.get(playing_file)
if item_id is not None and item_id not in playing_file and playing_file in playlist_data:
# if the current now_playing data isn't correct, pull it from the playlist_data
play_data = playlist_data.pop(playing_file)
# Update now_playing data
home_window.set_property('playlist', json.dumps(playlist_data))
play_data["current_position"] = play_time
play_data["duration"] = total_play_time
played_information[item_id] = play_data
home_window.set_property('now_playing', json.dumps(play_data))
home_window.set_property('played_information', json.dumps(played_information))
return play_data
return {}
class Service(xbmc.Player):
def __init__(self, *args):
log.debug("Starting monitor service: {0}".format(args))
self.played_information = {}
def onPlayBackStarted(self):
# Will be called when xbmc starts playing a file
stop_all_playback(self.played_information)
stop_all_playback()
if not xbmc.Player().isPlaying():
log.debug("onPlayBackStarted: not playing file!")
return
play_data = get_playing_data(self.played_information)
play_data = get_playing_data()
if play_data is None:
return
@@ -1141,6 +1148,12 @@ class Service(xbmc.Player):
if jellyfin_item_id is None:
return
home_window = HomeWindow()
played_information_string = home_window.get_property('played_information')
played_information = json.loads(played_information_string)
played_information[jellyfin_item_id] = play_data
home_window.set_property('played_information', json.dumps(played_information))
log.debug("Sending Playback Started")
postdata = {
'QueueableMediaTypes': "Video",
@@ -1162,37 +1175,37 @@ class Service(xbmc.Player):
def onPlayBackEnded(self):
# Will be called when kodi stops playing a file
log.debug("onPlayBackEnded")
stop_all_playback(self.played_information)
stop_all_playback()
def onPlayBackStopped(self):
# Will be called when user stops kodi playing a file
log.debug("onPlayBackStopped")
stop_all_playback(self.played_information)
stop_all_playback()
def onPlayBackPaused(self):
# Will be called when kodi pauses the video
log.debug("onPlayBackPaused")
play_data = get_playing_data(self.played_information)
play_data = get_playing_data()
if play_data is not None:
play_data['paused'] = True
send_progress(self)
send_progress()
def onPlayBackResumed(self):
# Will be called when kodi resumes the video
log.debug("onPlayBackResumed")
play_data = get_playing_data(self.played_information)
play_data = get_playing_data()
if play_data is not None:
play_data['paused'] = False
send_progress(self)
send_progress()
def onPlayBackSeek(self, time, seek_offset):
# Will be called when kodi seeks in video
log.debug("onPlayBackSeek")
send_progress(self)
send_progress()
class PlaybackService(xbmc.Monitor):
@@ -1202,17 +1215,18 @@ class PlaybackService(xbmc.Monitor):
self.monitor = monitor
def onNotification(self, sender, method, data):
log.debug("PlaybackService:onNotification:{0}:{1}:{2}".format(sender, method, data))
if method == 'GUI.OnScreensaverActivated':
self.screensaver_activated()
return
if method == 'GUI.OnScreensaverDeactivated':
elif method == 'GUI.OnScreensaverDeactivated':
self.screensaver_deactivated()
return
elif method == 'System.OnQuit':
home_window = HomeWindow()
home_window.set_property('exit', 'True')
return
if sender[-7:] != '.SIGNAL':
if sender != 'plugin.video.jellycon':
return
signal = method.split('.', 1)[-1]
@@ -1220,16 +1234,12 @@ class PlaybackService(xbmc.Monitor):
return
data_json = json.loads(data)
message_data = data_json[0]
log.debug("PlaybackService:onNotification:{0}".format(message_data))
decoded_data = base64.b64decode(message_data)
play_info = json.loads(decoded_data)
play_info = data_json[0]
log.debug("PlaybackService:onNotification:{0}".format(play_info))
if signal == "jellycon_play_action":
log.info("Received jellycon_play_action : {0}".format(play_info))
play_file(play_info, self.monitor)
play_file(play_info)
elif signal == "jellycon_play_youtube_trailer_action":
log.info("Received jellycon_play_trailer_action : {0}".format(play_info))
trailer_link = play_info["url"]
xbmc.executebuiltin(trailer_link)
elif signal == "set_view":
@@ -1250,13 +1260,11 @@ class PlaybackService(xbmc.Monitor):
player = xbmc.Player()
if player.isPlayingVideo():
log.debug("Screen Saver Activated : isPlayingVideo() = true")
play_data = get_playing_data(self.monitor.played_information)
play_data = get_playing_data()
if play_data:
log.debug("Screen Saver Activated : this is an JellyCon item so stop it")
player.stop()
# xbmc.executebuiltin("Dialog.Close(selectdialog, true)")
clear_old_cache_data()
cache_images = settings.getSetting('cacheImagesOnScreenSaver') == 'true'

View File

@@ -1,3 +1,6 @@
from __future__ import division, absolute_import, print_function, unicode_literals
import json
import os
import threading
@@ -7,6 +10,7 @@ import xbmcaddon
from .loghandler import LazyLogger
from .play_utils import send_event_notification
from .kodi_utils import HomeWindow
log = LazyLogger(__name__)
@@ -30,6 +34,8 @@ class PlayNextService(threading.Thread):
play_next_triggered = False
is_playing = False
now_playing = None
while not xbmc.Monitor().abortRequested() and not self.stop_thread:
player = xbmc.Player()
@@ -40,6 +46,13 @@ class PlayNextService(threading.Thread):
play_next_trigger_time = int(settings.getSetting('play_next_trigger_time'))
log.debug("New play_next_trigger_time value: {0}".format(play_next_trigger_time))
now_playing_file = player.getPlayingFile()
if now_playing_file != now_playing:
# If the playing file has changed, reset the play next values
play_next_dialog = None
play_next_triggered = False
now_playing = now_playing_file
duration = player.getTotalTime()
position = player.getTime()
trigger_time = play_next_trigger_time # 300
@@ -49,7 +62,7 @@ class PlayNextService(threading.Thread):
play_next_triggered = True
log.debug("play_next_triggered hit at {0} seconds from end".format(time_to_end))
play_data = get_playing_data(self.monitor.played_information)
play_data = get_playing_data()
log.debug("play_next_triggered play_data : {0}".format(play_data))
next_episode = play_data.get("next_episode")
@@ -76,6 +89,7 @@ class PlayNextService(threading.Thread):
play_next_dialog = None
is_playing = False
now_playing = None
if xbmc.Monitor().waitForAbort(1):
break

View File

@@ -1,4 +1,5 @@
# Gnu General Public License - see LICENSE.TXT
from __future__ import division, absolute_import, print_function, unicode_literals
import xbmcgui

View File

@@ -1,4 +1,5 @@
# Gnu General Public License - see LICENSE.TXT
from __future__ import division, absolute_import, print_function, unicode_literals
import xbmc
import xbmcgui

View File

@@ -1,8 +1,9 @@
# Gnu General Public License - see LICENSE.TXT
from __future__ import division, absolute_import, print_function, unicode_literals
import socket
import json
from urlparse import urlparse
from six.moves.urllib.parse import urlparse
import requests
import ssl
import time
@@ -12,6 +13,8 @@ from datetime import datetime
import xbmcaddon
import xbmcgui
import xbmc
from six import ensure_binary
from kodi_six.utils import py2_decode
from .kodi_utils import HomeWindow
from .downloadutils import DownloadUtils, save_user_details, load_user_details
@@ -98,9 +101,8 @@ def check_safe_delete_available():
log.debug("check_safe_delete_available")
du = DownloadUtils()
json_data = du.download_url("{server}/Plugins")
result = json.loads(json_data)
if result is not None:
result = du.download_url("{server}/Plugins")
if result:
log.debug("Server Plugin List: {0}".format(result))
safe_delete_found = False
@@ -124,23 +126,20 @@ def get_server_details():
log.debug("Getting Server Details from Network")
servers = []
message = "who is JellyfinServer?"
message = b"who is JellyfinServer?"
multi_group = ("<broadcast>", 7359)
# multi_group = ("127.0.0.1", 7359)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(4.0)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 3) # timeout
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
sock.setsockopt(socket.IPPROTO_IP, socket.SO_REUSEADDR, 1)
log.debug("MutliGroup: {0}".format(multi_group))
log.debug("Sending UDP Data: {0}".format(message))
progress = xbmcgui.DialogProgress()
progress.create(__addon_name__ + " : " + string_load(30373))
progress.create('{} : {}'.format(__addon_name__, string_load(30373)))
progress.update(0, string_load(30374))
xbmc.sleep(1000)
server_count = 0
@@ -151,7 +150,7 @@ def get_server_details():
while True:
try:
server_count += 1
progress.update(server_count * 10, string_load(30375) % server_count)
progress.update(server_count * 10, '{}: {}'.format(string_load(30375), server_count))
xbmc.sleep(1000)
data, addr = sock.recvfrom(1024)
servers.append(json.loads(data))
@@ -200,14 +199,14 @@ def check_server(force=False, change_user=False, notify=False):
server_list.append(server_item)
if len(server_list) > 0:
return_index = xbmcgui.Dialog().select(__addon_name__ + " : " + string_load(30166),
return_index = xbmcgui.Dialog().select('{} : {}'.format(__addon_name__, string_load(30166)),
server_list,
useDetails=True)
if return_index != -1:
server_url = server_info[return_index]["Address"]
if not server_url:
return_index = xbmcgui.Dialog().yesno(__addon_name__, string_load(30282), string_load(30370))
return_index = xbmcgui.Dialog().yesno(__addon_name__, '{}\n{}'.format(string_load(30282), string_load(30370)))
if not return_index:
xbmc.executebuiltin("ActivateWindow(Home)")
return
@@ -226,22 +225,21 @@ def check_server(force=False, change_user=False, notify=False):
xbmc.executebuiltin("ActivateWindow(Home)")
return
public_lookup_url = "%s/Users/Public?format=json" % (server_url)
public_lookup_url = "%s/System/Info/Public?format=json" % (server_url)
log.debug("Testing_Url: {0}".format(public_lookup_url))
progress = xbmcgui.DialogProgress()
progress.create(__addon_name__ + " : " + string_load(30376))
progress.create('{} : {}'.format(__addon_name__, string_load(30376)))
progress.update(0, string_load(30377))
json_data = du.download_url(public_lookup_url, authenticate=False)
result = du.download_url(public_lookup_url, authenticate=False)
progress.close()
result = json.loads(json_data)
if result is not None:
xbmcgui.Dialog().ok(__addon_name__ + " : " + string_load(30167),
if result:
xbmcgui.Dialog().ok('{} : {}'.format(__addon_name__, string_load(30167)),
server_url)
break
else:
return_index = xbmcgui.Dialog().yesno(__addon_name__ + " : " + string_load(30135),
return_index = xbmcgui.Dialog().yesno('{} : {}'.format(__addon_name__, string_load(30135)),
server_url,
string_load(30371))
if not return_index:
@@ -255,7 +253,7 @@ def check_server(force=False, change_user=False, notify=False):
# do we need to change the user
user_details = load_user_details(settings)
current_username = user_details.get("username", "")
current_username = unicode(current_username, "utf-8")
current_username = py2_decode(current_username)
# if asked or we have no current user then show user selection screen
if something_changed or change_user or len(current_username) == 0:
@@ -266,168 +264,158 @@ def check_server(force=False, change_user=False, notify=False):
# get a list of users
log.debug("Getting user list")
json_data = du.download_url(server_url + "/Users/Public?format=json", authenticate=False)
result = du.download_url(server_url + "/Users/Public?format=json", authenticate=False)
log.debug("jsonData: {0}".format(json_data))
try:
result = json.loads(json_data)
except:
result = None
log.debug("jsonData: {0}".format(py2_decode(result)))
if result is None:
xbmcgui.Dialog().ok(string_load(30135),
string_load(30201),
string_load(30169) + server_url)
selected_id = -1
users = []
for user in result:
config = user.get("Configuration")
if config is not None:
if config.get("IsHidden", False) is False:
name = user.get("Name")
admin = user.get("Policy", {}).get("IsAdministrator", False)
time_ago = ""
last_active = user.get("LastActivityDate")
if last_active:
last_active_date = datetime_from_string(last_active)
log.debug("LastActivityDate: {0}".format(last_active_date))
ago = datetime.now() - last_active_date
log.debug("LastActivityDate: {0}".format(ago))
days = divmod(ago.seconds, 86400)
hours = divmod(days[1], 3600)
minutes = divmod(hours[1], 60)
log.debug("LastActivityDate: {0} {1} {2}".format(days[0], hours[0], minutes[0]))
if days[0]:
time_ago += " %sd" % days[0]
if hours[0]:
time_ago += " %sh" % hours[0]
if minutes[0]:
time_ago += " %sm" % minutes[0]
time_ago = time_ago.strip()
if not time_ago:
time_ago = "Active: now"
else:
time_ago = "Active: %s ago" % time_ago
log.debug("LastActivityDate: {0}".format(time_ago))
user_item = xbmcgui.ListItem(name)
user_image = du.get_user_artwork(user, 'Primary')
if not user_image:
user_image = "DefaultUser.png"
art = {"Thumb": user_image}
user_item.setArt(art)
user_item.setLabel2("TEST")
sub_line = time_ago
if user.get("HasPassword", False) is True:
sub_line += ", Password"
user_item.setProperty("secure", "true")
m = hashlib.md5()
m.update(ensure_binary(name))
hashed_username = m.hexdigest()
saved_password = settings.getSetting("saved_user_password_" + hashed_username)
if saved_password:
sub_line += ": Saved"
else:
user_item.setProperty("secure", "false")
if admin:
sub_line += ", Admin"
else:
sub_line += ", User"
user_item.setProperty("manual", "false")
user_item.setLabel2(sub_line)
users.append(user_item)
if current_username == name:
selected_id = len(users) - 1
if current_username:
selection_title = string_load(30180) + " (" + current_username + ")"
else:
selected_id = -1
users = []
for user in result:
config = user.get("Configuration")
if config is not None:
if config.get("IsHidden", False) is False:
name = user.get("Name")
admin = user.get("Policy", {}).get("IsAdministrator", False)
selection_title = string_load(30180)
time_ago = ""
last_active = user.get("LastActivityDate")
if last_active:
last_active_date = datetime_from_string(last_active)
log.debug("LastActivityDate: {0}".format(last_active_date))
ago = datetime.now() - last_active_date
log.debug("LastActivityDate: {0}".format(ago))
days = divmod(ago.seconds, 86400)
hours = divmod(days[1], 3600)
minutes = divmod(hours[1], 60)
log.debug("LastActivityDate: {0} {1} {2}".format(days[0], hours[0], minutes[0]))
if days[0]:
time_ago += " %sd" % days[0]
if hours[0]:
time_ago += " %sh" % hours[0]
if minutes[0]:
time_ago += " %sm" % minutes[0]
time_ago = time_ago.strip()
if not time_ago:
time_ago = "Active: now"
else:
time_ago = "Active: %s ago" % time_ago
log.debug("LastActivityDate: {0}".format(time_ago))
# add manual login
user_item = xbmcgui.ListItem(string_load(30365))
art = {"Thumb": "DefaultUser.png"}
user_item.setArt(art)
user_item.setLabel2(string_load(30366))
user_item.setProperty("secure", "true")
user_item.setProperty("manual", "true")
users.append(user_item)
user_item = xbmcgui.ListItem(name)
user_image = du.get_user_artwork(user, 'Primary')
if not user_image:
user_image = "DefaultUser.png"
art = {"Thumb": user_image}
user_item.setArt(art)
user_item.setLabel2("TEST")
return_value = xbmcgui.Dialog().select(selection_title,
users,
preselect=selected_id,
autoclose=20000,
useDetails=True)
sub_line = time_ago
if return_value > -1 and return_value != selected_id:
if user.get("HasPassword", False) is True:
sub_line += ", Password"
user_item.setProperty("secure", "true")
something_changed = True
selected_user = users[return_value]
secured = selected_user.getProperty("secure") == "true"
manual = selected_user.getProperty("manual") == "true"
selected_user_name = selected_user.getLabel()
m = hashlib.md5()
m.update(name)
hashed_username = m.hexdigest()
saved_password = settings.getSetting("saved_user_password_" + hashed_username)
if saved_password:
sub_line += ": Saved"
log.debug("Selected User Name: {0} : {1}".format(return_value, selected_user_name))
else:
user_item.setProperty("secure", "false")
if manual:
kb = xbmc.Keyboard()
kb.setHeading(string_load(30005))
if current_username:
kb.setDefault(current_username)
kb.doModal()
if kb.isConfirmed():
selected_user_name = kb.getText()
log.debug("Manual entered username: {0}".format(selected_user_name))
else:
return
if admin:
sub_line += ", Admin"
else:
sub_line += ", User"
if secured:
# we need a password, check the settings first
m = hashlib.md5()
m.update(selected_user_name.encode())
hashed_username = m.hexdigest()
saved_password = settings.getSetting("saved_user_password_" + hashed_username)
allow_password_saving = settings.getSetting("allow_password_saving") == "true"
user_item.setProperty("manual", "false")
user_item.setLabel2(sub_line)
users.append(user_item)
# if not saving passwords but have a saved ask to clear it
if not allow_password_saving and saved_password:
clear_password = xbmcgui.Dialog().yesno(string_load(30368), string_load(30369))
if clear_password:
settings.setSetting("saved_user_password_" + hashed_username, "")
if current_username == name:
selected_id = len(users) - 1
if saved_password:
log.debug("Saving username and password: {0}".format(selected_user_name))
log.debug("Using stored password for user: {0}".format(hashed_username))
save_user_details(settings, selected_user_name, saved_password)
if current_username:
selection_title = string_load(30180) + " (" + current_username + ")"
else:
selection_title = string_load(30180)
# add manual login
user_item = xbmcgui.ListItem(string_load(30365))
art = {"Thumb": "DefaultUser.png"}
user_item.setArt(art)
user_item.setLabel2(string_load(30366))
user_item.setProperty("secure", "true")
user_item.setProperty("manual", "true")
users.append(user_item)
return_value = xbmcgui.Dialog().select(selection_title,
users,
preselect=selected_id,
autoclose=20000,
useDetails=True)
if return_value > -1 and return_value != selected_id:
something_changed = True
selected_user = users[return_value]
secured = selected_user.getProperty("secure") == "true"
manual = selected_user.getProperty("manual") == "true"
selected_user_name = selected_user.getLabel()
log.debug("Selected User Name: {0} : {1}".format(return_value, selected_user_name))
if manual:
else:
kb = xbmc.Keyboard()
kb.setHeading(string_load(30005))
if current_username:
kb.setDefault(current_username)
kb.setHeading(string_load(30006))
kb.setHiddenInput(True)
kb.doModal()
if kb.isConfirmed():
selected_user_name = kb.getText()
log.debug("Manual entered username: {0}".format(selected_user_name))
else:
return
if secured:
# we need a password, check the settings first
m = hashlib.md5()
m.update(selected_user_name)
hashed_username = m.hexdigest()
saved_password = settings.getSetting("saved_user_password_" + hashed_username)
allow_password_saving = settings.getSetting("allow_password_saving") == "true"
# if not saving passwords but have a saved ask to clear it
if not allow_password_saving and saved_password:
clear_password = xbmcgui.Dialog().yesno(string_load(30368), string_load(30369))
if clear_password:
settings.setSetting("saved_user_password_" + hashed_username, "")
if saved_password:
log.debug("Saving username and password: {0}".format(selected_user_name))
log.debug("Using stored password for user: {0}".format(hashed_username))
save_user_details(settings, selected_user_name, saved_password)
save_user_details(settings, selected_user_name, kb.getText())
else:
kb = xbmc.Keyboard()
kb.setHeading(string_load(30006))
kb.setHiddenInput(True)
kb.doModal()
if kb.isConfirmed():
log.debug("Saving username and password: {0}".format(selected_user_name))
save_user_details(settings, selected_user_name, kb.getText())
# should we save the password
if allow_password_saving:
save_password = xbmcgui.Dialog().yesno(string_load(30363), string_load(30364))
if save_password:
log.debug("Saving password for fast user switching: {0}".format(hashed_username))
settings.setSetting("saved_user_password_" + hashed_username, kb.getText())
else:
log.debug("Saving username with no password: {0}".format(selected_user_name))
save_user_details(settings, selected_user_name, "")
# should we save the password
if allow_password_saving:
save_password = xbmcgui.Dialog().yesno(string_load(30363), string_load(30364))
if save_password:
log.debug("Saving password for fast user switching: {0}".format(hashed_username))
settings.setSetting("saved_user_password_" + hashed_username, kb.getText())
else:
log.debug("Saving username with no password: {0}".format(selected_user_name))
save_user_details(settings, selected_user_name, "")
if something_changed:
home_window = HomeWindow()

View File

@@ -1,5 +1,5 @@
from __future__ import division, absolute_import, print_function, unicode_literals
import json
import sys
import xbmcgui
import xbmcplugin

View File

@@ -1,6 +1,8 @@
# Gnu General Public License - see LICENSE.TXT
from __future__ import division, absolute_import, print_function, unicode_literals
import os
import xml.etree.ElementTree as ET
import xbmc
import xbmcgui
@@ -27,9 +29,6 @@ def clone_default_skin():
set_skin_settings()
update_kodi_settings()
# xbmc.executebuiltin("ReloadSkin()")
# xbmc.executebuiltin("ActivateWindow(Home)")
def walk_path(root_path, relative_path, all_files):
files = xbmcvfs.listdir(root_path)
@@ -81,17 +80,13 @@ def clone_skin():
# alter skin addon.xml
addon_xml_path = os.path.join(kodi_skin_destination, "addon.xml")
with open(addon_xml_path, "r") as addon_file:
addon_xml_data = addon_file.read()
addon_tree = ET.parse(addon_xml_path)
addon_root = addon_tree.getroot()
addon_xml_data = addon_xml_data.replace("id=\"skin.estuary\"", "id=\"skin.estuary_jellycon\"")
addon_xml_data = addon_xml_data.replace("name=\"Estuary\"", "name=\"Estuary JellyCon\"")
addon_root.attrib['id'] = 'skin.estuary_jellycon'
addon_root.attrib['name'] = 'Estuary JellyCon'
# log.debug("{0}", addon_xml_data)
# update the addon.xml
with open(addon_xml_path, "w") as addon_file:
addon_file.write(addon_xml_data)
addon_tree.write(addon_xml_path)
# get jellycon path
jellycon_path = os.path.join(kodi_home_path, "addons", "plugin.video.jellycon")
@@ -104,6 +99,7 @@ def clone_skin():
"DialogSeekBar.xml",
"VideoOSD.xml"]
# Copy customized skin files from our addon into cloned skin
for file_name in file_list:
source = os.path.join(jellycon_path, "resources", "skins", "skin.estuary", ver, "xml", file_name)
destination = os.path.join(kodi_skin_destination, "xml", file_name)

View File

@@ -1,4 +1,5 @@
# Gnu General Public License - see LICENSE.TXT
from __future__ import division, absolute_import, print_function, unicode_literals
import sys
import functools

View File

@@ -1,6 +1,7 @@
# Gnu General Public License - see LICENSE.TXT
from __future__ import division, absolute_import, print_function, unicode_literals
import urllib
from six.moves.urllib.parse import quote, unquote
import encodings
import xbmc
@@ -19,11 +20,11 @@ icon = xbmc.translatePath('special://home/addons/plugin.video.jellycon/icon.png'
def not_found(content_string):
xbmcgui.Dialog().notification('JellyCon', string_load(30305) % content_string, icon=icon, sound=False)
xbmcgui.Dialog().notification('JellyCon', '{}: {}'.format(string_load(30305), content_string), icon=icon, sound=False)
def playback_starting(content_string):
xbmcgui.Dialog().notification('JellyCon', string_load(30306) % content_string, icon=icon, sound=False)
xbmcgui.Dialog().notification('JellyCon', '{}: {}'.format(string_load(30306), content_string), icon=icon, sound=False)
def search(item_type, query):
@@ -105,7 +106,7 @@ def get_episode_id(parent_id, episode):
def get_match(item_type, title, year, imdb_id):
query = urllib.quote(title)
query = quote(title)
results = search(item_type, query=query)
results = results.get('SearchHints')
@@ -137,7 +138,7 @@ def entry_point(parameters):
action = parameters.get('action', None)
video_type = parameters.get('video_type', None)
title = urllib.unquote(parameters.get('title', ''))
title = unquote(parameters.get('title', ''))
year = parameters.get('year', '')
episode = parameters.get('episode', '')
@@ -245,4 +246,4 @@ def entry_point(parameters):
not_found('{title} ({year}) - S{season}'.format(title=title, year=year, season=str_season))
if url and media_type:
xbmc.executebuiltin('ActivateWindow(Videos, plugin://plugin.video.jellycon/?mode=GET_CONTENT&url={url}&media_type={media_type})'.format(url=urllib.quote(url), media_type=media_type))
xbmc.executebuiltin('ActivateWindow(Videos, plugin://plugin.video.jellycon/?mode=GET_CONTENT&url={url}&media_type={media_type})'.format(url=quote(url), media_type=media_type))

View File

@@ -1,5 +1,8 @@
from __future__ import division, absolute_import, print_function, unicode_literals
import xbmcaddon
from .loghandler import LazyLogger
from kodi_six.utils import py2_encode
log = LazyLogger(__name__)
addon = xbmcaddon.Addon()
@@ -7,7 +10,7 @@ addon = xbmcaddon.Addon()
def string_load(string_id):
try:
return addon.getLocalizedString(string_id).encode('utf-8', 'ignore')
return py2_encode(addon.getLocalizedString(string_id))
except Exception as e:
log.error('Failed String Load: {0} ({1})', string_id, e)
return str(string_id)

View File

@@ -1,11 +1,13 @@
# Gnu General Public License - see LICENSE.TXT
from __future__ import division, absolute_import, print_function, unicode_literals
import xbmcaddon
import xbmc
import xbmcvfs
import binascii
import string
import random
import urllib
import json
import base64
import time
@@ -13,6 +15,8 @@ import math
from datetime import datetime
import calendar
import re
from six import ensure_text, ensure_binary
from six.moves.urllib.parse import urlencode
from .downloadutils import DownloadUtils
from .loghandler import LazyLogger
@@ -28,17 +32,11 @@ log = LazyLogger(__name__)
def get_jellyfin_url(base_url, params):
params["format"] = "json"
param_list = []
for key in params:
if params[key] is not None:
value = params[key]
if isinstance(value, unicode):
value = value.encode("utf8")
else:
value = str(value)
param_list.append(key + "=" + urllib.quote_plus(value, safe="{}"))
param_string = "&".join(param_list)
return base_url + "?" + param_string
url_params = urlencode(params)
# Filthy hack until I get around to reworking the network flow
# It relies on {thing} strings in downloadutils.py
url_params = url_params.replace('%7B', '{').replace('%7D', '}')
return base_url + "?" + url_params
###########################################################################
@@ -94,8 +92,8 @@ class PlayUtils:
playurl = direct_path
playback_type = "0"
# check if file can be direct streamed
if can_direct_stream and playurl is None:
# check if file can be direct streamed/played
if (can_direct_stream or can_direct_play) and playurl is None:
item_id = media_source.get('Id')
playurl = ("%s/Videos/%s/stream" +
"?static=true" +
@@ -137,7 +135,7 @@ class PlayUtils:
if playback_video_force_8:
transcode_params.update({"MaxVideoBitDepth": "8"})
transcode_path = urllib.urlencode(transcode_params)
transcode_path = urlencode(transcode_params)
playurl = "%s/Videos/%s/master.m3u8?%s" % (server, item_id, transcode_path)
@@ -218,9 +216,8 @@ def get_art(item, server):
'tvshow.landscape': ''
}
image_tags = item["ImageTags"]
if image_tags is not None and image_tags["Primary"] is not None:
# image_tag = image_tags["Primary"]
image_tags = item.get("ImageTags", {})
if image_tags and image_tags.get("Primary"):
art['thumb'] = downloadUtils.get_artwork(item, "Primary", server=server)
item_type = item["Type"]
@@ -229,7 +226,6 @@ def get_art(item, server):
art['poster'] = downloadUtils.get_artwork(item, "Primary", server=server)
elif item_type == "Episode":
art['tvshow.poster'] = downloadUtils.get_artwork(item, "Primary", parent=True, server=server)
# art['poster'] = downloadUtils.getArtwork(item, "Primary", parent=True, server=server)
art['tvshow.clearart'] = downloadUtils.get_artwork(item, "Art", parent=True, server=server)
art['clearart'] = downloadUtils.get_artwork(item, "Art", parent=True, server=server)
art['tvshow.clearlogo'] = downloadUtils.get_artwork(item, "Logo", parent=True, server=server)
@@ -288,27 +284,26 @@ def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
return ''.join(random.choice(chars) for _ in range(size))
def double_urlencode(text):
text = single_urlencode(text)
text = single_urlencode(text)
return text
def single_urlencode(text):
# urlencode needs a utf- string
text = urllib.urlencode({'blahblahblah': text.encode('utf-8')})
text = urlencode({'blahblahblah': text.encode('utf-8')})
text = text[13:]
return text.decode('utf-8') # return the result again as unicode
def send_event_notification(method, data):
message_data = json.dumps(data)
source_id = "jellycon"
base64_data = base64.b64encode(message_data)
escaped_data = '\\"[\\"{0}\\"]\\"'.format(base64_data)
command = 'XBMC.NotifyAll({0}.SIGNAL,{1},{2})'.format(source_id, method, escaped_data)
log.debug("Sending notification event data: {0}".format(command))
xbmc.executebuiltin(command)
def send_event_notification(method, data=None, hexlify=False):
'''
Send events through Kodi's notification system
'''
data = data or {}
if hexlify:
# Used exclusively for the upnext plugin
data = ensure_text(binascii.hexlify(ensure_binary(json.dumps(data))))
sender = 'plugin.video.jellycon'
data = '"[%s]"' % json.dumps(data).replace('"', '\\"')
xbmc.executebuiltin('NotifyAll(%s, %s, %s)' % (sender, method, data))
def datetime_from_string(time_string):

View File

@@ -1,946 +0,0 @@
"""
websocket - WebSocket client library for Python
Copyright (C) 2010 Hiroki Ohtani(liris)
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""
import socket
from base64 import b64encode
try:
import ssl
from ssl import SSLError
HAVE_SSL = True
except ImportError:
# dummy class of SSLError for ssl none-support environment.
class SSLError(Exception):
pass
HAVE_SSL = False
from urlparse import urlparse
import os
import array
import struct
import uuid
import hashlib
import base64
import threading
import time
import logging
import traceback
import sys
"""
websocket python client.
=========================
This version support only hybi-13.
Please see http://tools.ietf.org/html/rfc6455 for protocol.
"""
# websocket supported version.
VERSION = 13
# closing frame status codes.
STATUS_NORMAL = 1000
STATUS_GOING_AWAY = 1001
STATUS_PROTOCOL_ERROR = 1002
STATUS_UNSUPPORTED_DATA_TYPE = 1003
STATUS_STATUS_NOT_AVAILABLE = 1005
STATUS_ABNORMAL_CLOSED = 1006
STATUS_INVALID_PAYLOAD = 1007
STATUS_POLICY_VIOLATION = 1008
STATUS_MESSAGE_TOO_BIG = 1009
STATUS_INVALID_EXTENSION = 1010
STATUS_UNEXPECTED_CONDITION = 1011
STATUS_TLS_HANDSHAKE_ERROR = 1015
logger = logging.getLogger()
class WebSocketException(Exception):
"""
websocket exeception class.
"""
pass
class WebSocketConnectionClosedException(WebSocketException):
"""
If remote host closed the connection or some network error happened,
this exception will be raised.
"""
pass
class WebSocketTimeoutException(WebSocketException):
"""
WebSocketTimeoutException will be raised at socket timeout during read/write data.
"""
pass
default_timeout = None
traceEnabled = False
def enableTrace(tracable):
"""
turn on/off the tracability.
tracable: boolean value. if set True, tracability is enabled.
"""
global traceEnabled
traceEnabled = tracable
if tracable:
if not logger.handlers:
logger.addHandler(logging.StreamHandler())
logger.setLevel(logging.DEBUG)
def setdefaulttimeout(timeout):
"""
Set the global timeout setting to connect.
timeout: default socket timeout time. This value is second.
"""
global default_timeout
default_timeout = timeout
def getdefaulttimeout():
"""
Return the global timeout setting(second) to connect.
"""
return default_timeout
def _wrap_sni_socket(sock, sslopt, hostname):
context = ssl.SSLContext(sslopt.get('ssl_version', ssl.PROTOCOL_SSLv23))
if sslopt.get('cert_reqs', ssl.CERT_NONE) != ssl.CERT_NONE:
capath = ssl.get_default_verify_paths().capath
context.load_verify_locations(cafile=sslopt.get('ca_certs', None),
capath=sslopt.get('ca_cert_path', capath))
return context.wrap_socket(
sock,
do_handshake_on_connect=sslopt.get('do_handshake_on_connect', True),
suppress_ragged_eofs=sslopt.get('suppress_ragged_eofs', True),
server_hostname=hostname,
)
def _parse_url(url):
"""
parse url and the result is tuple of
(hostname, port, resource path and the flag of secure mode)
url: url string.
"""
if ":" not in url:
raise ValueError("url is invalid")
scheme, url = url.split(":", 1)
parsed = urlparse(url, scheme="http")
if parsed.hostname:
hostname = parsed.hostname
else:
raise ValueError("hostname is invalid")
port = 0
if parsed.port:
port = parsed.port
is_secure = False
if scheme == "ws":
if not port:
port = 80
elif scheme == "wss":
is_secure = True
if not port:
port = 443
else:
raise ValueError("scheme %s is invalid" % scheme)
if parsed.path:
resource = parsed.path
else:
resource = "/"
if parsed.query:
resource += "?" + parsed.query
user_name = parsed.username
user_password = parsed.password
return (hostname, port, resource, is_secure, user_name, user_password)
def create_connection(url, timeout=None, **options):
"""
connect to url and return websocket object.
Connect to url and return the WebSocket object.
Passing optional timeout parameter will set the timeout on the socket.
If no timeout is supplied, the global default timeout setting returned by getdefauttimeout() is used.
You can customize using 'options'.
If you set "header" list object, you can set your own custom header.
>>> conn = create_connection("ws://echo.websocket.org/",
... header=["User-Agent: MyProgram",
... "x-custom: header"])
timeout: socket timeout time. This value is integer.
if you set None for this value, it means "use default_timeout value"
options: current support option is only "header".
if you set header as dict value, the custom HTTP headers are added.
"""
sockopt = options.get("sockopt", [])
sslopt = options.get("sslopt", {})
websock = WebSocket(sockopt=sockopt, sslopt=sslopt)
websock.settimeout(timeout if timeout is not None else default_timeout)
websock.connect(url, **options)
return websock
_MAX_INTEGER = (1 << 32) -1
_AVAILABLE_KEY_CHARS = range(0x21, 0x2f + 1) + range(0x3a, 0x7e + 1)
_MAX_CHAR_BYTE = (1<<8) -1
# ref. Websocket gets an update, and it breaks stuff.
# http://axod.blogspot.com/2010/06/websocket-gets-update-and-it-breaks.html
def _create_sec_websocket_key():
uid = uuid.uuid4()
return base64.encodestring(uid.bytes).strip()
_HEADERS_TO_CHECK = {
"upgrade": "websocket",
"connection": "upgrade",
}
class ABNF(object):
"""
ABNF frame class.
see http://tools.ietf.org/html/rfc5234
and http://tools.ietf.org/html/rfc6455#section-5.2
"""
# operation code values.
OPCODE_CONT = 0x0
OPCODE_TEXT = 0x1
OPCODE_BINARY = 0x2
OPCODE_CLOSE = 0x8
OPCODE_PING = 0x9
OPCODE_PONG = 0xa
# available operation code value tuple
OPCODES = (OPCODE_CONT, OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE,
OPCODE_PING, OPCODE_PONG)
# opcode human readable string
OPCODE_MAP = {
OPCODE_CONT: "cont",
OPCODE_TEXT: "text",
OPCODE_BINARY: "binary",
OPCODE_CLOSE: "close",
OPCODE_PING: "ping",
OPCODE_PONG: "pong"
}
# data length threashold.
LENGTH_7 = 0x7d
LENGTH_16 = 1 << 16
LENGTH_63 = 1 << 63
def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0,
opcode=OPCODE_TEXT, mask=1, data=""):
"""
Constructor for ABNF.
please check RFC for arguments.
"""
self.fin = fin
self.rsv1 = rsv1
self.rsv2 = rsv2
self.rsv3 = rsv3
self.opcode = opcode
self.mask_value = mask
self.data = data
self.get_mask_key = os.urandom
def __str__(self):
return "fin=" + str(self.fin) \
+ " opcode=" + str(self.opcode) \
+ " data=" + str(self.data)
@staticmethod
def create_frame(data, opcode):
"""
create frame to send text, binary and other data.
data: data to send. This is string value(byte array).
if opcode is OPCODE_TEXT and this value is uniocde,
data value is conveted into unicode string, automatically.
opcode: operation code. please see OPCODE_XXX.
"""
if opcode == ABNF.OPCODE_TEXT and isinstance(data, unicode):
data = data.encode("utf-8")
# mask must be set if send data from client
return ABNF(1, 0, 0, 0, opcode, 1, data)
def format(self):
"""
format this object to string(byte array) to send data to server.
"""
if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]):
raise ValueError("not 0 or 1")
if self.opcode not in ABNF.OPCODES:
raise ValueError("Invalid OPCODE")
length = len(self.data)
if length >= ABNF.LENGTH_63:
raise ValueError("data is too long")
frame_header = chr(self.fin << 7
| self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4
| self.opcode)
if length < ABNF.LENGTH_7:
frame_header += chr(self.mask_value << 7 | length)
elif length < ABNF.LENGTH_16:
frame_header += chr(self.mask_value << 7 | 0x7e)
frame_header += struct.pack("!H", length)
else:
frame_header += chr(self.mask_value << 7 | 0x7f)
frame_header += struct.pack("!Q", length)
if not self.mask_value:
return frame_header + self.data
else:
mask_key = self.get_mask_key(4)
return frame_header + self._get_masked(mask_key)
def _get_masked(self, mask_key):
s = ABNF.mask(mask_key, self.data)
return mask_key + "".join(s)
@staticmethod
def mask(mask_key, data):
"""
mask or unmask data. Just do xor for each byte
mask_key: 4 byte string(byte).
data: data to mask/unmask.
"""
_m = array.array("B", mask_key)
_d = array.array("B", data)
for i in xrange(len(_d)):
_d[i] ^= _m[i % 4]
return _d.tostring()
class WebSocket(object):
"""
Low level WebSocket interface.
This class is based on
The WebSocket protocol draft-hixie-thewebsocketprotocol-76
http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
We can connect to the websocket server and send/recieve data.
The following example is a echo client.
>>> import websocket
>>> ws = websocket.WebSocket()
>>> ws.connect("ws://echo.websocket.org")
>>> ws.send("Hello, Server")
>>> ws.recv()
'Hello, Server'
>>> ws.close()
get_mask_key: a callable to produce new mask keys, see the set_mask_key
function's docstring for more details
sockopt: values for socket.setsockopt.
sockopt must be tuple and each element is argument of sock.setscokopt.
sslopt: dict object for ssl socket option.
"""
def __init__(self, get_mask_key=None, sockopt=None, sslopt=None):
"""
Initalize WebSocket object.
"""
if sockopt is None:
sockopt = []
if sslopt is None:
sslopt = {}
self.connected = False
self.sock = socket.socket()
for opts in sockopt:
self.sock.setsockopt(*opts)
self.sslopt = sslopt
self.get_mask_key = get_mask_key
# Buffers over the packets from the layer beneath until desired amount
# bytes of bytes are received.
self._recv_buffer = []
# These buffer over the build-up of a single frame.
self._frame_header = None
self._frame_length = None
self._frame_mask = None
self._cont_data = None
def fileno(self):
return self.sock.fileno()
def set_mask_key(self, func):
"""
set function to create musk key. You can custumize mask key generator.
Mainly, this is for testing purpose.
func: callable object. the fuct must 1 argument as integer.
The argument means length of mask key.
This func must be return string(byte array),
which length is argument specified.
"""
self.get_mask_key = func
def gettimeout(self):
"""
Get the websocket timeout(second).
"""
return self.sock.gettimeout()
def settimeout(self, timeout):
"""
Set the timeout to the websocket.
timeout: timeout time(second).
"""
self.sock.settimeout(timeout)
timeout = property(gettimeout, settimeout)
def connect(self, url, **options):
"""
Connect to url. url is websocket url scheme. ie. ws://host:port/resource
You can customize using 'options'.
If you set "header" dict object, you can set your own custom header.
>>> ws = WebSocket()
>>> ws.connect("ws://echo.websocket.org/",
... header={"User-Agent: MyProgram",
... "x-custom: header"})
timeout: socket timeout time. This value is integer.
if you set None for this value,
it means "use default_timeout value"
options: current support option is only "header".
if you set header as dict value,
the custom HTTP headers are added.
"""
hostname, port, resource, is_secure, user_name, user_password = _parse_url(url)
# TODO: we need to support proxy
self.sock.connect((hostname, port))
if is_secure:
if HAVE_SSL:
if self.sslopt is None:
sslopt = {}
else:
sslopt = self.sslopt
if ssl.HAS_SNI:
self.sock = _wrap_sni_socket(self.sock, sslopt, hostname)
else:
self.sock = ssl.wrap_socket(self.sock, **sslopt)
else:
raise WebSocketException("SSL not available.")
self._handshake(hostname, port, resource, user_name, user_password, **options)
def _handshake(self, host, port, resource, user_name, user_password, **options):
headers = []
headers.append("GET %s HTTP/1.1" % resource)
if user_name and user_password:
# add basic auth headers
userAndPass = b64encode(b"%s:%s" % (user_name, user_password)).decode("ascii")
headers.append("Authorization: Basic %s" % userAndPass)
headers.append("User-Agent: JellyConWebSocket")
headers.append("Upgrade: websocket")
headers.append("Connection: Upgrade")
if port == 80:
hostport = host
else:
hostport = "%s:%d" % (host, port)
headers.append("Host: %s" % hostport)
if "origin" in options:
headers.append("Origin: %s" % options["origin"])
else:
headers.append("Origin: http://%s" % hostport)
key = _create_sec_websocket_key()
headers.append("Sec-WebSocket-Key: %s" % key)
headers.append("Sec-WebSocket-Version: %s" % VERSION)
if "header" in options:
headers.extend(options["header"])
headers.append("")
headers.append("")
header_str = "\r\n".join(headers)
self._send(header_str)
if traceEnabled:
logger.debug("--- request header ---")
logger.debug(header_str)
logger.debug("-----------------------")
status, resp_headers = self._read_headers()
if status != 101:
self.close()
raise WebSocketException("Handshake Status %d" % status)
success = self._validate_header(resp_headers, key)
if not success:
self.close()
raise WebSocketException("Invalid WebSocket Header")
self.connected = True
def _validate_header(self, headers, key):
for k, v in _HEADERS_TO_CHECK.iteritems():
r = headers.get(k, None)
if not r:
return False
r = r.lower()
if v != r:
return False
result = headers.get("sec-websocket-accept", None)
if not result:
return False
result = result.lower()
value = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
hashed = base64.encodestring(hashlib.sha1(value).digest()).strip().lower()
return hashed == result
def _read_headers(self):
status = None
headers = {}
if traceEnabled:
logger.debug("--- response header ---")
while True:
line = self._recv_line()
if line == "\r\n":
break
line = line.strip()
if traceEnabled:
logger.debug(line)
if not status:
status_info = line.split(" ", 2)
status = int(status_info[1])
else:
kv = line.split(":", 1)
if len(kv) == 2:
key, value = kv
headers[key.lower()] = value.strip().lower()
else:
raise WebSocketException("Invalid header")
if traceEnabled:
logger.debug("-----------------------")
return status, headers
def send(self, payload, opcode=ABNF.OPCODE_TEXT):
"""
Send the data as string.
payload: Payload must be utf-8 string or unicoce,
if the opcode is OPCODE_TEXT.
Otherwise, it must be string(byte array)
opcode: operation code to send. Please see OPCODE_XXX.
"""
frame = ABNF.create_frame(payload, opcode)
if self.get_mask_key:
frame.get_mask_key = self.get_mask_key
data = frame.format()
length = len(data)
if traceEnabled:
logger.debug("send: " + repr(data))
while data:
l = self._send(data)
data = data[l:]
return length
def send_binary(self, payload):
return self.send(payload, ABNF.OPCODE_BINARY)
def ping(self, payload=""):
"""
send ping data.
payload: data payload to send server.
"""
self.send(payload, ABNF.OPCODE_PING)
def pong(self, payload):
"""
send pong data.
payload: data payload to send server.
"""
self.send(payload, ABNF.OPCODE_PONG)
def recv(self):
"""
Receive string data(byte array) from the server.
return value: string(byte array) value.
"""
opcode, data = self.recv_data()
return data
def recv_data(self):
"""
Recieve data with operation code.
return value: tuple of operation code and string(byte array) value.
"""
while True:
frame = self.recv_frame()
if not frame:
# handle error:
# 'NoneType' object has no attribute 'opcode'
raise WebSocketException("Not a valid frame %s" % frame)
elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY, ABNF.OPCODE_CONT):
if frame.opcode == ABNF.OPCODE_CONT and not self._cont_data:
raise WebSocketException("Illegal frame")
if self._cont_data:
self._cont_data[1] += frame.data
else:
self._cont_data = [frame.opcode, frame.data]
if frame.fin:
data = self._cont_data
self._cont_data = None
return data
elif frame.opcode == ABNF.OPCODE_CLOSE:
self.send_close()
return (frame.opcode, None)
elif frame.opcode == ABNF.OPCODE_PING:
self.pong(frame.data)
def recv_frame(self):
"""
recieve data as frame from server.
return value: ABNF frame object.
"""
# Header
if self._frame_header is None:
self._frame_header = self._recv_strict(2)
b1 = ord(self._frame_header[0])
fin = b1 >> 7 & 1
rsv1 = b1 >> 6 & 1
rsv2 = b1 >> 5 & 1
rsv3 = b1 >> 4 & 1
opcode = b1 & 0xf
b2 = ord(self._frame_header[1])
has_mask = b2 >> 7 & 1
# Frame length
if self._frame_length is None:
length_bits = b2 & 0x7f
if length_bits == 0x7e:
length_data = self._recv_strict(2)
self._frame_length = struct.unpack("!H", length_data)[0]
elif length_bits == 0x7f:
length_data = self._recv_strict(8)
self._frame_length = struct.unpack("!Q", length_data)[0]
else:
self._frame_length = length_bits
# Mask
if self._frame_mask is None:
self._frame_mask = self._recv_strict(4) if has_mask else ""
# Payload
payload = self._recv_strict(self._frame_length)
if has_mask:
payload = ABNF.mask(self._frame_mask, payload)
# Reset for next frame
self._frame_header = None
self._frame_length = None
self._frame_mask = None
return ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload)
def send_close(self, status=STATUS_NORMAL, reason=""):
"""
send close data to the server.
status: status code to send. see STATUS_XXX.
reason: the reason to close. This must be string.
"""
if status < 0 or status >= ABNF.LENGTH_16:
raise ValueError("code is invalid range")
self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE)
def close(self, status=STATUS_NORMAL, reason=""):
"""
Close Websocket object
status: status code to send. see STATUS_XXX.
reason: the reason to close. This must be string.
"""
try:
self.sock.shutdown(socket.SHUT_RDWR)
except:
pass
'''
if self.connected:
if status < 0 or status >= ABNF.LENGTH_16:
raise ValueError("code is invalid range")
try:
self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE)
timeout = self.sock.gettimeout()
self.sock.settimeout(3)
try:
frame = self.recv_frame()
if logger.isEnabledFor(logging.ERROR):
recv_status = struct.unpack("!H", frame.data)[0]
if recv_status != STATUS_NORMAL:
logger.error("close status: " + repr(recv_status))
except:
pass
self.sock.settimeout(timeout)
self.sock.shutdown(socket.SHUT_RDWR)
except:
pass
'''
self._closeInternal()
def _closeInternal(self):
self.connected = False
self.sock.close()
def _send(self, data):
try:
return self.sock.send(data)
except socket.timeout as e:
raise WebSocketTimeoutException(e.args[0])
except Exception as e:
if "timed out" in e.args[0]:
raise WebSocketTimeoutException(e.args[0])
else:
raise e
def _recv(self, bufsize):
try:
bytes = self.sock.recv(bufsize)
except socket.timeout as e:
raise WebSocketTimeoutException(e.args[0])
except SSLError as e:
if e.args[0] == "The read operation timed out":
raise WebSocketTimeoutException(e.args[0])
else:
raise
if not bytes:
raise WebSocketConnectionClosedException()
return bytes
def _recv_strict(self, bufsize):
shortage = bufsize - sum(len(x) for x in self._recv_buffer)
while shortage > 0:
bytes = self._recv(shortage)
self._recv_buffer.append(bytes)
shortage -= len(bytes)
unified = "".join(self._recv_buffer)
if shortage == 0:
self._recv_buffer = []
return unified
else:
self._recv_buffer = [unified[bufsize:]]
return unified[:bufsize]
def _recv_line(self):
line = []
while True:
c = self._recv(1)
line.append(c)
if c == "\n":
break
return "".join(line)
class WebSocketApp(object):
"""
Higher level of APIs are provided.
The interface is like JavaScript WebSocket object.
"""
def __init__(self, url, header=[],
on_open=None, on_message=None, on_error=None,
on_close=None, keep_running=True, get_mask_key=None):
"""
url: websocket url.
header: custom header for websocket handshake.
on_open: callable object which is called at opening websocket.
this function has one argument. The arugment is this class object.
on_message: callbale object which is called when recieved data.
on_message has 2 arguments.
The 1st arugment is this class object.
The passing 2nd arugment is utf-8 string which we get from the server.
on_error: callable object which is called when we get error.
on_error has 2 arguments.
The 1st arugment is this class object.
The passing 2nd arugment is exception object.
on_close: callable object which is called when closed the connection.
this function has one argument. The arugment is this class object.
keep_running: a boolean flag indicating whether the app's main loop should
keep running, defaults to True
get_mask_key: a callable to produce new mask keys, see the WebSocket.set_mask_key's
docstring for more information
"""
self.url = url
self.header = header
self.on_open = on_open
self.on_message = on_message
self.on_error = on_error
self.on_close = on_close
self.keep_running = keep_running
self.get_mask_key = get_mask_key
self.sock = None
def send(self, data, opcode=ABNF.OPCODE_TEXT):
"""
send message.
data: message to send. If you set opcode to OPCODE_TEXT, data must be utf-8 string or unicode.
opcode: operation code of data. default is OPCODE_TEXT.
"""
if self.sock.send(data, opcode) == 0:
raise WebSocketConnectionClosedException()
def close(self):
"""
close websocket connection.
"""
self.keep_running = False
if(self.sock != None):
self.sock.close()
def _send_ping(self, interval):
while True:
for i in range(interval):
time.sleep(1)
if not self.keep_running:
return
self.sock.ping()
def run_forever(self, sockopt=None, sslopt=None, ping_interval=0):
"""
run event loop for WebSocket framework.
This loop is infinite loop and is alive during websocket is available.
sockopt: values for socket.setsockopt.
sockopt must be tuple and each element is argument of sock.setscokopt.
sslopt: ssl socket optional dict.
ping_interval: automatically send "ping" command every specified period(second)
if set to 0, not send automatically.
"""
if sockopt is None:
sockopt = []
if sslopt is None:
sslopt = {}
if self.sock:
raise WebSocketException("socket is already opened")
thread = None
self.keep_running = True
try:
self.sock = WebSocket(self.get_mask_key, sockopt=sockopt, sslopt=sslopt)
self.sock.settimeout(default_timeout)
self.sock.connect(self.url, header=self.header)
self._callback(self.on_open)
if ping_interval:
thread = threading.Thread(target=self._send_ping, args=(ping_interval,))
thread.setDaemon(True)
thread.start()
while self.keep_running:
try:
data = self.sock.recv()
if data is None or self.keep_running == False:
break
self._callback(self.on_message, data)
except Exception as e:
found_timeout = False
for arg in e.args:
if isinstance(arg, str):
if "timed out" in arg:
found_timeout = True
if not found_timeout:
raise e
except Exception as e:
self._callback(self.on_error, e)
finally:
if thread:
self.keep_running = False
self.sock.close()
self._callback(self.on_close)
self.sock = None
def _callback(self, callback, *args):
if callback:
try:
callback(self, *args)
except Exception as e:
logger.error(e)
if True:#logger.isEnabledFor(logging.DEBUG):
_, _, tb = sys.exc_info()
traceback.print_tb(tb)
if __name__ == "__main__":
enableTrace(True)
ws = create_connection("ws://echo.websocket.org/")
print("Sending 'Hello, World'...")
ws.send("Hello, World")
print("Sent")
print("Receiving...")
result = ws.recv()
print("Received '%s'" % result)
ws.close()

View File

@@ -2,9 +2,11 @@
#################################################################################################
from __future__ import division, absolute_import, print_function, unicode_literals
import json
import threading
import websocket
import time
import xbmc
import xbmcgui
@@ -31,6 +33,7 @@ class WebSocketClient(threading.Thread):
self.__dict__ = self._shared_state
self.monitor = xbmc.Monitor()
self.retry_count = 0
self.client_info = clientinfo.ClientInformation()
self.device_id = self.client_info.get_device_id()
@@ -226,11 +229,9 @@ class WebSocketClient(threading.Thread):
if command in builtin:
xbmc.executebuiltin(builtin[command])
def on_close(self, ws):
log.debug("Closed")
def on_open(self, ws):
log.debug("Connected")
self.retry_count = 0
self.post_capabilities()
def on_error(self, ws, error):
@@ -249,23 +250,25 @@ class WebSocketClient(threading.Thread):
# Get the appropriate prefix for the websocket
server = download_utils.get_server()
if "https" in server:
server = server.replace('https', "wss")
if "https://" in server:
server = server.replace('https://', 'wss://')
else:
server = server.replace('http', "ws")
server = server.replace('http://', 'ws://')
websocket_url = "%s/websocket?api_key=%s&deviceId=%s" % (server, token, self.device_id)
websocket_url = "%s/socket?api_key=%s&deviceId=%s" % (server, token, self.device_id)
log.debug("websocket url: {0}".format(websocket_url))
self._client = websocket.WebSocketApp(websocket_url,
on_open=self.on_open,
on_message=self.on_message,
on_error=self.on_error,
on_close=self.on_close)
self._client = websocket.WebSocketApp(
websocket_url,
on_open=lambda ws: self.on_open(ws),
on_message=lambda ws, message: self.on_message(ws, message),
on_error=lambda ws, error: self.on_error(ws, error))
log.debug("Starting WebSocketClient")
while not self.monitor.abortRequested():
time.sleep(self.retry_count * 5)
self._client.run_forever(ping_interval=10)
if self._stop_websocket:
@@ -275,6 +278,8 @@ class WebSocketClient(threading.Thread):
# Abort was requested, exit
break
if self.retry_count < 12:
self.retry_count += 1
log.debug("Reconnecting WebSocket")
log.debug("WebSocketClient Stopped")

View File

@@ -1,8 +1,9 @@
from __future__ import division, absolute_import, print_function, unicode_literals
import xbmcaddon
import xbmcplugin
import xbmcgui
import xbmc
import json
import hashlib
import random
import time
@@ -43,7 +44,6 @@ def set_random_movies():
url = get_jellyfin_url("{server}/Users/{userid}/Items", url_params)
results = downloadUtils.download_url(url, suppress=True)
results = json.loads(results)
randon_movies_list = []
if results is not None:
@@ -55,7 +55,7 @@ def set_random_movies():
movies_list_string = ",".join(randon_movies_list)
home_window = HomeWindow()
m = hashlib.md5()
m.update(movies_list_string)
m.update(movies_list_string.encode())
new_widget_hash = m.hexdigest()
log.debug("set_random_movies : {0}".format(movies_list_string))
@@ -77,11 +77,11 @@ def set_background_image(force=False):
if len(background_items) == 0:
log.debug("set_background_image: Need to load more backgrounds {0} - {1}".format(
len(background_items), background_current_item))
len(background_items), background_current_item))
url_params = {}
url_params["Recursive"] = True
# url_params["limit"] = 60
url_params["limit"] = 100
url_params["SortBy"] = "Random"
url_params["IncludeItemTypes"] = "Movie,Series"
url_params["ImageTypeLimit"] = 1
@@ -90,14 +90,14 @@ def set_background_image(force=False):
server = downloadUtils.get_server()
results = downloadUtils.download_url(url, suppress=True)
results = json.loads(results)
if results is not None:
items = results.get("Items", [])
background_current_item = 0
background_items = []
for item in items:
bg_image = downloadUtils.get_artwork(item, "Backdrop", server=server)
bg_image = downloadUtils.get_artwork(
item, "Backdrop", server=server)
if bg_image:
label = item.get("Name")
item_background = {}
@@ -105,12 +105,14 @@ def set_background_image(force=False):
item_background["name"] = label
background_items.append(item_background)
log.debug("set_background_image: Loaded {0} more backgrounds".format(len(background_items)))
log.debug("set_background_image: Loaded {0} more backgrounds".format(
len(background_items)))
if len(background_items) > 0:
bg_image = background_items[background_current_item].get("image")
label = background_items[background_current_item].get("name")
log.debug("set_background_image: {0} - {1} - {2}".format(background_current_item, label, bg_image))
log.debug(
"set_background_image: {0} - {1} - {2}".format(background_current_item, label, bg_image))
background_current_item += 1
if background_current_item >= len(background_items):
@@ -127,7 +129,8 @@ def check_for_new_content():
home_window = HomeWindow()
settings = xbmcaddon.Addon()
simple_new_content_check = settings.getSetting("simple_new_content_check") == "true"
simple_new_content_check = settings.getSetting(
"simple_new_content_check") == "true"
if simple_new_content_check:
log.debug("Using simple new content check")
@@ -144,12 +147,10 @@ def check_for_new_content():
url_params["SortOrder"] = "Descending"
url_params["IncludeItemTypes"] = "Movie,Episode"
url_params["ImageTypeLimit"] = 0
url_params["format"] = "json"
added_url = get_jellyfin_url('{server}/Users/{userid}/Items', url_params)
added_result = downloadUtils.download_url(added_url, suppress=True)
result = json.loads(added_result)
result = downloadUtils.download_url(added_url, suppress=True)
log.debug("LATEST_ADDED_ITEM: {0}".format(result))
last_added_date = ""
@@ -168,12 +169,10 @@ def check_for_new_content():
url_params["SortOrder"] = "Descending"
url_params["IncludeItemTypes"] = "Movie,Episode"
url_params["ImageTypeLimit"] = 0
url_params["format"] = "json"
played_url = get_jellyfin_url('{server}/Users/{userid}/Items', url_params)
played_result = downloadUtils.download_url(played_url, suppress=True)
result = json.loads(played_result)
result = downloadUtils.download_url(played_url, suppress=True)
log.debug("LATEST_PLAYED_ITEM: {0}".format(result))
last_played_date = ""
@@ -181,7 +180,6 @@ def check_for_new_content():
items = result.get("Items", [])
if len(items) > 0:
item = items[0]
# last_played_date = item.get("Etag", "")
user_data = item.get("UserData", None)
if user_data is not None:
last_played_date = user_data.get("LastPlayedDate", "")
@@ -208,7 +206,8 @@ def get_widget_content_cast(handle, params):
item_id = params["id"]
data_manager = DataManager()
result = data_manager.get_content("{server}/Users/{userid}/Items/" + item_id + "?format=json")
result = data_manager.get_content(
"{server}/Users/{userid}/Items/" + item_id)
log.debug("ItemInfo: {0}".format(result))
if not result:
@@ -227,12 +226,6 @@ def get_widget_content_cast(handle, params):
people = []
for person in people:
# if (person.get("Type") == "Director"):
# director = director + person.get("Name") + ' '
# if (person.get("Type") == "Writing"):
# writer = person.get("Name")
# if (person.get("Type") == "Writer"):
# writer = person.get("Name")
if person.get("Type") == "Actor":
person_name = person.get("Name")
person_role = person.get("Role")
@@ -240,7 +233,8 @@ def get_widget_content_cast(handle, params):
person_tag = person.get("PrimaryImageTag")
person_thumbnail = None
if person_tag:
person_thumbnail = downloadUtils.image_url(person_id, "Primary", 0, 400, 400, person_tag, server=server)
person_thumbnail = downloadUtils.image_url(
person_id, "Primary", 0, 400, 400, person_tag, server=server)
if kodi_version > 17:
list_item = xbmcgui.ListItem(label=person_name, offscreen=True)
@@ -276,7 +270,8 @@ def get_widget_content(handle, params):
settings = xbmcaddon.Addon()
hide_watched = settings.getSetting("hide_watched") == "true"
use_cached_widget_data = settings.getSetting("use_cached_widget_data") == "true"
use_cached_widget_data = settings.getSetting(
"use_cached_widget_data") == "true"
widget_type = params.get("type")
if widget_type is None:
@@ -288,10 +283,10 @@ def get_widget_content(handle, params):
url_verb = "{server}/Users/{userid}/Items"
url_params = {}
url_params["Limit"] = "{ItemLimit}"
url_params["format"] = "json"
url_params["Fields"] = "{field_filters}"
url_params["ImageTypeLimit"] = 1
url_params["IsMissing"] = False
in_progress = False
if widget_type == "recent_movies":
xbmcplugin.setContent(handle, 'movies')
@@ -303,6 +298,7 @@ def get_widget_content(handle, params):
url_params["IsPlayed"] = False
url_params["IsVirtualUnaired"] = False
url_params["IncludeItemTypes"] = "Movie"
url_params["Limit"] = 20
elif widget_type == "inprogress_movies":
xbmcplugin.setContent(handle, 'movies')
@@ -312,6 +308,7 @@ def get_widget_content(handle, params):
url_params["Filters"] = "IsResumable"
url_params["IsVirtualUnaired"] = False
url_params["IncludeItemTypes"] = "Movie"
url_params["Limit"] = 20
elif widget_type == "random_movies":
xbmcplugin.setContent(handle, 'movies')
@@ -331,7 +328,7 @@ def get_widget_content(handle, params):
url_params["IsVirtualUnaired"] = False
url_params["IncludeItemTypes"] = "Episode"
url_params["ImageTypeLimit"] = 1
url_params["format"] = "json"
url_params["Limit"] = 20
elif widget_type == "recent_episodes":
xbmcplugin.setContent(handle, 'episodes')
@@ -343,6 +340,7 @@ def get_widget_content(handle, params):
url_params["IsPlayed"] = False
url_params["IsVirtualUnaired"] = False
url_params["IncludeItemTypes"] = "Episode"
url_params["Limit"] = 20
elif widget_type == "inprogress_episodes":
xbmcplugin.setContent(handle, 'episodes')
@@ -352,16 +350,27 @@ def get_widget_content(handle, params):
url_params["Filters"] = "IsResumable"
url_params["IsVirtualUnaired"] = False
url_params["IncludeItemTypes"] = "Episode"
url_params["Limit"] = 20
elif widget_type == "nextup_episodes":
xbmcplugin.setContent(handle, 'episodes')
url_verb = "{server}/Shows/NextUp"
url_params = url_params.copy()
url_params["Limit"] = "{ItemLimit}"
url_params["userid"] = "{userid}"
url_params["Recursive"] = True
url_params["Fields"] = "{field_filters}"
url_params["format"] = "json"
url_params["ImageTypeLimit"] = 1
# Collect InProgress items to be combined with NextUp
inprogress_url_verb = "{server}/Users/{userid}/Items"
inprogress_url_params = url_params.copy()
inprogress_url_params["Recursive"] = True
inprogress_url_params["SortBy"] = "DatePlayed"
inprogress_url_params["SortOrder"] = "Descending"
inprogress_url_params["Filters"] = "IsResumable"
inprogress_url_params["IsVirtualUnaired"] = False
inprogress_url_params["IncludeItemTypes"] = "Episode"
inprogress_url_params["Limit"] = 20
in_progress = True
elif widget_type == "movie_recommendations":
suggested_items_url_params = {}
@@ -369,7 +378,8 @@ def get_widget_content(handle, params):
suggested_items_url_params["categoryLimit"] = 15
suggested_items_url_params["ItemLimit"] = 20
suggested_items_url_params["ImageTypeLimit"] = 0
suggested_items_url = get_jellyfin_url("{server}/Movies/Recommendations", suggested_items_url_params)
suggested_items_url = get_jellyfin_url(
"{server}/Movies/Recommendations", suggested_items_url_params)
data_manager = DataManager()
suggested_items = data_manager.get_content(suggested_items_url)
@@ -377,20 +387,15 @@ def get_widget_content(handle, params):
set_id = 0
while len(ids) < 20 and suggested_items:
items = suggested_items[set_id]
log.debug("BaselineItemName : {0} - {1}".format(set_id, items.get("BaselineItemName")))
log.debug(
"BaselineItemName : {0} - {1}".format(set_id, items.get("BaselineItemName")))
items = items["Items"]
rand = random.randint(0, len(items) - 1)
# log.debug("random suggestions index : {0} {1}", rand, set_id)
item = items[rand]
if item["Type"] == "Movie" and item["Id"] not in ids and (not item["UserData"]["Played"] or not hide_watched):
# log.debug("random suggestions adding : {0}", item["Id"])
ids.append(item["Id"])
# else:
# log.debug("random suggestions not valid : {0} - {1} - {2}", item["Id"], item["Type"], item["UserData"]["Played"])
del items[rand]
# log.debug("items len {0}", len(items))
if len(items) == 0:
# log.debug("Removing Set {0}", set_id)
del suggested_items[set_id]
set_id += 1
if set_id >= len(suggested_items):
@@ -402,18 +407,18 @@ def get_widget_content(handle, params):
items_url = get_jellyfin_url(url_verb, url_params)
list_items, detected_type, total_records = process_directory(items_url, None, params, use_cached_widget_data)
list_items, detected_type, total_records = process_directory(
items_url, None, params, use_cached_widget_data)
# remove resumable items from next up
# Combine In Progress and Next Up Episodes, apend next up after In Progress
if widget_type == "nextup_episodes":
filtered_list = []
for item in list_items:
resume_time = item[1].getProperty("ResumeTime")
if resume_time is None or float(resume_time) == 0.0:
filtered_list.append(item)
list_items = filtered_list
inprogress_url = get_jellyfin_url(
inprogress_url_verb, inprogress_url_params)
# list_items = populateWidgetItems(items_url, widget_type)
list_items_inprogress, detected_type, total_records = process_directory(
inprogress_url, None, params, use_cached_widget_data)
list_items = list_items_inprogress + list_items
if detected_type is not None:
# if the media type is not set then try to use the detected type

View File

@@ -46,13 +46,6 @@
<setting id="audio_playback_bitrate" type="select" label="30418" values="128|160|192|256|320|384|448|640" default="256" visible="true"/>
<setting id="audio_max_channels" type="slider" label="30420" default="8" range="2,1,8" option="int" visible="true"/>
<!--
<setting label="30209" type="lsep"/>
<setting type="sep" />
<setting id="smbusername" type="text" label="30007" default="" enable="true" visible="true"/>
<setting id="smbpassword" type="text" label="30008" default="" option="hidden" enable="true" visible="true"/>
-->
</category>
<category label="30214">
@@ -74,8 +67,8 @@
<setting label="30329" type="lsep"/>
<setting type="sep" />
<setting id="stopPlaybackOnScreensaver" type="bool" label="30332" default="true" visible="true" enable="true" />
<setting id="changeUserOnScreenSaver" type="bool" label="30330" default="true" visible="true" enable="true" />
<setting id="stopPlaybackOnScreensaver" type="bool" label="30332" default="false" visible="true" enable="true" />
<setting id="changeUserOnScreenSaver" type="bool" label="30330" default="false" visible="true" enable="true" />
<setting id="cacheImagesOnScreenSaver" type="bool" label="30333" default="true" visible="true" enable="true" />
<setting id="cacheImagesOnScreenSaver_interval" type="slider" label="30400" default="0" range="0,1,60" option="int" visible="true"/>
@@ -155,4 +148,4 @@
<setting id="sort-Episodes" type="select" label="30235" lvalues="30423|30424|30426|30425|30427|30429|30430|30428" default="0" visible="true"/>
</category>
</settings>
</settings>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 312 B

After

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 B

After

Width:  |  Height:  |  Size: 109 B

View File

@@ -112,13 +112,11 @@ if enable_logging:
time=8000,
icon=xbmcgui.NOTIFICATION_WARNING)
# monitor.abortRequested() is causes issues, it currently triggers for all addon cancelations which causes
# the service to exit when a user cancels an addon load action. This is a bug in Kodi.
# I am switching back to xbmc.abortRequested approach until kodi is fixed or I find a work arround
prev_user_id = home_window.get_property("userid")
first_run = True
home_window.set_property('exit', 'False')
while not xbmc.abortRequested:
while home_window.get_property('exit') == 'False':
try:
if xbmc.Player().isPlaying():
@@ -126,7 +124,7 @@ while not xbmc.abortRequested:
# if playing every 10 seconds updated the server with progress
if (time.time() - last_progress_update) > 10:
last_progress_update = time.time()
send_progress(monitor)
send_progress()
else:
screen_saver_active = xbmc.getCondVisibility("System.ScreenSaverActive")
@@ -189,6 +187,9 @@ while not xbmc.abortRequested:
image_server.stop()
# stop the WebSocket Client
websocket_client.stop_client()
# call stop on the library update monitor
library_change_monitor.stop()
@@ -200,9 +201,6 @@ if play_next_service:
if context_monitor:
context_monitor.stop_monitor()
# stop the WebSocket Client
websocket_client.stop_client()
# clear user and token when loggin off
home_window.clear_property("userid")
home_window.clear_property("AccessToken")

9
tox.ini Normal file
View File

@@ -0,0 +1,9 @@
[flake8]
max-line-length = 9999
import-order-style = pep8
exclude = .git,.vscode,libraries,build.py,.github
extend-ignore =
I202
per-file-ignores =
*/__init__.py: F401
scripts/process_addon.py: F821