Compare commits
453 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
caa014a0ac | ||
|
|
8199642f35 | ||
|
|
da6525120d | ||
|
|
362760d3ab | ||
|
|
1cce67702d | ||
|
|
ff761dd088 | ||
|
|
beda70b1f9 | ||
|
|
eb9eb2009f | ||
|
|
73bcfef007 | ||
|
|
3104d5aec9 | ||
|
|
0d3c39cc0a | ||
|
|
e57059cf3c | ||
|
|
18e9a99d17 | ||
|
|
7b7549fef0 | ||
|
|
251f43fb77 | ||
|
|
6c4077b143 | ||
|
|
00a7e92ef7 | ||
|
|
61c1a3db8e | ||
|
|
bdcd65d6c9 | ||
|
|
f7a427b809 | ||
|
|
e96e727c7c | ||
|
|
f2ea26b529 | ||
|
|
7b10c48e8f | ||
|
|
0c71e82675 | ||
|
|
7fa7c8f9f8 | ||
|
|
102eb934af | ||
|
|
099e0193e9 | ||
|
|
c68b4bf392 | ||
|
|
14b2a10fdf | ||
|
|
a9ba96e08b | ||
|
|
d4c3ebab24 | ||
|
|
4f5396f945 | ||
|
|
59a5f72f7c | ||
|
|
47fbf495f1 | ||
|
|
55529f49e3 | ||
|
|
dd0f410161 | ||
|
|
6ee2f9c962 | ||
|
|
f24e20a902 | ||
|
|
1cf0155622 | ||
|
|
aa208d2ae5 | ||
|
|
1d8b9824ad | ||
|
|
bd1917a8a7 | ||
|
|
50a43e8e01 | ||
|
|
bee8be9698 | ||
|
|
774c9ba12d | ||
|
|
8be5c1b9eb | ||
|
|
9ada09e809 | ||
|
|
007a997f8f | ||
|
|
d70f9290f5 | ||
|
|
d32be4aba7 | ||
|
|
359dd05972 | ||
|
|
c005d331ee | ||
|
|
e51f136c4e | ||
|
|
95ee6e4e24 | ||
|
|
677d3a4a76 | ||
|
|
bd9f63de4c | ||
|
|
0a51bd366e | ||
|
|
2c959b0e00 | ||
|
|
291c3533e2 | ||
|
|
6080a3e6ec | ||
|
|
97785abff4 | ||
|
|
511f44e806 | ||
|
|
f5a97b4730 | ||
|
|
a47ba79413 | ||
|
|
429eaf76a5 | ||
|
|
da9136a08b | ||
|
|
cb0097a4e0 | ||
|
|
f66d2c55ca | ||
|
|
a30e58677d | ||
|
|
11d1898dc2 | ||
|
|
24548a081c | ||
|
|
09982b9f60 | ||
|
|
b56036b71e | ||
|
|
52fe89170f | ||
|
|
474043a569 | ||
|
|
7451605dbb | ||
|
|
860c487074 | ||
|
|
b1303413c6 | ||
|
|
6ad76560ed | ||
|
|
d6f449930c | ||
|
|
ebdf501115 | ||
|
|
34861fb9b9 | ||
|
|
2f3fe8ae6b | ||
|
|
4ee91654ea | ||
|
|
3190658e0c | ||
|
|
c330523b9e | ||
|
|
5a5c865135 | ||
|
|
f5ae301c97 | ||
|
|
d910441470 | ||
|
|
b667bf4117 | ||
|
|
4d8b4c9d43 | ||
|
|
02c44eef82 | ||
|
|
c21d10d7f9 | ||
|
|
d2d14e4c19 | ||
|
|
1da8bca9d1 | ||
|
|
dffbfd8860 | ||
|
|
7247c51b10 | ||
|
|
2517e30f55 | ||
|
|
caa0e47399 | ||
|
|
8ce6ec49e1 | ||
|
|
276b18650c | ||
|
|
a69a8a269f | ||
|
|
921628b2a1 | ||
|
|
53225b427c | ||
|
|
6aad1eb92d | ||
|
|
d4bccb9b05 | ||
|
|
02e9886e2e | ||
|
|
a05363d202 | ||
|
|
78fd1e206b | ||
|
|
74951b9dec | ||
|
|
5ac21ea240 | ||
|
|
086e92da4b | ||
|
|
e54c4c1bec | ||
|
|
5bb20e482f | ||
|
|
569462f755 | ||
|
|
658050548c | ||
|
|
4d635f6eb4 | ||
|
|
a0b1e9177b | ||
|
|
7a1a7843e6 | ||
|
|
66d4e02024 | ||
|
|
04a46f0cd9 | ||
|
|
4e0ac5330f | ||
|
|
32288aba97 | ||
|
|
801f119a5c | ||
|
|
567a02872b | ||
|
|
cfc19cafc5 | ||
|
|
44823f5043 | ||
|
|
bad5f8e561 | ||
|
|
075e1e8974 | ||
|
|
06f78ce620 | ||
|
|
cce9acb182 | ||
|
|
048b8f0385 | ||
|
|
fa318ac751 | ||
|
|
071ad16f83 | ||
|
|
eb44fc0ef7 | ||
|
|
f35239e1a4 | ||
|
|
5534878dcd | ||
|
|
f1b82cf89a | ||
|
|
4ae81b9f52 | ||
|
|
7af412e6c5 | ||
|
|
52baa2e8cc | ||
|
|
d48c159283 | ||
|
|
fae51bd9b2 | ||
|
|
9a6e16f505 | ||
|
|
44afd62989 | ||
|
|
69986cc40d | ||
|
|
9769a36c30 | ||
|
|
1621510b15 | ||
|
|
85f910dd48 | ||
|
|
e260124351 | ||
|
|
1eb2d21086 | ||
|
|
fedc67e1e2 | ||
|
|
c88a7b134f | ||
|
|
20dfc02624 | ||
|
|
2ca11e53be | ||
|
|
1b79794ede | ||
|
|
806de6910a | ||
|
|
ba30fc1613 | ||
|
|
cd5774bfd3 | ||
|
|
df9cd5fe48 | ||
|
|
76d189b8de | ||
|
|
0a0fd8a9a3 | ||
|
|
f78764d18a | ||
|
|
59b352df35 | ||
|
|
c241cc9df9 | ||
|
|
5ba6291fd9 | ||
|
|
c8715218c8 | ||
|
|
cc9caf3d54 | ||
|
|
6d298daea4 | ||
|
|
07ef6b6f41 | ||
|
|
6c8e193130 | ||
|
|
b7c31fa7a0 | ||
|
|
d25c7b351e | ||
|
|
90103b9fba | ||
|
|
7c39b06297 | ||
|
|
2ead20b2f7 | ||
|
|
c8821b1055 | ||
|
|
50b6f773f5 | ||
|
|
11705d46f2 | ||
|
|
3e96211433 | ||
|
|
0c3cefc0ff | ||
|
|
deab5fc2c2 | ||
|
|
9c68120447 | ||
|
|
6f3181a643 | ||
|
|
416b04fdec | ||
|
|
c2958445cb | ||
|
|
1176db8f41 | ||
|
|
136e22bb84 | ||
|
|
78b7f38f61 | ||
|
|
05ad58b46e | ||
|
|
8040349718 | ||
|
|
fca212edc3 | ||
|
|
7237fd2c63 | ||
|
|
26c02fdbac | ||
|
|
aa4a3531e9 | ||
|
|
4495129a66 | ||
|
|
791ed3fe32 | ||
|
|
b29688dc8d | ||
|
|
2936a12e25 | ||
|
|
213fa15ef3 | ||
|
|
80f25d016c | ||
|
|
a6ad9b5187 | ||
|
|
2f5765cc3f | ||
|
|
d08b5b7041 | ||
|
|
2772cf6389 | ||
|
|
6317aec577 | ||
|
|
43f576d1c5 | ||
|
|
d8493bc6bf | ||
|
|
96010b48c1 | ||
|
|
a803e007d0 | ||
|
|
0699bdd141 | ||
|
|
c5b1bb766b | ||
|
|
e0eff168bf | ||
|
|
1c88173b3e | ||
|
|
6c2e005cd7 | ||
|
|
08780edcc1 | ||
|
|
d86d785061 | ||
|
|
67cf11966b | ||
|
|
9c96cc4044 | ||
|
|
d1e4c1d09f | ||
|
|
944f3a363f | ||
|
|
79bd4b1925 | ||
|
|
8ea3351eee | ||
|
|
8d90ca0892 | ||
|
|
4bb7a95e73 | ||
|
|
13e553d818 | ||
|
|
92b8977797 | ||
|
|
a347255752 | ||
|
|
e8b329e688 | ||
|
|
d77d326950 | ||
|
|
9832e6b011 | ||
|
|
5bae9e72c9 | ||
|
|
dba2ee1556 | ||
|
|
357f28321c | ||
|
|
3402ef8d11 | ||
|
|
643e1d2ac8 | ||
|
|
f16ef03927 | ||
|
|
bf54539c39 | ||
|
|
e71714dbb8 | ||
|
|
a482172be4 | ||
|
|
6fbbd63ad6 | ||
|
|
6126d617f5 | ||
|
|
f507efdef7 | ||
|
|
7d4f50add1 | ||
|
|
201521a4d8 | ||
|
|
8c9289ef1c | ||
|
|
8ba1b0b0c0 | ||
|
|
7871422354 | ||
|
|
eb19d80b97 | ||
|
|
6888cbf7b8 | ||
|
|
5c1842877d | ||
|
|
0bf1d75e0c | ||
|
|
a3e3c33855 | ||
|
|
d5d5e7f74c | ||
|
|
8adfcbe20d | ||
|
|
f8f79ecc75 | ||
|
|
289f42392b | ||
|
|
71cea26ada | ||
|
|
5043a8db4e | ||
|
|
5d3199e306 | ||
|
|
37a132164f | ||
|
|
d02cad0fca | ||
|
|
3530d158a6 | ||
|
|
7eef3a30a8 | ||
|
|
36b23d0b19 | ||
|
|
816277512d | ||
|
|
bec7e06628 | ||
|
|
798a00ae3a | ||
|
|
c852d1e434 | ||
|
|
08a829ca97 | ||
|
|
1c18753511 | ||
|
|
6b1c3ccc39 | ||
|
|
bb207f1aee | ||
|
|
91cc66a8d1 | ||
|
|
cfbfb44307 | ||
|
|
8d29ffd7a7 | ||
|
|
44c5193f54 | ||
|
|
342eec8c26 | ||
|
|
4973404684 | ||
|
|
e45a59c184 | ||
|
|
6efa62ced2 | ||
|
|
a316a6e094 | ||
|
|
297b25a739 | ||
|
|
3aa4ee3548 | ||
|
|
e6e3c27371 | ||
|
|
bdf7298afa | ||
|
|
a81e125a29 | ||
|
|
a454eb16de | ||
|
|
50512e863d | ||
|
|
7ff8e12c4e | ||
|
|
a2815081d3 | ||
|
|
e14fa4b7de | ||
|
|
4c95caf957 | ||
|
|
4a242475c0 | ||
|
|
fb1348a306 | ||
|
|
1b3cea3042 | ||
|
|
264145c4b9 | ||
|
|
daea47bf38 | ||
|
|
2ff5a2e9fc | ||
|
|
fc690abee9 | ||
|
|
157a477a10 | ||
|
|
aa7a412a94 | ||
|
|
2832c21ffd | ||
|
|
3751794f81 | ||
|
|
5c3740b8a9 | ||
|
|
303f049004 | ||
|
|
a52ede2a3d | ||
|
|
f249eb7cae | ||
|
|
2d20dc4282 | ||
|
|
95f8dc8eb3 | ||
|
|
1ae1e824c0 | ||
|
|
72413ef3b4 | ||
|
|
d45c45a868 | ||
|
|
3b646da0a8 | ||
|
|
6121537216 | ||
|
|
2482f11a5a | ||
|
|
ade08f74a4 | ||
|
|
5eade9abe5 | ||
|
|
203986d54c | ||
|
|
8e8c376df3 | ||
|
|
8a6886c71d | ||
|
|
7f02ca1bca | ||
|
|
7df265b357 | ||
|
|
5427168f01 | ||
|
|
8ce7d851cc | ||
|
|
22d3a23099 | ||
|
|
b6dd0285a8 | ||
|
|
9d45d42efe | ||
|
|
6e6e753475 | ||
|
|
3b11c931d4 | ||
|
|
742fbb224f | ||
|
|
b35adac318 | ||
|
|
c33274709e | ||
|
|
89748156a6 | ||
|
|
c01a792e25 | ||
|
|
e4d0937782 | ||
|
|
524110dee9 | ||
|
|
ae480283a3 | ||
|
|
ccaf5878ae | ||
|
|
cb67d4b194 | ||
|
|
701ca68db7 | ||
|
|
cf9c3290b5 | ||
|
|
964994dd90 | ||
|
|
edbd3d37da | ||
|
|
c2d36e2ac2 | ||
|
|
b2c0caaa43 | ||
|
|
8155e77210 | ||
|
|
fdda442dc8 | ||
|
|
b962d9597b | ||
|
|
9e624d0db2 | ||
|
|
4f52ba2d4d | ||
|
|
c9a22c517c | ||
|
|
023cd5f720 | ||
|
|
8b2b03bf0a | ||
|
|
2a14caceeb | ||
|
|
0fd3687843 | ||
|
|
d1c205a588 | ||
|
|
21103ecaac | ||
|
|
2fcbbfce27 | ||
|
|
5e8eda0ea4 | ||
|
|
1e9ea80685 | ||
|
|
427ad71880 | ||
|
|
fbe2ebe98f | ||
|
|
8b8d61eacf | ||
|
|
7d07980a15 | ||
|
|
84829ba83d | ||
|
|
bb3e52d27f | ||
|
|
63d2a4cffc | ||
|
|
5c23ac47b2 | ||
|
|
a52a4a47f3 | ||
|
|
5f5328a280 | ||
|
|
dd81b1babf | ||
|
|
8adf8a2a05 | ||
|
|
4c8914ad8d | ||
|
|
cfe36f16f2 | ||
|
|
46a6d84101 | ||
|
|
2a5dd1c418 | ||
|
|
61253d7c9d | ||
|
|
d4b7262105 | ||
|
|
a644d4ffda | ||
|
|
84ea523d16 | ||
|
|
3ea93cbf13 | ||
|
|
3f4dc08dc7 | ||
|
|
ee8ae6f492 | ||
|
|
984c2dab54 | ||
|
|
d215d087b3 | ||
|
|
42187327d6 | ||
|
|
58a256c121 | ||
|
|
e3ec31ae99 | ||
|
|
f81301f62d | ||
|
|
b7601fda7b | ||
|
|
e3b205046b | ||
|
|
2c70cedaa6 | ||
|
|
cea6c532e0 | ||
|
|
2e28b5904d | ||
|
|
4288c032db | ||
|
|
04a5378a87 | ||
|
|
ca5918ded9 | ||
|
|
2e7737c1af | ||
|
|
441bb10624 | ||
|
|
9adb23b280 | ||
|
|
7b547b2bc8 | ||
|
|
4ec75ad266 | ||
|
|
7dcf68d2be | ||
|
|
9199eb4290 | ||
|
|
8831af3fb4 | ||
|
|
20b1686b04 | ||
|
|
ae028f485a | ||
|
|
d5af0c8d7e | ||
|
|
e596998a72 | ||
|
|
f224c0b94a | ||
|
|
bc06467784 | ||
|
|
b2f369de10 | ||
|
|
0e070308db | ||
|
|
1b7c3ffae0 | ||
|
|
1069bf73e7 | ||
|
|
483b708def | ||
|
|
be12c0d21f | ||
|
|
bc57964aed | ||
|
|
a6f2abaab9 | ||
|
|
304ff1a42c | ||
|
|
a5048b317d | ||
|
|
f42b5c2a99 | ||
|
|
5827b42732 | ||
|
|
6e62571cce | ||
|
|
a68e42657f | ||
|
|
bad47421c0 | ||
|
|
757f0a411c | ||
|
|
cba411658f | ||
|
|
e560b1e591 | ||
|
|
e280b82582 | ||
|
|
a49900a2d7 | ||
|
|
8ece4ae651 | ||
|
|
1949e8a9b7 | ||
|
|
52207a5ed8 | ||
|
|
f90db72f8b | ||
|
|
d298b4caa2 | ||
|
|
8109f5ae41 | ||
|
|
e4ba7b0eba | ||
|
|
ed3087a222 | ||
|
|
c6f6601f3c | ||
|
|
fb6a1c1329 | ||
|
|
920c012338 | ||
|
|
b629756f3e | ||
|
|
0cf4643d5f | ||
|
|
73d757122a | ||
|
|
975c953d78 | ||
|
|
9de1af4204 | ||
|
|
7b7502fa2f | ||
|
|
b565005219 | ||
|
|
68008c675e | ||
|
|
2cf86eb6ae | ||
|
|
b0a1f9a680 |
@@ -1,14 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<addon id="plugin.video.jellycon"
|
||||
name="JellyCon"
|
||||
version="0.3.0"
|
||||
version=""
|
||||
provider-name="Jellyfin Contributors">
|
||||
<requires>
|
||||
<import addon="xbmc.python" version="2.25.0"/>
|
||||
<import addon="script.module.pil" version="1.1.7"/>
|
||||
<import addon="script.module.requests" version="2.22.0"/>
|
||||
<import addon="script.module.six" version="1.13.0"/>
|
||||
<import addon="script.module.kodi-six" />
|
||||
</requires>
|
||||
<extension point="xbmc.python.pluginsource" library="default.py">
|
||||
<provides>video audio</provides>
|
||||
16
.github/dependabot.yaml
vendored
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
72
.github/workflows/create-prepare-release-pr.yaml
vendored
Normal 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.21.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
@@ -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.21.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.5
|
||||
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
@@ -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.21.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
49
.github/workflows/test.yaml
vendored
Normal 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
@@ -220,3 +220,6 @@ pip-log.txt
|
||||
|
||||
#Mr Developer
|
||||
.mr.developer.cfg
|
||||
|
||||
# Addon files
|
||||
addon.xml
|
||||
|
||||
43
README.md
@@ -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
@@ -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)
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import xbmcaddon
|
||||
|
||||
from resources.lib.loghandler import LazyLogger
|
||||
from resources.lib.lazylogger import LazyLogger
|
||||
from resources.lib.functions import main_entry_point
|
||||
from resources.lib.tracking import set_timing_enabled
|
||||
|
||||
@@ -16,6 +16,3 @@ if log_timing_data:
|
||||
log.debug("About to enter mainEntryPoint()")
|
||||
|
||||
main_entry_point()
|
||||
|
||||
# clear done and exit.
|
||||
# sys.modules.clear()
|
||||
|
||||
BIN
fanart.jpg
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 32 KiB |
BIN
icon.png
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 13 KiB |
BIN
kodi.png
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 15 KiB |
59
release.yaml
Normal file
@@ -0,0 +1,59 @@
|
||||
version: '0.6.0'
|
||||
changelog: |-
|
||||
:tada: New features and improvements
|
||||
------------------------------------
|
||||
+ Add support for mixed content libraries (#227) @mcarlton00
|
||||
+ Move quickconnect code into user selection list (#226) @mcarlton00
|
||||
+ Show saved users in the user selection list (#225) @mcarlton00
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
+ Add play option to music video context menu (#231) @mcarlton00
|
||||
+ Fix stale menu links when user switching (#222) @mcarlton00
|
||||
+ Force unique client IDs when using quick connect (#221) @mcarlton00
|
||||
+ Add an error message when authentication fails (#220) @mcarlton00
|
||||
+ subtitle not working on self sign certificate (#224) @aiosk
|
||||
+ Make it clear that the play next episode setting is in seconds (#217) @mcarlton00
|
||||
+ Fix playing livetv from tvheadend (#213) @mcarlton00
|
||||
|
||||
Code or Repo Maintenance
|
||||
------------------------
|
||||
+ Cleanup old https verify method (#228) @mcarlton00
|
||||
|
||||
CI & build changes
|
||||
------------------
|
||||
+ Bump release-drafter/release-drafter from 5.20.1 to 5.21.0 (#223) @dependabot
|
||||
+ Bump release-drafter/release-drafter from 5.20.0 to 5.20.1 (#208) @dependabot
|
||||
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
@@ -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
|
||||
747
resources/language/resource.language.ar/strings.po
Normal 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)"
|
||||
100
resources/language/resource.language.cs/strings.po
Normal file
@@ -0,0 +1,100 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2022-08-25 20:11+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.13.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"
|
||||
|
||||
msgctxt "#30027"
|
||||
msgid "Enable debug logging"
|
||||
msgstr "Povolit protokolování ladění"
|
||||
|
||||
msgctxt "#30026"
|
||||
msgid "Widget item select action"
|
||||
msgstr "Akce pro výběr položky widgetu"
|
||||
|
||||
msgctxt "#30025"
|
||||
msgid "Password:"
|
||||
msgstr "Heslo:"
|
||||
|
||||
msgctxt "#30024"
|
||||
msgid "Username:"
|
||||
msgstr "Uživatelské jméno:"
|
||||
|
||||
msgctxt "#30023"
|
||||
msgid "Hide unwatched episode details"
|
||||
msgstr "Schovat podrobnosti nezhlédnutých epizod"
|
||||
|
||||
msgctxt "#30022"
|
||||
msgid "Advanced"
|
||||
msgstr "Pokročilé"
|
||||
|
||||
msgctxt "#30021"
|
||||
msgid "Show all episodes item"
|
||||
msgstr "Zobrazit všechny epizody"
|
||||
|
||||
msgctxt "#30019"
|
||||
msgid "Filtered episode name format"
|
||||
msgstr "Formát názvu filtrované epizody"
|
||||
|
||||
msgctxt "#30018"
|
||||
msgid "Number of items to show in filtered lists"
|
||||
msgstr "Počet položek k zobrazení ve filtrovaném seznamu"
|
||||
|
||||
msgctxt "#30017"
|
||||
msgid "Show connected clients"
|
||||
msgstr "Zobrazit připojené klienty"
|
||||
418
resources/language/resource.language.cy/strings.po
Normal 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"
|
||||
1119
resources/language/resource.language.de/strings.po
Normal file
2
resources/language/resource.language.el/strings.po
Normal 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"
|
||||
2
resources/language/resource.language.enm/strings.po
Normal 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"
|
||||
1103
resources/language/resource.language.eo/strings.po
Normal file
1111
resources/language/resource.language.es/strings.po
Normal file
763
resources/language/resource.language.et/strings.po
Normal 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"
|
||||
1115
resources/language/resource.language.fi/strings.po
Normal file
998
resources/language/resource.language.fr/strings.po
Normal file
@@ -0,0 +1,998 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2022-10-05 17:54+0000\n"
|
||||
"Last-Translator: Paulo <paulletilly@gmail.com>\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.14.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"
|
||||
|
||||
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"
|
||||
|
||||
msgctxt "#30394"
|
||||
msgid "Cache files deleted"
|
||||
msgstr "Fichiers du cache supprimés"
|
||||
|
||||
#, fuzzy
|
||||
msgctxt "#30393"
|
||||
msgid "Clear Cache Result"
|
||||
msgstr "Vider le cache des résultats"
|
||||
|
||||
msgctxt "#30388"
|
||||
msgid "Server details"
|
||||
msgstr "Détails du serveur"
|
||||
|
||||
msgctxt "#30387"
|
||||
msgid "Unused images removed : "
|
||||
msgstr "Images inutilisées supprimées :"
|
||||
|
||||
msgctxt "#30371"
|
||||
msgid "Could not connect to the URL you entered, do you want to try again?"
|
||||
msgstr ""
|
||||
"Impossible de se connecter à l'adresse saisie, voulez-vous essayer à nouveau "
|
||||
"?"
|
||||
|
||||
msgctxt "#30370"
|
||||
msgid "Do you want to manually enter a server url?"
|
||||
msgstr "Voulez-vous saisir manuellement une adresse de serveur ?"
|
||||
|
||||
#, fuzzy
|
||||
msgctxt "#30369"
|
||||
msgid "Do you want to clear your saved password?"
|
||||
msgstr "Voulez-vous effacer votre mot de passe sauvé ?"
|
||||
|
||||
msgctxt "#30368"
|
||||
msgid "Clear Password?"
|
||||
msgstr "Effacer le mot de passe ?"
|
||||
|
||||
msgctxt "#30374"
|
||||
msgid "Sending request"
|
||||
msgstr "Envoyer une requête"
|
||||
|
||||
msgctxt "#30377"
|
||||
msgid "Sending request"
|
||||
msgstr "Envoyer une requête"
|
||||
|
||||
msgctxt "#30376"
|
||||
msgid "Checking server url"
|
||||
msgstr "Vérifier l'url du serveur"
|
||||
352
resources/language/resource.language.hi/strings.po
Normal 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 "आतिथेय"
|
||||
1113
resources/language/resource.language.hu/strings.po
Normal file
1103
resources/language/resource.language.id/strings.po
Normal file
483
resources/language/resource.language.it/strings.po
Normal file
@@ -0,0 +1,483 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2022-08-17 22:11+0000\n"
|
||||
"Last-Translator: momphucker <weblate@mailbox.enricomarogna.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.13.1\n"
|
||||
|
||||
msgctxt "#30120"
|
||||
msgid "Show load progress"
|
||||
msgstr "Mostra progressione 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"
|
||||
|
||||
msgctxt "#30331"
|
||||
msgid "Movies per page"
|
||||
msgstr "Film per pagina"
|
||||
|
||||
msgctxt "#30329"
|
||||
msgid "Screensaver"
|
||||
msgstr "Salvaschermo"
|
||||
|
||||
msgctxt "#30337"
|
||||
msgid "Song"
|
||||
msgstr "Canzone"
|
||||
|
||||
msgctxt "#30327"
|
||||
msgid "Go To Season"
|
||||
msgstr "Vai Alla Stagione"
|
||||
|
||||
msgctxt "#30325"
|
||||
msgid " - Genres"
|
||||
msgstr "- Generi"
|
||||
|
||||
msgctxt "#30323"
|
||||
msgid "Artists"
|
||||
msgstr "Artisti"
|
||||
|
||||
msgctxt "#30311"
|
||||
msgid "Library - "
|
||||
msgstr "Libreria -"
|
||||
|
||||
msgctxt "#30296"
|
||||
msgid "Delete"
|
||||
msgstr "Elimina"
|
||||
|
||||
msgctxt "#30288"
|
||||
msgid " - Latest"
|
||||
msgstr "- Ultimo"
|
||||
|
||||
msgctxt "#30280"
|
||||
msgid "Missing Title"
|
||||
msgstr "Titolo Mancante"
|
||||
1107
resources/language/resource.language.kk/strings.po
Normal file
1118
resources/language/resource.language.nl/strings.po
Normal file
1103
resources/language/resource.language.pl/strings.po
Normal file
1104
resources/language/resource.language.ru/strings.po
Normal file
1055
resources/language/resource.language.sv/strings.po
Normal file
328
resources/language/resource.language.tr/strings.po
Normal file
@@ -0,0 +1,328 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2022-10-01 13:54+0000\n"
|
||||
"Last-Translator: Thanos <ahmetspam.42@hotmail.com>\n"
|
||||
"Language-Team: Turkish <https://translate.jellyfin.org/projects/jellycon/"
|
||||
"jellycon/tr/>\n"
|
||||
"Language: tr\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.14.1\n"
|
||||
|
||||
msgctxt "#30237"
|
||||
msgid "Start from beginning"
|
||||
msgstr "Baştan başla"
|
||||
|
||||
msgctxt "#30236"
|
||||
msgid "Force transcode h265 (hevc)"
|
||||
msgstr "H265 (hevc) kodunu dönüştürmeye zorla"
|
||||
|
||||
msgctxt "#30235"
|
||||
msgid "Episodes"
|
||||
msgstr "Bölümler"
|
||||
|
||||
msgctxt "#30231"
|
||||
msgid "Movies"
|
||||
msgstr "Filmler"
|
||||
|
||||
msgctxt "#30229"
|
||||
msgid "TV Shows"
|
||||
msgstr "TV Şovları"
|
||||
|
||||
msgctxt "#30224"
|
||||
msgid "Interaction"
|
||||
msgstr "Etkileşim"
|
||||
|
||||
msgctxt "#30223"
|
||||
msgid "Page Size and Filtering"
|
||||
msgstr "Sayfa Boyutu ve Filtreleme"
|
||||
|
||||
msgctxt "#30222"
|
||||
msgid "Item Layout"
|
||||
msgstr "Öğe Düzeni"
|
||||
|
||||
msgctxt "#30220"
|
||||
msgid "Prompt to delete movie after %"
|
||||
msgstr "%'den sonra filmi silme istemi"
|
||||
|
||||
msgctxt "#30219"
|
||||
msgid " - Prompt before play"
|
||||
msgstr "- Oynamadan önce sor"
|
||||
|
||||
msgctxt "#30218"
|
||||
msgid "Play next episode after %"
|
||||
msgstr "% sonra sonraki bölümü oynat"
|
||||
|
||||
msgctxt "#30217"
|
||||
msgid "Prompt to delete episode after %"
|
||||
msgstr "%'den sonra bölümü silme istemi"
|
||||
|
||||
msgctxt "#30216"
|
||||
msgid "Item Details"
|
||||
msgstr "Ürün Detayları"
|
||||
|
||||
msgctxt "#30215"
|
||||
msgid "On playback stop (100% = disabled)"
|
||||
msgstr "Oynatma durduğunda (%100 = devre dışı)"
|
||||
|
||||
msgctxt "#30214"
|
||||
msgid "Events"
|
||||
msgstr "Olaylar"
|
||||
|
||||
msgctxt "#30213"
|
||||
msgid "Video force 8 bit"
|
||||
msgstr "Video 8 bit kullanimini zorla"
|
||||
|
||||
msgctxt "#30212"
|
||||
msgid "Video max width"
|
||||
msgstr "Video maksimum genişliği"
|
||||
|
||||
msgctxt "#30211"
|
||||
msgid "Transcode options"
|
||||
msgstr "Kod dönüştürme seçenekleri"
|
||||
|
||||
#, fuzzy
|
||||
msgctxt "#30210"
|
||||
msgid "HTTP direct stream"
|
||||
msgstr "HTTP doğrudan akışı"
|
||||
|
||||
#, fuzzy
|
||||
msgctxt "#30209"
|
||||
msgid "File direct path"
|
||||
msgstr "Dosyanin doğrudan yolu"
|
||||
|
||||
msgctxt "#30208"
|
||||
msgid "Max stream bitrate (Kbits)"
|
||||
msgstr "Maksimum akış bit hızı (Kbps)"
|
||||
|
||||
#, fuzzy
|
||||
msgctxt "#30207"
|
||||
msgid "Playback"
|
||||
msgstr "Geri çalma"
|
||||
|
||||
msgctxt "#30206"
|
||||
msgid "Playback type"
|
||||
msgstr "Oynatma türü"
|
||||
|
||||
msgctxt "#30201"
|
||||
msgid "Unable to connect to server"
|
||||
msgstr "Sunucuya bağlanılamıyor"
|
||||
|
||||
msgctxt "#30200"
|
||||
msgid "URL error"
|
||||
msgstr "URL hatası"
|
||||
|
||||
msgctxt "#30183"
|
||||
msgid "Include people"
|
||||
msgstr "Kişileri dahil et"
|
||||
|
||||
msgctxt "#30182"
|
||||
msgid "Include media stream info"
|
||||
msgstr "Medya akışı bilgilerini dahil et"
|
||||
|
||||
msgctxt "#30181"
|
||||
msgid "Include plot"
|
||||
msgstr "Konuyu dahil et"
|
||||
|
||||
msgctxt "#30180"
|
||||
msgid "Select User"
|
||||
msgstr "Kullanıcı seç"
|
||||
|
||||
msgctxt "#30169"
|
||||
msgid "Address: "
|
||||
msgstr "Adres:"
|
||||
|
||||
#, fuzzy
|
||||
msgctxt "#30167"
|
||||
msgid "Selected Server Address"
|
||||
msgstr "Seçilen Sunucunun Adresi"
|
||||
|
||||
msgctxt "#30166"
|
||||
msgid "Select Server"
|
||||
msgstr "Sunucu seç"
|
||||
|
||||
msgctxt "#30163"
|
||||
msgid "Add (cc) if subtitle is available"
|
||||
msgstr "Altyazı varsa (cc) ekleyin"
|
||||
|
||||
msgctxt "#30139"
|
||||
msgid "No Media Type Set"
|
||||
msgstr "Medya Türü Ayarı Yok"
|
||||
|
||||
msgctxt "#30135"
|
||||
msgid "Error"
|
||||
msgstr "Hata"
|
||||
|
||||
msgctxt "#30126"
|
||||
msgid "Processing Item : "
|
||||
msgstr "Öğenin İşlenmesi:"
|
||||
|
||||
msgctxt "#30125"
|
||||
msgid "Done"
|
||||
msgstr "Tamamlandı"
|
||||
|
||||
#, fuzzy
|
||||
msgctxt "#30121"
|
||||
msgid "On resume"
|
||||
msgstr "Sürdürunce"
|
||||
|
||||
msgctxt "#30120"
|
||||
msgid "Show load progress"
|
||||
msgstr "Yükleme ilerlemesini göster"
|
||||
|
||||
msgctxt "#30118"
|
||||
msgid "Add resume percent to names"
|
||||
msgstr "İsimlere özgeçmiş yüzdesi ekle"
|
||||
|
||||
#, fuzzy
|
||||
msgctxt "#30116"
|
||||
msgid "Add unwatched counts to names"
|
||||
msgstr "Adlara izlenmeyen sayıları ekleyin"
|
||||
|
||||
#, fuzzy
|
||||
msgctxt "#30114"
|
||||
msgid "Jump back seconds"
|
||||
msgstr "Saniyeler geri atla"
|
||||
|
||||
msgctxt "#30113"
|
||||
msgid "Retrieving Data"
|
||||
msgstr "Veri Aliniyor"
|
||||
|
||||
msgctxt "#30112"
|
||||
msgid "Loading Content"
|
||||
msgstr "İçerik Yükleniyor"
|
||||
|
||||
msgctxt "#30111"
|
||||
msgid "Services"
|
||||
msgstr "Hizmetler"
|
||||
|
||||
msgctxt "#30110"
|
||||
msgid "Interface"
|
||||
msgstr "Arayüz"
|
||||
|
||||
msgctxt "#30092"
|
||||
msgid "Warning: This action will delete the media files from the server."
|
||||
msgstr "Uyarı: Bu eylem, medya dosyalarını sunucudan siler."
|
||||
|
||||
msgctxt "#30091"
|
||||
msgid "Confirm delete?"
|
||||
msgstr "Silmeyi onayla?"
|
||||
|
||||
msgctxt "#30063"
|
||||
msgid "N/A"
|
||||
msgstr "Yok"
|
||||
|
||||
#, fuzzy
|
||||
msgctxt "#30053"
|
||||
msgid "Waiting for server to delete"
|
||||
msgstr "Sunucunun silmesi bekleniyor"
|
||||
|
||||
msgctxt "#30052"
|
||||
msgid "Deleting"
|
||||
msgstr "Siliniyor"
|
||||
|
||||
msgctxt "#30045"
|
||||
msgid "Username not found"
|
||||
msgstr "Kullanıcı bulunamadı"
|
||||
|
||||
msgctxt "#30044"
|
||||
msgid "Incorrect Username/Password"
|
||||
msgstr "Yanlış kullanıcı/şifre"
|
||||
|
||||
#, fuzzy
|
||||
msgctxt "#30027"
|
||||
msgid "Enable debug logging"
|
||||
msgstr "Hata ayıklama günlüğünü etkinleştir"
|
||||
|
||||
#, fuzzy
|
||||
msgctxt "#30026"
|
||||
msgid "Widget item select action"
|
||||
msgstr "Widget öğesi seçme eylemi"
|
||||
|
||||
msgctxt "#30025"
|
||||
msgid "Password:"
|
||||
msgstr "Şifre:"
|
||||
|
||||
msgctxt "#30024"
|
||||
msgid "Username:"
|
||||
msgstr "Kullanıcı:"
|
||||
|
||||
msgctxt "#30023"
|
||||
msgid "Hide unwatched episode details"
|
||||
msgstr "İzlenmeyen bölüm ayrıntılarını gizle"
|
||||
|
||||
msgctxt "#30022"
|
||||
msgid "Advanced"
|
||||
msgstr "Gelişmiş"
|
||||
|
||||
msgctxt "#30021"
|
||||
msgid "Show all episodes item"
|
||||
msgstr "Tüm bölümleri göster"
|
||||
|
||||
msgctxt "#30020"
|
||||
msgid "Flatten single season"
|
||||
msgstr "Tek sezon düzleştirin"
|
||||
|
||||
msgctxt "#30019"
|
||||
msgid "Filtered episode name format"
|
||||
msgstr "Filtrelenmiş bölüm adı biçimi"
|
||||
|
||||
msgctxt "#30018"
|
||||
msgid "Number of items to show in filtered lists"
|
||||
msgstr "Filtrelenmiş listelerde gösterilecek öğe sayısı"
|
||||
|
||||
msgctxt "#30017"
|
||||
msgid "Show connected clients"
|
||||
msgstr "Bağlı istemcileri göster"
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Device display name"
|
||||
msgstr "Cihazın görünen adı"
|
||||
|
||||
msgctxt "#30015"
|
||||
msgid "Log timing data"
|
||||
msgstr "Zamanlama verilerini günlüğe kaydet"
|
||||
|
||||
msgctxt "#30014"
|
||||
msgid "Jellyfin"
|
||||
msgstr "Jellyfin"
|
||||
|
||||
msgctxt "#30012"
|
||||
msgid "[Change user]"
|
||||
msgstr "[Kullanıcıyı değiştir]"
|
||||
|
||||
#, fuzzy
|
||||
msgctxt "#30011"
|
||||
msgid "[Detect local server]"
|
||||
msgstr "[Yerel sunucuyu algıla]"
|
||||
|
||||
#, fuzzy
|
||||
msgctxt "#30010"
|
||||
msgid "Number of performance profiles to capture"
|
||||
msgstr "Yakalanacak performans profili sayısı"
|
||||
|
||||
msgctxt "#30008"
|
||||
msgid "Samba password"
|
||||
msgstr "Samba Şifre"
|
||||
|
||||
msgctxt "#30007"
|
||||
msgid "Samba username"
|
||||
msgstr "Samba Kullanıcı"
|
||||
|
||||
msgctxt "#30006"
|
||||
msgid "Password"
|
||||
msgstr "Şifre"
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username"
|
||||
msgstr "Kullanıcı"
|
||||
|
||||
msgctxt "#30003"
|
||||
msgid "Verify HTTPS certificate"
|
||||
msgstr "HTTPS sertifikasını doğrulayın"
|
||||
|
||||
msgctxt "#30000"
|
||||
msgid "Host"
|
||||
msgstr "Sunucu"
|
||||
1106
resources/language/resource.language.uk/strings.po
Normal file
1100
resources/language/resource.language.zh_Hans/strings.po
Normal file
104
resources/language/resource.language.zh_Hant/strings.po
Normal 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 "主機"
|
||||
@@ -7,7 +7,7 @@ import threading
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
|
||||
from .loghandler import LazyLogger
|
||||
from .lazylogger import LazyLogger
|
||||
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
@@ -26,7 +26,7 @@ class ActionAutoClose(threading.Thread):
|
||||
|
||||
def run(self):
|
||||
log.debug("ActionAutoClose Running")
|
||||
while not xbmc.abortRequested and not self.stop_thread:
|
||||
while not xbmc.Monitor().abortRequested() and not self.stop_thread:
|
||||
time_since_last = time.time() - self.last_interaction
|
||||
log.debug("ActionAutoClose time_since_last : {0}".format(time_since_last))
|
||||
|
||||
@@ -70,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
|
||||
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
|
||||
from .loghandler import LazyLogger
|
||||
|
||||
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)
|
||||
|
||||
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):
|
||||
|
||||
# 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}".format(control_id))
|
||||
#self.close()
|
||||
@@ -2,7 +2,7 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import urllib
|
||||
from six.moves.urllib.parse import unquote
|
||||
import requests
|
||||
import base64
|
||||
import sys
|
||||
@@ -14,15 +14,13 @@ import xbmcplugin
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
|
||||
from .downloadutils import DownloadUtils
|
||||
from .loghandler import LazyLogger
|
||||
from .jellyfin import api
|
||||
from .lazylogger import LazyLogger
|
||||
from .jsonrpc import JsonRpc, get_value
|
||||
from .translation import string_load
|
||||
from .datamanager import DataManager
|
||||
from .utils import get_art, double_urlencode
|
||||
from .utils import translate_string, load_user_details
|
||||
from .kodi_utils import HomeWindow
|
||||
from .item_functions import get_art
|
||||
|
||||
downloadUtils = DownloadUtils()
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
@@ -70,8 +68,8 @@ class CacheArtwork(threading.Thread):
|
||||
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")
|
||||
@@ -82,7 +80,7 @@ class CacheArtwork(threading.Thread):
|
||||
result = JsonRpc('Settings.GetSettingValue').execute(web_query)
|
||||
xbmc_webserver_enabled = result['result']['value']
|
||||
if not xbmc_webserver_enabled:
|
||||
xbmcgui.Dialog().ok(string_load(30294), string_load(30295))
|
||||
xbmcgui.Dialog().ok(translate_string(30294), translate_string(30295))
|
||||
return
|
||||
|
||||
params = {"properties": ["url"]}
|
||||
@@ -90,7 +88,7 @@ class CacheArtwork(threading.Thread):
|
||||
textures = json_result.get("result", {}).get("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:
|
||||
@@ -104,10 +102,10 @@ class CacheArtwork(threading.Thread):
|
||||
|
||||
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")
|
||||
@@ -119,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"]}
|
||||
@@ -150,11 +148,10 @@ class CacheArtwork(threading.Thread):
|
||||
unused_texture_ids = set()
|
||||
for texture in textures:
|
||||
url = texture.get("url")
|
||||
url = urllib.unquote(url)
|
||||
url = unquote(url)
|
||||
url = url.replace("image://", "")
|
||||
url = url[0:-1]
|
||||
if url.find("/") > -1 and url not in jellyfin_texture_urls or url.find("localhost:24276") > -1:
|
||||
# log.debug("adding unused texture url: {0}", url)
|
||||
unused_texture_ids.add(texture["textureid"])
|
||||
|
||||
total = len(unused_texture_ids)
|
||||
@@ -172,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
|
||||
@@ -188,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
|
||||
@@ -197,12 +194,12 @@ 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)
|
||||
@@ -215,9 +212,11 @@ class CacheArtwork(threading.Thread):
|
||||
|
||||
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"
|
||||
@@ -225,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()
|
||||
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:
|
||||
@@ -270,7 +268,7 @@ class CacheArtwork(threading.Thread):
|
||||
# get the password
|
||||
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)
|
||||
@@ -280,12 +278,12 @@ class CacheArtwork(threading.Thread):
|
||||
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)
|
||||
@@ -298,7 +296,7 @@ class CacheArtwork(threading.Thread):
|
||||
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:
|
||||
@@ -306,7 +304,6 @@ class CacheArtwork(threading.Thread):
|
||||
|
||||
missing_texture_urls = set()
|
||||
|
||||
# image_types = ["thumb", "poster", "banner", "clearlogo", "tvshow.poster", "tvshow.banner", "tvshow.landscape"]
|
||||
for image_url in jellyfin_texture_urls:
|
||||
if image_url not in texture_urls and not image_url.endswith("&Tag=") and len(image_url) > 0:
|
||||
missing_texture_urls.add(image_url)
|
||||
@@ -329,9 +326,7 @@ class CacheArtwork(threading.Thread):
|
||||
|
||||
count_done = 0
|
||||
for index, get_url in enumerate(missing_texture_urls, 1):
|
||||
# log.debug("texture_url: {0}", get_url)
|
||||
url = double_urlencode(get_url)
|
||||
kodi_texture_url = ("/image/image://%s" % url)
|
||||
kodi_texture_url = "/image/image://{0}".format(get_url)
|
||||
log.debug("kodi_texture_url: {0}".format(kodi_texture_url))
|
||||
|
||||
percentage = int((float(index) / float(total)) * 100)
|
||||
@@ -345,8 +340,6 @@ class CacheArtwork(threading.Thread):
|
||||
count_done += 1
|
||||
log.debug("Get Image Result: {0}".format(data.status_code))
|
||||
|
||||
# if progress.iscanceled():
|
||||
# if "iscanceled" in dir(progress) and progress.iscanceled():
|
||||
if isinstance(progress, xbmcgui.DialogProgress) and progress.iscanceled():
|
||||
break
|
||||
|
||||
@@ -354,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
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
from uuid import uuid4 as uuid4
|
||||
import xbmcaddon
|
||||
import xbmc
|
||||
import xbmcvfs
|
||||
|
||||
from .kodi_utils import HomeWindow
|
||||
from .loghandler import LazyLogger
|
||||
|
||||
log = LazyLogger(__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}".format(jellyfin_guid_path))
|
||||
guid = xbmcvfs.File(jellyfin_guid_path)
|
||||
client_id = guid.read()
|
||||
guid.close()
|
||||
|
||||
if not client_id:
|
||||
client_id = str("%012X" % uuid4())
|
||||
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 client_id
|
||||
|
||||
@staticmethod
|
||||
def get_version():
|
||||
addon = xbmcaddon.Addon()
|
||||
version = addon.getAddonInfo("version")
|
||||
return version
|
||||
|
||||
@staticmethod
|
||||
def get_client():
|
||||
return 'Kodi JellyCon'
|
||||
@@ -1,40 +0,0 @@
|
||||
import threading
|
||||
import xbmc
|
||||
|
||||
from .loghandler import LazyLogger
|
||||
from resources.lib.functions import show_menu
|
||||
|
||||
log = LazyLogger(__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)
|
||||
|
||||
log.debug("ContextMonitor Thread Exited")
|
||||
|
||||
def stop_monitor(self):
|
||||
log.debug("ContextMonitor Stop Called")
|
||||
self.stop_thread = True
|
||||
@@ -1,20 +1,19 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
from collections import defaultdict
|
||||
import threading
|
||||
import hashlib
|
||||
import os
|
||||
import cPickle
|
||||
import time
|
||||
from six.moves import cPickle
|
||||
|
||||
from .downloadutils import DownloadUtils
|
||||
from .loghandler import LazyLogger
|
||||
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
|
||||
@@ -40,15 +39,12 @@ 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()
|
||||
|
||||
@timer
|
||||
def get_content(self, url):
|
||||
return DownloadUtils().download_url(url)
|
||||
self.api = api
|
||||
|
||||
@timer
|
||||
def get_items(self, url, gui_options, use_cache=False):
|
||||
@@ -57,19 +53,14 @@ class DataManager:
|
||||
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
|
||||
@@ -103,7 +94,7 @@ class DataManager:
|
||||
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 = []
|
||||
@@ -135,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
|
||||
@@ -171,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
|
||||
@@ -203,7 +192,7 @@ class CacheManagerThread(threading.Thread):
|
||||
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 = []
|
||||
|
||||
@@ -264,7 +253,7 @@ 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
|
||||
@@ -274,14 +263,15 @@ def clear_cached_server_data():
|
||||
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
|
||||
|
||||
206
resources/lib/dialogs.py
Normal file
@@ -0,0 +1,206 @@
|
||||
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
|
||||
@@ -5,17 +5,14 @@ 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 .loghandler import LazyLogger
|
||||
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 = LazyLogger(__name__)
|
||||
@@ -28,7 +25,7 @@ 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}".format(url))
|
||||
log.debug("MediaType: {0}".format(media_type))
|
||||
@@ -71,6 +68,11 @@ def get_content(url, params):
|
||||
content_type = 'episodes'
|
||||
elif media_type == "playlists":
|
||||
view_type = "Playlists"
|
||||
elif media_type == "musicvideos":
|
||||
view_type = "Music Videos"
|
||||
content_type = 'musicvideos'
|
||||
elif media_type == "mixed":
|
||||
content_type = 'videos'
|
||||
|
||||
log.debug("media_type:{0} content_type:{1} view_type:{2} ".format(media_type, content_type, view_type))
|
||||
|
||||
@@ -78,8 +80,8 @@ def get_content(url, params):
|
||||
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
|
||||
@@ -106,13 +108,6 @@ def get_content(url, params):
|
||||
url = url + "&StartIndex=" + str(start_index) + "&Limit=" + str(page_limit)
|
||||
log.debug("ADDING NEXT URL: {0}".format(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)
|
||||
|
||||
use_cache = params.get("use_cache", "true") == "true"
|
||||
|
||||
dir_items, detected_type, total_records = process_directory(url, progress, params, use_cache)
|
||||
@@ -126,7 +121,7 @@ def get_content(url, params):
|
||||
if url_prev:
|
||||
list_item = xbmcgui.ListItem("Prev Page (" + str(start_index - page_limit + 1) + "-" + str(start_index) +
|
||||
" of " + str(total_records) + ")")
|
||||
u = sys.argv[0] + "?url=" + urllib.quote(url_prev) + "&mode=GET_CONTENT&media_type=movies"
|
||||
u = sys.argv[0] + "?url=" + quote(url_prev) + "&mode=GET_CONTENT&media_type=movies"
|
||||
log.debug("ADDING PREV ListItem: {0} - {1}".format(u, list_item))
|
||||
dir_items.insert(0, (u, list_item, True))
|
||||
|
||||
@@ -136,7 +131,7 @@ def get_content(url, params):
|
||||
upper_count = total_records
|
||||
list_item = xbmcgui.ListItem("Next Page (" + str(start_index + page_limit + 1) + "-" +
|
||||
str(upper_count) + " of " + str(total_records) + ")")
|
||||
u = sys.argv[0] + "?url=" + urllib.quote(url_next) + "&mode=GET_CONTENT&media_type=movies"
|
||||
u = sys.argv[0] + "?url=" + quote(url_next) + "&mode=GET_CONTENT&media_type=movies"
|
||||
log.debug("ADDING NEXT ListItem: {0} - {1}".format(u, list_item))
|
||||
dir_items.append((u, list_item, True))
|
||||
|
||||
@@ -173,13 +168,8 @@ def get_content(url, params):
|
||||
else:
|
||||
log.debug("No view id for view type:{0}".format(view_key))
|
||||
|
||||
# send display items event
|
||||
# display_items_notification = {"view_type": view_type}
|
||||
# log.debug("Sending display_items with data {0}", display_items_notification)
|
||||
# send_event_notification("display_items", display_items_notification)
|
||||
|
||||
if progress is not None:
|
||||
progress.update(100, string_load(30125))
|
||||
progress.update(100, translate_string(30125))
|
||||
progress.close()
|
||||
|
||||
return
|
||||
@@ -232,13 +222,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]
|
||||
@@ -253,6 +244,18 @@ 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('{field_filters}', default_filters)
|
||||
url = url.replace('{ItemLimit}', str(item_limit))
|
||||
|
||||
# Need to replace at runtime so it always pulls the current user
|
||||
url = unquote(url)
|
||||
url = url.replace('{userid}', user_id)
|
||||
|
||||
cache_file, item_list, total_records, cache_thread = data_manager.get_items(url, gui_options, use_cache)
|
||||
|
||||
# flatten single season
|
||||
@@ -262,13 +265,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}' +
|
||||
'&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()
|
||||
@@ -302,7 +305,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:
|
||||
@@ -327,28 +330,30 @@ 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}' +
|
||||
'&Fields={}'.format(default_filters) +
|
||||
'&format=json')
|
||||
if not show_empty_folders:
|
||||
u = u + '&isMissing=False'
|
||||
|
||||
elif item_details.item_type == "Season":
|
||||
u = ('{server}/Shows/' + item_details.series_id +
|
||||
u = ('/Shows/' + item_details.series_id +
|
||||
'/Episodes'
|
||||
'?userId={userid}' +
|
||||
'&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/{userid}/items' +
|
||||
'?ParentId=' + item_details.id +
|
||||
'&IsVirtualUnAired=false' +
|
||||
'&IsMissing=false' +
|
||||
'&Fields={field_filters}' +
|
||||
'&Fields={}'.format(default_filters) +
|
||||
'&format=json')
|
||||
|
||||
default_sort = item_details.item_type == "Playlist"
|
||||
@@ -361,7 +366,7 @@ def process_directory(url, progress, params, use_cache_data=False):
|
||||
log.debug("Dropping empty folder item : {0}".format(item_details.__dict__))
|
||||
|
||||
elif item_details.item_type == "MusicArtist":
|
||||
u = ('{server}/Users/{userid}/items' +
|
||||
u = ('/Users/{userid}/items' +
|
||||
'?ArtistIds=' + item_details.id +
|
||||
'&IncludeItemTypes=MusicAlbum' +
|
||||
'&CollapseBoxSetItems=false' +
|
||||
@@ -383,13 +388,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 +
|
||||
'&IsVirtualUnAired=false' +
|
||||
'&IsMissing=false' +
|
||||
'&Fields=SpecialEpisodeNumbers,{field_filters}' +
|
||||
'&Fields=SpecialEpisodeNumbers,{}'.format(default_filters) +
|
||||
'&format=json')
|
||||
played = 0
|
||||
overlay = "7"
|
||||
@@ -400,7 +404,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
|
||||
|
||||
@@ -1,743 +0,0 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
|
||||
import requests
|
||||
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 traceback import format_exc
|
||||
from kodi_six.utils import py2_decode
|
||||
|
||||
from .kodi_utils import HomeWindow
|
||||
from .clientinfo import ClientInformation
|
||||
from .loghandler import LazyLogger
|
||||
from .translation import string_load
|
||||
from .tracking import timer
|
||||
|
||||
log = LazyLogger(__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}".format(self.use_https))
|
||||
|
||||
self.verify_cert = settings.getSetting('verify_cert') == 'true'
|
||||
log.debug("verify_cert: {0}".format(self.verify_cert))
|
||||
|
||||
def post_capabilities(self):
|
||||
|
||||
url = "{server}/Sessions/Capabilities/Full?format=json"
|
||||
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.download_url(url, post_body=data, method="POST")
|
||||
log.debug("Posted Capabilities: {0}".format(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}".format(url))
|
||||
log.debug("PlaybackInfo : {0}".format(profile))
|
||||
play_info_result = self.download_url(url, post_body=playback_info, method="POST")
|
||||
log.debug("PlaybackInfo : {0}".format(play_info_result))
|
||||
|
||||
return play_info_result
|
||||
|
||||
def get_server(self):
|
||||
settings = xbmcaddon.Addon()
|
||||
|
||||
#For migration from storing URL parts to just one URL
|
||||
if settings.getSetting('ipaddress') != "" and settings.getSetting('ipaddress') != "<none>":
|
||||
log.info("Migrating to new server url storage")
|
||||
url = ("http://" if settings.getSetting('protocol') == "0" else "https://") + settings.getSetting('ipaddress') + ":" + settings.getSetting('port')
|
||||
settings.setSetting('server_address', url)
|
||||
settings.setSetting('ipaddress', "")
|
||||
|
||||
return settings.getSetting('server_address')
|
||||
|
||||
@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.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]
|
||||
# log.debug("Background Image Tag: {0}", imageTag)
|
||||
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
|
||||
# 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.get(id_name)
|
||||
parent_image_tag = data.get(tag_name)
|
||||
if parent_image_id is not None and parent_image_tag is not None:
|
||||
item_id = parent_image_id
|
||||
image_tag = parent_image_tag
|
||||
# log.debug("Parent Image Tag: {0}", imageTag)
|
||||
|
||||
# ParentTag not passed for Banner and Art
|
||||
if not image_tag and not ((art_type == 'Banner' or art_type == 'Art') and parent is True):
|
||||
# log.debug("No Image Tag for request:{0} item:{1} parent:{2}", art_type, item_type, parent)
|
||||
return ""
|
||||
|
||||
artwork = "%s/Items/%s/Images/%s/%s?Format=original&Tag=%s" % (server, item_id, art_type, index, image_tag)
|
||||
|
||||
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:
|
||||
log.debug("JellyCon DownloadUtils -> Returning saved UserID: {0}".format(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}".format(user_name))
|
||||
|
||||
try:
|
||||
result = self.download_url("{server}/Users/Public?format=json", suppress=True, authenticate=False)
|
||||
except Exception as msg:
|
||||
log.error("Get User unable to connect: {0}".format(msg))
|
||||
return ""
|
||||
|
||||
log.debug("GETUSER_JSONDATA_01: {0}".format(py2_decode(result)))
|
||||
|
||||
if not result:
|
||||
return ""
|
||||
|
||||
log.debug("GETUSER_JSONDATA_02: {0}".format(result))
|
||||
|
||||
secure = False
|
||||
for user in result:
|
||||
if user.get("Name") == unicode(user_name, "utf-8"):
|
||||
userid = user.get("Id")
|
||||
user_image = self.get_user_artwork(user, 'Primary')
|
||||
log.debug("Username Found: {0}".format(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}".format(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}".format(token))
|
||||
return token
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
server_address = settings.getSetting("server_address")
|
||||
|
||||
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}
|
||||
|
||||
result = self.download_url(url, post_body=message_data, method="POST", suppress=True, authenticate=False)
|
||||
log.debug("AuthenticateByName: {0}".format(result))
|
||||
|
||||
access_token = None
|
||||
userid = None
|
||||
access_token = result.get("AccessToken")
|
||||
userid = result["User"].get("Id")
|
||||
|
||||
if access_token is not None:
|
||||
log.debug("User Authenticated: {0}".format(access_token))
|
||||
log.debug("User Id: {0}".format(userid))
|
||||
window.set_property("AccessToken", access_token)
|
||||
window.set_property("userid", userid)
|
||||
# WINDOW.setProperty("userimage", "")
|
||||
|
||||
self.post_capabilities()
|
||||
|
||||
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['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['X-Emby-Authorization'] = auth_string
|
||||
|
||||
auth_token = self.authenticate()
|
||||
if auth_token != "":
|
||||
headers["X-MediaBrowser-Token"] = auth_token
|
||||
|
||||
log.debug("JellyCon Authentication Header: {0}".format(headers))
|
||||
return headers
|
||||
|
||||
@timer
|
||||
def download_url(self, url, suppress=False, post_body=None, method="GET", authenticate=True, headers=None):
|
||||
log.debug("downloadUrl")
|
||||
|
||||
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 {}
|
||||
|
||||
if settings.getSetting("suppressErrors") == "true":
|
||||
suppress = True
|
||||
|
||||
log.debug("Before: {0}".format(url))
|
||||
|
||||
if url.find("{server}") != -1:
|
||||
server = self.get_server()
|
||||
if server is None:
|
||||
return {}
|
||||
url = url.replace("{server}", server)
|
||||
|
||||
if url.find("{userid}") != -1:
|
||||
userid = self.get_user_id()
|
||||
if not userid:
|
||||
return {}
|
||||
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 {}
|
||||
url = url.replace("{random_movies}", random_movies)
|
||||
|
||||
log.debug("After: {0}".format(url))
|
||||
|
||||
try:
|
||||
url_bits = urlparse(url.strip())
|
||||
user_name = url_bits.username
|
||||
user_password = url_bits.password
|
||||
|
||||
head = self.get_auth_header(authenticate)
|
||||
|
||||
if user_name and user_password:
|
||||
log.info("Replacing username & Password info")
|
||||
# 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()
|
||||
|
||||
http_request = getattr(requests, method.lower())
|
||||
|
||||
if post_body:
|
||||
|
||||
if isinstance(post_body, dict):
|
||||
head["Content-Type"] = "application/json"
|
||||
post_body = json.dumps(post_body)
|
||||
else:
|
||||
head["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
|
||||
log.debug("Content-Type: {0}".format(head["Content-Type"]))
|
||||
log.debug("POST DATA: {0}".format(post_body))
|
||||
|
||||
data = http_request(url, data=post_body, headers=head)
|
||||
else:
|
||||
data = http_request(url, headers=head)
|
||||
|
||||
|
||||
if data.status_code == 200:
|
||||
|
||||
if headers is not None and isinstance(headers, dict):
|
||||
headers.update(data.headers)
|
||||
log.debug("{0}".format(data.json()))
|
||||
|
||||
elif data.status_code >= 400:
|
||||
|
||||
if data.status_code == 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}".format(hashed_username))
|
||||
settings.setSetting("saved_user_password_" + hashed_username, "")
|
||||
save_user_details(settings, "", "")
|
||||
|
||||
log.error("HTTP response error for {0}: {1} {2}".format(url, data.status_code, data.content))
|
||||
if suppress is False:
|
||||
xbmcgui.Dialog().notification(string_load(30316),
|
||||
string_load(30200) % str(data.content),
|
||||
icon="special://home/addons/plugin.video.jellycon/icon.png")
|
||||
try:
|
||||
result = data.json()
|
||||
except:
|
||||
result = {}
|
||||
return result
|
||||
except Exception as msg:
|
||||
log.error("{0}".format(format_exc()))
|
||||
log.error("Unable to connect to {0} : {1}".format(server, msg))
|
||||
if not suppress:
|
||||
xbmcgui.Dialog().notification(string_load(30316),
|
||||
str(msg),
|
||||
icon="special://home/addons/plugin.video.jellycon/icon.png")
|
||||
@@ -90,6 +90,7 @@ import sys
|
||||
import time
|
||||
import errno
|
||||
|
||||
|
||||
class FileLock(object):
|
||||
""" A file locking mechanism that has context-manager support so
|
||||
you can use it in a ``with`` statement. This should be relatively cross
|
||||
|
||||
@@ -1,51 +1,44 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import urllib
|
||||
from six.moves.urllib.parse import quote, unquote, parse_qsl
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import cProfile
|
||||
import pstats
|
||||
import json
|
||||
import StringIO
|
||||
from six import StringIO
|
||||
|
||||
import xbmcplugin
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
import xbmc
|
||||
|
||||
from .downloadutils import DownloadUtils, load_user_details
|
||||
from .utils import get_art, send_event_notification, convert_size
|
||||
from .jellyfin import api
|
||||
from .utils import translate_string, get_version, load_user_details, get_art_url, get_default_filters, translate_path, kodi_version
|
||||
from .kodi_utils import HomeWindow
|
||||
from .clientinfo import ClientInformation
|
||||
from .datamanager import DataManager, clear_cached_server_data
|
||||
from .datamanager import clear_cached_server_data
|
||||
from .server_detect import check_server, check_connection_speed
|
||||
from .loghandler import LazyLogger
|
||||
from .menu_functions import display_main_menu, display_menu, show_movie_alpha_list, show_tvshow_alpha_list, show_genre_list, show_search, show_movie_pages
|
||||
from .translation import string_load
|
||||
from .lazylogger import LazyLogger
|
||||
from .menu_functions import display_main_menu, display_menu, show_movie_alpha_list, show_tvshow_alpha_list, show_genre_list, show_search, show_movie_pages, show_artist_alpha_list
|
||||
from .server_sessions import show_server_sessions
|
||||
from .action_menu import ActionMenu
|
||||
from .bitrate_dialog import BitrateDialog
|
||||
from .safe_delete_dialog import SafeDeleteDialog
|
||||
from .dialogs import BitrateDialog
|
||||
from .widgets import get_widget_content, get_widget_content_cast, check_for_new_content
|
||||
from . import trakttokodi
|
||||
from .cache_images import CacheArtwork
|
||||
from .dir_functions import get_content, process_directory
|
||||
from .tracking import timer
|
||||
from .skin_cloner import clone_default_skin
|
||||
from .play_utils import play_file
|
||||
|
||||
__addon__ = xbmcaddon.Addon()
|
||||
__addondir__ = xbmc.translatePath(__addon__.getAddonInfo('profile'))
|
||||
__addondir__ = translate_path(__addon__.getAddonInfo('profile'))
|
||||
__cwd__ = __addon__.getAddonInfo('path')
|
||||
PLUGINPATH = xbmc.translatePath(os.path.join(__cwd__))
|
||||
PLUGINPATH = translate_path(os.path.join(__cwd__))
|
||||
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2])
|
||||
|
||||
downloadUtils = DownloadUtils()
|
||||
dataManager = DataManager()
|
||||
user_details = load_user_details()
|
||||
|
||||
|
||||
@timer
|
||||
@@ -62,9 +55,9 @@ def main_entry_point():
|
||||
pr.enable()
|
||||
|
||||
log.debug("Running Python: {0}".format(sys.version_info))
|
||||
log.debug("Running JellyCon: {0}".format(ClientInformation().get_version()))
|
||||
log.debug("Running JellyCon: {0}".format(get_version()))
|
||||
log.debug("Kodi BuildVersion: {0}".format(xbmc.getInfoLabel("System.BuildVersion")))
|
||||
log.debug("Kodi Version: {0}".format(kodi_version))
|
||||
log.debug("Kodi Version: {0}".format(kodi_version()))
|
||||
log.debug("Script argument data: {0}".format(sys.argv))
|
||||
|
||||
params = get_params()
|
||||
@@ -73,9 +66,6 @@ def main_entry_point():
|
||||
request_path = params.get("request_path", None)
|
||||
param_url = params.get('url', None)
|
||||
|
||||
if param_url:
|
||||
param_url = urllib.unquote(param_url)
|
||||
|
||||
mode = params.get("mode", None)
|
||||
|
||||
if len(params) == 1 and request_path and request_path.find("/library/movies") > -1:
|
||||
@@ -101,6 +91,8 @@ def main_entry_point():
|
||||
show_movie_alpha_list(params)
|
||||
elif mode == "TVSHOW_ALPHA":
|
||||
show_tvshow_alpha_list(params)
|
||||
elif mode == "ARTIST_ALPHA":
|
||||
show_artist_alpha_list(params)
|
||||
elif mode == "GENRES":
|
||||
show_genre_list(params)
|
||||
elif mode == "MOVIE_PAGES":
|
||||
@@ -124,11 +116,9 @@ def main_entry_point():
|
||||
elif mode == "WIDGET_CONTENT_CAST":
|
||||
get_widget_content_cast(int(sys.argv[1]), params)
|
||||
elif mode == "SHOW_CONTENT":
|
||||
# plugin://plugin.video.jellycon?mode=SHOW_CONTENT&item_type=Movie|Series
|
||||
check_server()
|
||||
show_content(params)
|
||||
elif mode == "SEARCH":
|
||||
# plugin://plugin.video.jellycon?mode=SEARCH
|
||||
xbmcplugin.setContent(int(sys.argv[1]), 'files')
|
||||
show_search()
|
||||
elif mode == "NEW_SEARCH":
|
||||
@@ -137,8 +127,6 @@ def main_entry_point():
|
||||
search_results_person(params)
|
||||
elif mode == "SHOW_SERVER_SESSIONS":
|
||||
show_server_sessions()
|
||||
elif mode == "TRAKTTOKODI":
|
||||
trakttokodi.entry_point(params)
|
||||
elif mode == "SHOW_ADDON_MENU":
|
||||
display_menu(params)
|
||||
elif mode == "GET_CONTENT_BY_TV_SHOW":
|
||||
@@ -165,14 +153,14 @@ def main_entry_point():
|
||||
|
||||
file_time_stamp = time.strftime("%Y%m%d-%H%M%S")
|
||||
tab_file_name = __addondir__ + "profile(" + file_time_stamp + ").txt"
|
||||
s = StringIO.StringIO()
|
||||
s = StringIO()
|
||||
ps = pstats.Stats(pr, stream=s)
|
||||
ps = ps.sort_stats('cumulative')
|
||||
ps.print_stats()
|
||||
ps.strip_dirs()
|
||||
ps = ps.sort_stats('tottime')
|
||||
ps.print_stats()
|
||||
with open(tab_file_name, 'wb') as f:
|
||||
with open(tab_file_name, 'w') as f:
|
||||
f.write(s.getvalue())
|
||||
|
||||
log.debug("===== JellyCon FINISHED =====")
|
||||
@@ -191,10 +179,10 @@ def __get_parent_id_from(params):
|
||||
show_provider_ids = params.get("show_ids")
|
||||
if show_provider_ids is not None:
|
||||
log.debug("TV show providers IDs: {}".format(show_provider_ids))
|
||||
get_show_url = "{server}/Users/{userid}/Items?fields=MediaStreams&Recursive=true" \
|
||||
get_show_url = "/Users/{}/Items?fields=MediaStreams&Recursive=true" \
|
||||
"&IncludeItemTypes=series&IncludeMedia=true&ImageTypeLimit=1&Limit=16" \
|
||||
"&AnyProviderIdEquals=" + show_provider_ids
|
||||
content = dataManager.get_content(get_show_url)
|
||||
"&AnyProviderIdEquals={}".format(api.user_id, show_provider_ids)
|
||||
content = api.get(get_show_url)
|
||||
show = content.get("Items")
|
||||
if len(show) == 1:
|
||||
result = content.get("Items")[0].get("Id")
|
||||
@@ -210,9 +198,8 @@ def toggle_watched(params):
|
||||
item_id = params.get("item_id", None)
|
||||
if item_id is None:
|
||||
return
|
||||
url = "{server}/Users/{userid}/Items/" + item_id + "?format=json"
|
||||
data_manager = DataManager()
|
||||
result = data_manager.get_content(url)
|
||||
url = "/Users/{}/Items/{}?format=json".format(api.user_id, item_id)
|
||||
result = api.get(url)
|
||||
log.debug("toggle_watched item info: {0}".format(result))
|
||||
|
||||
user_data = result.get("UserData", None)
|
||||
@@ -227,8 +214,8 @@ def toggle_watched(params):
|
||||
|
||||
def mark_item_watched(item_id):
|
||||
log.debug("Mark Item Watched: {0}".format(item_id))
|
||||
url = "{server}/Users/{userid}/PlayedItems/" + item_id
|
||||
downloadUtils.download_url(url, post_body="", method="POST")
|
||||
url = "/Users/{}/PlayedItems/{}".format(user_details.get('user_id'), item_id)
|
||||
api.post(url)
|
||||
check_for_new_content()
|
||||
home_window = HomeWindow()
|
||||
last_url = home_window.get_property("last_content_url")
|
||||
@@ -241,8 +228,8 @@ def mark_item_watched(item_id):
|
||||
|
||||
def mark_item_unwatched(item_id):
|
||||
log.debug("Mark Item UnWatched: {0}".format(item_id))
|
||||
url = "{server}/Users/{userid}/PlayedItems/" + item_id
|
||||
downloadUtils.download_url(url, method="DELETE")
|
||||
url = "/Users/{}/PlayedItems/{}".format(user_details.get('user_id'), item_id)
|
||||
api.delete(url)
|
||||
check_for_new_content()
|
||||
home_window = HomeWindow()
|
||||
last_url = home_window.get_property("last_content_url")
|
||||
@@ -255,8 +242,8 @@ def mark_item_unwatched(item_id):
|
||||
|
||||
def mark_item_favorite(item_id):
|
||||
log.debug("Add item to favourites: {0}".format(item_id))
|
||||
url = "{server}/Users/{userid}/FavoriteItems/" + item_id
|
||||
downloadUtils.download_url(url, post_body="", method="POST")
|
||||
url = "/Users/{}/FavoriteItems/{}".format(user_details.get('user_id'), item_id)
|
||||
api.post(url)
|
||||
check_for_new_content()
|
||||
home_window = HomeWindow()
|
||||
last_url = home_window.get_property("last_content_url")
|
||||
@@ -268,8 +255,8 @@ def mark_item_favorite(item_id):
|
||||
|
||||
def unmark_item_favorite(item_id):
|
||||
log.debug("Remove item from favourites: {0}".format(item_id))
|
||||
url = "{server}/Users/{userid}/FavoriteItems/" + item_id
|
||||
downloadUtils.download_url(url, method="DELETE")
|
||||
url = "/Users/{}/FavoriteItems/{}".format(user_details.get('user_id'), item_id)
|
||||
api.delete(url)
|
||||
check_for_new_content()
|
||||
home_window = HomeWindow()
|
||||
last_url = home_window.get_property("last_content_url")
|
||||
@@ -281,7 +268,7 @@ def unmark_item_favorite(item_id):
|
||||
|
||||
def delete(item_id):
|
||||
|
||||
item = downloadUtils.download_url("{server}/Users/{userid}/Items/" + item_id + "?format=json")
|
||||
item = api.delete("/Users/{}/Items/{}".format(user_details.get('user_id'), item_id))
|
||||
|
||||
item_id = item.get("Id")
|
||||
item_name = item.get("Name", "")
|
||||
@@ -299,16 +286,16 @@ def delete(item_id):
|
||||
final_name += item_name
|
||||
|
||||
if not item.get("CanDelete", False):
|
||||
xbmcgui.Dialog().ok(string_load(30135), string_load(30417), final_name)
|
||||
xbmcgui.Dialog().ok(translate_string(30135), translate_string(30417), final_name)
|
||||
return
|
||||
|
||||
return_value = xbmcgui.Dialog().yesno(string_load(30091), final_name, string_load(30092))
|
||||
return_value = xbmcgui.Dialog().yesno(translate_string(30091), '{}\n{}'.format(final_name, translate_string(30092)))
|
||||
if return_value:
|
||||
log.debug('Deleting Item: {0}'.format(item_id))
|
||||
url = '{server}/Items/' + item_id
|
||||
url = '/Items/{}'.format(item_id)
|
||||
progress = xbmcgui.DialogProgress()
|
||||
progress.create(string_load(30052), string_load(30053))
|
||||
downloadUtils.download_url(url, method="DELETE")
|
||||
progress.create(translate_string(30052), translate_string(30053))
|
||||
api.delete(url)
|
||||
progress.close()
|
||||
check_for_new_content()
|
||||
home_window = HomeWindow()
|
||||
@@ -320,6 +307,9 @@ def delete(item_id):
|
||||
|
||||
|
||||
def get_params():
|
||||
'''
|
||||
Retrieve the request data from Kodi
|
||||
'''
|
||||
|
||||
plugin_path = sys.argv[0]
|
||||
paramstring = sys.argv[2]
|
||||
@@ -327,27 +317,12 @@ def get_params():
|
||||
log.debug("Parameter string: {0}".format(paramstring))
|
||||
log.debug("Plugin Path string: {0}".format(plugin_path))
|
||||
|
||||
param = {}
|
||||
param = dict(parse_qsl(paramstring[1:]))
|
||||
|
||||
# add plugin path
|
||||
request_path = plugin_path.replace("plugin://plugin.video.jellycon", "")
|
||||
param["request_path"] = request_path
|
||||
|
||||
if len(paramstring) >= 2:
|
||||
if paramstring[0] == "?":
|
||||
paramstring = paramstring[1:]
|
||||
|
||||
if paramstring[len(paramstring) - 1] == '/':
|
||||
paramstring = paramstring[0:len(paramstring) - 2]
|
||||
|
||||
pairsofparams = paramstring.split('&')
|
||||
for i in range(len(pairsofparams)):
|
||||
splitparams = pairsofparams[i].split('=')
|
||||
if (len(splitparams)) == 2:
|
||||
param[splitparams[0]] = splitparams[1]
|
||||
elif (len(splitparams)) == 3:
|
||||
param[splitparams[0]] = splitparams[1] + "=" + splitparams[2]
|
||||
|
||||
log.debug("JellyCon -> Detected parameters: {0}".format(param))
|
||||
return param
|
||||
|
||||
@@ -359,9 +334,8 @@ def show_menu(params):
|
||||
settings = xbmcaddon.Addon()
|
||||
item_id = params["item_id"]
|
||||
|
||||
url = "{server}/Users/{userid}/Items/" + item_id + "?format=json"
|
||||
data_manager = DataManager()
|
||||
result = data_manager.get_content(url)
|
||||
url = "/Users/{}/Items/{}?format=json".format(api.user_id, item_id)
|
||||
result = api.get(url)
|
||||
log.debug("Menu item info: {0}".format(result))
|
||||
|
||||
if result is None:
|
||||
@@ -369,43 +343,43 @@ def show_menu(params):
|
||||
|
||||
action_items = []
|
||||
|
||||
if result["Type"] in ["Episode", "Movie", "Music", "Video", "Audio", "TvChannel", "Program"]:
|
||||
li = xbmcgui.ListItem(string_load(30314))
|
||||
if result["Type"] in ["Episode", "Movie", "Music", "Video", "Audio", "TvChannel", "Program", "MusicVideo"]:
|
||||
li = xbmcgui.ListItem(translate_string(30314), offscreen=True)
|
||||
li.setProperty('menu_id', 'play')
|
||||
action_items.append(li)
|
||||
|
||||
if result["Type"] in ["Season", "MusicAlbum", "Playlist"]:
|
||||
li = xbmcgui.ListItem(string_load(30317))
|
||||
if result["Type"] in ["Season", "MusicArtist", "MusicAlbum", "Playlist"]:
|
||||
li = xbmcgui.ListItem(translate_string(30317), offscreen=True)
|
||||
li.setProperty('menu_id', 'play_all')
|
||||
action_items.append(li)
|
||||
|
||||
if result["Type"] in ["Episode", "Movie", "Video", "TvChannel", "Program"]:
|
||||
li = xbmcgui.ListItem(string_load(30275))
|
||||
if result["Type"] in ["Episode", "Movie", "Video", "TvChannel", "Program", "MusicVideo"]:
|
||||
li = xbmcgui.ListItem(translate_string(30275), offscreen=True)
|
||||
li.setProperty('menu_id', 'transcode')
|
||||
action_items.append(li)
|
||||
|
||||
if result["Type"] in ["Episode", "Movie", "Music", "Video", "Audio"]:
|
||||
li = xbmcgui.ListItem(string_load(30402))
|
||||
if result["Type"] in ["Episode", "Movie", "Music", "Video", "Audio", "MusicArtist", "MusicAlbum", "MusicVideo"]:
|
||||
li = xbmcgui.ListItem(translate_string(30402), offscreen=True)
|
||||
li.setProperty('menu_id', 'add_to_playlist')
|
||||
action_items.append(li)
|
||||
|
||||
if result["Type"] in ("Movie", "Series"):
|
||||
li = xbmcgui.ListItem(string_load(30307))
|
||||
li = xbmcgui.ListItem(translate_string(30307), offscreen=True)
|
||||
li.setProperty('menu_id', 'play_trailer')
|
||||
action_items.append(li)
|
||||
|
||||
if result["Type"] == "Episode" and result["ParentId"] is not None:
|
||||
li = xbmcgui.ListItem(string_load(30327))
|
||||
li = xbmcgui.ListItem(translate_string(30327), offscreen=True)
|
||||
li.setProperty('menu_id', 'view_season')
|
||||
action_items.append(li)
|
||||
|
||||
if result["Type"] in ("Series", "Season", "Episode"):
|
||||
li = xbmcgui.ListItem(string_load(30354))
|
||||
li = xbmcgui.ListItem(translate_string(30354), offscreen=True)
|
||||
li.setProperty('menu_id', 'view_series')
|
||||
action_items.append(li)
|
||||
|
||||
if result["Type"] == "Movie":
|
||||
li = xbmcgui.ListItem("Show Extras")
|
||||
li = xbmcgui.ListItem("Show Extras", offscreen=True)
|
||||
li.setProperty('menu_id', 'show_extras')
|
||||
action_items.append(li)
|
||||
|
||||
@@ -414,49 +388,43 @@ def show_menu(params):
|
||||
progress = user_data.get("PlaybackPositionTicks", 0) != 0
|
||||
played = user_data.get("Played", False)
|
||||
if not played or progress:
|
||||
li = xbmcgui.ListItem(string_load(30270))
|
||||
li = xbmcgui.ListItem(translate_string(30270), offscreen=True)
|
||||
li.setProperty('menu_id', 'mark_watched')
|
||||
action_items.append(li)
|
||||
if played or progress:
|
||||
li = xbmcgui.ListItem(string_load(30271))
|
||||
li = xbmcgui.ListItem(translate_string(30271), offscreen=True)
|
||||
li.setProperty('menu_id', 'mark_unwatched')
|
||||
action_items.append(li)
|
||||
|
||||
if user_data.get("IsFavorite", False) is False:
|
||||
li = xbmcgui.ListItem(string_load(30272))
|
||||
li = xbmcgui.ListItem(translate_string(30272), offscreen=True)
|
||||
li.setProperty('menu_id', 'jellyfin_set_favorite')
|
||||
action_items.append(li)
|
||||
else:
|
||||
li = xbmcgui.ListItem(string_load(30273))
|
||||
li = xbmcgui.ListItem(translate_string(30273), offscreen=True)
|
||||
li.setProperty('menu_id', 'jellyfin_unset_favorite')
|
||||
action_items.append(li)
|
||||
|
||||
can_delete = result.get("CanDelete", False)
|
||||
if can_delete:
|
||||
li = xbmcgui.ListItem(string_load(30274))
|
||||
li = xbmcgui.ListItem(translate_string(30274), offscreen=True)
|
||||
li.setProperty('menu_id', 'delete')
|
||||
action_items.append(li)
|
||||
|
||||
safe_delete = home_window.get_property("safe_delete_plugin_available") == "true"
|
||||
if safe_delete:
|
||||
li = xbmcgui.ListItem("Safe Delete")
|
||||
li.setProperty('menu_id', 'safe_delete')
|
||||
action_items.append(li)
|
||||
|
||||
li = xbmcgui.ListItem(string_load(30398))
|
||||
li = xbmcgui.ListItem(translate_string(30398), offscreen=True)
|
||||
li.setProperty('menu_id', 'refresh_server')
|
||||
action_items.append(li)
|
||||
|
||||
li = xbmcgui.ListItem(string_load(30281))
|
||||
li = xbmcgui.ListItem(translate_string(30281), offscreen=True)
|
||||
li.setProperty('menu_id', 'refresh_images')
|
||||
action_items.append(li)
|
||||
|
||||
if result["Type"] in ["Movie", "Series"]:
|
||||
li = xbmcgui.ListItem(string_load(30399))
|
||||
li = xbmcgui.ListItem(translate_string(30399), offscreen=True)
|
||||
li.setProperty('menu_id', 'hide')
|
||||
action_items.append(li)
|
||||
|
||||
li = xbmcgui.ListItem(string_load(30401))
|
||||
li = xbmcgui.ListItem(translate_string(30401), offscreen=True)
|
||||
li.setProperty('menu_id', 'info')
|
||||
action_items.append(li)
|
||||
|
||||
@@ -470,16 +438,14 @@ def show_menu(params):
|
||||
|
||||
if container_content_type in ["movies", "tvshows", "seasons", "episodes", "sets"]:
|
||||
if view_match:
|
||||
li = xbmcgui.ListItem("Unset as default view")
|
||||
li = xbmcgui.ListItem("Unset as default view", offscreen=True)
|
||||
li.setProperty('menu_id', 'unset_view')
|
||||
action_items.append(li)
|
||||
else:
|
||||
li = xbmcgui.ListItem("Set as default view")
|
||||
li = xbmcgui.ListItem("Set as default view", offscreen=True)
|
||||
li.setProperty('menu_id', 'set_view')
|
||||
action_items.append(li)
|
||||
|
||||
# xbmcplugin.endOfDirectory(int(sys.argv[1]), cacheToDisc=False)
|
||||
|
||||
action_menu = ActionMenu("ActionMenu.xml", PLUGINPATH, "default", "720p")
|
||||
action_menu.setActionItems(action_items)
|
||||
action_menu.doModal()
|
||||
@@ -492,9 +458,6 @@ def show_menu(params):
|
||||
|
||||
if selected_action == "play":
|
||||
log.debug("Play Item")
|
||||
# list_item = populate_listitem(params["item_id"])
|
||||
# result = xbmcgui.Dialog().info(list_item)
|
||||
# log.debug("xbmcgui.Dialog().info: {0}", result)
|
||||
play_action(params)
|
||||
|
||||
elif selected_action == "set_view":
|
||||
@@ -506,22 +469,22 @@ def show_menu(params):
|
||||
settings.setSetting(view_key, "")
|
||||
|
||||
elif selected_action == "refresh_server":
|
||||
url = ("{server}/Items/" + item_id + "/Refresh" +
|
||||
url = ("/Items/" + item_id + "/Refresh" +
|
||||
"?Recursive=true" +
|
||||
"&ImageRefreshMode=FullRefresh" +
|
||||
"&MetadataRefreshMode=FullRefresh" +
|
||||
"&ReplaceAllImages=true" +
|
||||
"&ReplaceAllMetadata=true")
|
||||
res = downloadUtils.download_url(url, post_body="", method="POST")
|
||||
res = api.post(url)
|
||||
log.debug("Refresh Server Responce: {0}".format(res))
|
||||
|
||||
elif selected_action == "hide":
|
||||
user_details = load_user_details(settings)
|
||||
user_name = user_details["username"]
|
||||
user_details = load_user_details()
|
||||
user_name = user_details["user_name"]
|
||||
hide_tag_string = "hide-" + user_name
|
||||
url = "{server}/Items/" + item_id + "/Tags/Add"
|
||||
url = "/Items/{}/Tags/Add".format(item_id)
|
||||
post_tag_data = {"Tags": [{"Name": hide_tag_string}]}
|
||||
res = downloadUtils.download_url(url, post_body=post_tag_data, method="POST")
|
||||
res = api.post(url, post_tag_data)
|
||||
log.debug("Add Tag Responce: {0}".format(res))
|
||||
|
||||
check_for_new_content()
|
||||
@@ -575,64 +538,9 @@ def show_menu(params):
|
||||
elif selected_action == "delete":
|
||||
delete(item_id)
|
||||
|
||||
elif selected_action == "safe_delete":
|
||||
url = "{server}/jellyfin_safe_delete/delete_item/" + item_id
|
||||
result = downloadUtils.download_url(url)
|
||||
dialog = xbmcgui.Dialog()
|
||||
if result:
|
||||
log.debug("Safe_Delete_Action: {0}".format(result))
|
||||
action_token = result["action_token"]
|
||||
|
||||
message = "You are about to delete the following item[CR][CR]"
|
||||
|
||||
message += "Type: " + result["item_info"]["Item_type"] + "[CR]"
|
||||
|
||||
if result["item_info"]["Item_type"] == "Series":
|
||||
message += "Name: " + result["item_info"]["item_name"] + "[CR]"
|
||||
elif result["item_info"]["Item_type"] == "Season":
|
||||
message += "Season: " + str(result["item_info"]["season_number"]) + "[CR]"
|
||||
message += "Name: " + result["item_info"]["season_name"] + "[CR]"
|
||||
elif result["item_info"]["Item_type"] == "Episode":
|
||||
message += "Series: " + result["item_info"]["series_name"] + "[CR]"
|
||||
message += "Season: " + result["item_info"]["season_name"] + "[CR]"
|
||||
message += "Episode: " + str(result["item_info"]["episode_number"]) + "[CR]"
|
||||
message += "Name: " + result["item_info"]["item_name"] + "[CR]"
|
||||
else:
|
||||
message += "Name: " + result["item_info"]["item_name"] + "[CR]"
|
||||
|
||||
message += "[CR]File List[CR][CR]"
|
||||
|
||||
for file_info in result["file_list"]:
|
||||
message += " - " + file_info["Key"] + " (" + convert_size(file_info["Value"]) + ")[CR]"
|
||||
message += "[CR][CR]Are you sure?[CR][CR]"
|
||||
|
||||
confirm_dialog = SafeDeleteDialog("SafeDeleteDialog.xml", PLUGINPATH, "default", "720p")
|
||||
confirm_dialog.message = message
|
||||
confirm_dialog.heading = "Confirm delete files?"
|
||||
confirm_dialog.doModal()
|
||||
log.debug("safe_delete_confirm_dialog: {0}".format(confirm_dialog.confirm))
|
||||
|
||||
if confirm_dialog.confirm:
|
||||
url = "{server}/jellyfin_safe_delete/delete_item_action"
|
||||
playback_info = {
|
||||
'item_id': item_id,
|
||||
'action_token': action_token
|
||||
}
|
||||
delete_action = downloadUtils.download_url(url, method="POST", post_body=playback_info)
|
||||
log.debug("Delete result action: {0}".format(delete_action))
|
||||
if not delete_action:
|
||||
dialog.ok("Error", "Error deleting files", "Error in responce from server")
|
||||
elif not delete_action.get("result"):
|
||||
dialog.ok("Error", "Error deleting files", delete_action["message"])
|
||||
else:
|
||||
dialog.ok("Deleted", "Files deleted")
|
||||
else:
|
||||
dialog.ok("Error", "Error getting safe delete confirmation")
|
||||
|
||||
elif selected_action == "show_extras":
|
||||
# "http://localhost:8096/Users/3138bed521e5465b9be26d2c63be94af/Items/78/SpecialFeatures"
|
||||
u = "{server}/Users/{userid}/Items/" + item_id + "/SpecialFeatures"
|
||||
action_url = ("plugin://plugin.video.jellycon/?url=" + urllib.quote(u) + "&mode=GET_CONTENT&media_type=Videos")
|
||||
u = "/Users/{}/Items/{}/SpecialFeatures".format(api.user_id, item_id)
|
||||
action_url = ("plugin://plugin.video.jellycon/?url=" + quote(u) + "&mode=GET_CONTENT&media_type=Videos")
|
||||
built_in_command = 'ActivateWindow(Videos, ' + action_url + ', return)'
|
||||
xbmc.executebuiltin(built_in_command)
|
||||
|
||||
@@ -640,15 +548,15 @@ def show_menu(params):
|
||||
xbmc.executebuiltin("Dialog.Close(all,true)")
|
||||
parent_id = result["ParentId"]
|
||||
series_id = result["SeriesId"]
|
||||
u = ('{server}/Shows/' + series_id +
|
||||
u = ('/Shows/' + series_id +
|
||||
'/Episodes'
|
||||
'?userId={userid}' +
|
||||
'?userId={}'.format(api.user_id) +
|
||||
'&seasonId=' + parent_id +
|
||||
'&IsVirtualUnAired=false' +
|
||||
'&IsMissing=false' +
|
||||
'&Fields=SpecialEpisodeNumbers,{field_filters}' +
|
||||
'&Fields=SpecialEpisodeNumbers,{}'.format(get_default_filters()) +
|
||||
'&format=json')
|
||||
action_url = ("plugin://plugin.video.jellycon/?url=" + urllib.quote(u) + "&mode=GET_CONTENT&media_type=Season")
|
||||
action_url = ("plugin://plugin.video.jellycon/?url=" + quote(u) + "&mode=GET_CONTENT&media_type=Season")
|
||||
built_in_command = 'ActivateWindow(Videos, ' + action_url + ', return)'
|
||||
xbmc.executebuiltin(built_in_command)
|
||||
|
||||
@@ -659,18 +567,17 @@ def show_menu(params):
|
||||
if not series_id:
|
||||
series_id = item_id
|
||||
|
||||
u = ('{server}/Shows/' + series_id +
|
||||
u = ('/Shows/' + series_id +
|
||||
'/Seasons'
|
||||
'?userId={userid}' +
|
||||
'&Fields={field_filters}' +
|
||||
'?userId={}'.format(api.user_id) +
|
||||
'&Fields={}'.format(get_default_filters()) +
|
||||
'&format=json')
|
||||
|
||||
action_url = ("plugin://plugin.video.jellycon/?url=" + urllib.quote(u) + "&mode=GET_CONTENT&media_type=Series")
|
||||
action_url = ("plugin://plugin.video.jellycon/?url=" + quote(u) + "&mode=GET_CONTENT&media_type=Series")
|
||||
|
||||
if xbmc.getCondVisibility("Window.IsActive(home)"):
|
||||
built_in_command = 'ActivateWindow(Videos, ' + action_url + ', return)'
|
||||
else:
|
||||
# built_in_command = 'Container.Update(' + action_url + ', replace)'
|
||||
built_in_command = 'Container.Update(' + action_url + ')'
|
||||
|
||||
xbmc.executebuiltin(built_in_command)
|
||||
@@ -683,40 +590,6 @@ def show_menu(params):
|
||||
xbmc.executebuiltin("Action(info)")
|
||||
|
||||
|
||||
def populate_listitem(item_id):
|
||||
log.debug("populate_listitem: {0}".format(item_id))
|
||||
|
||||
url = "{server}/Users/{userid}/Items/" + item_id + "?format=json"
|
||||
result = downloadUtils.download_url(url)
|
||||
log.debug("populate_listitem item info: {0}".format(result))
|
||||
|
||||
item_title = result.get("Name", string_load(30280))
|
||||
|
||||
list_item = xbmcgui.ListItem(label=item_title)
|
||||
|
||||
server = downloadUtils.get_server()
|
||||
|
||||
art = get_art(result, server=server)
|
||||
list_item.setIconImage(art['thumb']) # back compat
|
||||
list_item.setProperty('fanart_image', art['fanart']) # back compat
|
||||
list_item.setProperty('discart', art['discart']) # not avail to setArt
|
||||
list_item.setArt(art)
|
||||
|
||||
list_item.setProperty('IsPlayable', 'false')
|
||||
list_item.setProperty('IsFolder', 'false')
|
||||
list_item.setProperty('id', result.get("Id"))
|
||||
|
||||
# play info
|
||||
details = {
|
||||
'title': item_title,
|
||||
'plot': result.get("Overview")
|
||||
}
|
||||
|
||||
list_item.setInfo("Video", infoLabels=details)
|
||||
|
||||
return list_item
|
||||
|
||||
|
||||
def show_content(params):
|
||||
log.debug("showContent Called: {0}".format(params))
|
||||
|
||||
@@ -727,11 +600,11 @@ def show_content(params):
|
||||
if item_type.lower().find("movie") == -1:
|
||||
group_movies = False
|
||||
|
||||
content_url = ("{server}/Users/{userid}/Items" +
|
||||
content_url = ("/Users/{}/Items".format(api.user_id) +
|
||||
"?format=json" +
|
||||
"&ImageTypeLimit=1" +
|
||||
"&IsMissing=False" +
|
||||
"&Fields={field_filters}" +
|
||||
"&Fields={}".format(get_default_filters()) +
|
||||
'&CollapseBoxSetItems=' + str(group_movies) +
|
||||
'&GroupItemsIntoCollections=' + str(group_movies) +
|
||||
"&Recursive=true" +
|
||||
@@ -749,11 +622,10 @@ def search_results_person(params):
|
||||
handle = int(sys.argv[1])
|
||||
|
||||
person_id = params.get("person_id")
|
||||
details_url = ('{server}/Users/{userid}/items' +
|
||||
details_url = ('/Users/{}/Items'.format(api.user_id) +
|
||||
'?PersonIds=' + person_id +
|
||||
# '&IncludeItemTypes=Movie' +
|
||||
'&Recursive=true' +
|
||||
'&Fields={field_filters}' +
|
||||
'&Fields={}'.format(get_default_filters()) +
|
||||
'&format=json')
|
||||
|
||||
params["name_format"] = "Episode|episode_name_format"
|
||||
@@ -780,8 +652,6 @@ def search_results_person(params):
|
||||
if content_type:
|
||||
xbmcplugin.setContent(handle, content_type)
|
||||
|
||||
# xbmcplugin.setContent(handle, detected_type)
|
||||
|
||||
if dir_items is not None:
|
||||
xbmcplugin.addDirectoryItems(handle, dir_items)
|
||||
|
||||
@@ -794,19 +664,19 @@ def search_results(params):
|
||||
query_string = params.get('query')
|
||||
if query_string:
|
||||
log.debug("query_string : {0}".format(query_string))
|
||||
query_string = urllib.unquote(query_string)
|
||||
query_string = unquote(query_string)
|
||||
log.debug("query_string : {0}".format(query_string))
|
||||
|
||||
item_type = item_type.lower()
|
||||
|
||||
if item_type == 'movie':
|
||||
heading_type = string_load(30231)
|
||||
heading_type = translate_string(30231)
|
||||
content_type = 'movies'
|
||||
elif item_type == 'series':
|
||||
heading_type = string_load(30229)
|
||||
heading_type = translate_string(30229)
|
||||
content_type = 'tvshows'
|
||||
elif item_type == 'episode':
|
||||
heading_type = string_load(30235)
|
||||
heading_type = translate_string(30235)
|
||||
content_type = 'episodes'
|
||||
params["name_format"] = "Episode|episode_name_format"
|
||||
elif item_type == "music" or item_type == "audio" or item_type == "musicalbum":
|
||||
@@ -825,7 +695,7 @@ def search_results(params):
|
||||
home_window = HomeWindow()
|
||||
last_search = home_window.get_property("last_search")
|
||||
kb = xbmc.Keyboard()
|
||||
kb.setHeading(heading_type.capitalize() + ' ' + string_load(30246).lower())
|
||||
kb.setHeading(heading_type.capitalize() + ' ' + translate_string(30246).lower())
|
||||
kb.setDefault(last_search)
|
||||
kb.doModal()
|
||||
|
||||
@@ -841,7 +711,7 @@ def search_results(params):
|
||||
else:
|
||||
query = query_string
|
||||
|
||||
query = urllib.quote(query)
|
||||
query = quote(query)
|
||||
log.debug("query : {0}".format(query))
|
||||
|
||||
if (not item_type) or (not query):
|
||||
@@ -852,12 +722,12 @@ def search_results(params):
|
||||
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))
|
||||
|
||||
# what type of search
|
||||
if item_type == "person":
|
||||
search_url = ("{server}/Persons" +
|
||||
search_url = ("/Persons" +
|
||||
"?searchTerm=" + query +
|
||||
"&IncludePeople=true" +
|
||||
"&IncludeMedia=false" +
|
||||
@@ -869,27 +739,25 @@ def search_results(params):
|
||||
"&Recursive=true" +
|
||||
"&EnableTotalRecordCount=false" +
|
||||
"&ImageTypeLimit=1" +
|
||||
"&userId={userid}")
|
||||
"&userId={}".format(api.user_id))
|
||||
|
||||
person_search_results = dataManager.get_content(search_url)
|
||||
person_search_results = api.get(search_url)
|
||||
log.debug("Person Search Result : {0}".format(person_search_results))
|
||||
if person_search_results is None:
|
||||
return
|
||||
|
||||
person_items = person_search_results.get("Items", [])
|
||||
|
||||
server = downloadUtils.get_server()
|
||||
server = settings.getSetting('server_address')
|
||||
list_items = []
|
||||
for item in person_items:
|
||||
person_id = item.get('Id')
|
||||
person_name = item.get('Name')
|
||||
# image_tags = item.get('ImageTags', {})
|
||||
# image_tag = image_tags.get('PrimaryImageTag', '')
|
||||
person_thumbnail = downloadUtils.get_artwork(item, "Primary", server=server)
|
||||
person_thumbnail = get_art_url(item, "Primary", server=server)
|
||||
|
||||
action_url = sys.argv[0] + "?mode=NEW_SEARCH_PERSON&person_id=" + person_id
|
||||
|
||||
list_item = xbmcgui.ListItem(label=person_name)
|
||||
list_item = xbmcgui.ListItem(label=person_name, offscreen=True)
|
||||
list_item.setProperty("id", person_id)
|
||||
|
||||
art_links = {}
|
||||
@@ -907,7 +775,7 @@ def search_results(params):
|
||||
xbmcplugin.endOfDirectory(handle, cacheToDisc=False)
|
||||
|
||||
else:
|
||||
search_url = ("{server}/Users/{userid}/Items" +
|
||||
search_url = ("/Users/{}/Items".format(api.user_id) +
|
||||
"?searchTerm=" + query +
|
||||
"&IncludePeople=false" +
|
||||
"&IncludeMedia=true" +
|
||||
@@ -916,7 +784,7 @@ def search_results(params):
|
||||
"&IncludeArtists=false" +
|
||||
"&IncludeItemTypes=" + item_type +
|
||||
"&Limit=16" +
|
||||
"&Fields={field_filters}" +
|
||||
"&Fields={}".format(get_default_filters()) +
|
||||
"&Recursive=true" +
|
||||
"&EnableTotalRecordCount=false" +
|
||||
"&ImageTypeLimit=1")
|
||||
@@ -928,7 +796,7 @@ def search_results(params):
|
||||
xbmcplugin.endOfDirectory(handle, cacheToDisc=False)
|
||||
|
||||
if progress is not None:
|
||||
progress.update(100, string_load(30125))
|
||||
progress.update(100, translate_string(30125))
|
||||
progress.close()
|
||||
|
||||
|
||||
@@ -977,15 +845,15 @@ def play_action(params):
|
||||
play_info["subtitle_stream_index"] = subtitle_stream_index
|
||||
play_info["audio_stream_index"] = audio_stream_index
|
||||
log.info("Sending jellycon_play_action : {0}".format(play_info))
|
||||
send_event_notification("jellycon_play_action", play_info)
|
||||
play_file(play_info)
|
||||
|
||||
|
||||
def play_item_trailer(item_id):
|
||||
log.debug("== ENTER: playTrailer ==")
|
||||
|
||||
url = ("{server}/Users/{userid}/Items/%s/LocalTrailers?format=json" % item_id)
|
||||
url = "/Users/{}/Items/{}/LocalTrailers?format=json".format(user_details.get('user_id'), item_id)
|
||||
|
||||
result = downloadUtils.download_url(url)
|
||||
result = api.get(url)
|
||||
|
||||
if result is None:
|
||||
return
|
||||
@@ -1008,8 +876,8 @@ def play_item_trailer(item_id):
|
||||
trailer_names.append(name)
|
||||
trailer_list.append(info)
|
||||
|
||||
url = ("{server}/Users/{userid}/Items/%s?format=json&Fields=RemoteTrailers" % item_id)
|
||||
result = downloadUtils.download_url(url)
|
||||
url = "/Users/{}/Items/{}?format=json&Fields=RemoteTrailers".format(user_details.get('user_id'), item_id)
|
||||
result = api.get(url)
|
||||
log.debug("RemoteTrailers: {0}".format(result))
|
||||
count = 1
|
||||
|
||||
@@ -1039,7 +907,7 @@ def play_item_trailer(item_id):
|
||||
trailer_text.append(name)
|
||||
|
||||
dialog = xbmcgui.Dialog()
|
||||
resp = dialog.select(string_load(30308), trailer_text)
|
||||
resp = dialog.select(translate_string(30308), trailer_text)
|
||||
if resp > -1:
|
||||
trailer = trailer_list[resp]
|
||||
log.debug("SelectedTrailer: {0}".format(trailer))
|
||||
@@ -1054,8 +922,4 @@ def play_item_trailer(item_id):
|
||||
youtube_plugin = "RunPlugin(plugin://plugin.video.youtube/play/?video_id=%s)" % youtube_id
|
||||
log.debug("youtube_plugin: {0}".format(youtube_plugin))
|
||||
|
||||
# play_info = {}
|
||||
# play_info["url"] = youtube_plugin
|
||||
# log.info("Sending jellycon_play_trailer_action : {0}", play_info)
|
||||
# send_event_notification("jellycon_play_youtube_trailer_action", play_info)
|
||||
xbmc.executebuiltin(youtube_plugin)
|
||||
|
||||
@@ -2,26 +2,28 @@ from __future__ import division, absolute_import, print_function, unicode_litera
|
||||
|
||||
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 requests
|
||||
import io
|
||||
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
from .loghandler import LazyLogger
|
||||
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
|
||||
@@ -30,21 +32,11 @@ 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")
|
||||
@@ -59,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:
|
||||
@@ -86,7 +77,7 @@ def build_image(path):
|
||||
if request_path == "favicon.ico":
|
||||
return []
|
||||
|
||||
decoded_url = base64.b64decode(request_path)
|
||||
decoded_url = ensure_text(base64.b64decode(request_path))
|
||||
log.debug("decoded_url : {0}".format(decoded_url))
|
||||
|
||||
image_urls = get_image_links(decoded_url)
|
||||
@@ -109,8 +100,6 @@ def build_image(path):
|
||||
|
||||
host_name = url_bits.hostname
|
||||
port = url_bits.port
|
||||
# user_name = url_bits.username
|
||||
# user_password = url_bits.password
|
||||
url_path = url_bits.path
|
||||
url_query = url_bits.query
|
||||
|
||||
@@ -182,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()
|
||||
@@ -198,20 +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):
|
||||
log.debug("HttpImageServerThread:stop called")
|
||||
self.keep_running = False
|
||||
self.server.shutdown()
|
||||
|
||||
def run(self):
|
||||
log.debug("HttpImageServerThread:started")
|
||||
server = HTTPServer(('', PORT_NUMBER), HttpImageHandler)
|
||||
self.server = HTTPServer(('', PORT_NUMBER), HttpImageHandler)
|
||||
|
||||
while self.keep_running:
|
||||
server.handle_request()
|
||||
self.server.serve_forever()
|
||||
xbmc.sleep(1000)
|
||||
|
||||
log.debug("HttpImageServerThread:exiting")
|
||||
|
||||
@@ -1,31 +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 .loghandler import LazyLogger
|
||||
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 = LazyLogger(__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()
|
||||
|
||||
|
||||
class ItemDetails:
|
||||
@@ -178,7 +165,7 @@ def extract_item_info(item, gui_options):
|
||||
name_info["SeasonIndex"] = u"%02d" % item_details.season_number
|
||||
name_info["EpisodeIndex"] = u"%02d" % item_details.episode_number
|
||||
log.debug("FormatName: {0} | {1}".format(name_format, name_info))
|
||||
item_details.name = unicode(name_format).format(**name_info).strip()
|
||||
item_details.name = ensure_text(name_format).format(**name_info).strip()
|
||||
|
||||
year = item.get("ProductionYear")
|
||||
prem_date = item.get("PremiereDate")
|
||||
@@ -256,16 +243,14 @@ def extract_item_info(item, gui_options):
|
||||
elif person_type == "Writing":
|
||||
item_details.writer = person["Name"]
|
||||
elif person_type == "Actor":
|
||||
# log.debug("Person: {0}", person)
|
||||
person_name = person.get("Name")
|
||||
person_role = person.get("Role")
|
||||
person_id = person.get("Id")
|
||||
person_tag = person.get("PrimaryImageTag")
|
||||
if person_tag:
|
||||
person_thumbnail = download_utils.image_url(person_id,
|
||||
"Primary", 0, 400, 400,
|
||||
person_tag,
|
||||
server=gui_options["server"])
|
||||
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}
|
||||
@@ -283,7 +268,6 @@ def extract_item_info(item, gui_options):
|
||||
|
||||
# production location
|
||||
prod_location = item.get("ProductionLocations", [])
|
||||
# log.debug("ProductionLocations : {0}", prod_location)
|
||||
if prod_location:
|
||||
item_details.production_location = prod_location[0]
|
||||
|
||||
@@ -318,7 +302,7 @@ def extract_item_info(item, gui_options):
|
||||
|
||||
runtime = item.get("RunTimeTicks")
|
||||
if item_details.is_folder is False and runtime:
|
||||
item_details.duration = long(runtime) / 10000000
|
||||
item_details.duration = runtime / 10000000
|
||||
|
||||
child_count = item.get("ChildCount")
|
||||
if child_count:
|
||||
@@ -358,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
|
||||
|
||||
@@ -370,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:
|
||||
@@ -421,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"
|
||||
@@ -444,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 = {}
|
||||
|
||||
@@ -476,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:
|
||||
@@ -499,8 +480,8 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
|
||||
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)
|
||||
|
||||
@@ -522,6 +503,8 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
|
||||
mediatype = 'artist'
|
||||
elif item_type == 'audio' or item_type == 'music':
|
||||
mediatype = 'song'
|
||||
elif item_type == 'musicvideo':
|
||||
mediatype = 'musicvideo'
|
||||
|
||||
info_labels["mediatype"] = mediatype
|
||||
|
||||
@@ -568,7 +551,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:
|
||||
@@ -595,7 +577,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:
|
||||
@@ -606,7 +587,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)
|
||||
@@ -616,11 +596,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
|
||||
|
||||
193
resources/lib/jellyfin.py
Normal file
@@ -0,0 +1,193 @@
|
||||
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 or self.token not in self.headers:
|
||||
self.create_headers(True)
|
||||
|
||||
# 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 or self.token not in self.headers:
|
||||
self.create_headers(True)
|
||||
|
||||
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 or self.token not in self.headers:
|
||||
self.create_headers(True)
|
||||
|
||||
url = '{}{}'.format(self.server, url)
|
||||
|
||||
requests.delete(url, headers=self.headers, verify=self.verify_cert)
|
||||
|
||||
def authenticate(self, auth_data):
|
||||
# Always force create fresh headers during authentication
|
||||
self.create_headers(True)
|
||||
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, force=False):
|
||||
|
||||
# If the headers already exist with an auth token, return unless we're regenerating
|
||||
if self.headers and 'x-mediabrowser-token' in self.headers and force is False:
|
||||
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 unless we're regenerating
|
||||
if self.token and force is False:
|
||||
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')
|
||||
)
|
||||
@@ -1,14 +1,12 @@
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcplugin
|
||||
import xbmcaddon
|
||||
|
||||
import sys
|
||||
import json
|
||||
|
||||
from .loghandler import LazyLogger
|
||||
from .lazylogger import LazyLogger
|
||||
|
||||
log = LazyLogger(__name__)
|
||||
addon = xbmcaddon.Addon()
|
||||
@@ -26,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}".format(version, version_data))
|
||||
except:
|
||||
version = 0.0
|
||||
log.error("Version Error : RAW Version Data: {0}".format(result))
|
||||
|
||||
return version
|
||||
|
||||
19
resources/lib/lazylogger.py
Normal 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)
|
||||
@@ -10,12 +10,13 @@ import traceback
|
||||
|
||||
from six import ensure_text
|
||||
from kodi_six import xbmc, xbmcaddon
|
||||
from urlparse import urlparse
|
||||
|
||||
from .utils import translate_path
|
||||
|
||||
##################################################################################################
|
||||
|
||||
__addon__ = xbmcaddon.Addon(id='plugin.video.jellycon')
|
||||
__pluginpath__ = xbmc.translatePath(__addon__.getAddonInfo('path'))
|
||||
__pluginpath__ = translate_path(__addon__.getAddonInfo('path'))
|
||||
|
||||
##################################################################################################
|
||||
|
||||
@@ -48,7 +49,13 @@ class LogHandler(logging.StreamHandler):
|
||||
# Hide server URL in logs
|
||||
string = string.replace(self.server or "{server}", "{jellyfin-server}")
|
||||
|
||||
xbmc.log(string, level=xbmc.LOGNOTICE)
|
||||
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):
|
||||
|
||||
@@ -105,22 +112,6 @@ class MyFormatter(logging.Formatter):
|
||||
record.relpath = os.path.relpath(record.pathname, __pluginpath__)
|
||||
|
||||
|
||||
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:
|
||||
self.__logger = getLogger(self.__logger_name)
|
||||
return getattr(self.__logger, name)
|
||||
|
||||
|
||||
def get_filesystem_encoding():
|
||||
enc = sys.getfilesystemencoding()
|
||||
|
||||
|
||||
@@ -4,14 +4,47 @@ import threading
|
||||
import time
|
||||
|
||||
import xbmc
|
||||
|
||||
from .loghandler import LazyLogger
|
||||
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
|
||||
@@ -1,13 +1,12 @@
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
import xbmcgui
|
||||
|
||||
from .loghandler import LazyLogger
|
||||
from .lazylogger import LazyLogger
|
||||
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
class PictureViewer(xbmcgui.WindowXMLDialog):
|
||||
picture_url = None
|
||||
action_exitkeys_id = None
|
||||
@@ -23,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
|
||||
|
||||
@@ -4,11 +4,11 @@ import os
|
||||
import threading
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
|
||||
from .loghandler import LazyLogger
|
||||
from .play_utils import send_event_notification
|
||||
from .lazylogger import LazyLogger
|
||||
from .dialogs import PlayNextDialog
|
||||
from .utils import translate_path
|
||||
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
@@ -32,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()
|
||||
@@ -42,6 +44,13 @@ class PlayNextService(threading.Thread):
|
||||
play_next_trigger_time = int(settings.getSetting('play_next_trigger_time'))
|
||||
log.debug("New play_next_trigger_time value: {0}".format(play_next_trigger_time))
|
||||
|
||||
now_playing_file = player.getPlayingFile()
|
||||
if now_playing_file != now_playing:
|
||||
# If the playing file has changed, reset the play next values
|
||||
play_next_dialog = None
|
||||
play_next_triggered = False
|
||||
now_playing = now_playing_file
|
||||
|
||||
duration = player.getTotalTime()
|
||||
position = player.getTime()
|
||||
trigger_time = play_next_trigger_time # 300
|
||||
@@ -51,7 +60,7 @@ class PlayNextService(threading.Thread):
|
||||
play_next_triggered = True
|
||||
log.debug("play_next_triggered hit at {0} seconds from end".format(time_to_end))
|
||||
|
||||
play_data = get_playing_data(self.monitor.played_information)
|
||||
play_data = get_playing_data()
|
||||
log.debug("play_next_triggered play_data : {0}".format(play_data))
|
||||
|
||||
next_episode = play_data.get("next_episode")
|
||||
@@ -61,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)
|
||||
@@ -78,6 +87,7 @@ class PlayNextService(threading.Thread):
|
||||
play_next_dialog = None
|
||||
|
||||
is_playing = False
|
||||
now_playing = None
|
||||
|
||||
if xbmc.Monitor().waitForAbort(1):
|
||||
break
|
||||
@@ -85,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}".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
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import xbmcgui
|
||||
|
||||
from .loghandler import LazyLogger
|
||||
from .translation import string_load
|
||||
|
||||
log = LazyLogger(__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
|
||||
@@ -1,57 +0,0 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
|
||||
from .loghandler import LazyLogger
|
||||
|
||||
log = LazyLogger(__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}".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()
|
||||
@@ -3,24 +3,17 @@ from __future__ import division, absolute_import, print_function, unicode_litera
|
||||
|
||||
import socket
|
||||
import json
|
||||
from urlparse import urlparse
|
||||
import requests
|
||||
import ssl
|
||||
import time
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
|
||||
import xbmcaddon
|
||||
import xbmcgui
|
||||
import xbmc
|
||||
from kodi_six.utils import py2_decode
|
||||
|
||||
from .kodi_utils import HomeWindow
|
||||
from .downloadutils import DownloadUtils, save_user_details, load_user_details
|
||||
from .loghandler import LazyLogger
|
||||
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, get_saved_users
|
||||
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
@@ -32,26 +25,15 @@ 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
|
||||
|
||||
head = du.get_auth_header(True)
|
||||
head["User-Agent"] = "JellyCon-" + ClientInformation().get_version()
|
||||
|
||||
request_details = {
|
||||
"stream": True,
|
||||
"headers": head
|
||||
}
|
||||
|
||||
if not verify_cert:
|
||||
request_details["verify"] = False
|
||||
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)
|
||||
@@ -59,8 +41,7 @@ def check_connection_speed():
|
||||
start_time = time.time()
|
||||
|
||||
log.debug("Starting Connection Speed Test")
|
||||
|
||||
response = requests.get(url, **request_details)
|
||||
response = api.speedtest(test_data_size)
|
||||
|
||||
last_percentage_done = 0
|
||||
total_data_read = 0
|
||||
@@ -96,63 +77,33 @@ def check_connection_speed():
|
||||
return speed
|
||||
|
||||
|
||||
def check_safe_delete_available():
|
||||
log.debug("check_safe_delete_available")
|
||||
|
||||
du = DownloadUtils()
|
||||
result = du.download_url("{server}/Plugins")
|
||||
if result:
|
||||
log.debug("Server Plugin List: {0}".format(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}".format(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}".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))
|
||||
@@ -171,18 +122,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()
|
||||
@@ -192,7 +142,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"))
|
||||
@@ -201,234 +151,278 @@ 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
|
||||
|
||||
public_lookup_url = "%s/System/Info/Public?format=json" % (server_url)
|
||||
|
||||
log.debug("Testing_Url: {0}".format(public_lookup_url))
|
||||
progress = xbmcgui.DialogProgress()
|
||||
progress.create(__addon_name__ + " : " + string_load(30376))
|
||||
progress.update(0, string_load(30377))
|
||||
result = du.download_url(public_lookup_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()
|
||||
|
||||
if result:
|
||||
xbmcgui.Dialog().ok(__addon_name__ + " : " + string_load(30167),
|
||||
server_url)
|
||||
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}".format(server_url))
|
||||
settings.setSetting("server_address", server_url)
|
||||
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()
|
||||
home_window = HomeWindow()
|
||||
home_window.set_property('user_name', current_username)
|
||||
|
||||
# if asked or we have no current user then show user selection screen
|
||||
if something_changed or change_user or len(current_username) == 0:
|
||||
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")
|
||||
result = du.download_url(server_url + "/Users/Public?format=json", authenticate=False)
|
||||
|
||||
log.debug("jsonData: {0}".format(py2_decode(result)))
|
||||
|
||||
selected_id = -1
|
||||
users = []
|
||||
for user in result:
|
||||
config = user.get("Configuration")
|
||||
if config is not None:
|
||||
if config.get("IsHidden", False) is False:
|
||||
name = user.get("Name")
|
||||
admin = user.get("Policy", {}).get("IsAdministrator", False)
|
||||
|
||||
time_ago = ""
|
||||
last_active = user.get("LastActivityDate")
|
||||
if last_active:
|
||||
last_active_date = datetime_from_string(last_active)
|
||||
log.debug("LastActivityDate: {0}".format(last_active_date))
|
||||
ago = datetime.now() - last_active_date
|
||||
log.debug("LastActivityDate: {0}".format(ago))
|
||||
days = divmod(ago.seconds, 86400)
|
||||
hours = divmod(days[1], 3600)
|
||||
minutes = divmod(hours[1], 60)
|
||||
log.debug("LastActivityDate: {0} {1} {2}".format(days[0], hours[0], minutes[0]))
|
||||
if days[0]:
|
||||
time_ago += " %sd" % days[0]
|
||||
if hours[0]:
|
||||
time_ago += " %sh" % hours[0]
|
||||
if minutes[0]:
|
||||
time_ago += " %sm" % minutes[0]
|
||||
time_ago = time_ago.strip()
|
||||
if not time_ago:
|
||||
time_ago = "Active: now"
|
||||
else:
|
||||
time_ago = "Active: %s ago" % time_ago
|
||||
log.debug("LastActivityDate: {0}".format(time_ago))
|
||||
|
||||
user_item = xbmcgui.ListItem(name)
|
||||
user_image = du.get_user_artwork(user, 'Primary')
|
||||
if not user_image:
|
||||
user_image = "DefaultUser.png"
|
||||
art = {"Thumb": user_image}
|
||||
user_item.setArt(art)
|
||||
user_item.setLabel2("TEST")
|
||||
|
||||
sub_line = time_ago
|
||||
|
||||
if user.get("HasPassword", False) is True:
|
||||
sub_line += ", Password"
|
||||
user_item.setProperty("secure", "true")
|
||||
|
||||
m = hashlib.md5()
|
||||
m.update(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:
|
||||
# Check if quick connect is active on the server, initiate connection
|
||||
quick = api.get('/QuickConnect/Initiate')
|
||||
code = quick.get('Code')
|
||||
secret = quick.get('Secret')
|
||||
users, user_selection = user_select(api, current_username, code)
|
||||
|
||||
if user_selection > -1:
|
||||
# The user made a selection in the dialog
|
||||
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()
|
||||
selected_user = users[user_selection]
|
||||
quick_connect = selected_user.getProperty("quickconnect") == "true"
|
||||
count = 0
|
||||
if quick_connect:
|
||||
# Try to authenticate to server with secret code 10 times
|
||||
while count < 10:
|
||||
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)
|
||||
|
||||
log.debug("Selected User Name: {0} : {1}".format(return_value, selected_user_name))
|
||||
auth = api.post('/Users/AuthenticateWithQuickConnect',
|
||||
{'secret': secret})
|
||||
|
||||
if manual:
|
||||
kb = xbmc.Keyboard()
|
||||
kb.setHeading(string_load(30005))
|
||||
if current_username:
|
||||
kb.setDefault(current_username)
|
||||
kb.doModal()
|
||||
if kb.isConfirmed():
|
||||
selected_user_name = kb.getText()
|
||||
log.debug("Manual entered username: {0}".format(selected_user_name))
|
||||
# If authentication was successful, save the username
|
||||
if auth:
|
||||
selected_user_name = auth['User'].get('Name')
|
||||
else:
|
||||
return
|
||||
# Login failed, we don't want to change anything
|
||||
something_changed = False
|
||||
log.info("There was an error logging in with quick connect")
|
||||
|
||||
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"
|
||||
else:
|
||||
selected_user_name = selected_user.getLabel()
|
||||
secured = selected_user.getProperty("secure") == "true"
|
||||
manual = selected_user.getProperty("manual") == "true"
|
||||
|
||||
# if not saving passwords but have a saved ask to clear it
|
||||
if not allow_password_saving and saved_password:
|
||||
clear_password = xbmcgui.Dialog().yesno(string_load(30368), string_load(30369))
|
||||
if clear_password:
|
||||
settings.setSetting("saved_user_password_" + hashed_username, "")
|
||||
|
||||
if saved_password:
|
||||
log.debug("Saving username and password: {0}".format(selected_user_name))
|
||||
log.debug("Using stored password for user: {0}".format(hashed_username))
|
||||
save_user_details(settings, selected_user_name, saved_password)
|
||||
|
||||
else:
|
||||
# If using a manual login, ask for username
|
||||
if manual:
|
||||
kb = xbmc.Keyboard()
|
||||
kb.setHeading(string_load(30006))
|
||||
kb.setHiddenInput(True)
|
||||
kb.setHeading(translate_string(30005))
|
||||
if current_username:
|
||||
kb.setDefault(current_username)
|
||||
kb.doModal()
|
||||
if kb.isConfirmed():
|
||||
log.debug("Saving username and password: {0}".format(selected_user_name))
|
||||
save_user_details(settings, selected_user_name, kb.getText())
|
||||
selected_user_name = kb.getText()
|
||||
log.debug("Manual entered username: {0}".format(selected_user_name))
|
||||
else:
|
||||
return
|
||||
|
||||
home_window.set_property('user_name', selected_user_name)
|
||||
settings.setSetting('username', selected_user_name)
|
||||
user_details = load_user_details()
|
||||
|
||||
if not 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 not auth:
|
||||
# Login failed, we don't want to change anything
|
||||
something_changed = False
|
||||
log.info('There was an error logging in with user {}'.format(selected_user_name))
|
||||
xbmcgui.Dialog().ok(__addon_name__, translate_string(30446))
|
||||
|
||||
# should we save the password
|
||||
if allow_password_saving:
|
||||
save_password = xbmcgui.Dialog().yesno(string_load(30363), string_load(30364))
|
||||
if save_password:
|
||||
log.debug("Saving password for fast user switching: {0}".format(hashed_username))
|
||||
settings.setSetting("saved_user_password_" + hashed_username, kb.getText())
|
||||
else:
|
||||
log.debug("Saving username with no password: {0}".format(selected_user_name))
|
||||
save_user_details(settings, selected_user_name, "")
|
||||
|
||||
if something_changed:
|
||||
home_window = HomeWindow()
|
||||
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()
|
||||
if auth:
|
||||
token = auth.get('AccessToken')
|
||||
user_id = auth.get('User').get('Id')
|
||||
else:
|
||||
token = user_details.get('token')
|
||||
user_id = user_details.get('user_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 user_select(api, current_username, code):
|
||||
'''
|
||||
Display user selection screen
|
||||
'''
|
||||
# Retrieve list of public users from server
|
||||
public = api.get('/Users/Public')
|
||||
|
||||
# Get list of saved users
|
||||
saved_users = get_saved_users()
|
||||
|
||||
# Combine public and saved users
|
||||
for user in saved_users:
|
||||
name = user.get('Name')
|
||||
# Check if saved user is in public list
|
||||
if name not in [x.get('Name', '') for x in public]:
|
||||
# If saved user is not already in list, add it
|
||||
public.append(user)
|
||||
|
||||
# Build user display
|
||||
selected_id = -1
|
||||
users = []
|
||||
# If quick connect is active, make it the first entry
|
||||
if code:
|
||||
user_item = xbmcgui.ListItem(code)
|
||||
user_image = "DefaultUser.png"
|
||||
art = {"Thumb": user_image}
|
||||
user_item.setArt(art)
|
||||
user_item.setLabel2(translate_string(30443))
|
||||
user_item.setProperty('quickconnect', "true")
|
||||
users.append(user_item)
|
||||
|
||||
for user in public:
|
||||
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=60000,
|
||||
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
|
||||
|
||||
@@ -3,11 +3,12 @@ from __future__ import division, absolute_import, print_function, unicode_litera
|
||||
import sys
|
||||
import xbmcgui
|
||||
import xbmcplugin
|
||||
import xbmcaddon
|
||||
|
||||
from .downloadutils import DownloadUtils
|
||||
from .loghandler import LazyLogger
|
||||
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 = LazyLogger(__name__)
|
||||
|
||||
@@ -16,25 +17,26 @@ 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)
|
||||
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)
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
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 .loghandler import LazyLogger
|
||||
from .lazylogger import LazyLogger
|
||||
from .utils import translate_path, kodi_version
|
||||
|
||||
log = LazyLogger(__name__)
|
||||
ver = xbmc.getInfoLabel('System.BuildVersion')[:2]
|
||||
|
||||
|
||||
def clone_default_skin():
|
||||
@@ -28,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)
|
||||
@@ -51,7 +49,7 @@ 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}".format(kodi_skin_source))
|
||||
|
||||
@@ -63,7 +61,7 @@ def clone_skin():
|
||||
for found in all_files:
|
||||
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}".format(kodi_skin_destination))
|
||||
|
||||
@@ -82,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}".format(ver))
|
||||
log.debug("Major Version: {0}".format(kodi_version()))
|
||||
|
||||
file_list = ["Home.xml",
|
||||
"Includes_Home.xml",
|
||||
@@ -105,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)
|
||||
|
||||
@@ -135,7 +130,6 @@ def clone_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)
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import division, absolute_import, print_function, unicode_litera
|
||||
import sys
|
||||
import functools
|
||||
import time
|
||||
from .loghandler import LazyLogger
|
||||
from .lazylogger import LazyLogger
|
||||
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import urllib
|
||||
import encodings
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
|
||||
from .loghandler import LazyLogger
|
||||
from .datamanager import DataManager
|
||||
|
||||
from .translation import string_load
|
||||
|
||||
log = LazyLogger(__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}'.format(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}'.format(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}'.format(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))
|
||||
@@ -1,15 +0,0 @@
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
import xbmcaddon
|
||||
from .loghandler import LazyLogger
|
||||
|
||||
log = LazyLogger(__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)
|
||||
@@ -4,182 +4,50 @@ from __future__ import division, absolute_import, print_function, unicode_litera
|
||||
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 urllib import urlencode
|
||||
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 .loghandler import LazyLogger
|
||||
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 = 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"
|
||||
url_params = urlencode(params)
|
||||
# Filthy hack until I get around to reworking the network flow
|
||||
# It relies on {thing} strings in downloadutils.py
|
||||
url_params = url_params.replace('%7B', '{').replace('%7D', '}')
|
||||
return base_url + "?" + url_params
|
||||
|
||||
|
||||
###########################################################################
|
||||
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}".format(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"
|
||||
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}".format(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}".format(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}".format(playurl, listitem_props))
|
||||
return playurl, listitem_props
|
||||
return '{}?{}'.format(path, url_params)
|
||||
|
||||
|
||||
def get_checksum(item):
|
||||
@@ -197,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.get("ImageTags", {})
|
||||
if image_tags and image_tags.get("Primary"):
|
||||
# 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}".format(command))
|
||||
xbmc.executebuiltin(command)
|
||||
def send_event_notification(method, data=None, hexlify=False):
|
||||
'''
|
||||
Send events through Kodi's notification system
|
||||
'''
|
||||
data = data or {}
|
||||
|
||||
if hexlify:
|
||||
# Used exclusively for the upnext plugin
|
||||
data = ensure_text(binascii.hexlify(ensure_binary(json.dumps(data))))
|
||||
sender = 'plugin.video.jellycon'
|
||||
data = '"[%s]"' % json.dumps(data).replace('"', '\\"')
|
||||
|
||||
xbmc.executebuiltin('NotifyAll(%s, %s, %s)' % (sender, method, data))
|
||||
|
||||
|
||||
def datetime_from_string(time_string):
|
||||
|
||||
# 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}".format(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):
|
||||
@@ -332,3 +129,293 @@ 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('user_name')
|
||||
client_id = window.get_property("client_id")
|
||||
hashed_name = hashlib.md5(username.encode()).hexdigest()
|
||||
|
||||
if client_id and username:
|
||||
return '{}-{}'.format(client_id, hashed_name)
|
||||
elif client_id and not username:
|
||||
# Quick Connect, needs to be unique so sessions don't overwrite
|
||||
rand_id = uuid4().hex
|
||||
return '{}-{}'.format(client_id, rand_id)
|
||||
|
||||
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 doesn't exist yet
|
||||
if not user_data:
|
||||
return {}
|
||||
|
||||
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_saved_users():
|
||||
settings = xbmcaddon.Addon()
|
||||
save_user_to_settings = settings.getSetting('save_user_to_settings') == 'true'
|
||||
addon_data = translate_path(xbmcaddon.Addon().getAddonInfo('profile'))
|
||||
if not save_user_to_settings:
|
||||
return []
|
||||
|
||||
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 []
|
||||
|
||||
users = []
|
||||
for user,values in auth_data.items():
|
||||
users.append(
|
||||
{
|
||||
'Name': user,
|
||||
'Id': values.get('user_id'),
|
||||
# We need something here for the listitem function
|
||||
'Configuration': {'Dummy': True}
|
||||
}
|
||||
)
|
||||
|
||||
return users
|
||||
|
||||
|
||||
|
||||
|
||||
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):
|
||||
addon_settings = xbmcaddon.Addon()
|
||||
verify_cert = addon_settings.getSetting('verify_cert') == 'true'
|
||||
|
||||
# Download the subtitle file
|
||||
r = requests.get(url, verify=verify_cert)
|
||||
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
|
||||
|
||||
@@ -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()
|
||||
@@ -2,19 +2,22 @@
|
||||
|
||||
#################################################################################################
|
||||
|
||||
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 .loghandler import LazyLogger
|
||||
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 = LazyLogger(__name__)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']
|
||||
@@ -226,11 +228,9 @@ class WebSocketClient(threading.Thread):
|
||||
if command in builtin:
|
||||
xbmc.executebuiltin(builtin[command])
|
||||
|
||||
def on_close(self, ws):
|
||||
log.debug("Closed")
|
||||
|
||||
def on_open(self, ws):
|
||||
log.debug("Connected")
|
||||
self.retry_count = 0
|
||||
self.post_capabilities()
|
||||
|
||||
def on_error(self, ws, error):
|
||||
@@ -238,34 +238,35 @@ class WebSocketClient(threading.Thread):
|
||||
|
||||
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/socket?api_key=%s&deviceId=%s" % (server, token, self.device_id)
|
||||
log.debug("websocket url: {0}".format(websocket_url))
|
||||
|
||||
self._client = websocket.WebSocketApp(websocket_url,
|
||||
on_open=self.on_open,
|
||||
on_message=self.on_message,
|
||||
on_error=self.on_error,
|
||||
on_close=self.on_close)
|
||||
self._client = websocket.WebSocketApp(
|
||||
websocket_url,
|
||||
on_open=lambda ws: self.on_open(ws),
|
||||
on_message=lambda ws, message: self.on_message(ws, message),
|
||||
on_error=lambda ws, error: self.on_error(ws, error))
|
||||
|
||||
log.debug("Starting WebSocketClient")
|
||||
|
||||
while not self.monitor.abortRequested():
|
||||
|
||||
time.sleep(self.retry_count * 5)
|
||||
self._client.run_forever(ping_interval=10)
|
||||
|
||||
if self._stop_websocket:
|
||||
@@ -275,6 +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()
|
||||
|
||||
@@ -3,23 +3,18 @@ from __future__ import division, absolute_import, print_function, unicode_litera
|
||||
import xbmcaddon
|
||||
import xbmcplugin
|
||||
import xbmcgui
|
||||
import xbmc
|
||||
import hashlib
|
||||
import random
|
||||
import time
|
||||
|
||||
from .downloadutils import DownloadUtils
|
||||
from .utils import get_jellyfin_url
|
||||
from .datamanager import DataManager
|
||||
from .loghandler import LazyLogger
|
||||
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 = LazyLogger(__name__)
|
||||
downloadUtils = DownloadUtils()
|
||||
dataManager = DataManager()
|
||||
kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2])
|
||||
|
||||
background_items = []
|
||||
background_current_item = 0
|
||||
@@ -30,20 +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 = api.get(url)
|
||||
|
||||
randon_movies_list = []
|
||||
if results is not None:
|
||||
@@ -55,7 +52,7 @@ def set_random_movies():
|
||||
movies_list_string = ",".join(randon_movies_list)
|
||||
home_window = HomeWindow()
|
||||
m = hashlib.md5()
|
||||
m.update(movies_list_string)
|
||||
m.update(movies_list_string.encode())
|
||||
new_widget_hash = m.hexdigest()
|
||||
|
||||
log.debug("set_random_movies : {0}".format(movies_list_string))
|
||||
@@ -70,6 +67,10 @@ def set_background_image(force=False):
|
||||
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
|
||||
@@ -77,7 +78,7 @@ def set_background_image(force=False):
|
||||
|
||||
if len(background_items) == 0:
|
||||
log.debug("set_background_image: Need to load more backgrounds {0} - {1}".format(
|
||||
len(background_items), background_current_item))
|
||||
len(background_items), background_current_item))
|
||||
|
||||
url_params = {}
|
||||
url_params["Recursive"] = True
|
||||
@@ -86,17 +87,17 @@ def set_background_image(force=False):
|
||||
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 = 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 = {}
|
||||
@@ -104,12 +105,14 @@ def set_background_image(force=False):
|
||||
item_background["name"] = label
|
||||
background_items.append(item_background)
|
||||
|
||||
log.debug("set_background_image: Loaded {0} more backgrounds".format(len(background_items)))
|
||||
log.debug("set_background_image: Loaded {0} more backgrounds".format(
|
||||
len(background_items)))
|
||||
|
||||
if len(background_items) > 0:
|
||||
bg_image = background_items[background_current_item].get("image")
|
||||
label = background_items[background_current_item].get("name")
|
||||
log.debug("set_background_image: {0} - {1} - {2}".format(background_current_item, label, bg_image))
|
||||
log.debug(
|
||||
"set_background_image: {0} - {1} - {2}".format(background_current_item, label, bg_image))
|
||||
|
||||
background_current_item += 1
|
||||
if background_current_item >= len(background_items):
|
||||
@@ -126,7 +129,8 @@ def check_for_new_content():
|
||||
|
||||
home_window = HomeWindow()
|
||||
settings = xbmcaddon.Addon()
|
||||
simple_new_content_check = settings.getSetting("simple_new_content_check") == "true"
|
||||
simple_new_content_check = settings.getSetting(
|
||||
"simple_new_content_check") == "true"
|
||||
|
||||
if simple_new_content_check:
|
||||
log.debug("Using simple new content check")
|
||||
@@ -134,6 +138,7 @@ def check_for_new_content():
|
||||
home_window.set_property("jellycon_widget_reload", 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,9 +149,9 @@ def check_for_new_content():
|
||||
url_params["IncludeItemTypes"] = "Movie,Episode"
|
||||
url_params["ImageTypeLimit"] = 0
|
||||
|
||||
added_url = get_jellyfin_url('{server}/Users/{userid}/Items', url_params)
|
||||
added_url = get_jellyfin_url('/Users/{}/Items'.format(user_id), url_params)
|
||||
|
||||
result = downloadUtils.download_url(added_url, suppress=True)
|
||||
result = api.get(added_url)
|
||||
log.debug("LATEST_ADDED_ITEM: {0}".format(result))
|
||||
|
||||
last_added_date = ""
|
||||
@@ -166,9 +171,9 @@ def check_for_new_content():
|
||||
url_params["IncludeItemTypes"] = "Movie,Episode"
|
||||
url_params["ImageTypeLimit"] = 0
|
||||
|
||||
played_url = get_jellyfin_url('{server}/Users/{userid}/Items', url_params)
|
||||
played_url = get_jellyfin_url('/Users/{}/Items'.format(user_id), url_params)
|
||||
|
||||
result = downloadUtils.download_url(played_url, suppress=True)
|
||||
result = api.get(played_url)
|
||||
log.debug("LATEST_PLAYED_ITEM: {0}".format(result))
|
||||
|
||||
last_played_date = ""
|
||||
@@ -176,7 +181,6 @@ def check_for_new_content():
|
||||
items = result.get("Items", [])
|
||||
if len(items) > 0:
|
||||
item = items[0]
|
||||
# last_played_date = item.get("Etag", "")
|
||||
user_data = item.get("UserData", None)
|
||||
if user_data is not None:
|
||||
last_played_date = user_data.get("LastPlayedDate", "")
|
||||
@@ -187,7 +191,7 @@ def check_for_new_content():
|
||||
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}".format(new_widget_hash))
|
||||
|
||||
@@ -199,11 +203,13 @@ def check_for_new_content():
|
||||
@timer
|
||||
def get_widget_content_cast(handle, params):
|
||||
log.debug("getWigetContentCast Called: {0}".format(params))
|
||||
server = downloadUtils.get_server()
|
||||
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)
|
||||
result = api.get(
|
||||
"/Users/{}/Items/{}".format(user_id, item_id))
|
||||
log.debug("ItemInfo: {0}".format(result))
|
||||
|
||||
if not result:
|
||||
@@ -222,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")
|
||||
@@ -235,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)
|
||||
|
||||
@@ -270,20 +268,23 @@ def get_widget_content(handle, 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}".format(widget_type))
|
||||
|
||||
url_verb = "{server}/Users/{userid}/Items"
|
||||
url_verb = "/Users/{}/Items".format(user_id)
|
||||
url_params = {}
|
||||
url_params["Limit"] = "{ItemLimit}"
|
||||
url_params["Fields"] = "{field_filters}"
|
||||
url_params["Limit"] = item_limit
|
||||
url_params["Fields"] = get_default_filters()
|
||||
url_params["ImageTypeLimit"] = 1
|
||||
url_params["IsMissing"] = False
|
||||
|
||||
@@ -297,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')
|
||||
@@ -306,25 +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["Limit"] = item_limit
|
||||
|
||||
elif widget_type == "recent_episodes":
|
||||
xbmcplugin.setContent(handle, 'episodes')
|
||||
@@ -336,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')
|
||||
@@ -345,67 +351,77 @@ 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["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}".format(set_id, items.get("BaselineItemName")))
|
||||
log.debug(
|
||||
"BaselineItemName : {0} - {1}".format(set_id, items.get("BaselineItemName")))
|
||||
items = items["Items"]
|
||||
rand = random.randint(0, len(items) - 1)
|
||||
# log.debug("random suggestions index : {0} {1}", rand, set_id)
|
||||
item = items[rand]
|
||||
if item["Type"] == "Movie" and item["Id"] not in ids and (not item["UserData"]["Played"] or not hide_watched):
|
||||
# log.debug("random suggestions adding : {0}", item["Id"])
|
||||
ids.append(item["Id"])
|
||||
# else:
|
||||
# log.debug("random suggestions not valid : {0} - {1} - {2}", item["Id"], item["Type"], item["UserData"]["Played"])
|
||||
del items[rand]
|
||||
# log.debug("items len {0}", len(items))
|
||||
if len(items) == 0:
|
||||
# log.debug("Removing Set {0}", set_id)
|
||||
del suggested_items[set_id]
|
||||
set_id += 1
|
||||
if set_id >= len(suggested_items):
|
||||
set_id = 0
|
||||
|
||||
id_list = ",".join(ids)
|
||||
log.debug("Recommended Items : {0}".format(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
|
||||
|
||||
@@ -8,13 +8,12 @@
|
||||
<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="false" 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" />
|
||||
|
||||
@@ -46,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">
|
||||
|
||||
@@ -74,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"/>
|
||||
|
||||
@@ -92,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" />
|
||||
@@ -155,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
Before Width: | Height: | Size: 312 B After Width: | Height: | Size: 137 B |
|
Before Width: | Height: | Size: 263 B After Width: | Height: | Size: 109 B |
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
50
service.py
@@ -8,16 +8,14 @@ import xbmc
|
||||
import xbmcaddon
|
||||
import xbmcgui
|
||||
|
||||
from resources.lib.downloadutils import DownloadUtils, save_user_details
|
||||
from resources.lib.loghandler import LazyLogger
|
||||
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
|
||||
@@ -31,7 +29,7 @@ if log_timing_data:
|
||||
|
||||
# clear user and token when logging in
|
||||
home_window = HomeWindow()
|
||||
home_window.clear_property("userid")
|
||||
home_window.clear_property("user_name")
|
||||
home_window.clear_property("AccessToken")
|
||||
home_window.clear_property("Params")
|
||||
|
||||
@@ -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}".format(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")
|
||||
prev_user = home_window.get_property("user_name")
|
||||
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,22 +111,23 @@ 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")
|
||||
|
||||
if not screen_saver_active:
|
||||
user_changed = False
|
||||
if prev_user_id != home_window.get_property("userid"):
|
||||
if prev_user != home_window.get_property("user_name"):
|
||||
log.debug("user_change_detected")
|
||||
prev_user_id = home_window.get_property("userid")
|
||||
prev_user = home_window.get_property("user_name")
|
||||
user_changed = True
|
||||
|
||||
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):
|
||||
@@ -189,6 +171,9 @@ while not xbmc.abortRequested:
|
||||
|
||||
image_server.stop()
|
||||
|
||||
# stop the WebSocket Client
|
||||
websocket_client.stop_client()
|
||||
|
||||
# call stop on the library update monitor
|
||||
library_change_monitor.stop()
|
||||
|
||||
@@ -200,11 +185,8 @@ 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("user_name")
|
||||
home_window.clear_property("AccessToken")
|
||||
home_window.clear_property("userimage")
|
||||
|
||||
|
||||