425 Commits

Author SHA1 Message Date
mcarlton00
6ad76560ed Merge pull request #195 from jellyfin/prepare-0.5.5
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
Prepare for release v0.5.5
2022-07-21 22:02:17 -04:00
jellyfin-bot
d6f449930c bump version to 0.5.5 2022-07-22 02:01:11 +00:00
mcarlton00
ebdf501115 Merge pull request #194 from mcarlton00/playback-handles
Ensure a handle is valid before trying to use it to start playback
2022-07-21 21:58:38 -04:00
Matt
34861fb9b9 Ensure a handle is valid before trying to use it to start playback 2022-07-21 21:45:33 -04:00
mcarlton00
2f3fe8ae6b Merge pull request #190 from jellyfin/prepare-0.5.4
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
Prepare for release v0.5.4
2022-07-20 08:57:26 -04:00
jellyfin-bot
4ee91654ea bump version to 0.5.4 2022-07-20 12:56:04 +00:00
mcarlton00
3190658e0c Merge pull request #189 from mcarlton00/fix-playback
Fix sys.argv comparison
2022-07-20 08:36:50 -04:00
Matt
c330523b9e Make playback comments more accurate 2022-07-20 08:22:31 -04:00
Matt
5a5c865135 Fix sys.argv comparison 2022-07-20 08:20:24 -04:00
mcarlton00
f5ae301c97 Merge pull request #187 from jellyfin/prepare-0.5.3
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
Prepare for release v0.5.3
2022-07-19 14:24:50 -04:00
jellyfin-bot
d910441470 bump version to 0.5.3 2022-07-19 18:16:04 +00:00
mcarlton00
b667bf4117 Merge pull request #186 from mcarlton00/emoji-update
CI - remove use_aliases from emoji command
2022-07-19 10:46:57 -04:00
Matt
4d8b4c9d43 CI - remove use_aliases from emoji command 2022-07-19 10:11:29 -04:00
mcarlton00
02c44eef82 Merge pull request #184 from aiosk/fix_sdh_subtitle_not_displayed
Fix forced and sdh/cc subtitle not working
2022-07-18 15:40:03 -04:00
andry.yosua
c21d10d7f9 fix couple minor style things 2022-07-18 14:52:22 +07:00
andry.yosua
d2d14e4c19 fix couple minor style things 2022-07-18 14:47:21 +07:00
andry.yosua
1da8bca9d1 remove unique id hi to identify sdh/cc 2022-07-12 14:29:14 +07:00
andry.yosua
dffbfd8860 add no language fallback 2022-07-12 06:41:19 +07:00
andry.yosua
7247c51b10 fix cannot choose sdh/cc/hi subtitle
condition:
I have multiple subtitle
- movie_name.en.srt
- movie_name.en.sdh.srt

expected:
I can play movie with sdh subtitle, either by choosing subtitle
before play movies, or while playing movies

result:
I cannot play movie with sdh subtitle

This happen because downloaded subtitle file only has language id in
it's name. It doesn't have unique identifier such as sdh/cc/hi. This fix
add sdh/cc/hi id to the downloaded subtitle.

This fix doesn't handle transcode procedure.
2022-07-12 04:58:09 +07:00
andry.yosua
2517e30f55 fix cannot choose forced subtitle
condition:
I have multiple subtitle
- movie_name.en.srt
- movie_name.en.forced.srt
- movie_name.id.srt

expected:
I can play movie with forced subtitle, either by choosing subtitle
before play movies, or while playing movies

result:
I cannot play movie with forced subtitle

This happen because downloaded subtitle file only has language id in
it's name. It doesn't have unique identifier such as forced. This fix
add forced id to the downloaded subtitle.

This fix doesn't handle transcode procedure.
2022-07-12 04:43:18 +07:00
mcarlton00
caa0e47399 Merge pull request #183 from mcarlton00/json-decoding
Manually decode json responses
2022-07-10 19:28:52 -04:00
mcarlton00
8ce6ec49e1 Merge pull request #182 from mcarlton00/py3-profile
Fix performance profiling
2022-07-08 21:48:00 -04:00
Matt
276b18650c Don't use requests.json() if we can help it 2022-07-08 21:44:58 -04:00
Matt
a69a8a269f Fix profiling for py3 2022-07-08 21:22:20 -04:00
Fedir Smilianets
921628b2a1 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (271 of 271 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/uk/
2022-07-07 13:22:18 -04:00
Fedir Smilianets
53225b427c Translated using Weblate (Ukrainian)
Currently translated at 100.0% (271 of 271 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/uk/
2022-07-06 09:22:18 -04:00
Thomas Schwery
6aad1eb92d Translated using Weblate (French)
Currently translated at 84.1% (228 of 271 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/fr/
2022-07-06 09:22:17 -04:00
Patarimi
d4bccb9b05 Translated using Weblate (French)
Currently translated at 84.1% (228 of 271 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/fr/
2022-07-06 09:22:17 -04:00
Joaquín Díaz
02e9886e2e Translated using Weblate (Spanish)
Currently translated at 100.0% (271 of 271 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/es/
2022-07-06 09:22:17 -04:00
Marcin Woliński
a05363d202 Translated using Weblate (Polish)
Currently translated at 100.0% (271 of 271 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/pl/
2022-06-28 09:22:15 -04:00
Michele Fattoruso
78fd1e206b Translated using Weblate (Italian)
Currently translated at 38.7% (105 of 271 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/it/
2022-06-28 09:22:15 -04:00
mcarlton00
74951b9dec Merge pull request #178 from jellyfin/dependabot/github_actions/actions/setup-python-4
Bump actions/setup-python from 3 to 4
2022-06-24 15:22:19 -04:00
mcarlton00
5ac21ea240 Merge pull request #179 from mcarlton00/info-play
Allow playback from Info menu
2022-06-24 15:21:53 -04:00
Matt
086e92da4b use setResolvedUrl for playing single items where possible 2022-06-23 20:14:53 -04:00
wolong gl
e54c4c1bec Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (271 of 271 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/zh_Hans/
2022-06-20 12:22:12 -04:00
dependabot[bot]
5bb20e482f Bump actions/setup-python from 3 to 4
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 3 to 4.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-20 16:16:28 +00:00
mcarlton00
569462f755 Merge pull request #177 from jellyfin/prepare-0.5.2
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
Prepare for release v0.5.2
2022-06-20 09:32:07 -04:00
jellyfin-bot
658050548c bump version to 0.5.2 2022-06-20 13:25:17 +00:00
mcarlton00
4d635f6eb4 Merge pull request #176 from mcarlton00/external-subs-10.8
Download all external subs when playback starts
2022-06-20 09:24:14 -04:00
mcarlton00
a0b1e9177b Merge branch 'master' into external-subs-10.8 2022-06-19 16:01:12 -04:00
Matt
7a1a7843e6 Download all external subs when playback starts 2022-06-19 15:55:46 -04:00
mcarlton00
66d4e02024 Merge pull request #174 from jellyfin/prepare-0.5.1
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
Prepare for release v0.5.1
2022-06-17 21:14:02 -04:00
jellyfin-bot
04a46f0cd9 bump version to 0.5.1 2022-06-18 01:11:55 +00:00
mcarlton00
4e0ac5330f Merge pull request #170 from nitschis/master
Update README.md
2022-06-17 19:03:45 -04:00
mcarlton00
32288aba97 Apply suggestions from code review 2022-06-17 19:03:32 -04:00
mcarlton00
801f119a5c Merge pull request #173 from mcarlton00/music-play-options
Add context menu play options for music
2022-06-16 21:39:30 -04:00
Matt
567a02872b Recursively play tracks of artists 2022-06-16 20:33:40 -04:00
Matt
cfc19cafc5 Add context menu play options for music 2022-06-16 19:47:11 -04:00
mcarlton00
44823f5043 Merge pull request #172 from mcarlton00/i-hate-dates
Do all date comparisons in UTC
2022-06-16 19:45:16 -04:00
Matt
bad5f8e561 Remove unnecessary import 2022-06-16 19:38:15 -04:00
Matt
075e1e8974 Move current date calculation to a function 2022-06-16 18:24:16 -04:00
Matt
06f78ce620 Do all date calculations in UTC 2022-06-16 18:15:40 -04:00
nitschis
cce9acb182 Update README.md 2022-06-15 23:01:17 +02:00
nitschis
048b8f0385 Update README.md 2022-06-15 06:04:00 +02:00
mcarlton00
fa318ac751 Merge pull request #169 from jellyfin/prepare-0.4.8
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
Prepare for release v0.5.0
2022-06-14 17:54:59 -04:00
mcarlton00
071ad16f83 Update release.yaml 2022-06-14 17:52:56 -04:00
jellyfin-bot
eb44fc0ef7 bump version to 0.4.8 2022-06-14 21:51:17 +00:00
mcarlton00
f35239e1a4 Merge pull request #168 from mcarlton00/offscreen-listitems
Use offscreen option when generating all listitems
2022-06-12 11:12:28 -04:00
Matt
5534878dcd Use offscreen option when generating all listitems 2022-06-12 10:52:49 -04:00
Oskari Lavinto
f1b82cf89a Translated using Weblate (Finnish)
Currently translated at 100.0% (271 of 271 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/fi/
2022-06-12 08:22:09 -04:00
Moritz
4ae81b9f52 Translated using Weblate (German)
Currently translated at 100.0% (271 of 271 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/de/
2022-06-12 08:22:09 -04:00
mcarlton00
7af412e6c5 Merge pull request #167 from mcarlton00/quick-connect-10.7
Make sure manual login shows when connecting to a 10.7 server
2022-06-11 08:21:53 -04:00
Matt
52baa2e8cc Make sure manual login shows when connecting to a 10.7 server 2022-06-11 08:04:40 -04:00
mcarlton00
d48c159283 Merge pull request #166 from mcarlton00/safe-delete
Remove safe delete code
2022-06-09 21:37:43 -04:00
Matt
fae51bd9b2 Remove safe delete code 2022-06-09 21:31:16 -04:00
mcarlton00
9a6e16f505 Merge pull request #165 from mcarlton00/flake8-fixes
Address flake8 warnings
2022-06-09 20:54:10 -04:00
Matt
44afd62989 Address flake8 warnings 2022-06-09 20:35:06 -04:00
mcarlton00
69986cc40d Merge pull request #164 from mcarlton00/dict-definitions
Simplify url param dict definitions
2022-06-09 20:30:50 -04:00
Matt
9769a36c30 Simplify dict definitions 2022-06-09 20:12:48 -04:00
mcarlton00
1621510b15 Merge pull request #163 from mcarlton00/optimize-loops
Optimize loops while building menus
2022-06-09 20:00:44 -04:00
DJSweder
85f910dd48 Translated using Weblate (Czech)
Currently translated at 4.4% (12 of 271 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/cs/
2022-06-09 17:22:08 -04:00
DJSweder
e260124351 Added translation using Weblate (Czech) 2022-06-08 16:46:16 -04:00
Csaba
1eb2d21086 Translated using Weblate (Hungarian)
Currently translated at 100.0% (271 of 271 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/hu/
2022-06-08 00:22:07 -04:00
WWWesten
fedc67e1e2 Translated using Weblate (Esperanto)
Currently translated at 100.0% (271 of 271 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/eo/
2022-06-08 00:22:07 -04:00
WWWesten
c88a7b134f Translated using Weblate (Russian)
Currently translated at 100.0% (271 of 271 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/ru/
2022-06-08 00:22:07 -04:00
WWWesten
20dfc02624 Translated using Weblate (Kazakh)
Currently translated at 100.0% (271 of 271 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/kk/
2022-06-08 00:22:07 -04:00
egymoh
2ca11e53be Translated using Weblate (Arabic)
Currently translated at 66.2% (179 of 270 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/ar/
2022-06-05 10:22:07 -04:00
WWWesten
1b79794ede Translated using Weblate (Esperanto)
Currently translated at 100.0% (270 of 270 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/eo/
2022-06-05 10:22:07 -04:00
WWWesten
806de6910a Translated using Weblate (Russian)
Currently translated at 100.0% (270 of 270 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/ru/
2022-06-05 10:22:07 -04:00
WWWesten
ba30fc1613 Translated using Weblate (Kazakh)
Currently translated at 100.0% (270 of 270 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/kk/
2022-06-05 10:22:07 -04:00
Matt
cd5774bfd3 Remove unneeded variable 2022-06-05 10:19:51 -04:00
Matt
df9cd5fe48 Reuse URL parameters during loops 2022-06-05 10:19:01 -04:00
Matt
76d189b8de Combine sequential for loops 2022-06-05 10:05:17 -04:00
mcarlton00
0a0fd8a9a3 Merge pull request #162 from mcarlton00/fix-play-all
Fix playing all files
2022-06-04 22:56:42 -04:00
Matt
f78764d18a Fix playing all files 2022-06-04 22:38:58 -04:00
mcarlton00
59b352df35 Merge pull request #161 from mcarlton00/music-options
Add genres and alpha picker to music
2022-06-04 22:26:43 -04:00
Matt
c241cc9df9 Ensure variables are initialized before use 2022-06-04 22:10:54 -04:00
Matt
5ba6291fd9 Add genre and alpha picker to music 2022-06-04 21:56:40 -04:00
hogenf
c8715218c8 Translated using Weblate (Swedish)
Currently translated at 75.7% (203 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/sv/
2022-06-04 06:22:06 -04:00
mcarlton00
cc9caf3d54 Merge pull request #160 from mcarlton00/add-button-to-bitrate-dialog
Add button to the bitrate selector
2022-06-01 16:46:18 -04:00
mcarlton00
6d298daea4 Merge pull request #159 from mcarlton00/quick-connect
Add quick connect authentication
2022-06-01 16:45:41 -04:00
B v H
07ef6b6f41 Translated using Weblate (Dutch)
Currently translated at 48.8% (131 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/nl/
2022-06-01 13:51:37 -04:00
Matt
6c8e193130 Add button to the bitrate selector 2022-05-30 16:45:29 -04:00
Matt
b7c31fa7a0 Add custom dialog for quick connect 2022-05-30 15:27:35 -04:00
Matt
d25c7b351e Default to using quick connect to log into server 2022-05-28 19:27:51 -04:00
mcarlton00
90103b9fba Merge pull request #157 from mcarlton00/ensure-server
Ensure server is present in API requests
2022-05-28 10:22:17 -04:00
Matt
7c39b06297 Ensure server is present in API requests 2022-05-28 10:06:29 -04:00
mcarlton00
2ead20b2f7 Merge pull request #156 from mcarlton00/stop-transcode
Fix errors when stopping transcoded playback
2022-05-28 09:09:58 -04:00
mcarlton00
c8821b1055 Merge pull request #155 from mcarlton00/context-menu
Fix content update checks
2022-05-28 09:09:31 -04:00
Matt
50b6f773f5 Fix errors when stopping a transcode 2022-05-26 21:43:05 -04:00
mcarlton00
11705d46f2 Merge pull request #154 from mcarlton00/verify-certs
Verify certificates by default
2022-05-26 21:39:10 -04:00
Matt
3e96211433 Fix opening context menu multiple times 2022-05-26 21:32:42 -04:00
Matt
0c3cefc0ff Verify certificates by default 2022-05-26 21:05:12 -04:00
mcarlton00
deab5fc2c2 Merge pull request #153 from mcarlton00/stale-migration
Fix stale data after storage migration
2022-05-22 12:08:51 -04:00
Matt
9c68120447 Check for updated credentials before making requests 2022-05-21 00:10:55 -04:00
Matt
6f3181a643 Ensure auth token is in request headers 2022-05-20 23:56:58 -04:00
Matt
416b04fdec Don't use stale data 2 - widgets 2022-05-20 23:46:59 -04:00
Matt
c2958445cb Get current data when generating URLs 2022-05-20 23:40:13 -04:00
liimee
1176db8f41 Translated using Weblate (Indonesian)
Currently translated at 44.0% (118 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/id/
2022-05-20 00:22:01 -04:00
Sherlock
136e22bb84 Translated using Weblate (Hindi)
Currently translated at 31.7% (85 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/hi/
2022-05-20 00:22:01 -04:00
MoFanFr
78b7f38f61 Translated using Weblate (French)
Currently translated at 80.2% (215 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/fr/
2022-05-20 00:22:01 -04:00
mcarlton00
05ad58b46e Merge pull request #152 from jellyfin/dependabot/github_actions/release-drafter/release-drafter-5.20.0
Bump release-drafter/release-drafter from 5.19.0 to 5.20.0
2022-05-18 19:01:00 -04:00
liimee
8040349718 Added translation using Weblate (Indonesian) 2022-05-18 09:14:01 -04:00
José Albano
fca212edc3 Translated using Weblate (Spanish)
Currently translated at 100.0% (268 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/es/
2022-05-17 00:13:56 -04:00
dependabot[bot]
7237fd2c63 Bump release-drafter/release-drafter from 5.19.0 to 5.20.0
Bumps [release-drafter/release-drafter](https://github.com/release-drafter/release-drafter) from 5.19.0 to 5.20.0.
- [Release notes](https://github.com/release-drafter/release-drafter/releases)
- [Commits](https://github.com/release-drafter/release-drafter/compare/v5.19.0...v5.20.0)

---
updated-dependencies:
- dependency-name: release-drafter/release-drafter
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-16 16:15:41 +00:00
mcarlton00
26c02fdbac Merge pull request #150 from mcarlton00/skin-shortcuts
Fix legacy skin shortcuts
2022-05-12 18:08:04 -04:00
Rhodri
aa4a3531e9 Translated using Weblate (Welsh)
Currently translated at 37.6% (101 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/cy/
2022-05-08 07:13:52 -04:00
Matt
4495129a66 Fix legacy skin shortcuts 2022-05-06 15:53:51 -04:00
Rhodri
791ed3fe32 Translated using Weblate (Welsh)
Currently translated at 27.2% (73 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/cy/
2022-05-04 14:13:51 -04:00
Rhodri
b29688dc8d Translated using Weblate (Welsh)
Currently translated at 23.1% (62 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/cy/
2022-05-03 11:13:51 -04:00
Rhodri
2936a12e25 Translated using Weblate (Welsh)
Currently translated at 14.1% (38 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/cy/
2022-05-02 09:13:51 -04:00
mcarlton00
213fa15ef3 Merge pull request #148 from mcarlton00/direct-live-stream
Rework live tv playback
2022-05-01 15:47:16 -04:00
mcarlton00
80f25d016c Merge pull request #147 from mcarlton00/timezone-login
Use timezone when calculating last activity date
2022-05-01 12:23:01 -04:00
Matt
a6ad9b5187 Remove unintended newline 2022-05-01 12:20:32 -04:00
Matt
2f5765cc3f Rework livestream playback 2022-05-01 12:13:35 -04:00
Matt
d08b5b7041 Get channel metadata for live streams 2022-05-01 12:12:33 -04:00
Matt
2772cf6389 Use timezone when calculating last activity date 2022-04-30 20:53:01 -04:00
mcarlton00
6317aec577 Merge pull request #144 from mcarlton00/live-tv-playback
Fix live tv playback
2022-04-30 10:36:00 -04:00
mcarlton00
43f576d1c5 Fix browsing the Live TV programs menu (#145)
* Convert server times to local timezone

* Fix naming format of programs

* Simplify string formatting

* Remove unneeded space
2022-04-30 10:34:47 -04:00
Matt
d8493bc6bf Fix live tv playback 2022-04-27 22:00:32 -04:00
mcarlton00
96010b48c1 Merge pull request #143 from jellyfin/dependabot/github_actions/github/codeql-action-2
Bump github/codeql-action from 1 to 2
2022-04-26 20:39:01 -04:00
dependabot[bot]
a803e007d0 Bump github/codeql-action from 1 to 2
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 1 to 2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-25 16:11:53 +00:00
mcarlton00
0699bdd141 Merge pull request #141 from jellyfin/dependabot/github_actions/actions/upload-artifact-3
Bump actions/upload-artifact from 2 to 3
2022-04-20 19:20:14 -04:00
rayanamukami
c5b1bb766b Translated using Weblate (Chinese (Traditional))
Currently translated at 8.5% (23 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/zh_Hant/
2022-04-14 02:13:44 -04:00
dependabot[bot]
e0eff168bf Bump actions/upload-artifact from 2 to 3
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2 to 3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-11 16:19:22 +00:00
Floris
1c88173b3e Translated using Weblate (Dutch)
Currently translated at 33.2% (89 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/nl/
2022-03-28 05:38:55 -04:00
3ole
6c2e005cd7 Translated using Weblate (German)
Currently translated at 100.0% (268 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/de/
2022-03-28 05:38:55 -04:00
3ole
08780edcc1 Translated using Weblate (German)
Currently translated at 99.6% (267 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/de/
2022-03-26 21:54:45 -04:00
3ole
d86d785061 Translated using Weblate (German)
Currently translated at 85.8% (230 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/de/
2022-03-26 17:59:18 -04:00
Rhodri
67cf11966b Translated using Weblate (Welsh)
Currently translated at 6.7% (18 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/cy/
2022-03-24 12:11:54 -04:00
mcarlton00
9c96cc4044 Merge pull request #139 from mcarlton00/user-migration
Force login if no saved credentials
2022-03-18 18:15:00 -04:00
Matt
d1e4c1d09f Fix user upgrade path from 0.4.7 2022-03-17 21:33:28 -04:00
mcarlton00
944f3a363f Merge pull request #132 from mcarlton00/movie-recommendations
Fix movie recommendations
2022-03-17 19:34:10 -04:00
mcarlton00
79bd4b1925 Merge pull request #133 from mcarlton00/remove-trakt
Remove trakt integration
2022-03-17 19:33:51 -04:00
mcarlton00
8ea3351eee Merge pull request #134 from mcarlton00/remote-control
Fix remote control
2022-03-17 19:33:37 -04:00
mcarlton00
8d90ca0892 Merge pull request #135 from mcarlton00/non-jellycon-items
Don't throw errors when non-JellyCon content is playing
2022-03-17 19:33:21 -04:00
mcarlton00
4bb7a95e73 Merge pull request #136 from mcarlton00/check-empty-items
Fallback to empty lists instead of failing
2022-03-17 19:31:56 -04:00
mcarlton00
13e553d818 Merge pull request #137 from mcarlton00/widgets-name-format
Make widgets respect episode name format setting
2022-03-17 19:31:43 -04:00
mcarlton00
92b8977797 Merge pull request #138 from jellyfin/dependabot/github_actions/release-drafter/release-drafter-5.19.0
Bump release-drafter/release-drafter from 5.18.1 to 5.19.0
2022-03-17 19:30:13 -04:00
dependabot[bot]
a347255752 Bump release-drafter/release-drafter from 5.18.1 to 5.19.0
Bumps [release-drafter/release-drafter](https://github.com/release-drafter/release-drafter) from 5.18.1 to 5.19.0.
- [Release notes](https://github.com/release-drafter/release-drafter/releases)
- [Commits](https://github.com/release-drafter/release-drafter/compare/v5.18.1...v5.19.0)

---
updated-dependencies:
- dependency-name: release-drafter/release-drafter
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-14 16:13:38 +00:00
Oskari Lavinto
e8b329e688 Translated using Weblate (Finnish)
Currently translated at 100.0% (268 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/fi/
2022-03-13 10:13:33 -04:00
Oskari Lavinto
d77d326950 Added translation using Weblate (Finnish) 2022-03-12 07:17:40 -05:00
Matt
9832e6b011 Make custom widgets respect episode naming setting 2022-03-10 21:43:30 -05:00
Matt
5bae9e72c9 Fallback to empty lists instead of failing 2022-03-10 20:51:49 -05:00
Matt
dba2ee1556 Don't throw errors when non-JellyCon content is playing 2022-03-10 20:36:53 -05:00
Matt
357f28321c Fix remote control URL 2022-03-09 20:52:35 -05:00
Matt
3402ef8d11 Fix websocket url 2022-03-09 20:49:16 -05:00
Matt
643e1d2ac8 Remove trakt integration 2022-03-09 19:35:54 -05:00
Matt
f16ef03927 Fix movie recommendations 2022-03-09 19:30:13 -05:00
mcarlton00
bf54539c39 Merge pull request #131 from mcarlton00/verify-cert
Make requests respect the verify certificate setting
2022-03-09 17:48:28 -05:00
mcarlton00
e71714dbb8 Merge pull request #130 from oddstr13/fix/129
Copy translate_path from Jf4Kodi, fix LazyLogger
2022-03-09 17:47:07 -05:00
Odd Stråbø
a482172be4 Utilize the new kodi_version function 2022-03-09 23:22:26 +01:00
Matt
6fbbd63ad6 Make requests respect the verify certificate setting 2022-03-09 17:11:47 -05:00
Odd Stråbø
6126d617f5 Copy translate_path from Jf4Kodi, fix LazyLogger 2022-03-09 22:51:38 +01:00
mcarlton00
f507efdef7 Merge pull request #127 from jellyfin/dependabot/github_actions/actions/checkout-3
Bump actions/checkout from 2 to 3
2022-03-09 16:04:39 -05:00
mcarlton00
7d4f50add1 Merge pull request #126 from jellyfin/dependabot/github_actions/actions/setup-python-3
Bump actions/setup-python from 2 to 3
2022-03-09 16:04:14 -05:00
mcarlton00
201521a4d8 Merge pull request #124 from mcarlton00/network-rework
Rework the network stack
2022-03-09 15:56:12 -05:00
Matt
8c9289ef1c Fix codeql issues 2022-03-09 15:36:33 -05:00
Matt
8ba1b0b0c0 Remove unused function 2022-03-09 15:26:09 -05:00
Matt
7871422354 Optimize api initialization 2022-03-09 15:25:35 -05:00
mcarlton00
eb19d80b97 Error catching in api requests 2022-03-07 19:36:29 -05:00
mcarlton00
6888cbf7b8 Address flake8 in new api class 2022-03-07 19:28:40 -05:00
mcarlton00
5c1842877d Remove most datamanager references 2022-03-07 19:25:44 -05:00
Thomas Schwery
0bf1d75e0c Translated using Weblate (French)
Currently translated at 79.8% (214 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/fr/
2022-03-07 19:04:17 -05:00
axel
a3e3c33855 Translated using Weblate (French)
Currently translated at 46.2% (124 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/fr/
2022-03-07 12:06:26 -05:00
dependabot[bot]
d5d5e7f74c Bump actions/checkout from 2 to 3
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-07 16:16:54 +00:00
Rhodri
8adfcbe20d Translated using Weblate (Welsh)
Currently translated at 1.8% (5 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/cy/
2022-03-05 13:13:30 -05:00
Jorge IG
f8f79ecc75 Translated using Weblate (Spanish)
Currently translated at 98.5% (264 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/es/
2022-03-05 13:13:29 -05:00
小造xu_zh
289f42392b 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/
2022-03-04 07:13:29 -05:00
Csaba
71cea26ada 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/
2022-03-02 03:13:29 -05:00
WWWesten
5043a8db4e 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/
2022-03-02 03:13:29 -05:00
WWWesten
5d3199e306 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/
2022-03-02 03:13:29 -05:00
dependabot[bot]
37a132164f Bump actions/setup-python from 2 to 3
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 3.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-28 16:18:17 +00:00
Matt
d02cad0fca Fix strings in functions 2022-02-27 23:52:05 -05:00
Matt
3530d158a6 More flake8 fixes 2022-02-27 23:47:31 -05:00
Matt
7eef3a30a8 Fix undefined errors 2022-02-27 23:36:01 -05:00
Matt
36b23d0b19 Reworking the network stack 2022-02-27 22:15:54 -05:00
mcarlton00
816277512d Merge pull request #123 from mcarlton00/manual-user
Fix manual user login
2022-02-26 20:43:53 -05:00
Matt
bec7e06628 Fix manual user login 2022-02-26 20:11:03 -05:00
mcarlton00
798a00ae3a Merge pull request #122 from jellyfin/dependabot/github_actions/release-drafter/release-drafter-5.18.1
Bump release-drafter/release-drafter from 5.17.5 to 5.18.1
2022-02-26 11:20:48 -05:00
Moritz
c852d1e434 Translated using Weblate (German)
Currently translated at 99.5% (234 of 235 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/de/
2022-02-23 15:29:22 -05:00
Ocuriz
08a829ca97 Translated using Weblate (French)
Currently translated at 47.4% (122 of 257 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/fr/
2022-02-14 10:13:22 -05:00
Andrew Lee
1c18753511 Translated using Weblate (Chinese (Traditional))
Currently translated at 8.5% (23 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/zh_Hant/
2022-02-08 22:13:20 -05:00
dependabot[bot]
6b1c3ccc39 Bump release-drafter/release-drafter from 5.17.5 to 5.18.1
Bumps [release-drafter/release-drafter](https://github.com/release-drafter/release-drafter) from 5.17.5 to 5.18.1.
- [Release notes](https://github.com/release-drafter/release-drafter/releases)
- [Commits](https://github.com/release-drafter/release-drafter/compare/v5.17.5...v5.18.1)

---
updated-dependencies:
- dependency-name: release-drafter/release-drafter
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-07 16:15:04 +00:00
Romain Eggermont
bb207f1aee Translated using Weblate (French)
Currently translated at 49.4% (132 of 267 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/fr/
2022-02-02 12:13:18 -05:00
Angel Sandoval
91cc66a8d1 Translated using Weblate (Spanish)
Currently translated at 96.2% (258 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/es/
2022-02-02 12:13:18 -05:00
MrFish1604
cfbfb44307 Translated using Weblate (French)
Currently translated at 17.9% (48 of 267 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/fr/
2022-01-30 13:13:17 -05:00
Mathias Dejerud
8d29ffd7a7 Translated using Weblate (Swedish)
Currently translated at 64.5% (173 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/sv/
2022-01-23 10:28:20 -05:00
Mathias Dejerud
44c5193f54 Added translation using Weblate (Swedish) 2022-01-22 18:11:03 -05:00
mcarlton00
342eec8c26 Merge pull request #118 from jellyfin/dependabot/github_actions/release-drafter/release-drafter-5.17.5
Bump release-drafter/release-drafter from 5.15.0 to 5.17.5
2022-01-19 19:43:01 -05:00
mcarlton00
4973404684 Merge pull request #119 from mcarlton00/user-credentials
Rework user data storage
2022-01-19 19:42:14 -05:00
Matt
e45a59c184 Remove unused import and variable 2022-01-18 12:36:39 -05:00
Matt
6efa62ced2 Ensure deviceId is unique and URL safe 2022-01-17 15:16:51 -05:00
Matt
a316a6e094 Make sure password variable is initialized 2022-01-17 14:08:21 -05:00
Matt
297b25a739 Rework user data storage 2022-01-17 12:17:21 -05:00
dependabot[bot]
3aa4ee3548 Bump release-drafter/release-drafter from 5.15.0 to 5.17.5
Bumps [release-drafter/release-drafter](https://github.com/release-drafter/release-drafter) from 5.15.0 to 5.17.5.
- [Release notes](https://github.com/release-drafter/release-drafter/releases)
- [Commits](https://github.com/release-drafter/release-drafter/compare/v5.15.0...v5.17.5)

---
updated-dependencies:
- dependency-name: release-drafter/release-drafter
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-17 16:15:58 +00:00
WWWesten
e6e3c27371 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/
2022-01-16 06:13:12 -05:00
WWWesten
bdf7298afa 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/
2022-01-16 06:13:12 -05:00
mcarlton00
a81e125a29 Merge pull request #111 from mcarlton00/function-shuffle
Code reorganizing
2022-01-08 13:54:24 -05:00
mcarlton00
a454eb16de Merge pull request #113 from jellyfin/dependabot/github_actions/burnett01/rsync-deployments-5.2
Bump burnett01/rsync-deployments from 5.1 to 5.2
2022-01-08 13:53:54 -05:00
Joel Jose
50512e863d Added translation using Weblate (English (Middle)) 2022-01-08 04:35:25 -05:00
Sebastian Knappe
7ff8e12c4e Translated using Weblate (German)
Currently translated at 99.2% (266 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/de/
2022-01-06 16:13:09 -05:00
Thomas Schwery
a2815081d3 Translated using Weblate (French)
Currently translated at 13.8% (37 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/fr/
2022-01-06 16:13:08 -05:00
LowHigh
e14fa4b7de Translated using Weblate (French)
Currently translated at 13.8% (37 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/fr/
2022-01-06 16:13:08 -05:00
rimasx
4c95caf957 Translated using Weblate (Estonian)
Currently translated at 69.7% (187 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/et/
2022-01-03 13:05:57 -05:00
Tim Gaubel
4a242475c0 Translated using Weblate (German)
Currently translated at 16.0% (43 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/de/
2022-01-03 13:05:56 -05:00
dependabot[bot]
fb1348a306 Bump burnett01/rsync-deployments from 5.1 to 5.2
Bumps [burnett01/rsync-deployments](https://github.com/burnett01/rsync-deployments) from 5.1 to 5.2.
- [Release notes](https://github.com/burnett01/rsync-deployments/releases)
- [Commits](https://github.com/burnett01/rsync-deployments/compare/5.1...5.2)

---
updated-dependencies:
- dependency-name: burnett01/rsync-deployments
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-03 16:12:40 +00:00
rimasx
1b3cea3042 Translated using Weblate (Estonian)
Currently translated at 5.5% (15 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/et/
2021-12-31 04:05:55 -05:00
Matt
264145c4b9 Address flake8 complaints 2021-12-30 18:40:08 -05:00
Matt
daea47bf38 More small tweaks 2021-12-30 18:13:57 -05:00
Matt
2ff5a2e9fc Fix missing translate calls 2021-12-30 17:41:25 -05:00
Matt
fc690abee9 Rename translate function 2021-12-30 17:05:10 -05:00
Matt
157a477a10 Move monitors into a single file 2021-12-30 12:18:08 -05:00
Matt
aa7a412a94 Fix circular dependency on context menu 2021-12-30 09:44:25 -05:00
rimasx
2832c21ffd Added translation using Weblate (Estonian) 2021-12-30 02:56:43 -05:00
Fahad
3751794f81 Translated using Weblate (Arabic)
Currently translated at 66.4% (178 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/ar/
2021-12-29 23:05:55 -05:00
Matt
5c3740b8a9 Function refactoring and reordering 2021-12-29 16:54:05 -05:00
WWWesten
303f049004 Translated using Weblate (French)
Currently translated at 8.5% (23 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/fr/
2021-12-26 04:05:53 -05:00
Thomas Schwery
a52ede2a3d Translated using Weblate (French)
Currently translated at 8.5% (23 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/fr/
2021-12-22 09:05:52 -05:00
mcarlton00
f249eb7cae Merge pull request #109 from mcarlton00/unneeded-code
Remove unused and commented out code
2021-12-21 21:45:54 -05:00
Matt
2d20dc4282 Remove unused and commented out code 2021-12-21 21:22:16 -05:00
LowHigh
95f8dc8eb3 Translated using Weblate (French)
Currently translated at 6.3% (17 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/fr/
2021-12-20 19:05:51 -05:00
Marcin Woliński
1ae1e824c0 Translated using Weblate (Polish)
Currently translated at 100.0% (268 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/pl/
2021-12-13 12:05:50 -05:00
Martin Schmidt
72413ef3b4 Translated using Weblate (German)
Currently translated at 15.2% (41 of 268 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/de/
2021-12-13 12:05:50 -05:00
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
Matt
ec1a5add73 version bump 2020-08-16 21:19:12 -04:00
mcarlton00
b7110a7222 Merge pull request #15 from ltGuillaume/patch-1
Fix annoying typo "defalt"
2020-08-16 19:13:45 -04:00
Guillaume
2e19d2eac1 Fix annoying typo "defalt" 2020-08-16 23:51:56 +02:00
mcarlton00
ad7f388d68 Merge pull request #11 from mcarlton00/improve-logging
Improve logging
2020-07-25 16:01:58 -04:00
Matt
f28c1e7fae Cleanup less helpful logging 2020-07-25 13:36:21 -04:00
Matt
6ce342c0b3 Remove debug statement 2020-07-25 13:36:03 -04:00
Matt
622bdf613c Sanitize server url from logs 2020-07-25 13:35:29 -04:00
Matt
a0efd1087f Copy log handler from JF for Kodi, modify for strings 2020-07-25 01:01:30 -04:00
mcarlton00
9813295fd3 Merge pull request #8 from TrueTechy/server_address_storing_fix
Store full URL instead of component parts. Fixes #1 and Fixes #2
2020-07-19 18:27:32 -04:00
Abby Gourlay
303fdfc9ad Merge branch 'master' of github.com:mcarlton00/jellycon into server_address_storing_fix 2020-07-19 23:19:28 +01:00
mcarlton00
90d88a998c Merge pull request #9 from TrueTechy/issue-template
Create issue templates
2020-07-19 17:59:13 -04:00
Abby
1999e1daf2 Update issue templates 2020-07-19 17:17:45 +01:00
mcarlton00
433f39dd38 Merge pull request #7 from TrueTechy/httplib_replacement
Replace httplib with requests
2020-07-18 23:24:46 -04:00
Abby
110746e859 Update resources/lib/image_server.py spelling error
Co-authored-by: mcarlton00 <mcarlton00@gmail.com>
2020-07-19 04:21:37 +01:00
Abby Gourlay
640860a3af Store full URL instead of component parts. Fixes #1 and Fixes #2 2020-07-19 03:54:15 +01:00
Abby Gourlay
b21fa807db Fixed auth bug 2020-07-19 02:05:34 +01:00
Abby Gourlay
446dd921bf Replace httplib with requests 2020-07-19 01:31:42 +01:00
mcarlton00
594fccd602 Merge pull request #6 from TrueTechy/fix_connection_test
Lowered the maximum connection test size to comply with API limits
2020-07-18 11:31:33 -04:00
Abby Gourlay
47993b612f Removed debugging code 2020-07-18 15:55:24 +01:00
Abby Gourlay
2302f2dbef Lowered the maximum connection test size to comply with API limits 2020-07-18 15:53:21 +01:00
mcarlton00
62469e806a Remove debug statement 2020-07-05 12:10:26 -04:00
mcarlton00
086a068fb1 Fix version string 2020-07-05 12:05:16 -04:00
mcarlton00
97342edee0 Remove bad variable 2020-07-05 12:03:28 -04:00
93 changed files with 21182 additions and 5292 deletions

View File

@@ -1,11 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.jellycon"
name="JellyCon"
version="1.9.100"
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"/>
</requires>
<extension point="xbmc.python.pluginsource" library="default.py">
<provides>video audio</provides>
@@ -20,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>

36
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,36 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**To Reproduce**
<!-- Steps to reproduce the behavior: -->
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Logs**
<!-- Please paste any log errors. -->
**Screenshots**
<!-- If applicable, add screenshots to help explain your problem. -->
**System (please complete the following information):**
- OS: [e.g. Android, Debian, Windows]
- Jellyfin Version: [e.g. 10.0.1]
- Kodi Version: [e.g. 18.3]
- Addon Version: [e.g. 0.1.1]
**Additional context**
<!-- Add any other context about the problem here. -->

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(), 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@v3
- name: Set up Python 3.x
uses: actions/setup-python@v4
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@v3
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@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
queries: +security-and-quality
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.version }}
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

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.20.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@v3
- 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.20.0
if: ${{ matrix.py_version == 'py3' }}
with:
publish: true
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up Python 3.x
uses: actions/setup-python@v4
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@v3
with:
retention-days: 14
name: ${{ matrix.py_version }}-build-artifact
path: |
*.zip
- name: Upload to repo server
uses: burnett01/rsync-deployments@5.2
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.20.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@v3
- name: Set up Python ${{ matrix.py_version }}
uses: actions/setup-python@v4
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@v3
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

View File

@@ -1,6 +1,47 @@
# JellyCon
JellyCon is a lightweight Kodi addon that lets you browse and play media files from your Jellyfin server directly within the Kodi interface.
JellyCon is a lightweight Kodi add-on that lets you browse and play media files directly from your Jellyfin server within the Kodi interface. It can be thought of as a thin frontend for a Jellyfin server.
JellyCon can be used with Movie, TV Show, Music Video, and Music libraries, in addition to viewing LiveTV from the server. It can easily switch between multiple user accounts at will. It's easy to integrate with any customizable Kodi skin with a large collection of custom menus. Media items are populated from the server dynamically, and menu speed will vary based on local device speed.
## Installation
#### 1. Adding the Jellyfin repository
https://jellyfin.org/docs/general/clients/kodi.html#install-add-on-repository
#### 2. Install JellyCon Add-on
- From within Kodi, navigate to "Add-on Browser"
- Select "Install from Repository"
- Choose "Kodi Jellyfin Add-ons", followed by "Video Add-ons"
- Select the JellyCon add-on and choose install
#### 3. Login
- Within a few seconds after the installation you should be prompted for your server details.
- If a Jellyfin server is detected on your local network, it will displayed in a dialog. Otherwise, you will be prompted to enter the URL of your Jellyfin server
- If Quick Connect is enabled in the server, a code will be displayed that you can use to log in via Quick Connect in the web UI or a mobile app.
- If Quick Connect is not enabled, or if you select the "Manual Login" button, you will be able to select a user from the list, or manually login using your username and password.
## Configuration
#### Configuring Home
Many Kodi skins allow for customizing of the home menu with custom nodes and widgets. However, all of these use slightly different layouts and terminology. Rather than a step by step guide, this section serves as an barebones introduction to customizing a skin.
Examples
If you would like a link on the home screen to open a library in your Jellyfin server called "Kid's Movies", you would point the menu item to the path: Add-On -> Video Add-On -> JellyCon -> Jellyfin Libraries -> Kid's Movies -> Create menu item to here.
Beyond just modifying where the home menu headers go, many skins also allow you to use widgets. Widgets help populate the home screen with data, often the posters of media in the selected image. If you would like to display the most recent movies across all of your Jellyfin libraries on the home screen, the path would be: Add-On -> Video Add-On -> JellyCon -> Global Lists -> Movies -> Movies - Recently Added (20) -> Use as widget
Another common use case of widgets would be to display the next available episodes of shows that you may be watching. As above, this can be done both with individual libraries or with all libraries combined:
Add-On -> Video Add-On -> JellyCon -> Jellyfin Libraries -> Anime -> Anime - Next Up (20) -> Use as widget
Add-On -> Video Add-On -> JellyCon -> Global Lists -> TV Shows -> TV Shows - Next Up (20) -> Use as widget
## License

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)

View File

@@ -2,11 +2,11 @@
import xbmcaddon
from resources.lib.simple_logging import SimpleLogging
from resources.lib.lazylogger import LazyLogger
from resources.lib.functions import main_entry_point
from resources.lib.tracking import set_timing_enabled
log = SimpleLogging('default')
log = LazyLogger('default')
settings = xbmcaddon.Addon()
log_timing_data = settings.getSetting('log_timing') == "true"
@@ -16,6 +16,3 @@ if log_timing_data:
log.debug("About to enter mainEntryPoint()")
main_entry_point()
# clear done and exit.
# sys.modules.clear()

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

38
release.yaml Normal file
View File

@@ -0,0 +1,38 @@
version: '0.5.5'
changelog: |-
Bug Fixes
---------
+ Ensure a handle is valid before trying to use it to start playback (#194) @mcarlton00
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,747 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2022-06-05 14:22+0000\n"
"Last-Translator: egymoh <egymoh2@hotmail.com>\n"
"Language-Team: Arabic <https://translate.jellyfin.org/projects/jellycon/"
"jellycon/ar/>\n"
"Language: ar\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n"
"X-Generator: Weblate 4.10.1\n"
msgctxt "#30361"
msgid " - Programs"
msgstr "البرامج"
msgctxt "#30360"
msgid " - Channels"
msgstr "القنوات"
msgctxt "#30359"
msgid "Building full image list"
msgstr "بناء قائمة كاملة بالصور"
msgctxt "#30358"
msgid "Retreiving remote image list"
msgstr "استعادة قائمة الصور البعيدة"
msgctxt "#30357"
msgid "Processing existing image list"
msgstr "معالجة قائمة الصور الموجودة"
msgctxt "#30356"
msgid "Loading existing image list"
msgstr "تحميل قائمة الصور الموجودة"
msgctxt "#30355"
msgid "Kodi Settings->Services->Allow remote control via HTTP"
msgstr "إعدادات Kodi-> الخدمات-> السماح بالتحكم عن بعد عبر HTTP"
msgctxt "#30354"
msgid "Go To Series"
msgstr "انتقل إلى السلسلة"
msgctxt "#30321"
msgid " - Album Artists"
msgstr "فنانو الألبوم"
msgctxt "#30319"
msgid "Music - All Album Artists"
msgstr "كل فناني الألبوم"
#, fuzzy
msgctxt "#30351"
msgid "Music - Recently Played"
msgstr "لعبت مؤخرا"
msgctxt "#30350"
msgid "Music - Recently Added"
msgstr "أضيف مؤخرا"
#, fuzzy
msgctxt "#30349"
msgid " - Recently Played"
msgstr "لعبت مؤخرا"
msgctxt "#30348"
msgid "Add user ratings"
msgstr "إضافة تقييمات المستخدم"
msgctxt "#30347"
msgid "Getting Existing Images"
msgstr "الحصول على الصور الموجودة"
msgctxt "#30346"
msgid "Deleteing Cached Images"
msgstr "حذف الصور التخزين المؤقتة"
msgctxt "#30345"
msgid "Cache Jellyfin server data requests"
msgstr "ذاكرة التخزين المؤقت لطلبات بيانات خادم Jellyfin"
msgctxt "#30344"
msgid "Number of images removed from cache"
msgstr "عدد الصور التي تمت إزالتها من ذاكرة التخزين المؤقت"
msgctxt "#30343"
msgid "Changes Require Kodi Restart"
msgstr "تتطلب التغييرات إعادة تشغيل Kodi"
msgctxt "#30342"
msgid "New content check interval (0 = disabled)"
msgstr "فاصل زمني لفحص المحتوى الجديد (0 = معطل)"
msgctxt "#30341"
msgid "Background image update interval (0 = disabled)"
msgstr "الفاصل الزمني لتحديث صورة الخلفية (0 = معطل)"
msgctxt "#30340"
msgid "Group movies into collections"
msgstr "تجميع الأفلام في مجموعات"
msgctxt "#30339"
msgid "Person"
msgstr "شخص"
msgctxt "#30338"
msgid "Album"
msgstr "البوم"
msgctxt "#30337"
msgid "Song"
msgstr "أغنية"
msgctxt "#30334"
msgid "Use JellyCon context menu"
msgstr "استخدام قائمة سياق JellyCon"
msgctxt "#30332"
msgid "Stop media playback on screensaver activation"
msgstr "إيقاف تشغيل الوسائط عند تنشيط شاشة التوقف"
msgctxt "#30331"
msgid "Movies per page"
msgstr "أفلام لكل صفحة"
msgctxt "#30330"
msgid "Show change user dialog"
msgstr "إظهار مربع حوار تغيير المستخدم"
msgctxt "#30329"
msgid "Screensaver"
msgstr "حافظة الشاشة"
msgctxt "#30328"
msgid "Show empty folders (shows, seasons, collections)"
msgstr "إظهار المجلدات الفارغة (المسلسلات، المواسم ، المجموعات)"
msgctxt "#30327"
msgid "Go To Season"
msgstr "اذهب إلى الموسم"
msgctxt "#30325"
msgid " - Genres"
msgstr "الأنواع"
msgctxt "#30322"
msgid "Auto resume"
msgstr "استئناف تلقائي"
msgctxt "#30320"
msgid " - Albums"
msgstr "ألبومات"
msgctxt "#30318"
msgid "Music - Albums"
msgstr "موسيقى - ألبومات"
msgctxt "#30317"
msgid "Play All"
msgstr "تشغيل الكل"
msgctxt "#30316"
msgid "Connection Error"
msgstr "خطأ في الإتصال"
msgctxt "#30315"
msgid "Suppress notifications for connection errors"
msgstr "قم بإيقاف الأشعارات الخاصة بأخطاء الاتصال"
msgctxt "#30314"
msgid "Play"
msgstr "تشغيل"
msgctxt "#30313"
msgid "Menu"
msgstr "قائمة"
msgctxt "#30312"
msgid "All - "
msgstr "الكل -"
msgctxt "#30311"
msgid "Library - "
msgstr "مكتبة -"
msgctxt "#30310"
msgid "Enable Jellyfin remote control"
msgstr "تفعيل جهاز التحكم عن بعد Jellyfin"
msgctxt "#30309"
msgid "Select Media Source"
msgstr "حدد مصدر الوسائط"
msgctxt "#30308"
msgid "Select Trailer"
msgstr "حدد المقطع الدعائي"
msgctxt "#30307"
msgid "Play Trailer"
msgstr "تشغيل المقطع الدعائي"
msgctxt "#30306"
msgid "Playback starting"
msgstr "بدء التشغيل"
msgctxt "#30305"
msgid "Not Found"
msgstr "لم يتم العثور عليه"
msgctxt "#30304"
msgid "Cached Jellyfin images : "
msgstr "صور Jellyfin المخزنة مؤقتًا:"
msgctxt "#30303"
msgid "Missing Jellyfin images : "
msgstr "صور Jellyfin المفقودة:"
msgctxt "#30302"
msgid "Existing images : "
msgstr "الصور الموجودة:"
msgctxt "#30301"
msgid "Caching Images"
msgstr "صور التخزين المؤقت"
msgctxt "#30300"
msgid "Cache all Jellyfin images as local Kodi images?"
msgstr "تخزين جميع صور Jellyfin مؤقتًا كصور Kodi محلية؟"
msgctxt "#30299"
msgid "Cache Images"
msgstr "‌صور مخبأة"
msgctxt "#30298"
msgid "Deleting Kodi Images"
msgstr "حذف صور Kodi"
msgctxt "#30297"
msgid "Delete unused images?"
msgstr "هل تريد حذف الصور غير المستخدمة؟"
msgctxt "#30296"
msgid "Delete"
msgstr "حذف"
msgctxt "#30295"
msgid "To use this feature you need HTTP control enabled"
msgstr "لاستخدام هذه الميزة تحتاج إلى تمكين تحكم HTTP"
msgctxt "#30294"
msgid "Notice"
msgstr "ملاحظة"
msgctxt "#30293"
msgid "Cache images"
msgstr "صور ذاكرة التخزين المؤقت"
msgctxt "#30292"
msgid "Select Subtitle Stream"
msgstr "حدد الترجمة"
msgctxt "#30291"
msgid "Select Audio Stream"
msgstr "حدد الصوت"
msgctxt "#30290"
msgid "All"
msgstr "الكل"
msgctxt "#30289"
msgid "TV Shows - Genres"
msgstr "مسلسلات - الأنواع"
msgctxt "#30288"
msgid " - Latest"
msgstr "الأحدث"
msgctxt "#30287"
msgid "TV Shows - Latest"
msgstr "مسلسلات - الأحدث"
msgctxt "#30286"
msgid "Movies - Unwatched"
msgstr "أفلام - لم تتم مشاهدتها"
msgctxt "#30285"
msgid " - Unwatched"
msgstr "لم تتم مشاهدتها"
msgctxt "#30283"
msgid "Play Next Episode?"
msgstr "تشغيل الحلقة التالية؟"
msgctxt "#30282"
msgid "No Jellyfin servers detected on your local network."
msgstr "لم يتم اكتشاف خوادم Jellyfin على شبكتك المحلية."
msgctxt "#30281"
msgid "Refresh Cached Images"
msgstr "قم بتحديث الصور المخزنة مؤقتًا"
msgctxt "#30280"
msgid "Missing Title"
msgstr "عنوان مفقود"
msgctxt "#30279"
msgid "TV Shows - Unwatched"
msgstr "مسلسلات - لم تتم مشاهدته"
msgctxt "#30278"
msgid " - Next Up"
msgstr "القادم"
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 إلى المطالبة بالاستئناف على العناصر التي يتم تشغيلها جزئيًا ، "
"ويمكن لـ Kodi أيضًا المطالبة ، وقد يتسبب ذلك في مطالبة مزدوجة. هل تريد إزالة "
"المطالبة المزدوجة؟"
msgctxt "#30276"
msgid "Extra Resume Prompt Detected"
msgstr "تم اكتشاف استئناف إضافي"
msgctxt "#30275"
msgid "Force Transcode"
msgstr "تحويل اجباري"
msgctxt "#30274"
msgid "Delete"
msgstr "حذف"
msgctxt "#30273"
msgid "Unset Favourite"
msgstr "عدم تحديد المفضلة"
msgctxt "#30272"
msgid "Set Favourite"
msgstr "تعيين المفضلة"
msgctxt "#30271"
msgid "Mark Unwatched"
msgstr "حدد لم تتم المشاهدة"
msgctxt "#30270"
msgid "Mark Watched"
msgstr "حدد تمت المشاهدة"
msgctxt "#30269"
msgid "Movies - Random"
msgstr "أفلام - عشوائية"
msgctxt "#30268"
msgid " - Recently Added"
msgstr "أضيف مؤخرا"
msgctxt "#30267"
msgid " - In Progress"
msgstr "في تقدم"
msgctxt "#30266"
msgid "Movies - Pages"
msgstr "أفلام - صفحات"
msgctxt "#30265"
msgid "Episodes - Next Up"
msgstr "الحلقات - التالي"
msgctxt "#30264"
msgid "Episodes - In Progress"
msgstr "الحلقات - قيد التقدم"
msgctxt "#30263"
msgid "Episodes - Recently Added"
msgstr "الحلقات - المضافة حديثًا"
msgctxt "#30262"
msgid "TV Shows - Favorites"
msgstr "مسلسلات - المفضلة"
msgctxt "#30261"
msgid "TV Shows"
msgstr "مسلسلات"
msgctxt "#30259"
msgid "Movies - Favorites"
msgstr "أفلام - المفضلة"
msgctxt "#30258"
msgid "Movies - In Progress"
msgstr "أفلام - قيد التحميل"
msgctxt "#30257"
msgid "Movies - Recently Added"
msgstr "أفلام - أضيفت مؤخرًا"
msgctxt "#30256"
msgid "Movies"
msgstr "أفلام"
msgctxt "#30255"
msgid "TV Shows - A-Z"
msgstr "مسلسلات - من الألف إلى الياء"
msgctxt "#30254"
msgid "Show add-on settings"
msgstr "إظهار إعدادات الوظائف الإضافية"
msgctxt "#30252"
msgid "Movies - A-Z"
msgstr "أفلام - من الألف إلى الياء"
msgctxt "#30251"
msgid "Movies - Genres"
msgstr "أفلام - الأنواع"
msgctxt "#30250"
msgid "Unknown"
msgstr "غير معروف"
msgctxt "#30247"
msgid "Custom Widget Content"
msgstr "محتوى القطعة المخصص"
msgctxt "#30246"
msgid "Search"
msgstr "بحث"
msgctxt "#30241"
msgid "Force transcode mpeg4"
msgstr "فرض تحويل mpeg4"
msgctxt "#30240"
msgid "Force transcode msmpeg4v3 (divx)"
msgstr "فرض تحويل الشفرة msmpeg4v3 (divx)"
msgctxt "#30239"
msgid "Force transcode mpeg2"
msgstr "فرض تحويل mpeg2"
msgctxt "#30238"
msgid "Playback stream options"
msgstr "خيارات التشغيل"
msgctxt "#30237"
msgid "Start from beginning"
msgstr "ابدأ من البداية"
msgctxt "#30236"
msgid "Force transcode h265 (hevc)"
msgstr "فرض تحويل الشفرة H265 (HEVC)"
msgctxt "#30235"
msgid "Episodes"
msgstr "حلقات"
msgctxt "#30231"
msgid "Movies"
msgstr "أفلام"
msgctxt "#30229"
msgid "TV Shows"
msgstr "مسلسلات"
msgctxt "#30224"
msgid "Interaction"
msgstr "تفاعل"
msgctxt "#30223"
msgid "Page Size and Filtering"
msgstr "حجم الصفحة والتصفية"
msgctxt "#30222"
msgid "Item Layout"
msgstr "تخطيط العنصر"
msgctxt "#30220"
msgid "Prompt to delete movie after %"
msgstr "مطالبة بحذف الفيلم بعد٪"
msgctxt "#30219"
msgid " - Prompt before play"
msgstr "الموافقه قبل التشغيل"
msgctxt "#30218"
msgid "Play next episode after %"
msgstr "تشغيل الحلقة التالية بعد٪"
msgctxt "#30217"
msgid "Prompt to delete episode after %"
msgstr "مطالبة بحذف الحلقة بعد٪"
msgctxt "#30216"
msgid "Item Details"
msgstr "تفاصيل العنصر"
msgctxt "#30215"
msgid "On playback stop (100% = disabled)"
msgstr "توقف التشغيل (100٪ = معطل)"
#, fuzzy
msgctxt "#30214"
msgid "Events"
msgstr "أحداث"
msgctxt "#30213"
msgid "Video force 8 bit"
msgstr "قوة الفيديو 8 بت"
msgctxt "#30212"
msgid "Video max width"
msgstr "أقصى عرض للفيديو"
msgctxt "#30211"
msgid "Transcode options"
msgstr "خيارات التحويل"
msgctxt "#30209"
msgid "File direct path"
msgstr "مسار الملف المباشر"
msgctxt "#30207"
msgid "Playback"
msgstr "تشغيل"
msgctxt "#30206"
msgid "Playback type"
msgstr "نوع التشغيل"
msgctxt "#30201"
msgid "Unable to connect to server"
msgstr "غير قادر على الإتصال بالسيرفر"
msgctxt "#30200"
msgid "URL error"
msgstr "خطأ في الرابط"
msgctxt "#30183"
msgid "Include people"
msgstr "اشمل الاشخاص"
msgctxt "#30182"
msgid "Include media stream info"
msgstr "تضمين معلومات الوسائط"
msgctxt "#30181"
msgid "Include plot"
msgstr "تضمين المقدمة"
msgctxt "#30180"
msgid "Select User"
msgstr "اختر المستخدم"
msgctxt "#30169"
msgid "Address: "
msgstr "العنوان:"
msgctxt "#30167"
msgid "Selected Server Address"
msgstr "عنوان الخادم المحدد"
msgctxt "#30166"
msgid "Select Server"
msgstr "حدد الخادم"
msgctxt "#30163"
msgid "Add (cc) if subtitle is available"
msgstr "أضف (cc) إذا كان العنوان الفرعي متاحًا"
msgctxt "#30139"
msgid "No Media Type Set"
msgstr "لم يتم تعيين نوع الوسائط"
msgctxt "#30135"
msgid "Error"
msgstr "خطأ"
msgctxt "#30126"
msgid "Processing Item : "
msgstr "عنصر المعالجة:"
msgctxt "#30125"
msgid "Done"
msgstr "انتهئ"
msgctxt "#30121"
msgid "On resume"
msgstr "عند الاستئناف"
msgctxt "#30120"
msgid "Show load progress"
msgstr "إظهار تقدم التحميل"
msgctxt "#30118"
msgid "Add resume percent to names"
msgstr "إضافة نسبة الاستئناف إلى الأسماء"
msgctxt "#30114"
msgid "Jump back seconds"
msgstr "الرجوع الى الوراء بثواني"
msgctxt "#30113"
msgid "Retrieving Data"
msgstr "استرجاع البيانات"
msgctxt "#30112"
msgid "Loading Content"
msgstr "تحميل المحتوى"
msgctxt "#30111"
msgid "Services"
msgstr "خدمات"
msgctxt "#30110"
msgid "Interface"
msgstr "واجهه المستخدم"
msgctxt "#30092"
msgid "Warning: This action will delete the media files from the server."
msgstr "تحذير: سيؤدي هذا الإجراء إلى حذف ملفات الوسائط من الخادم."
msgctxt "#30091"
msgid "Confirm delete?"
msgstr "تأكيد الحذف؟"
msgctxt "#30063"
msgid "N/A"
msgstr "غير متاح"
msgctxt "#30053"
msgid "Waiting for server to delete"
msgstr "في انتظار حذف الخادم"
msgctxt "#30052"
msgid "Deleting"
msgstr "حذف"
msgctxt "#30045"
msgid "Username not found"
msgstr "اسم المستخدم لم يتم العثور عليه"
msgctxt "#30044"
msgid "Incorrect Username/Password"
msgstr "اسم المستخدم / كلمة المرور غير صحيحة"
msgctxt "#30027"
msgid "Enable debug logging"
msgstr "تفعيل تسجيل التصحيح"
msgctxt "#30026"
msgid "Widget item select action"
msgstr "حدد الإجراء القطعة"
msgctxt "#30025"
msgid "Password:"
msgstr "كلمة المرور:"
msgctxt "#30024"
msgid "Username:"
msgstr "اسم المستخدم:"
msgctxt "#30023"
msgid "Hide unwatched episode details"
msgstr "إخفاء تفاصيل الحلقة التي لم تتم مشاهدتها"
msgctxt "#30022"
msgid "Advanced"
msgstr "متقدم"
msgctxt "#30021"
msgid "Show all episodes item"
msgstr "إظهار جميع الحلقات"
msgctxt "#30020"
msgid "Flatten single season"
msgstr "تسطيح موسم واحد"
msgctxt "#30019"
msgid "Filtered episode name format"
msgstr "تنسيق اسم الحلقة"
msgctxt "#30018"
msgid "Number of items to show in filtered lists"
msgstr "عدد العناصر المراد إظهارها في القوائم المصفاة"
msgctxt "#30017"
msgid "Show connected clients"
msgstr "إظهار العملاء المتصلين"
msgctxt "#30016"
msgid "Device display name"
msgstr "عرض اسم الجهاز"
msgctxt "#30015"
msgid "Log timing data"
msgstr "تسجيل بيانات التوقيت"
msgctxt "#30014"
msgid "Jellyfin"
msgstr "جيليفن"
msgctxt "#30012"
msgid "[Change user]"
msgstr "[تغيير المستخدم]"
msgctxt "#30011"
msgid "[Detect local server]"
msgstr "[كشف الخادم المحلي]"
msgctxt "#30010"
msgid "Number of performance profiles to capture"
msgstr "عدد ملفات تعريف الأداء المطلوب التقاطها"
msgctxt "#30008"
msgid "Samba password"
msgstr "كلمة المرور (سامبا)"
msgctxt "#30007"
msgid "Samba username"
msgstr "اسم المستخدم (سامبا)"
msgctxt "#30006"
msgid "Password"
msgstr "كلمة المرور"
msgctxt "#30005"
msgid "Username"
msgstr "اسم المستخدم"
msgctxt "#30003"
msgid "Verify HTTPS certificate"
msgstr "تحقق من شهادة HTTPS"
msgctxt "#30001"
msgid "Port"
msgstr "شبكة"
msgctxt "#30000"
msgid "Host"
msgstr "أستضافة"
msgctxt "#30208"
msgid "Max stream bitrate (Kbits)"
msgstr "الحد الأقصى لمعدل نقل البيانات (Kbps)"

View File

@@ -0,0 +1,60 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2022-06-09 21:22+0000\n"
"Last-Translator: DJSweder <djsweder@gmail.com>\n"
"Language-Team: Czech <https://translate.jellyfin.org/projects/jellycon/"
"jellycon/cs/>\n"
"Language: cs\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>=2 && n<=4) ? 1 : 2;\n"
"X-Generator: Weblate 4.10.1\n"
msgctxt "#30016"
msgid "Device display name"
msgstr "Zobrazované jméno zařízení"
msgctxt "#30015"
msgid "Log timing data"
msgstr "Zaznamenat časové údaje"
msgctxt "#30014"
msgid "Jellyfin"
msgstr "Jellyfin"
msgctxt "#30012"
msgid "[Change user]"
msgstr "[Změnit uživatele]"
msgctxt "#30011"
msgid "[Detect local server]"
msgstr "[Zjistit místní server]"
msgctxt "#30008"
msgid "Samba password"
msgstr "Samba heslo"
msgctxt "#30007"
msgid "Samba username"
msgstr "Samba uživatelské jméno"
msgctxt "#30006"
msgid "Password"
msgstr "Heslo"
msgctxt "#30005"
msgid "Username"
msgstr "Uživatelské jméno"
msgctxt "#30003"
msgid "Verify HTTPS certificate"
msgstr "Ověř HTTPS certifikát"
msgctxt "#30001"
msgid "Port"
msgstr "Port"
msgctxt "#30000"
msgid "Host"
msgstr "Hostitel"

View File

@@ -0,0 +1,418 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2022-05-08 11:13+0000\n"
"Last-Translator: Rhodri <rhodrilld@gmail.com>\n"
"Language-Team: Welsh <https://translate.jellyfin.org/projects/jellycon/"
"jellycon/cy/>\n"
"Language: cy\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=6; plural=(n==0) ? 0 : (n==1) ? 1 : (n==2) ? 2 : "
"(n==3) ? 3 :(n==6) ? 4 : 5;\n"
"X-Generator: Weblate 4.10.1\n"
msgctxt "#30006"
msgid "Password"
msgstr "Cyfrinair"
msgctxt "#30005"
msgid "Username"
msgstr "Enw defnyddiwr"
msgctxt "#30003"
msgid "Verify HTTPS certificate"
msgstr "Gwiriwch dystysgrif HTTPS"
msgctxt "#30001"
msgid "Port"
msgstr "Port"
msgctxt "#30000"
msgid "Host"
msgstr "Gwesteiwr"
msgctxt "#30021"
msgid "Show all episodes item"
msgstr "Dangos bob pennod"
msgctxt "#30020"
msgid "Flatten single season"
msgstr "Gwastadu tymor sengl"
msgctxt "#30019"
msgid "Filtered episode name format"
msgstr "Fformat enw pennod wedi'i hidlo"
msgctxt "#30018"
msgid "Number of items to show in filtered lists"
msgstr "Nifer yr eitemau i'w dangos mewn rhestrau wedi'u hidlo"
msgctxt "#30017"
msgid "Show connected clients"
msgstr "Dangos cleients cysylltiedig"
msgctxt "#30016"
msgid "Device display name"
msgstr "Enw arddangos dyfais"
msgctxt "#30015"
msgid "Log timing data"
msgstr "Logio data amseru"
msgctxt "#30014"
msgid "Jellyfin"
msgstr "Jellyfin"
msgctxt "#30012"
msgid "[Change user]"
msgstr "[Newid defnyddiwr]"
msgctxt "#30011"
msgid "[Detect local server]"
msgstr "[Canfod gweinydd lleol]"
msgctxt "#30010"
msgid "Number of performance profiles to capture"
msgstr "Nifer y proffiliau perfformiad i'w gynhyrchu"
msgctxt "#30008"
msgid "Samba password"
msgstr "Cyfrinair Samba"
msgctxt "#30007"
msgid "Samba username"
msgstr "Enw defnyddiwr Samba"
msgctxt "#30169"
msgid "Address: "
msgstr "Cyfeiriad:"
msgctxt "#30167"
msgid "Selected Server Address"
msgstr "Cyfeiriad Gweinydd a ddewiswyd"
msgctxt "#30166"
msgid "Select Server"
msgstr "Dewis Gweinydd"
msgctxt "#30135"
msgid "Error"
msgstr "Gwall"
msgctxt "#30126"
msgid "Processing Item : "
msgstr "Prosesi'r eitem:"
msgctxt "#30125"
msgid "Done"
msgstr "Wedi gorffen"
msgctxt "#30121"
msgid "On resume"
msgstr "Ar barhad"
msgctxt "#30113"
msgid "Retrieving Data"
msgstr "Adalw Data"
msgctxt "#30112"
msgid "Loading Content"
msgstr "Llwytho Cynnwys"
msgctxt "#30111"
msgid "Services"
msgstr "Gwasanaethau"
msgctxt "#30092"
msgid "Warning: This action will delete the media files from the server."
msgstr ""
"Rhybudd: Bydd y weithred hon yn dileu'r ffeiliau cyfryngau o'r gweinydd."
msgctxt "#30091"
msgid "Confirm delete?"
msgstr "Cadarnhau dileu?"
msgctxt "#30063"
msgid "N/A"
msgstr "N/A"
msgctxt "#30053"
msgid "Waiting for server to delete"
msgstr "Aros i'r gweinydd i'w ddileu"
msgctxt "#30052"
msgid "Deleting"
msgstr "Wrthi'n dileu"
msgctxt "#30045"
msgid "Username not found"
msgstr "Enw defnyddiwr ddim yn bodoli"
msgctxt "#30044"
msgid "Incorrect Username/Password"
msgstr "Enw defnyddiwr/Cyfrinair anghywir"
msgctxt "#30025"
msgid "Password:"
msgstr "Cyfrinair:"
msgctxt "#30024"
msgid "Username:"
msgstr "Enw defnyddiwr:"
msgctxt "#30022"
msgid "Advanced"
msgstr "Uwchraddol"
msgctxt "#30216"
msgid "Item Details"
msgstr "Gwybodaeth Eitem"
msgctxt "#30200"
msgid "URL error"
msgstr "gwall URL"
msgctxt "#30180"
msgid "Select User"
msgstr "Dewis Defnyddiwr"
msgctxt "#30429"
msgid "Genre"
msgstr "Genre"
msgctxt "#30430"
msgid "Label"
msgstr "Label"
msgctxt "#30426"
msgid "Title"
msgstr "Teitl"
msgctxt "#30425"
msgid "Year"
msgstr "Blwyddyn"
msgctxt "#30401"
msgid "Info"
msgstr "Gwybodaeth"
msgctxt "#30399"
msgid "Hide"
msgstr "Cuddio"
msgctxt "#30392"
msgid "HTTPS"
msgstr "HTTPS"
msgctxt "#30391"
msgid "HTTP"
msgstr "HTTP"
msgctxt "#30380"
msgid "Never"
msgstr "Byth"
msgctxt "#30339"
msgid "Person"
msgstr "Person"
msgctxt "#30338"
msgid "Album"
msgstr "Albwm"
msgctxt "#30337"
msgid "Song"
msgstr "Cân"
msgctxt "#30314"
msgid "Play"
msgstr "Chwarae"
msgctxt "#30313"
msgid "Menu"
msgstr "Dewislen"
msgctxt "#30296"
msgid "Delete"
msgstr "Dileu"
msgctxt "#30274"
msgid "Delete"
msgstr "Dileu"
msgctxt "#30256"
msgid "Movies"
msgstr "Ffilmiau"
msgctxt "#30250"
msgid "Unknown"
msgstr "Anhysbys"
msgctxt "#30246"
msgid "Search"
msgstr "Chwilio"
msgctxt "#30235"
msgid "Episodes"
msgstr "Pennodau"
msgctxt "#30231"
msgid "Movies"
msgstr "Ffilmiau"
msgctxt "#30218"
msgid "Play next episode after %"
msgstr "Chwarae'r bennod nesaf ar ôl %"
msgctxt "#30214"
msgid "Events"
msgstr "Digwyddiadau"
msgctxt "#30211"
msgid "Transcode options"
msgstr "Opsiynau trawsgodio"
msgctxt "#30201"
msgid "Unable to connect to server"
msgstr "Methu cysylltu â'r gweinydd"
msgctxt "#30183"
msgid "Include people"
msgstr "Cynnwys pobl"
msgctxt "#30181"
msgid "Include plot"
msgstr "Cynnwys plot"
msgctxt "#30163"
msgid "Add (cc) if subtitle is available"
msgstr "Ychwanegu (cc) os oes is-deitlau ar gael"
msgctxt "#30114"
msgid "Jump back seconds"
msgstr "Neidio yn ôl eiliadau"
msgctxt "#30110"
msgid "Interface"
msgstr "Rhyngwyneb"
msgctxt "#30027"
msgid "Enable debug logging"
msgstr "Actifadu logio dadfygio"
msgctxt "#30023"
msgid "Hide unwatched episode details"
msgstr "Cuddio manylion penodau heb eu gwylio"
msgctxt "#30381"
msgid "More than one"
msgstr "Mwy na un"
msgctxt "#30362"
msgid " - Recordings"
msgstr "- Recordiadau"
msgctxt "#30361"
msgid " - Programs"
msgstr "- Rhaglenni"
msgctxt "#30360"
msgid " - Channels"
msgstr "- Sianeli"
msgctxt "#30325"
msgid " - Genres"
msgstr "- Genres"
msgctxt "#30316"
msgid "Connection Error"
msgstr "Gwall cysylltiad"
msgctxt "#30312"
msgid "All - "
msgstr "Pobeth -"
msgctxt "#30311"
msgid "Library - "
msgstr "Llyfrgell -"
msgctxt "#30294"
msgid "Notice"
msgstr "Rhybydd"
msgctxt "#30290"
msgid "All"
msgstr "Pobeth"
msgctxt "#30288"
msgid " - Latest"
msgstr "- Diweddaraf"
msgctxt "#30285"
msgid " - Unwatched"
msgstr "- Heb ei wylio"
msgctxt "#30283"
msgid "Play Next Episode?"
msgstr "Chwarae'r bennod nesaf?"
msgctxt "#30280"
msgid "Missing Title"
msgstr "Teitl ar goll"
msgctxt "#30278"
msgid " - Next Up"
msgstr "- Nesaf"
msgctxt "#30286"
msgid "Movies - Unwatched"
msgstr "Ffilmiau - Heb ei wylio"
msgctxt "#30252"
msgid "Movies - A-Z"
msgstr "Ffilmiau - A-Z"
msgctxt "#30251"
msgid "Movies - Genres"
msgstr "Ffilmiau - Genres"
msgctxt "#30259"
msgid "Movies - Favorites"
msgstr "Ffilmiau - Ffefrynnau"
msgctxt "#30415"
msgid " - Favorite Collections"
msgstr "- Hoff Gasgliadau"
msgctxt "#30414"
msgid " - Favorites"
msgstr "- Ffefrynnau"
msgctxt "#30289"
msgid "TV Shows - Genres"
msgstr "Rhaglenni teledu - Genres"
msgctxt "#30287"
msgid "TV Shows - Latest"
msgstr "Rhaglenni teledu - Diweddaraf"
msgctxt "#30279"
msgid "TV Shows - Unwatched"
msgstr "Rhaglenni teledu - Heb ei wylio"
msgctxt "#30262"
msgid "TV Shows - Favorites"
msgstr "Rhaglenni teledu - Ffefrynnau"
msgctxt "#30261"
msgid "TV Shows"
msgstr "Rhaglenni teledu"
msgctxt "#30255"
msgid "TV Shows - A-Z"
msgstr "Rhaglenni teledu - A-Z"
msgctxt "#30229"
msgid "TV Shows"
msgstr "Rhaglenni teledu"

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"

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"

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,763 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2022-01-03 18:05+0000\n"
"Last-Translator: rimasx <riks_12@hot.ee>\n"
"Language-Team: Estonian <https://translate.jellyfin.org/projects/jellycon/"
"jellycon/et/>\n"
"Language: et\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 "#30021"
msgid "Show all episodes item"
msgstr "Kuva kõik episoodid"
msgctxt "#30018"
msgid "Number of items to show in filtered lists"
msgstr "Filtreeritud loendites kuvatavate üksuste arv"
msgctxt "#30017"
msgid "Show connected clients"
msgstr "Kuva ühendatud kliendid"
msgctxt "#30016"
msgid "Device display name"
msgstr "Seadme kuvatav nimi"
msgctxt "#30015"
msgid "Log timing data"
msgstr "Logi ajakirjed"
msgctxt "#30014"
msgid "Jellyfin"
msgstr "Jellyfin"
msgctxt "#30012"
msgid "[Change user]"
msgstr "[Muuda kasutajat]"
msgctxt "#30011"
msgid "[Detect local server]"
msgstr "[Tuvasta kohalik server]"
msgctxt "#30010"
msgid "Number of performance profiles to capture"
msgstr "Jäädvustavate jõudlusprofiilide arv"
msgctxt "#30008"
msgid "Samba password"
msgstr "Samba parool"
msgctxt "#30007"
msgid "Samba username"
msgstr "Samba kasutajanimi"
msgctxt "#30006"
msgid "Password"
msgstr "Parool"
msgctxt "#30005"
msgid "Username"
msgstr "Kasutajanimi"
msgctxt "#30003"
msgid "Verify HTTPS certificate"
msgstr "Kinnita HTTPS sertifikaat"
msgctxt "#30001"
msgid "Port"
msgstr "Port"
msgctxt "#30366"
msgid "Manually enter user details"
msgstr "Sisesta kasutaja andmed käsitsi"
msgctxt "#30241"
msgid "Force transcode mpeg4"
msgstr "Transkoodi jõuga mpeg4"
msgctxt "#30240"
msgid "Force transcode msmpeg4v3 (divx)"
msgstr "Transkoodi jõuga msmpeg4v3 (divx)"
msgctxt "#30365"
msgid "Manual Login"
msgstr "Käsitsi sisselogimine"
msgctxt "#30364"
msgid "Do you want to save the password?"
msgstr "Kas soovid parooli salvestada?"
msgctxt "#30363"
msgid "Save Password?"
msgstr "Kas salvestada parool?"
msgctxt "#30362"
msgid " - Recordings"
msgstr "- salvestised"
msgctxt "#30361"
msgid " - Programs"
msgstr "- saated"
msgctxt "#30360"
msgid " - Channels"
msgstr "- kanalid"
msgctxt "#30359"
msgid "Building full image list"
msgstr "Täieliku piltide loendi koostamine"
msgctxt "#30358"
msgid "Retreiving remote image list"
msgstr "Kaugpiltide loendi vastuvõtt"
msgctxt "#30357"
msgid "Processing existing image list"
msgstr "Olemasoleva piltide loendi töötlemine"
msgctxt "#30356"
msgid "Loading existing image list"
msgstr "Olemasoleva piltide loendi laadimine"
msgctxt "#30355"
msgid "Kodi Settings->Services->Allow remote control via HTTP"
msgstr "Kodi seaded-> Teenused-> Luba kaugjuhtimine HTTP kaudu"
msgctxt "#30354"
msgid "Go To Series"
msgstr "Ava seriaal"
msgctxt "#30353"
msgid " - Frequently Played"
msgstr "- sageli esitatud"
msgctxt "#30321"
msgid " - Album Artists"
msgstr " albumi esitajad"
msgctxt "#30319"
msgid "Music - All Album Artists"
msgstr "Muusika kõik albumi esitajad"
msgctxt "#30352"
msgid "Music - Frequently Played"
msgstr "Muusika sageli esitatud"
msgctxt "#30327"
msgid "Go To Season"
msgstr "Ava hooaeg"
msgctxt "#30209"
msgid "File direct path"
msgstr "Faili otserada"
msgctxt "#30263"
msgid "Episodes - Recently Added"
msgstr "Episoodid viimati lisatud"
msgctxt "#30257"
msgid "Movies - Recently Added"
msgstr "Filmid viimati lisatud"
msgctxt "#30349"
msgid " - Recently Played"
msgstr "- viimati esitatud"
msgctxt "#30268"
msgid " - Recently Added"
msgstr "- viimati lisatud"
msgctxt "#30351"
msgid "Music - Recently Played"
msgstr "Muusika viimati esitatud"
msgctxt "#30350"
msgid "Music - Recently Added"
msgstr "Muusika viimati lisatud"
msgctxt "#30348"
msgid "Add user ratings"
msgstr "Lisa kasutajahinded"
msgctxt "#30347"
msgid "Getting Existing Images"
msgstr "Olemasolevate piltide hankimine"
msgctxt "#30346"
msgid "Deleteing Cached Images"
msgstr "Vahemällu salvestatud piltide kustutamine"
msgctxt "#30345"
msgid "Cache Jellyfin server data requests"
msgstr "Salvesta Jellyfin serveri andmepäringud vahemällu"
msgctxt "#30344"
msgid "Number of images removed from cache"
msgstr "Vahemälust eemaldatud piltide arv"
msgctxt "#30343"
msgid "Changes Require Kodi Restart"
msgstr "Muudatused nõuavad Kodi taaskäivitamist"
msgctxt "#30342"
msgid "New content check interval (0 = disabled)"
msgstr "Uus sisu kontrollimise intervall (0 = keelatud)"
msgctxt "#30341"
msgid "Background image update interval (0 = disabled)"
msgstr "Taustapildi värskendamise intervall (0 = keelatud)"
msgctxt "#30340"
msgid "Group movies into collections"
msgstr "Rühmita filmid kogumikesse"
msgctxt "#30339"
msgid "Person"
msgstr "Isik"
msgctxt "#30338"
msgid "Album"
msgstr "Album"
msgctxt "#30337"
msgid "Song"
msgstr "Lugu"
msgctxt "#30334"
msgid "Use JellyCon context menu"
msgstr "Kasuta JellyCon kontekstimenüüd"
msgctxt "#30333"
msgid "Cache artwork in the background"
msgstr "Salvesta pildid vahemällu taustal"
msgctxt "#30332"
msgid "Stop media playback on screensaver activation"
msgstr "Peata meedia taasesitus ekraanisäästja aktiveerimisel"
msgctxt "#30331"
msgid "Movies per page"
msgstr "Filme lehel"
msgctxt "#30330"
msgid "Show change user dialog"
msgstr "Kuva kasutaja muutmise dialoog"
msgctxt "#30329"
msgid "Screensaver"
msgstr "Ekraanisäästja"
msgctxt "#30328"
msgid "Show empty folders (shows, seasons, collections)"
msgstr "Kuva tühjad kaustad (saated, hooajad, kogumikud)"
msgctxt "#30325"
msgid " - Genres"
msgstr "- žanrid"
msgctxt "#30322"
msgid "Auto resume"
msgstr "Automaatne jätkamine"
msgctxt "#30320"
msgid " - Albums"
msgstr "- albumid"
msgctxt "#30318"
msgid "Music - Albums"
msgstr "Muusika albumid"
msgctxt "#30317"
msgid "Play All"
msgstr "Esita kõik"
msgctxt "#30316"
msgid "Connection Error"
msgstr "Ühenduse viga"
msgctxt "#30315"
msgid "Suppress notifications for connection errors"
msgstr "Lülita ühenduse veateavitused välja"
msgctxt "#30314"
msgid "Play"
msgstr "Esita"
msgctxt "#30313"
msgid "Menu"
msgstr "Menüü"
msgctxt "#30312"
msgid "All - "
msgstr "Kõik -"
msgctxt "#30311"
msgid "Library - "
msgstr "Meediakogu -"
msgctxt "#30310"
msgid "Enable Jellyfin remote control"
msgstr "Luba Jellyfini kaugjuhtimine"
msgctxt "#30309"
msgid "Select Media Source"
msgstr "Vali meedia allikas"
msgctxt "#30308"
msgid "Select Trailer"
msgstr "Vali treiler"
msgctxt "#30307"
msgid "Play Trailer"
msgstr "Esita treiler"
msgctxt "#30306"
msgid "Playback starting"
msgstr "Taasesitus algab"
msgctxt "#30305"
msgid "Not Found"
msgstr "Ei leitud"
msgctxt "#30304"
msgid "Cached Jellyfin images : "
msgstr "Vahemällu salvestatud Jellyfini pildid:"
msgctxt "#30303"
msgid "Missing Jellyfin images : "
msgstr "Puuduvad Jellyfini pildid:"
msgctxt "#30302"
msgid "Existing images : "
msgstr "Olemasolevad pildid:"
msgctxt "#30301"
msgid "Caching Images"
msgstr "Piltide vahemällu salvestamine"
msgctxt "#30300"
msgid "Cache all Jellyfin images as local Kodi images?"
msgstr "Kas salvestada kõik Jellyfini pildid vahemällu kohalike Kodi piltidena?"
msgctxt "#30299"
msgid "Cache Images"
msgstr "Pildid vahemällu"
msgctxt "#30298"
msgid "Deleting Kodi Images"
msgstr "Kodi piltide kustutamine"
msgctxt "#30297"
msgid "Delete unused images?"
msgstr "Kas kustutada kasutamata pildid?"
msgctxt "#30296"
msgid "Delete"
msgstr "Kustuta"
msgctxt "#30295"
msgid "To use this feature you need HTTP control enabled"
msgstr "Selle funktsiooni kasutamiseks peab HTTP juhtimine olema lubatud"
msgctxt "#30294"
msgid "Notice"
msgstr "Märkus"
msgctxt "#30293"
msgid "Cache images"
msgstr "Pildid vahemällu"
msgctxt "#30292"
msgid "Select Subtitle Stream"
msgstr "Vali subtiitrirada"
msgctxt "#30291"
msgid "Select Audio Stream"
msgstr "Vali heliriba"
msgctxt "#30290"
msgid "All"
msgstr "Kõik"
msgctxt "#30289"
msgid "TV Shows - Genres"
msgstr "Sarjad - žanrid"
msgctxt "#30288"
msgid " - Latest"
msgstr "- uued"
msgctxt "#30287"
msgid "TV Shows - Latest"
msgstr "Sarjad - uued"
msgctxt "#30286"
msgid "Movies - Unwatched"
msgstr "Filmid - vaatamata"
msgctxt "#30285"
msgid " - Unwatched"
msgstr "- vaatamata"
msgctxt "#30283"
msgid "Play Next Episode?"
msgstr "Kas esitada järgmine episood?"
msgctxt "#30282"
msgid "No Jellyfin servers detected on your local network."
msgstr "Kohtvõrgus ei tuvastatud Jellyfini servereid."
msgctxt "#30281"
msgid "Refresh Cached Images"
msgstr "Värskenda vahemällu salvestatud pilte"
msgctxt "#30280"
msgid "Missing Title"
msgstr "Puuduv pealkiri"
msgctxt "#30279"
msgid "TV Shows - Unwatched"
msgstr "Sarjad - vaatamata"
msgctxt "#30278"
msgid " - Next Up"
msgstr "- järgmisena"
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 peab küsima jätkamist osaliselt esitatud üksuste puhul. Kodi võib "
"samuti küsida ja see võib põhjustada päringu. Kas soovid topeltpäringu "
"eemaldada?"
msgctxt "#30276"
msgid "Extra Resume Prompt Detected"
msgstr "Tuvastati täiendav jätkamise päring"
msgctxt "#30275"
msgid "Force Transcode"
msgstr "Sunnitud transkoodimine"
msgctxt "#30274"
msgid "Delete"
msgstr "Kustuta"
msgctxt "#30272"
msgid "Set Favourite"
msgstr "Määra lemmikuks"
msgctxt "#30271"
msgid "Mark Unwatched"
msgstr "Märgi mittevaadatuks"
msgctxt "#30270"
msgid "Mark Watched"
msgstr "Märgi vaadatuks"
msgctxt "#30269"
msgid "Movies - Random"
msgstr "Filmid juhuslikud"
msgctxt "#30267"
msgid " - In Progress"
msgstr "- pooleli"
msgctxt "#30266"
msgid "Movies - Pages"
msgstr "Filmid lehed"
msgctxt "#30265"
msgid "Episodes - Next Up"
msgstr "Episoodid järgmine"
msgctxt "#30264"
msgid "Episodes - In Progress"
msgstr "Episoodid pooleli"
msgctxt "#30262"
msgid "TV Shows - Favorites"
msgstr "Sarjad - lemmikud"
msgctxt "#30261"
msgid "TV Shows"
msgstr "Sarjad"
msgctxt "#30260"
msgid "BoxSets"
msgstr "Kogumikud"
msgctxt "#30259"
msgid "Movies - Favorites"
msgstr "Filmid lemmikud"
msgctxt "#30258"
msgid "Movies - In Progress"
msgstr "Filmid pooleli"
msgctxt "#30256"
msgid "Movies"
msgstr "Filmid"
msgctxt "#30255"
msgid "TV Shows - A-Z"
msgstr "Sarjad - A-Ü"
msgctxt "#30254"
msgid "Show add-on settings"
msgstr "Kuva lisamooduli seaded"
msgctxt "#30252"
msgid "Movies - A-Z"
msgstr "Filmid A-Ü"
msgctxt "#30251"
msgid "Movies - Genres"
msgstr "Filmid žanrid"
msgctxt "#30250"
msgid "Unknown"
msgstr "Teadmata"
msgctxt "#30246"
msgid "Search"
msgstr "Otsi"
msgctxt "#30239"
msgid "Force transcode mpeg2"
msgstr "Transkoodi jõuga mpeg2"
msgctxt "#30238"
msgid "Playback stream options"
msgstr "Taasesituse striimimise valikud"
msgctxt "#30237"
msgid "Start from beginning"
msgstr "Alusta algusest"
msgctxt "#30236"
msgid "Force transcode h265 (hevc)"
msgstr "Transkoodi jõuga h265 (hevc)"
msgctxt "#30235"
msgid "Episodes"
msgstr "Episoodid"
msgctxt "#30231"
msgid "Movies"
msgstr "Filmid"
msgctxt "#30229"
msgid "TV Shows"
msgstr "Sarjad"
msgctxt "#30223"
msgid "Page Size and Filtering"
msgstr "Lehekülje suurus ja filtreerimine"
msgctxt "#30222"
msgid "Item Layout"
msgstr "Üksuse paigutus"
msgctxt "#30220"
msgid "Prompt to delete movie after %"
msgstr "Paku filmi kustutamist pärast %"
msgctxt "#30219"
msgid " - Prompt before play"
msgstr "- Küsi enne esitust"
msgctxt "#30218"
msgid "Play next episode after %"
msgstr "Esita järgmine episood pärast %"
msgctxt "#30217"
msgid "Prompt to delete episode after %"
msgstr "Paku episoodi kustutamist pärast %"
msgctxt "#30216"
msgid "Item Details"
msgstr "Üksuse üksikasjad"
msgctxt "#30215"
msgid "On playback stop (100% = disabled)"
msgstr "Taasesituse peatamisel (100% = keelatud)"
msgctxt "#30214"
msgid "Events"
msgstr "Sündmused"
msgctxt "#30212"
msgid "Video max width"
msgstr "Video maksimaalne laius"
msgctxt "#30211"
msgid "Transcode options"
msgstr "Transkoodimise valikud"
msgctxt "#30210"
msgid "HTTP direct stream"
msgstr "HTTP otsevoog"
msgctxt "#30000"
msgid "Host"
msgstr "Peremeesmasin"
msgctxt "#30182"
msgid "Include media stream info"
msgstr "Kaasa meediavoo teave"
msgctxt "#30208"
msgid "Max stream bitrate (Kbits)"
msgstr "Voo maksimaalne bitikiirus (Kbps)"
msgctxt "#30207"
msgid "Playback"
msgstr "Taasesitus"
msgctxt "#30206"
msgid "Playback type"
msgstr "Taasesituse tüüp"
msgctxt "#30201"
msgid "Unable to connect to server"
msgstr "Serveriga ei saa ühendust"
msgctxt "#30200"
msgid "URL error"
msgstr "URL viga"
msgctxt "#30183"
msgid "Include people"
msgstr "Kaasa inimesed"
msgctxt "#30181"
msgid "Include plot"
msgstr "Kaasa süžee"
msgctxt "#30180"
msgid "Select User"
msgstr "Vali kasutaja"
msgctxt "#30169"
msgid "Address: "
msgstr "Aadress:"
msgctxt "#30167"
msgid "Selected Server Address"
msgstr "Valitud serveri aadress"
msgctxt "#30166"
msgid "Select Server"
msgstr "Vali server"
msgctxt "#30163"
msgid "Add (cc) if subtitle is available"
msgstr "Lisa (cc) subtiitrite olemasolul"
msgctxt "#30139"
msgid "No Media Type Set"
msgstr "Meediatüüp määramata"
msgctxt "#30135"
msgid "Error"
msgstr "Viga"
msgctxt "#30126"
msgid "Processing Item : "
msgstr "Üksuse töötlemine:"
msgctxt "#30125"
msgid "Done"
msgstr "Tehtud"
msgctxt "#30121"
msgid "On resume"
msgstr "Jätkamisel"
msgctxt "#30120"
msgid "Show load progress"
msgstr "Kuva laadimise edenemine"
msgctxt "#30116"
msgid "Add unwatched counts to names"
msgstr "Lisa nimedele vaatamata arv"
msgctxt "#30118"
msgid "Add resume percent to names"
msgstr "Lisa nimedele jätkamise protsent"
msgctxt "#30114"
msgid "Jump back seconds"
msgstr "Tagasihüpe (sek)"
msgctxt "#30113"
msgid "Retrieving Data"
msgstr "Andmete toomine"
msgctxt "#30112"
msgid "Loading Content"
msgstr "Sisu laadimine"
msgctxt "#30111"
msgid "Services"
msgstr "Teenused"
msgctxt "#30110"
msgid "Interface"
msgstr "Liides"
msgctxt "#30092"
msgid "Warning: This action will delete the media files from the server."
msgstr "Hoiatus: see toiming kustutab meediafailid serverist."
msgctxt "#30091"
msgid "Confirm delete?"
msgstr "Kas kinnitada kustutamine?"
msgctxt "#30063"
msgid "N/A"
msgstr "Pole saadaval"
msgctxt "#30053"
msgid "Waiting for server to delete"
msgstr "Serveri kustutamise ootel"
msgctxt "#30052"
msgid "Deleting"
msgstr "Kustutamine"
msgctxt "#30045"
msgid "Username not found"
msgstr "Kasutajanime ei leitud"
msgctxt "#30044"
msgid "Incorrect Username/Password"
msgstr "Vale kasutajanimi/parool"
msgctxt "#30027"
msgid "Enable debug logging"
msgstr "Luba silumislogimine"
msgctxt "#30026"
msgid "Widget item select action"
msgstr "Vidina toiming üksuse valikul"
msgctxt "#30025"
msgid "Password:"
msgstr "Parool:"
msgctxt "#30024"
msgid "Username:"
msgstr "Kasutajanimi:"
msgctxt "#30023"
msgid "Hide unwatched episode details"
msgstr "Peida vaatamata episoodi üksikasjad"
msgctxt "#30022"
msgid "Advanced"
msgstr "Täpsem"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,951 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2022-07-06 13:22+0000\n"
"Last-Translator: Thomas Schwery <thomas@inf3.ch>\n"
"Language-Team: French <https://translate.jellyfin.org/projects/jellycon/"
"jellycon/fr/>\n"
"Language: fr\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.10.1\n"
msgctxt "#30018"
msgid "Number of items to show in filtered lists"
msgstr "Nombre d'éléments à afficher dans les listes filtrées"
msgctxt "#30017"
msgid "Show connected clients"
msgstr "Afficher les clients connectés"
msgctxt "#30016"
msgid "Device display name"
msgstr "Nom d'affichage de l'appareil"
msgctxt "#30015"
msgid "Log timing data"
msgstr "Enregistrer les données de synchronisation"
msgctxt "#30014"
msgid "Jellyfin"
msgstr "Jellyfin"
msgctxt "#30012"
msgid "[Change user]"
msgstr "[Changer d'utilisateur]"
msgctxt "#30011"
msgid "[Detect local server]"
msgstr "[Détecter le serveur local]"
msgctxt "#30010"
msgid "Number of performance profiles to capture"
msgstr "Nombre de profils de performances à capturer"
msgctxt "#30000"
msgid "Host"
msgstr "Hôte"
msgctxt "#30045"
msgid "Username not found"
msgstr "Nom d'utilisateur introuvable"
msgctxt "#30025"
msgid "Password:"
msgstr "Mot de passe :"
msgctxt "#30008"
msgid "Samba password"
msgstr "Mot de passe Samba"
msgctxt "#30007"
msgid "Samba username"
msgstr "Nom d'utilisateur Samba"
msgctxt "#30006"
msgid "Password"
msgstr "Mot de passe"
msgctxt "#30005"
msgid "Username"
msgstr "Nom d'utilisateur"
msgctxt "#30003"
msgid "Verify HTTPS certificate"
msgstr "Vérifier le certificat HTTPS"
msgctxt "#30001"
msgid "Port"
msgstr "Port"
msgctxt "#30044"
msgid "Incorrect Username/Password"
msgstr "Utilisateur/Mot de passe incorrect"
msgctxt "#30027"
msgid "Enable debug logging"
msgstr "Activer le débogage dans le journal dévènements"
msgctxt "#30024"
msgid "Username:"
msgstr "Nom d'utilisateur :"
msgctxt "#30023"
msgid "Hide unwatched episode details"
msgstr "Cacher les détails des épisodes non-visionnés"
msgctxt "#30022"
msgid "Advanced"
msgstr "Avancés"
msgctxt "#30021"
msgid "Show all episodes item"
msgstr "Afficher tous les épisodes"
msgctxt "#30211"
msgid "Transcode options"
msgstr "Options de transcodage"
msgctxt "#30209"
msgid "File direct path"
msgstr "Chemin d'accès du fichier"
msgctxt "#30201"
msgid "Unable to connect to server"
msgstr "Impossible de se connecter au serveur"
msgctxt "#30200"
msgid "URL error"
msgstr "Erreur d'URL"
msgctxt "#30180"
msgid "Select User"
msgstr "Sélectionner l'utilisateur"
msgctxt "#30092"
msgid "Warning: This action will delete the media files from the server."
msgstr ""
"Avertissement : Cette action va supprimer tous les fichier multimédias "
"serveur."
msgctxt "#30167"
msgid "Selected Server Address"
msgstr "Sélectionner l'adresse du serveur"
msgctxt "#30166"
msgid "Select Server"
msgstr "Sélectionner le serveur"
msgctxt "#30135"
msgid "Error"
msgstr "Erreur"
msgctxt "#30111"
msgid "Services"
msgstr "Services"
msgctxt "#30110"
msgid "Interface"
msgstr "Interface"
msgctxt "#30091"
msgid "Confirm delete?"
msgstr "Confirmer la suppression ?"
msgctxt "#30052"
msgid "Deleting"
msgstr "Suppression"
msgctxt "#30214"
msgid "Events"
msgstr "Évènement"
msgctxt "#30212"
msgid "Video max width"
msgstr "Largeur vidéo max"
msgctxt "#30210"
msgid "HTTP direct stream"
msgstr "Stream direct HTTP"
msgctxt "#30208"
msgid "Max stream bitrate (Kbits)"
msgstr "Débit maximum du flux (Kbps)"
msgctxt "#30207"
msgid "Playback"
msgstr "Lecture"
msgctxt "#30182"
msgid "Include media stream info"
msgstr "Inclure info du flux media"
msgctxt "#30181"
msgid "Include plot"
msgstr "Inclure l'intrigue"
msgctxt "#30163"
msgid "Add (cc) if subtitle is available"
msgstr "Ajouter (cc) si sous-titre disponible"
msgctxt "#30139"
msgid "No Media Type Set"
msgstr "Type de média non configurer"
msgctxt "#30125"
msgid "Done"
msgstr "Fait"
msgctxt "#30120"
msgid "Show load progress"
msgstr "Afficher la progression du chargement"
msgctxt "#30113"
msgid "Retrieving Data"
msgstr "Récupération des données"
msgctxt "#30112"
msgid "Loading Content"
msgstr "Chargement du contenu"
msgctxt "#30063"
msgid "N/A"
msgstr "N/A"
msgctxt "#30053"
msgid "Waiting for server to delete"
msgstr "En attente de suppression par le serveur"
msgctxt "#30306"
msgid "Playback starting"
msgstr "Démarrage de la lecture"
msgctxt "#30305"
msgid "Not Found"
msgstr "Non trouvé"
msgctxt "#30301"
msgid "Caching Images"
msgstr "Mise en cache des images"
msgctxt "#30300"
msgid "Cache all Jellyfin images as local Kodi images?"
msgstr ""
"Mettre en cache toutes les images Jellyfin en tant qu'images locales Kodi ?"
msgctxt "#30299"
msgid "Cache Images"
msgstr "Images en cache"
msgctxt "#30298"
msgid "Deleting Kodi Images"
msgstr "Suppression des images de Kodi en cours"
msgctxt "#30297"
msgid "Delete unused images?"
msgstr "Supprimer les images non-utilisées ?"
msgctxt "#30296"
msgid "Delete"
msgstr "Supprimer"
msgctxt "#30295"
msgid "To use this feature you need HTTP control enabled"
msgstr "Pour utiliser cette fonctionnalité vous devez activer le contrôle HTTP"
msgctxt "#30293"
msgid "Cache images"
msgstr "Images en cache"
msgctxt "#30292"
msgid "Select Subtitle Stream"
msgstr "Choisir le flux des sous-titres"
msgctxt "#30291"
msgid "Select Audio Stream"
msgstr "Choisir le flux audio"
msgctxt "#30290"
msgid "All"
msgstr "Tous"
msgctxt "#30289"
msgid "TV Shows - Genres"
msgstr "Séries TV - Genres"
msgctxt "#30287"
msgid "TV Shows - Latest"
msgstr "Séries TV - Derniers"
msgctxt "#30286"
msgid "Movies - Unwatched"
msgstr "Films - Non-vus"
msgctxt "#30283"
msgid "Play Next Episode?"
msgstr "Lire l'épisode suivant ?"
msgctxt "#30282"
msgid "No Jellyfin servers detected on your local network."
msgstr "Aucun serveur Jellyfin détecté sur le réseau local."
msgctxt "#30281"
msgid "Refresh Cached Images"
msgstr "Actualiser les images en cache"
msgctxt "#30280"
msgid "Missing Title"
msgstr "Titre manquant"
msgctxt "#30279"
msgid "TV Shows - Unwatched"
msgstr "Séries TV - Non-vus"
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 doit demander la reprise des éléments partiellement lus, Kodi peut "
"également le demander, cela peut provoquer une double invite. Voulez-vous "
"supprimer la double invite ?"
msgctxt "#30276"
msgid "Extra Resume Prompt Detected"
msgstr "Invitation supplémentaire à reprendre la lecture détectée"
msgctxt "#30275"
msgid "Force Transcode"
msgstr "Forcer le transcodage"
msgctxt "#30274"
msgid "Delete"
msgstr "Supprimer"
msgctxt "#30273"
msgid "Unset Favourite"
msgstr "Retirer des favoris"
msgctxt "#30272"
msgid "Set Favourite"
msgstr "Ajouter en favori"
msgctxt "#30271"
msgid "Mark Unwatched"
msgstr "Noter comme non-vu"
msgctxt "#30270"
msgid "Mark Watched"
msgstr "Noter comme vu"
msgctxt "#30269"
msgid "Movies - Random"
msgstr "Films - Aléatoires"
msgctxt "#30266"
msgid "Movies - Pages"
msgstr "Films - Pages"
msgctxt "#30265"
msgid "Episodes - Next Up"
msgstr "Épisodes - À suivre"
msgctxt "#30264"
msgid "Episodes - In Progress"
msgstr "Épisodes - En cours"
msgctxt "#30263"
msgid "Episodes - Recently Added"
msgstr "Épisodes - Ajoutés récemment"
msgctxt "#30262"
msgid "TV Shows - Favorites"
msgstr "Séries TV - Favoris"
msgctxt "#30261"
msgid "TV Shows"
msgstr "Séries TV"
msgctxt "#30260"
msgid "BoxSets"
msgstr "Coffrets"
msgctxt "#30259"
msgid "Movies - Favorites"
msgstr "Films - Favoris"
msgctxt "#30258"
msgid "Movies - In Progress"
msgstr "Films - En cours"
msgctxt "#30257"
msgid "Movies - Recently Added"
msgstr "Films - Ajoutés récemment"
msgctxt "#30256"
msgid "Movies"
msgstr "Films"
msgctxt "#30255"
msgid "TV Shows - A-Z"
msgstr "Séries TV - A-Z"
msgctxt "#30254"
msgid "Show add-on settings"
msgstr "Afficher les paramètres du module"
msgctxt "#30252"
msgid "Movies - A-Z"
msgstr "Films - A-Z"
msgctxt "#30251"
msgid "Movies - Genres"
msgstr "Films - Genres"
msgctxt "#30250"
msgid "Unknown"
msgstr "Inconnu"
msgctxt "#30247"
msgid "Custom Widget Content"
msgstr "Contenu de widget personnalisé"
msgctxt "#30246"
msgid "Search"
msgstr "Rechercher"
msgctxt "#30241"
msgid "Force transcode mpeg4"
msgstr "Forcer le transcodage mpeg4"
msgctxt "#30240"
msgid "Force transcode msmpeg4v3 (divx)"
msgstr "Forcer le transcodage msmpeg4v3 (divx)"
msgctxt "#30239"
msgid "Force transcode mpeg2"
msgstr "Forcer le transcodage mpeg2"
msgctxt "#30238"
msgid "Playback stream options"
msgstr "Options du flux de lecture"
msgctxt "#30237"
msgid "Start from beginning"
msgstr "Lire depuis le début"
msgctxt "#30236"
msgid "Force transcode h265 (hevc)"
msgstr "Forcer le transcodage h265 (hevc)"
msgctxt "#30235"
msgid "Episodes"
msgstr "Épisodes"
msgctxt "#30231"
msgid "Movies"
msgstr "Films"
msgctxt "#30229"
msgid "TV Shows"
msgstr "Séries TV"
msgctxt "#30224"
msgid "Interaction"
msgstr "Intéraction"
msgctxt "#30223"
msgid "Page Size and Filtering"
msgstr "Taille et filtre de la page"
msgctxt "#30222"
msgid "Item Layout"
msgstr "Disposition de l'élément"
msgctxt "#30220"
msgid "Prompt to delete movie after %"
msgstr "Proposer de supprimer le film après %"
msgctxt "#30218"
msgid "Play next episode after %"
msgstr "Lire l'épisode suivant après %"
msgctxt "#30217"
msgid "Prompt to delete episode after %"
msgstr "Proposer de supprimer l'épisode après %"
msgctxt "#30216"
msgid "Item Details"
msgstr "Détails de l'élément"
msgctxt "#30215"
msgid "On playback stop (100% = disabled)"
msgstr "À l'arrêt de la lecture (100% = désactivé)"
msgctxt "#30213"
msgid "Video force 8 bit"
msgstr "Forcer le mode vidéo 8-bit"
msgctxt "#30206"
msgid "Playback type"
msgstr "Type de lecture"
msgctxt "#30183"
msgid "Include people"
msgstr "Inclure les personnes"
msgctxt "#30121"
msgid "On resume"
msgstr "À la reprise de la lecture"
msgctxt "#30118"
msgid "Add resume percent to names"
msgstr "Ajouter le pourcentage de continuation aux noms"
msgctxt "#30116"
msgid "Add unwatched counts to names"
msgstr "Ajouter un compteur d'éléments non-vus aux noms"
msgctxt "#30020"
msgid "Flatten single season"
msgstr "Aplatir une seule saison"
msgctxt "#30019"
msgid "Filtered episode name format"
msgstr "Format de nom d'épisode filtré"
msgctxt "#30114"
msgid "Jump back seconds"
msgstr "Revenir en arrière"
msgctxt "#30268"
msgid " - Recently Added"
msgstr "- Ajouté récemment"
msgctxt "#30267"
msgid " - In Progress"
msgstr "- En cours"
msgctxt "#30169"
msgid "Address: "
msgstr "Adresse :"
msgctxt "#30437"
msgid "Playback options"
msgstr "Options de lecture"
msgctxt "#30431"
msgid "Seasons"
msgstr "Saisons"
msgctxt "#30429"
msgid "Genre"
msgstr "Genre"
msgctxt "#30428"
msgid "Rating"
msgstr "Critique"
msgctxt "#30427"
msgid "Added"
msgstr "Ajouté"
msgctxt "#30426"
msgid "Title"
msgstr "Titre"
msgctxt "#30425"
msgid "Year"
msgstr "Année"
msgctxt "#30424"
msgid "Default"
msgstr "Défaut"
msgctxt "#30423"
msgid "NotSet"
msgstr "Non-défini"
msgctxt "#30422"
msgid "Sorting"
msgstr "Tri"
msgctxt "#30421"
msgid "Views"
msgstr "Vues"
msgctxt "#30420"
msgid "Audio max channels"
msgstr "Nombre maximal de canaux audio"
msgctxt "#30419"
msgid "Audio codec"
msgstr "Codec audio"
msgctxt "#30417"
msgid "You do not have permision to delete this item"
msgstr "Vous n'avez pas les permissions pour supprimer cet élément"
msgctxt "#30415"
msgid " - Favorite Collections"
msgstr "- Collections Favorites"
msgctxt "#30414"
msgid " - Favorites"
msgstr "- Favoris"
msgctxt "#30412"
msgid " - Decades"
msgstr "- Décennies"
msgctxt "#30411"
msgid " - Years"
msgstr "- Années"
msgctxt "#30410"
msgid " - Collections"
msgstr "- Collections"
msgctxt "#30407"
msgid "Global Lists"
msgstr "Listes globales"
msgctxt "#30406"
msgid "Jellyfin Libraries"
msgstr "Bibliothèques Jellyfin"
msgctxt "#30405"
msgid " - Show All"
msgstr "- Tout afficher"
msgctxt "#30404"
msgid " - A-Z"
msgstr "- A - Z"
msgctxt "#30403"
msgid "Movies - Recommendations"
msgstr "Films - Recommendations"
msgctxt "#30402"
msgid "Add to Kodi Playlist"
msgstr "Ajouter à la liste de lecture Kodi"
msgctxt "#30401"
msgid "Info"
msgstr "Informations"
msgctxt "#30399"
msgid "Hide"
msgstr "Cacher"
msgctxt "#30398"
msgid "Refresh Jellyfin Metadata"
msgstr "Rafraîchir les métadonnées Jellyfin"
msgctxt "#30397"
msgid " - Pages"
msgstr "- Pages"
msgctxt "#30392"
msgid "HTTPS"
msgstr "HTTPS"
msgctxt "#30391"
msgid "HTTP"
msgstr "HTTP"
msgctxt "#30390"
msgid "Protocol"
msgstr "Protocole"
msgctxt "#30389"
msgid "User details"
msgstr "Détails utilisateur"
msgctxt "#30386"
msgid "Unused Jellyfin images : "
msgstr "Images Jellyfin inutilisées :"
msgctxt "#30383"
msgid "System - "
msgstr "Système -"
msgctxt "#30382"
msgid "Always"
msgstr "Toujours"
msgctxt "#30381"
msgid "More than one"
msgstr "Plus d'un(e)"
msgctxt "#30380"
msgid "Never"
msgstr "Jamais"
msgctxt "#30372"
msgid "Server URL"
msgstr "URL du serveur"
msgctxt "#30365"
msgid "Manual Login"
msgstr "Connexion manuelle"
msgctxt "#30364"
msgid "Do you want to save the password?"
msgstr "Voulez-vous sauver le mot de passe ?"
msgctxt "#30363"
msgid "Save Password?"
msgstr "Sauver le mot de passe ?"
msgctxt "#30362"
msgid " - Recordings"
msgstr "- Enregistrements"
msgctxt "#30361"
msgid " - Programs"
msgstr "- Programmes"
msgctxt "#30360"
msgid " - Channels"
msgstr "- Canaux"
msgctxt "#30353"
msgid " - Frequently Played"
msgstr "- Fréquemment lus"
msgctxt "#30321"
msgid " - Album Artists"
msgstr "- Artistes de l'album"
msgctxt "#30352"
msgid "Music - Frequently Played"
msgstr "Musique - Joués fréquemment"
msgctxt "#30351"
msgid "Music - Recently Played"
msgstr "Musique - Joués récemment"
msgctxt "#30350"
msgid "Music - Recently Added"
msgstr "Musique - Ajoutés récemment"
msgctxt "#30349"
msgid " - Recently Played"
msgstr "- Lus récemment"
msgctxt "#30347"
msgid "Getting Existing Images"
msgstr "Récupération des images existantes"
msgctxt "#30346"
msgid "Deleteing Cached Images"
msgstr "Suppression des images en cache"
msgctxt "#30345"
msgid "Cache Jellyfin server data requests"
msgstr "Garder les requêtes de données serveur de Jellyfin en cache"
msgctxt "#30344"
msgid "Number of images removed from cache"
msgstr "Nombre d'images enlevées du cache"
msgctxt "#30342"
msgid "New content check interval (0 = disabled)"
msgstr "Intervalle de vérification de nouveau contenu (0 = désactivé)"
msgctxt "#30322"
msgid "Auto resume"
msgstr "Reprise automatique"
msgctxt "#30314"
msgid "Play"
msgstr "Lecture"
msgctxt "#30294"
msgid "Notice"
msgstr "Annonce"
msgctxt "#30340"
msgid "Group movies into collections"
msgstr "Grouper les films en collections"
msgctxt "#30339"
msgid "Person"
msgstr "Personne"
msgctxt "#30338"
msgid "Album"
msgstr "Album"
msgctxt "#30337"
msgid "Song"
msgstr "Chanson"
msgctxt "#30334"
msgid "Use JellyCon context menu"
msgstr "Utiliser le menu contextuel JellyCon"
msgctxt "#30332"
msgid "Stop media playback on screensaver activation"
msgstr ""
"Arrêter la lecture du média lors de l'activation de l'économiseur d'écran"
msgctxt "#30331"
msgid "Movies per page"
msgstr "Films par page"
msgctxt "#30330"
msgid "Show change user dialog"
msgstr "Afficher le dialogue de changement d'utilisateur"
msgctxt "#30329"
msgid "Screensaver"
msgstr "Économiseur d'écran"
msgctxt "#30328"
msgid "Show empty folders (shows, seasons, collections)"
msgstr "Afficher les dossiers vides (séries, saisons, collections)"
msgctxt "#30327"
msgid "Go To Season"
msgstr "Aller à la saison"
msgctxt "#30325"
msgid " - Genres"
msgstr "- Genres"
msgctxt "#30320"
msgid " - Albums"
msgstr "- Albums"
msgctxt "#30318"
msgid "Music - Albums"
msgstr "Musique - Albums"
msgctxt "#30317"
msgid "Play All"
msgstr "Lire tout"
msgctxt "#30316"
msgid "Connection Error"
msgstr "Erreur de connexion"
msgctxt "#30315"
msgid "Suppress notifications for connection errors"
msgstr "Cacher les notifications pour des erreurs de connexion"
msgctxt "#30313"
msgid "Menu"
msgstr "Menu"
msgctxt "#30312"
msgid "All - "
msgstr "Tout -"
msgctxt "#30311"
msgid "Library - "
msgstr "Médiathèque -"
msgctxt "#30310"
msgid "Enable Jellyfin remote control"
msgstr "Activer le contrôle à distance Jellyfin"
msgctxt "#30309"
msgid "Select Media Source"
msgstr "Sélectionner la source de média"
msgctxt "#30308"
msgid "Select Trailer"
msgstr "Sélectionner la bande-annonce"
msgctxt "#30307"
msgid "Play Trailer"
msgstr "Lire la bande-annonce"
msgctxt "#30304"
msgid "Cached Jellyfin images : "
msgstr "Images Jellyfin en cache :"
msgctxt "#30303"
msgid "Missing Jellyfin images : "
msgstr "Images Jellyfin manquantes :"
msgctxt "#30302"
msgid "Existing images : "
msgstr "Images existantes :"
msgctxt "#30288"
msgid " - Latest"
msgstr "- Derniers"
msgctxt "#30285"
msgid " - Unwatched"
msgstr "- Non-vus"
msgctxt "#30278"
msgid " - Next Up"
msgstr "- À suivre"
msgctxt "#30219"
msgid " - Prompt before play"
msgstr "- Afficher avant de lire"
msgctxt "#30126"
msgid "Processing Item : "
msgstr "Traitement de l'élément :"
msgctxt "#30356"
msgid "Loading existing image list"
msgstr "Chargement de la liste des images existantes"
msgctxt "#30354"
msgid "Go To Series"
msgstr "Voir les Séries"
#, fuzzy
msgctxt "#30319"
msgid "Music - All Album Artists"
msgstr "Musique - Tous les Artistes d'album"
msgctxt "#30343"
msgid "Changes Require Kodi Restart"
msgstr "Les modifications nécessitent le redémarrage de Kodi"
msgctxt "#30341"
msgid "Background image update interval (0 = disabled)"
msgstr "Intervalle de mise à jour des images en arrière plan (0=désactivé)"
msgctxt "#30333"
msgid "Cache artwork in the background"
msgstr "Mise en cache des illustrations en arrière plan"
msgctxt "#30366"
msgid "Manually enter user details"
msgstr "Renseigner manuellement les détails utilisateurs"
#, fuzzy
msgctxt "#30359"
msgid "Building full image list"
msgstr "Construction de la liste complète des images"
msgctxt "#30358"
msgid "Retreiving remote image list"
msgstr "Récupération de la liste des images distantes"
msgctxt "#30357"
msgid "Processing existing image list"
msgstr "Traitement de la liste des images existantes"
#, fuzzy
msgctxt "#30355"
msgid "Kodi Settings->Services->Allow remote control via HTTP"
msgstr "Paramètres de Kodi->Services->Autoriser le contrôle à distance par HTTP"
#, fuzzy
msgctxt "#30348"
msgid "Add user ratings"
msgstr "Ajouter une note"
msgctxt "#30323"
msgid "Artists"
msgstr "Artistes"

View File

@@ -0,0 +1,352 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2022-05-20 04:22+0000\n"
"Last-Translator: Sherlock <aggybooy2@gmail.com>\n"
"Language-Team: Hindi <https://translate.jellyfin.org/projects/jellycon/"
"jellycon/hi/>\n"
"Language: hi\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.10.1\n"
msgctxt "#30407"
msgid "Global Lists"
msgstr "वैश्विक सूची"
msgctxt "#30408"
msgid "Custom Widgets"
msgstr "कस्टम विड्जेट"
msgctxt "#30409"
msgid "Add-on Actions"
msgstr "अतिरिक्त कार्य"
msgctxt "#30410"
msgid " - Collections"
msgstr "संग्रह"
msgctxt "#30417"
msgid "You do not have permision to delete this item"
msgstr "आपके पास इस वस्तु को मिटाने की अनुमति नहीं है"
msgctxt "#30416"
msgid "HTTP timeout seconds"
msgstr "एचटीटीपी के समय समाप्ति (सेकड़ो में)"
msgctxt "#30413"
msgid " - Tags"
msgstr "चिप्पी"
msgctxt "#30418"
msgid "Audio bitrate (Kbits)"
msgstr "ऑडियो बितरते (किलो बिट प्रति घंटा)"
msgctxt "#30419"
msgid "Audio codec"
msgstr "ऑडियो कोडेक"
msgctxt "#30420"
msgid "Audio max channels"
msgstr "अधिकतम ऑडियो चैनल"
msgctxt "#30421"
msgid "Views"
msgstr "कितनी बार देखा गया"
msgctxt "#30422"
msgid "Sorting"
msgstr "छंटाई"
msgctxt "#30423"
msgid "NotSet"
msgstr "नहीं लगाया गया"
msgctxt "#30424"
msgid "Default"
msgstr "पहले से चुना हुआ"
msgctxt "#30425"
msgid "Year"
msgstr "साल"
msgctxt "#30434"
msgid "Force transcode stream bitrate (Kbits)"
msgstr "जबरदस्ती बिट्रेट तय करे (किलो बिट प्रति सेकंड)"
msgctxt "#30436"
msgid "Speed test data size (MB)"
msgstr "इंटरनेट की रफ़्तार नापने के लिए डाटा का माप (मेगा बाईट)"
msgctxt "#30433"
msgid "Allow direct file playback"
msgstr "सीधे फाइल से प्लेबैक की अनुमति दें"
msgctxt "#30437"
msgid "Playback options"
msgstr "चलने के विकल्प दिखाएं"
msgctxt "#30439"
msgid "Show play next episode at time left"
msgstr "कितने समय पहले अगले अध्याय पर जाने का बटन दिखाएं"
msgctxt "#30438"
msgid "Play cinema intros"
msgstr "सिनेमा उपक्षेप चलायें"
msgctxt "#30441"
msgid "Use cached widget data"
msgstr "पुराणी विद्गट जानकारी इस्तेमाल करें"
msgctxt "#30442"
msgid "Simple new content check"
msgstr "नए कंटेंट के लिए चेक करें"
msgctxt "#30435"
msgid "Connection speed test"
msgstr "कनेक्शन की स्पीड नापें"
msgctxt "#30440"
msgid "Play next"
msgstr "अगला चलाएं"
msgctxt "#30114"
msgid "Jump back seconds"
msgstr "कुछ सेकंड पीछे जाएं"
msgctxt "#30113"
msgid "Retrieving Data"
msgstr "देता प्राप्त कर रहे है"
msgctxt "#30112"
msgid "Loading Content"
msgstr "कंटेंट लोड हो रहा है"
msgctxt "#30027"
msgid "Enable debug logging"
msgstr "डीबग सूचि बनाएं"
msgctxt "#30026"
msgid "Widget item select action"
msgstr "विद्गट वास्तु का कार्य चुनें"
msgctxt "#30020"
msgid "Flatten single season"
msgstr "सिर्फ एक सत्र वाले शो के लिए सत्र पेज हटाएं"
msgctxt "#30415"
msgid " - Favorite Collections"
msgstr "पसंदीदा संग्रह"
msgctxt "#30414"
msgid " - Favorites"
msgstr "पसंदीदा"
msgctxt "#30411"
msgid " - Years"
msgstr "साल"
msgctxt "#30399"
msgid "Hide"
msgstr "छुपाएँ"
msgctxt "#30412"
msgid " - Decades"
msgstr "दशक"
msgctxt "#30432"
msgid "Hide watched items in lists"
msgstr "देखि हुईं वस्तुएं हटाएं"
msgctxt "#30431"
msgid "Seasons"
msgstr "सत्र"
msgctxt "#30430"
msgid "Label"
msgstr "लेबल"
msgctxt "#30427"
msgid "Added"
msgstr "जोड़ा हुआ"
msgctxt "#30426"
msgid "Title"
msgstr "शीर्षक"
msgctxt "#30428"
msgid "Rating"
msgstr "रेटिंग"
msgctxt "#30429"
msgid "Genre"
msgstr "शैली"
msgctxt "#30019"
msgid "Filtered episode name format"
msgstr "छठें हुएं अध्यायों के नामों का प्रारूप"
msgctxt "#30018"
msgid "Number of items to show in filtered lists"
msgstr "छांटी हुई सूचि में कितने वास्तु दिखाए"
msgctxt "#30015"
msgid "Log timing data"
msgstr "समय की जानकारी लिखें"
msgctxt "#30016"
msgid "Device display name"
msgstr "उपकरण का नाम"
msgctxt "#30017"
msgid "Show connected clients"
msgstr "जुड़े हुए क्लाइंट्स दिखाएं"
msgctxt "#30023"
msgid "Hide unwatched episode details"
msgstr "अंधेके एपिसोडों का विवरण हटाएँ"
msgctxt "#30021"
msgid "Show all episodes item"
msgstr "सारे एपिसोड दिखाएं"
msgctxt "#30025"
msgid "Password:"
msgstr "पासवर्ड :"
msgctxt "#30246"
msgid "Search"
msgstr "खोज"
msgctxt "#30235"
msgid "Episodes"
msgstr "अध्याय"
msgctxt "#30229"
msgid "TV Shows"
msgstr "टीवी शो"
msgctxt "#30231"
msgid "Movies"
msgstr "फ़िल्म"
msgctxt "#30216"
msgid "Item Details"
msgstr "वास्तु का विवरण"
msgctxt "#30169"
msgid "Address: "
msgstr "पता :"
msgctxt "#30044"
msgid "Incorrect Username/Password"
msgstr "गलत उपयोगरता का नाम / पासवर्ड"
msgctxt "#30045"
msgid "Username not found"
msgstr "उपयोगकर्ता का नाम नहीं पाया गया"
msgctxt "#30022"
msgid "Advanced"
msgstr "उन्नत"
msgctxt "#30092"
msgid "Warning: This action will delete the media files from the server."
msgstr "चेतावनी : यह कार्य आपकी मिडिया मिटा देगा |"
msgctxt "#30135"
msgid "Error"
msgstr "समस्या"
msgctxt "#30125"
msgid "Done"
msgstr "खत्म"
msgctxt "#30181"
msgid "Include plot"
msgstr "कहानी जोड़ें"
msgctxt "#30180"
msgid "Select User"
msgstr "उपयोगकर्ता चुनें"
msgctxt "#30167"
msgid "Selected Server Address"
msgstr "चुनें हुए सर्वर का पता"
msgctxt "#30166"
msgid "Select Server"
msgstr "सर्वर चुनें"
msgctxt "#30111"
msgid "Services"
msgstr "सेवाएं"
msgctxt "#30110"
msgid "Interface"
msgstr "अंतराफलक"
msgctxt "#30091"
msgid "Confirm delete?"
msgstr "क्या आप पक्का मिटाना चाहतें हैं?"
msgctxt "#30063"
msgid "N/A"
msgstr "उपलब्ध नहीं है"
msgctxt "#30053"
msgid "Waiting for server to delete"
msgstr "सर्वर द्वारा हटाए जानें का इंतज़ार कर रहें हैं"
msgctxt "#30052"
msgid "Deleting"
msgstr "हटा रहें हैं"
msgctxt "#30024"
msgid "Username:"
msgstr "उपयोगकर्ता का नाम :"
msgctxt "#30010"
msgid "Number of performance profiles to capture"
msgstr "कितने कार्य प्रोफाइल रखने हैं"
msgctxt "#30011"
msgid "[Detect local server]"
msgstr "स्थानीय सर्वर का पता करें"
msgctxt "#30012"
msgid "[Change user]"
msgstr "उपयोगकर्ता बदलें"
msgctxt "#30014"
msgid "Jellyfin"
msgstr "जेलीफिन"
msgctxt "#30008"
msgid "Samba password"
msgstr "साम्बा में पासवर्ड"
msgctxt "#30007"
msgid "Samba username"
msgstr "साम्बा में उपयोगकर्ता का नाम"
msgctxt "#30006"
msgid "Password"
msgstr "पासवर्ड"
msgctxt "#30005"
msgid "Username"
msgstr "उपयोगकर्ता का नाम"
msgctxt "#30003"
msgid "Verify HTTPS certificate"
msgstr "HTTPS प्रमाणपत्र की जांच करें"
msgctxt "#30001"
msgid "Port"
msgstr "द्वार"
msgctxt "#30000"
msgid "Host"
msgstr "आतिथेय"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,484 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2022-05-20 04:22+0000\n"
"Last-Translator: liimee <git.taaa@fedora.email>\n"
"Language-Team: Indonesian <https://translate.jellyfin.org/projects/jellycon/"
"jellycon/id/>\n"
"Language: id\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 4.10.1\n"
msgctxt "#30414"
msgid " - Favorites"
msgstr "- Favorit"
msgctxt "#30440"
msgid "Play next"
msgstr "Putar berikutnya"
msgctxt "#30431"
msgid "Seasons"
msgstr "Musim"
msgctxt "#30430"
msgid "Label"
msgstr "Label"
msgctxt "#30427"
msgid "Added"
msgstr "Ditambahkan"
msgctxt "#30426"
msgid "Title"
msgstr "Judul"
msgctxt "#30425"
msgid "Year"
msgstr "Tahun"
msgctxt "#30417"
msgid "You do not have permision to delete this item"
msgstr "Anda tidak memiliki izin untuk menghapus item ini"
msgctxt "#30405"
msgid " - Show All"
msgstr "- Tampilkan Semua"
msgctxt "#30404"
msgid " - A-Z"
msgstr "- A-Z"
msgctxt "#30403"
msgid "Movies - Recommendations"
msgstr "Film - Disarankan"
msgctxt "#30402"
msgid "Add to Kodi Playlist"
msgstr "Tambahkan ke Daftar Putar Kodi"
msgctxt "#30401"
msgid "Info"
msgstr "Informasi"
msgctxt "#30399"
msgid "Hide"
msgstr "Sembunyikan"
msgctxt "#30398"
msgid "Refresh Jellyfin Metadata"
msgstr "Muat Ulang Metadata Jellyfin"
msgctxt "#30392"
msgid "HTTPS"
msgstr "HTTPS"
msgctxt "#30391"
msgid "HTTP"
msgstr "HTTP"
msgctxt "#30390"
msgid "Protocol"
msgstr "Protokol"
msgctxt "#30389"
msgid "User details"
msgstr "Keterangan pengguna"
msgctxt "#30388"
msgid "Server details"
msgstr "Keterangan server"
msgctxt "#30382"
msgid "Always"
msgstr "Selalu"
msgctxt "#30380"
msgid "Never"
msgstr "Tidak pernah"
msgctxt "#30381"
msgid "More than one"
msgstr "Lebih dari satu"
msgctxt "#30383"
msgid "System - "
msgstr "Sistem -"
msgctxt "#30377"
msgid "Sending request"
msgstr "Mengirim permintaan"
msgctxt "#30376"
msgid "Checking server url"
msgstr "Memeriksa url server"
msgctxt "#30374"
msgid "Sending request"
msgstr "Mengirim permintaan"
msgctxt "#30373"
msgid "Scanning for local servers"
msgstr "Memindai server lokal"
msgctxt "#30372"
msgid "Server URL"
msgstr "URL Server"
msgctxt "#30371"
msgid "Could not connect to the URL you entered, do you want to try again?"
msgstr "Tidak dapat menghubungi URL yang dimasukkan, apa Anda ingin coba lagi?"
msgctxt "#30364"
msgid "Do you want to save the password?"
msgstr "Apakah Anda ingin menyimpan kata sandi nya?"
msgctxt "#30363"
msgid "Save Password?"
msgstr "Simpan Kata Sandi?"
msgctxt "#30362"
msgid " - Recordings"
msgstr "- Rekaman"
msgctxt "#30361"
msgid " - Programs"
msgstr "- Program"
msgctxt "#30360"
msgid " - Channels"
msgstr "- Saluran"
msgctxt "#30353"
msgid " - Frequently Played"
msgstr "Sering diputar"
msgctxt "#30352"
msgid "Music - Frequently Played"
msgstr "Musik - Sering Diputar"
msgctxt "#30350"
msgid "Music - Recently Added"
msgstr "Musik - Baru Ditambahkan"
msgctxt "#30341"
msgid "Background image update interval (0 = disabled)"
msgstr "Interval pembaruan gambar latar belakang (0 = nonaktif)"
msgctxt "#30339"
msgid "Person"
msgstr "Orang"
msgctxt "#30338"
msgid "Album"
msgstr "Album"
msgctxt "#30337"
msgid "Song"
msgstr "Lagu"
msgctxt "#30331"
msgid "Movies per page"
msgstr "Film per halaman"
msgctxt "#30320"
msgid " - Albums"
msgstr "- Album"
msgctxt "#30318"
msgid "Music - Albums"
msgstr "Musik - Album"
msgctxt "#30317"
msgid "Play All"
msgstr "Putar Semua"
msgctxt "#30314"
msgid "Play"
msgstr "Putar"
msgctxt "#30313"
msgid "Menu"
msgstr "Menu"
msgctxt "#30312"
msgid "All - "
msgstr "Semua -"
msgctxt "#30309"
msgid "Select Media Source"
msgstr "Pilih Sumber Media"
msgctxt "#30306"
msgid "Playback starting"
msgstr "Pemutaran dimulai"
msgctxt "#30305"
msgid "Not Found"
msgstr "Tak Ditemukan"
msgctxt "#30297"
msgid "Delete unused images?"
msgstr "Hapus gambar yang tidak digunakan?"
msgctxt "#30296"
msgid "Delete"
msgstr "Hapus"
msgctxt "#30290"
msgid "All"
msgstr "Semua"
msgctxt "#30288"
msgid " - Latest"
msgstr "- Terbaru"
msgctxt "#30287"
msgid "TV Shows - Latest"
msgstr "Acara TV - Terbaru"
msgctxt "#30286"
msgid "Movies - Unwatched"
msgstr "Film - Belum Ditonton"
msgctxt "#30285"
msgid " - Unwatched"
msgstr "- Belum Ditonton"
msgctxt "#30283"
msgid "Play Next Episode?"
msgstr "Putar Episode Berikutnya?"
msgctxt "#30282"
msgid "No Jellyfin servers detected on your local network."
msgstr "Tak ada server Jellyfin yang terdeteksi pada jaringan lokal Anda."
msgctxt "#30279"
msgid "TV Shows - Unwatched"
msgstr "Acara TV - Belum Ditonton"
msgctxt "#30278"
msgid " - Next Up"
msgstr "- Berikutnya"
msgctxt "#30274"
msgid "Delete"
msgstr "Hapus"
msgctxt "#30271"
msgid "Mark Unwatched"
msgstr "Tandai Belum Ditonton"
msgctxt "#30270"
msgid "Mark Watched"
msgstr "Tandai Sudah Ditonton"
msgctxt "#30269"
msgid "Movies - Random"
msgstr "Film - Acak"
msgctxt "#30268"
msgid " - Recently Added"
msgstr "- Baru Ditambahkan"
msgctxt "#30265"
msgid "Episodes - Next Up"
msgstr "Episode - Berikutnya"
msgctxt "#30263"
msgid "Episodes - Recently Added"
msgstr "Episode - Baru Ditambahkan"
msgctxt "#30262"
msgid "TV Shows - Favorites"
msgstr "Acara TV - Favorit"
msgctxt "#30261"
msgid "TV Shows"
msgstr "Acara TV"
msgctxt "#30259"
msgid "Movies - Favorites"
msgstr "Film - Favorit"
msgctxt "#30257"
msgid "Movies - Recently Added"
msgstr "Film - Baru Ditambahkan"
msgctxt "#30256"
msgid "Movies"
msgstr "Film"
msgctxt "#30255"
msgid "TV Shows - A-Z"
msgstr "Acara TV - A-Z"
msgctxt "#30254"
msgid "Show add-on settings"
msgstr "Tampilkan pengaturan add-on"
msgctxt "#30252"
msgid "Movies - A-Z"
msgstr "Film - A-Z"
msgctxt "#30246"
msgid "Search"
msgstr "Cari"
msgctxt "#30237"
msgid "Start from beginning"
msgstr "Mulai dari awal"
msgctxt "#30235"
msgid "Episodes"
msgstr "Episode"
msgctxt "#30231"
msgid "Movies"
msgstr "Film"
msgctxt "#30229"
msgid "TV Shows"
msgstr "Acara TV"
msgctxt "#30224"
msgid "Interaction"
msgstr "Interaksi"
msgctxt "#30222"
msgid "Item Layout"
msgstr "Tata Letak Item"
msgctxt "#30216"
msgid "Item Details"
msgstr "Keterangan Item"
msgctxt "#30207"
msgid "Playback"
msgstr "Pemutaran"
msgctxt "#30206"
msgid "Playback type"
msgstr "Jenis pemutaran"
msgctxt "#30201"
msgid "Unable to connect to server"
msgstr "Tidak dapat terhubung dengan server"
msgctxt "#30200"
msgid "URL error"
msgstr "Kesalahan pada URL"
msgctxt "#30180"
msgid "Select User"
msgstr "Pilih Pengguna"
msgctxt "#30169"
msgid "Address: "
msgstr "Alamat:"
msgctxt "#30167"
msgid "Selected Server Address"
msgstr "Alamat Server Terpilih"
msgctxt "#30166"
msgid "Select Server"
msgstr "Pilih Server"
msgctxt "#30163"
msgid "Add (cc) if subtitle is available"
msgstr "Tambahkan (cc) apabila subtitel tersedia"
msgctxt "#30135"
msgid "Error"
msgstr "Kesalahan"
msgctxt "#30126"
msgid "Processing Item : "
msgstr "Memproses Item:"
msgctxt "#30113"
msgid "Retrieving Data"
msgstr "Menerima Data"
msgctxt "#30001"
msgid "Port"
msgstr "Port"
msgctxt "#30111"
msgid "Services"
msgstr "Layanan"
msgctxt "#30110"
msgid "Interface"
msgstr "Antarmuka"
msgctxt "#30092"
msgid "Warning: This action will delete the media files from the server."
msgstr "Peringatan: Tindakan ini akan menghapus berkas media dari server."
msgctxt "#30091"
msgid "Confirm delete?"
msgstr "Konfirmasi penghapusan?"
msgctxt "#30052"
msgid "Deleting"
msgstr "Menghapus"
msgctxt "#30045"
msgid "Username not found"
msgstr "Nama pengguna tidak ditemukan"
msgctxt "#30044"
msgid "Incorrect Username/Password"
msgstr "Nama Pengguna/Kata Sandi Salah"
msgctxt "#30024"
msgid "Username:"
msgstr "Nama Pengguna:"
msgctxt "#30025"
msgid "Password:"
msgstr "Kata Sandi:"
msgctxt "#30023"
msgid "Hide unwatched episode details"
msgstr "Sembunyikan keterangan episode yang belum ditonton"
msgctxt "#30022"
msgid "Advanced"
msgstr "Lanjutan"
msgctxt "#30021"
msgid "Show all episodes item"
msgstr "Tampilkan semua episode"
msgctxt "#30003"
msgid "Verify HTTPS certificate"
msgstr "Verifikasi sertifikat HTTPS"
msgctxt "#30017"
msgid "Show connected clients"
msgstr "Tampilkan klien yang terhubung"
msgctxt "#30014"
msgid "Jellyfin"
msgstr "Jellyfin"
msgctxt "#30008"
msgid "Samba password"
msgstr "Kata sandi Samba"
msgctxt "#30007"
msgid "Samba username"
msgstr "Nama pengguna Samba"
msgctxt "#30006"
msgid "Password"
msgstr "Kata sandi"
msgctxt "#30005"
msgid "Username"
msgstr "Nama pengguna"

View File

@@ -0,0 +1,443 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2022-06-28 13:22+0000\n"
"Last-Translator: Michele Fattoruso <michele.fattoruso@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.10.1\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 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)"
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 richiede una finestra per continuare la riproduzione di elementi, "
"anche Kodi può richiedere una finestra, causando lapertura di una doppia "
"finestra. Vuoi rimuovere la doppia finestra?"
msgctxt "#30213"
msgid "Video force 8 bit"
msgstr "Forza video 8 bit"
msgctxt "#30114"
msgid "Jump back seconds"
msgstr "Riavvolgi secondi"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,536 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2022-06-01 17:51+0000\n"
"Last-Translator: B v H <bobsieflopsie@outlook.com>\n"
"Language-Team: Dutch <https://translate.jellyfin.org/projects/jellycon/"
"jellycon/nl/>\n"
"Language: nl\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.10.1\n"
msgctxt "#30257"
msgid "Movies - Recently Added"
msgstr "Films - Onlangs Toegevoegd"
msgctxt "#30256"
msgid "Movies"
msgstr "Films"
msgctxt "#30255"
msgid "TV Shows - A-Z"
msgstr "TV Shows - A-Z"
msgctxt "#30254"
msgid "Show add-on settings"
msgstr "Toon add-on instellingen"
msgctxt "#30252"
msgid "Movies - A-Z"
msgstr "Films - A-Z"
msgctxt "#30251"
msgid "Movies - Genres"
msgstr "Films - Genres"
msgctxt "#30250"
msgid "Unknown"
msgstr "Onbekend"
msgctxt "#30247"
msgid "Custom Widget Content"
msgstr "Custom Widget Inhoud"
msgctxt "#30246"
msgid "Search"
msgstr "Zoek"
msgctxt "#30241"
msgid "Force transcode mpeg4"
msgstr "Forceer transcoderen mpeg4"
msgctxt "#30240"
msgid "Force transcode msmpeg4v3 (divx)"
msgstr "Forceer transcoderen msmpeg4v3 (divx)"
msgctxt "#30239"
msgid "Force transcode mpeg2"
msgstr "Forceer transcoderen mpeg2"
msgctxt "#30238"
msgid "Playback stream options"
msgstr "Afspeelstreamopties"
msgctxt "#30237"
msgid "Start from beginning"
msgstr "Speel vanaf begin"
msgctxt "#30236"
msgid "Force transcode h265 (hevc)"
msgstr "Forceer transcoderen h265 (hevc)"
msgctxt "#30235"
msgid "Episodes"
msgstr "Afleveringen"
msgctxt "#30231"
msgid "Movies"
msgstr "Films"
msgctxt "#30229"
msgid "TV Shows"
msgstr "TV Shows"
msgctxt "#30224"
msgid "Interaction"
msgstr "Interactie"
msgctxt "#30216"
msgid "Item Details"
msgstr "Item Details"
msgctxt "#30223"
msgid "Page Size and Filtering"
msgstr "Paginagrootte en Filteren"
msgctxt "#30222"
msgid "Item Layout"
msgstr "Item Layout"
msgctxt "#30220"
msgid "Prompt to delete movie after %"
msgstr "Vraag om film te verwijderen na %"
msgctxt "#30219"
msgid " - Prompt before play"
msgstr "- Vraag voor afspelen"
msgctxt "#30218"
msgid "Play next episode after %"
msgstr "Speel volgende aflevering na %"
msgctxt "#30217"
msgid "Prompt to delete episode after %"
msgstr "Vraag om aflevering te verwijderen na %"
msgctxt "#30215"
msgid "On playback stop (100% = disabled)"
msgstr "Bij stoppen playback (100% = uitgeschakeld)"
msgctxt "#30214"
msgid "Events"
msgstr "Events"
msgctxt "#30213"
msgid "Video force 8 bit"
msgstr "Forceer 8 bit video"
msgctxt "#30212"
msgid "Video max width"
msgstr "Maximale videobreedte"
msgctxt "#30211"
msgid "Transcode options"
msgstr "Transcode opties"
msgctxt "#30210"
msgid "HTTP direct stream"
msgstr "HTTP direct stream"
msgctxt "#30209"
msgid "File direct path"
msgstr "Directe bestandslocatie"
msgctxt "#30208"
msgid "Max stream bitrate (Kbits)"
msgstr "Maximale stream bitrate (Kbps)"
msgctxt "#30207"
msgid "Playback"
msgstr "Afspelen"
msgctxt "#30206"
msgid "Playback type"
msgstr "Afspeeltype"
msgctxt "#30201"
msgid "Unable to connect to server"
msgstr "Kan niet verbinden met de server"
msgctxt "#30200"
msgid "URL error"
msgstr "URL error"
msgctxt "#30183"
msgid "Include people"
msgstr "Mensen toevoegen"
msgctxt "#30182"
msgid "Include media stream info"
msgstr "Mediastream informatie toevoegen"
msgctxt "#30181"
msgid "Include plot"
msgstr "Plot toevoegen"
msgctxt "#30180"
msgid "Select User"
msgstr "Selecteer gebruiker"
msgctxt "#30169"
msgid "Address: "
msgstr "Adres:"
msgctxt "#30167"
msgid "Selected Server Address"
msgstr "Adres geselecteerde server"
msgctxt "#30166"
msgid "Select Server"
msgstr "Selecteer server"
msgctxt "#30163"
msgid "Add (cc) if subtitle is available"
msgstr "Voeg (cc) toe als ondertiteling beschikbaar is"
msgctxt "#30139"
msgid "No Media Type Set"
msgstr "Geen mediatype ingesteld"
msgctxt "#30135"
msgid "Error"
msgstr "Error"
msgctxt "#30126"
msgid "Processing Item : "
msgstr "Item verwerken:"
msgctxt "#30125"
msgid "Done"
msgstr "Gereed"
msgctxt "#30121"
msgid "On resume"
msgstr "Bij hervatten"
msgctxt "#30120"
msgid "Show load progress"
msgstr "Toon laadvoortgang"
msgctxt "#30118"
msgid "Add resume percent to names"
msgstr "Voeg percentage bekeken to aan namen"
msgctxt "#30116"
msgid "Add unwatched counts to names"
msgstr "Voeg hoeveelheid niet bekeken to aan namen"
msgctxt "#30114"
msgid "Jump back seconds"
msgstr "Spring seconden terug"
msgctxt "#30113"
msgid "Retrieving Data"
msgstr "Data Ophalen"
msgctxt "#30112"
msgid "Loading Content"
msgstr "Content aan het laden"
msgctxt "#30111"
msgid "Services"
msgstr "Services"
msgctxt "#30110"
msgid "Interface"
msgstr "Interface"
msgctxt "#30092"
msgid "Warning: This action will delete the media files from the server."
msgstr "Waarschuwing: Deze stap zal mediabestanden van de server verwijderen."
msgctxt "#30091"
msgid "Confirm delete?"
msgstr "Bevestig verwijderen?"
msgctxt "#30063"
msgid "N/A"
msgstr "N/A"
msgctxt "#30053"
msgid "Waiting for server to delete"
msgstr "Wachten op server om te wissen"
msgctxt "#30052"
msgid "Deleting"
msgstr "Bezig met verwijderen"
msgctxt "#30045"
msgid "Username not found"
msgstr "Gebruikersnaam niet gevonden"
msgctxt "#30044"
msgid "Incorrect Username/Password"
msgstr "Onjuiste gebruikersnaam/wachtwoord"
msgctxt "#30027"
msgid "Enable debug logging"
msgstr "Schakel debug logging in"
msgctxt "#30026"
msgid "Widget item select action"
msgstr "Widget item selecteer actie"
msgctxt "#30025"
msgid "Password:"
msgstr "Wachtwoord:"
msgctxt "#30024"
msgid "Username:"
msgstr "Gebruikersnaam:"
msgctxt "#30023"
msgid "Hide unwatched episode details"
msgstr "Verberg details van niet bekeken afleveringen"
msgctxt "#30022"
msgid "Advanced"
msgstr "Geavanceerd"
msgctxt "#30021"
msgid "Show all episodes item"
msgstr "Toon alle afleveringen"
msgctxt "#30019"
msgid "Filtered episode name format"
msgstr "Naamformat gefilterde aflevering"
msgctxt "#30018"
msgid "Number of items to show in filtered lists"
msgstr "Aantal weer te geven items in gefilterde lijsten"
msgctxt "#30017"
msgid "Show connected clients"
msgstr "Toon verbonden clients"
msgctxt "#30016"
msgid "Device display name"
msgstr "Weergavenaam apparaat"
msgctxt "#30015"
msgid "Log timing data"
msgstr "Log timing gegevens"
msgctxt "#30014"
msgid "Jellyfin"
msgstr "Jellyfin"
msgctxt "#30012"
msgid "[Change user]"
msgstr "[Wijzig gebruiker]"
msgctxt "#30011"
msgid "[Detect local server]"
msgstr "[Detecteer lokale server]"
msgctxt "#30010"
msgid "Number of performance profiles to capture"
msgstr "Aantal vast te leggen prestatieprofielen"
msgctxt "#30008"
msgid "Samba password"
msgstr "Samba wachtwoord"
msgctxt "#30007"
msgid "Samba username"
msgstr "Samba gebruikersnaam"
msgctxt "#30006"
msgid "Password"
msgstr "Wachtwoord"
msgctxt "#30005"
msgid "Username"
msgstr "Gebruikersnaam"
msgctxt "#30003"
msgid "Verify HTTPS certificate"
msgstr "Verifieer HTTPS certificaat"
msgctxt "#30001"
msgid "Port"
msgstr "Poort"
msgctxt "#30000"
msgid "Host"
msgstr "Host"
msgctxt "#30352"
msgid "Music - Frequently Played"
msgstr "Muziek - Vaak Afgespeeld"
msgctxt "#30351"
msgid "Music - Recently Played"
msgstr "Muziek - Recent Afgespeeld"
msgctxt "#30350"
msgid "Music - Recently Added"
msgstr "Muziek - Recent Toegevoegd"
msgctxt "#30339"
msgid "Person"
msgstr "Persoon"
msgctxt "#30338"
msgid "Album"
msgstr "Album"
msgctxt "#30337"
msgid "Song"
msgstr "Nummer"
msgctxt "#30331"
msgid "Movies per page"
msgstr "Films per pagina"
msgctxt "#30327"
msgid "Go To Season"
msgstr "Ga Naar Seizoen"
msgctxt "#30325"
msgid " - Genres"
msgstr "- Genres"
msgctxt "#30322"
msgid "Auto resume"
msgstr "Automatisch Hervatten"
msgctxt "#30320"
msgid " - Albums"
msgstr "- Albums"
msgctxt "#30318"
msgid "Music - Albums"
msgstr "Muziek - Albums"
msgctxt "#30317"
msgid "Play All"
msgstr "Speel Alles Af"
msgctxt "#30316"
msgid "Connection Error"
msgstr "Verbindingsfout"
msgctxt "#30315"
msgid "Suppress notifications for connection errors"
msgstr "Meldingen van verbindingsfouten onderdrukken"
msgctxt "#30314"
msgid "Play"
msgstr "Speel Af"
msgctxt "#30313"
msgid "Menu"
msgstr "Menu"
msgctxt "#30308"
msgid "Select Trailer"
msgstr "Selecteer Trailer"
msgctxt "#30307"
msgid "Play Trailer"
msgstr "Speel Trailer Af"
msgctxt "#30305"
msgid "Not Found"
msgstr "Niet Gevonden"
msgctxt "#30302"
msgid "Existing images : "
msgstr "Bestaande Afbeeldingen:"
msgctxt "#30298"
msgid "Deleting Kodi Images"
msgstr "Kodi Afbeeldingen Verwijderen"
msgctxt "#30297"
msgid "Delete unused images?"
msgstr "Verwijder niet gebruikte afbeeldingen?"
msgctxt "#30296"
msgid "Delete"
msgstr "Verwijderen"
msgctxt "#30290"
msgid "All"
msgstr "Alles"
msgctxt "#30288"
msgid " - Latest"
msgstr "- Nieuwste"
msgctxt "#30286"
msgid "Movies - Unwatched"
msgstr "Films - Niet Bekeken"
msgctxt "#30285"
msgid " - Unwatched"
msgstr "- Niet Bekeken"
msgctxt "#30283"
msgid "Play Next Episode?"
msgstr "Speel Volgende Aflevering Af?"
msgctxt "#30282"
msgid "No Jellyfin servers detected on your local network."
msgstr "Geen Jellyfin Servers Gevonden Op Uw Locale Netwerk."
msgctxt "#30280"
msgid "Missing Title"
msgstr "Missende Titel"
msgctxt "#30278"
msgid " - Next Up"
msgstr "- Volgende"
msgctxt "#30275"
msgid "Force Transcode"
msgstr "Forceer Transcoderen"
msgctxt "#30274"
msgid "Delete"
msgstr "Verwijderen"
msgctxt "#30273"
msgid "Unset Favourite"
msgstr "Demarkeer Favoriet"
msgctxt "#30272"
msgid "Set Favourite"
msgstr "Markeer Favoriet"
msgctxt "#30271"
msgid "Mark Unwatched"
msgstr "Markeer Niet Bekeken"
msgctxt "#30270"
msgid "Mark Watched"
msgstr "Markeer Bekeken"
msgctxt "#30269"
msgid "Movies - Random"
msgstr "Films - Willekeurig"
msgctxt "#30268"
msgid " - Recently Added"
msgstr "- Recent Toegevoegd"
msgctxt "#30259"
msgid "Movies - Favorites"
msgstr "Films - Favorieten"
msgctxt "#30258"
msgid "Movies - In Progress"
msgstr "Films - In Uitvoering"

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,917 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2022-06-04 10:22+0000\n"
"Last-Translator: hogenf <hogen.fasth@gmail.com>\n"
"Language-Team: Swedish <https://translate.jellyfin.org/projects/jellycon/"
"jellycon/sv/>\n"
"Language: sv\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.10.1\n"
msgctxt "#30283"
msgid "Play Next Episode?"
msgstr "Spela Nästa Avsnitt?"
msgctxt "#30240"
msgid "Force transcode msmpeg4v3 (divx)"
msgstr "Tvinga msmpeg4v3 (divx) transkoding"
msgctxt "#30218"
msgid "Play next episode after %"
msgstr "Spela nästa avsnitt efter %"
msgctxt "#30239"
msgid "Force transcode mpeg2"
msgstr "Tvinga mpeg2 transkoding"
msgctxt "#30224"
msgid "Interaction"
msgstr "Interaktion"
msgctxt "#30213"
msgid "Video force 8 bit"
msgstr "Video tvinga 8 bit"
msgctxt "#30236"
msgid "Force transcode h265 (hevc)"
msgstr "Tvinga h265 (hevc) transkoding"
msgctxt "#30254"
msgid "Show add-on settings"
msgstr "Visa tilläggs inställningar"
msgctxt "#30223"
msgid "Page Size and Filtering"
msgstr "Sido Storlek och Filtrering"
msgctxt "#30212"
msgid "Video max width"
msgstr "Video max bredd"
msgctxt "#30211"
msgid "Transcode options"
msgstr "Transkoding alternativ"
msgctxt "#30163"
msgid "Add (cc) if subtitle is available"
msgstr "Lägg till (cc) om undertexter är tillgängliga"
msgctxt "#30092"
msgid "Warning: This action will delete the media files from the server."
msgstr "Varning: Detta kommer radera media filerna från servern."
msgctxt "#30373"
msgid "Scanning for local servers"
msgstr "Skannar efter lokala servrar"
msgctxt "#30365"
msgid "Manual Login"
msgstr "Manuell Inloggning"
msgctxt "#30372"
msgid "Server URL"
msgstr "Server URL"
msgctxt "#30364"
msgid "Do you want to save the password?"
msgstr "Vill du spara lösenordet?"
msgctxt "#30350"
msgid "Music - Recently Added"
msgstr "Musik - Nyligen Tillagt"
msgctxt "#30360"
msgid " - Channels"
msgstr "- Kanaler"
msgctxt "#30353"
msgid " - Frequently Played"
msgstr "- Ofta Spelat"
msgctxt "#30349"
msgid " - Recently Played"
msgstr "- Nyligen Spelat"
msgctxt "#30351"
msgid "Music - Recently Played"
msgstr "Musik - Nyligen Spelat"
msgctxt "#30339"
msgid "Person"
msgstr "Person"
msgctxt "#30348"
msgid "Add user ratings"
msgstr "Lägg till användar betyg"
msgctxt "#30321"
msgid " - Album Artists"
msgstr "- Album Artister"
msgctxt "#30338"
msgid "Album"
msgstr "Album"
msgctxt "#30328"
msgid "Show empty folders (shows, seasons, collections)"
msgstr "Visa tomma mappar (serier, säsonger, samlingar)"
msgctxt "#30287"
msgid "TV Shows - Latest"
msgstr "TV Serier - Senaste"
msgctxt "#30280"
msgid "Missing Title"
msgstr "Saknar titel"
msgctxt "#30289"
msgid "TV Shows - Genres"
msgstr "TV Serier - Genrer"
msgctxt "#30302"
msgid "Existing images : "
msgstr "Existerande bilder:"
msgctxt "#30305"
msgid "Not Found"
msgstr "Hittades Inte"
msgctxt "#30316"
msgid "Connection Error"
msgstr "Anslutnings Error"
msgctxt "#30310"
msgid "Enable Jellyfin remote control"
msgstr "Aktivera Jellyfin fjärrkontrol"
msgctxt "#30325"
msgid " - Genres"
msgstr "- Genrer"
msgctxt "#30337"
msgid "Song"
msgstr "Låt"
msgctxt "#30346"
msgid "Deleteing Cached Images"
msgstr "Raderar Cachade Bilder"
msgctxt "#30354"
msgid "Go To Series"
msgstr "Gå Till Serie"
msgctxt "#30361"
msgid " - Programs"
msgstr "- Program"
msgctxt "#30362"
msgid " - Recordings"
msgstr "- Inspelningar"
msgctxt "#30363"
msgid "Save Password?"
msgstr "Spara Lösenord?"
msgctxt "#30374"
msgid "Sending request"
msgstr "Skickar begäran"
msgctxt "#30386"
msgid "Unused Jellyfin images : "
msgstr "Oanvända Jellyfin bilder:"
msgctxt "#30395"
msgid "Clear cached server data"
msgstr "Töm cachad server data"
msgctxt "#30394"
msgid "Cache files deleted"
msgstr "Cache filer raderade"
msgctxt "#30393"
msgid "Clear Cache Result"
msgstr "Töm Cache Resultat"
msgctxt "#30392"
msgid "HTTPS"
msgstr "HTTPS"
msgctxt "#30391"
msgid "HTTP"
msgstr "HTTP"
msgctxt "#30121"
msgid "On resume"
msgstr "Vid återupptagning"
msgctxt "#30201"
msgid "Unable to connect to server"
msgstr "Kunde inte ansluta till servern"
msgctxt "#30216"
msgid "Item Details"
msgstr "Objekt Detaljer"
msgctxt "#30183"
msgid "Include people"
msgstr "Inkludera personer"
msgctxt "#30182"
msgid "Include media stream info"
msgstr "Inkludera media stream information"
msgctxt "#30181"
msgid "Include plot"
msgstr "Inkludera intrig"
msgctxt "#30214"
msgid "Events"
msgstr "Evenemang"
msgctxt "#30222"
msgid "Item Layout"
msgstr "Objekt Layout"
msgctxt "#30241"
msgid "Force transcode mpeg4"
msgstr "Tvinga mpeg4 transkodning"
msgctxt "#30260"
msgid "BoxSets"
msgstr "BoxSet"
msgctxt "#30259"
msgid "Movies - Favorites"
msgstr "Filmer - Favoriter"
msgctxt "#30258"
msgid "Movies - In Progress"
msgstr "Filmer - Pågående"
msgctxt "#30266"
msgid "Movies - Pages"
msgstr "Filmer - Sidor"
msgctxt "#30265"
msgid "Episodes - Next Up"
msgstr "Avsnitt - Härnäst"
msgctxt "#30264"
msgid "Episodes - In Progress"
msgstr "Avsnitt - Pågående"
msgctxt "#30263"
msgid "Episodes - Recently Added"
msgstr "Avsnitt - Nyligen Tillagt"
msgctxt "#30275"
msgid "Force Transcode"
msgstr "Tvinga Transkoding"
msgctxt "#30273"
msgid "Unset Favourite"
msgstr "Ta Bort Som Favorit"
msgctxt "#30272"
msgid "Set Favourite"
msgstr "Sätt Som Favorit"
msgctxt "#30271"
msgid "Mark Unwatched"
msgstr "Markera Som Ej Sedd"
msgctxt "#30270"
msgid "Mark Watched"
msgstr "Markera Som Sedd"
msgctxt "#30269"
msgid "Movies - Random"
msgstr "Filmer - Slumpmässig"
msgctxt "#30268"
msgid " - Recently Added"
msgstr "- Nyligen Tillagt"
msgctxt "#30267"
msgid " - In Progress"
msgstr "- Pågående"
msgctxt "#30021"
msgid "Show all episodes item"
msgstr "Visa alla avsnitt"
msgctxt "#30262"
msgid "TV Shows - Favorites"
msgstr "TV Serier - Favoriter"
msgctxt "#30251"
msgid "Movies - Genres"
msgstr "Filmer - Genrer"
msgctxt "#30237"
msgid "Start from beginning"
msgstr "Starta från början"
msgctxt "#30252"
msgid "Movies - A-Z"
msgstr "Filmer - A-Z"
msgctxt "#30261"
msgid "TV Shows"
msgstr "TV Serier"
msgctxt "#30255"
msgid "TV Shows - A-Z"
msgstr "TV Serier - A-Z"
msgctxt "#30257"
msgid "Movies - Recently Added"
msgstr "Filmer - Nyligen Tillagda"
msgctxt "#30256"
msgid "Movies"
msgstr "Filmer"
msgctxt "#30246"
msgid "Search"
msgstr "Sök"
msgctxt "#30229"
msgid "TV Shows"
msgstr "TV Serier"
msgctxt "#30231"
msgid "Movies"
msgstr "Filmer"
msgctxt "#30235"
msgid "Episodes"
msgstr "Avsnitt"
msgctxt "#30206"
msgid "Playback type"
msgstr "Uppspelnings typ"
msgctxt "#30207"
msgid "Playback"
msgstr "Uppspelning"
msgctxt "#30000"
msgid "Host"
msgstr "Värd"
msgctxt "#30290"
msgid "All"
msgstr "Alla"
msgctxt "#30274"
msgid "Delete"
msgstr "Radera"
msgctxt "#30288"
msgid " - Latest"
msgstr "- Senaste"
msgctxt "#30296"
msgid "Delete"
msgstr "Radera"
msgctxt "#30309"
msgid "Select Media Source"
msgstr "Välj Media Källa"
msgctxt "#30322"
msgid "Auto resume"
msgstr "Återuppta automatiskt"
msgctxt "#30320"
msgid " - Albums"
msgstr "- Album"
msgctxt "#30318"
msgid "Music - Albums"
msgstr "Musik - Album"
msgctxt "#30317"
msgid "Play All"
msgstr "Spela Alla"
msgctxt "#30306"
msgid "Playback starting"
msgstr "Uppspelning startar"
msgctxt "#30312"
msgid "All - "
msgstr "Alla -"
msgctxt "#30311"
msgid "Library - "
msgstr "Bibliotek -"
msgctxt "#30308"
msgid "Select Trailer"
msgstr "Välj Trailer"
msgctxt "#30307"
msgid "Play Trailer"
msgstr "Spela Trailer"
msgctxt "#30313"
msgid "Menu"
msgstr "Meny"
msgctxt "#30331"
msgid "Movies per page"
msgstr "Filmer per sida"
msgctxt "#30314"
msgid "Play"
msgstr "Spela"
msgctxt "#30329"
msgid "Screensaver"
msgstr "Skärmsläckare"
msgctxt "#30402"
msgid "Add to Kodi Playlist"
msgstr "Lägg till i Kodi Spellista"
msgctxt "#30397"
msgid " - Pages"
msgstr "- Sidor"
msgctxt "#30383"
msgid "System - "
msgstr "System -"
msgctxt "#30381"
msgid "More than one"
msgstr "Mer än en gång"
msgctxt "#30377"
msgid "Sending request"
msgstr "Skickar förfrågan"
msgctxt "#30410"
msgid " - Collections"
msgstr "- Samlingar"
msgctxt "#30399"
msgid "Hide"
msgstr "Göm"
msgctxt "#30382"
msgid "Always"
msgstr "Alltid"
msgctxt "#30380"
msgid "Never"
msgstr "Aldrig"
msgctxt "#30431"
msgid "Seasons"
msgstr "Säsonger"
msgctxt "#30166"
msgid "Select Server"
msgstr "Välj Server"
msgctxt "#30200"
msgid "URL error"
msgstr "URL error"
msgctxt "#30114"
msgid "Jump back seconds"
msgstr "Hoppa tillbaka sekunder"
msgctxt "#30125"
msgid "Done"
msgstr "Färdig"
msgctxt "#30139"
msgid "No Media Type Set"
msgstr "Ingen Media Typ Satt"
msgctxt "#30389"
msgid "User details"
msgstr "Användar Detaljer"
msgctxt "#30404"
msgid " - A-Z"
msgstr "- A-Z"
msgctxt "#30413"
msgid " - Tags"
msgstr "- Etiketter"
msgctxt "#30422"
msgid "Sorting"
msgstr "Sorterar"
msgctxt "#30441"
msgid "Use cached widget data"
msgstr "Använd cachad widget data"
msgctxt "#30440"
msgid "Play next"
msgstr "Spela härnest"
msgctxt "#30250"
msgid "Unknown"
msgstr "Okänd"
msgctxt "#30167"
msgid "Selected Server Address"
msgstr "Vald Server Adress"
msgctxt "#30111"
msgid "Services"
msgstr "Tjänster"
msgctxt "#30091"
msgid "Confirm delete?"
msgstr "Bekräfta radering?"
msgctxt "#30052"
msgid "Deleting"
msgstr "Raderar"
msgctxt "#30022"
msgid "Advanced"
msgstr "Avancerat"
msgctxt "#30011"
msgid "[Detect local server]"
msgstr "[Upptäck lokal server]"
msgctxt "#30003"
msgid "Verify HTTPS certificate"
msgstr "Verifiera HTTPS certifikat"
msgctxt "#30001"
msgid "Port"
msgstr "Port"
msgctxt "#30327"
msgid "Go To Season"
msgstr "Gå Till Säsong"
msgctxt "#30437"
msgid "Playback options"
msgstr "Uppspelnings alternativ"
msgctxt "#30433"
msgid "Allow direct file playback"
msgstr "Tillåt direkt fil uppspelning"
msgctxt "#30432"
msgid "Hide watched items in lists"
msgstr "Dölj visade objekt i listor"
msgctxt "#30430"
msgid "Label"
msgstr "Etikett"
msgctxt "#30429"
msgid "Genre"
msgstr "Genre"
msgctxt "#30427"
msgid "Added"
msgstr "Tillagd"
msgctxt "#30426"
msgid "Title"
msgstr "Titel"
msgctxt "#30425"
msgid "Year"
msgstr "År"
msgctxt "#30428"
msgid "Rating"
msgstr "Betyg"
msgctxt "#30417"
msgid "You do not have permision to delete this item"
msgstr "Du har inte behörighet att ta bort detta objekt"
msgctxt "#30412"
msgid " - Decades"
msgstr "- Decennier"
msgctxt "#30421"
msgid "Views"
msgstr "Visningar"
msgctxt "#30406"
msgid "Jellyfin Libraries"
msgstr "Jellyfin Bibliotek"
msgctxt "#30388"
msgid "Server details"
msgstr "Server Detaljer"
msgctxt "#30403"
msgid "Movies - Recommendations"
msgstr "Filmer - Rekommendationer"
msgctxt "#30405"
msgid " - Show All"
msgstr "- Visa Alla"
msgctxt "#30390"
msgid "Protocol"
msgstr "Protokol"
msgctxt "#30401"
msgid "Info"
msgstr "Information"
msgctxt "#30411"
msgid " - Years"
msgstr "- År"
msgctxt "#30424"
msgid "Default"
msgstr "Standard"
msgctxt "#30180"
msgid "Select User"
msgstr "Välj användare"
msgctxt "#30169"
msgid "Address: "
msgstr "Adress:"
msgctxt "#30110"
msgid "Interface"
msgstr "Användargränssnitt"
msgctxt "#30135"
msgid "Error"
msgstr "Error"
msgctxt "#30113"
msgid "Retrieving Data"
msgstr "Hämtar data"
msgctxt "#30112"
msgid "Loading Content"
msgstr "Laddar Innehåll"
msgctxt "#30045"
msgid "Username not found"
msgstr "Användarnamn kunde inte hittas"
msgctxt "#30012"
msgid "[Change user]"
msgstr "[Ändra användare]"
msgctxt "#30025"
msgid "Password:"
msgstr "Lösenord:"
msgctxt "#30024"
msgid "Username:"
msgstr "Användarnamn:"
msgctxt "#30017"
msgid "Show connected clients"
msgstr "Visa anslutna enheter"
msgctxt "#30014"
msgid "Jellyfin"
msgstr "Jellyfin"
msgctxt "#30008"
msgid "Samba password"
msgstr "Samba lösenord"
msgctxt "#30007"
msgid "Samba username"
msgstr "Samba användarnamn"
msgctxt "#30006"
msgid "Password"
msgstr "Lösenord"
msgctxt "#30005"
msgid "Username"
msgstr "Användarnamn"
msgctxt "#30332"
msgid "Stop media playback on screensaver activation"
msgstr "Stoppa mediauppspelning vid aktivering av skärmsläckare"
msgctxt "#30330"
msgid "Show change user dialog"
msgstr "Visa ändra användardialogrutan"
#, fuzzy
msgctxt "#30315"
msgid "Suppress notifications for connection errors"
msgstr "Undertryck aviseringar för anslutningsfel"
msgctxt "#30304"
msgid "Cached Jellyfin images : "
msgstr "Cachade Jellyfin-bilder:"
msgctxt "#30303"
msgid "Missing Jellyfin images : "
msgstr "Saknade Jellyfin-bilder:"
msgctxt "#30301"
msgid "Caching Images"
msgstr "Cacha bilder"
msgctxt "#30300"
msgid "Cache all Jellyfin images as local Kodi images?"
msgstr "Cache alla Jellyfin-bilder som lokala Kodi-bilder?"
msgctxt "#30299"
msgid "Cache Images"
msgstr "Cachebilder"
msgctxt "#30298"
msgid "Deleting Kodi Images"
msgstr "Ta bort Kodi-bilder"
msgctxt "#30297"
msgid "Delete unused images?"
msgstr "Ta bort oanvända bilder?"
msgctxt "#30295"
msgid "To use this feature you need HTTP control enabled"
msgstr "För att använda den här funktionen måste du aktivera HTTP-kontroll"
#, fuzzy
msgctxt "#30294"
msgid "Notice"
msgstr "Notis"
msgctxt "#30293"
msgid "Cache images"
msgstr "Cachebilder"
msgctxt "#30292"
msgid "Select Subtitle Stream"
msgstr "Välj undertextström"
msgctxt "#30291"
msgid "Select Audio Stream"
msgstr "Välj ljudström"
#, fuzzy
msgctxt "#30118"
msgid "Add resume percent to names"
msgstr "Lägg till fortsättningsprocent i namn"
#, fuzzy
msgctxt "#30116"
msgid "Add unwatched counts to names"
msgstr "Lägg till ej sedda räkningar till namn"
msgctxt "#30279"
msgid "TV Shows - Unwatched"
msgstr "TV Serier- Ej sedd"
#, fuzzy
msgctxt "#30286"
msgid "Movies - Unwatched"
msgstr "Film - Ej sedd"
msgctxt "#30285"
msgid " - Unwatched"
msgstr "- Ej sedd"
msgctxt "#30282"
msgid "No Jellyfin servers detected on your local network."
msgstr "Inga Jellyfin-servrar upptäcktes på ditt lokala nätverk."
msgctxt "#30281"
msgid "Refresh Cached Images"
msgstr "Uppdatera cachelagrade bilder"
#, fuzzy
msgctxt "#30278"
msgid " - Next Up"
msgstr "- Nästa upp"
#, fuzzy
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 måste fråga om återuppta på partiellt spelade objekt, Kodi kan "
"också fråga, detta kan orsaka en dubbel prompt. Vill du ta bort den dubbla "
"prompten?"
#, fuzzy
msgctxt "#30276"
msgid "Extra Resume Prompt Detected"
msgstr "Extra uppmaning om återuppta upptäckt"
msgctxt "#30247"
msgid "Custom Widget Content"
msgstr "Anpassat widgetinnehåll"
msgctxt "#30238"
msgid "Playback stream options"
msgstr "Alternativ för uppspelning av stream"
msgctxt "#30220"
msgid "Prompt to delete movie after %"
msgstr "Uppmaning att radera film efter %"
#, fuzzy
msgctxt "#30219"
msgid " - Prompt before play"
msgstr "- Fråga före uppspelning"
msgctxt "#30217"
msgid "Prompt to delete episode after %"
msgstr "Uppmaning att radera avsnitt efter %"
#, fuzzy
msgctxt "#30215"
msgid "On playback stop (100% = disabled)"
msgstr "Vid uppspelningsstopp (100 % = avaktiverad)"
#, fuzzy
msgctxt "#30210"
msgid "HTTP direct stream"
msgstr "HTTP direktström"
msgctxt "#30209"
msgid "File direct path"
msgstr "Direkt sökväg till filen"
msgctxt "#30208"
msgid "Max stream bitrate (Kbits)"
msgstr "Högsta strömbithastighet (kbps)"
msgctxt "#30126"
msgid "Processing Item : "
msgstr "Bearbetning av objektet:"
msgctxt "#30120"
msgid "Show load progress"
msgstr "Visa laddningsförloppet"
#, fuzzy
msgctxt "#30063"
msgid "N/A"
msgstr "Ej tillgängligt"
#, fuzzy
msgctxt "#30053"
msgid "Waiting for server to delete"
msgstr "Väntar på att servern ska radera"
msgctxt "#30044"
msgid "Incorrect Username/Password"
msgstr "Fel användarnamn/lösenord"
msgctxt "#30027"
msgid "Enable debug logging"
msgstr "Aktivera felsökningsloggning"
#, fuzzy
msgctxt "#30026"
msgid "Widget item select action"
msgstr "Välj åtgärd för widgetobjekt"
msgctxt "#30023"
msgid "Hide unwatched episode details"
msgstr "Dölj detaljer om avsnittet som inte har setts"
#, fuzzy
msgctxt "#30020"
msgid "Flatten single season"
msgstr "Platta enstaka säsong"
#, fuzzy
msgctxt "#30019"
msgid "Filtered episode name format"
msgstr "Filtrerat avsnittsnamnformat"
msgctxt "#30018"
msgid "Number of items to show in filtered lists"
msgstr "Antal objekt som ska visas i filtrerade listor"
msgctxt "#30016"
msgid "Device display name"
msgstr "Enhetens visningsnamn"
#, fuzzy
msgctxt "#30015"
msgid "Log timing data"
msgstr "Logga tidsdata"
#, fuzzy
msgctxt "#30010"
msgid "Number of performance profiles to capture"
msgstr "Antal prestandaprofiler som ska fångas"

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,104 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2022-04-14 06:13+0000\n"
"Last-Translator: rayanamukami <rayanamukami@gmail.com>\n"
"Language-Team: Chinese (Traditional) <https://translate.jellyfin.org/"
"projects/jellycon/jellycon/zh_Hant/>\n"
"Language: zh_Hant\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 4.10.1\n"
msgctxt "#30207"
msgid "Playback"
msgstr "撥放"
msgctxt "#30206"
msgid "Playback type"
msgstr "撥放類型"
msgctxt "#30201"
msgid "Unable to connect to server"
msgstr "無法連接到伺服器"
msgctxt "#30200"
msgid "URL error"
msgstr "網址錯誤"
msgctxt "#30180"
msgid "Select User"
msgstr "選擇使用者"
msgctxt "#30167"
msgid "Selected Server Address"
msgstr "選擇伺服器位置"
msgctxt "#30166"
msgid "Select Server"
msgstr "選擇伺服器"
msgctxt "#30135"
msgid "Error"
msgstr "錯誤"
msgctxt "#30063"
msgid "N/A"
msgstr "N/A"
msgctxt "#30091"
msgid "Confirm delete?"
msgstr "是否確認刪除?"
msgctxt "#30025"
msgid "Password:"
msgstr "密碼:"
msgctxt "#30024"
msgid "Username:"
msgstr "使用者名稱:"
msgctxt "#30110"
msgid "Interface"
msgstr "介面"
msgctxt "#30022"
msgid "Advanced"
msgstr "進階"
msgctxt "#30014"
msgid "Jellyfin"
msgstr "Jellyfin"
msgctxt "#30012"
msgid "[Change user]"
msgstr "[變更使用者]"
msgctxt "#30008"
msgid "Samba password"
msgstr "Samba 密碼"
msgctxt "#30007"
msgid "Samba username"
msgstr "Samba 使用者名稱"
msgctxt "#30003"
msgid "Verify HTTPS certificate"
msgstr "驗證 HTTPS 憑證"
msgctxt "#30006"
msgid "Password"
msgstr "密碼"
msgctxt "#30005"
msgid "Username"
msgstr "使用者名稱"
msgctxt "#30001"
msgid "Port"
msgstr "埠"
msgctxt "#30000"
msgid "Host"
msgstr "主機"

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
@@ -6,9 +7,9 @@ import threading
import xbmc
import xbmcgui
from .simple_logging import SimpleLogging
from .lazylogger import LazyLogger
log = SimpleLogging(__name__)
log = LazyLogger(__name__)
class ActionAutoClose(threading.Thread):
@@ -25,9 +26,9 @@ 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}", time_since_last)
log.debug("ActionAutoClose time_since_last : {0}".format(time_since_last))
if time_since_last > 20:
log.debug("ActionAutoClose Closing Parent")
@@ -40,7 +41,7 @@ class ActionAutoClose(threading.Thread):
def set_last(self):
self.last_interaction = time.time()
log.debug("ActionAutoClose set_last : {0}", self.last_interaction)
log.debug("ActionAutoClose set_last : {0}".format(self.last_interaction))
def stop(self):
log.debug("ActionAutoClose stop_thread called")
@@ -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
@@ -79,7 +77,7 @@ class ActionMenu(xbmcgui.WindowXMLDialog):
pass
def onMessage(self, message):
log.debug("ActionMenu: onMessage: {0}", message)
log.debug("ActionMenu: onMessage: {0}".format(message))
def onAction(self, action):
@@ -91,12 +89,12 @@ class ActionMenu(xbmcgui.WindowXMLDialog):
self.close()
else:
self.auto_close_thread.set_last()
log.debug("ActionMenu: onAction: {0}", action.getId())
log.debug("ActionMenu: onAction: {0}".format(action.getId()))
def onClick(self, control_id):
if control_id == 3000:
self.selected_action = self.listControl.getSelectedItem()
log.debug("ActionMenu: Selected Item: {0}", self.selected_action)
log.debug("ActionMenu: Selected Item: {0}".format(self.selected_action))
self.auto_close_thread.stop()
self.close()

View File

@@ -1,58 +0,0 @@
import xbmc
import xbmcgui
from .simple_logging import SimpleLogging
log = SimpleLogging(__name__)
class BitrateDialog(xbmcgui.WindowXMLDialog):
slider_control = None
bitrate_label = None
initial_bitrate_value = 0
selected_transcode_value = 0
def __init__(self, *args, **kwargs):
log.debug("BitrateDialog: __init__")
xbmcgui.WindowXML.__init__(self, *args, **kwargs)
def onInit(self):
log.debug("ActionMenu: onInit")
self.action_exitkeys_id = [10, 13]
self.slider_control = self.getControl(3000)
self.slider_control.setInt(self.initial_bitrate_value, 400, 100, 15000)
self.bitrate_label = self.getControl(3030)
bitrate_label_string = str(self.slider_control.getInt()) + " Kbs"
self.bitrate_label.setLabel(bitrate_label_string)
def onFocus(self, control_id):
pass
def doAction(self, action_id):
pass
def onMessage(self, message):
log.debug("ActionMenu: onMessage: {0}", message)
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)
if action.getId() == 10: # ACTION_PREVIOUS_MENU
self.close()
elif action.getId() == 92: # ACTION_NAV_BACK
self.close()
elif action.getId() == 7: # ENTER
self.selected_transcode_value = self.slider_control.getInt()
self.close()
def onClick(self, control_id):
if control_id == 3000:
log.debug("ActionMenu: Selected Item: {0}", control_id)
#self.close()

View File

@@ -1,8 +1,9 @@
# coding=utf-8
# Gnu General Public License - see LICENSE.TXT
from __future__ import division, absolute_import, print_function, unicode_literals
import urllib
import httplib
from six.moves.urllib.parse import unquote
import requests
import base64
import sys
import threading
@@ -13,16 +14,14 @@ import xbmcplugin
import xbmc
import xbmcaddon
from .downloadutils import DownloadUtils
from .simple_logging import SimpleLogging
from .jsonrpc import JsonRpc
from .translation import string_load
from .datamanager import DataManager
from .utils import get_art, double_urlencode
from .jellyfin import api
from .lazylogger import LazyLogger
from .jsonrpc import JsonRpc, get_value
from .utils import translate_string, load_user_details
from .kodi_utils import HomeWindow
from .item_functions import get_art
downloadUtils = DownloadUtils()
log = SimpleLogging(__name__)
log = LazyLogger(__name__)
class CacheArtwork(threading.Thread):
@@ -62,34 +61,34 @@ class CacheArtwork(threading.Thread):
monitor.waitForAbort(5)
log.debug("CacheArtwork background thread exited : stop_all_activity : {0}", self.stop_all_activity)
log.debug("CacheArtwork background thread exited : stop_all_activity : {0}".format(self.stop_all_activity))
@staticmethod
def delete_cached_images(item_id):
log.debug("cache_delete_for_links")
progress = xbmcgui.DialogProgress()
progress.create(string_load(30281))
progress.update(30, string_load(30347))
progress.create(translate_string(30281))
progress.update(30, translate_string(30347))
item_image_url_part = "Items/%s/Images/" % item_id
item_image_url_part = item_image_url_part.replace("/", "%2f")
log.debug("texture ids: {0}", item_image_url_part)
log.debug("texture ids: {0}".format(item_image_url_part))
# is the web server enabled
web_query = {"setting": "services.webserver"}
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))
xbmcgui.Dialog().ok(translate_string(30294), translate_string(30295))
return
params = {"properties": ["url"]}
json_result = JsonRpc('Textures.GetTextures').execute(params)
textures = json_result.get("result", {}).get("textures", [])
log.debug("texture ids: {0}", textures)
log.debug("texture ids: {0}".format(textures))
progress.update(70, string_load(30346))
progress.update(70, translate_string(30346))
delete_count = 0
for texture in textures:
@@ -97,16 +96,16 @@ class CacheArtwork(threading.Thread):
texture_url = texture["url"]
if item_image_url_part in texture_url:
delete_count += 1
log.debug("removing texture id: {0}", texture_id)
log.debug("removing texture id: {0}".format(texture_id))
params = {"textureid": int(texture_id)}
JsonRpc('Textures.RemoveTexture').execute(params)
del textures
progress.update(100, string_load(30125))
progress.update(100, translate_string(30125))
progress.close()
xbmcgui.Dialog().ok(string_load(30281), string_load(30344) % delete_count)
xbmcgui.Dialog().ok(translate_string(30281), '{}: {}'.format(translate_string(30344), delete_count))
def cache_artwork_interactive(self):
log.debug("cache_artwork_interactive")
@@ -118,21 +117,21 @@ 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(translate_string(30294), '{} - {}'.format(translate_string(30295), translate_string(30355)))
xbmc.executebuiltin('ActivateWindow(servicesettings)')
return
result_report = []
# ask questions
question_delete_unused = xbmcgui.Dialog().yesno(string_load(30296), string_load(30297))
question_cache_images = xbmcgui.Dialog().yesno(string_load(30299), string_load(30300))
question_delete_unused = xbmcgui.Dialog().yesno(translate_string(30296), translate_string(30297))
question_cache_images = xbmcgui.Dialog().yesno(translate_string(30299), translate_string(30300))
delete_canceled = False
# now do work - delete unused
if question_delete_unused:
delete_pdialog = xbmcgui.DialogProgress()
delete_pdialog.create(string_load(30298), "")
delete_pdialog.create(translate_string(30298), "")
index = 0
params = {"properties": ["url"]}
@@ -141,23 +140,22 @@ class CacheArtwork(threading.Thread):
jellyfin_texture_urls = self.get_jellyfin_artwork(delete_pdialog)
log.debug("kodi textures: {0}", textures)
log.debug("jellyfin texture urls: {0}", jellyfin_texture_urls)
log.debug("kodi textures: {0}".format(textures))
log.debug("jellyfin texture urls: {0}".format(jellyfin_texture_urls))
if jellyfin_texture_urls is not None:
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)
log.debug("unused texture ids: {0}", unused_texture_ids)
log.debug("unused texture ids: {0}".format(unused_texture_ids))
for texture_id in unused_texture_ids:
params = {"textureid": int(texture_id)}
@@ -171,9 +169,9 @@ class CacheArtwork(threading.Thread):
delete_canceled = True
break
result_report.append(string_load(30385) + str(len(textures)))
result_report.append(string_load(30386) + str(len(unused_texture_ids)))
result_report.append(string_load(30387) + str(index))
result_report.append(translate_string(30385) + str(len(textures)))
result_report.append(translate_string(30386) + str(len(unused_texture_ids)))
result_report.append(translate_string(30387) + str(index))
del textures
del jellyfin_texture_urls
@@ -187,7 +185,7 @@ class CacheArtwork(threading.Thread):
# now do work - cache images
if question_cache_images:
cache_pdialog = xbmcgui.DialogProgress()
cache_pdialog.create(string_load(30301), "")
cache_pdialog.create(translate_string(30301), "")
cache_report = self.cache_artwork(cache_pdialog)
cache_pdialog.close()
del cache_pdialog
@@ -196,27 +194,29 @@ class CacheArtwork(threading.Thread):
if len(result_report) > 0:
msg = "\r\n".join(result_report)
xbmcgui.Dialog().textviewer(string_load(30125), msg, usemono=True)
xbmcgui.Dialog().textviewer(translate_string(30125), msg, usemono=True)
def cache_artwork_background(self):
log.debug("cache_artwork_background")
dp = xbmcgui.DialogProgressBG()
dp.create(string_load(30301), "")
dp.create(translate_string(30301), "")
result_text = None
try:
result_text = self.cache_artwork(dp)
except Exception as err:
log.error("Cache Images Failed : {0}", err)
log.error("Cache Images Failed : {0}".format(err))
dp.close()
del dp
if result_text is not None:
log.debug("Cache Images reuslt : {0}", " - ".join(result_text))
log.debug("Cache Images reuslt : {0}".format(" - ".join(result_text)))
def get_jellyfin_artwork(self, progress):
log.debug("get_jellyfin_artwork")
user_details = load_user_details()
user_id = user_details.get('user_id')
url = ""
url += "{server}/Users/{userid}/Items"
url += "/Users/{}/Items".format(user_id)
url += "?Recursive=true"
url += "&EnableUserData=False"
url += "&Fields=BasicSyncInfo"
@@ -224,25 +224,24 @@ class CacheArtwork(threading.Thread):
url += "&ImageTypeLimit=1"
url += "&format=json"
data_manager = DataManager()
results = data_manager.get_content(url)
results = api.get(url)
if results is None:
results = []
if isinstance(results, dict):
results = results.get("Items")
server = downloadUtils.get_server()
log.debug("Jellyfin Item Count Count: {0}", len(results))
settings = xbmcaddon.Addon()
server = settings.getSetting('server_address')
log.debug("Jellyfin Item Count Count: {0}".format(len(results)))
if self.stop_all_activity:
return None
progress.update(0, string_load(30359))
progress.update(0, translate_string(30359))
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:
@@ -254,46 +253,37 @@ class CacheArtwork(threading.Thread):
log.debug("cache_artwork")
# is the web server enabled
web_query = {"setting": "services.webserver"}
result = JsonRpc('Settings.GetSettingValue').execute(web_query)
xbmc_webserver_enabled = result['result']['value']
if not xbmc_webserver_enabled:
if not get_value("services.webserver"):
log.error("Kodi web server not enabled, can not cache images")
return
# get the port
web_port = {"setting": "services.webserverport"}
result = JsonRpc('Settings.GetSettingValue').execute(web_port)
xbmc_port = result['result']['value']
log.debug("xbmc_port: {0}", xbmc_port)
xbmc_port = get_value("services.webserverport")
log.debug("xbmc_port: {0}".format(xbmc_port))
# get the user
web_user = {"setting": "services.webserverusername"}
result = JsonRpc('Settings.GetSettingValue').execute(web_user)
xbmc_username = result['result']['value']
log.debug("xbmc_username: {0}", xbmc_username)
xbmc_username = get_value("services.webserverusername")
log.debug("xbmc_username: {0}".format(xbmc_username))
# get the password
web_pass = {"setting": "services.webserverpassword"}
result = JsonRpc('Settings.GetSettingValue').execute(web_pass)
xbmc_password = result['result']['value']
xbmc_password = get_value("services.webserverpassword")
progress.update(0, string_load(30356))
progress.update(0, translate_string(30356))
params = {"properties": ["url"]}
json_result = JsonRpc('Textures.GetTextures').execute(params)
textures = json_result.get("result", {}).get("textures", [])
log.debug("Textures.GetTextures Count: {0}", len(textures))
log.debug("Textures.GetTextures Count: {0}".format(len(textures)))
if self.stop_all_activity:
return
progress.update(0, string_load(30357))
progress.update(0, translate_string(30357))
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)
@@ -301,19 +291,19 @@ class CacheArtwork(threading.Thread):
del textures
del json_result
log.debug("texture_urls Count: {0}", len(texture_urls))
log.debug("texture_urls Count: {0}".format(len(texture_urls)))
if self.stop_all_activity:
return
progress.update(0, string_load(30358))
progress.update(0, translate_string(30358))
jellyfin_texture_urls = self.get_jellyfin_artwork(progress)
if jellyfin_texture_urls is None:
return
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)
@@ -321,10 +311,10 @@ class CacheArtwork(threading.Thread):
if self.stop_all_activity:
return
log.debug("texture_urls: {0}", texture_urls)
log.debug("missing_texture_urls: {0}", missing_texture_urls)
log.debug("Number of existing textures: {0}", len(texture_urls))
log.debug("Number of missing textures: {0}", len(missing_texture_urls))
log.debug("texture_urls: {0}".format(texture_urls))
log.debug("missing_texture_urls: {0}".format(missing_texture_urls))
log.debug("Number of existing textures: {0}".format(len(texture_urls)))
log.debug("Number of missing textures: {0}".format(len(missing_texture_urls)))
kodi_http_server = "localhost:" + str(xbmc_port)
headers = {}
@@ -333,29 +323,23 @@ class CacheArtwork(threading.Thread):
headers = {'Authorization': 'Basic %s' % base64.b64encode(auth)}
total = len(missing_texture_urls)
index = 1
count_done = 0
for get_url in missing_texture_urls:
# log.debug("texture_url: {0}", get_url)
url = double_urlencode(get_url)
kodi_texture_url = ("/image/image://%s" % url)
log.debug("kodi_texture_url: {0}", kodi_texture_url)
for index, get_url in enumerate(missing_texture_urls, 1):
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)
message = "%s of %s" % (index, total)
progress.update(percentage, message)
conn = httplib.HTTPConnection(kodi_http_server, timeout=20)
conn.request(method="GET", url=kodi_texture_url, headers=headers)
data = conn.getresponse()
if data.status == 200:
count_done += 1
log.debug("Get Image Result: {0}", data.status)
cache_url = "http://%s%s" % (kodi_http_server, kodi_texture_url)
data = requests.get(cache_url, timeout=20, headers=headers)
if data.status_code == 200:
count_done += 1
log.debug("Get Image Result: {0}".format(data.status_code))
index += 1
# if progress.iscanceled():
# if "iscanceled" in dir(progress) and progress.iscanceled():
if isinstance(progress, xbmcgui.DialogProgress) and progress.iscanceled():
break
@@ -363,7 +347,7 @@ class CacheArtwork(threading.Thread):
break
result_report = []
result_report.append(string_load(30302) + str(len(texture_urls)))
result_report.append(string_load(30303) + str(len(missing_texture_urls)))
result_report.append(string_load(30304) + str(count_done))
result_report.append(translate_string(30302) + str(len(texture_urls)))
result_report.append(translate_string(30303) + str(len(missing_texture_urls)))
result_report.append(translate_string(30304) + str(count_done))
return result_report

View File

@@ -1,52 +0,0 @@
# Gnu General Public License - see LICENSE.TXT
from uuid import uuid4 as uuid4
import xbmcaddon
import xbmc
import xbmcvfs
from .kodi_utils import HomeWindow
from .simple_logging import SimpleLogging
log = SimpleLogging(__name__)
class ClientInformation:
@staticmethod
def get_device_id():
window = HomeWindow()
client_id = window.get_property("client_id")
if client_id:
return client_id
jellyfin_guid_path = xbmc.translatePath("special://temp/jellycon_guid").decode('utf-8')
log.debug("jellyfin_guid_path: {0}", jellyfin_guid_path)
guid = xbmcvfs.File(jellyfin_guid_path)
client_id = guid.read()
guid.close()
if not client_id:
client_id = str("%012X" % uuid4())
log.debug("Generating a new guid: {0}", client_id)
guid = xbmcvfs.File(jellyfin_guid_path, 'w')
guid.write(client_id)
guid.close()
log.debug("jellyfin_client_id (NEW): {0}", client_id)
else:
log.debug("jellyfin_client_id: {0}", client_id)
window.set_property("client_id", client_id)
return client_id
@staticmethod
def get_version():
addon = xbmcaddon.Addon()
version = addon.getAddonInfo("version")
return version
@staticmethod
def get_client():
return 'Kodi JellyCon'

View File

@@ -1,75 +0,0 @@
import threading
import xbmc
from .simple_logging import SimpleLogging
from resources.lib.functions import show_menu
log = SimpleLogging(__name__)
class ContextMonitor(threading.Thread):
stop_thread = False
def run(self):
item_id = None
log.debug("ContextMonitor Thread Started")
while not xbmc.abortRequested and not self.stop_thread:
if xbmc.getCondVisibility("Window.IsActive(fullscreenvideo) | Window.IsActive(visualisation)"):
xbmc.sleep(1000)
else:
if xbmc.getCondVisibility("Window.IsVisible(contextmenu)"):
if item_id:
xbmc.executebuiltin("Dialog.Close(contextmenu,true)")
params = {}
params["item_id"] = item_id
show_menu(params)
container_id = xbmc.getInfoLabel("System.CurrentControlID")
item_id = xbmc.getInfoLabel("Container(" + str(container_id) + ").ListItem.Property(id)")
xbmc.sleep(100)
'''
context_up = False
is_jellycon_item = False
while not xbmc.abortRequested and not self.stop_thread:
if xbmc.getCondVisibility("Window.IsActive(fullscreenvideo) | Window.IsActive(visualisation)"):
xbmc.sleep(1000)
else:
if xbmc.getCondVisibility("Window.IsVisible(contextmenu)"):
context_up = True
if is_jellycon_item:
xbmc.executebuiltin("Dialog.Close(contextmenu,true)")
else:
if context_up: # context now down, do something
context_up = False
container_id = xbmc.getInfoLabel("System.CurrentControlID")
log.debug("ContextMonitor Container ID: {0}", container_id)
item_id = xbmc.getInfoLabel("Container(" + str(container_id) + ").ListItem.Property(id)")
log.debug("ContextMonitor Item ID: {0}", item_id)
if item_id:
params = {}
params["item_id"] = item_id
show_menu(params)
container_id = xbmc.getInfoLabel("System.CurrentControlID")
condition = ("String.StartsWith(Container(" + str(container_id) +
").ListItem.Path,plugin://plugin.video.jellycon) + !String.IsEmpty(Container(" +
str(container_id) + ").ListItem.Property(id))")
is_jellycon_item = xbmc.getCondVisibility(condition)
xbmc.sleep(200)
'''
log.debug("ContextMonitor Thread Exited")
def stop_monitor(self):
log.debug("ContextMonitor Stop Called")
self.stop_thread = True

View File

@@ -1,27 +1,26 @@
# 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 .simple_logging import SimpleLogging
from .jellyfin import api
from .lazylogger import LazyLogger
from .item_functions import extract_item_info
from .kodi_utils import HomeWindow
from .translation import string_load
from .tracking import timer
from .filelock import FileLock
from .utils import translate_string, load_user_details, translate_path
import xbmc
import xbmcaddon
import xbmcvfs
import xbmcgui
log = SimpleLogging(__name__)
log = LazyLogger(__name__)
class CacheItem:
@@ -40,42 +39,28 @@ class CacheItem:
class DataManager:
addon_dir = xbmc.translatePath(xbmcaddon.Addon().getAddonInfo('profile'))
addon_dir = translate_path(xbmcaddon.Addon().getAddonInfo('profile'))
def __init__(self, *args):
# log.debug("DataManager __init__")
pass
self.user_details = load_user_details()
@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
self.api = api
@timer
def get_items(self, url, gui_options, use_cache=False):
home_window = HomeWindow()
log.debug("last_content_url : use_cache={0} url={1}", use_cache, url)
log.debug("last_content_url : use_cache={0} url={1}".format(use_cache, url))
home_window.set_property("last_content_url", url)
download_utils = DownloadUtils()
user_id = download_utils.get_user_id()
server = download_utils.get_server()
user_id = self.user_details.get('user_id')
server = self.api.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")
# changed_url = url + "&MinDateLastSavedForUser=" + urllib.unquote("2019-09-16T13:45:30")
# results = self.GetContent(changed_url)
# log.debug("DataManager Changes Since Date : {0}", results)
item_list = None
total_records = 0
baseline_name = None
@@ -102,14 +87,14 @@ class DataManager:
item_list = cache_item.item_list
total_records = cache_item.total_records
except Exception as err:
log.error("Pickle Data Load Failed : {0}", err)
log.error("Pickle Data Load Failed : {0}".format(err))
item_list = None
# we need to load the list item data form the server
if item_list is None or len(item_list) == 0:
log.debug("Loading url data from server")
results = self.get_content(url)
results = self.api.get(url)
if results is None:
results = []
@@ -141,7 +126,6 @@ class DataManager:
cache_item.total_records = total_records
cache_thread.cached_item = cache_item
# copy.deepcopy(item_list)
if not use_cache:
cache_thread = None
@@ -177,7 +161,6 @@ class CacheManagerThread(threading.Thread):
def run(self):
log.debug("CacheManagerThread : Started")
# log.debug("CacheManagerThread : Cache Item : {0}", self.cached_item.__dict__)
home_window = HomeWindow()
is_fresh = False
@@ -206,10 +189,10 @@ class CacheManagerThread(threading.Thread):
else:
log.debug("CacheManagerThread : Reloading to recheck data hashes")
cached_hash = self.cached_item.item_list_hash
log.debug("CacheManagerThread : Cache Hash : {0}", cached_hash)
log.debug("CacheManagerThread : Cache Hash : {0}".format(cached_hash))
data_manager = DataManager()
results = data_manager.get_content(self.cached_item.items_url)
results = data_manager.api.get(self.cached_item.items_url)
if results is None:
results = []
@@ -232,7 +215,7 @@ class CacheManagerThread(threading.Thread):
return
loaded_hash = self.get_data_hash(loaded_items)
log.debug("CacheManagerThread : Loaded Hash : {0}", loaded_hash)
log.debug("CacheManagerThread : Loaded Hash : {0}".format(loaded_hash))
# if they dont match then save the data and trigger a content reload
if cached_hash != loaded_hash:
@@ -252,7 +235,7 @@ class CacheManagerThread(threading.Thread):
# TODO: probably should only set this in simple check mode
current_time_stamp = str(time.time())
home_window.set_property("jellycon_widget_reload", current_time_stamp)
log.debug("Setting New Widget Hash: {0}", current_time_stamp)
log.debug("Setting New Widget Hash: {0}".format(current_time_stamp))
log.debug("CacheManagerThread : Sending container refresh")
xbmc.executebuiltin("Container.Refresh")
@@ -270,30 +253,31 @@ class CacheManagerThread(threading.Thread):
def clear_cached_server_data():
log.debug("clear_cached_server_data() called")
addon_dir = xbmc.translatePath(xbmcaddon.Addon().getAddonInfo('profile'))
addon_dir = translate_path(xbmcaddon.Addon().getAddonInfo('profile'))
dirs, files = xbmcvfs.listdir(addon_dir)
del_count = 0
for filename in files:
if filename.startswith("cache_") and filename.endswith(".pickle"):
log.debug("Deleteing CacheFile: {0}", filename)
log.debug("Deleteing CacheFile: {0}".format(filename))
xbmcvfs.delete(os.path.join(addon_dir, filename))
del_count += 1
msg = string_load(30394) % del_count
xbmcgui.Dialog().ok(string_load(30393), msg)
log.debug('Deleted {} files'.format(del_count))
msg = translate_string(30394)
xbmcgui.Dialog().ok(translate_string(30393), msg)
def clear_old_cache_data():
log.debug("clear_old_cache_data() : called")
addon_dir = xbmc.translatePath(xbmcaddon.Addon().getAddonInfo('profile'))
addon_dir = translate_path(xbmcaddon.Addon().getAddonInfo('profile'))
dirs, files = xbmcvfs.listdir(addon_dir)
del_count = 0
for filename in files:
if filename.startswith("cache_") and filename.endswith(".pickle"):
log.debug("clear_old_cache_data() : Checking CacheFile : {0}", filename)
log.debug("clear_old_cache_data() : Checking CacheFile : {0}".format(filename))
cache_item = None
for x in range(0, 5):
@@ -304,7 +288,7 @@ def clear_old_cache_data():
cache_item = cPickle.load(handle)
break
except Exception as error:
log.debug("clear_old_cache_data() : Pickle load error : {0}", error)
log.debug("clear_old_cache_data() : Pickle load error : {0}".format(error))
cache_item = None
xbmc.sleep(1000)
@@ -313,9 +297,9 @@ def clear_old_cache_data():
if cache_item.date_last_used is not None:
item_last_used = time.time() - cache_item.date_last_used
log.debug("clear_old_cache_data() : Cache item last used : {0} sec ago", item_last_used)
log.debug("clear_old_cache_data() : Cache item last used : {0} sec ago".format(item_last_used))
if item_last_used == -1 or item_last_used > (3600 * 24 * 7):
log.debug("clear_old_cache_data() : Deleting cache item age : {0}", item_last_used)
log.debug("clear_old_cache_data() : Deleting cache item age : {0}".format(item_last_used))
data_file = os.path.join(addon_dir, filename)
with FileLock(data_file + ".locked", timeout=5):
xbmcvfs.delete(data_file)
@@ -326,4 +310,4 @@ def clear_old_cache_data():
with FileLock(data_file + ".locked", timeout=5):
xbmcvfs.delete(data_file)
log.debug("clear_old_cache_data() : Cache items deleted : {0}", del_count)
log.debug("clear_old_cache_data() : Cache items deleted : {0}".format(del_count))

246
resources/lib/dialogs.py Normal file
View File

@@ -0,0 +1,246 @@
from __future__ import division, absolute_import, print_function, unicode_literals
import xbmcgui
from .lazylogger import LazyLogger
from .utils import translate_string, send_event_notification
log = LazyLogger(__name__)
class BitrateDialog(xbmcgui.WindowXMLDialog):
slider_control = None
bitrate_label = None
initial_bitrate_value = 0
selected_transcode_value = 0
def __init__(self, *args, **kwargs):
log.debug("BitrateDialog: __init__")
xbmcgui.WindowXML.__init__(self, *args, **kwargs)
def onInit(self):
log.debug("ActionMenu: onInit")
self.action_exitkeys_id = [10, 13]
self.slider_control = self.getControl(3000)
self.slider_control.setInt(self.initial_bitrate_value, 400, 100, 15000)
self.bitrate_label = self.getControl(3030)
bitrate_label_string = str(self.slider_control.getInt()) + " Kbs"
self.bitrate_label.setLabel(bitrate_label_string)
self.getControl(3011).setLabel(translate_string(30314))
def onFocus(self, control_id):
pass
def doAction(self, action_id):
pass
def onMessage(self, message):
log.debug("ActionMenu: onMessage: {0}".format(message))
def onAction(self, action):
bitrate_label_string = str(self.slider_control.getInt()) + " Kbs"
self.bitrate_label.setLabel(bitrate_label_string)
if action.getId() == 10: # ACTION_PREVIOUS_MENU
self.close()
elif action.getId() == 92: # ACTION_NAV_BACK
self.close()
elif action.getId() == 7: # ENTER
self.selected_transcode_value = self.slider_control.getInt()
self.close()
def onClick(self, control_id):
if control_id == 3000:
log.debug("ActionMenu: Selected Item: {0}".format(control_id))
class ResumeDialog(xbmcgui.WindowXMLDialog):
resumePlay = -1
resumeTimeStamp = ""
action_exitkeys_id = None
def __init__(self, *args, **kwargs):
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
log.debug("ResumeDialog INITIALISED")
def onInit(self):
self.action_exitkeys_id = [10, 13]
self.getControl(3010).setLabel(self.resumeTimeStamp)
self.getControl(3011).setLabel(translate_string(30237))
def onFocus(self, controlId):
pass
def doAction(self, actionID):
pass
def onClick(self, controlID):
if controlID == 3010:
self.resumePlay = 0
self.close()
if controlID == 3011:
self.resumePlay = 1
self.close()
def setResumeTime(self, timeStamp):
self.resumeTimeStamp = timeStamp
def getResumeAction(self):
return self.resumePlay
class SafeDeleteDialog(xbmcgui.WindowXMLDialog):
confirm = False
message = "Demo Message"
heading = "Demo Heading"
action_exitkeys_id = None
def __init__(self, *args, **kwargs):
log.debug("SafeDeleteDialog: __init__")
xbmcgui.WindowXML.__init__(self, *args, **kwargs)
def onInit(self):
log.debug("SafeDeleteDialog: onInit")
self.action_exitkeys_id = [10, 13]
message_control = self.getControl(3)
message_control.setText(self.message)
message_control = self.getControl(4)
message_control.setLabel(self.heading)
def onFocus(self, controlId):
pass
def doAction(self, actionID):
pass
def onMessage(self, message):
log.debug("SafeDeleteDialog: onMessage: {0}".format(message))
def onAction(self, action):
if action.getId() == 10: # ACTION_PREVIOUS_MENU
self.close()
elif action.getId() == 92: # ACTION_NAV_BACK
self.close()
else:
log.debug("SafeDeleteDialog: onAction: {0}".format(action.getId()))
def onClick(self, controlID):
if controlID == 1:
self.confirm = True
self.close()
elif controlID == 2:
self.confirm = False
self.close()
class PlayNextDialog(xbmcgui.WindowXMLDialog):
action_exitkeys_id = None
episode_info = None
play_called = False
def __init__(self, *args, **kwargs):
log.debug("PlayNextDialog: __init__")
xbmcgui.WindowXML.__init__(self, *args, **kwargs)
def onInit(self):
log.debug("PlayNextDialog: onInit")
self.action_exitkeys_id = [10, 13]
index = self.episode_info.get("IndexNumber", -1)
series_name = self.episode_info.get("SeriesName")
next_epp_name = "Episode %02d - (%s)" % (index, self.episode_info.get("Name", "n/a"))
series_label = self.getControl(3011)
series_label.setLabel(series_name)
series_label = self.getControl(3012)
series_label.setLabel(next_epp_name)
def onFocus(self, control_id):
pass
def doAction(self, action_id):
pass
def onMessage(self, message):
log.debug("PlayNextDialog: onMessage: {0}".format(message))
def onAction(self, action):
if action.getId() == 10: # ACTION_PREVIOUS_MENU
self.close()
elif action.getId() == 92: # ACTION_NAV_BACK
self.close()
else:
log.debug("PlayNextDialog: onAction: {0}".format(action.getId()))
def onClick(self, control_id):
if control_id == 3013:
log.debug("PlayNextDialog: Play Next Episode")
self.play_called
self.close()
next_item_id = self.episode_info.get("Id")
log.debug("Playing Next Episode: {0}".format(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)
elif control_id == 3014:
self.close()
def set_episode_info(self, info):
self.episode_info = info
def get_play_called(self):
return self.play_called
class QuickConnectDialog(xbmcgui.WindowXMLDialog):
connect_method = -1
def __init__(self, *args, **kwargs):
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
log.debug("QuickConnectDialog INITIALISED")
self.code = ''
def onInit(self):
self.action_exitkeys_id = [10, 13]
message_control = self.getControl(3)
message_control.setText(self.code)
message_control = self.getControl(4)
message_control.setLabel(translate_string(30443))
self.getControl(3010).setLabel(translate_string(30444))
self.getControl(3011).setLabel(translate_string(30365))
def onFocus(self, controlId):
pass
def doAction(self, actionID):
pass
def onClick(self, controlID):
if controlID == 3010:
self.connect_method = 1
self.close()
if controlID == 3011:
self.connect_method = 0
self.close()
def getConnectMethod(self):
return self.connect_method

View File

@@ -1,23 +1,21 @@
# 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
from .datamanager import DataManager
from .kodi_utils import HomeWindow
from .downloadutils import DownloadUtils
from .translation import string_load
from .simple_logging import SimpleLogging
from .lazylogger import LazyLogger
from .item_functions import add_gui_item, ItemDetails
from .utils import send_event_notification
from .utils import send_event_notification, translate_string, load_user_details, get_default_filters
from .tracking import timer
log = SimpleLogging(__name__)
log = LazyLogger(__name__)
@timer
@@ -27,10 +25,10 @@ def get_content(url, params):
default_sort = params.get("sort")
media_type = params.get("media_type", None)
if not media_type:
xbmcgui.Dialog().ok(string_load(30135), string_load(30139))
xbmcgui.Dialog().ok(translate_string(30135), translate_string(30139))
log.debug("URL: {0}", url)
log.debug("MediaType: {0}", media_type)
log.debug("URL: {0}".format(url))
log.debug("MediaType: {0}".format(media_type))
pluginhandle = int(sys.argv[1])
settings = xbmcaddon.Addon()
@@ -71,14 +69,14 @@ def get_content(url, params):
elif media_type == "playlists":
view_type = "Playlists"
log.debug("media_type:{0} content_type:{1} view_type:{2} ", media_type, content_type, view_type)
log.debug("media_type:{0} content_type:{1} view_type:{2} ".format(media_type, content_type, view_type))
# show a progress indicator if needed
progress = None
if settings.getSetting('showLoadProgress') == "true":
progress = xbmcgui.DialogProgress()
progress.create(string_load(30112))
progress.update(0, string_load(30113))
progress.create(translate_string(30112))
progress.update(0, translate_string(30113))
# update url for paging
start_index = 0
@@ -88,29 +86,22 @@ def get_content(url, params):
if page_limit > 0 and media_type.startswith("movie"):
m = re.search('StartIndex=([0-9]{1,4})', url)
if m and m.group(1):
log.debug("UPDATING NEXT URL: {0}", url)
log.debug("UPDATING NEXT URL: {0}".format(url))
start_index = int(m.group(1))
log.debug("current_start : {0}", start_index)
log.debug("current_start : {0}".format(start_index))
if start_index > 0:
prev_index = start_index - page_limit
if prev_index < 0:
prev_index = 0
url_prev = re.sub('StartIndex=([0-9]{1,4})', 'StartIndex=' + str(prev_index), url)
url_next = re.sub('StartIndex=([0-9]{1,4})', 'StartIndex=' + str(start_index + page_limit), url)
log.debug("UPDATING NEXT URL: {0}", url_next)
log.debug("UPDATING NEXT URL: {0}".format(url_next))
else:
log.debug("ADDING NEXT URL: {0}", url)
log.debug("ADDING NEXT URL: {0}".format(url))
url_next = url + "&StartIndex=" + str(start_index + page_limit) + "&Limit=" + str(page_limit)
url = url + "&StartIndex=" + str(start_index) + "&Limit=" + str(page_limit)
log.debug("ADDING NEXT URL: {0}", url_next)
# use the data manager to get the data
# result = dataManager.GetContent(url)
# total_records = 0
# if result is not None and isinstance(result, dict):
# total_records = result.get("TotalRecordCount", 0)
log.debug("ADDING NEXT URL: {0}".format(url_next))
use_cache = params.get("use_cache", "true") == "true"
@@ -118,15 +109,15 @@ def get_content(url, params):
if dir_items is None:
return
log.debug("total_records: {0}", total_records)
log.debug("total_records: {0}".format(total_records))
# add paging items
if page_limit > 0 and media_type.startswith("movie"):
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"
log.debug("ADDING PREV ListItem: {0} - {1}", u, list_item)
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))
if start_index + page_limit < total_records:
@@ -135,8 +126,8 @@ 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"
log.debug("ADDING NEXT ListItem: {0} - {1}", u, list_item)
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))
# set the Kodi content type
@@ -144,7 +135,7 @@ def get_content(url, params):
xbmcplugin.setContent(pluginhandle, content_type)
elif detected_type is not None:
# if the media type is not set then try to use the detected type
log.debug("Detected content type: {0}", detected_type)
log.debug("Detected content type: {0}".format(detected_type))
if detected_type == "Movie":
view_type = "Movies"
content_type = 'movies'
@@ -166,26 +157,21 @@ def get_content(url, params):
view_key = "view-" + content_type
view_id = settings.getSetting(view_key)
if view_id:
log.debug("Setting view for type:{0} to id:{1}", view_key, view_id)
log.debug("Setting view for type:{0} to id:{1}".format(view_key, view_id))
display_items_notification = {"view_id": view_id}
send_event_notification("set_view", display_items_notification)
else:
log.debug("No view id for view type:{0}", 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)
log.debug("No view id for view type:{0}".format(view_key))
if progress is not None:
progress.update(100, string_load(30125))
progress.update(100, translate_string(30125))
progress.close()
return
def set_sort(pluginhandle, view_type, default_sort):
log.debug("SETTING_SORT for media type: {0}", view_type)
log.debug("SETTING_SORT for media type: {0}".format(view_type))
if default_sort == "none":
xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_UNSORTED)
@@ -202,7 +188,7 @@ def set_sort(pluginhandle, view_type, default_sort):
settings = xbmcaddon.Addon()
preset_sort_order = settings.getSetting("sort-" + view_type)
log.debug("SETTING_SORT preset_sort_order: {0}", preset_sort_order)
log.debug("SETTING_SORT preset_sort_order: {0}".format(preset_sort_order))
if preset_sort_order in sorting_order_mapping:
xbmcplugin.addSortMethod(pluginhandle, sorting_order_mapping[preset_sort_order])
@@ -231,13 +217,14 @@ def process_directory(url, progress, params, use_cache_data=False):
data_manager = DataManager()
settings = xbmcaddon.Addon()
download_utils = DownloadUtils()
server = download_utils.get_server()
server = settings.getSetting('server_address')
user_details = load_user_details()
user_id = user_details.get('user_id')
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]
@@ -252,6 +239,15 @@ def process_directory(url, progress, params, use_cache_data=False):
gui_options["name_format_type"] = name_format_type
use_cache = settings.getSetting("use_cache") == "true" and use_cache_data
default_filters = get_default_filters()
# Fix skin shortcuts from pre-0.5.0
item_limit = int(settings.getSetting("show_x_filtered_items"))
url = url.replace('{server}', '')
url = url.replace('{userid}', user_id)
url = url.replace('{field_filters}', default_filters)
url = url.replace('{ItemLimit}', str(item_limit))
cache_file, item_list, total_records, cache_thread = data_manager.get_items(url, gui_options, use_cache)
# flatten single season
@@ -261,13 +257,13 @@ def process_directory(url, progress, params, use_cache_data=False):
if flatten_single_season and len(item_list) == 1 and item_list[0].item_type == "Season":
season_id = item_list[0].id
series_id = item_list[0].series_id
season_url = ('{server}/Shows/' + series_id +
season_url = ('/Shows/' + series_id +
'/Episodes'
'?userId={userid}' +
'?userId={}'.format(user_id) +
'&seasonId=' + season_id +
'&IsVirtualUnAired=false' +
'&IsMissing=false' +
'&Fields=SpecialEpisodeNumbers,{field_filters}' +
'&Fields=SpecialEpisodeNumbers,{}'.format(default_filters) +
'&format=json')
if progress is not None:
progress.close()
@@ -301,7 +297,7 @@ def process_directory(url, progress, params, use_cache_data=False):
if progress is not None:
percent_done = (float(current_item) / float(item_count)) * 100
progress.update(int(percent_done), string_load(30126) + str(current_item))
progress.update(int(percent_done), translate_string(30126) + str(current_item))
current_item = current_item + 1
if detected_type is not None:
@@ -311,7 +307,7 @@ def process_directory(url, progress, params, use_cache_data=False):
detected_type = item_details.item_type
if item_details.item_type == "Season" and first_season_item is None:
log.debug("Setting First Season to : {0}", item_details.__dict__)
log.debug("Setting First Season to : {0}".format(item_details.__dict__))
first_season_item = item_details
total_unwatched += item_details.unwatched_episodes
@@ -326,28 +322,28 @@ def process_directory(url, progress, params, use_cache_data=False):
if item_details.is_folder is True:
if item_details.item_type == "Series":
u = ('{server}/Shows/' + item_details.id +
u = ('/Shows/' + item_details.id +
'/Seasons'
'?userId={userid}' +
'&Fields={field_filters}' +
'?userId={}'.format(user_id) +
'&Fields={}'.format(default_filters) +
'&format=json')
elif item_details.item_type == "Season":
u = ('{server}/Shows/' + item_details.series_id +
u = ('/Shows/' + item_details.series_id +
'/Episodes'
'?userId={userid}' +
'?userId={}'.format(user_id) +
'&seasonId=' + item_details.id +
'&IsVirtualUnAired=false' +
'&IsMissing=false' +
'&Fields=SpecialEpisodeNumbers,{field_filters}' +
'&Fields=SpecialEpisodeNumbers,{}'.format(default_filters) +
'&format=json')
else:
u = ('{server}/Users/{userid}/items' +
u = ('/Users/{}/items'.format(user_id) +
'?ParentId=' + item_details.id +
'&IsVirtualUnAired=false' +
'&IsMissing=false' +
'&Fields={field_filters}' +
'&Fields={}'.format(default_filters) +
'&format=json')
default_sort = item_details.item_type == "Playlist"
@@ -357,10 +353,10 @@ def process_directory(url, progress, params, use_cache_data=False):
if gui_item:
dir_items.append(gui_item)
else:
log.debug("Dropping empty folder item : {0}", item_details.__dict__)
log.debug("Dropping empty folder item : {0}".format(item_details.__dict__))
elif item_details.item_type == "MusicArtist":
u = ('{server}/Users/{userid}/items' +
u = ('/Users/{}/items'.format(user_id) +
'?ArtistIds=' + item_details.id +
'&IncludeItemTypes=MusicAlbum' +
'&CollapseBoxSetItems=false' +
@@ -382,13 +378,12 @@ def process_directory(url, progress, params, use_cache_data=False):
and first_season_item is not None
and len(dir_items) > 1
and first_season_item.series_id is not None):
series_url = ('{server}/Shows/' + first_season_item.series_id +
series_url = ('/Shows/' + first_season_item.series_id +
'/Episodes'
'?userId={userid}' +
# '&seasonId=' + season_id +
'?userId={}'.format(user_id) +
'&IsVirtualUnAired=false' +
'&IsMissing=false' +
'&Fields=SpecialEpisodeNumbers,{field_filters}' +
'&Fields=SpecialEpisodeNumbers,{}'.format(default_filters) +
'&format=json')
played = 0
overlay = "7"
@@ -399,7 +394,7 @@ def process_directory(url, progress, params, use_cache_data=False):
item_details = ItemDetails()
item_details.id = first_season_item.id
item_details.name = string_load(30290)
item_details.name = translate_string(30290)
item_details.art = first_season_item.art
item_details.play_count = played
item_details.overlay = overlay

View File

@@ -1,839 +0,0 @@
# Gnu General Public License - see LICENSE.TXT
import xbmcgui
import xbmcaddon
import httplib
import hashlib
import ssl
import StringIO
import gzip
import json
from urlparse import urlparse
import urllib
from base64 import b64encode
from collections import defaultdict
from .kodi_utils import HomeWindow
from .clientinfo import ClientInformation
from .simple_logging import SimpleLogging
from .translation import string_load
from .tracking import timer
log = SimpleLogging(__name__)
def save_user_details(settings, user_name, user_password):
save_user_to_settings = settings.getSetting("save_user_to_settings") == "true"
if save_user_to_settings:
settings.setSetting("username", user_name)
settings.setSetting("password", user_password)
else:
settings.setSetting("username", "")
settings.setSetting("password", "")
home_window = HomeWindow()
home_window.set_property("username", user_name)
home_window.set_property("password", user_password)
def load_user_details(settings):
save_user_to_settings = settings.getSetting("save_user_to_settings") == "true"
if save_user_to_settings:
user_name = settings.getSetting("username")
user_password = settings.getSetting("password")
else:
home_window = HomeWindow()
user_name = home_window.get_property("username")
user_password = home_window.get_property("password")
user_details = {}
user_details["username"] = user_name
user_details["password"] = user_password
return user_details
def get_details_string():
addon_settings = xbmcaddon.Addon()
include_media = addon_settings.getSetting("include_media") == "true"
include_people = addon_settings.getSetting("include_people") == "true"
include_overview = addon_settings.getSetting("include_overview") == "true"
filer_list = [
"DateCreated",
"EpisodeCount",
"SeasonCount",
"Path",
"Genres",
"Studios",
"Etag",
"Taglines",
"SortName",
"RecursiveItemCount",
"ChildCount",
"ProductionLocations",
"CriticRating",
"OfficialRating",
"CommunityRating",
"PremiereDate",
"ProductionYear",
"AirTime",
"Status",
"Tags"
]
if include_media:
filer_list.append("MediaStreams")
if include_people:
filer_list.append("People")
if include_overview:
filer_list.append("Overview")
return ",".join(filer_list)
class DownloadUtils:
use_https = False
verify_cert = False
def __init__(self, *args):
settings = xbmcaddon.Addon()
self.use_https = False
if settings.getSetting('protocol') == "1":
self.use_https = True
log.debug("use_https: {0}", self.use_https)
self.verify_cert = settings.getSetting('verify_cert') == 'true'
log.debug("verify_cert: {0}", self.verify_cert)
def post_capabilities(self):
url = "{server}/Sessions/Capabilities/Full?format=json"
data = {
'IconUrl': "https://raw.githubusercontent.com/faush01/plugin.video.jellycon/develop/kodi.png",
'SupportsMediaControl': True,
'PlayableMediaTypes': ["Video", "Audio"],
'SupportedCommands': ["MoveUp",
"MoveDown",
"MoveLeft",
"MoveRight",
"Select",
"Back",
"ToggleContextMenu",
"ToggleFullscreen",
"ToggleOsdMenu",
"GoHome",
"PageUp",
"NextLetter",
"GoToSearch",
"GoToSettings",
"PageDown",
"PreviousLetter",
"TakeScreenshot",
"VolumeUp",
"VolumeDown",
"ToggleMute",
"SendString",
"DisplayMessage",
"SetAudioStreamIndex",
"SetSubtitleStreamIndex",
"SetRepeatMode",
"Mute",
"Unmute",
"SetVolume",
"PlayNext",
"Play",
"Playstate",
"PlayMediaSource"]
}
self.download_url(url, post_body=data, method="POST")
log.debug("Posted Capabilities: {0}", data)
def get_item_playback_info(self, item_id, force_transcode):
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")
filtered_codecs.append("h265")
if addon_settings.getSetting("force_transcode_mpeg2") == "true":
filtered_codecs.append("mpeg2video")
if addon_settings.getSetting("force_transcode_msmpeg4v3") == "true":
filtered_codecs.append("msmpeg4v3")
if addon_settings.getSetting("force_transcode_mpeg4") == "true":
filtered_codecs.append("mpeg4")
playback_bitrate = addon_settings.getSetting("max_stream_bitrate")
force_playback_bitrate = addon_settings.getSetting("force_max_stream_bitrate")
if force_transcode:
playback_bitrate = force_playback_bitrate
audio_codec = addon_settings.getSetting("audio_codec")
audio_playback_bitrate = addon_settings.getSetting("audio_playback_bitrate")
audio_max_channels = addon_settings.getSetting("audio_max_channels")
audio_bitrate = int(audio_playback_bitrate) * 1000
bitrate = int(playback_bitrate) * 1000
profile = {
"Name": "Kodi",
"MaxStaticBitrate": bitrate,
"MaxStreamingBitrate": bitrate,
"MusicStreamingTranscodingBitrate": audio_bitrate,
"TimelineOffsetSeconds": 5,
"TranscodingProfiles": [
{
"Type": "Audio"
},
{
"Container": "ts",
"Protocol": "hls",
"Type": "Video",
"AudioCodec": audio_codec,
"VideoCodec": "h264",
"MaxAudioChannels": audio_max_channels
},
{
"Container": "jpeg",
"Type": "Photo"
}
],
"DirectPlayProfiles": [
{
"Type": "Video"
},
{
"Type": "Audio"
},
{
"Type": "Photo"
}
],
"ResponseProfiles": [],
"ContainerProfiles": [],
"CodecProfiles": [],
"SubtitleProfiles": [
{
"Format": "srt",
"Method": "External"
},
{
"Format": "srt",
"Method": "Embed"
},
{
"Format": "ass",
"Method": "External"
},
{
"Format": "ass",
"Method": "Embed"
},
{
"Format": "sub",
"Method": "Embed"
},
{
"Format": "sub",
"Method": "External"
},
{
"Format": "ssa",
"Method": "Embed"
},
{
"Format": "ssa",
"Method": "External"
},
{
"Format": "smi",
"Method": "Embed"
},
{
"Format": "smi",
"Method": "External"
},
{
"Format": "pgssub",
"Method": "Embed"
},
{
"Format": "pgssub",
"Method": "External"
},
{
"Format": "dvdsub",
"Method": "Embed"
},
{
"Format": "dvdsub",
"Method": "External"
},
{
"Format": "pgs",
"Method": "Embed"
},
{
"Format": "pgs",
"Method": "External"
}
]
}
if len(filtered_codecs) > 0:
profile['DirectPlayProfiles'][0]['VideoCodec'] = "-%s" % ",".join(filtered_codecs)
if force_transcode:
profile['DirectPlayProfiles'] = []
if addon_settings.getSetting("playback_video_force_8") == "true":
profile['CodecProfiles'].append(
{
"Type": "Video",
"Codec": "h264",
"Conditions": [
{
"Condition": "LessThanEqual",
"Property": "VideoBitDepth",
"Value": "8",
"IsRequired": False
}
]
}
)
profile['CodecProfiles'].append(
{
"Type": "Video",
"Codec": "h265,hevc",
"Conditions": [
{
"Condition": "EqualsAny",
"Property": "VideoProfile",
"Value": "main"
}
]
}
)
playback_info = {
'UserId': self.get_user_id(),
'DeviceProfile': profile,
'AutoOpenLiveStream': True
}
if force_transcode:
url = "{server}/Items/%s/PlaybackInfo?MaxStreamingBitrate=%s&EnableDirectPlay=false&EnableDirectStream=false" % (item_id, bitrate)
else:
url = "{server}/Items/%s/PlaybackInfo?MaxStreamingBitrate=%s" % (item_id, bitrate)
log.debug("PlaybackInfo : {0}", url)
log.debug("PlaybackInfo : {0}", 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}", play_info_result)
return play_info_result
def get_server(self):
settings = xbmcaddon.Addon()
host = settings.getSetting('ipaddress')
if len(host) == 0 or host == "<none>":
return None
port = settings.getSetting('port')
if not port and self.use_https:
port = "443"
settings.setSetting("port", port)
elif not port:
port = "80"
settings.setSetting("port", port)
# if user entered a full path i.e. http://some_host:port
if host.lower().strip().startswith("http://") or host.lower().strip().startswith("https://"):
log.debug("Extracting host info from url: {0}", host)
url_bits = urlparse(host.strip())
if host.lower().strip().startswith("http://"):
settings.setSetting('protocol', '0')
self.use_https = False
elif host.lower().strip().startswith("https://"):
settings.setSetting('protocol', '1')
self.use_https = True
if url_bits.hostname is not None and len(url_bits.hostname) > 0:
host = url_bits.hostname
if url_bits.username and url_bits.password:
host = "%s:%s@" % (url_bits.username, url_bits.password) + host
settings.setSetting("ipaddress", host)
if url_bits.port is not None and url_bits.port > 0:
port = str(url_bits.port)
settings.setSetting("port", port)
if self.use_https:
server = "https://" + host + ":" + port
else:
server = "http://" + host + ":" + port
return server
@staticmethod
def get_all_artwork(item, server):
all_art = defaultdict(lambda: "")
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:
tag = image_tags[tag_name]
art_url = "%s/Items/%s/Images/%s/0?Format=original&Tag=%s" % (server, item_id, tag_name, tag)
all_art[tag_name] = art_url
# Series images
if item_type in ["Episode", "Season"]:
image_tag = item["SeriesPrimaryImageTag"]
series_id = item["SeriesId"]
if image_tag and series_id:
art_url = "%s/Items/%s/Images/Primary/0?Format=original&Tag=%s" % (server, series_id, image_tag)
all_art["Primary.Series"] = art_url
return all_art
def get_artwork(self, data, art_type, parent=False, index=0, server=None):
item_id = data["Id"]
item_type = data["Type"]
if item_type in ["Episode", "Season"]:
if art_type != "Primary" or parent is True:
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:
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:
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:
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_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'
id_name = 'SeriesId'
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]
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)
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
artwork = "%s/Items/%s/Images/%s/%s?Format=original&Tag=%s" % (server, item_id, art_type, index, image_tag)
if int(width) > 0:
artwork += '&MaxWidth=%s' % width
if int(height) > 0:
artwork += '&MaxHeight=%s' % height
if self.use_https and not self.verify_cert:
artwork += "|verifypeer=false"
return artwork
def get_user_artwork(self, user, item_type):
if "PrimaryImageTag" not in user:
return ""
user_id = user.get("Id")
tag = user.get("PrimaryImageTag")
server = self.get_server()
artwork = "%s/Users/%s/Images/%s?Format=original&tag=%s" % (server, user_id, item_type, tag)
if self.use_https and not self.verify_cert:
artwork += "|verifypeer=false"
return artwork
def get_user_id(self):
window = HomeWindow()
userid = window.get_property("userid")
user_image = window.get_property("userimage")
if userid and user_image:
log.debug("JellyCon DownloadUtils -> Returning saved UserID: {0}", userid)
return userid
settings = xbmcaddon.Addon()
user_details = load_user_details(settings)
user_name = user_details.get("username", "")
if not user_name:
return ""
log.debug("Looking for user name: {0}", user_name)
try:
json_data = self.download_url("{server}/Users/Public?format=json", suppress=True, authenticate=False)
except Exception as msg:
log.error("Get User unable to connect: {0}", msg)
return ""
log.debug("GETUSER_JSONDATA_01: {0}", json_data)
try:
result = json.loads(json_data)
except Exception as e:
log.debug("Could not load user data: {0}", e)
return ""
if result is None:
return ""
log.debug("GETUSER_JSONDATA_02: {0}", result)
secure = False
for user in result:
if user.get("Name") == unicode(user_name, "utf-8"):
userid = user.get("Id")
user_image = self.get_user_artwork(user, 'Primary')
log.debug("Username Found: {0}", user.get("Name"))
if user.get("HasPassword", False):
secure = True
log.debug("Username Is Secure (HasPassword=True)")
break
if secure or not userid:
auth_ok = self.authenticate()
if auth_ok == "":
xbmcgui.Dialog().notification(string_load(30316),
string_load(30044),
icon="special://home/addons/plugin.video.jellycon/icon.png")
return ""
if not userid:
userid = window.get_property("userid")
if userid and not user_image:
user_image = 'DefaultUser.png'
if userid == "":
xbmcgui.Dialog().notification(string_load(30316),
string_load(30045),
icon="special://home/addons/plugin.video.jellycon/icon.png")
log.debug("userid: {0}", userid)
window.set_property("userid", userid)
window.set_property("userimage", user_image)
return userid
def authenticate(self):
window = HomeWindow()
token = window.get_property("AccessToken")
if token is not None and token != "":
log.debug("JellyCon DownloadUtils -> Returning saved AccessToken: {0}", token)
return token
settings = xbmcaddon.Addon()
port = settings.getSetting("port")
host = settings.getSetting("ipaddress")
if host is None or host == "" or port is None or port == "":
return ""
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", ""))
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}", resp)
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
if access_token is not None:
log.debug("User Authenticated: {0}", access_token)
log.debug("User Id: {0}", userid)
window.set_property("AccessToken", access_token)
window.set_property("userid", userid)
# WINDOW.setProperty("userimage", "")
self.post_capabilities()
return access_token
else:
log.debug("User NOT Authenticated")
window.set_property("AccessToken", "")
window.set_property("userid", "")
window.set_property("userimage", "")
return ""
def get_auth_header(self, authenticate=True):
client_info = ClientInformation()
txt_mac = client_info.get_device_id()
version = client_info.get_version()
client = client_info.get_client()
settings = xbmcaddon.Addon()
device_name = settings.getSetting('deviceName')
# remove none ascii chars
device_name = device_name.decode("ascii", errors='ignore')
# remove some chars not valid for names
device_name = device_name.replace("\"", "_")
if len(device_name) == 0:
device_name = "JellyCon"
headers = {}
headers["Accept-encoding"] = "gzip"
headers["Accept-Charset"] = "UTF-8,*"
if authenticate is False:
auth_string = "MediaBrowser Client=\"" + client + "\",Device=\"" + device_name + "\",DeviceId=\"" + txt_mac + "\",Version=\"" + version + "\""
# headers["Authorization"] = authString
headers['X-Emby-Authorization'] = auth_string
return headers
else:
userid = self.get_user_id()
auth_string = "MediaBrowser UserId=\"" + userid + "\",Client=\"" + client + "\",Device=\"" + device_name + "\",DeviceId=\"" + txt_mac + "\",Version=\"" + version + "\""
# headers["Authorization"] = authString
headers['X-Emby-Authorization'] = auth_string
auth_token = self.authenticate()
if auth_token != "":
headers["X-MediaBrowser-Token"] = auth_token
log.debug("JellyCon Authentication Header: {0}", headers)
return headers
@timer
def download_url(self, url, suppress=False, post_body=None, method="GET", authenticate=True, headers=None):
log.debug("downloadUrl")
return_data = "null"
settings = xbmcaddon.Addon()
user_details = load_user_details(settings)
username = user_details.get("username", "")
server = None
http_timeout = int(settings.getSetting("http_timeout"))
if authenticate and username == "":
return return_data
if settings.getSetting("suppressErrors") == "true":
suppress = True
log.debug("Before: {0}", url)
if url.find("{server}") != -1:
server = self.get_server()
if server is None:
return return_data
url = url.replace("{server}", server)
if url.find("{userid}") != -1:
userid = self.get_user_id()
if not userid:
return return_data
url = url.replace("{userid}", userid)
if url.find("{ItemLimit}") != -1:
show_x_filtered_items = settings.getSetting("show_x_filtered_items")
url = url.replace("{ItemLimit}", show_x_filtered_items)
if url.find("{field_filters}") != -1:
filter_string = get_details_string()
url = url.replace("{field_filters}", filter_string)
if url.find("{random_movies}") != -1:
home_window = HomeWindow()
random_movies = home_window.get_property("random-movies")
if not random_movies:
return return_data
url = url.replace("{random_movies}", random_movies)
log.debug("After: {0}", url)
conn = None
try:
url_bits = urlparse(url.strip())
protocol = url_bits.scheme
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_puery = url_bits.query
if not host_name or host_name == "<none>":
return return_data
local_use_https = False
if protocol.lower() == "https":
local_use_https = True
server = "%s:%s" % (host_name, port)
url_path = url_path + "?" + url_puery
if local_use_https and self.verify_cert:
log.debug("Connection: HTTPS, Cert checked")
conn = httplib.HTTPSConnection(server, timeout=http_timeout)
elif local_use_https and not self.verify_cert:
log.debug("Connection: HTTPS, Cert NOT checked")
conn = httplib.HTTPSConnection(server, timeout=http_timeout, context=ssl._create_unverified_context())
else:
log.debug("Connection: HTTP")
conn = httplib.HTTPConnection(server, timeout=http_timeout)
head = self.get_auth_header(authenticate)
if user_name and user_password:
# add basic auth headers
user_and_pass = b64encode(b"%s:%s" % (user_name, user_password)).decode("ascii")
head["Authorization"] = 'Basic %s' % user_and_pass
head["User-Agent"] = "JellyCon-" + ClientInformation().get_version()
log.debug("HEADERS: {0}", head)
if post_body is not None:
if isinstance(post_body, dict):
content_type = "application/json"
post_body = json.dumps(post_body)
else:
content_type = "application/x-www-form-urlencoded"
head["Content-Type"] = content_type
log.debug("Content-Type: {0}", content_type)
log.debug("POST DATA: {0}", post_body)
conn.request(method=method, url=url_path, body=post_body, headers=head)
else:
conn.request(method=method, url=url_path, headers=head)
data = conn.getresponse()
log.debug("HTTP response: {0} {1}", data.status, data.reason)
log.debug("GET URL HEADERS: {0}", data.getheaders())
if int(data.status) == 200:
ret_data = data.read()
content_type = data.getheader('content-encoding')
log.debug("Data Len Before: {0}", len(ret_data))
if content_type == "gzip":
ret_data = StringIO.StringIO(ret_data)
gzipper = gzip.GzipFile(fileobj=ret_data)
return_data = gzipper.read()
else:
return_data = ret_data
if headers is not None and isinstance(headers, dict):
headers.update(data.getheaders())
log.debug("Data Len After: {0}", len(return_data))
log.debug("====== 200 returned =======")
log.debug("Content-Type: {0}", content_type)
log.debug("{0}", return_data)
log.debug("====== 200 finished ======")
elif int(data.status) >= 400:
if int(data.status) == 401:
# remove any saved password
m = hashlib.md5()
m.update(username)
hashed_username = m.hexdigest()
log.error("HTTP response error 401 auth error, removing any saved passwords for user: {0}", hashed_username)
settings.setSetting("saved_user_password_" + hashed_username, "")
save_user_details(settings, "", "")
log.error("HTTP response error: {0} {1}", data.status, data.reason)
if suppress is False:
xbmcgui.Dialog().notification(string_load(30316),
string_load(30200) % str(data.reason),
icon="special://home/addons/plugin.video.jellycon/icon.png")
except Exception as msg:
log.error("Unable to connect to {0} : {1}", server, msg)
if suppress is False:
xbmcgui.Dialog().notification(string_load(30316),
str(msg),
icon="special://home/addons/plugin.video.jellycon/icon.png")
finally:
try:
log.debug("Closing HTTP connection: {0}", conn)
conn.close()
except:
pass
return return_data

View File

@@ -90,9 +90,6 @@ import sys
import time
import errno
# from .simple_logging import SimpleLogging
# log = SimpleLogging(__name__)
class FileLock(object):
""" A file locking mechanism that has context-manager support so
@@ -201,7 +198,7 @@ if __name__ == "__main__":
import threading
import tempfile
from builtins import range
temp_dir = tempfile.mkdtemp()
protected_filepath = os.path.join(temp_dir, "somefile.txt")
@@ -236,4 +233,4 @@ if __name__ == "__main__":
# Please manually inspect the output. Does it look like the operations were atomic?
with open(protected_filepath, "r") as f:
sys.stdout.write(f.read())
"""
"""

File diff suppressed because it is too large Load Diff

View File

@@ -1,49 +1,42 @@
from __future__ import division, absolute_import, print_function, unicode_literals
import xbmcvfs
import xbmc
import xbmcaddon
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 httplib
import requests
import io
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from .simple_logging import SimpleLogging
from .datamanager import DataManager
from .downloadutils import DownloadUtils
from .utils import get_art
from .jellyfin import api
from .lazylogger import LazyLogger
from .item_functions import get_art
from .utils import translate_path
pil_loaded = False
try:
from PIL import ImageFilter, Image, ImageOps
from PIL import Image, ImageOps
pil_loaded = True
except Exception as err:
except ImportError:
pil_loaded = False
PORT_NUMBER = 24276
log = SimpleLogging(__name__)
log = LazyLogger(__name__)
def get_image_links(url):
download_utils = DownloadUtils()
server = download_utils.get_server()
settings = xbmcaddon.Addon()
server = settings.getSetting('server_address')
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")
@@ -58,8 +51,7 @@ def get_image_links(url):
if not re.search('EnableUserData=', url, re.IGNORECASE):
url += "&EnableUserData=False"
data_manager = DataManager()
result = data_manager.get_content(url)
result = api.get(url)
items = result.get("Items")
if not items:
@@ -78,15 +70,15 @@ def get_image_links(url):
def build_image(path):
log.debug("build_image()")
log.debug("Request Path : {0}", path)
log.debug("Request Path : {0}".format(path))
request_path = path[1:]
if request_path == "favicon.ico":
return []
decoded_url = base64.b64decode(request_path)
log.debug("decoded_url : {0}", decoded_url)
decoded_url = ensure_text(base64.b64decode(request_path))
log.debug("decoded_url : {0}".format(decoded_url))
image_urls = get_image_links(decoded_url)
@@ -108,21 +100,17 @@ 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
server = "%s:%s" % (host_name, port)
url_full_path = url_path + "?" + url_query
log.debug("Loading image from : {0} {1} {2}", image_count, server, url_full_path)
log.debug("Loading image from : {0} {1} {2}".format(image_count, server, url_full_path))
try:
conn = httplib.HTTPConnection(server)
conn.request("GET", url_full_path)
image_responce = conn.getresponse()
image_data = image_responce.read()
image_response = requests.get(thumb_url)
image_data = image_response.content
loaded_image = Image.open(io.BytesIO(image_data))
image = ImageOps.fit(loaded_image, size, method=Image.ANTIALIAS, bleed=0.0, centering=(0.5, 0.5))
@@ -136,7 +124,7 @@ def build_image(path):
del image_data
except Exception as con_err:
log.debug("Error loading image : {0}", str(con_err))
log.debug("Error loading image : {0}".format(con_err))
image_count += 1
@@ -170,12 +158,6 @@ class HttpImageHandler(BaseHTTPRequestHandler):
self.end_headers()
return
def do_QUIT(self):
log.debug("HttpImageHandler:do_QUIT()")
self.send_response(200)
self.end_headers()
return
def serve_image(self):
if pil_loaded:
@@ -189,7 +171,7 @@ class HttpImageHandler(BaseHTTPRequestHandler):
else:
image_path = xbmc.translatePath("special://home/addons/plugin.video.jellycon/icon.png").decode('utf-8')
image_path = translate_path("special://home/addons/plugin.video.jellycon/icon.png").decode('utf-8')
self.send_response(200)
self.send_header('Content-type', 'image/png')
modified = xbmcvfs.Stat(image_path).st_mtime()
@@ -205,26 +187,21 @@ class HttpImageHandler(BaseHTTPRequestHandler):
class HttpImageServerThread(threading.Thread):
keep_running = True
def __init__(self):
threading.Thread.__init__(self)
self.keep_running = True
def stop(self):
self.keep_running = False
log.debug("HttpImageServerThread:stop called")
try:
conn = httplib.HTTPConnection("localhost:%d" % PORT_NUMBER)
conn.request("QUIT", "/")
conn.getresponse()
except:
pass
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,30 +1,18 @@
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
from dateutil import tz
from collections import defaultdict
import xbmc
import xbmcaddon
import xbmcgui
from .utils import get_art, datetime_from_string
from .simple_logging import SimpleLogging
from .downloadutils import DownloadUtils
from .kodi_utils import HomeWindow
from .utils import datetime_from_string, get_art_url, image_url, get_current_datetime
from .lazylogger import LazyLogger
from six import ensure_text
log = SimpleLogging(__name__)
kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2])
addon_instance = xbmcaddon.Addon()
addon_path = addon_instance.getAddonInfo('path')
PLUGINPATH = xbmc.translatePath(os.path.join(addon_path))
download_utils = DownloadUtils()
home_window = HomeWindow()
log = LazyLogger(__name__)
class ItemDetails:
@@ -100,27 +88,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 +119,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,34 +141,34 @@ 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:
name_info["SeriesName"] = ""
name_info["SeasonIndex"] = u"%02d" % item_details.season_number
name_info["EpisodeIndex"] = u"%02d" % item_details.episode_number
log.debug("FormatName: {0} | {1}", name_format, name_info)
item_details.name = unicode(name_format).format(**name_info).strip()
log.debug("FormatName: {0} | {1}".format(name_format, name_info))
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 +179,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,40 +219,38 @@ 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_thumbnail = download_utils.image_url(person_id,
"Primary", 0, 400, 400,
person_tag,
server=gui_options["server"])
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 = image_url(person_id, "Primary", 0, 400,
400, person_tag,
server=gui_options["server"])
else:
person_thumbnail = ""
person = {"name": person_name, "role": person_role, "thumbnail": person_thumbnail}
@@ -272,64 +258,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 +320,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 +342,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 +352,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:
@@ -422,20 +403,27 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
end_time = datetime_from_string(item_details.program_end_date)
duration = (end_time - start_time).total_seconds()
time_done = (datetime.now() - start_time).total_seconds()
now = get_current_datetime()
time_done = (now - start_time).total_seconds()
percentage_done = (float(time_done) / float(duration)) * 100.0
capped_percentage = int(percentage_done)
start_time_string = start_time.strftime("%H:%M")
end_time_string = end_time.strftime("%H:%M")
# Convert dates to local timezone for display
local = tz.tzlocal()
start_time_string = start_time.astimezone(local).strftime("%H:%M")
end_time_string = end_time.astimezone(local).strftime("%H:%M")
item_details.duration = int(duration)
item_details.resume_time = int(time_done)
list_item_name = (item_details.program_channel_name +
" - " + list_item_name +
" - " + start_time_string + " to " + end_time_string +
" (" + str(int(percentage_done)) + "%)")
if item_details.program_channel_name:
list_item_name = '{} - {} - {} to {} ({}%)'.format(
item_details.program_channel_name, list_item_name,
start_time_string, end_time_string, capped_percentage)
else:
list_item_name = '{} - {} to {} ({}%)'.format(
list_item_name, start_time_string, end_time_string,
capped_percentage)
time_info = "Start : " + start_time_string + "\n"
time_info += "End : " + end_time_string + "\n"
@@ -445,12 +433,7 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
else:
item_details.plot = time_info
if kodi_version > 17:
list_item = xbmcgui.ListItem(list_item_name, offscreen=True)
else:
list_item = xbmcgui.ListItem(list_item_name, iconImage=thumb_path, thumbnailImage=thumb_path)
# log.debug("Setting thumbnail as: {0}", thumbPath)
list_item = xbmcgui.ListItem(list_item_name, offscreen=True)
item_properties = {}
@@ -477,11 +460,8 @@ 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 kodi_version >= 17:
list_item.setCast(item_details.cast)
else:
info_labels['cast'] = info_labels['castandrole'] = [(cast_member['name'], cast_member['role']) for cast_member in item_details.cast]
if item_details.cast:
list_item.setCast(item_details.cast)
info_labels["title"] = list_item_name
if item_details.sort_name:
@@ -497,11 +477,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 +549,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 +575,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 +585,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,11 +594,89 @@ 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:
for key, value in item_properties.iteritems():
list_item.setProperty(key, value)
list_item.setProperties(item_properties)
return u, list_item, folder
def get_art(item, server):
art = {
'thumb': '',
'fanart': '',
'poster': '',
'banner': '',
'clearlogo': '',
'clearart': '',
'discart': '',
'landscape': '',
'tvshow.fanart': '',
'tvshow.poster': '',
'tvshow.clearart': '',
'tvshow.clearlogo': '',
'tvshow.banner': '',
'tvshow.landscape': ''
}
image_tags = item.get("ImageTags", {})
if image_tags and image_tags.get("Primary"):
art['thumb'] = get_art_url(item, "Primary", server=server)
item_type = item["Type"]
if item_type == "Genre":
art['poster'] = get_art_url(item, "Primary", server=server)
elif item_type == "Episode":
art['tvshow.poster'] = get_art_url(item, "Primary", parent=True, server=server)
art['tvshow.clearart'] = get_art_url(item, "Art", parent=True, server=server)
art['clearart'] = get_art_url(item, "Art", parent=True, server=server)
art['tvshow.clearlogo'] = get_art_url(item, "Logo", parent=True, server=server)
art['clearlogo'] = get_art_url(item, "Logo", parent=True, server=server)
art['tvshow.banner'] = get_art_url(item, "Banner", parent=True, server=server)
art['banner'] = get_art_url(item, "Banner", parent=True, server=server)
art['tvshow.landscape'] = get_art_url(item, "Thumb", parent=True, server=server)
art['landscape'] = get_art_url(item, "Thumb", parent=True, server=server)
art['tvshow.fanart'] = get_art_url(item, "Backdrop", parent=True, server=server)
art['fanart'] = get_art_url(item, "Backdrop", parent=True, server=server)
elif item_type == "Season":
art['tvshow.poster'] = get_art_url(item, "Primary", parent=True, server=server)
art['season.poster'] = get_art_url(item, "Primary", parent=False, server=server)
art['poster'] = get_art_url(item, "Primary", parent=False, server=server)
art['tvshow.clearart'] = get_art_url(item, "Art", parent=True, server=server)
art['clearart'] = get_art_url(item, "Art", parent=True, server=server)
art['tvshow.clearlogo'] = get_art_url(item, "Logo", parent=True, server=server)
art['clearlogo'] = get_art_url(item, "Logo", parent=True, server=server)
art['tvshow.banner'] = get_art_url(item, "Banner", parent=True, server=server)
art['season.banner'] = get_art_url(item, "Banner", parent=False, server=server)
art['banner'] = get_art_url(item, "Banner", parent=False, server=server)
art['tvshow.landscape'] = get_art_url(item, "Thumb", parent=True, server=server)
art['season.landscape'] = get_art_url(item, "Thumb", parent=False, server=server)
art['landscape'] = get_art_url(item, "Thumb", parent=False, server=server)
art['tvshow.fanart'] = get_art_url(item, "Backdrop", parent=True, server=server)
art['fanart'] = get_art_url(item, "Backdrop", parent=True, server=server)
elif item_type == "Series":
art['tvshow.poster'] = get_art_url(item, "Primary", parent=False, server=server)
art['poster'] = get_art_url(item, "Primary", parent=False, server=server)
art['tvshow.clearart'] = get_art_url(item, "Art", parent=False, server=server)
art['clearart'] = get_art_url(item, "Art", parent=False, server=server)
art['tvshow.clearlogo'] = get_art_url(item, "Logo", parent=False, server=server)
art['clearlogo'] = get_art_url(item, "Logo", parent=False, server=server)
art['tvshow.banner'] = get_art_url(item, "Banner", parent=False, server=server)
art['banner'] = get_art_url(item, "Banner", parent=False, server=server)
art['tvshow.landscape'] = get_art_url(item, "Thumb", parent=False, server=server)
art['landscape'] = get_art_url(item, "Thumb", parent=False, server=server)
art['tvshow.fanart'] = get_art_url(item, "Backdrop", parent=False, server=server)
art['fanart'] = get_art_url(item, "Backdrop", parent=False, server=server)
elif item_type == "Movie" or item_type == "BoxSet":
art['poster'] = get_art_url(item, "Primary", server=server)
art['landscape'] = get_art_url(item, "Thumb", server=server)
art['banner'] = get_art_url(item, "Banner", server=server)
art['clearlogo'] = get_art_url(item, "Logo", server=server)
art['clearart'] = get_art_url(item, "Art", server=server)
art['discart'] = get_art_url(item, "Disc", server=server)
art['fanart'] = get_art_url(item, "Backdrop", server=server)
if not art['fanart']:
art['fanart'] = get_art_url(item, "Backdrop", parent=True, server=server)
return art

191
resources/lib/jellyfin.py Normal file
View File

@@ -0,0 +1,191 @@
from __future__ import division, absolute_import, print_function, unicode_literals
import xbmcaddon
from kodi_six.utils import py2_decode
import requests
import json
from .utils import get_device_id, get_version, load_user_details
from .lazylogger import LazyLogger
log = LazyLogger(__name__)
class API:
def __init__(self, server=None, user_id=None, token=None):
self.server = server
self.user_id = user_id
self.token = token
self.settings = xbmcaddon.Addon()
self.headers = {}
self.create_headers()
self.verify_cert = settings.getSetting('verify_cert') == 'true'
def get(self, path):
if 'x-mediabrowser-token' not in self.headers:
self.create_headers()
# Fixes initial login where class is initialized before wizard completes
if not self.server:
self.settings = xbmcaddon.Addon()
self.server = self.settings.getSetting('server_address')
url = '{}{}'.format(self.server, path)
r = requests.get(url, headers=self.headers, verify=self.verify_cert)
try:
try:
'''
The requests library defaults to using simplejson to handle
json decoding. On low power devices and using Py3, this is
significantly slower than the builtin json library. Skip that
and just parse the json ourselves. Fall back to using
requests/simplejson if there's a parsing error.
'''
response_data = json.loads(r.text)
except ValueError:
response_data = r.json()
except:
response_data = {}
return response_data
def post(self, url, payload={}):
if 'x-mediabrowser-token' not in self.headers:
self.create_headers()
url = '{}{}'.format(self.server, url)
r = requests.post(url, json=payload, headers=self.headers, verify=self.verify_cert)
try:
try:
# Much faster on low power devices, see above comment
response_data = json.loads(r.text)
except ValueError:
response_data = r.json()
except:
response_data = {}
return response_data
def delete(self, url):
if 'x-mediabrowser-token' not in self.headers:
self.create_headers()
url = '{}{}'.format(self.server, url)
requests.delete(url, headers=self.headers, verify=self.verify_cert)
def authenticate(self, auth_data):
response = self.post('/Users/AuthenticateByName', auth_data)
token = response.get('AccessToken')
if token:
self.token = token
self.user_id = response.get('User').get('Id')
# Create headers again to include auth token
self.create_headers()
return response
else:
log.error('Unable to authenticate to Jellyfin server')
return {}
def create_headers(self):
# If the headers already exist with an auth token, return
if self.headers and 'x-mediabrowser-token' in self.headers:
return
headers = {}
device_name = self.settings.getSetting('deviceName')
if len(device_name) == 0:
device_name = "JellyCon"
# Ensure ascii and remove invalid characters
device_name = py2_decode(device_name).replace('"', '_').replace(',', '_')
device_id = get_device_id()
version = get_version()
authorization = (
'MediaBrowser Client="Kodi JellyCon", Device="{device}", '
'DeviceId="{device_id}", Version="{version}"'
).format(
device=device_name,
device_id=device_id,
version=version
)
headers['x-emby-authorization'] = authorization
# If we have a valid token, ensure it's included in the headers
if self.token:
headers['x-mediabrowser-token'] = self.token
else:
# Check for updated credentials since initialization
user_details = load_user_details()
token = user_details.get('token')
if token:
self.token = token
headers['x-mediabrowser-token'] = self.token
# Make headers available to api calls
self.headers = headers
def post_capabilities(self):
url = '/Sessions/Capabilities/Full'
data = {
'SupportsMediaControl': True,
'PlayableMediaTypes': ["Video", "Audio"],
'SupportedCommands': ["MoveUp",
"MoveDown",
"MoveLeft",
"MoveRight",
"Select",
"Back",
"ToggleContextMenu",
"ToggleFullscreen",
"ToggleOsdMenu",
"GoHome",
"PageUp",
"NextLetter",
"GoToSearch",
"GoToSettings",
"PageDown",
"PreviousLetter",
"TakeScreenshot",
"VolumeUp",
"VolumeDown",
"ToggleMute",
"SendString",
"DisplayMessage",
"SetAudioStreamIndex",
"SetSubtitleStreamIndex",
"SetRepeatMode",
"Mute",
"Unmute",
"SetVolume",
"PlayNext",
"Play",
"Playstate",
"PlayMediaSource"]
}
self.post(url, data)
def speedtest(self, test_data_size):
self.create_headers()
url = '{}/playback/bitratetest?size={}'.format(self.server, test_data_size)
# Because this needs the stream argument, this doesn't go through self.get()
response = requests.get(url, stream=True, headers=self.headers, verify=self.verify_cert)
return response
settings = xbmcaddon.Addon()
user_details = load_user_details()
api = API(
settings.getSetting('server_address'),
user_details.get('user_id'),
user_details.get('token')
)

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,14 +1,14 @@
import xbmc
from __future__ import division, absolute_import, print_function, unicode_literals
import xbmcgui
import xbmcplugin
import xbmcaddon
import sys
import json
from .simple_logging import SimpleLogging
from .lazylogger import LazyLogger
log = SimpleLogging(__name__)
log = LazyLogger(__name__)
addon = xbmcaddon.Addon()
@@ -24,44 +24,22 @@ 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)
def add_menu_directory_item(label, path, folder=True, art=None):
li = xbmcgui.ListItem(label, path=path)
li = xbmcgui.ListItem(label, path=path, offscreen=True)
if art is None:
art = {}
art["thumb"] = addon.getAddonInfo('icon')
li.setArt(art)
xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=path, listitem=li, isFolder=folder)
def get_kodi_version():
json_data = xbmc.executeJSONRPC(
'{ "jsonrpc": "2.0", "method": "Application.GetProperties", "params": {"properties": ["version", "name"]}, "id": 1 }')
result = json.loads(json_data)
try:
result = result.get("result")
version_data = result.get("version")
version = float(str(version_data.get("major")) + "." + str(version_data.get("minor")))
log.debug("Version: {0} - {1}", version, version_data)
except:
version = 0.0
log.error("Version Error : RAW Version Data: {0}", result)
return version

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, print_function, unicode_literals
class LazyLogger(object):
"""`helper.loghandler.getLogger()` is used everywhere.
This class helps avoiding import errors.
"""
__logger = None
__logger_name = None
def __init__(self, logger_name=None):
self.__logger_name = logger_name
def __getattr__(self, name):
if self.__logger is None:
from .loghandler import getLogger
self.__logger = getLogger(self.__logger_name)
return getattr(self.__logger, name)

View File

@@ -1,45 +0,0 @@
import threading
import time
import xbmc
from .simple_logging import SimpleLogging
from .widgets import check_for_new_content
from .tracking import timer
log = SimpleLogging(__name__)
class LibraryChangeMonitor(threading.Thread):
last_library_change_check = 0
library_check_triggered = False
exit_now = False
time_between_checks = 10
def __init__(self):
threading.Thread.__init__(self)
def stop(self):
self.exit_now = True
@timer
def check_for_updates(self):
log.debug("Trigger check for updates")
self.library_check_triggered = True
def run(self):
log.debug("Library Monitor Started")
monitor = xbmc.Monitor()
while not self.exit_now and not monitor.abortRequested():
if self.library_check_triggered and not xbmc.Player().isPlaying():
log.debug("Doing new content check")
check_for_new_content()
self.library_check_triggered = False
self.last_library_change_check = time.time()
if self.exit_now or monitor.waitForAbort(self.time_between_checks):
break
log.debug("Library Monitor Exited")

132
resources/lib/loghandler.py Normal file
View File

@@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, print_function, unicode_literals
##################################################################################################
import os
import logging
import sys
import traceback
from six import ensure_text
from kodi_six import xbmc, xbmcaddon
from .utils import translate_path
##################################################################################################
__addon__ = xbmcaddon.Addon(id='plugin.video.jellycon')
__pluginpath__ = translate_path(__addon__.getAddonInfo('path'))
##################################################################################################
def getLogger(name=None):
if name is None:
return __LOGGER
return __LOGGER.getChild(name)
class LogHandler(logging.StreamHandler):
def __init__(self):
logging.StreamHandler.__init__(self)
self.setFormatter(MyFormatter())
self.sensitive = {'Token': [], 'Server': []}
settings = xbmcaddon.Addon()
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)
# Hide server URL in logs
string = string.replace(self.server or "{server}", "{jellyfin-server}")
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)
def _get_log_level(self, level):
levels = {
logging.ERROR: 0,
logging.WARNING: 0,
logging.INFO: 1,
logging.DEBUG: 2
}
if self.debug == 'true':
log_level = 2
else:
log_level = 1
return log_level >= levels[level]
class MyFormatter(logging.Formatter):
def __init__(self, fmt='%(name)s -> %(levelname)s::%(relpath)s:%(lineno)s %(message)s'):
logging.Formatter.__init__(self, fmt)
def format(self, record):
if record.pathname:
record.pathname = ensure_text(record.pathname, get_filesystem_encoding())
self._gen_rel_path(record)
# Call the original formatter class to do the grunt work
result = logging.Formatter.format(self, record)
return result
def formatException(self, exc_info):
_pluginpath_real = os.path.realpath(__pluginpath__)
res = []
for o in traceback.format_exception(*exc_info):
o = ensure_text(o, get_filesystem_encoding())
if o.startswith(' File "'):
# If this split can't handle your file names, you should seriously consider renaming your files.
fn = o.split(' File "', 2)[1].split('", line ', 1)[0]
rfn = os.path.realpath(fn)
if rfn.startswith(_pluginpath_real):
o = o.replace(fn, os.path.relpath(rfn, _pluginpath_real))
res.append(o)
return ''.join(res)
def _gen_rel_path(self, record):
if record.pathname:
record.relpath = os.path.relpath(record.pathname, __pluginpath__)
def get_filesystem_encoding():
enc = sys.getfilesystemencoding()
if not enc:
enc = sys.getdefaultencoding()
if not enc or enc == 'ascii':
enc = 'utf-8'
return enc
__LOGGER = logging.getLogger('JELLYFIN')
for handler in __LOGGER.handlers:
__LOGGER.removeHandler(handler)
__LOGGER.addHandler(LogHandler())
__LOGGER.setLevel(logging.DEBUG)

File diff suppressed because it is too large Load Diff

80
resources/lib/monitors.py Normal file
View File

@@ -0,0 +1,80 @@
from __future__ import division, absolute_import, print_function, unicode_literals
import threading
import time
import xbmc
from .functions import show_menu
from .lazylogger import LazyLogger
from .widgets import check_for_new_content
from .tracking import timer
log = LazyLogger(__name__)
class ContextMonitor(threading.Thread):
stop_thread = False
def run(self):
item_id = None
log.debug("ContextMonitor Thread Started")
while not xbmc.Monitor().abortRequested() and not self.stop_thread:
if xbmc.getCondVisibility("Window.IsActive(fullscreenvideo) | Window.IsActive(visualisation)"):
xbmc.sleep(1000)
else:
if xbmc.getCondVisibility("Window.IsVisible(contextmenu)"):
if item_id:
xbmc.executebuiltin("Dialog.Close(contextmenu,true)")
params = {}
params["item_id"] = item_id
show_menu(params)
container_id = xbmc.getInfoLabel("System.CurrentControlID")
item_id = xbmc.getInfoLabel("Container(" + str(container_id) + ").ListItem.Property(id)")
xbmc.sleep(100)
log.debug("ContextMonitor Thread Exited")
def stop_monitor(self):
log.debug("ContextMonitor Stop Called")
self.stop_thread = True
class LibraryChangeMonitor(threading.Thread):
last_library_change_check = 0
library_check_triggered = False
exit_now = False
time_between_checks = 10
def __init__(self):
threading.Thread.__init__(self)
def stop(self):
self.exit_now = True
@timer
def check_for_updates(self):
log.debug("Trigger check for updates")
self.library_check_triggered = True
def run(self):
log.debug("Library Monitor Started")
monitor = xbmc.Monitor()
while not self.exit_now and not monitor.abortRequested():
if self.library_check_triggered and not xbmc.Player().isPlaying():
log.debug("Doing new content check")
check_for_new_content()
self.library_check_triggered = False
self.last_library_change_check = time.time()
if self.exit_now or monitor.waitForAbort(self.time_between_checks):
break
log.debug("Library Monitor Exited")

View File

@@ -1,10 +1,11 @@
import xbmc
import xbmcaddon
from __future__ import division, absolute_import, print_function, unicode_literals
import xbmcgui
from .simple_logging import SimpleLogging
from .lazylogger import LazyLogger
log = LazyLogger(__name__)
log = SimpleLogging(__name__)
class PictureViewer(xbmcgui.WindowXMLDialog):
picture_url = None
@@ -21,11 +22,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

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,16 @@
from __future__ import division, absolute_import, print_function, unicode_literals
import os
import threading
import xbmc
import xbmcgui
import xbmcaddon
from .simple_logging import SimpleLogging
from .play_utils import send_event_notification
from .lazylogger import LazyLogger
from .dialogs import PlayNextDialog
from .utils import translate_path
log = SimpleLogging(__name__)
log = LazyLogger(__name__)
class PlayNextService(threading.Thread):
@@ -30,6 +32,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()
@@ -38,7 +42,14 @@ class PlayNextService(threading.Thread):
if not is_playing:
settings = xbmcaddon.Addon()
play_next_trigger_time = int(settings.getSetting('play_next_trigger_time'))
log.debug("New play_next_trigger_time value: {0}", 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()
@@ -47,10 +58,10 @@ class PlayNextService(threading.Thread):
if not play_next_triggered and (trigger_time > time_to_end) and play_next_dialog is None:
play_next_triggered = True
log.debug("play_next_triggered hit at {0} seconds from end", time_to_end)
log.debug("play_next_triggered hit at {0} seconds from end".format(time_to_end))
play_data = get_playing_data(self.monitor.played_information)
log.debug("play_next_triggered play_data : {0}", play_data)
play_data = get_playing_data()
log.debug("play_next_triggered play_data : {0}".format(play_data))
next_episode = play_data.get("next_episode")
item_type = play_data.get("item_type")
@@ -59,7 +70,7 @@ class PlayNextService(threading.Thread):
settings = xbmcaddon.Addon()
plugin_path = settings.getAddonInfo('path')
plugin_path_real = xbmc.translatePath(os.path.join(plugin_path))
plugin_path_real = translate_path(os.path.join(plugin_path))
play_next_dialog = PlayNextDialog("PlayNextDialog.xml", plugin_path_real, "default", "720p")
play_next_dialog.set_episode_info(next_episode)
@@ -76,6 +87,7 @@ class PlayNextService(threading.Thread):
play_next_dialog = None
is_playing = False
now_playing = None
if xbmc.Monitor().waitForAbort(1):
break
@@ -83,67 +95,3 @@ class PlayNextService(threading.Thread):
def stop_servcie(self):
log.debug("PlayNextService Stop Called")
self.stop_thread = True
class PlayNextDialog(xbmcgui.WindowXMLDialog):
action_exitkeys_id = None
episode_info = None
play_called = False
def __init__(self, *args, **kwargs):
log.debug("PlayNextDialog: __init__")
xbmcgui.WindowXML.__init__(self, *args, **kwargs)
def onInit(self):
log.debug("PlayNextDialog: onInit")
self.action_exitkeys_id = [10, 13]
index = self.episode_info.get("IndexNumber", -1)
series_name = self.episode_info.get("SeriesName")
next_epp_name = "Episode %02d - (%s)" % (index, self.episode_info.get("Name", "n/a"))
series_label = self.getControl(3011)
series_label.setLabel(series_name)
series_label = self.getControl(3012)
series_label.setLabel(next_epp_name)
def onFocus(self, control_id):
pass
def doAction(self, action_id):
pass
def onMessage(self, message):
log.debug("PlayNextDialog: onMessage: {0}", message)
def onAction(self, action):
if action.getId() == 10: # ACTION_PREVIOUS_MENU
self.close()
elif action.getId() == 92: # ACTION_NAV_BACK
self.close()
else:
log.debug("PlayNextDialog: onAction: {0}", action.getId())
def onClick(self, control_id):
if control_id == 3013:
log.debug("PlayNextDialog: Play Next Episode")
self.play_called
self.close()
next_item_id = self.episode_info.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)
elif control_id == 3014:
self.close()
def set_episode_info(self, info):
self.episode_info = info
def get_play_called(self):
return self.play_called

View File

@@ -1,44 +0,0 @@
# Gnu General Public License - see LICENSE.TXT
import xbmcgui
from .simple_logging import SimpleLogging
from .translation import string_load
log = SimpleLogging(__name__)
class ResumeDialog(xbmcgui.WindowXMLDialog):
resumePlay = -1
resumeTimeStamp = ""
action_exitkeys_id = None
def __init__(self, *args, **kwargs):
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
log.debug("ResumeDialog INITIALISED")
def onInit(self):
self.action_exitkeys_id = [10, 13]
self.getControl(3010).setLabel(self.resumeTimeStamp)
self.getControl(3011).setLabel(string_load(30237))
def onFocus(self, controlId):
pass
def doAction(self, actionID):
pass
def onClick(self, controlID):
if controlID == 3010:
self.resumePlay = 0
self.close()
if controlID == 3011:
self.resumePlay = 1
self.close()
def setResumeTime(self, timeStamp):
self.resumeTimeStamp = timeStamp
def getResumeAction(self):
return self.resumePlay

View File

@@ -1,56 +0,0 @@
# Gnu General Public License - see LICENSE.TXT
import xbmc
import xbmcgui
from .simple_logging import SimpleLogging
log = SimpleLogging(__name__)
class SafeDeleteDialog(xbmcgui.WindowXMLDialog):
confirm = False
message = "Demo Message"
heading = "Demo Heading"
action_exitkeys_id = None
def __init__(self, *args, **kwargs):
log.debug("SafeDeleteDialog: __init__")
xbmcgui.WindowXML.__init__(self, *args, **kwargs)
def onInit(self):
log.debug("SafeDeleteDialog: onInit")
self.action_exitkeys_id = [10, 13]
message_control = self.getControl(3)
message_control.setText(self.message)
message_control = self.getControl(4)
message_control.setLabel(self.heading)
def onFocus(self, controlId):
pass
def doAction(self, actionID):
pass
def onMessage(self, message):
log.debug("SafeDeleteDialog: onMessage: {0}", message)
def onAction(self, action):
if action.getId() == 10: # ACTION_PREVIOUS_MENU
self.close()
elif action.getId() == 92: # ACTION_NAV_BACK
self.close()
else:
log.debug("SafeDeleteDialog: onAction: {0}", action.getId())
def onClick(self, controlID):
if controlID == 1:
self.confirm = True
self.close()
elif controlID == 2:
self.confirm = False
self.close()

View File

@@ -1,12 +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
import httplib
import ssl
import time
import hashlib
from datetime import datetime
import xbmcaddon
@@ -14,13 +11,12 @@ import xbmcgui
import xbmc
from .kodi_utils import HomeWindow
from .downloadutils import DownloadUtils, save_user_details, load_user_details
from .simple_logging import SimpleLogging
from .translation import string_load
from .utils import datetime_from_string
from .clientinfo import ClientInformation
from .jellyfin import API
from .lazylogger import LazyLogger
from .utils import datetime_from_string, translate_string, save_user_details, load_user_details, get_current_datetime
from .dialogs import QuickConnectDialog
log = SimpleLogging(__name__)
log = LazyLogger(__name__)
__addon__ = xbmcaddon.Addon()
__addon_name__ = __addon__.getAddonInfo('name')
@@ -30,76 +26,43 @@ def check_connection_speed():
log.debug("check_connection_speed")
settings = xbmcaddon.Addon()
verify_cert = settings.getSetting('verify_cert') == 'true'
http_timeout = int(settings.getSetting("http_timeout"))
speed_test_data_size = int(settings.getSetting("speed_test_data_size"))
test_data_size = speed_test_data_size * 1000000
user_details = load_user_details()
du = DownloadUtils()
server = du.get_server()
url = server + "/playback/bitratetest?size=%s" % test_data_size
url_bits = urlparse(url.strip())
protocol = url_bits.scheme
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_puery = url_bits.query
server = "%s:%s" % (host_name, port)
url_path = url_path + "?" + url_puery
local_use_https = False
if protocol.lower() == "https":
local_use_https = True
if local_use_https and verify_cert:
log.debug("Connection: HTTPS, Cert checked")
conn = httplib.HTTPSConnection(server, timeout=http_timeout)
elif local_use_https and not verify_cert:
log.debug("Connection: HTTPS, Cert NOT checked")
conn = httplib.HTTPSConnection(server, timeout=http_timeout, context=ssl._create_unverified_context())
else:
log.debug("Connection: HTTP")
conn = httplib.HTTPConnection(server, timeout=http_timeout)
head = du.get_auth_header(True)
head["User-Agent"] = "JellyCon-" + ClientInformation().get_version()
conn.request(method="GET", url=url_path, headers=head)
api = API(
settings.getSetting('server_address'),
user_details.get('user_id'),
user_details.get('token')
)
progress_dialog = xbmcgui.DialogProgress()
message = 'Testing with {0} MB of data'.format(speed_test_data_size)
progress_dialog.create("JellyCon connection speed test", message)
total_data_read = 0
total_time = time.time()
start_time = time.time()
log.debug("Starting Connection Speed Test")
response = conn.getresponse()
response = api.speedtest(test_data_size)
last_percentage_done = 0
if int(response.status) == 200:
data = response.read(10240)
while len(data) > 0:
total_data_read = 0
if response.status_code == 200:
for data in response.iter_content(chunk_size=10240):
total_data_read += len(data)
percentage_done = int(float(total_data_read) / float(test_data_size) * 100.0)
if last_percentage_done != percentage_done:
progress_dialog.update(percentage_done)
last_percentage_done = percentage_done
data = response.read(10240)
else:
log.error("HTTP response error: {0} {1}", response.status, response.reason)
error_message = "HTTP response error: %s\n%s" % (response.status, response.reason)
log.error("HTTP response error: {0} {1}".format(response.status_code, response.content))
error_message = "HTTP response error: %s\n%s" % (response.status_code, response.content)
xbmcgui.Dialog().ok("Speed Test Error", error_message)
return -1
total_data_read_kbits = (total_data_read * 8) / 1000
total_time = time.time() - total_time
total_time = time.time() - start_time
speed = int(total_data_read_kbits / total_time)
log.debug("Finished Connection Speed Test, speed: {0} total_data: {1}, total_time: {2}", speed, total_data_read, total_time)
log.debug("Finished Connection Speed Test, speed: {0} total_data: {1}, total_time: {2}".format(speed, total_data_read, total_time))
progress_dialog.close()
del progress_dialog
@@ -115,75 +78,44 @@ def check_connection_speed():
return speed
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:
log.debug("Server Plugin List: {0}", result)
safe_delete_found = False
for plugin in result:
if plugin["Name"] == "Safe Delete":
safe_delete_found = True
break
log.debug("Safe Delete Plugin Available: {0}", safe_delete_found)
home_window = HomeWindow()
if safe_delete_found:
home_window.set_property("safe_delete_plugin_available", "true")
else:
home_window.clear_property("safe_delete_plugin_available")
else:
log.debug("Error getting server plugin list")
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}", multi_group)
log.debug("Sending UDP Data: {0}", message)
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.update(0, string_load(30374))
progress.create('{} : {}'.format(__addon_name__, translate_string(30373)))
progress.update(0, translate_string(30374))
xbmc.sleep(1000)
server_count = 0
# while True:
try:
sock.sendto(message, multi_group)
while True:
try:
server_count += 1
progress.update(server_count * 10, string_load(30375) % server_count)
progress.update(server_count * 10, '{}: {}'.format(translate_string(30375), server_count))
xbmc.sleep(1000)
data, addr = sock.recvfrom(1024)
servers.append(json.loads(data))
except:
break
except Exception as e:
log.error("UPD Discovery Error: {0}", e)
log.error("UPD Discovery Error: {0}".format(e))
progress.close()
log.debug("Found Servers: {0}", servers)
log.debug("Found Servers: {0}".format(servers))
return servers
@@ -191,18 +123,17 @@ def check_server(force=False, change_user=False, notify=False):
log.debug("checkServer Called")
settings = xbmcaddon.Addon()
server_url = ""
something_changed = False
du = DownloadUtils()
# Initialize api object
api = API()
if force is False:
# if not forcing use server details from settings
svr = du.get_server()
if svr is not None:
server_url = svr
api.server = settings.getSetting('server_address')
# if the server is not set then try to detect it
if server_url == "":
if not api.server:
# scan for local server
server_info = get_server_details()
@@ -212,7 +143,7 @@ def check_server(force=False, change_user=False, notify=False):
server_list = []
for server in server_info:
server_item = xbmcgui.ListItem(server.get("Name", string_load(30063)))
server_item = xbmcgui.ListItem(server.get("Name", translate_string(30063)))
sub_line = server.get("Address")
server_item.setLabel2(sub_line)
server_item.setProperty("address", server.get("Address"))
@@ -221,277 +152,273 @@ 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__, translate_string(30166)),
server_list,
useDetails=True)
if return_index != -1:
server_url = server_info[return_index]["Address"]
api.server = server_info[return_index]["Address"]
if not server_url:
return_index = xbmcgui.Dialog().yesno(__addon_name__, string_load(30282), string_load(30370))
if not api.server:
return_index = xbmcgui.Dialog().yesno(__addon_name__, '{}\n{}'.format(translate_string(30282), translate_string(30370)))
if not return_index:
xbmc.executebuiltin("ActivateWindow(Home)")
return
while True:
kb = xbmc.Keyboard()
kb.setHeading(string_load(30372))
if server_url:
kb.setDefault(server_url)
kb.setHeading(translate_string(30372))
if api.server:
kb.setDefault(api.server)
else:
kb.setDefault("http://<server address>:8096")
kb.setDefault("http://")
kb.doModal()
if kb.isConfirmed():
server_url = kb.getText()
api.server = kb.getText()
else:
xbmc.executebuiltin("ActivateWindow(Home)")
return
url_bits = urlparse(server_url)
server_address = url_bits.hostname
server_port = str(url_bits.port)
server_protocol = url_bits.scheme
user_name = url_bits.username
user_password = url_bits.password
if user_name and user_password:
temp_url = "%s://%s:%s@%s:%s/Users/Public?format=json" % (server_protocol, user_name, user_password, server_address, server_port)
else:
temp_url = "%s://%s:%s/Users/Public?format=json" % (server_protocol, server_address, server_port)
log.debug("Testing_Url: {0}", temp_url)
progress = xbmcgui.DialogProgress()
progress.create(__addon_name__ + " : " + string_load(30376))
progress.update(0, string_load(30377))
json_data = du.download_url(temp_url, authenticate=False)
progress.create('{} : {}'.format(__addon_name__, translate_string(30376)))
progress.update(0, translate_string(30377))
result = api.get('/System/Info/Public')
progress.close()
result = json.loads(json_data)
if result is not None:
xbmcgui.Dialog().ok(__addon_name__ + " : " + string_load(30167),
"%s://%s:%s/" % (server_protocol, server_address, server_port))
if result:
xbmcgui.Dialog().ok('{} : {}'.format(__addon_name__, translate_string(30167)),
api.server)
break
else:
return_index = xbmcgui.Dialog().yesno(__addon_name__ + " : " + string_load(30135),
server_url,
string_load(30371))
return_index = xbmcgui.Dialog().yesno('{} : {}'.format(__addon_name__, translate_string(30135)),
api.server,
translate_string(30371))
if not return_index:
xbmc.executebuiltin("ActivateWindow(Home)")
return
log.debug("Selected server: {0}", server_url)
# parse the url
url_bits = urlparse(server_url)
server_address = url_bits.hostname
server_port = str(url_bits.port)
server_protocol = url_bits.scheme
user_name = url_bits.username
user_password = url_bits.password
log.debug("Detected server info {0} - {1} - {2}", server_protocol, server_address, server_port)
# save the server info
settings.setSetting("port", server_port)
if user_name and user_password:
server_address = "%s:%s@%s" % (url_bits.username, url_bits.password, server_address)
settings.setSetting("ipaddress", server_address)
if server_protocol == "https":
settings.setSetting("protocol", "1")
else:
settings.setSetting("protocol", "0")
log.debug("Selected server: {0}".format(api.server))
settings.setSetting("server_address", api.server)
something_changed = True
# 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 = settings.getSetting('username')
user_details = load_user_details()
# if asked or we have no current user then show user selection screen
if something_changed or change_user or len(current_username) == 0:
if something_changed or change_user or len(current_username) == 0 or not user_details:
# stop playback when switching users
xbmc.Player().stop()
du = DownloadUtils()
# get a list of users
log.debug("Getting user list")
json_data = du.download_url(server_url + "/Users/Public?format=json", authenticate=False)
auth = quick_connect(api)
log.debug("jsonData: {0}", json_data)
try:
result = json.loads(json_data)
except:
result = None
if result is None:
xbmcgui.Dialog().ok(string_load(30135),
string_load(30201),
string_load(30169) + server_url)
else:
selected_id = -1
if auth:
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)
user_selection = -1
selected_user_name = auth.get('User', {}).get('Name')
something_changed = True
else:
users, user_selection = user_select(api, current_username)
time_ago = ""
last_active = user.get("LastActivityDate")
if last_active:
last_active_date = datetime_from_string(last_active)
log.debug("LastActivityDate: {0}", last_active_date)
ago = datetime.now() - last_active_date
log.debug("LastActivityDate: {0}", ago)
days = divmod(ago.seconds, 86400)
hours = divmod(days[1], 3600)
minutes = divmod(hours[1], 60)
log.debug("LastActivityDate: {0} {1} {2}", 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}", time_ago)
if not auth and user_selection > -1:
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")
something_changed = True
selected_user = users[user_selection]
selected_user_name = selected_user.getLabel()
secured = selected_user.getProperty("secure") == "true"
manual = selected_user.getProperty("manual") == "true"
sub_line = time_ago
home_window = HomeWindow()
if user.get("HasPassword", False) is True:
sub_line += ", Password"
user_item.setProperty("secure", "true")
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"
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:
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}", return_value, selected_user_name)
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}", 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}", selected_user_name)
log.debug("Using stored password for user: {0}", hashed_username)
save_user_details(settings, selected_user_name, saved_password)
else:
kb = xbmc.Keyboard()
kb.setHeading(string_load(30006))
kb.setHiddenInput(True)
kb.doModal()
if kb.isConfirmed():
log.debug("Saving username and password: {0}", 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}", hashed_username)
settings.setSetting("saved_user_password_" + hashed_username, kb.getText())
# If using a manual login, ask for username
if manual:
kb = xbmc.Keyboard()
kb.setHeading(translate_string(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:
log.debug("Saving username with no password: {0}", selected_user_name)
save_user_details(settings, selected_user_name, "")
return
home_window.set_property('user_name', selected_user_name)
user_details = load_user_details()
# Ask for password if user has one
password = ''
if secured and not user_details.get('token'):
kb = xbmc.Keyboard()
kb.setHeading(translate_string(30006))
kb.setHiddenInput(True)
kb.doModal()
if kb.isConfirmed():
password = kb.getText()
auth_payload = {'username': selected_user_name, 'pw': password}
auth = api.authenticate(auth_payload)
if something_changed:
home_window = HomeWindow()
home_window.clear_property("userid")
home_window.clear_property("AccessToken")
home_window.clear_property("userimage")
home_window.clear_property("jellycon_widget_reload")
du = DownloadUtils()
du.authenticate()
du.get_user_id()
token = auth.get('AccessToken')
user_id = auth.get('User').get('Id')
save_user_details(selected_user_name, user_id, token)
xbmc.executebuiltin("ActivateWindow(Home)")
if "estuary_jellycon" in xbmc.getSkinDir():
xbmc.executebuiltin("SetFocus(9000, 0, absolute)")
xbmc.executebuiltin("ReloadSkin()")
def quick_connect(api):
'''
Log in using quick connect funcion
'''
settings = xbmcaddon.Addon()
addon_path = settings.getAddonInfo('path')
result = api.get('/QuickConnect/Initiate')
if not isinstance(result, dict) or not result:
log.debug('Quick connect is disabled on the server')
return {}
code = result.get('Code')
secret = result.get('Secret')
# Open Quick Connect dialog, ask to proceed
qc_dialog = QuickConnectDialog("QuickConnectDialog.xml", addon_path, "default", "720p")
qc_dialog.code = code
qc_dialog.doModal()
connect_method = qc_dialog.getConnectMethod()
del qc_dialog
if connect_method < 1:
# User backed out or selected manual login
return {}
count = 0
while count < 15:
# Check the server to see if the auth request has been completed
log.debug('Checking for quick connect auth: attempt {}'.format(count))
check = api.get('/QuickConnect/Connect?secret={}'.format(secret))
if check.get('Authenticated'):
break
count += 1
xbmc.sleep(1000)
if not check.get('Authenticated'):
log.info('Quick connect not authorized in 15 seconds, defaulting to manual authentication')
return {}
# Retrieve authentication information
auth = api.post('/Users/AuthenticateWithQuickConnect',
{'secret': secret})
return auth
def user_select(api, current_username):
'''
Display user selection screen
'''
# Retrieve list of public users from server
result = api.get('/Users/Public')
# Build user display
selected_id = -1
users = []
for user in result:
user_item = create_user_listitem(api.server, user)
if user_item:
users.append(user_item)
name = user.get("Name")
# Highlight currently logged in user
if current_username == name:
selected_id = len(users) - 1
if current_username:
selection_title = translate_string(30180) + " (" + current_username + ")"
else:
selection_title = translate_string(30180)
# Add manual login item
user_item = xbmcgui.ListItem(translate_string(30365))
art = {"Thumb": "DefaultUser.png"}
user_item.setArt(art)
user_item.setLabel2(translate_string(30366))
user_item.setProperty("secure", "true")
user_item.setProperty("manual", "true")
users.append(user_item)
user_selection = xbmcgui.Dialog().select(
selection_title,
users,
preselect=selected_id,
autoclose=20000,
useDetails=True)
return (users, user_selection)
def create_user_listitem(server, user):
'''
Create a user listitem for the user selection screen
'''
config = user.get("Configuration")
now = get_current_datetime()
if config is not None:
name = user.get("Name")
time_ago = ""
last_active = user.get("LastActivityDate")
# Calculate how long it's been since the user was last active
if last_active:
last_active_date = datetime_from_string(last_active)
ago = now - last_active_date
# Check days
if ago.days > 0:
time_ago += ' {}d'.format(ago.days)
# Check minutes
if ago.seconds > 60:
hours = 0
# Check hours
if ago.seconds > 3600:
hours = int(ago.seconds/3600)
time_ago += ' {}h'.format(hours)
minutes = int((ago.seconds - (hours * 3600)) / 60)
time_ago += ' {}m'.format(minutes)
time_ago = time_ago.strip()
if not time_ago:
time_ago = "Active: now"
else:
time_ago = "Active: {} ago".format(time_ago)
user_item = xbmcgui.ListItem(name)
# If the user doesn't have a profile image, user the default
if 'PrimaryImageTag' not in user:
user_image = "DefaultUser.png"
else:
user_id = user.get('Id')
tag = user.get('PrimaryImageTag')
user_image = '{}/Users/{}/Images/Primary?Format=original&tag={}'.format(
server, user_id, tag
)
art = {"Thumb": user_image}
user_item.setArt(art)
sub_line = time_ago
if user.get("HasPassword", False) is True:
user_item.setProperty("secure", "true")
else:
user_item.setProperty("secure", "false")
user_item.setProperty("manual", "false")
user_item.setLabel2(sub_line)
return user_item
return None

View File

@@ -1,40 +1,42 @@
from __future__ import division, absolute_import, print_function, unicode_literals
import json
import sys
import xbmcgui
import xbmcplugin
import xbmcaddon
from .downloadutils import DownloadUtils
from .simple_logging import SimpleLogging
from .utils import get_art
from .datamanager import DataManager
from .jellyfin import api
from .lazylogger import LazyLogger
from .item_functions import get_art
from .utils import load_user_details
log = SimpleLogging(__name__)
log = LazyLogger(__name__)
def show_server_sessions():
log.debug("showServerSessions Called")
handle = int(sys.argv[1])
download_utils = DownloadUtils()
data_manager = DataManager()
url = "{server}/Users/{userid}"
results = data_manager.get_content(url)
user_details = load_user_details()
url = "/Users/{}".format(user_details.get('user_id'))
results = api.get(url)
is_admin = results.get("Policy", {}).get("IsAdministrator", False)
if not is_admin:
xbmcplugin.endOfDirectory(handle, cacheToDisc=False)
return
url = "{server}/Sessions"
results = data_manager.get_content(url)
log.debug("session_info: {0}", results)
url = "/Sessions"
results = api.get(url)
log.debug("session_info: {0}".format(results))
if results is None:
return
list_items = []
settings = xbmcaddon.Addon()
server = settings.getSetting('server_address')
for session in results:
device_name = session.get("DeviceName", "na")
user_name = session.get("UserName", "na")
@@ -59,7 +61,6 @@ def show_server_sessions():
art = {}
if now_playing:
server = download_utils.get_server()
art = get_art(now_playing, server)
runtime = now_playing.get("RunTimeTicks", 0)

View File

@@ -1,51 +0,0 @@
# Gnu General Public License - see LICENSE.TXT
import xbmc
import xbmcaddon
from .jsonrpc import JsonRpc
class SimpleLogging:
name = ""
enable_logging = False
def __init__(self, name):
settings = xbmcaddon.Addon()
prefix = settings.getAddonInfo('name')
self.name = prefix + '.' + name
self.enable_logging = settings.getSetting('log_debug') == "true"
# params = {"setting": "debug.showloginfo"}
# setting_result = json_rpc('Settings.getSettingValue').execute(params)
# current_value = setting_result.get("result", None)
# if current_value is not None:
# self.enable_logging = current_value.get("value", False)
# xbmc.log("LOGGING_ENABLED %s : %s" % (self.name, str(self.enable_logging)), level=xbmc.LOGDEBUG)
def __str__(self):
return "LoggingEnabled: " + str(self.enable_logging)
def info(self, fmt, *args, **kwargs):
log_line = self.name + "|INFO|" + self.log_line(fmt, *args)
xbmc.log(log_line, level=xbmc.LOGNOTICE)
def error(self, fmt, *args, **kwargs):
log_line = self.name + "|ERROR|" + self.log_line(fmt, *args)
xbmc.log(log_line, level=xbmc.LOGERROR)
def debug(self, fmt, *args, **kwargs):
if self.enable_logging:
log_line = self.name + "|DEBUG|" + self.log_line(fmt, *args)
xbmc.log(log_line, level=xbmc.LOGNOTICE)
@staticmethod
def log_line(fmt, *args):
new_args = []
# convert any unicode to utf-8 strings
for arg in args:
if isinstance(arg, unicode):
new_args.append(arg.encode("utf-8"))
else:
new_args.append(arg)
log_line = fmt.format(*new_args)
return log_line

View File

@@ -1,16 +1,18 @@
# 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
import xbmcvfs
from .jsonrpc import JsonRpc, get_value, set_value
from .simple_logging import SimpleLogging
from .lazylogger import LazyLogger
from .utils import translate_path, kodi_version
log = SimpleLogging(__name__)
ver = xbmc.getInfoLabel('System.BuildVersion')[:2]
log = LazyLogger(__name__)
def clone_default_skin():
@@ -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)
@@ -50,9 +49,9 @@ def walk_path(root_path, relative_path, all_files):
def clone_skin():
log.debug("Cloning Estuary Skin")
kodi_path = xbmc.translatePath("special://xbmc")
kodi_path = translate_path("special://xbmc")
kodi_skin_source = os.path.join(kodi_path, "addons", "skin.estuary")
log.debug("Kodi Skin Source: {0}", kodi_skin_source)
log.debug("Kodi Skin Source: {0}".format(kodi_skin_source))
pdialog = xbmcgui.DialogProgress()
pdialog.create("JellyCon Skin Cloner", "")
@@ -60,11 +59,11 @@ def clone_skin():
all_files = []
walk_path(kodi_skin_source, "", all_files)
for found in all_files:
log.debug("Found Path: {0}", found)
log.debug("Found Path: {0}".format(found))
kodi_home_path = xbmc.translatePath("special://home")
kodi_home_path = translate_path("special://home")
kodi_skin_destination = os.path.join(kodi_home_path, "addons", "skin.estuary_jellycon")
log.debug("Kodi Skin Destination: {0}", kodi_skin_destination)
log.debug("Kodi Skin Destination: {0}".format(kodi_skin_destination))
# copy all skin files (clone)
count = 0
@@ -81,22 +80,18 @@ 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")
log.debug("Major Version: {0}", ver)
log.debug("Major Version: {0}".format(kodi_version()))
file_list = ["Home.xml",
"Includes_Home.xml",
@@ -104,8 +99,9 @@ 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)
source = os.path.join(jellycon_path, "resources", "skins", "skin.estuary", str(kodi_version), "xml", file_name)
destination = os.path.join(kodi_skin_destination, "xml", file_name)
xbmcvfs.copy(source, destination)
@@ -123,18 +119,17 @@ def clone_skin():
'enabled': True
}
result = JsonRpc('Addons.SetAddonEnabled').execute(params)
log.debug("Addons.SetAddonEnabled : {0}", result)
log.debug("Addons.SetAddonEnabled : {0}".format(result))
log.debug("SkinCloner : Current Skin : " + get_value("lookandfeel.skin"))
set_result = set_value("lookandfeel.skin", "skin.estuary_jellycon")
log.debug("Save Setting : lookandfeel.skin : {0}", set_result)
log.debug("Save Setting : lookandfeel.skin : {0}".format(set_result))
log.debug("SkinCloner : Current Skin : " + get_value("lookandfeel.skin"))
def update_kodi_settings():
log.debug("Settings Kodi Settings")
# set_value("screensaver.mode", "script.screensaver.logoff")
set_value("videoplayer.seekdelay", 0)
set_value("filelists.showparentdiritems", False)
set_value("filelists.showaddsourcebuttons", False)

View File

@@ -1,11 +1,12 @@
# Gnu General Public License - see LICENSE.TXT
from __future__ import division, absolute_import, print_function, unicode_literals
import sys
import functools
import time
from .simple_logging import SimpleLogging
from .lazylogger import LazyLogger
log = SimpleLogging(__name__)
log = LazyLogger(__name__)
enabled = False
@@ -27,6 +28,6 @@ def timer(func):
data = args[1]
elif func.__name__ == "main_entry_point" and len(sys.argv) > 2:
data = sys.argv[2]
log.info("timing_data|{0}|{1}|{2}|{3}", func.__name__, started, ended, data)
log.info("timing_data|{0}|{1}|{2}|{3}".format(func.__name__, started, ended, data))
return value
return wrapper

View File

@@ -1,248 +0,0 @@
# Gnu General Public License - see LICENSE.TXT
import urllib
import encodings
import xbmc
import xbmcgui
from .simple_logging import SimpleLogging
from .datamanager import DataManager
from .translation import string_load
log = SimpleLogging(__name__)
dataManager = DataManager()
details_string = 'EpisodeCount,SeasonCount,Path,Etag,MediaStreams'
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)
def playback_starting(content_string):
xbmcgui.Dialog().notification('JellyCon', string_load(30306) % content_string, icon=icon, sound=False)
def search(item_type, query):
content_url = ('{server}/Search/Hints?searchTerm=' + query +
'&IncludeItemTypes=' + item_type +
'&UserId={userid}'
'&StartIndex=0' +
'&Limit=25' +
'&IncludePeople=false&IncludeMedia=true&IncludeGenres=false&IncludeStudios=false&IncludeArtists=false')
result = dataManager.get_content(content_url)
return result
def get_items(video_type, item_id=None, parent_id=None):
content_url = None
result = dict()
if video_type == 'season':
content_url = ('{server}/Shows/' + item_id +
'/Seasons'
'?userId={userid}' +
'&Fields=' + details_string +
'&format=json')
elif video_type == 'movie' or video_type == 'episode':
content_url = ('{server}/Users/{userid}/items' +
'?ParentId=' + parent_id +
'&IsVirtualUnAired=false' +
'&IsMissing=false' +
'&Fields=' + details_string +
'&format=json')
if content_url:
result = dataManager.get_content(content_url)
return result
def get_item(item_id):
result = dataManager.get_content('{server}/Users/{userid}/Items/' + item_id + '?Fields=ProviderIds&format=json')
return result
def get_imdb_id(item_id):
item = get_item(item_id)
imdb = item.get('ProviderIds', {}).get('Imdb')
return imdb
def get_season_id(parent_id, season):
season_items = get_items('season', parent_id)
season_items = season_items.get('Items')
if season_items is None:
season_items = []
for season_item in season_items:
if season_item.get('IndexNumber') == int(season):
season_id = season_item.get('Id')
return season_id
return None
def get_episode_id(parent_id, episode):
episode_items = get_items('episode', parent_id=parent_id)
episode_items = episode_items.get('Items')
if episode_items is None:
episode_items = []
for episode_item in episode_items:
if episode_item.get('IndexNumber') == int(episode):
episode_id = episode_item.get('Id')
return episode_id
return None
def get_match(item_type, title, year, imdb_id):
query = urllib.quote(title)
results = search(item_type, query=query)
results = results.get('SearchHints')
if results is None:
results = []
log.debug('SearchHints jsonData: {0}', results)
potential_matches = []
for item in results:
name = item.get('Name')
production_year = item.get('ProductionYear')
if (name == title and int(year) == production_year) or (int(year) == production_year):
potential_matches.append(item)
log.debug('Potential matches: {0}', potential_matches)
for item in potential_matches:
item_imdb_id = get_imdb_id(item.get('ItemId'))
if item_imdb_id == imdb_id:
log.debug('Found match: {0}', item)
return item
return None
def entry_point(parameters):
item_type = None
action = parameters.get('action', None)
video_type = parameters.get('video_type', None)
title = urllib.unquote(parameters.get('title', ''))
year = parameters.get('year', '')
episode = parameters.get('episode', '')
season = parameters.get('season', '')
imdb_id = parameters.get('imdb_id', '')
if video_type == 'show' or video_type == 'season' or video_type == 'episode':
item_type = 'Series'
elif video_type == 'movie':
item_type = 'Movie'
if not item_type:
return
match = get_match(item_type, title, year, imdb_id)
if not match:
title_search_word = ''
title_words = title.split(' ')
for word in title_words:
if len(word) > len(title_search_word):
title_search_word = word
title_search_word = title_search_word.replace(':', '')
if title_search_word:
match = get_match(item_type, title_search_word, year, imdb_id)
str_season = str(season)
if len(str_season) == 1:
str_season = '0' + str_season
str_episode = str(episode)
if len(str_episode) == 1:
str_episode = '0' + str_episode
if action == 'play':
play_item_id = None
if video_type == 'movie':
if match:
play_item_id = match.get('ItemId')
if not play_item_id:
not_found('{title} ({year})'.format(title=title, year=year))
elif video_type == 'episode':
if not season or not episode:
return
if match:
item_id = match.get('ItemId')
season_id = get_season_id(item_id, season)
if season_id:
episode_id = get_episode_id(season_id, episode)
if episode_id:
play_item_id = episode_id
if not play_item_id:
not_found('{title} ({year}) - S{season}E{episode}'.format(title=title, year=year, season=str_season, episode=str_episode))
if play_item_id:
if video_type == 'episode':
playback_starting('{title} ({year}) - S{season}E{episode}'.format(title=title, year=year, season=str_season, episode=str_episode))
else:
playback_starting('{title} ({year})'.format(title=title, year=year))
xbmc.executebuiltin('RunPlugin(plugin://plugin.video.jellycon/?mode=PLAY&item_id={item_id})'.format(item_id=play_item_id))
elif action == 'open':
url = media_type = None
if video_type == 'show':
if match:
item_id = match.get('ItemId')
media_type = 'series'
url = ('{server}/Shows/' + item_id +
'/Seasons'
'?userId={userid}' +
'&Fields=' + details_string +
'&format=json')
if not url:
not_found('{title} ({year})'.format(title=title, year=year))
elif video_type == 'season':
if not season:
return
if match:
item_id = match.get('ItemId')
season_id = get_season_id(item_id, season)
if season_id:
media_type = 'episodes'
url = ('{server}/Users/{userid}/items' +
'?ParentId=' + season_id +
'&IsVirtualUnAired=false' +
'&IsMissing=false' +
'&Fields=' + details_string +
'&format=json')
if not url:
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))

View File

@@ -1,13 +0,0 @@
import xbmcaddon
from .simple_logging import SimpleLogging
log = SimpleLogging(__name__)
addon = xbmcaddon.Addon()
def string_load(string_id):
try:
return addon.getLocalizedString(string_id).encode('utf-8', 'ignore')
except Exception as e:
log.error('Failed String Load: {0} ({1})', string_id, e)
return str(string_id)

View File

@@ -1,189 +1,53 @@
# Gnu General Public License - see LICENSE.TXT
from __future__ import division, absolute_import, print_function, unicode_literals
import xbmcaddon
import xbmc
import xbmcvfs
from kodi_six.utils import py2_encode, py2_decode
import sys
import binascii
import string
import random
import urllib
import json
import base64
import time
import math
import os
import hashlib
import requests
from datetime import datetime
import calendar
from dateutil import tz
import re
from uuid import uuid4
from six import ensure_text, ensure_binary, text_type
from six.moves.urllib.parse import urlencode
from .downloadutils import DownloadUtils
from .simple_logging import SimpleLogging
from .clientinfo import ClientInformation
from .lazylogger import LazyLogger
from .kodi_utils import HomeWindow
# hack to get datetime strptime loaded
throwaway = time.strptime('20110101', '%Y%m%d')
# define our global download utils
downloadUtils = DownloadUtils()
log = SimpleLogging(__name__)
log = LazyLogger(__name__)
def get_jellyfin_url(base_url, params):
def kodi_version():
# Kodistubs returns empty string, causing Python 3 tests to choke on int()
# TODO: Make Kodistubs version configurable for testing purposes
if sys.version_info.major == 2:
default_versionstring = "18"
else:
default_versionstring = "19.1 (19.1.0) Git:20210509-85e05228b4"
version_string = xbmc.getInfoLabel('System.BuildVersion') or default_versionstring
return int(version_string.split(' ', 1)[0].split('.', 1)[0])
def get_jellyfin_url(path, 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
###########################################################################
class PlayUtils:
@staticmethod
def get_play_url(media_source, play_session_id):
log.debug("get_play_url - media_source: {0}", media_source)
# check if strm file Container
if media_source.get('Container') == 'strm':
log.debug("Detected STRM Container")
playurl, listitem_props = PlayUtils().get_strm_details(media_source)
if playurl is None:
log.debug("Error, no strm content")
return None, None, None
else:
return playurl, "0", listitem_props
# get all the options
addon_settings = xbmcaddon.Addon()
server = downloadUtils.get_server()
use_https = addon_settings.getSetting('protocol') == "1"
verify_cert = addon_settings.getSetting('verify_cert') == 'true'
allow_direct_file_play = addon_settings.getSetting('allow_direct_file_play') == 'true'
can_direct_play = media_source["SupportsDirectPlay"]
can_direct_stream = media_source["SupportsDirectStream"]
can_transcode = media_source["SupportsTranscoding"]
container = media_source["Container"]
playurl = None
playback_type = None
# check if file can be directly played
if allow_direct_file_play and can_direct_play:
direct_path = media_source["Path"]
direct_path = direct_path.replace("\\", "/")
direct_path = direct_path.strip()
# handle DVD structure
if container == "dvd":
direct_path = direct_path + "/VIDEO_TS/VIDEO_TS.IFO"
elif container == "bluray":
direct_path = direct_path + "/BDMV/index.bdmv"
if direct_path.startswith("//"):
direct_path = "smb://" + direct_path[2:]
log.debug("playback_direct_path: {0}", direct_path)
if xbmcvfs.exists(direct_path):
playurl = direct_path
playback_type = "0"
# check if file can be direct streamed
if can_direct_stream and playurl is None:
item_id = media_source.get('Id')
playurl = ("%s/Videos/%s/stream" +
"?static=true" +
"&PlaySessionId=%s" +
"&MediaSourceId=%s")
playurl = playurl % (server, item_id, play_session_id, item_id)
if use_https and not verify_cert:
playurl += "|verifypeer=false"
playurl = direct_stream_path
playback_type = "1"
# check is file can be transcoded
if can_transcode and playurl is None:
item_id = media_source.get('Id')
client_info = ClientInformation()
device_id = client_info.get_device_id()
user_token = downloadUtils.authenticate()
playback_bitrate = addon_settings.getSetting("force_max_stream_bitrate")
bitrate = int(playback_bitrate) * 1000
playback_max_width = addon_settings.getSetting("playback_max_width")
audio_codec = addon_settings.getSetting("audio_codec")
audio_playback_bitrate = addon_settings.getSetting("audio_playback_bitrate")
audio_bitrate = int(audio_playback_bitrate) * 1000
audio_max_channels = addon_settings.getSetting("audio_max_channels")
playback_video_force_8 = addon_settings.getSetting("playback_video_force_8") == "true"
transcode_params = {
"MediaSourceId": item_id,
"DeviceId": device_id,
"PlaySessionId": play_session_id,
"api_key": user_token,
"SegmentContainer": "ts",
"VideoCodec": "h264",
"VideoBitrate": bitrate,
"MaxWidth": playback_max_width,
"AudioCodec": audio_codec,
"TranscodingMaxAudioChannels": audio_max_channels,
"AudioBitrate": audio_bitrate
}
if playback_video_force_8:
transcode_params.update({"MaxVideoBitDepth": "8"})
transcode_path = urllib.urlencode(transcode_params)
playurl = "%s/Videos/%s/master.m3u8?%s" % (server, item_id, transcode_path)
if use_https and not verify_cert:
playurl += "|verifypeer=false"
playback_type = "2"
return playurl, playback_type, []
@staticmethod
def get_strm_details(media_source):
playurl = None
listitem_props = []
contents = media_source.get('Path').encode('utf-8') # contains contents of strm file with linebreaks
line_break = '\r'
if '\r\n' in contents:
line_break = '\r\n'
elif '\n' in contents:
line_break = '\n'
lines = contents.split(line_break)
for line in lines:
line = line.strip()
log.debug("STRM Line: {0}", line)
if line.startswith('#KODIPROP:'):
match = re.search('#KODIPROP:(?P<item_property>[^=]+?)=(?P<property_value>.+)', line)
if match:
item_property = match.group('item_property')
property_value = match.group('property_value')
log.debug("STRM property found: {0} value: {1}", item_property, property_value)
listitem_props.append((item_property, property_value))
else:
log.debug("STRM #KODIPROP incorrect format")
elif line.startswith('#'):
# unrecognized, treat as comment
log.debug("STRM unrecognized line identifier, ignored")
elif line != '':
playurl = line
log.debug("STRM playback url found")
log.debug("Playback URL: {0} ListItem Properties: {1}", playurl, listitem_props)
return playurl, listitem_props
url_params = urlencode(params)
return '{}?{}'.format(path, url_params)
def get_checksum(item):
@@ -201,131 +65,60 @@ def get_checksum(item):
return checksum
def get_art(item, server):
art = {
'thumb': '',
'fanart': '',
'poster': '',
'banner': '',
'clearlogo': '',
'clearart': '',
'discart': '',
'landscape': '',
'tvshow.fanart': '',
'tvshow.poster': '',
'tvshow.clearart': '',
'tvshow.clearlogo': '',
'tvshow.banner': '',
'tvshow.landscape': ''
}
image_tags = item["ImageTags"]
if image_tags is not None and image_tags["Primary"] is not None:
# image_tag = image_tags["Primary"]
art['thumb'] = downloadUtils.get_artwork(item, "Primary", server=server)
item_type = item["Type"]
if item_type == "Genre":
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)
art['clearlogo'] = downloadUtils.get_artwork(item, "Logo", parent=True, server=server)
art['tvshow.banner'] = downloadUtils.get_artwork(item, "Banner", parent=True, server=server)
art['banner'] = downloadUtils.get_artwork(item, "Banner", parent=True, server=server)
art['tvshow.landscape'] = downloadUtils.get_artwork(item, "Thumb", parent=True, server=server)
art['landscape'] = downloadUtils.get_artwork(item, "Thumb", parent=True, server=server)
art['tvshow.fanart'] = downloadUtils.get_artwork(item, "Backdrop", parent=True, server=server)
art['fanart'] = downloadUtils.get_artwork(item, "Backdrop", parent=True, server=server)
elif item_type == "Season":
art['tvshow.poster'] = downloadUtils.get_artwork(item, "Primary", parent=True, server=server)
art['season.poster'] = downloadUtils.get_artwork(item, "Primary", parent=False, server=server)
art['poster'] = downloadUtils.get_artwork(item, "Primary", parent=False, 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)
art['clearlogo'] = downloadUtils.get_artwork(item, "Logo", parent=True, server=server)
art['tvshow.banner'] = downloadUtils.get_artwork(item, "Banner", parent=True, server=server)
art['season.banner'] = downloadUtils.get_artwork(item, "Banner", parent=False, server=server)
art['banner'] = downloadUtils.get_artwork(item, "Banner", parent=False, server=server)
art['tvshow.landscape'] = downloadUtils.get_artwork(item, "Thumb", parent=True, server=server)
art['season.landscape'] = downloadUtils.get_artwork(item, "Thumb", parent=False, server=server)
art['landscape'] = downloadUtils.get_artwork(item, "Thumb", parent=False, server=server)
art['tvshow.fanart'] = downloadUtils.get_artwork(item, "Backdrop", parent=True, server=server)
art['fanart'] = downloadUtils.get_artwork(item, "Backdrop", parent=True, server=server)
elif item_type == "Series":
art['tvshow.poster'] = downloadUtils.get_artwork(item, "Primary", parent=False, server=server)
art['poster'] = downloadUtils.get_artwork(item, "Primary", parent=False, server=server)
art['tvshow.clearart'] = downloadUtils.get_artwork(item, "Art", parent=False, server=server)
art['clearart'] = downloadUtils.get_artwork(item, "Art", parent=False, server=server)
art['tvshow.clearlogo'] = downloadUtils.get_artwork(item, "Logo", parent=False, server=server)
art['clearlogo'] = downloadUtils.get_artwork(item, "Logo", parent=False, server=server)
art['tvshow.banner'] = downloadUtils.get_artwork(item, "Banner", parent=False, server=server)
art['banner'] = downloadUtils.get_artwork(item, "Banner", parent=False, server=server)
art['tvshow.landscape'] = downloadUtils.get_artwork(item, "Thumb", parent=False, server=server)
art['landscape'] = downloadUtils.get_artwork(item, "Thumb", parent=False, server=server)
art['tvshow.fanart'] = downloadUtils.get_artwork(item, "Backdrop", parent=False, server=server)
art['fanart'] = downloadUtils.get_artwork(item, "Backdrop", parent=False, server=server)
elif item_type == "Movie" or item_type == "BoxSet":
art['poster'] = downloadUtils.get_artwork(item, "Primary", server=server)
art['landscape'] = downloadUtils.get_artwork(item, "Thumb", server=server)
art['banner'] = downloadUtils.get_artwork(item, "Banner", server=server)
art['clearlogo'] = downloadUtils.get_artwork(item, "Logo", server=server)
art['clearart'] = downloadUtils.get_artwork(item, "Art", server=server)
art['discart'] = downloadUtils.get_artwork(item, "Disc", server=server)
art['fanart'] = downloadUtils.get_artwork(item, "Backdrop", server=server)
if not art['fanart']:
art['fanart'] = downloadUtils.get_artwork(item, "Backdrop", parent=True, server=server)
return art
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}", 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):
# Builtin python library can't handle ISO-8601 well. Make it compatible
if time_string[-1:] == "Z":
time_string = re.sub("[0-9]{1}Z", " UTC", time_string)
elif time_string[-6:] == "+00:00":
time_string = re.sub("[0-9]{1}\+00:00", " UTC", time_string)
log.debug("New Time String : {0}", time_string)
start_time = time.strptime(time_string, "%Y-%m-%dT%H:%M:%S.%f %Z")
dt = datetime(*(start_time[0:6]))
timestamp = calendar.timegm(dt.timetuple())
local_dt = datetime.fromtimestamp(timestamp)
local_dt.replace(microsecond=dt.microsecond)
return local_dt
try:
dt = datetime.strptime(time_string, "%Y-%m-%dT%H:%M:%S.%f %Z")
except TypeError:
# https://bugs.python.org/issue27400
dt = datetime(*(time.strptime(time_string, "%Y-%m-%dT%H:%M:%S.%f %Z")[0:6]))
# Dates received from the server are in UTC, but parsing them results in naive objects
utc = tz.tzutc()
utc_dt = dt.replace(tzinfo=utc)
return utc_dt
def get_current_datetime():
# Get current time in UTC
now = datetime.utcnow()
utc = tz.tzutc()
now_dt = now.replace(tzinfo=utc)
return now_dt
def convert_size(size_bytes):
@@ -336,3 +129,253 @@ def convert_size(size_bytes):
p = math.pow(1024, i)
s = round(size_bytes / p, 2)
return "%s %s" % (s, size_name[i])
def translate_string(string_id):
try:
addon = xbmcaddon.Addon()
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)
def get_device_id():
window = HomeWindow()
username = window.get_property('username')
client_id = window.get_property("client_id")
hashed_name = hashlib.md5(username.encode()).hexdigest()
if client_id:
return '{}-{}'.format(client_id, hashed_name)
jellyfin_guid_path = py2_decode(translate_path("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 = uuid4().hex
log.debug("Generating a new guid: {0}".format(client_id))
guid = xbmcvfs.File(jellyfin_guid_path, 'w')
guid.write(client_id)
guid.close()
log.debug("jellyfin_client_id (NEW): {0}".format(client_id))
else:
log.debug("jellyfin_client_id: {0}".format(client_id))
window.set_property("client_id", client_id)
return '{}-{}'.format(client_id, hashed_name)
def get_version():
addon = xbmcaddon.Addon()
version = addon.getAddonInfo("version")
return version
def save_user_details(user_name, user_id, token):
settings = xbmcaddon.Addon()
save_user_to_settings = settings.getSetting('save_user_to_settings') == 'true'
addon_data = translate_path(xbmcaddon.Addon().getAddonInfo('profile'))
# Save to a config file for reference later if desired
if save_user_to_settings:
try:
with open(os.path.join(addon_data, 'auth.json'), 'rb') as infile:
auth_data = json.load(infile)
except:
# File doesn't exist or is empty
auth_data = {}
auth_data[user_name] = {
'user_id': user_id,
'token': token
}
with open(os.path.join(addon_data, 'auth.json'), 'wb') as outfile:
data = json.dumps(auth_data, sort_keys=True, indent=4, ensure_ascii=False)
if isinstance(data, text_type):
data = data.encode('utf-8')
outfile.write(data)
# Make the username available for easy lookup
window = HomeWindow()
settings.setSetting('username', user_name)
window.set_property('user_name', user_name)
def load_user_details():
settings = xbmcaddon.Addon()
window = HomeWindow()
# Check current variables first, then check settings
user_name = window.get_property('user_name')
if not user_name:
user_name = settings.getSetting('username')
save_user_to_settings = settings.getSetting('save_user_to_settings') == 'true'
addon_data = translate_path(xbmcaddon.Addon().getAddonInfo('profile'))
if save_user_to_settings:
try:
with open(os.path.join(addon_data, 'auth.json'), 'rb') as infile:
auth_data = json.load(infile)
except:
# File doesn't exist yet
return {}
user_data = auth_data.get(user_name, {})
user_id = user_data.get('user_id')
auth_token = user_data.get('token')
# Payload to return to calling function
user_details = {}
user_details['user_name'] = user_name
user_details['user_id'] = user_id
user_details['token'] = auth_token
return user_details
else:
return {}
def get_current_user_id():
user_details = load_user_details()
user_id = user_details.get('user_id')
return user_id
def get_art_url(data, art_type, parent=False, index=0, server=None):
item_id = data["Id"]
item_type = data["Type"]
if item_type in ["Episode", "Season"]:
if art_type != "Primary" or parent is True:
item_id = data["SeriesId"]
image_tag = ""
# for episodes always use the parent BG
if item_type == "Episode" and art_type == "Backdrop":
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.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.get("BackdropImageTags", [])
if bg_tags:
image_tag = bg_tags[index]
elif parent is False:
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
elif parent is True:
if (item_type == "Episode" or item_type == "Season") and art_type == 'Primary':
tag_name = 'SeriesPrimaryImageTag'
id_name = 'SeriesId'
else:
tag_name = 'Parent%sImageTag' % art_type
id_name = 'Parent%sItemId' % art_type
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
# ParentTag not passed for Banner and Art
if not image_tag and not ((art_type == 'Banner' or art_type == 'Art') and parent is True):
return ""
artwork = "{}/Items/{}/Images/{}/{}?Format=original&Tag={}".format(
server, item_id, art_type, index, image_tag)
return artwork
def image_url(item_id, art_type, index, width, height, image_tag, server):
# test imageTag e3ab56fe27d389446754d0fb04910a34
artwork = "{}/Items/{}/Images/{}/{}?Format=original&Tag={}".format(server, item_id, art_type, index, image_tag)
if int(width) > 0:
artwork += '&MaxWidth={}'.format(width)
if int(height) > 0:
artwork += '&MaxHeight={}'.format(height)
return artwork
def get_default_filters():
addon_settings = xbmcaddon.Addon()
include_media = addon_settings.getSetting("include_media") == "true"
include_people = addon_settings.getSetting("include_people") == "true"
include_overview = addon_settings.getSetting("include_overview") == "true"
filer_list = [
"DateCreated",
"EpisodeCount",
"SeasonCount",
"Path",
"Genres",
"Studios",
"Etag",
"Taglines",
"SortName",
"RecursiveItemCount",
"ChildCount",
"ProductionLocations",
"CriticRating",
"OfficialRating",
"CommunityRating",
"PremiereDate",
"ProductionYear",
"AirTime",
"Status",
"Tags"
]
if include_media:
filer_list.append("MediaStreams")
if include_people:
filer_list.append("People")
if include_overview:
filer_list.append("Overview")
return ','.join(filer_list)
def translate_path(path):
'''
Use new library location for translate path starting in Kodi 19
'''
version = kodi_version()
if version > 18:
return xbmcvfs.translatePath(path)
else:
return xbmc.translatePath(path)
def download_external_sub(language, codec, url):
# Download the subtitle file
r = requests.get(url)
r.raise_for_status()
# Write the subtitle file to the local filesystem
file_name = 'Stream.{}.{}'.format(language, codec)
file_path = py2_decode(translate_path('special://temp/{}'.format(file_name)))
with open(file_path, 'wb') as f:
f.write(r.content)
return file_path

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,21 +2,24 @@
#################################################################################################
from __future__ import division, absolute_import, print_function, unicode_literals
import json
import threading
import websocket
import time
import xbmc
import xbmcaddon
import xbmcgui
from .jellyfin import API
from .functions import play_action
from .simple_logging import SimpleLogging
from . import clientinfo
from . import downloadutils
from .lazylogger import LazyLogger
from .jsonrpc import JsonRpc
from .kodi_utils import HomeWindow
from .utils import get_device_id, load_user_details
log = SimpleLogging(__name__)
log = LazyLogger(__name__)
class WebSocketClient(threading.Thread):
@@ -31,9 +34,9 @@ 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()
self.device_id = get_device_id()
self._library_monitor = library_change_monitor
@@ -65,10 +68,10 @@ class WebSocketClient(threading.Thread):
self._general_commands(data)
else:
log.debug("WebSocket Message Type: {0}", message)
log.debug("WebSocket Message Type: {0}".format(message))
def _library_changed(self, data):
log.debug("Library_Changed: {0}", data)
log.debug("Library_Changed: {0}".format(data))
self._library_monitor.check_for_updates()
def _play(self, data):
@@ -81,7 +84,7 @@ class WebSocketClient(threading.Thread):
home_screen.set_property("skip_select_user", "true")
startat = data.get('StartPositionTicks', -1)
log.debug("WebSocket Message PlayNow: {0}", data)
log.debug("WebSocket Message PlayNow: {0}".format(data))
media_source_id = data.get("MediaSourceId", "")
subtitle_stream_index = data.get("SubtitleStreamIndex", None)
@@ -103,7 +106,6 @@ class WebSocketClient(threading.Thread):
params["audio_stream_index"] = audio_stream_index
play_action(params)
def _playstate(self, data):
command = data['Command']
@@ -124,14 +126,14 @@ class WebSocketClient(threading.Thread):
seek_to = data['SeekPositionTicks']
seek_time = seek_to / 10000000.0
player.seekTime(seek_time)
log.debug("Seek to {0}", seek_time)
log.debug("Seek to {0}".format(seek_time))
elif command in actions:
actions[command]()
log.debug("Command: {0} completed", command)
log.debug("Command: {0} completed".format(command))
else:
log.debug("Unknown command: {0}", command)
log.debug("Unknown command: {0}".format(command))
return
def _general_commands(self, data):
@@ -175,7 +177,7 @@ class WebSocketClient(threading.Thread):
# header = arguments['Header']
text = arguments['Text']
# show notification here
log.debug("WebSocket DisplayMessage: {0}", text)
log.debug("WebSocket DisplayMessage: {0}".format(text))
xbmcgui.Dialog().notification("JellyCon", text)
elif command == 'SendString':
@@ -226,46 +228,45 @@ 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):
log.debug("Error: {0}", error)
log.debug("Error: {0}".format(error))
def run(self):
# websocket.enableTrace(True)
download_utils = downloadutils.DownloadUtils()
token = None
while token is None or token == "":
token = download_utils.authenticate()
user_details = load_user_details()
token = user_details.get('token')
if self.monitor.waitForAbort(10):
return
# Get the appropriate prefix for the websocket
server = download_utils.get_server()
if "https" in server:
server = server.replace('https', "wss")
settings = xbmcaddon.Addon()
server = settings.getSetting('server_address')
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)
log.debug("websocket url: {0}", websocket_url)
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=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))
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)
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 +276,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")
@@ -288,5 +291,13 @@ class WebSocketClient(threading.Thread):
def post_capabilities(self):
download_utils = downloadutils.DownloadUtils()
download_utils.post_capabilities()
settings = xbmcaddon.Addon()
user_details = load_user_details()
api = API(
settings.getSetting('server_address'),
user_details.get('user_id'),
user_details.get('token')
)
api.post_capabilities()

View File

@@ -1,24 +1,20 @@
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
from .downloadutils import DownloadUtils
from .utils import get_jellyfin_url
from .datamanager import DataManager
from .simple_logging import SimpleLogging
from .jellyfin import api
from .utils import get_jellyfin_url, image_url, get_current_user_id, get_art_url, get_default_filters, kodi_version
from .lazylogger import LazyLogger
from .kodi_utils import HomeWindow
from .dir_functions import process_directory
from .tracking import timer
log = SimpleLogging(__name__)
downloadUtils = DownloadUtils()
dataManager = DataManager()
kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2])
log = LazyLogger(__name__)
background_items = []
background_current_item = 0
@@ -29,21 +25,22 @@ def set_random_movies():
log.debug("set_random_movies Called")
settings = xbmcaddon.Addon()
item_limit = settings.getSetting("show_x_filtered_items")
hide_watched = settings.getSetting("hide_watched") == "true"
user_id = get_current_user_id()
url_params = {}
url_params["Recursive"] = True
url_params["limit"] = 20
url_params["limit"] = item_limit
if hide_watched:
url_params["IsPlayed"] = False
url_params["SortBy"] = "Random"
url_params["IncludeItemTypes"] = "Movie"
url_params["ImageTypeLimit"] = 0
url = get_jellyfin_url("{server}/Users/{userid}/Items", url_params)
url = get_jellyfin_url("/Users/{}/Items".format(user_id), url_params)
results = downloadUtils.download_url(url, suppress=True)
results = json.loads(results)
results = api.get(url)
randon_movies_list = []
if results is not None:
@@ -55,49 +52,52 @@ 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}", movies_list_string)
log.debug("set_random_movies : {0}", new_widget_hash)
log.debug("set_random_movies : {0}".format(movies_list_string))
log.debug("set_random_movies : {0}".format(new_widget_hash))
home_window.set_property("random-movies", movies_list_string)
home_window.set_property("random-movies-changed", new_widget_hash)
def set_background_image(force=False):
log.debug("set_background_image Called forced={0}", force)
log.debug("set_background_image Called forced={0}".format(force))
global background_current_item
global background_items
settings = xbmcaddon.Addon()
server = settings.getSetting('server_address')
user_id = get_current_user_id()
if force:
background_current_item = 0
del background_items
background_items = []
if len(background_items) == 0:
log.debug("set_background_image: Need to load more backgrounds {0} - {1}",
len(background_items), background_current_item)
log.debug("set_background_image: Need to load more backgrounds {0} - {1}".format(
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
url = get_jellyfin_url('{server}/Users/{userid}/Items', url_params)
url = get_jellyfin_url('/Users/{}/Items'.format(user_id), url_params)
server = downloadUtils.get_server()
results = downloadUtils.download_url(url, suppress=True)
results = json.loads(results)
results = api.get(url)
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 = get_art_url(
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", 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}", 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,14 +129,16 @@ 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")
current_time_stamp = str(time.time())
home_window.set_property("jellycon_widget_reload", current_time_stamp)
log.debug("Setting New Widget Hash: {0}", current_time_stamp)
log.debug("Setting New Widget Hash: {0}".format(current_time_stamp))
return
user_id = get_current_user_id()
url_params = {}
url_params["Recursive"] = True
@@ -144,13 +148,11 @@ 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_url = get_jellyfin_url('/Users/{}/Items'.format(user_id), url_params)
added_result = downloadUtils.download_url(added_url, suppress=True)
result = json.loads(added_result)
log.debug("LATEST_ADDED_ITEM: {0}", result)
result = api.get(added_url)
log.debug("LATEST_ADDED_ITEM: {0}".format(result))
last_added_date = ""
if result is not None:
@@ -158,7 +160,7 @@ def check_for_new_content():
if len(items) > 0:
item = items[0]
last_added_date = item.get("Etag", "")
log.debug("last_added_date: {0}", last_added_date)
log.debug("last_added_date: {0}".format(last_added_date))
url_params = {}
url_params["Recursive"] = True
@@ -168,48 +170,47 @@ 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_url = get_jellyfin_url('/Users/{}/Items'.format(user_id), url_params)
played_result = downloadUtils.download_url(played_url, suppress=True)
result = json.loads(played_result)
log.debug("LATEST_PLAYED_ITEM: {0}", result)
result = api.get(played_url)
log.debug("LATEST_PLAYED_ITEM: {0}".format(result))
last_played_date = ""
if result is not None:
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", "")
log.debug("last_played_date: {0}", last_played_date)
log.debug("last_played_date: {0}".format(last_played_date))
current_widget_hash = home_window.get_property("jellycon_widget_reload")
log.debug("Current Widget Hash: {0}", current_widget_hash)
log.debug("Current Widget Hash: {0}".format(current_widget_hash))
m = hashlib.md5()
m.update(last_played_date + last_added_date)
m.update((last_played_date + last_added_date).encode())
new_widget_hash = m.hexdigest()
log.debug("New Widget Hash: {0}", new_widget_hash)
log.debug("New Widget Hash: {0}".format(new_widget_hash))
if current_widget_hash != new_widget_hash:
home_window.set_property("jellycon_widget_reload", new_widget_hash)
log.debug("Setting New Widget Hash: {0}", new_widget_hash)
log.debug("Setting New Widget Hash: {0}".format(new_widget_hash))
@timer
def get_widget_content_cast(handle, params):
log.debug("getWigetContentCast Called: {0}", params)
server = downloadUtils.get_server()
log.debug("getWigetContentCast Called: {0}".format(params))
settings = xbmcaddon.Addon()
server = settings.getSetting('server_address')
user_id = get_current_user_id()
item_id = params["id"]
data_manager = DataManager()
result = data_manager.get_content("{server}/Users/{userid}/Items/" + item_id + "?format=json")
log.debug("ItemInfo: {0}", result)
result = api.get(
"/Users/{}/Items/{}".format(user_id, item_id))
log.debug("ItemInfo: {0}".format(result))
if not result:
return
@@ -227,12 +228,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,12 +235,10 @@ 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 = 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)
else:
list_item = xbmcgui.ListItem(label=person_name)
list_item = xbmcgui.ListItem(label=person_name, offscreen=True)
list_item.setProperty("id", person_id)
@@ -272,24 +265,26 @@ def get_widget_content_cast(handle, params):
@timer
def get_widget_content(handle, params):
log.debug("getWigetContent Called: {0}", params)
log.debug("getWigetContent Called: {0}".format(params))
settings = xbmcaddon.Addon()
item_limit = int(settings.getSetting("show_x_filtered_items"))
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:
log.error("getWigetContent type not set")
return
user_id = get_current_user_id()
log.debug("widget_type: {0}", widget_type)
log.debug("widget_type: {0}".format(widget_type))
url_verb = "{server}/Users/{userid}/Items"
url_verb = "/Users/{}/Items".format(user_id)
url_params = {}
url_params["Limit"] = "{ItemLimit}"
url_params["format"] = "json"
url_params["Fields"] = "{field_filters}"
url_params["Limit"] = item_limit
url_params["Fields"] = get_default_filters()
url_params["ImageTypeLimit"] = 1
url_params["IsMissing"] = False
@@ -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"] = item_limit
elif widget_type == "inprogress_movies":
xbmcplugin.setContent(handle, 'movies')
@@ -312,26 +308,28 @@ def get_widget_content(handle, params):
url_params["Filters"] = "IsResumable"
url_params["IsVirtualUnaired"] = False
url_params["IncludeItemTypes"] = "Movie"
url_params["Limit"] = item_limit
elif widget_type == "random_movies":
home_window = HomeWindow()
xbmcplugin.setContent(handle, 'movies')
url_params["Ids"] = "{random_movies}"
url_params["Ids"] = home_window.get_property("random-movies")
elif widget_type == "recent_tvshows":
xbmcplugin.setContent(handle, 'episodes')
url_verb = '{server}/Users/{userid}/Items/Latest'
url_verb = '/Users/{}/Items/Latest'.format(user_id)
url_params["GroupItems"] = True
url_params["Limit"] = 45
url_params["Recursive"] = True
url_params["SortBy"] = "DateCreated"
url_params["SortOrder"] = "Descending"
url_params["Fields"] = "{field_filters}"
url_params["Fields"] = get_default_filters()
if hide_watched:
url_params["IsPlayed"] = False
url_params["IsVirtualUnaired"] = False
url_params["IncludeItemTypes"] = "Episode"
url_params["ImageTypeLimit"] = 1
url_params["format"] = "json"
url_params["Limit"] = item_limit
elif widget_type == "recent_episodes":
xbmcplugin.setContent(handle, 'episodes')
@@ -343,6 +341,7 @@ def get_widget_content(handle, params):
url_params["IsPlayed"] = False
url_params["IsVirtualUnaired"] = False
url_params["IncludeItemTypes"] = "Episode"
url_params["Limit"] = item_limit
elif widget_type == "inprogress_episodes":
xbmcplugin.setContent(handle, 'episodes')
@@ -352,72 +351,81 @@ def get_widget_content(handle, params):
url_params["Filters"] = "IsResumable"
url_params["IsVirtualUnaired"] = False
url_params["IncludeItemTypes"] = "Episode"
url_params["Limit"] = item_limit
elif widget_type == "nextup_episodes":
xbmcplugin.setContent(handle, 'episodes')
url_verb = "{server}/Shows/NextUp"
url_params["Limit"] = "{ItemLimit}"
url_params["userid"] = "{userid}"
url_verb = "/Shows/NextUp"
url_params = url_params.copy()
url_params["Limit"] = item_limit
url_params["userid"] = user_id
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 = "/Users/{}/Items".format(user_id)
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"] = item_limit
elif widget_type == "movie_recommendations":
suggested_items_url_params = {}
suggested_items_url_params["userId"] = "{userid}"
suggested_items_url_params["userId"] = user_id
suggested_items_url_params["categoryLimit"] = 15
suggested_items_url_params["ItemLimit"] = 20
suggested_items_url_params["ItemLimit"] = item_limit
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(
"/Movies/Recommendations", suggested_items_url_params)
data_manager = DataManager()
suggested_items = data_manager.get_content(suggested_items_url)
suggested_items = api.get(suggested_items_url)
ids = []
set_id = 0
while len(ids) < 20 and suggested_items:
while len(ids) < item_limit and suggested_items:
items = suggested_items[set_id]
log.debug("BaselineItemName : {0} - {1}", 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):
set_id = 0
id_list = ",".join(ids)
log.debug("Recommended Items : {0}", len(ids), id_list)
log.debug("Recommended Items : {0}".format(len(ids)))
url_params["Ids"] = id_list
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)
if url_params.get('IncludeItemTypes', '') == 'Episode' or params.get('type', '') == 'nextup_episodes':
params["name_format"] = "Episode|episode_name_format"
# remove resumable items from next up
list_items, detected_type, total_records = process_directory(
items_url, None, params, use_cached_widget_data)
# Combine In Progress and Next Up Episodes, append 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)
params["name_format"] = "Episode|episode_name_format"
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
log.debug("Detected content type: {0}", detected_type)
log.debug("Detected content type: {0}".format(detected_type))
content_type = None
if detected_type == "Movie":

View File

@@ -4,16 +4,16 @@
<setting label="30388" type="lsep"/>
<setting label="30011" type="action" action="RunScript(plugin.video.jellycon,0,?mode=DETECT_SERVER_USER)" option="close"/>
<setting id="protocol" type="select" label="30390" lvalues="30391|30392" default="0" />
<setting id="ipaddress" type="text" label="30000" default="&lt;none&gt;" visible="true" enable="true" />
<setting id="port" type="text" label="30001" default="8096" visible="true" enable="true" />
<setting id="verify_cert" type="bool" label="30003" default="false" visible="true" enable="true" />
<setting id="ipaddress" type="text" label="30000" default="" visible="false" enable="false" />
<setting id="protocol" type="select" label="30390" lvalues="30391|30392" default="0" visible="false"/>
<setting id="port" type="text" label="30001" default="8096" visible="false" enable="false" />
<setting id="server_address" type="text" label="30000" default="" visible="true" enable="true" />
<setting id="verify_cert" type="bool" label="30003" default="true" visible="true" enable="true" />
<setting label="30389" type="lsep"/>
<setting type="sep" />
<setting label="30012" type="action" action="RunScript(plugin.video.jellycon,0,?mode=CHANGE_USER)" option="close"/>
<setting id="username" type="text" label="30024" />
<setting id="password" type="text" option="hidden" label="30025" />
<setting id="save_user_to_settings" type="bool" label="30378" default="true" visible="true" enable="true" />
<setting id="allow_password_saving" type="bool" label="30367" default="true" visible="true" enable="true" />
@@ -45,13 +45,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">
@@ -73,8 +66,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"/>
@@ -91,7 +84,7 @@
<setting id="add_user_ratings" type="bool" label="30348" default="true" visible="true" enable="true" />
<setting id="include_people" type="bool" label="30183" default="false" visible="true" enable="true" />
<setting id="hide_unwatched_details" type="bool" label="30023" default="false" visible="true" enable="true" />
<setting id="episode_name_format" type="select" label="30019" default="{SeriesName} - {ItemName}" values="{SeriesName} - {ItemName}|{ItemName}|s{SeasonIndex}e{EpisodeIndex} - {ItemName}|{SeriesName} - s{SeasonIndex}e{EpisodeIndex} - {ItemName}" />
<setting id="episode_name_format" type="select" label="30019" default="{SeriesName} - {ItemName}" values="{SeriesName} - {ItemName}|{ItemName}|S{SeasonIndex}E{EpisodeIndex} - {ItemName}|{SeriesName} - S{SeasonIndex}E{EpisodeIndex} - {ItemName}" />
<setting label="30222" type="lsep"/>
<setting type="sep" />
@@ -133,7 +126,7 @@
<setting id="use_cached_widget_data" type="bool" label="30441" default="false" visible="true" enable="true" />
<setting id="showLoadProgress" type="bool" label="30120" default="false" visible="true" enable="true" />
<setting id="suppressErrors" type="bool" label="30315" default="false" visible="true" enable="true" />
<setting id="speed_test_data_size" type="slider" label="30436" default="15" range="5,1,100" option="int" visible="true"/>
<setting id="speed_test_data_size" type="slider" label="30436" default="10" range="1,1,10" option="int" visible="true"/>
</category>
<category label="30421">
@@ -154,4 +147,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>

View File

@@ -13,39 +13,53 @@
<left>0</left>
<top>0</top>
<width>380</width>
<height>100</height>
<height>150</height>
<texture border="40">bg.png</texture>
</control>
<control type="label" id="3020">
<width>120</width>
<left>20</left>
<top>5</top>
<height>45</height>
<label>Bitrate : </label>
<textcolor>99FFFFFF</textcolor>
<font>font14</font>
<align>left</align>
</control>
<control type="label" id="3020">
<width>120</width>
<left>20</left>
<top>5</top>
<height>45</height>
<label>Bitrate : </label>
<textcolor>99FFFFFF</textcolor>
<font>font14</font>
<align>left</align>
</control>
<control type="label" id="3030">
<width>150</width>
<left>120</left>
<top>5</top>
<height>45</height>
<label>100 Mbs</label>
<textcolor>99FFFFFF</textcolor>
<font>font14</font>
<align>left</align>
</control>
<control type="label" id="3030">
<width>150</width>
<left>120</left>
<top>5</top>
<height>45</height>
<label>100 Mbs</label>
<textcolor>99FFFFFF</textcolor>
<font>font14</font>
<align>left</align>
</control>
<control type="slider" id="3000">
<left>20</left>
<top>55</top>
<width>340</width>
<height>30</height>
<visible>true</visible>
</control>
<control type="slider" id="3000">
<left>20</left>
<top>55</top>
<width>340</width>
<height>30</height>
<ondown>3011</ondown>
<visible>true</visible>
</control>
<control type="button" id="3011">
<texturenofocus border="1" colordiffuse="ff161616">white.png</texturenofocus>
<texturefocus border="1" colordiffuse="ff525252">white.png</texturefocus>
<left>20</left>
<top>100</top>
<width>340</width>
<height>40</height>
<label></label>
<onup>3000</onup>
<font>font14</font>
<align>center</align>
</control>
</controls>
</window>
</window>

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<window id="3301" type="dialog">
<defaultcontrol always="true">3010</defaultcontrol>
<zorder>2</zorder>
<coordinates>
<system>1</system>
<left>450</left>
<top>200</top>
</coordinates>
<controls>
<control type="image">
<left>0</left>
<top>0</top>
<width>400</width>
<height>270</height>
<texture border="40">bg.png</texture>
</control>
<control type="label" id="4">
<left>20</left>
<top>5</top>
<width>360</width>
<height>50</height>
<label>Heading</label>
<font>font45_title</font>
<align>center</align>
</control>
<control type="textbox" id="3">
<left>20</left>
<top>65</top>
<width>360</width>
<height>50</height>
<font>font45</font>
<align>center</align>
</control>
<control type="button" id="3010">
<texturenofocus border="1" colordiffuse="ff161616">white.png</texturenofocus>
<texturefocus border="1" colordiffuse="ff525252">white.png</texturefocus>
<left>20</left>
<top>135</top>
<width>360</width>
<height>40</height>
<label></label>
<onup></onup>
<ondown>3011</ondown>
<font>font14</font>
<align>center</align>
</control>
<control type="button" id="3011">
<texturenofocus border="1" colordiffuse="ff161616">white.png</texturenofocus>
<texturefocus border="1" colordiffuse="ff525252">white.png</texturefocus>
<left>20</left>
<top>195</top>
<width>360</width>
<height>40</height>
<label></label>
<onup>3010</onup>
<ondown>3012</ondown>
<font>font14</font>
<align>center</align>
</control>
</controls>
</window>

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

@@ -1,15 +0,0 @@
$string_ids = @()
Select-String -path resources\language\resource.language.en_gb\strings.po -pattern "msgctxt " | select Line | ForEach {
$id = [regex]::match($_.Line.ToString(), '\"#([0-9]+)\"').Groups[1].Value
if($string_ids -contains $id)
{
Write-Host "ERROR: String ID Already Exists : " $id
}
else
{
$string_ids += $id
Get-ChildItem *.py,settings.xml,resources\language\resource.language.en_gb\strings.po -recurse | Select-String -pattern $id | group Pattern | where {$_.Count -eq 1} | select Name, Count
}
}

View File

@@ -1,15 +0,0 @@
del /F /Q /S %HOMEPATH%\AppData\Roaming\Kodi\addons\plugin.video.jellycon
rmdir /Q /S %HOMEPATH%\AppData\Roaming\Kodi\addons\plugin.video.jellycon
xcopy /Y addon.xml %HOMEPATH%\AppData\Roaming\Kodi\addons\plugin.video.jellycon\
xcopy /Y default.py %HOMEPATH%\AppData\Roaming\Kodi\addons\plugin.video.jellycon\
xcopy /Y fanart.jpg %HOMEPATH%\AppData\Roaming\Kodi\addons\plugin.video.jellycon\
xcopy /Y icon.png %HOMEPATH%\AppData\Roaming\Kodi\addons\plugin.video.jellycon\
xcopy /Y kodi.png %HOMEPATH%\AppData\Roaming\Kodi\addons\plugin.video.jellycon\
xcopy /Y service.py %HOMEPATH%\AppData\Roaming\Kodi\addons\plugin.video.jellycon\
xcopy /E /Y resources %HOMEPATH%\AppData\Roaming\Kodi\addons\plugin.video.jellycon\resources\
cd "%programfiles%\Kodi"
kodi.exe

View File

@@ -1,45 +0,0 @@
import xml.etree.ElementTree as ET
import subprocess
from shutil import copy2, copytree, rmtree
import os
import sys
package_path = "package"
def ignore_files(path, item_list):
return [".idea", ".git", ".gitignore", "scripts", package_path]
zip_path = "c:\\Program Files\\7-Zip\\7z.exe"
addon_path = sys.argv[1]
tree = ET.parse(addon_path + "\\addon.xml")
root = tree.getroot()
id = root.attrib["id"]
version = root.attrib["version"]
print (package_path + " - " + version)
try:
rmtree(package_path + "\\" + id)
except FileNotFoundError as err:
pass
copytree(addon_path, package_path + "\\" + id, ignore=ignore_files)
zip_name = id + "-" + version + ".zip"
os.chdir(package_path)
cmd_7zip = [zip_path, "a", zip_name, id]
sp = subprocess.Popen(cmd_7zip, stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
sp.wait()
os.chdir("..")
copy2(package_path + "\\" + id + "\\addon.xml", package_path + "\\addon.xml")
try:
rmtree(package_path + "\\" + id)
except FileNotFoundError as err:
pass

View File

@@ -8,16 +8,14 @@ import xbmc
import xbmcaddon
import xbmcgui
from resources.lib.downloadutils import DownloadUtils, save_user_details
from resources.lib.simple_logging import SimpleLogging
from resources.lib.lazylogger import LazyLogger
from resources.lib.play_utils import Service, PlaybackService, send_progress
from resources.lib.kodi_utils import HomeWindow
from resources.lib.widgets import set_background_image, set_random_movies
from resources.lib.websocket_client import WebSocketClient
from resources.lib.menu_functions import set_library_window_values
from resources.lib.context_monitor import ContextMonitor
from resources.lib.server_detect import check_server, check_safe_delete_available, check_connection_speed
from resources.lib.library_change_monitor import LibraryChangeMonitor
from resources.lib.server_detect import check_server, check_connection_speed
from resources.lib.monitors import LibraryChangeMonitor, ContextMonitor
from resources.lib.datamanager import clear_old_cache_data
from resources.lib.tracking import set_timing_enabled
from resources.lib.image_server import HttpImageServerThread
@@ -35,13 +33,13 @@ home_window.clear_property("userid")
home_window.clear_property("AccessToken")
home_window.clear_property("Params")
log = SimpleLogging('service')
log = LazyLogger('service')
monitor = xbmc.Monitor()
try:
clear_old_cache_data()
except Exception as error:
log.error("Error in clear_old_cache_data() : {0}", error)
log.error("Error in clear_old_cache_data() : {0}".format(error))
# wait for 10 seconds for the Kodi splash screen to close
i = 0
@@ -53,16 +51,6 @@ while not monitor.abortRequested():
check_server()
download_utils = DownloadUtils()
# auth the service
try:
download_utils.authenticate()
download_utils.get_user_id()
except Exception as error:
log.error("Error with initial service auth: {0}", error)
image_server = HttpImageServerThread()
image_server.start()
@@ -75,7 +63,6 @@ last_progress_update = time.time()
last_content_check = time.time()
last_background_update = 0
last_random_movie_update = 0
safe_delete_check = False
# start the library update monitor
library_change_monitor = LibraryChangeMonitor()
@@ -112,13 +99,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 +111,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")
@@ -141,7 +126,8 @@ while not xbmc.abortRequested:
if user_changed or first_run:
settings = xbmcaddon.Addon()
server_speed_check_data = settings.getSetting("server_speed_check_data")
server_host = download_utils.get_server()
server_speed_check_data = settings.getSetting("server_speed_check_data")
server_host = settings.getSetting('server_address')
if server_host is not None and server_host != "" and server_host != "<none>" and server_host not in server_speed_check_data:
message = "This is the first time you have connected to this server.\nDo you want to run a connection speed test?"
response = xbmcgui.Dialog().yesno("First Connection", message)
@@ -170,10 +156,6 @@ while not xbmc.abortRequested:
websocket_client = WebSocketClient(library_change_monitor)
websocket_client.start()
if user_changed or not safe_delete_check:
check_safe_delete_available()
safe_delete_check = True
elif screen_saver_active:
last_random_movie_update = time.time() - (random_movie_list_interval - 15)
if background_interval != 0 and ((time.time() - last_background_update) > background_interval):
@@ -181,14 +163,17 @@ while not xbmc.abortRequested:
set_background_image(False)
except Exception as error:
log.error("Exception in Playback Monitor: {0}", error)
log.error("{0}", traceback.format_exc())
log.error("Exception in Playback Monitor: {0}".format(error))
log.error("{0}".format(traceback.format_exc()))
first_run = False
xbmc.sleep(1000)
image_server.stop()
# stop the WebSocket Client
websocket_client.stop_client()
# call stop on the library update monitor
library_change_monitor.stop()
@@ -200,9 +185,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")

8
tox.ini Normal file
View File

@@ -0,0 +1,8 @@
[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